git @ Cat's Eye Technologies Erratic-Turtle-Graphics / 166c073
Initial import of files for Erratic Turtle Graphics. Chris Pressey 2 years ago
6 changed file(s) with 460 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 Erratic Turtle Graphics
1 =======================
2
3 ![screenshot](images/erratic-circles.png?raw=true)
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 };