Initial import of files for Latcarf distribution.
Chris Pressey
3 years ago
0 | Latcarf | |
1 | ======= | |
2 | ||
3 |  | |
4 | ||
5 | _Try it online [here](https://catseye.tc/installation/Latcarf)._ | |
6 | ||
7 | This is a gewgaw from an idea I had in (I think) 2017 or 2018, | |
8 | which was this: | |
9 | ||
10 | Most recursive fractals (such as the [Koch snowflake][]) are developed | |
11 | top-down; you start with a large shape, and then you say it is | |
12 | made up of smaller shapes similar to the first shape, and those | |
13 | smaller shapes are made up of even smaller shapes, and so forth. | |
14 | ||
15 | But what happens if you try to develop a fractal the other way around — | |
16 | bottom-up? You start with some small shapes, then you connect them | |
17 | into larger shapes, and connect those into even larger shapes... | |
18 | ||
19 | Latcarf was an attempt to implement a bottom-up fractal such as this. | |
20 | ||
21 | [Koch snowflake]: https://en.wikipedia.org/wiki/Koch_snowflake |
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 latcarf.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'), textarea=DAM.maker('textarea'), canvas=DAM.maker('canvas'); | |
7 | ||
8 | var can = canvas({ width: 800, height: 600 }); | |
9 | config.container.appendChild(can); | |
10 | ||
11 | var gewgaw = new Latcarf(); | |
12 | gewgaw.init({ | |
13 | 'canvas': can | |
14 | }); | |
15 | ||
16 | var controlPanel = div( | |
17 | div( | |
18 | button("Re-roll", { | |
19 | onclick: function() { | |
20 | gewgaw.reset(); | |
21 | } | |
22 | }) | |
23 | ) | |
24 | ); | |
25 | config.container.appendChild(controlPanel); | |
26 | } |
0 | <!DOCTYPE html> | |
1 | <head> | |
2 | <meta charset="utf-8"> | |
3 | <title>Latcarf</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>Latcarf</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/latcarf.js"></script> | |
19 | <script src="latcarf-launcher.js"></script> | |
20 | <script> | |
21 | launch({ | |
22 | container: document.getElementById('installation'), | |
23 | }); | |
24 | </script> | |
25 | </body> |
Binary diff not shown
0 | var TWO_PI = Math.PI * 2; | |
1 | ||
2 | ||
3 | Latcarf = function() { | |
4 | this.init = function(cfg) { | |
5 | this.canvas = cfg.canvas; | |
6 | this.ctx = this.canvas.getContext('2d'); | |
7 | this.start(); | |
8 | }; | |
9 | ||
10 | this.start = function() { | |
11 | this.objects = []; | |
12 | ||
13 | // add many small objects | |
14 | for (var i = 0; i < 100; i++) { | |
15 | var nobj = { | |
16 | x: Math.floor(Math.random() * this.canvas.width), | |
17 | y: Math.floor(Math.random() * this.canvas.height), | |
18 | r: 2, | |
19 | connection: null | |
20 | }; | |
21 | this.objects.push(nobj); | |
22 | } | |
23 | ||
24 | var $this = this; | |
25 | this.interval = setInterval(function() { | |
26 | $this.update(); | |
27 | $this.draw(); | |
28 | }, 25); | |
29 | }; | |
30 | ||
31 | this.stop = function() { | |
32 | if (this.interval !== undefined) { | |
33 | clearInterval(this.interval); | |
34 | this.interval = undefined; | |
35 | } | |
36 | }; | |
37 | ||
38 | this.reset = function() { | |
39 | this.stop(); | |
40 | this.start(); | |
41 | }; | |
42 | ||
43 | this.draw = function() { | |
44 | this.ctx.fillStyle = "white"; | |
45 | this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
46 | for (var i = 0; i < this.objects.length; i++) { | |
47 | var o = this.objects[i]; | |
48 | ||
49 | this.ctx.beginPath(); | |
50 | this.ctx.lineWidth = 2; | |
51 | this.ctx.strokeStyle = "black"; | |
52 | this.ctx.fillStyle = "black"; | |
53 | this.ctx.arc(o.x, o.y, o.r * 2, 0, TWO_PI, false); | |
54 | this.ctx.fill(); | |
55 | ||
56 | if (o.connection !== null) { | |
57 | this.ctx.beginPath(); | |
58 | this.ctx.moveTo(o.x, o.y); | |
59 | this.ctx.lineTo(o.connection.x, o.connection.y); | |
60 | this.ctx.stroke(); | |
61 | } | |
62 | } | |
63 | }; | |
64 | ||
65 | this.update = function() { | |
66 | var selected = undefined; | |
67 | ||
68 | // pick an unconnected object | |
69 | for (var i = 0; i < this.objects.length; i++) { | |
70 | if (this.objects[i].connection === null) { | |
71 | selected = this.objects[i]; | |
72 | break; | |
73 | } | |
74 | } | |
75 | ||
76 | if (selected === undefined) { | |
77 | this.stop(); | |
78 | return; | |
79 | } | |
80 | ||
81 | // pick the closest unconnected object of the same size | |
82 | var target = undefined; | |
83 | var distance = 1000000; | |
84 | for (var i = 0; i < this.objects.length; i++) { | |
85 | if (this.objects[i] === selected || this.objects[i].connection !== null) continue; | |
86 | var dx = this.objects[i].x - selected.x; | |
87 | // FUN BUG // var dy = this.objects[i].x - selected.x; | |
88 | var dy = this.objects[i].y - selected.y; | |
89 | var d = Math.sqrt(dx * dx + dy * dy); | |
90 | if (d < distance) { | |
91 | target = this.objects[i]; | |
92 | distance = d; | |
93 | } | |
94 | } | |
95 | ||
96 | if (target === undefined) { | |
97 | this.stop(); | |
98 | return; | |
99 | } | |
100 | ||
101 | // create a larger object at their midpoint | |
102 | var nobj = { | |
103 | x: (selected.x + target.x) / 2, | |
104 | y: (selected.y + target.y) / 2, | |
105 | r: (selected.r + 1), | |
106 | connection: null | |
107 | }; | |
108 | this.objects.push(nobj); | |
109 | ||
110 | // connect the old to the new | |
111 | selected.connection = nobj; | |
112 | target.connection = nobj; | |
113 | }; | |
114 | ||
115 | }; |