Initial import of files for Erratic Turtle Graphics.
Chris Pressey
2 years ago
0 | Erratic Turtle Graphics | |
1 | ======================= | |
2 | ||
3 |  | |
4 | ||
5 | A gewgaw that I prototyped in 2018 sometime I think. I don't | |
6 | remember when I had the original idea, but I think it was not | |
7 | long before that. | |
8 | ||
9 | And that idea is: turtle graphics, except there's a small | |
10 | margin of error. You might ask for "Turn right 90 degrees" | |
11 | but you might get only "Turn right 89.91 degrees". | |
12 | ||
13 | If you use a faint pen, and repeat the drawing instructions | |
14 | many times over, you get a nice pencilly noisy effect. |
0 | This is free and unencumbered software released into the public domain. | |
1 | ||
2 | Anyone is free to copy, modify, publish, use, compile, sell, or | |
3 | distribute this software, either in source code form or as a compiled | |
4 | binary, for any purpose, commercial or non-commercial, and by any | |
5 | means. | |
6 | ||
7 | In jurisdictions that recognize copyright laws, the author or authors | |
8 | of this software dedicate any and all copyright interest in the | |
9 | software to the public domain. We make this dedication for the benefit | |
10 | of the public at large and to the detriment of our heirs and | |
11 | successors. We intend this dedication to be an overt act of | |
12 | relinquishment in perpetuity of all present and future rights to this | |
13 | software under copyright law. | |
14 | ||
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
18 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR | |
19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, | |
20 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
21 | OTHER DEALINGS IN THE SOFTWARE. | |
22 | ||
23 | For more information, please refer to <http://unlicense.org/> |
0 | /* dam-plus-widgets-web.js version 0.1. This file is in the public domain. */ | |
1 | ||
2 | /* This file is recommended if you just want to use DAM and its standard | |
3 | widget library on an HTML page without bothering with JS build stuff. | |
4 | It consists of dam.js followed by dam-widgets.js, both with only small | |
5 | hand modifications to make them load as-is in ES5. */ | |
6 | ||
7 | var DAM = (function() { | |
8 | var DAM = {}; | |
9 | DAM.makeElem = function(tag, args) { | |
10 | args = args || []; | |
11 | var elem = document.createElement(tag); | |
12 | for (var i = 0; i < args.length; i++) { | |
13 | var arg = args[i]; | |
14 | if (arg instanceof Element) { | |
15 | elem.appendChild(arg); | |
16 | } else if (typeof arg === 'string' || arg instanceof String) { | |
17 | elem.appendChild(document.createTextNode(arg)); | |
18 | } else if (typeof arg === 'object' && arg !== null) { | |
19 | Object.keys(arg).forEach(function(key) { | |
20 | if (key.substring(0, 2) === 'on') { | |
21 | elem.addEventListener(key.substring(2), arg[key]); | |
22 | } else if (arg[key] === null) { | |
23 | elem.removeAttribute(key); | |
24 | } else { | |
25 | elem.setAttribute(key, arg[key]); | |
26 | } | |
27 | }); | |
28 | } else { | |
29 | console.log(arg); | |
30 | } | |
31 | } | |
32 | return elem; | |
33 | }; | |
34 | DAM.maker = function(tag) { | |
35 | return function() { | |
36 | return DAM.makeElem(tag, arguments); | |
37 | }; | |
38 | }; | |
39 | return DAM; | |
40 | })(); | |
41 | ||
42 | (function(DAM) { // ENTER-SCOPE | |
43 | ||
44 | /* | |
45 | * A labelled checkbox, where the checkbox appears to the left of the label. | |
46 | * Arguments after the first (config) argument will be applied to the label element. | |
47 | */ | |
48 | DAM.makeCheckbox = function(config) { | |
49 | if (typeof DAM.makeCheckboxCounter === 'undefined') DAM.makeCheckboxCounter = 0; | |
50 | var checkboxId = 'cfzzzb_' + (DAM.makeCheckboxCounter++); | |
51 | ||
52 | var onchange = config.onchange || function(b) {}; | |
53 | ||
54 | // config label: make copy of arguments, replace first with a bespoke config | |
55 | var args = new Array(arguments.length); | |
56 | for(var i = 0; i < args.length; ++i) { | |
57 | args[i] = arguments[i]; | |
58 | } | |
59 | args[0] = { 'for': checkboxId, 'class': "dam-widget dam-checkbox" } | |
60 | ||
61 | return DAM.makeElem('span', [ | |
62 | DAM.makeElem('input', [ | |
63 | { | |
64 | type: 'checkbox', | |
65 | id: checkboxId, | |
66 | onchange: function(e) { | |
67 | onchange(e.target.checked); | |
68 | } | |
69 | }, | |
70 | config.checkboxAttrs || {} | |
71 | ]), | |
72 | DAM.makeElem('label', args) | |
73 | ]); | |
74 | }; | |
75 | ||
76 | /* | |
77 | * A collapsible panel. | |
78 | * Arguments after the first (config) argument will be applied to the inner container div element. | |
79 | */ | |
80 | DAM.makePanel = function(config) { | |
81 | var isOpen = !!(config.isOpen); | |
82 | var title = config.title || ""; | |
83 | ||
84 | function getLabel() { | |
85 | return (isOpen ? "∇" : "⊳") + " " + title; | |
86 | } | |
87 | ||
88 | // config inner container | |
89 | var args = new Array(arguments.length); | |
90 | for(var i = 0; i < args.length; ++i) { | |
91 | args[i] = arguments[i]; | |
92 | } | |
93 | args[0] = {} | |
94 | ||
95 | var innerContainer = DAM.makeElem('div', args); | |
96 | innerContainer.style.display = isOpen ? "block" : "none"; | |
97 | ||
98 | var button = DAM.makeElem('button', [ | |
99 | getLabel(), | |
100 | { | |
101 | onclick: function(e) { | |
102 | isOpen = !isOpen; | |
103 | button.textContent = getLabel(); | |
104 | innerContainer.style.display = isOpen ? "block" : "none"; | |
105 | } | |
106 | } | |
107 | ]); | |
108 | ||
109 | return DAM.makeElem("div", [{ 'class': "dam-widget dam-panel" }, button, innerContainer]); | |
110 | }; | |
111 | ||
112 | /* | |
113 | * A select dropdown. | |
114 | */ | |
115 | DAM.makeSelect = function(config) { | |
116 | var title = config.title || ""; | |
117 | var options = config.options || []; | |
118 | var onchange = config.onchange || function(v) {}; | |
119 | ||
120 | var select = DAM.makeElem('select'); | |
121 | for (var i = 0; i < options.length; i++) { | |
122 | var op = DAM.makeElem('option'); | |
123 | op.value = options[i].value; | |
124 | op.text = options[i].text; | |
125 | op.selected = !!(options[i].selected); | |
126 | select.options.add(op); | |
127 | } | |
128 | select.addEventListener('change', function(e) { | |
129 | onchange(options[select.selectedIndex]); | |
130 | }); | |
131 | return DAM.makeElem('label', [{ 'class': "dam-widget dam-select" }, title, select]); | |
132 | }; | |
133 | ||
134 | /* | |
135 | * A range control. | |
136 | */ | |
137 | DAM.makeRange = function(config) { | |
138 | var title = config.title || ""; | |
139 | var min_ = config['min']; | |
140 | var max_ = config['max']; | |
141 | var value = config.value || min_; | |
142 | var onchange = config.onchange || function(v) {}; | |
143 | var textInputSize = config.textInputSize || 5; | |
144 | ||
145 | var textInput; var slider; | |
146 | ||
147 | slider = DAM.makeElem('input', [ | |
148 | { | |
149 | type: "range", min: min_, max: max_, value: value, | |
150 | onchange: function(e) { | |
151 | var v = parseInt(slider.value, 10); | |
152 | if (!isNaN(v)) { | |
153 | textInput.value = "" + v; | |
154 | onchange(v); | |
155 | } | |
156 | } | |
157 | } | |
158 | ]); | |
159 | ||
160 | textInput = DAM.makeElem('input', [ | |
161 | { | |
162 | size: "" + textInputSize, | |
163 | value: "" + value, | |
164 | onchange: function(e) { | |
165 | var v = parseInt(textInput.value, 10); | |
166 | if (!isNaN(v) && v >= min_ && v <= max_) { | |
167 | slider.value = "" + v; | |
168 | onchange(v); | |
169 | } | |
170 | } | |
171 | } | |
172 | ]); | |
173 | ||
174 | var incButton = DAM.makeElem('button', ['+', | |
175 | { | |
176 | onclick: function(e) { | |
177 | var v = parseInt(textInput.value, 10); | |
178 | if ((!isNaN(v)) && v < max_) { | |
179 | v++; | |
180 | textInput.value = "" + v; | |
181 | slider.value = "" + v; | |
182 | onchange(v); | |
183 | } | |
184 | } | |
185 | } | |
186 | ]); | |
187 | ||
188 | var decButton = DAM.makeElem('button', ['-', | |
189 | { | |
190 | onclick: function(e) { | |
191 | var v = parseInt(textInput.value, 10); | |
192 | if ((!isNaN(v)) && v > min_) { | |
193 | v--; | |
194 | textInput.value = "" + v; | |
195 | slider.value = "" + v; | |
196 | onchange(v); | |
197 | } | |
198 | } | |
199 | } | |
200 | ]); | |
201 | ||
202 | return DAM.makeElem('span', [{ 'class': "dam-widget dam-range" }, DAM.makeElem('label', [title, slider]), textInput, decButton, incButton]); | |
203 | }; | |
204 | ||
205 | })(DAM); // EXIT-SCOPE | |
206 | ||
207 | if (typeof module !== 'undefined') module.exports = DAM; |
0 | /* | |
1 | * dam-plus-widgets-web.js and erratic-turtle.js should be loaded before this. | |
2 | * After this is loaded, call launch() to start the gewgaw. | |
3 | */ | |
4 | ||
5 | function launch(config) { | |
6 | var div=DAM.maker('div'), button=DAM.maker('button'), canvas=DAM.maker('canvas'); | |
7 | ||
8 | var can = canvas({ width: 1000, height: 400 }); | |
9 | config.container.appendChild(can); | |
10 | ||
11 | var gewgaw = (new ErraticTurtle()).init({ canvas: can }); | |
12 | gewgaw.reset(); | |
13 | var method = 'drawLines'; | |
14 | gewgaw[method](); | |
15 | ||
16 | var controlPanel = div( | |
17 | div( | |
18 | DAM.makeSelect({ | |
19 | title: "Form", | |
20 | options: [ | |
21 | { | |
22 | text: 'Lines', | |
23 | value: 'drawLines', | |
24 | }, | |
25 | { | |
26 | text: 'Boxes', | |
27 | value: 'drawBoxes', | |
28 | }, | |
29 | { | |
30 | text: 'Circles', | |
31 | value: 'drawCircles', | |
32 | }, | |
33 | { | |
34 | text: 'Circle Chain', | |
35 | value: 'drawCircleChain', | |
36 | } | |
37 | ], | |
38 | onchange: function(option) { | |
39 | method = option.value; | |
40 | gewgaw.reset(); | |
41 | gewgaw[method](); | |
42 | } | |
43 | }) | |
44 | ), | |
45 | div( | |
46 | button("Re-roll", { onclick: function() { gewgaw.reset(); gewgaw[method](); }}) | |
47 | ) | |
48 | ); | |
49 | config.container.appendChild(controlPanel); | |
50 | } |
0 | <!DOCTYPE html> | |
1 | <head> | |
2 | <meta charset="utf-8"> | |
3 | <title>Erratic Turtle Graphics</title> | |
4 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
5 | <style> | |
6 | canvas { border: 1px solid blue; } | |
7 | </style> | |
8 | </head> | |
9 | <body> | |
10 | ||
11 | <h1>Erratic Turtle Graphics</h1> | |
12 | ||
13 | <article> | |
14 | <div id="installation"></div> | |
15 | </article> | |
16 | ||
17 | <script src="dam-plus-widgets-web.js"></script> | |
18 | <script src="../src/erratic-turtle.js"></script> | |
19 | <script src="erratic-turtle-graphics-launcher.js"></script> | |
20 | <script> | |
21 | launch({ | |
22 | container: document.getElementById('installation'), | |
23 | }); | |
24 | </script> | |
25 | </body> |
0 | var TWO_PI = Math.PI * 2; | |
1 | var DEG = TWO_PI / 360.0; | |
2 | ||
3 | var ErraticTurtle = function() { | |
4 | this.init = function(cfg) { | |
5 | this.canvas = cfg.canvas; | |
6 | this.ctx = this.canvas.getContext('2d'); | |
7 | return this; | |
8 | }; | |
9 | ||
10 | this.reset = function() { | |
11 | this.ctx.fillStyle = '#ffffff'; | |
12 | this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
13 | ||
14 | this.x = this.canvas.width / 2; | |
15 | this.y = this.canvas.height / 2; | |
16 | this.setTheta(0.0); | |
17 | this.ctx.strokeStyle = 'rgba(0,0,0,0.05)'; | |
18 | this.ctx.lineWidth = 1; | |
19 | ||
20 | this.rotateError = 0.0; | |
21 | this.moveError = 0.0; | |
22 | }; | |
23 | ||
24 | /* theta is in radians */ | |
25 | this.setTheta = function(theta) { | |
26 | this.theta = theta; | |
27 | this.dx = Math.cos(theta); | |
28 | this.dy = Math.sin(theta); | |
29 | }; | |
30 | ||
31 | /* dtheta is in radians */ | |
32 | this.rotateBy = function(dtheta) { | |
33 | var error = (Math.random() - 0.5) * this.rotateError; | |
34 | this.setTheta(this.theta + dtheta + error); | |
35 | }; | |
36 | ||
37 | this.moveBy = function(units) { | |
38 | var error = (Math.random() - 0.5) * this.moveError; | |
39 | ||
40 | var nx = this.x + this.dx * (units + error); | |
41 | var ny = this.y + this.dy * (units + error); | |
42 | ||
43 | var ctx = this.ctx; | |
44 | ctx.beginPath(); | |
45 | ctx.moveTo(this.x, this.y); | |
46 | ctx.lineTo(nx, ny); | |
47 | ctx.stroke(); | |
48 | ||
49 | this.x = nx; | |
50 | this.y = ny; | |
51 | }; | |
52 | ||
53 | this.drawItems = function(y, drawItem) { | |
54 | this.x = this.canvas.width * (1/8); | |
55 | this.y = y; | |
56 | this.setTheta(-90.0 * DEG); | |
57 | ||
58 | for (var i = 0; i < 7; i++) { | |
59 | drawItem(i); | |
60 | this.x += this.canvas.width * (1/8); | |
61 | this.y = y; | |
62 | } | |
63 | }; | |
64 | ||
65 | this.drawLine = function(size) { | |
66 | for (var i = 0; i < 50; i++) { | |
67 | this.moveBy(size); | |
68 | this.rotateBy(-180.0 * DEG); | |
69 | } | |
70 | }; | |
71 | ||
72 | this.drawBox = function(size) { | |
73 | for (var i = 0; i < 400; i++) { | |
74 | this.moveBy(size); | |
75 | this.rotateBy(-90.0 * DEG); | |
76 | } | |
77 | }; | |
78 | ||
79 | this.drawCircle = function(size, reps) { | |
80 | for (var i = 0; i < 90 * reps; i++) { | |
81 | this.moveBy(size); | |
82 | this.rotateBy(-4.0 * DEG); | |
83 | } | |
84 | }; | |
85 | ||
86 | this.drawLines = function() { | |
87 | var $this = this; | |
88 | this.drawItems(this.canvas.height * (1/2), function(n) { | |
89 | $this.rotateError = 0.01 * (n/7); | |
90 | $this.moveError = 2.0 * (n/7); | |
91 | $this.drawLine(150); | |
92 | }); | |
93 | }; | |
94 | ||
95 | this.drawBoxes = function() { | |
96 | var $this = this; | |
97 | this.drawItems(this.canvas.height * (2/3), function(n) { | |
98 | $this.rotateError = 0.01 * (n/7); | |
99 | $this.moveError = 2.0 * (n/7); | |
100 | $this.drawBox(50); | |
101 | }); | |
102 | }; | |
103 | ||
104 | this.drawCircles = function() { | |
105 | var $this = this; | |
106 | this.drawItems(this.canvas.height * (7/8), function(n) { | |
107 | $this.rotateError = 0.025 * (n/7); | |
108 | $this.moveError = 0.333 * (n/7); | |
109 | $this.drawCircle(1.0, 50); | |
110 | }); | |
111 | }; | |
112 | ||
113 | this.drawCircleChain = function(size) { | |
114 | this.x = this.canvas.width; | |
115 | this.y = this.canvas.height * (1/2); | |
116 | this.setTheta(-90.0 * DEG); | |
117 | ||
118 | var SEGS = 7; | |
119 | ||
120 | for (var n = 0; n <= SEGS; n++) { | |
121 | this.rotateError = 0.025 * (n/SEGS); | |
122 | this.moveError = 1.5 * (n/SEGS); | |
123 | ||
124 | this.drawCircle(2.0, 20.5); | |
125 | this.rotateBy(-180.0 * DEG); | |
126 | } | |
127 | for (var n = SEGS; n >= 0; n--) { | |
128 | this.rotateError = 0.025 * (n/SEGS); | |
129 | this.moveError = 1.5 * (n/SEGS); | |
130 | ||
131 | this.drawCircle(2.0, 20.5); | |
132 | this.rotateBy(-180.0 * DEG); | |
133 | } | |
134 | }; | |
135 | }; |