git @ Cat's Eye Technologies Latcarf / 742f73e
Initial import of files for Latcarf distribution. Chris Pressey 3 years ago
7 changed file(s) with 423 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 Latcarf
1 =======
2
3 ![screenshot](images/latcarf1.png?raw=true)
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 };