├── .github
└── FUNDING.yml
├── LICENSE
├── README.md
├── index.html
├── libraries
├── habitat-colour-constants-embed.js
├── habitat-embed.js
└── show.js
├── notes.txt
└── source
├── address.js
├── colour.js
├── corners.js
├── draw.js
├── global.js
├── hand.js
├── keyboard.js
├── lerp.js
├── list.js
├── main.js
├── number.js
├── part.js
├── pick.js
├── position.js
├── preset.js
├── route.js
├── screen.js
├── vector.js
├── world.js
└── zoom.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: TodePond
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Luke Wilson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ScreenPond
2 |
3 | ScreenPond lets you make fractals and the 'droste effect'.
4 | I made it for my video: 📺 **[Screens in Screens in Screens](https://youtu.be/Q4OIcwt8vcE)**
5 |
6 | ## Try it out!
7 |
8 | You can try it at [screenpond.cool](https://screenpond.cool)
9 |
10 | Draw screens by clicking and dragging!
11 | Press the number keys to change colour.
12 | Press "C" to clear the screen.
13 |
14 | ## Running
15 |
16 | To run locally...
17 | you need to run a local server because it uses javascript modules.
18 | (ie: you can't just open `index.html` like most of my other projects)
19 |
20 | I recommend getting [deno](https://deno.land)
21 | and then installing `file_server` with this command:
22 |
23 | ```
24 | deno install --allow-read --allow-net https://deno.land/std@0.142.0/http/file_server.ts
25 | ```
26 |
27 | Then you can run this command to run a local server:
28 |
29 | ```
30 | file_server
31 | ```
32 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/libraries/habitat-colour-constants-embed.js:
--------------------------------------------------------------------------------
1 | const BLACK = Colour.Black.hex;
2 | const SILVER = Colour.Silver.hex;
3 | const GREEN = Colour.Green.hex;
4 | const RED = Colour.Red.hex;
5 | const BLUE = Colour.Blue.hex;
6 | const YELLOW = Colour.Yellow.hex;
7 | const ORANGE = Colour.Orange.hex;
8 | const ROSE = Colour.Rose.hex;
9 | const CYAN = Colour.Cyan.hex;
10 | const PURPLE = Colour.Purple.hex;
11 | const GREY = Colour.Grey.hex;
12 | // const GREY = "#ffffff";
13 | // const GREY = "#232940";
14 |
--------------------------------------------------------------------------------
/libraries/habitat-embed.js:
--------------------------------------------------------------------------------
1 | const Habitat = {};
2 |
3 | //=======//
4 | // Array //
5 | //=======//
6 | {
7 | const install = (global) => {
8 | Reflect.defineProperty(global.Array.prototype, "last", {
9 | get() {
10 | return this[this.length - 1];
11 | },
12 | set(value) {
13 | Reflect.defineProperty(this, "last", {
14 | value,
15 | configurable: true,
16 | writable: true,
17 | enumerable: true,
18 | });
19 | },
20 | configurable: true,
21 | enumerable: false,
22 | });
23 |
24 | Reflect.defineProperty(global.Array.prototype, "clone", {
25 | get() {
26 | return [...this];
27 | },
28 | set(value) {
29 | Reflect.defineProperty(this, "clone", {
30 | value,
31 | configurable: true,
32 | writable: true,
33 | enumerable: true,
34 | });
35 | },
36 | configurable: true,
37 | enumerable: false,
38 | });
39 |
40 | Reflect.defineProperty(global.Array.prototype, "at", {
41 | value(position) {
42 | if (position >= 0) return this[position];
43 | return this[this.length + position];
44 | },
45 | configurable: true,
46 | enumerable: false,
47 | writable: true,
48 | });
49 |
50 | Reflect.defineProperty(global.Array.prototype, "shuffle", {
51 | value() {
52 | for (let i = this.length - 1; i > 0; i--) {
53 | const r = Math.floor(Math.random() * (i + 1));
54 | [this[i], this[r]] = [this[r], this[i]];
55 | }
56 | return this;
57 | },
58 | configurable: true,
59 | enumerable: false,
60 | writable: true,
61 | });
62 |
63 | Reflect.defineProperty(global.Array.prototype, "trim", {
64 | value() {
65 | if (this.length == 0) return this;
66 | let start = this.length - 1;
67 | let end = 0;
68 | for (let i = 0; i < this.length; i++) {
69 | const value = this[i];
70 | if (value !== undefined) {
71 | start = i;
72 | break;
73 | }
74 | }
75 | for (let i = this.length - 1; i >= 0; i--) {
76 | const value = this[i];
77 | if (value !== undefined) {
78 | end = i + 1;
79 | break;
80 | }
81 | }
82 | this.splice(end);
83 | this.splice(0, start);
84 | return this;
85 | },
86 | configurable: true,
87 | enumerable: false,
88 | writable: true,
89 | });
90 |
91 | Reflect.defineProperty(global.Array.prototype, "repeat", {
92 | value(n) {
93 | if (n === 0) {
94 | this.splice(0);
95 | return this;
96 | }
97 | if (n < 0) {
98 | this.reverse();
99 | n = n - n - n;
100 | }
101 | const clone = [...this];
102 | for (let i = 1; i < n; i++) {
103 | this.push(...clone);
104 | }
105 | return this;
106 | },
107 | configurable: true,
108 | enumerable: false,
109 | writable: true,
110 | });
111 |
112 | Habitat.Array.installed = true;
113 | };
114 |
115 | Habitat.Array = { install };
116 | }
117 |
118 | //=======//
119 | // Async //
120 | //=======//
121 | {
122 | const sleep = (duration) =>
123 | new Promise((resolve) => setTimeout(resolve, duration));
124 | const install = (global) => {
125 | global.sleep = sleep;
126 | Habitat.Async.installed = true;
127 | };
128 |
129 | Habitat.Async = { install, sleep };
130 | }
131 |
132 | //========//
133 | // Colour //
134 | //========//
135 | {
136 | Habitat.Colour = {};
137 |
138 | Habitat.Colour.make = (style) => {
139 | if (typeof style === "number") {
140 | let string = style.toString();
141 | while (string.length < 3) string = "0" + string;
142 |
143 | const redId = parseInt(string[0]);
144 | const greenId = parseInt(string[1]);
145 | const blueId = parseInt(string[2]);
146 |
147 | const red = reds[redId];
148 | const green = greens[greenId];
149 | const blue = blues[blueId];
150 |
151 | const rgb = `rgb(${red}, ${green}, ${blue})`;
152 |
153 | const colour = Habitat.Colour.make(rgb);
154 | colour.splash = style;
155 | return colour;
156 | }
157 |
158 | const canvas = document.createElement("canvas");
159 | const context = canvas.getContext("2d");
160 | canvas.width = 1;
161 | canvas.height = 1;
162 | context.fillStyle = style;
163 | context.fillRect(0, 0, 1, 1);
164 |
165 | const data = context.getImageData(0, 0, 1, 1).data;
166 | const [red, green, blue] = data;
167 | const splash = getSplash(red, green, blue);
168 | const alpha = data[3] / 255;
169 | const [hue, saturation, lightness] = getHSL(red, green, blue);
170 | const colour = new Uint8Array([red, green, blue, alpha]);
171 | colour.fullColour = true;
172 |
173 | colour.red = red;
174 | colour.green = green;
175 | colour.blue = blue;
176 | colour.alpha = alpha;
177 |
178 | colour.splash = splash;
179 |
180 | colour.hue = hue;
181 | colour.saturation = saturation;
182 | colour.lightness = lightness;
183 |
184 | colour.r = red;
185 | colour.g = green;
186 | colour.b = blue;
187 | colour.a = alpha;
188 |
189 | const rgb = `rgb(${red}, ${green}, ${blue})`;
190 | const rgba = `rgba(${red}, ${green}, ${blue}, ${alpha})`;
191 | const hex = `#${hexify(red)}${hexify(green)}${hexify(blue)}`;
192 | const hsl = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
193 |
194 | colour.toString = () => hex;
195 | colour.rgb = rgb;
196 | colour.rgba = rgba;
197 | colour.hex = hex;
198 | colour.hsl = hsl;
199 |
200 | colour.brightness = (red * 299 + green * 587 + blue * 114) / 1000 / 255;
201 | colour.isBright = colour.brightness > 0.7;
202 | colour.isDark = colour.brightness < 0.3;
203 |
204 | return colour;
205 | };
206 |
207 | const hexify = (number) => {
208 | const string = number.toString(16);
209 | if (string.length === 2) return string;
210 | return "0" + string;
211 | };
212 |
213 | const getSplash = (red, green, blue) => {
214 | const closestRed = getClosest(red, reds).toString();
215 | const closestGreen = getClosest(green, greens).toString();
216 | const closestBlue = getClosest(blue, blues).toString();
217 | const string = closestRed + closestGreen + closestBlue;
218 | const splash = parseInt(string);
219 | return splash;
220 | };
221 |
222 | const getClosest = (number, array) => {
223 | let highscore = Infinity;
224 | let winner = 0;
225 | for (let i = 0; i < array.length; i++) {
226 | const value = array[i];
227 | const difference = Math.abs(number - value);
228 | if (difference < highscore) {
229 | highscore = difference;
230 | winner = i;
231 | }
232 | }
233 | return winner;
234 | };
235 |
236 | //https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
237 | const getHSL = (red, green, blue) => {
238 | red /= 255;
239 | green /= 255;
240 | blue /= 255;
241 |
242 | const max = Math.max(red, green, blue);
243 | const min = Math.min(red, green, blue);
244 | const chroma = max - min;
245 |
246 | let lightness = (max + min) / 2;
247 | let saturation = 0;
248 | if (lightness !== 0 && lightness !== 1) {
249 | saturation = (max - lightness) / Math.min(lightness, 1 - lightness);
250 | }
251 |
252 | let hue = 0;
253 | if (max === red) hue = (green - blue) / chroma;
254 | if (max === green) hue = 2 + (blue - red) / chroma;
255 | if (max === blue) hue = 4 + (red - green) / chroma;
256 | if (chroma === 0) hue = 0;
257 |
258 | lightness *= 100;
259 | saturation *= 100;
260 | hue *= 60;
261 | while (hue < 0) hue += 360;
262 |
263 | return [hue, saturation, lightness];
264 | };
265 |
266 | Habitat.Colour.add = (
267 | colour,
268 | {
269 | red = 0,
270 | green = 0,
271 | blue = 0,
272 | alpha = 0,
273 | hue = 0,
274 | saturation = 0,
275 | lightness = 0,
276 | r = 0,
277 | g = 0,
278 | b = 0,
279 | a = 0,
280 | h = 0,
281 | s = 0,
282 | l = 0,
283 | splash,
284 | fullColour = false,
285 | } = {}
286 | ) => {
287 | const newRed = clamp(colour.red + r + red, 0, 255);
288 | const newGreen = clamp(colour.green + g + green, 0, 255);
289 | const newBlue = clamp(colour.blue + b + blue, 0, 255);
290 | const newAlpha = clamp(colour.alpha + a + alpha, 0, 1);
291 | const rgbaStyle = `rgba(${newRed}, ${newGreen}, ${newBlue}, ${newAlpha})`;
292 | const rgbaColour = Habitat.Colour.make(rgbaStyle);
293 |
294 | if (fullColour) return rgbaColour;
295 |
296 | const newHue = wrap(rgbaColour.hue + h + hue, 0, 360);
297 | const newSaturation = clamp(rgbaColour.saturation + s + saturation, 0, 100);
298 | const newLightness = clamp(rgbaColour.lightness + l + lightness, 0, 100);
299 | const hslStyle = `hsl(${newHue}, ${newSaturation}%, ${newLightness}%)`;
300 | const hslColour = Habitat.Colour.make(hslStyle);
301 |
302 | if (splash !== undefined && splashed) {
303 | const newSplash = hslColour.splash + splash;
304 | const splashColour = Habitat.Colour.make(newSplash);
305 | return splashColour;
306 | }
307 |
308 | return hslColour;
309 | };
310 |
311 | Habitat.Colour.multiply = (
312 | colour,
313 | {
314 | red = 1,
315 | green = 1,
316 | blue = 1,
317 | alpha = 1,
318 | hue = 1,
319 | saturation = 1,
320 | lightness = 1,
321 | r = 1,
322 | g = 1,
323 | b = 1,
324 | a = 1,
325 | h = 1,
326 | s = 1,
327 | l = 1,
328 | splash,
329 | fullColour = false,
330 | } = {}
331 | ) => {
332 | const newRed = clamp(colour.red * r * red, 0, 255);
333 | const newGreen = clamp(colour.green * g * green, 0, 255);
334 | const newBlue = clamp(colour.blue * b * blue, 0, 255);
335 | const newAlpha = clamp(colour.alpha * a * alpha, 0, 1);
336 | const rgbaStyle = `rgba(${newRed}, ${newGreen}, ${newBlue}, ${newAlpha})`;
337 | const rgbaColour = Habitat.Colour.make(rgbaStyle);
338 |
339 | if (fullColour) return rgbaColour;
340 |
341 | const newHue = wrap(rgbaColour.hue * h * hue, 0, 360);
342 | const newSaturation = clamp(rgbaColour.saturation * s * saturation, 0, 100);
343 | const newLightness = clamp(rgbaColour.lightness * l * lightness, 0, 100);
344 | const hslStyle = `hsl(${newHue}, ${newSaturation}%, ${newLightness}%)`;
345 | const hslColour = Habitat.Colour.make(hslStyle);
346 |
347 | if (splash !== undefined) {
348 | const newSplash = hslColour.splash * splash;
349 | const splashColour = Habitat.Colour.make(newSplash);
350 | return splashColour;
351 | }
352 |
353 | return hslColour;
354 | };
355 |
356 | const clamp = (number, min, max) => {
357 | if (number < min) return min;
358 | if (number > max) return max;
359 | return number;
360 | };
361 |
362 | const wrap = (number, min, max) => {
363 | const difference = max - min;
364 | while (number < min) number += difference;
365 | while (number > max) number -= difference;
366 | return number;
367 | };
368 |
369 | const reds = [23, 55, 70, 98, 128, 159, 174, 204, 242, 255];
370 | const greens = [29, 67, 98, 128, 159, 174, 204, 222, 245, 255];
371 | const blues = [40, 70, 98, 128, 159, 174, 204, 222, 247, 255];
372 |
373 | Habitat.Colour.Void = Habitat.Colour.make("rgb(6, 7, 10)");
374 | Habitat.Colour.Black = Habitat.Colour.make(0);
375 | Habitat.Colour.Grey = Habitat.Colour.make(112);
376 | Habitat.Colour.Silver = Habitat.Colour.make(556);
377 | Habitat.Colour.White = Habitat.Colour.make(888);
378 |
379 | Habitat.Colour.Green = Habitat.Colour.make(293);
380 | Habitat.Colour.Red = Habitat.Colour.make(911);
381 | Habitat.Colour.Blue = Habitat.Colour.make(239);
382 | Habitat.Colour.Yellow = Habitat.Colour.make(961);
383 | Habitat.Colour.Orange = Habitat.Colour.make(931);
384 | Habitat.Colour.Pink = Habitat.Colour.make(933);
385 | Habitat.Colour.Rose = Habitat.Colour.make(936);
386 | Habitat.Colour.Cyan = Habitat.Colour.make(269);
387 | Habitat.Colour.Purple = Habitat.Colour.make(418);
388 |
389 | Habitat.Colour.cache = [];
390 | Habitat.Colour.splash = (number) => {
391 | if (Habitat.Colour.cache.length === 0) {
392 | for (let i = 0; i < 1000; i++) {
393 | const colour = Habitat.Colour.make(i);
394 | Habitat.Colour.cache.push(colour);
395 | }
396 | }
397 |
398 | return Habitat.Colour.cache[number];
399 | };
400 |
401 | Habitat.Colour.install = (global) => {
402 | global.Colour = Habitat.Colour;
403 | Habitat.Colour.installed = true;
404 | };
405 | }
406 |
407 | //=========//
408 | // Console //
409 | //=========//
410 | {
411 | const print = console.log.bind(console);
412 | const dir = (value) => console.dir(Object(value));
413 |
414 | let print9Counter = 0;
415 | const print9 = (message) => {
416 | if (print9Counter >= 9) return;
417 | print9Counter++;
418 | console.log(message);
419 | };
420 |
421 | const install = (global) => {
422 | global.print = print;
423 | global.dir = dir;
424 | global.print9 = print9;
425 |
426 | Reflect.defineProperty(global.Object.prototype, "d", {
427 | get() {
428 | const value = this.valueOf();
429 | console.log(value);
430 | return value;
431 | },
432 | set(value) {
433 | Reflect.defineProperty(this, "d", {
434 | value,
435 | configurable: true,
436 | writable: true,
437 | enumerable: true,
438 | });
439 | },
440 | configurable: true,
441 | enumerable: false,
442 | });
443 |
444 | Reflect.defineProperty(global.Object.prototype, "dir", {
445 | get() {
446 | console.dir(this);
447 | return this.valueOf();
448 | },
449 | set(value) {
450 | Reflect.defineProperty(this, "dir", {
451 | value,
452 | configurable: true,
453 | writable: true,
454 | enumerable: true,
455 | });
456 | },
457 | configurable: true,
458 | enumerable: false,
459 | });
460 |
461 | let d9Counter = 0;
462 | Reflect.defineProperty(global.Object.prototype, "d9", {
463 | get() {
464 | const value = this.valueOf();
465 | if (d9Counter < 9) {
466 | console.log(value);
467 | d9Counter++;
468 | }
469 | return value;
470 | },
471 | set(value) {
472 | Reflect.defineProperty(this, "d9", {
473 | value,
474 | configurable: true,
475 | writable: true,
476 | enumerable: true,
477 | });
478 | },
479 | configurable: true,
480 | enumerable: false,
481 | });
482 |
483 | Habitat.Console.installed = true;
484 | };
485 |
486 | Habitat.Console = { install, print, dir, print9 };
487 | }
488 |
489 | //==========//
490 | // Document //
491 | //==========//
492 | {
493 | const $ = (...args) => document.querySelector(...args);
494 | const $$ = (...args) => document.querySelectorAll(...args);
495 |
496 | const install = (global) => {
497 | global.$ = $;
498 | global.$$ = $$;
499 |
500 | if (global.Node === undefined) return;
501 |
502 | Reflect.defineProperty(global.Node.prototype, "$", {
503 | value(...args) {
504 | return this.querySelector(...args);
505 | },
506 | configurable: true,
507 | enumerable: false,
508 | writable: true,
509 | });
510 |
511 | Reflect.defineProperty(global.Node.prototype, "$$", {
512 | value(...args) {
513 | return this.querySelectorAll(...args);
514 | },
515 | configurable: true,
516 | enumerable: false,
517 | writable: true,
518 | });
519 |
520 | Habitat.Document.installed = true;
521 | };
522 |
523 | Habitat.Document = { install, $, $$ };
524 | }
525 |
526 | //=======//
527 | // Event //
528 | //=======//
529 | {
530 | const install = (global) => {
531 | Reflect.defineProperty(global.EventTarget.prototype, "on", {
532 | get() {
533 | return new Proxy(this, {
534 | get:
535 | (element, eventName) =>
536 | (...args) =>
537 | element.addEventListener(eventName, ...args),
538 | });
539 | },
540 | set(value) {
541 | Reflect.defineProperty(this, "on", {
542 | value,
543 | configurable: true,
544 | writable: true,
545 | enumerable: true,
546 | });
547 | },
548 | configurable: true,
549 | enumerable: false,
550 | });
551 |
552 | Reflect.defineProperty(global.EventTarget.prototype, "trigger", {
553 | value(name, options = {}) {
554 | const { bubbles = true, cancelable = true, ...data } = options;
555 | const event = new Event(name, { bubbles, cancelable });
556 | for (const key in data) event[key] = data[key];
557 | this.dispatchEvent(event);
558 | },
559 | configurable: true,
560 | enumerable: false,
561 | writable: true,
562 | });
563 |
564 | Habitat.Event.installed = true;
565 | };
566 |
567 | Habitat.Event = { install };
568 | }
569 |
570 | //======//
571 | // HTML //
572 | //======//
573 | {
574 | Habitat.HTML = (...args) => {
575 | const source = String.raw(...args);
576 | const template = document.createElement("template");
577 | template.innerHTML = source;
578 | return template.content;
579 | };
580 |
581 | Habitat.HTML.install = (global) => {
582 | global.HTML = Habitat.HTML;
583 | Habitat.HTML.installed = true;
584 | };
585 | }
586 |
587 | //============//
588 | // JavaScript //
589 | //============//
590 | {
591 | Habitat.JavaScript = (...args) => {
592 | const source = String.raw(...args);
593 | const code = `return ${source}`;
594 | const func = new Function(code)();
595 | return func;
596 | };
597 |
598 | Habitat.JavaScript.install = (global) => {
599 | global.JavaScript = Habitat.JavaScript;
600 | Habitat.JavaScript.installed = true;
601 | };
602 | }
603 |
604 | //==========//
605 | // Keyboard //
606 | //==========//
607 | {
608 | const Keyboard = (Habitat.Keyboard = {});
609 | Reflect.defineProperty(Keyboard, "install", {
610 | value(global) {
611 | global.Keyboard = Keyboard;
612 | global.addEventListener("keydown", (e) => {
613 | Keyboard[e.key] = true;
614 | });
615 |
616 | global.addEventListener("keyup", (e) => {
617 | Keyboard[e.key] = false;
618 | });
619 |
620 | Reflect.defineProperty(Keyboard, "installed", {
621 | value: true,
622 | configurable: true,
623 | enumerable: false,
624 | writable: true,
625 | });
626 | },
627 | configurable: true,
628 | enumerable: false,
629 | writable: true,
630 | });
631 | }
632 |
633 | //======//
634 | // Main //
635 | //======//
636 | Habitat.install = (global) => {
637 | if (Habitat.installed) return;
638 |
639 | if (!Habitat.Array.installed) Habitat.Array.install(global);
640 | if (!Habitat.Async.installed) Habitat.Async.install(global);
641 | if (!Habitat.Colour.installed) Habitat.Colour.install(global);
642 | if (!Habitat.Console.installed) Habitat.Console.install(global);
643 | if (!Habitat.Document.installed) Habitat.Document.install(global);
644 | if (!Habitat.Event.installed) Habitat.Event.install(global);
645 | if (!Habitat.HTML.installed) Habitat.HTML.install(global);
646 | if (!Habitat.JavaScript.installed) Habitat.JavaScript.install(global);
647 | if (!Habitat.Keyboard.installed) Habitat.Keyboard.install(global);
648 | if (!Habitat.Math.installed) Habitat.Math.install(global);
649 | if (!Habitat.Mouse.installed) Habitat.Mouse.install(global);
650 | if (!Habitat.Number.installed) Habitat.Number.install(global);
651 | if (!Habitat.Object.installed) Habitat.Object.install(global);
652 | if (!Habitat.Property.installed) Habitat.Property.install(global);
653 | if (!Habitat.Random.installed) Habitat.Random.install(global);
654 | if (!Habitat.Stage.installed) Habitat.Stage.install(global);
655 | if (!Habitat.String.installed) Habitat.String.install(global);
656 | if (!Habitat.Touches.installed) Habitat.Touches.install(global);
657 | if (!Habitat.Tween.installed) Habitat.Tween.install(global);
658 | if (!Habitat.Type.installed) Habitat.Type.install(global);
659 |
660 | Habitat.installed = true;
661 | };
662 |
663 | //======//
664 | // Math //
665 | //======//
666 | {
667 | const gcd = (...numbers) => {
668 | const [head, ...tail] = numbers;
669 | if (numbers.length === 1) return head;
670 | if (numbers.length > 2) return gcd(head, gcd(...tail));
671 |
672 | let [a, b] = [head, ...tail];
673 |
674 | while (true) {
675 | if (b === 0) return a;
676 | a = a % b;
677 | if (a === 0) return b;
678 | b = b % a;
679 | }
680 | };
681 |
682 | const reduce = (...numbers) => {
683 | const divisor = gcd(...numbers);
684 | return numbers.map((n) => n / divisor);
685 | };
686 |
687 | const wrap = (number, min, max) => {
688 | const difference = max - min;
689 | while (number > max) {
690 | number -= difference;
691 | }
692 | while (number < min) {
693 | number += difference;
694 | }
695 | return number;
696 | };
697 |
698 | const install = (global) => {
699 | global.Math.gcd = Habitat.Math.gcd;
700 | global.Math.reduce = Habitat.Math.reduce;
701 | global.Math.wrap = Habitat.Math.wrap;
702 | Habitat.Math.installed = true;
703 | };
704 |
705 | Habitat.Math = { install, gcd, reduce, wrap };
706 | }
707 |
708 | //=======//
709 | // Mouse //
710 | //=======//
711 | {
712 | const Mouse = (Habitat.Mouse = {
713 | position: [undefined, undefined],
714 | });
715 |
716 | const buttonMap = ["Left", "Middle", "Right", "Back", "Forward"];
717 | const touchEventEquivalents = {
718 | touchstart: "mousedown",
719 | touchmove: "mousemove",
720 | touchend: "mouseup",
721 | touchcancel: "mouseup",
722 | };
723 |
724 | Reflect.defineProperty(Mouse, "install", {
725 | value(global) {
726 | global.Mouse = Mouse;
727 | global.addEventListener("pointerdown", (e) => {
728 | Mouse.position[0] = e.clientX;
729 | Mouse.position[1] = e.clientY;
730 | const buttonName = buttonMap[e.button];
731 | Mouse[buttonName] = true;
732 | });
733 |
734 | global.addEventListener("pointerup", (e) => {
735 | Mouse.position[0] = e.clientX;
736 | Mouse.position[1] = e.clientY;
737 | const buttonName = buttonMap[e.button];
738 | Mouse[buttonName] = false;
739 | });
740 |
741 | global.addEventListener("pointermove", (e) => {
742 | Mouse.position[0] = e.clientX;
743 | Mouse.position[1] = e.clientY;
744 | });
745 |
746 | const handleTouchEvent = (event) => {
747 | const [touch] = event.changedTouches;
748 | const type = touchEventEquivalents[event.type];
749 | const { screenX, screenY, clientX, clientY } = touch;
750 | const mouseEvent = new MouseEvent(type, {
751 | type,
752 | bubbles: true,
753 | cancelable: true,
754 | view: global,
755 | detail: 1,
756 | screenX,
757 | screenY,
758 | clientX,
759 | clientY,
760 | });
761 |
762 | touch.target.dispatchEvent(mouseEvent);
763 | event.preventDefault();
764 | };
765 |
766 | global.addEventListener("touchstart", handleTouchEvent, true);
767 | global.addEventListener("touchmove", handleTouchEvent, true);
768 | global.addEventListener("touchend", handleTouchEvent, true);
769 | global.addEventListener("touchcancel", handleTouchEvent, true);
770 |
771 | Reflect.defineProperty(Mouse, "installed", {
772 | value: true,
773 | configurable: true,
774 | enumerable: false,
775 | writable: true,
776 | });
777 | },
778 | configurable: true,
779 | enumerable: false,
780 | writable: true,
781 | });
782 | }
783 |
784 | //========//
785 | // Number //
786 | //========//
787 | {
788 | const install = (global) => {
789 | Reflect.defineProperty(global.Number.prototype, "to", {
790 | value: function* (v) {
791 | let i = this.valueOf();
792 | if (i <= v) {
793 | while (i <= v) {
794 | yield i;
795 | i++;
796 | }
797 | } else {
798 | while (i >= v) {
799 | yield i;
800 | i--;
801 | }
802 | }
803 | },
804 | configurable: true,
805 | enumerable: false,
806 | writable: true,
807 | });
808 |
809 | const numberToString = global.Number.prototype.toString;
810 | Reflect.defineProperty(global.Number.prototype, "toString", {
811 | value(base, size) {
812 | if (size === undefined) return numberToString.call(this, base);
813 | if (size <= 0) return "";
814 | const string = numberToString.call(this, base);
815 | return string.slice(-size).padStart(size, "0");
816 | },
817 | configurable: true,
818 | enumerable: false,
819 | writable: true,
820 | });
821 |
822 | if (global.BigInt !== undefined) {
823 | const bigIntToString = global.BigInt.prototype.toString;
824 | Reflect.defineProperty(global.BigInt.prototype, "toString", {
825 | value(base, size) {
826 | if (size === undefined) return bigIntToString.call(this, base);
827 | if (size <= 0) return "";
828 | const string = bigIntToString.call(this, base);
829 | return string.slice(-size).padStart(size, "0");
830 | },
831 | configurable: true,
832 | enumerable: false,
833 | writable: true,
834 | });
835 | }
836 |
837 | Habitat.Number.installed = true;
838 | };
839 |
840 | Habitat.Number = { install };
841 | }
842 |
843 | //========//
844 | // Object //
845 | //========//
846 | {
847 | Habitat.Object = {};
848 | Habitat.Object.install = (global) => {
849 | Reflect.defineProperty(global.Object.prototype, Symbol.iterator, {
850 | value: function* () {
851 | for (const key in this) {
852 | yield this[key];
853 | }
854 | },
855 | configurable: true,
856 | enumerable: false,
857 | writable: true,
858 | });
859 |
860 | Reflect.defineProperty(global.Object.prototype, "keys", {
861 | value() {
862 | return Object.keys(this);
863 | },
864 | configurable: true,
865 | enumerable: false,
866 | writable: true,
867 | });
868 |
869 | Reflect.defineProperty(global.Object.prototype, "values", {
870 | value() {
871 | return Object.values(this);
872 | },
873 | configurable: true,
874 | enumerable: false,
875 | writable: true,
876 | });
877 |
878 | Reflect.defineProperty(global.Object.prototype, "entries", {
879 | value() {
880 | return Object.entries(this);
881 | },
882 | configurable: true,
883 | enumerable: false,
884 | writable: true,
885 | });
886 |
887 | Habitat.Object.installed = true;
888 | };
889 | }
890 |
891 | //==========//
892 | // Property //
893 | //==========//
894 | {
895 | const install = (global) => {
896 | Reflect.defineProperty(global.Object.prototype, "_", {
897 | get() {
898 | return new Proxy(this, {
899 | set(object, propertyName, descriptor) {
900 | Reflect.defineProperty(object, propertyName, descriptor);
901 | },
902 | get(object, propertyName) {
903 | const editor = {
904 | get value() {
905 | const descriptor =
906 | Reflect.getOwnPropertyDescriptor(object, propertyName) || {};
907 | const { value } = descriptor;
908 | return value;
909 | },
910 | set value(value) {
911 | const { enumerable, configurable, writable } =
912 | Reflect.getOwnPropertyDescriptor(object, propertyName) || {
913 | enumerable: true,
914 | configurable: true,
915 | writable: true,
916 | };
917 | const descriptor = {
918 | value,
919 | enumerable,
920 | configurable,
921 | writable,
922 | };
923 | Reflect.defineProperty(object, propertyName, descriptor);
924 | },
925 | get get() {
926 | const descriptor =
927 | Reflect.getOwnPropertyDescriptor(object, propertyName) || {};
928 | const { get } = descriptor;
929 | return get;
930 | },
931 | set get(get) {
932 | const { set, enumerable, configurable } =
933 | Reflect.getOwnPropertyDescriptor(object, propertyName) || {
934 | enumerable: true,
935 | configurable: true,
936 | };
937 | const descriptor = { get, set, enumerable, configurable };
938 | Reflect.defineProperty(object, propertyName, descriptor);
939 | },
940 | get set() {
941 | const descriptor =
942 | Reflect.getOwnPropertyDescriptor(object, propertyName) || {};
943 | const { set } = descriptor;
944 | return set;
945 | },
946 | set set(set) {
947 | const { get, enumerable, configurable } =
948 | Reflect.getOwnPropertyDescriptor(object, propertyName) || {
949 | enumerable: true,
950 | configurable: true,
951 | };
952 | const descriptor = { get, set, enumerable, configurable };
953 | Reflect.defineProperty(object, propertyName, descriptor);
954 | },
955 | get enumerable() {
956 | const descriptor =
957 | Reflect.getOwnPropertyDescriptor(object, propertyName) || {};
958 | const { enumerable } = descriptor;
959 | return enumerable;
960 | },
961 | set enumerable(v) {
962 | const descriptor = Reflect.getOwnPropertyDescriptor(
963 | object,
964 | propertyName
965 | ) || { configurable: true, writable: true };
966 | descriptor.enumerable = v;
967 | Reflect.defineProperty(object, propertyName, descriptor);
968 | },
969 | get configurable() {
970 | const descriptor =
971 | Reflect.getOwnPropertyDescriptor(object, propertyName) || {};
972 | const { configurable } = descriptor;
973 | return configurable;
974 | },
975 | set configurable(v) {
976 | const descriptor = Reflect.getOwnPropertyDescriptor(
977 | object,
978 | propertyName
979 | ) || { enumerable: true, writable: true };
980 | descriptor.configurable = v;
981 | Reflect.defineProperty(object, propertyName, descriptor);
982 | },
983 | get writable() {
984 | const descriptor =
985 | Reflect.getOwnPropertyDescriptor(object, propertyName) || {};
986 | const { writable } = descriptor;
987 | return writable;
988 | },
989 | set writable(v) {
990 | const oldDescriptor = Reflect.getOwnPropertyDescriptor(
991 | object,
992 | propertyName
993 | ) || { enumerable: true, configurable: true };
994 | const { get, set, writable, ...rest } = oldDescriptor;
995 | const newDescriptor = { ...rest, writable: v };
996 | Reflect.defineProperty(object, propertyName, newDescriptor);
997 | },
998 | };
999 | return editor;
1000 | },
1001 | });
1002 | },
1003 | set(value) {
1004 | Reflect.defineProperty(this, "_", {
1005 | value,
1006 | configurable: true,
1007 | writable: true,
1008 | enumerable: true,
1009 | });
1010 | },
1011 | configurable: true,
1012 | enumerable: false,
1013 | });
1014 |
1015 | Habitat.Property.installed = true;
1016 | };
1017 |
1018 | Habitat.Property = { install };
1019 | }
1020 |
1021 | //========//
1022 | // Random //
1023 | //========//
1024 | {
1025 | Habitat.Random = {};
1026 |
1027 | const maxId8 = 2 ** 16;
1028 | const u8s = new Uint8Array(maxId8);
1029 | let id8 = maxId8;
1030 | const getRandomUint8 = () => {
1031 | if (id8 >= maxId8) {
1032 | crypto.getRandomValues(u8s);
1033 | id8 = 0;
1034 | }
1035 |
1036 | const result = u8s[id8];
1037 | id8++;
1038 | return result;
1039 | };
1040 |
1041 | Reflect.defineProperty(Habitat.Random, "Uint8", {
1042 | get: getRandomUint8,
1043 | configurable: true,
1044 | enumerable: true,
1045 | });
1046 |
1047 | const maxId32 = 2 ** 14;
1048 | const u32s = new Uint32Array(maxId32);
1049 | let id32 = maxId32;
1050 | const getRandomUint32 = () => {
1051 | if (id32 >= maxId32) {
1052 | crypto.getRandomValues(u32s);
1053 | id32 = 0;
1054 | }
1055 |
1056 | const result = u32s[id32];
1057 | id32++;
1058 | return result;
1059 | };
1060 |
1061 | Reflect.defineProperty(Habitat.Random, "Uint32", {
1062 | get: getRandomUint32,
1063 | configurable: true,
1064 | enumerable: true,
1065 | });
1066 |
1067 | Habitat.Random.oneIn = (n) => {
1068 | const result = getRandomUint32();
1069 | return result % n === 0;
1070 | };
1071 |
1072 | Habitat.Random.maybe = (chance) => {
1073 | return Habitat.Random.oneIn(1 / chance);
1074 | };
1075 |
1076 | Habitat.Random.install = (global) => {
1077 | global.Random = Habitat.Random;
1078 | global.oneIn = Habitat.Random.oneIn;
1079 | global.maybe = Habitat.Random.maybe;
1080 | Habitat.Random.installed = true;
1081 | };
1082 | }
1083 |
1084 | //=======//
1085 | // Stage //
1086 | //=======//
1087 | {
1088 | Habitat.Stage = {};
1089 | Habitat.Stage.make = () => {
1090 | const canvas = document.createElement("canvas");
1091 | const context = canvas.getContext("2d");
1092 |
1093 | const stage = {
1094 | canvas,
1095 | context,
1096 | update: () => {},
1097 | draw: () => {},
1098 | tick: () => {
1099 | stage.update();
1100 | stage.draw();
1101 | requestAnimationFrame(stage.tick);
1102 | },
1103 | };
1104 |
1105 | requestAnimationFrame(stage.tick);
1106 | return stage;
1107 | };
1108 |
1109 | Habitat.Stage.install = (global) => {
1110 | global.Stage = Habitat.Stage;
1111 | Habitat.Stage.installed = true;
1112 | };
1113 | }
1114 |
1115 | //========//
1116 | // String //
1117 | //========//
1118 | {
1119 | const install = (global) => {
1120 | Reflect.defineProperty(global.String.prototype, "divide", {
1121 | value(n) {
1122 | const regExp = RegExp(`[^]{1,${n}}`, "g");
1123 | return this.match(regExp);
1124 | },
1125 | configurable: true,
1126 | enumerable: false,
1127 | writable: true,
1128 | });
1129 |
1130 | Reflect.defineProperty(global.String.prototype, "toNumber", {
1131 | value(base) {
1132 | return parseInt(this, base);
1133 | },
1134 | configurable: true,
1135 | enumerable: false,
1136 | writable: true,
1137 | });
1138 |
1139 | Habitat.String.installed = true;
1140 | };
1141 |
1142 | Habitat.String = { install };
1143 | }
1144 |
1145 | //=======//
1146 | // Touch //
1147 | //=======//
1148 | {
1149 | const Touches = (Habitat.Touches = []);
1150 |
1151 | const trim = (a) => {
1152 | if (a.length == 0) return a;
1153 | let start = a.length - 1;
1154 | let end = 0;
1155 | for (let i = 0; i < a.length; i++) {
1156 | const value = a[i];
1157 | if (value !== undefined) {
1158 | start = i;
1159 | break;
1160 | }
1161 | }
1162 | for (let i = a.length - 1; i >= 0; i--) {
1163 | const value = a[i];
1164 | if (value !== undefined) {
1165 | end = i + 1;
1166 | break;
1167 | }
1168 | }
1169 | a.splice(end);
1170 | a.splice(0, start);
1171 | return a;
1172 | };
1173 |
1174 | Reflect.defineProperty(Touches, "install", {
1175 | value(global) {
1176 | global.Touches = Touches;
1177 | global.addEventListener("touchstart", (e) => {
1178 | for (const changedTouch of e.changedTouches) {
1179 | const x = changedTouch.clientX;
1180 | const y = changedTouch.clientY;
1181 | const id = changedTouch.identifier;
1182 | if (Touches[id] === undefined)
1183 | Touches[id] = { position: [undefined, undefined] };
1184 | const touch = Touches[id];
1185 | touch.position[0] = x;
1186 | touch.position[1] = y;
1187 | }
1188 | });
1189 |
1190 | global.addEventListener("touchmove", (e) => {
1191 | try {
1192 | for (const changedTouch of e.changedTouches) {
1193 | const x = changedTouch.clientX;
1194 | const y = changedTouch.clientY;
1195 | const id = changedTouch.identifier;
1196 | let touch = Touches[id];
1197 | if (touch == undefined) {
1198 | touch = { position: [undefined, undefined] };
1199 | Touches[id] = touch;
1200 | }
1201 |
1202 | touch.position[0] = x;
1203 | touch.position[1] = y;
1204 | }
1205 | } catch (e) {
1206 | console.error(e);
1207 | }
1208 | });
1209 |
1210 | global.addEventListener("touchend", (e) => {
1211 | for (const changedTouch of e.changedTouches) {
1212 | const id = changedTouch.identifier;
1213 | Touches[id] = undefined;
1214 | }
1215 | trim(Touches);
1216 | });
1217 |
1218 | Reflect.defineProperty(Touches, "installed", {
1219 | value: true,
1220 | configurable: true,
1221 | enumerable: false,
1222 | writable: true,
1223 | });
1224 | },
1225 | configurable: true,
1226 | enumerable: false,
1227 | writable: true,
1228 | });
1229 | }
1230 |
1231 | //=======//
1232 | // Tween //
1233 | //=======//
1234 |
1235 | {
1236 | Habitat.Tween = {};
1237 |
1238 | // all from https://easings.net
1239 | Habitat.Tween.EASE_IN_LINEAR = (t) => t;
1240 | Habitat.Tween.EASE_OUT_LINEAR = (t) => t;
1241 | Habitat.Tween.EASE_IN_OUT_LINEAR = (t) => t;
1242 | Habitat.Tween.EASE_IN_SINE = (t) => 1 - Math.cos((t * Math.PI) / 2);
1243 | Habitat.Tween.EASE_OUT_SINE = (t) => Math.sin((t * Math.PI) / 2);
1244 | Habitat.Tween.EASE_IN_OUT_SINE = (t) => -(Math.cos(t * Math.PI) - 1) / 2;
1245 | Habitat.Tween.EASE_IN_POWER = (p) => (t) => Math.pow(t, p);
1246 | Habitat.Tween.EASE_OUT_POWER = (p) => (t) => 1 - Math.pow(1 - t, p);
1247 | Habitat.Tween.EASE_IN_OUT_POWER = (p) => (t) => {
1248 | if (t < 0.5) return Math.pow(2, p - 1) * Math.pow(t, p);
1249 | return 1 - Math.pow(2 - 2 * t, p) / 2;
1250 | };
1251 | Habitat.Tween.EASE_IN_EXP = Habitat.Tween.EASE_IN_EXPONENTIAL = (e) => (t) =>
1252 | Math.pow(2, e * t - e) * t;
1253 | Habitat.Tween.EASE_OUT_EXP = Habitat.Tween.EASE_OUT_EXPONENTIAL =
1254 | (e) => (t) =>
1255 | 1 - Math.pow(2, -e * t) * (1 - t);
1256 | Habitat.Tween.EASE_IN_OUT_EXP = Habitat.Tween.EASE_IN_OUT_EXPONENTIAL =
1257 | (e) => (t) => {
1258 | let f;
1259 | if (t < 0.5) f = (t) => Math.pow(2, 2 * e * t - e) / 2;
1260 | else f = (t) => (2 - Math.pow(2, -2 * e * t + e)) / 2;
1261 | return f(t) * ((1 - t) * f(0) + t * (f(1) - 1));
1262 | };
1263 | Habitat.Tween.EASE_IN_CIRCULAR = (t) => 1 - Math.sqrt(1 - Math.pow(t, 2));
1264 | Habitat.Tween.EASE_OUT_CIRCULAR = (t) => Math.sqrt(1 - Math.pow(t - 1, 2));
1265 | Habitat.Tween.EASE_IN_OUT_CIRCULAR = (t) => {
1266 | if (t < 0.5) return 0.5 - Math.sqrt(1 - Math.pow(2 * t, 2)) / 2;
1267 | return 0.5 + Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) / 2;
1268 | };
1269 | Habitat.Tween.EASE_IN_BACK = (t) => 2.70158 * t * t * t - 1.70158 * t * t;
1270 | Habitat.Tween.EASE_OUT_BACK = (t) =>
1271 | 1 + 2.70158 * Math.pow(t - 1, 3) + 1.70158 * Math.pow(t - 1, 2);
1272 | Habitat.Tween.EASE_IN_OUT_BACK = (t) => {
1273 | if (t < 0.5) return (Math.pow(2 * t, 2) * (3.59491 * 2 * t - 2.59491)) / 2;
1274 | return (Math.pow(2 * t - 2, 2) * (3.59491 * (t * 2 - 2) + 2.59491) + 2) / 2;
1275 | };
1276 | Habitat.Tween.EASE_IN_ELASTIC = (t) =>
1277 | -Math.pow(2, 10 * t - 10) * Math.sin(((t * 10 - 10.75) * 2 * Math.PI) / 3);
1278 | Habitat.Tween.EASE_OUT_ELASTIC = (t) =>
1279 | Math.pow(2, -10 * t) * Math.sin(((t * 10 - 0.75) * 2 * Math.PI) / 3) + 1;
1280 | Habitat.Tween.EASE_IN_OUT_ELASTIC = (t) => {
1281 | if (t < 0.5)
1282 | return (
1283 | -(
1284 | Math.pow(2, 20 * t - 10) *
1285 | Math.sin(((20 * t - 11.125) * 2 * Math.PI) / 4.5)
1286 | ) / 2
1287 | );
1288 | return (
1289 | (Math.pow(2, -20 * t + 10) *
1290 | Math.sin(((20 * t - 11.125) * 2 * Math.PI) / 4.5)) /
1291 | 2 +
1292 | 1
1293 | );
1294 | };
1295 | Habitat.Tween.EASE_OUT_BOUNCE = (t) => (t) => {
1296 | const n1 = 7.5625;
1297 | const d1 = 2.75;
1298 |
1299 | if (t < 1 / d1) return n1 * t * t;
1300 | else if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
1301 | else if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
1302 | else return n1 * (t -= 2.625 / d1) * t + 0.984375;
1303 | };
1304 | Habitat.Tween.EASE_IN_BOUNCE = (t) =>
1305 | 1 - Habitat.Tween.EASE_OUT_BOUNCE(1 - t);
1306 | Habitat.Tween.EASE_IN_OUT_BOUNCE = (t) => {
1307 | if (t < 0.5) return (1 - Habitat.Tween.EASE_OUT_BOUNCE(1 - 2 * t)) / 2;
1308 | return (1 + Habitat.Tween.EASE_OUT_BOUNCE(2 * t - 1)) / 2;
1309 | };
1310 |
1311 | Habitat.Tween.install = (global) => {
1312 | Habitat.Tween.installed = true;
1313 |
1314 | Reflect.defineProperty(global.Object.prototype, "tween", {
1315 | value(
1316 | propertyName,
1317 | {
1318 | to,
1319 | from,
1320 | over = 1000,
1321 | launch = 0.5,
1322 | land = 0.5,
1323 | strength = 1,
1324 | ease = false,
1325 | } = {}
1326 | ) {
1327 | const before = this[propertyName];
1328 | if (from === undefined) from = before;
1329 | if (to === undefined) to = before;
1330 |
1331 | launch *= 2 / 3;
1332 | land = 1 / 3 + ((1 - land) * 2) / 3;
1333 |
1334 | const start = performance.now();
1335 |
1336 | Reflect.defineProperty(this, propertyName, {
1337 | get() {
1338 | const now = performance.now();
1339 |
1340 | if (now > start + over) {
1341 | Reflect.defineProperty(this, propertyName, {
1342 | value: to,
1343 | writable: true,
1344 | configurable: true,
1345 | enumerable: true,
1346 | });
1347 | return to;
1348 | }
1349 |
1350 | const t = (now - start) / over;
1351 |
1352 | if (ease) {
1353 | const v = ease(strength);
1354 | if (typeof v == "function") return v(t) * (to - from) + from;
1355 | return ease(t) * (to - from) + from;
1356 | }
1357 |
1358 | const v =
1359 | 3 * t * (1 - t) * (1 - t) * launch +
1360 | 3 * t * t * (1 - t) * land +
1361 | t * t * t;
1362 | return v * (to - from) + from;
1363 | },
1364 | set() {},
1365 |
1366 | configurable: true,
1367 | enumerable: true,
1368 | });
1369 | },
1370 |
1371 | configurable: true,
1372 | enumerable: false,
1373 | writable: true,
1374 | });
1375 | };
1376 | }
1377 |
1378 | //======//
1379 | // Type //
1380 | //======//
1381 | {
1382 | const Int = {
1383 | check: (n) => n % 1 == 0,
1384 | convert: (n) => parseInt(n),
1385 | };
1386 |
1387 | const Positive = {
1388 | check: (n) => n >= 0,
1389 | convert: (n) => Math.abs(n),
1390 | };
1391 |
1392 | const Negative = {
1393 | check: (n) => n <= 0,
1394 | convert: (n) => -Math.abs(n),
1395 | };
1396 |
1397 | const UInt = {
1398 | check: (n) => n % 1 == 0 && n >= 0,
1399 | convert: (n) => Math.abs(parseInt(n)),
1400 | };
1401 |
1402 | const UpperCase = {
1403 | check: (s) => s == s.toUpperCase(),
1404 | convert: (s) => s.toUpperCase(),
1405 | };
1406 |
1407 | const LowerCase = {
1408 | check: (s) => s == s.toLowerCase(),
1409 | convert: (s) => s.toLowerCase(),
1410 | };
1411 |
1412 | const WhiteSpace = {
1413 | check: (s) => /^[ | ]*$/.test(s),
1414 | };
1415 |
1416 | const PureObject = {
1417 | check: (o) => o.constructor == Object,
1418 | };
1419 |
1420 | const Primitive = {
1421 | check: (p) => p.is(Number) || p.is(String) || p.is(RegExp) || p.is(Symbol),
1422 | };
1423 |
1424 | const install = (global) => {
1425 | global.Int = Int;
1426 | global.Positive = Positive;
1427 | global.Negative = Negative;
1428 | global.UInt = UInt;
1429 | global.UpperCase = UpperCase;
1430 | global.LowerCase = LowerCase;
1431 | global.WhiteSpace = WhiteSpace;
1432 | global.PureObject = PureObject;
1433 | global.Primitive = Primitive;
1434 |
1435 | Reflect.defineProperty(global.Object.prototype, "is", {
1436 | value(type) {
1437 | if ("check" in type) {
1438 | try {
1439 | return type.check(this);
1440 | } catch {}
1441 | }
1442 | try {
1443 | return this instanceof type;
1444 | } catch {
1445 | return false;
1446 | }
1447 | },
1448 | configurable: true,
1449 | enumerable: false,
1450 | writable: true,
1451 | });
1452 |
1453 | Reflect.defineProperty(global.Object.prototype, "as", {
1454 | value(type) {
1455 | if ("convert" in type) {
1456 | try {
1457 | return type.convert(this);
1458 | } catch {}
1459 | }
1460 | return type(this);
1461 | },
1462 | configurable: true,
1463 | enumerable: false,
1464 | writable: true,
1465 | });
1466 |
1467 | Habitat.Type.installed = true;
1468 | };
1469 |
1470 | Habitat.Type = {
1471 | install,
1472 | Int,
1473 | Positive,
1474 | Negative,
1475 | UInt,
1476 | UpperCase,
1477 | LowerCase,
1478 | WhiteSpace,
1479 | PureObject,
1480 | Primitive,
1481 | };
1482 | }
1483 |
--------------------------------------------------------------------------------
/libraries/show.js:
--------------------------------------------------------------------------------
1 | const Show = {};
2 |
3 | {
4 | Show.start = ({
5 | canvas,
6 | context,
7 | paused = false,
8 | scale = 1.0,
9 | aspect,
10 | speed = 1.0,
11 | resize = () => {},
12 | tick = () => {},
13 | supertick = () => {},
14 | } = {}) => {
15 | const show = {
16 | canvas,
17 | context,
18 | paused,
19 | scale,
20 | speed,
21 | resize,
22 | tick,
23 | supertick,
24 | };
25 |
26 | if (document.body === null) {
27 | addEventListener("load", () => start(show));
28 | } else {
29 | requestAnimationFrame(() => start(show));
30 | }
31 |
32 | return show;
33 | };
34 |
35 | const start = (show) => {
36 | // TODO: support canvases of different sizes. just for provided ones? or all?
37 | if (show.canvas === undefined) {
38 | document.body.style["margin"] = "0px";
39 | document.body.style["overflow"] = "hidden";
40 | document.body.style.setProperty("position", "fixed");
41 | document.body.style.setProperty("width", "100vw");
42 | document.body.style.setProperty("height", "100vh");
43 | document.body.style.setProperty("pointer-events", "all");
44 | document.body.style.setProperty("touch-action", "none");
45 |
46 | show.canvas = document.createElement("canvas");
47 | // show.canvas.style["background-color"] = Colour.Void;
48 | show.canvas.style["background-color"] = Colour.Black;
49 | show.canvas.style["image-rendering"] = "pixelated";
50 | document.body.appendChild(show.canvas);
51 | }
52 |
53 | if (show.context === undefined) {
54 | show.context = show.canvas.getContext("2d");
55 | }
56 |
57 | const resize = () => {
58 | show.canvas.width = Math.round(innerWidth * show.scale);
59 | show.canvas.height = Math.round(innerHeight * show.scale);
60 | show.canvas.style["width"] = show.canvas.width;
61 | show.canvas.style["height"] = show.canvas.height;
62 | show.resize(show.context, show.canvas);
63 | };
64 |
65 | let t = 0;
66 | const tick = () => {
67 | t += show.speed;
68 | while (t > 0) {
69 | if (!show.paused) show.tick(show.context, show.canvas);
70 | show.supertick(show.context, show.canvas);
71 | t--;
72 | }
73 |
74 | requestAnimationFrame(tick);
75 | };
76 |
77 | addEventListener("resize", resize);
78 | addEventListener("keydown", (e) => {
79 | if (e.key === " ") show.paused = !show.paused;
80 | });
81 |
82 | resize();
83 | requestAnimationFrame(tick);
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/notes.txt:
--------------------------------------------------------------------------------
1 | =============================================================
2 |
3 | Hand = Object that stores information about the cursor/touch
4 |
5 | Canvas = Client's whole screen (0 to max pixel)
6 | View = Client's whole screen (0.0 to 1.0)
7 |
8 | CanvasPosition = Vector representing a location in the canvas (0 to max pixel)
9 | ViewPosition = Vector representing a location in the view (0.0 to 1.0)
10 |
11 | Corners = Four view positions (from 0.0 to 1.0) representing the corners of a quadrilateral
12 |
13 | Hex = Hex string representing a colour value
14 | Colour = Object containing a hex and list of screens
15 | World = Single screen that fills up the view
16 | Screen = Object representing a positioned colour
17 |
18 | Drawn Screen = Screen object representing an instance of a drawn screen
19 | Addressed Screen = Screen object that is stored in a Colour object to represent what's in all screens of that colour
20 |
21 | Address = Object containing the number and parent colour of a screen
22 | Pick = Object representing a position in a specific colour. When a pick is used to represent a screen, its 'position' is the corner A
23 | Part = Object representing a part of a screen (Inside, Outside, Edge, Corner)
24 | Route = An object containing the steps we took to pick a screen
25 |
26 | Yank = Sometimes, the 'camera' needs to be adjusted to keep things in the same place (eg: the cursor) - I call this process 'yanking' (mainly appears in github issues)
27 |
28 | ==============================================================
29 |
30 | Pick
31 | .screen = The screen object that was picked
32 | .corners = The view corners of the picked screen's parent
33 | .position = The position of the picked screen in its parent
34 | .part = The part of the picked screen that was picked
35 | .parent = The addressed parent screen of the picked screen
36 | .address = The address of the picked screen
37 | .depth = How many layers deep we had to go through to pick the screen
38 | .route = The route we took to get to this pick
39 |
40 | =============================================================
41 |
42 | Corners
43 | A B
44 | C D
45 |
46 | Corner Parts
47 | 0 1
48 | 2 3
49 |
50 | Edge Parts
51 | 0
52 | 1 2
53 | 3
54 |
55 | =============================================================
--------------------------------------------------------------------------------
/source/address.js:
--------------------------------------------------------------------------------
1 | //=========//
2 | // ADDRESS //
3 | //=========//
4 | export const makeAddress = (colour, number) => {
5 | const address = { colour, number };
6 | return address;
7 | };
8 |
9 | export const getScreenFromAddress = (address, world = undefined) => {
10 | if (address === undefined) return world;
11 | const { colour, number } = address;
12 | const screen = colour.screens[number];
13 | return screen;
14 | };
15 |
16 | export const areAddressesEqual = (a, b) => {
17 | if (a.colour !== b.colour) return false;
18 | if (a.number !== b.number) return false;
19 | return true;
20 | };
21 |
22 | export const getAddressFromScreen = (screen, colour) => {
23 | const number = colour.screens.indexOf(screen);
24 | if (number === -1) return undefined;
25 | return makeAddress(colour, number);
26 | };
27 |
--------------------------------------------------------------------------------
/source/colour.js:
--------------------------------------------------------------------------------
1 | import { makeAddress } from "./address.js";
2 | import { getRotatedCorners, VIEW_CORNERS } from "./corners.js";
3 | import { getMappedPositions } from "./position.js";
4 | import { addStep, makeRoute } from "./route.js";
5 | import { makeScreen } from "./screen.js";
6 |
7 | //========//
8 | // COLOUR //
9 | //========//
10 | export const COLOUR_HEXES = [
11 | GREEN,
12 | BLUE,
13 | RED,
14 | YELLOW,
15 | ORANGE,
16 | ROSE,
17 | CYAN,
18 | PURPLE,
19 | GREY,
20 | ];
21 |
22 | export const makeColours = () => {
23 | const colours = {};
24 | for (const hex of COLOUR_HEXES) {
25 | colours[hex] = makeColour(hex);
26 | }
27 | return colours;
28 | };
29 |
30 | export const makeColour = (hex) => {
31 | const screens = [];
32 | const parentNumber = 0;
33 | const colour = { hex, screens, parentNumber };
34 | return colour;
35 | };
36 |
37 | //=========//
38 | // SCREENS //
39 | //=========//
40 | export const removeAllScreens = (colour) => {
41 | colour.screens.length = 0;
42 | };
43 |
44 | export const removeScreensSet = (colour, screensSet) => {
45 | colour.screens = colour.screens.filter((screen) => !screensSet.has(screen));
46 | };
47 |
48 | export const removeScreen = (colour, screen) => {
49 | colour.screens = colour.screens.filter((s) => s !== screen);
50 | };
51 |
52 | export const removeScreenNumber = (colour, number) => {
53 | colour.screens.splice(number, 1);
54 | };
55 |
56 | export const removeScreenAddress = (address) => {
57 | const { colour, number } = address;
58 | removeScreenNumber(colour, number);
59 | };
60 |
61 | export const addScreen = (colour, screen) => {
62 | const number = colour.screens.push(screen) - 1;
63 | return number;
64 | };
65 |
66 | export const setScreenNumber = (colour, number, screen) => {
67 | colour.screens[number] = screen;
68 | };
69 |
70 | export const rotateScreenNumber = (colour, number, angle) => {
71 | const screen = colour.screens[number];
72 | screen.corners = getRotatedCorners(screen.corners, angle);
73 | };
74 |
75 | // Corrects a provided route too
76 | export const moveAddressToBack = (address, route = undefined) => {
77 | const { colour, number } = address;
78 | const screen = colour.screens[number];
79 | removeScreenAddress(address);
80 | const newNumber = addScreen(colour, screen);
81 | const newAddress = makeAddress(colour, newNumber);
82 | if (route === undefined) {
83 | return newAddress;
84 | }
85 |
86 | const { start, steps } = route;
87 | let routeScreen = start;
88 |
89 | const newRoute = makeRoute(start);
90 |
91 | for (const step of steps) {
92 | let stepNumber = step.item;
93 | if (routeScreen.colour === colour) {
94 | if (stepNumber === number) {
95 | stepNumber = newNumber;
96 | } else if (stepNumber > number) {
97 | stepNumber--;
98 | }
99 | }
100 | addStep(newRoute, stepNumber);
101 | routeScreen = routeScreen.colour.screens[stepNumber];
102 | }
103 |
104 | return [newAddress, newRoute];
105 | };
106 |
107 | // This could be cached if a performance boost is needed
108 | export const getColourParents = (childColour, colours) => {
109 | const parents = [];
110 | for (const colour of colours) {
111 | let i = 0;
112 | for (const screen of colour.screens) {
113 | if (screen.colour === childColour) {
114 | const inverseCorners = getMappedPositions(VIEW_CORNERS, screen.corners);
115 | const parent = makeScreen(colour, inverseCorners);
116 | parent.number = i;
117 | parents.push(parent);
118 | }
119 | i++;
120 | }
121 | }
122 | return parents;
123 | };
124 |
--------------------------------------------------------------------------------
/source/corners.js:
--------------------------------------------------------------------------------
1 | import {
2 | addVector,
3 | scaleVector,
4 | distanceBetweenVectors,
5 | subtractVector,
6 | angleBetweenVectors,
7 | } from "./vector.js";
8 | import { getRotatedPosition } from "./position.js";
9 |
10 | //=========//
11 | // CORNERS //
12 | //=========//
13 | // a b
14 | // c d
15 | export const makeRectangleCorners = (x, y, width, height) => {
16 | const a = [x, y];
17 | const b = [x + width, y];
18 | const c = [x, y + height];
19 | const d = [x + width, y + height];
20 | const corners = [a, b, c, d];
21 | return corners;
22 | };
23 |
24 | export const VIEW_CORNERS = makeRectangleCorners(0, 0, 1, 1);
25 |
26 | export const getRotatedCorners = (corners, angle) => {
27 | const center = getCornersCenter(corners);
28 | const rotatedCorners = corners.map((corner) =>
29 | getRotatedPosition(corner, center, angle)
30 | );
31 | return rotatedCorners;
32 | };
33 |
34 | export const getCornersCenter = (corners) => {
35 | const sum = corners.reduce((a, b) => addVector(a, b));
36 | const center = scaleVector(sum, 1 / 4);
37 | return center;
38 | };
39 |
40 | export const getMovedCorners = (corners, displacement) => {
41 | const movedCorners = corners.map((corner) => addVector(corner, displacement));
42 | return movedCorners;
43 | };
44 |
45 | export const getPositionedCorners = (corners, position) => {
46 | const [a] = corners;
47 | const displacement = subtractVector(position, a);
48 | const movedCorners = getMovedCorners(corners, displacement);
49 | return movedCorners;
50 | };
51 |
52 | export const getCornersPerimeter = (corners) => {
53 | const [a, b, c, d] = corners;
54 | const ab = distanceBetweenVectors(a, b);
55 | const bd = distanceBetweenVectors(b, d);
56 | const dc = distanceBetweenVectors(d, c);
57 | const ca = distanceBetweenVectors(c, a);
58 | const perimeter = ab + bd + dc + ca;
59 | return perimeter;
60 | };
61 |
62 | export const getZeroedCorners = (corners) => {
63 | const [a] = corners;
64 | const [ax, ay] = a;
65 | const zeroedCorners = corners.map(([x, y]) => [x - ax, y - ay]);
66 | return zeroedCorners;
67 | };
68 |
69 | export const getCornersPosition = (corners, number = 0) => {
70 | const corner = corners[number];
71 | const position = [...corner];
72 | return position;
73 | };
74 |
75 | export const getClonedCorners = (corners) => {
76 | const clonedCorners = corners.map((corner) => [...corner]);
77 | return clonedCorners;
78 | };
79 |
80 | export const getSubtractedCorners = (a, b) => {
81 | const differences = [];
82 | for (let i = 0; i < 4; i++) {
83 | const difference = subtractVector(a[i], b[i]);
84 | differences.push(difference);
85 | }
86 | return differences;
87 | };
88 |
89 | export const getAddedCorners = (a, b) => {
90 | const totals = [];
91 | for (let i = 0; i < 4; i++) {
92 | const total = addVector(a[i], b[i]);
93 | totals.push(total);
94 | }
95 | return totals;
96 | };
97 |
98 | export const getRotatedToPositionCorners = (corners, number, position) => {
99 | const center = getCornersCenter(corners);
100 | const distances = corners.map((corner) =>
101 | distanceBetweenVectors(center, corner)
102 | );
103 | const angles = corners.map((corner) => angleBetweenVectors(center, corner));
104 |
105 | const oldDistance = distances[number];
106 | const oldAngle = angles[number];
107 |
108 | const newDistance = distanceBetweenVectors(center, position);
109 | const newAngle = angleBetweenVectors(center, position);
110 |
111 | const mdistance = newDistance / oldDistance;
112 | const dangle = newAngle - oldAngle;
113 |
114 | const newDistances = distances.map((distance) => distance * mdistance);
115 | const newAngles = angles.map((angle) => angle + dangle);
116 |
117 | const rotatedCorners = corners.map((corner, i) => {
118 | const x = Math.cos(newAngles[i]) * newDistances[i];
119 | const y = Math.sin(newAngles[i]) * newDistances[i];
120 | return subtractVector(center, [x, y]);
121 | });
122 |
123 | //const rotatedCorners = getClonedCorners(corners)
124 | //rotatedCorners[number] = position
125 | return rotatedCorners;
126 | };
127 |
--------------------------------------------------------------------------------
/source/draw.js:
--------------------------------------------------------------------------------
1 | import { getCanvasPositions, getRelativePositions } from "./position.js";
2 | import { makeScreen } from "./screen.js";
3 |
4 | //======//
5 | // DRAW //
6 | //======//
7 | // This file contains primitive + agnostic drawing functions
8 | // For higher-level drawing functions, go to 'colour.js'
9 | export const SCREEN_BORDER_WIDTH = 2;
10 | export const drawBorder = (context, screen) => {
11 | const { colour, corners } = screen;
12 | // fillBackground(context, { colour: Colour.Black, corners });
13 |
14 | const canvasCornerPositions = getCanvasPositions(context, corners);
15 | const [a, b, c, d] = canvasCornerPositions;
16 |
17 | let { depth } = screen;
18 |
19 | // console.log(depth);
20 | const overflow = depth - 500;
21 | if (overflow > 0) {
22 | const alpha = 1 - overflow / 500;
23 | if (alpha < 0.01) return;
24 | context.globalAlpha = 1 - overflow / 500;
25 | }
26 | // if (context.globalAlpha <= 0.1) return;
27 | context.beginPath();
28 | context.moveTo(...a);
29 | context.lineTo(...b);
30 | context.lineTo(...d);
31 | context.lineTo(...c);
32 | context.closePath();
33 |
34 | // context.fillStyle = "#232940aa";
35 | context.fillStyle = "#232940";
36 | context.fill();
37 |
38 | context.lineWidth = SCREEN_BORDER_WIDTH;
39 | context.strokeStyle = colour.hex;
40 | // context.strokeStyle = "#232940aa";
41 | context.stroke();
42 |
43 | if (screen.parent) {
44 | // const canvasParentPositions = getCanvasPositions(
45 | // context,
46 | // screen.parent.corners
47 | // );
48 | // const [pa] = canvasParentPositions;
49 | // context.beginPath();
50 | // context.moveTo(...pa);
51 | // context.lineTo(...a);
52 | // context.closePath();
53 | // context.lineWidth = SCREEN_BORDER_WIDTH;
54 | // context.strokeStyle = "white";
55 | // context.stroke();
56 | }
57 |
58 | if (window.debugCorners) {
59 | context.fillStyle = "#ffffff99";
60 | context.font = `${30 / devicePixelRatio}px Arial`;
61 | context.fillText(
62 | `${corners[0][0].toFixed(2)}, ${corners[0][1].toFixed(2)}`,
63 | a[0] + 10 / devicePixelRatio,
64 | a[1] - 10 / devicePixelRatio
65 | );
66 |
67 | context.fillText(
68 | `${corners[1][0].toFixed(2)}, ${corners[1][1].toFixed(2)}`,
69 | b[0] + 10 / devicePixelRatio,
70 | b[1] - 10 / devicePixelRatio
71 | );
72 |
73 | context.fillText(
74 | `${corners[2][0].toFixed(2)}, ${corners[2][1].toFixed(2)}`,
75 | c[0] + 10 / devicePixelRatio,
76 | c[1] - 10 / devicePixelRatio
77 | );
78 |
79 | context.fillText(
80 | `${corners[3][0].toFixed(2)}, ${corners[3][1].toFixed(2)}`,
81 | d[0] + 10 / devicePixelRatio,
82 | d[1] - 10 / devicePixelRatio
83 | );
84 |
85 | context.beginPath();
86 | context.arc(a[0], a[1], 8 / devicePixelRatio, 0, 2 * Math.PI);
87 | context.fill();
88 |
89 | context.beginPath();
90 | context.arc(b[0], b[1], 8 / devicePixelRatio, 0, 2 * Math.PI);
91 | context.fill();
92 |
93 | context.beginPath();
94 | context.arc(c[0], c[1], 8 / devicePixelRatio, 0, 2 * Math.PI);
95 | context.fill();
96 |
97 | context.beginPath();
98 | context.arc(d[0], d[1], 8 / devicePixelRatio, 0, 2 * Math.PI);
99 | context.fill();
100 | }
101 | if (overflow > 0) {
102 | context.globalAlpha = 1;
103 | }
104 | };
105 |
106 | addEventListener("keydown", (e) => {
107 | if (e.key === "/") {
108 | window.debugCorners = !window.debugCorners;
109 | }
110 | });
111 |
112 | //=======//
113 | // QUEUE //
114 | //=======//
115 | export const clearQueue = (context, queue, world) => {
116 | const { canvas } = context;
117 | context.clearRect(0, 0, canvas.width, canvas.height);
118 |
119 | const { colour } = world;
120 | const screen = makeScreen(colour, world.corners);
121 | queue.clear();
122 | queue.push({ ...screen, depth: 0, parent: null });
123 | };
124 |
125 | export const addChildrenToQueue = (queue, parent) => {
126 | let i = 1;
127 | const { colour, corners } = parent;
128 | for (let c = colour.screens.length - 1; c >= 0; c--) {
129 | const child = colour.screens[c];
130 | const relativeCorners = getRelativePositions(child.corners, corners);
131 | const screen = makeScreen(child.colour, relativeCorners);
132 | const depth = parent.depth + 1;
133 | if (depth < 1000) {
134 | queue.push({ ...screen, depth: parent.depth + 1, parent });
135 | }
136 | i++;
137 | }
138 | return i;
139 | };
140 |
141 | // export const DRAW_COUNT = 2_000;
142 | export const continueDrawingQueue = (context, queue) => {
143 | // If the draw queue is empty, that means we've drawn everything already :)
144 | if (queue.isEmpty) {
145 | return;
146 | }
147 |
148 | const drawCount = window.debugCorners ? 2_000 : 4_000;
149 | let i = 0;
150 | while (!queue.isEmpty) {
151 | if (i >= drawCount) break;
152 | const screen = queue.shift();
153 | drawBorder(context, screen);
154 | i += addChildrenToQueue(queue, screen);
155 | }
156 | };
157 |
--------------------------------------------------------------------------------
/source/global.js:
--------------------------------------------------------------------------------
1 | import { makeHand } from "./hand.js";
2 | import { makeColours } from "./colour.js";
3 | import { makeWorld } from "./world.js";
4 | import { LinkedList } from "./list.js";
5 | import { makeZoomer } from "./zoom.js";
6 |
7 | //========//
8 | // GLOBAL //
9 | //========//
10 | const colours = makeColours();
11 | const hand = makeHand(colours);
12 | const world = makeWorld(colours, GREY);
13 | const queue = new LinkedList();
14 | const show = Show.start();
15 | const zoomer = makeZoomer();
16 | const update = () => {};
17 |
18 | export const global = {
19 | // Updating
20 | world,
21 | colours,
22 | update,
23 |
24 | // Drawing
25 | show,
26 | queue,
27 |
28 | // Interaction
29 | hand,
30 | zoomer,
31 | };
32 |
33 | window.global = global;
34 |
--------------------------------------------------------------------------------
/source/hand.js:
--------------------------------------------------------------------------------
1 | import {
2 | getMappedPosition,
3 | getMappedPositions,
4 | getMousePosition,
5 | getRelativePositions,
6 | getScaledPosition,
7 | } from "./position.js";
8 | import {
9 | makeRectangleCorners,
10 | getPositionedCorners,
11 | getCornersPosition,
12 | VIEW_CORNERS,
13 | getMovedCorners,
14 | getClonedCorners,
15 | getSubtractedCorners,
16 | getAddedCorners,
17 | getRotatedToPositionCorners,
18 | } from "./corners.js";
19 | import { makeScreen } from "./screen.js";
20 | import {
21 | pickInScreen,
22 | placeScreen,
23 | replaceAddress,
24 | tryToSurroundScreens,
25 | } from "./pick.js";
26 | import { subtractVector, addVector, scaleVector } from "./vector.js";
27 | import { clearQueue } from "./draw.js";
28 | import { onkeydown } from "./keyboard.js";
29 | import { getEdgeCorners, PART_TYPE } from "./part.js";
30 | import {
31 | areRoutesEqual,
32 | getAddressedScreenFromRoute,
33 | getDrawnScreenFromRoute,
34 | } from "./route.js";
35 | import {
36 | areAddressesEqual,
37 | getScreenFromAddress,
38 | makeAddress,
39 | } from "./address.js";
40 | import { moveAddressToBack, removeScreenAddress } from "./colour.js";
41 | import { setWorldCorners } from "./world.js";
42 | import { wrap } from "./number.js";
43 |
44 | //======//
45 | // HAND //
46 | //======//
47 | export const makeHand = (colours) => ({
48 | state: HAND_STATE.START,
49 | cursor: HAND_STATE.START.cursor,
50 | colour: colours[GREEN],
51 | hidden: false,
52 |
53 | // What is the hand holding?
54 | pick: undefined,
55 | selectedAddress: undefined,
56 |
57 | // Where is the hand coming from?
58 | handStart: [undefined, undefined],
59 | pickStart: [undefined, undefined],
60 | startAddressedScreen: undefined,
61 | startDrawnParent: undefined,
62 | hasChangedParent: false,
63 | });
64 |
65 | const HAND_STATE = {};
66 | export const fireHandEvent = (context, hand, eventName, args = {}) => {
67 | let oldState = hand.state;
68 | let newState = hand.state;
69 |
70 | // Keep firing state's events until the state stops changing
71 | do {
72 | oldState = newState;
73 | const event = oldState[eventName];
74 | if (event === undefined) break;
75 | newState = event({ context, hand, ...args });
76 | } while (oldState !== newState);
77 |
78 | // Update cursor if we need to
79 | if (newState.cursor !== hand.cursor) {
80 | context.canvas.style["cursor"] = newState.cursor;
81 | hand.cursor = newState.cursor;
82 | }
83 |
84 | if (hand.hidden) {
85 | context.canvas.style["cursor"] = "crosshair";
86 | }
87 |
88 | hand.state = newState;
89 | };
90 |
91 | export const registerRightClick = () => {
92 | on.contextmenu((e) => e.preventDefault(), { passive: false });
93 | };
94 |
95 | export const registerDeleteKey = (hand) => {
96 | function deleteSelectedScreen() {
97 | if (hand.selectedAddress === undefined) return;
98 | removeScreenAddress(hand.selectedAddress);
99 | hand.selectedAddress = undefined;
100 | clearQueue(global.show.context, global.queue, global.world);
101 | }
102 | onkeydown("Delete", deleteSelectedScreen);
103 | onkeydown("Backspace", deleteSelectedScreen);
104 | };
105 |
106 | //==========//
107 | // KEYBOARD //
108 | //==========//
109 | export const registerColourPickers = (hand, hexes, colours) => {
110 | for (let i = 0; i < hexes.length; i++) {
111 | const hex = hexes[i];
112 | onkeydown(`${i + 1}`, () => (hand.colour = colours[hex]));
113 | }
114 |
115 | onkeydown("q", () => (hand.hidden = !hand.hidden));
116 | };
117 |
118 | //========//
119 | // STATES //
120 | //========//
121 | HAND_STATE.START = {
122 | cursor: "default",
123 | tick: () => HAND_STATE.FREE,
124 | };
125 |
126 | const HAND_PICK_BRUTE_FORCE_DEPTH = 10;
127 | const HAND_MAX_BRUTE_FORCE = 1000;
128 | const HAND_PICK_PITY = [0.006].repeat(2);
129 | HAND_STATE.FREE = {
130 | cursor: "default",
131 | tick: ({ context, hand, world, queue }) => {
132 | //======== HOVER ========//
133 | const mousePosition = getMousePosition(context, VIEW_CORNERS);
134 | const worldMousePosition = getMappedPosition(mousePosition, world.corners);
135 | hand.handStart = mousePosition;
136 |
137 | const pity = HAND_PICK_PITY;
138 | const pick = pickInScreen(world, worldMousePosition, {
139 | pity,
140 | //bruteForceDepth: HAND_PICK_BRUTE_FORCE_DEPTH,
141 | //maxBruteForce: HAND_MAX_BRUTE_FORCE,
142 | safe: false,
143 | });
144 |
145 | hand.pick = pick;
146 |
147 | if (pick.address && pick.part.type === PART_TYPE.EDGE) {
148 | HAND_STATE.FREE.cursor = "move";
149 | } else if (pick.address && pick.part.type === PART_TYPE.CORNER) {
150 | HAND_STATE.FREE.cursor = "pointer";
151 | } else if (
152 | !pick.address &&
153 | (pick.part.type === PART_TYPE.EDGE || pick.part.type === PART_TYPE.CORNER)
154 | ) {
155 | HAND_STATE.FREE.cursor = "pointer";
156 | } else {
157 | HAND_STATE.FREE.cursor = "default";
158 | }
159 |
160 | if (Mouse.Left) {
161 | //======== MOVE ========//
162 | if (pick.address && pick.part.type === PART_TYPE.EDGE) {
163 | const [newAddress, newRoute] = moveAddressToBack(
164 | pick.address,
165 | pick.route
166 | );
167 | hand.pick.address = newAddress;
168 | hand.pick.route = newRoute;
169 |
170 | hand.startCorners = getDrawnScreenFromRoute(pick.route).corners;
171 | hand.startPosition = getCornersPosition(hand.startCorners);
172 | hand.startDrawnParent = getDrawnScreenFromRoute(
173 | pick.route,
174 | pick.route.length - 2
175 | );
176 |
177 | hand.hasChangedParent = false;
178 | hand.selectedAddress = hand.pick.address;
179 | return HAND_STATE.MOVING;
180 |
181 | //======== ROTATE + SCALE ========//
182 | } else if (pick.address && pick.part.type === PART_TYPE.CORNER) {
183 | const [newAddress, newRoute] = moveAddressToBack(
184 | pick.address,
185 | pick.route
186 | );
187 | hand.pick.address = newAddress;
188 | hand.pick.route = newRoute;
189 |
190 | hand.startCorners = getDrawnScreenFromRoute(pick.route).corners;
191 | hand.startPositions = getClonedCorners(hand.startCorners); //probably don't need to clone but let's do it just in case
192 | hand.startDrawnParent = getDrawnScreenFromRoute(
193 | pick.route,
194 | pick.route.length - 2
195 | );
196 |
197 | hand.hasChangedParent = false;
198 | hand.selectedAddress = hand.pick.address;
199 | return HAND_STATE.ROTATING;
200 | }
201 |
202 | //======== DRAW ========//
203 | const [x, y] = mousePosition;
204 | const corners = makeRectangleCorners(x, y, 0, 0);
205 | const screen = makeScreen(hand.colour, corners);
206 |
207 | hand.pick = placeScreen(screen, world);
208 | hand.pickStart = getCornersPosition(hand.pick.screen.corners);
209 |
210 | hand.selectedAddress = hand.pick.address;
211 | return HAND_STATE.DRAWING;
212 | } else if (Mouse.Right) {
213 | //======== WARP ========//
214 | if (pick.address && pick.part.type === PART_TYPE.CORNER) {
215 | const [newAddress, newRoute] = moveAddressToBack(
216 | pick.address,
217 | pick.route
218 | );
219 | hand.pick.address = newAddress;
220 | hand.pick.route = newRoute;
221 |
222 | hand.startCorners = getDrawnScreenFromRoute(pick.route).corners;
223 | hand.startPosition = getCornersPosition(
224 | hand.startCorners,
225 | hand.pick.part.number
226 | );
227 | hand.startDrawnParent = getDrawnScreenFromRoute(
228 | pick.route,
229 | pick.route.length - 2
230 | );
231 |
232 | hand.hasChangedParent = false;
233 | hand.selectedAddress = hand.pick.address;
234 | return HAND_STATE.WARPING;
235 |
236 | //======== STRETCH ========//
237 | } else if (pick.address && pick.part.type === PART_TYPE.EDGE) {
238 | const [newAddress, newRoute] = moveAddressToBack(
239 | pick.address,
240 | pick.route
241 | );
242 | hand.pick.address = newAddress;
243 | hand.pick.route = newRoute;
244 |
245 | hand.startCorners = getDrawnScreenFromRoute(pick.route).corners;
246 | const [a, b] = getEdgeCorners(hand.pick.part.number);
247 | hand.startPosition1 = getCornersPosition(hand.startCorners, a);
248 | hand.startPosition2 = getCornersPosition(hand.startCorners, b);
249 | hand.startDrawnParent = getDrawnScreenFromRoute(
250 | pick.route,
251 | pick.route.length - 2
252 | );
253 |
254 | hand.hasChangedParent = false;
255 | hand.selectedAddress = hand.pick.address;
256 | return HAND_STATE.STRETCHING;
257 | }
258 | } else if (Mouse.Middle || Keyboard["m"]) {
259 | //======== COLOUR ========//
260 | const addressedScreen = getScreenFromAddress(hand.pick.address, world);
261 | hand.selectedAddress = hand.pick.address;
262 | if (addressedScreen.colour !== hand.colour) {
263 | addressedScreen.colour = hand.colour;
264 | clearQueue(context, queue, world);
265 | }
266 | return HAND_STATE.COLOURING;
267 | }
268 |
269 | return HAND_STATE.FREE;
270 | },
271 | };
272 |
273 | HAND_STATE.COLOURING = {
274 | cursor: "default",
275 | tick: () => {
276 | if (Mouse.Middle || Keyboard["m"]) {
277 | return HAND_STATE.COLOURING;
278 | }
279 |
280 | return HAND_STATE.FREE;
281 | },
282 | };
283 |
284 | HAND_STATE.MOVING = {
285 | cursor: "move",
286 | tick: ({ context, hand, world, queue, colours }) => {
287 | const { pick } = hand;
288 |
289 | // Remember some stuff for after the move
290 | const oldDrawnParent = hand.startDrawnParent;
291 |
292 | // Work out mouse movement
293 | const mousePosition = getMousePosition(context, VIEW_CORNERS);
294 | const movement = subtractVector(mousePosition, hand.handStart);
295 |
296 | // Work out screen movement
297 | const movedPosition = addVector(hand.startPosition, movement);
298 | const movedCorners = getPositionedCorners(hand.startCorners, movedPosition);
299 |
300 | // Replace screen with moved screen
301 | const movedScreen = makeScreen(pick.screen.colour, movedCorners);
302 | const newPick = replaceAddress({
303 | address: pick.address,
304 | screen: movedScreen,
305 | target: Keyboard[" "] ? pick.parent : world,
306 | parent: pick.parent,
307 | depth: pick.depth,
308 | });
309 |
310 | pick.address = newPick.address;
311 | pick.parent = newPick.parent;
312 | pick.depth = newPick.depth;
313 | hand.selectedAddress = pick.address;
314 |
315 | // Yank the camera
316 | if (!hand.hasChangedParent && newPick.isWithinParent) {
317 | const newDrawnParent = getDrawnScreenFromRoute(
318 | hand.pick.route,
319 | hand.pick.route.length - 2
320 | );
321 | const parentDifferences = getSubtractedCorners(
322 | oldDrawnParent.corners,
323 | newDrawnParent.corners
324 | );
325 | const worldCorners = getAddedCorners(world.corners, parentDifferences);
326 | setWorldCorners(world, worldCorners, colours);
327 | hand.startDrawnParent = getDrawnScreenFromRoute(
328 | hand.pick.route,
329 | hand.pick.route.length - 2
330 | );
331 | hand.startCorners = getDrawnScreenFromRoute(pick.route).corners;
332 | } else {
333 | hand.hasChangedParent = true;
334 | }
335 |
336 | if (!Mouse.Left) {
337 | hand.pick.address = tryToSurroundScreens(hand.pick.address);
338 | hand.selectedAddress = hand.pick.address;
339 | clearQueue(context, queue, world);
340 | return HAND_STATE.FREE;
341 | }
342 |
343 | clearQueue(context, queue, world);
344 | return HAND_STATE.MOVING;
345 | },
346 | };
347 |
348 | HAND_STATE.STRETCHING = {
349 | cursor: "pointer",
350 | tick: ({ context, hand, world, queue, colours }) => {
351 | const { pick } = hand;
352 |
353 | // Work out mouse movement
354 | const mousePosition = getMousePosition(context, VIEW_CORNERS);
355 | const movement = subtractVector(mousePosition, hand.handStart);
356 |
357 | // Work out screen movement
358 | const movedPosition1 = addVector(hand.startPosition1, movement);
359 | const movedPosition2 = addVector(hand.startPosition2, movement);
360 | const movedCorners = getClonedCorners(hand.startCorners);
361 | const [a, b] = getEdgeCorners(pick.part.number);
362 | movedCorners[a] = movedPosition1;
363 | movedCorners[b] = movedPosition2;
364 |
365 | // Replace screen with moved screen
366 | const movedScreen = makeScreen(pick.screen.colour, movedCorners);
367 | const newPick = replaceAddress({
368 | address: pick.address,
369 | screen: movedScreen,
370 | target: Keyboard[" "] ? pick.parent : world,
371 | parent: pick.parent,
372 | depth: pick.depth,
373 | });
374 |
375 | pick.address = newPick.address;
376 | pick.parent = newPick.parent;
377 | pick.depth = newPick.depth;
378 | hand.selectedAddress = pick.address;
379 |
380 | // Yank the camera
381 | if (!hand.hasChangedParent && newPick.isWithinParent) {
382 | const newCorners = getDrawnScreenFromRoute(pick.route).corners;
383 |
384 | const [a, b] = getEdgeCorners(pick.part.number);
385 | const newPosition1 = newCorners[a];
386 | const newPosition2 = newCorners[b];
387 | const difference1 = subtractVector(movedPosition1, newPosition1);
388 | const difference2 = subtractVector(movedPosition2, newPosition2);
389 | const difference = scaleVector(addVector(difference1, difference2), 0.5);
390 | const worldCorners = getMovedCorners(world.corners, difference);
391 | setWorldCorners(world, worldCorners, colours);
392 | hand.startCorners = getDrawnScreenFromRoute(pick.route).corners;
393 | } else {
394 | hand.hasChangedParent = true;
395 | }
396 |
397 | if (!Mouse.Right) {
398 | hand.pick.address = tryToSurroundScreens(hand.pick.address);
399 | hand.selectedAddress = hand.pick.address;
400 | clearQueue(context, queue, world);
401 | return HAND_STATE.FREE;
402 | }
403 |
404 | clearQueue(context, queue, world);
405 | return HAND_STATE.STRETCHING;
406 | },
407 | };
408 |
409 | HAND_STATE.ROTATING = {
410 | cursor: "pointer",
411 | tick: ({ context, hand, world, queue, colours }) => {
412 | const { pick } = hand;
413 |
414 | // Work out mouse movement
415 | const mousePosition = getMousePosition(context, VIEW_CORNERS);
416 | const movement = subtractVector(mousePosition, hand.handStart);
417 |
418 | // Work out screen movement
419 | const movedPosition = addVector(
420 | hand.startPositions[pick.part.number],
421 | movement
422 | );
423 | const movedCorners = getRotatedToPositionCorners(
424 | hand.startCorners,
425 | pick.part.number,
426 | movedPosition
427 | );
428 |
429 | // Replace screen with moved screen
430 | const movedScreen = makeScreen(pick.screen.colour, movedCorners);
431 | const newPick = replaceAddress({
432 | address: pick.address,
433 | screen: movedScreen,
434 | target: Keyboard[" "] ? pick.parent : world,
435 | parent: pick.parent,
436 | depth: pick.depth,
437 | });
438 |
439 | pick.address = newPick.address;
440 | pick.parent = newPick.parent;
441 | pick.depth = newPick.depth;
442 | hand.selectedAddress = pick.address;
443 |
444 | // Yank the camera
445 | if (!hand.hasChangedParent && newPick.isWithinParent) {
446 | const newCorners = getDrawnScreenFromRoute(pick.route).corners;
447 | const newPosition = newCorners[hand.pick.part.number];
448 | const difference = subtractVector(movedPosition, newPosition);
449 | const worldCorners = getMovedCorners(world.corners, difference);
450 | setWorldCorners(world, worldCorners, colours);
451 | hand.startCorners = getDrawnScreenFromRoute(pick.route).corners;
452 | } else {
453 | hand.hasChangedParent = true;
454 | }
455 |
456 | if (!Mouse.Left) {
457 | hand.pick.address = tryToSurroundScreens(hand.pick.address);
458 | hand.selectedAddress = hand.pick.address;
459 | clearQueue(context, queue, world);
460 | return HAND_STATE.FREE;
461 | }
462 |
463 | clearQueue(context, queue, world);
464 | return HAND_STATE.ROTATING;
465 | },
466 | };
467 |
468 | HAND_STATE.WARPING = {
469 | cursor: "pointer",
470 | tick: ({ context, hand, world, queue, colours }) => {
471 | const { pick } = hand;
472 |
473 | // Work out mouse movement
474 | const mousePosition = getMousePosition(context, VIEW_CORNERS);
475 | const movement = subtractVector(mousePosition, hand.handStart);
476 |
477 | // Work out screen movement
478 | const movedPosition = addVector(hand.startPosition, movement);
479 | const movedCorners = getClonedCorners(hand.startCorners);
480 | movedCorners[pick.part.number] = movedPosition;
481 |
482 | // Replace screen with moved screen
483 | const movedScreen = makeScreen(pick.screen.colour, movedCorners);
484 | const newPick = replaceAddress({
485 | address: pick.address,
486 | screen: movedScreen,
487 | target: Keyboard[" "] ? pick.parent : world,
488 | parent: pick.parent,
489 | depth: pick.depth,
490 | });
491 |
492 | pick.address = newPick.address;
493 | pick.parent = newPick.parent;
494 | pick.depth = newPick.depth;
495 | hand.selectedAddress = pick.address;
496 |
497 | // Yank the camera
498 | if (!hand.hasChangedParent && newPick.isWithinParent) {
499 | const newCorners = getDrawnScreenFromRoute(pick.route).corners;
500 | const newPosition = newCorners[hand.pick.part.number];
501 | const difference = subtractVector(movedPosition, newPosition);
502 | const worldCorners = getMovedCorners(world.corners, difference);
503 | setWorldCorners(world, worldCorners, colours);
504 | hand.startCorners = getDrawnScreenFromRoute(pick.route).corners;
505 | } else {
506 | hand.hasChangedParent = true;
507 | }
508 |
509 | if (!Mouse.Right) {
510 | hand.pick.address = tryToSurroundScreens(hand.pick.address);
511 | hand.selectedAddress = hand.pick.address;
512 | clearQueue(context, queue, world);
513 | return HAND_STATE.FREE;
514 | }
515 |
516 | clearQueue(context, queue, world);
517 | return HAND_STATE.WARPING;
518 | },
519 | };
520 |
521 | HAND_STATE.DRAWING = {
522 | cursor: "default",
523 | tick: ({ context, hand, world, queue }) => {
524 | const { pick } = hand;
525 |
526 | // Draw
527 | const mousePosition = getMousePosition(context, VIEW_CORNERS);
528 | const handMovement = subtractVector(mousePosition, hand.handStart);
529 | const [width, height] = handMovement;
530 | const [x, y] = hand.pickStart;
531 | const drawnCorners = makeRectangleCorners(x, y, width, height);
532 | const drawnScreen = makeScreen(pick.screen.colour, drawnCorners);
533 |
534 | // Replace
535 | hand.pick = replaceAddress({
536 | address: pick.address,
537 | screen: drawnScreen,
538 | target: Keyboard[" "] ? pick.parent : world,
539 | parent: pick.parent,
540 | depth: pick.depth,
541 | //bruteForceDepth: HAND_PICK_BRUTE_FORCE_DEPTH,
542 | //maxBruteForce: HAND_MAX_BRUTE_FORCE,
543 | });
544 |
545 | hand.selectedAddress = hand.pick.address;
546 |
547 | if (!Mouse.Left) {
548 | // Check for surrounded screens
549 | hand.pick.address = tryToSurroundScreens(hand.pick.address);
550 | hand.selectedAddress = hand.pick.address;
551 | clearQueue(context, queue, world);
552 | return HAND_STATE.FREE;
553 | }
554 |
555 | clearQueue(context, queue, world);
556 | return HAND_STATE.DRAWING;
557 | },
558 | };
559 |
--------------------------------------------------------------------------------
/source/keyboard.js:
--------------------------------------------------------------------------------
1 | //==========//
2 | // KEYBOARD //
3 | //==========//
4 | const KEYDOWN = {};
5 | on.keydown((e) => {
6 | const { key } = e;
7 | const event = KEYDOWN[key];
8 | if (event === undefined) return;
9 | event(e);
10 | });
11 |
12 | export const onkeydown = (key, func) => (KEYDOWN[key] = func);
13 |
--------------------------------------------------------------------------------
/source/lerp.js:
--------------------------------------------------------------------------------
1 | import { addVector, subtractVector, crossProductVector } from "./vector.js";
2 |
3 | //======//
4 | // LERP //
5 | //======//
6 | export const lerp = (distance, line) => {
7 | const [a, b] = line;
8 | const [ax, ay] = a;
9 | const [bx, by] = b;
10 |
11 | const x = ax + (bx - ax) * distance;
12 | const y = ay + (by - ay) * distance;
13 |
14 | const point = [x, y];
15 | return point;
16 | };
17 |
18 | export const bilerp = (displacement, corners) => {
19 | const [dx, dy] = displacement;
20 | const [a, b, c, d] = corners;
21 |
22 | const la = lerp(dx, [a, b]);
23 | const lb = lerp(dx, [c, d]);
24 | const line = [la, lb];
25 |
26 | const point = lerp(dy, line);
27 | return point;
28 | };
29 |
30 | // based on https://iquilezles.org/articles/ibilinear/
31 | // adapted by Magnogen https://magnogen.net/
32 | export const ibilerp = (point, corners) => {
33 | const p = point;
34 | const [a, b, d, c] = corners;
35 |
36 | const e = subtractVector(b, a);
37 | const f = subtractVector(d, a);
38 | const g = addVector(subtractVector(a, b), subtractVector(c, d));
39 | const h = subtractVector(p, a);
40 |
41 | const k2 = crossProductVector(g, f);
42 | const k1 = crossProductVector(e, f) + crossProductVector(h, g);
43 | const k0 = crossProductVector(h, e);
44 |
45 | // If edges are parallel, this is a linear equation
46 | if (Math.abs(k2) < 0.0001) {
47 | const x = (h[0] * k1 + f[0] * k0) / (e[0] * k1 - g[0] * k0);
48 | const y = -k0 / k1;
49 | return [x, y];
50 | }
51 |
52 | // Otherwise, it's a quadratic
53 | let w = k1 * k1 - 4 * k0 * k2;
54 | w = Math.sqrt(w);
55 |
56 | const ik2 = 0.5 / k2;
57 | let v = (-k1 - w) * ik2;
58 | let u = (h[0] - f[0] * v) / (e[0] + g[0] * v);
59 |
60 | if (u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0) {
61 | v = (-k1 + w) * ik2;
62 | u = (h[0] - f[0] * v) / (e[0] + g[0] * v);
63 | }
64 |
65 | return [u, v];
66 | };
67 |
--------------------------------------------------------------------------------
/source/list.js:
--------------------------------------------------------------------------------
1 | //======//
2 | // LIST //
3 | //======//
4 | export class LinkedList {
5 | constructor(iterable = []) {
6 | this.start = undefined;
7 | this.end = undefined;
8 | this.isEmpty = true;
9 |
10 | for (const item of iterable) {
11 | this.push(item);
12 | }
13 | }
14 |
15 | *[Symbol.iterator]() {
16 | let link = this.start;
17 | while (link !== undefined) {
18 | yield link;
19 | link = link.next;
20 | }
21 | }
22 |
23 | push(item) {
24 | const link = makeLink(item);
25 | if (this.isEmpty) {
26 | this.start = link;
27 | this.end = link;
28 | this.isEmpty = false;
29 | } else {
30 | this.end.next = link;
31 | link.previous = this.end;
32 | this.end = link;
33 | }
34 | }
35 |
36 | pop() {
37 | if (this.isEmpty) {
38 | return undefined;
39 | }
40 |
41 | const item = this.start.item;
42 | if (this.start === this.end) {
43 | this.clear();
44 | return item;
45 | }
46 |
47 | this.end = this.end.previous;
48 | this.end.next = undefined;
49 | return item;
50 | }
51 |
52 | shift() {
53 | if (this.isEmpty) {
54 | return undefined;
55 | }
56 |
57 | const item = this.start.item;
58 | if (this.start === this.end) {
59 | this.clear();
60 | return item;
61 | }
62 |
63 | this.start = this.start.next;
64 | this.start.previous = undefined;
65 | return item;
66 | }
67 |
68 | clear() {
69 | this.start = undefined;
70 | this.end = undefined;
71 | this.isEmpty = true;
72 | }
73 |
74 | setStart(link) {
75 | this.start = link;
76 | link.previous = undefined;
77 | }
78 | }
79 |
80 | const makeLink = (item) => {
81 | const previous = undefined;
82 | const next = undefined;
83 | const link = { item, previous, next };
84 | return link;
85 | };
86 |
--------------------------------------------------------------------------------
/source/main.js:
--------------------------------------------------------------------------------
1 | import { global } from "./global.js";
2 | import {
3 | fireHandEvent,
4 | registerColourPickers,
5 | registerDeleteKey,
6 | registerRightClick,
7 | } from "./hand.js";
8 | import {
9 | getPresetFromCurrentState,
10 | loadPreset,
11 | loadPresetName,
12 | } from "./preset.js";
13 | import { clearQueue, continueDrawingQueue } from "./draw.js";
14 | import { COLOUR_HEXES } from "./colour.js";
15 | import { registerMouseWheel, updateZoom } from "./zoom.js";
16 | import {
17 | addPondiverseButton,
18 | fetchPondiverseCreation,
19 | } from "https://www.pondiverse.com/pondiverse.js";
20 |
21 | //======//
22 | // MAIN //
23 | //======//
24 | const { show } = global;
25 | show.resize = (context) => {
26 | // Oversize the canvas to ensure a square canvas (messy fix for dodgy rotation)
27 | const { canvas } = context;
28 | const { width, height } = canvas;
29 | const max = Math.max(width, height);
30 | canvas.width = max;
31 | canvas.height = max;
32 | canvas.style["width"] = max;
33 | canvas.style["height"] = max;
34 |
35 | const { queue, world } = global;
36 | clearQueue(context, queue, world);
37 | show.tick(context);
38 | };
39 |
40 | show.tick = () => {
41 | const { update } = global;
42 | update(global);
43 | };
44 |
45 | show.supertick = (context) => {
46 | const { queue, hand, zoomer, world, colours } = global;
47 | fireHandEvent(context, hand, "tick", global);
48 | updateZoom(context, queue, zoomer, world, colours);
49 | continueDrawingQueue(context, queue);
50 | };
51 |
52 | registerColourPickers(global.hand, COLOUR_HEXES, global.colours);
53 | registerMouseWheel(global.zoomer);
54 | registerRightClick();
55 | registerDeleteKey(global.hand);
56 |
57 | const urlSearchParams = new URLSearchParams(window.location.search);
58 | const pondiverseCreationId = urlSearchParams.get("creation");
59 | if (pondiverseCreationId) {
60 | fetchPondiverseCreation(pondiverseCreationId).then((creation) => {
61 | const data = JSON.parse(creation.data);
62 | loadPreset(global, data);
63 | });
64 | } else {
65 | loadPresetName(global, "EMPTY");
66 | // loadPresetName(global, "TREE");
67 | }
68 |
69 | addPondiverseButton(() => {
70 | const canvas = document.querySelector("canvas");
71 | const preset = getPresetFromCurrentState();
72 | return {
73 | type: "screenpond",
74 | data: JSON.stringify(preset),
75 | image: canvas?.toDataURL("image/png"),
76 | };
77 | });
78 |
--------------------------------------------------------------------------------
/source/number.js:
--------------------------------------------------------------------------------
1 | //========//
2 | // NUMBER //
3 | //========//
4 | export const wrap = (number, min, max) => {
5 | if (number < min) return min;
6 | if (number > max) return max;
7 | return number;
8 | };
9 |
--------------------------------------------------------------------------------
/source/part.js:
--------------------------------------------------------------------------------
1 | import { wrap } from "./number.js";
2 |
3 | //======//
4 | // PART //
5 | //======//
6 | // Corners
7 | // 0 1
8 | // 2 3
9 | //
10 | // Edges
11 | // 0
12 | // 1 2
13 | // 3
14 | export const getEdgeCorners = (edgeNumber) => {
15 | const unwrappedCornerNumbers = [edgeNumber - 1, edgeNumber + 1];
16 | const cornerNumbers = unwrappedCornerNumbers.map((v) => wrap(v, 0, 3));
17 | return cornerNumbers;
18 | };
19 |
20 | export const PART_TYPE = {
21 | OUTSIDE: Symbol("PART_TYPE.OUTSIDE"),
22 | INSIDE: Symbol("PART_TYPE.INSIDE"),
23 | EDGE: Symbol("PART_TYPE.EDGE"),
24 | CORNER: Symbol("PART_TYPE.CORNER"),
25 | UNKNOWN: Symbol("PART_TYPE.UNKNOWN"),
26 | IGNORE: Symbol("PART_TYPE.IGNORE"),
27 | };
28 |
29 | export const makePart = (type, number = 0) => {
30 | return { type, number };
31 | };
32 |
33 | export const PART_OUTSIDE_PITY_SCALE = 1.0;
34 | export const getMappedPositionPart = (position, pity = [0, 0]) => {
35 | const [x, y] = position;
36 | const [px, py] = pity.map((axis) => Math.min(0.25, axis));
37 |
38 | if (x <= 0.0 - px * PART_OUTSIDE_PITY_SCALE)
39 | return makePart(PART_TYPE.OUTSIDE);
40 | if (x >= 1.0 + px * PART_OUTSIDE_PITY_SCALE)
41 | return makePart(PART_TYPE.OUTSIDE);
42 | if (y <= 0.0 - py * PART_OUTSIDE_PITY_SCALE)
43 | return makePart(PART_TYPE.OUTSIDE);
44 | if (y >= 1.0 + py * PART_OUTSIDE_PITY_SCALE)
45 | return makePart(PART_TYPE.OUTSIDE);
46 |
47 | // Left Edge
48 | if (x <= 0.0 + px) {
49 | if (y <= 0.0 + py) return makePart(PART_TYPE.CORNER, 0);
50 | if (y >= 1.0 - py) return makePart(PART_TYPE.CORNER, 2);
51 | return makePart(PART_TYPE.EDGE, 1);
52 | }
53 |
54 | // Right Edge
55 | if (x >= 1.0 - px) {
56 | if (y <= 0.0 + py) return makePart(PART_TYPE.CORNER, 1);
57 | if (y >= 1.0 - py) return makePart(PART_TYPE.CORNER, 3);
58 | return makePart(PART_TYPE.EDGE, 2);
59 | }
60 |
61 | // Top + Bottom Edges
62 | if (y <= 0.0 + py) return makePart(PART_TYPE.EDGE, 0);
63 | if (y >= 1.0 - py) return makePart(PART_TYPE.EDGE, 3);
64 |
65 | return makePart(PART_TYPE.INSIDE);
66 | };
67 |
--------------------------------------------------------------------------------
/source/pick.js:
--------------------------------------------------------------------------------
1 | import {
2 | getMappedPosition,
3 | getRelativePositions,
4 | getRelativePosition,
5 | getScaledPosition,
6 | getMappedPositions,
7 | isMappedPositionInCorners,
8 | } from "./position.js";
9 | import { makeScreen } from "./screen.js";
10 | import { PART_TYPE, getMappedPositionPart } from "./part.js";
11 | import {
12 | addScreen,
13 | removeScreenAddress,
14 | removeScreensSet,
15 | setScreenNumber,
16 | } from "./colour.js";
17 | import {
18 | getAddressFromScreen,
19 | getScreenFromAddress,
20 | makeAddress,
21 | } from "./address.js";
22 | import { addStep, makeRoute, popStep } from "./route.js";
23 |
24 | //======//
25 | // PICK //
26 | //======//
27 | // A pick
28 | const makePick = ({
29 | screen,
30 | corners,
31 | position,
32 | part,
33 | parent,
34 | number,
35 | depth,
36 | address,
37 | route,
38 | ...data
39 | } = {}) => {
40 | if (address === undefined && parent !== undefined && number !== undefined) {
41 | address = makeAddress(parent.colour, number);
42 | }
43 |
44 | const pick = {
45 | screen,
46 | corners,
47 | position,
48 | part,
49 | parent,
50 | number,
51 | depth,
52 | address,
53 | route,
54 | ...data,
55 | };
56 | return pick;
57 | };
58 |
59 | export const pickInScreen = (screen, position, options = {}) => {
60 | // Options
61 | let {
62 | parent = undefined,
63 | pity = [0, 0],
64 | depth = 0,
65 | maxDepth = 1000,
66 | ignore = undefined,
67 | ignoreDepth = undefined,
68 | part = undefined,
69 | number = undefined,
70 | snap = undefined,
71 | address = undefined,
72 | route = undefined,
73 | bruteForceDepth = 0,
74 | maxBruteForce = Infinity,
75 | safe = true,
76 | } = options;
77 |
78 | // Check if this screen is the one we want to snap to!
79 | let snapped = false;
80 | if (snap !== undefined && address !== undefined) {
81 | const addressedScreen = getScreenFromAddress(address);
82 | if (snap === addressedScreen) {
83 | snapped = true;
84 | }
85 | }
86 |
87 | // Keep track of the route we go on
88 | if (route === undefined) {
89 | const start = screen;
90 | route = makeRoute(start);
91 | }
92 |
93 | // Look through all this screen's children
94 | if (!snapped && depth < maxDepth) {
95 | let i = -1;
96 | for (const child of screen.colour.screens) {
97 | i++;
98 |
99 | if (
100 | child === ignore &&
101 | (ignoreDepth === undefined || ignoreDepth === depth + 1)
102 | ) {
103 | continue;
104 | }
105 |
106 | const scaledPity = getScaledPosition(pity, child.corners).map((axis) =>
107 | Math.abs(axis)
108 | );
109 | const mappedPosition = getMappedPosition(position, child.corners, safe);
110 | if (mappedPosition.some((axis) => isNaN(axis))) {
111 | continue;
112 | }
113 | const childPart = getMappedPositionPart(mappedPosition, scaledPity);
114 |
115 | if (childPart.type === PART_TYPE.OUTSIDE) {
116 | if (bruteForceDepth <= 0) continue;
117 | if (maxBruteForce <= 0) continue;
118 | maxBruteForce--;
119 | const relativeCorners = getRelativePositions(
120 | child.corners,
121 | screen.corners
122 | );
123 | const relativeChild = makeScreen(child.colour, relativeCorners);
124 | addStep(route, i);
125 | const addressedScreen =
126 | address === undefined ? screen : getScreenFromAddress(address);
127 | const result = pickInScreen(relativeChild, mappedPosition, {
128 | ...options,
129 | pity: scaledPity,
130 | parent: addressedScreen,
131 | part: childPart,
132 | number: i,
133 | depth: depth + 1,
134 | address: makeAddress(screen.colour, i),
135 | route,
136 | bruteForceDepth: bruteForceDepth - 1,
137 | maxBruteForce,
138 | });
139 | if (result.part.type !== PART_TYPE.OUTSIDE) {
140 | return result;
141 | }
142 | popStep(route);
143 | continue;
144 | }
145 |
146 | const relativeCorners = getRelativePositions(
147 | child.corners,
148 | screen.corners
149 | );
150 | const relativeChild = makeScreen(child.colour, relativeCorners);
151 |
152 | addStep(route, i);
153 | const addressedScreen =
154 | address === undefined ? screen : getScreenFromAddress(address);
155 |
156 | return pickInScreen(relativeChild, mappedPosition, {
157 | ...options,
158 | pity: scaledPity,
159 | parent: addressedScreen,
160 | part: childPart,
161 | number: i,
162 | depth: depth + 1,
163 | address: makeAddress(screen.colour, i),
164 | route,
165 | maxBruteForce,
166 | });
167 | }
168 | }
169 |
170 | // If we didn't pick any children, pick this screen
171 | let pickedScreen =
172 | parent === undefined ? screen : parent.colour.screens[number];
173 |
174 | // Get the part of the screen
175 | if (part === undefined) part = getMappedPositionPart(position, pity);
176 |
177 | // Collect together the pick information
178 | const pick = makePick({
179 | screen: pickedScreen,
180 | corners: screen.corners,
181 | position,
182 | part,
183 | parent,
184 | number,
185 | depth,
186 | route,
187 | });
188 | return pick;
189 | };
190 |
191 | // Finds where to place a NEW screen in a colour
192 | // It picks a parent based on corner A
193 | // Returns a pick object for the placed screen
194 | export const placeScreen = (screen, target, options = {}) => {
195 | const picks = screen.corners.map((corner) =>
196 | pickInScreen(target, corner, { ...options, bruteForceDepth: 0 })
197 | );
198 | const [head] = picks;
199 |
200 | const relativeCorners = getMappedPositions(screen.corners, head.corners);
201 | const relativeScreen = makeScreen(screen.colour, relativeCorners);
202 |
203 | const parent = head.screen;
204 | const number = addScreen(parent.colour, relativeScreen);
205 | const { part = PART_TYPE.UNKNOWN } = options;
206 |
207 | const pick = makePick({
208 | screen,
209 | corners: screen.corners,
210 | position: head.position,
211 | parent,
212 | number,
213 | part,
214 | depth: head.depth,
215 | });
216 |
217 | return pick;
218 | };
219 |
220 | // address = Address of the screen we want to replace
221 | // screen = What we want to replace the it with
222 | // target = The screen that we are placing into (at the top level) - nearly always 'world'!
223 | // parent = The current screen's parent, which we should try our best to stay in
224 | //
225 | // note: 'screen' and 'target' should have their corners be relative to the view
226 | export const replaceAddress = ({
227 | address,
228 | screen,
229 | target,
230 | parent,
231 | depth,
232 | ...options
233 | } = {}) => {
234 | const oldScreen = getScreenFromAddress(address);
235 | const targetedCorners = getMappedPositions(screen.corners, target.corners);
236 | const targetedScreen = makeScreen(screen.colour, targetedCorners);
237 |
238 | // Pick where to place the screen
239 | const pickOptions = { ...options, ignore: oldScreen, ignoreDepth: depth };
240 | let picks = targetedScreen.corners.map((corner) =>
241 | pickInScreen(target, corner, pickOptions)
242 | );
243 | let isStillWithParent = picks.some((pick) => pick.screen === parent);
244 |
245 | if (!isStillWithParent) {
246 | const snapPicks = targetedScreen.corners.map((corner) =>
247 | pickInScreen(target, corner, { ...pickOptions, snap: parent })
248 | );
249 | const snapIsStillWithParent = snapPicks.some(
250 | (pick) => pick.screen === parent
251 | );
252 | if (snapIsStillWithParent) {
253 | const depth = Math.min(...picks.map((pick) => pick.depth));
254 | const snapDepth = Math.min(...snapPicks.map((pick) => pick.depth));
255 | if (snapDepth >= depth) {
256 | picks = snapPicks;
257 | isStillWithParent = snapIsStillWithParent;
258 | }
259 | }
260 | }
261 |
262 | // Decide which pick (out of the 4) to use as the basis for the placement
263 | const [head, ...tail] = picks;
264 | let pickLeader = head;
265 | if (isStillWithParent) {
266 | pickLeader = picks.find((pick) => pick.screen === parent);
267 | } else
268 | for (const pick of tail) {
269 | if (pick.depth > pickLeader.depth) {
270 | pickLeader = pick;
271 | }
272 | }
273 |
274 | // Place the screen
275 | const mappedCorners = getMappedPositions(screen.corners, pickLeader.corners);
276 | let number = address.number;
277 | if (false && isStillWithParent) {
278 | oldScreen.corners = mappedCorners;
279 | } else {
280 | const mappedScreen = makeScreen(screen.colour, mappedCorners);
281 | if (address.colour === pickLeader.screen.colour) {
282 | setScreenNumber(address.colour, address.number, mappedScreen);
283 | number = address.number;
284 | } else {
285 | removeScreenAddress(address);
286 | number = addScreen(pickLeader.screen.colour, mappedScreen);
287 | }
288 | }
289 |
290 | // Return info about the picked placement
291 | const { part = PART_TYPE.UNKNOWN } = options;
292 | const route = pickLeader.route;
293 | //addStep(route, number)
294 |
295 | //print(pickLeader.screen === window.global.colours[GREEN].screens[0])
296 |
297 | let resultParent =
298 | pickLeader.address === undefined
299 | ? pickLeader.screen
300 | : getScreenFromAddress(pickLeader.address);
301 | if (resultParent === undefined) resultParent = pickLeader.screen;
302 |
303 | const pick = makePick({
304 | screen,
305 | corners: screen.corners,
306 | position: pickLeader.position,
307 | parent: resultParent,
308 | number,
309 | part,
310 | depth: pickLeader.depth + 1,
311 | route,
312 | isWithinParent: isStillWithParent,
313 | });
314 | return pick;
315 | };
316 |
317 | export const tryToSurroundScreens = (address) => {
318 | const { colour } = address;
319 | const screen = getScreenFromAddress(address);
320 |
321 | const surroundedScreensSet = new Set();
322 | const length = colour.screens.length;
323 |
324 | for (let i = 0; i < length; i++) {
325 | const child = colour.screens[i];
326 | if (child === screen) continue;
327 |
328 | const mappedChildCorners = getMappedPositions(
329 | child.corners,
330 | screen.corners
331 | );
332 | const insideScreen = mappedChildCorners.every((corner) =>
333 | isMappedPositionInCorners(corner)
334 | );
335 |
336 | if (!insideScreen) continue;
337 | surroundedScreensSet.add(child);
338 | const newChild = makeScreen(child.colour, mappedChildCorners);
339 | addScreen(screen.colour, newChild);
340 | }
341 |
342 | removeScreensSet(colour, surroundedScreensSet);
343 |
344 | const newAddress = getAddressFromScreen(screen, colour);
345 | return newAddress;
346 | };
347 |
--------------------------------------------------------------------------------
/source/position.js:
--------------------------------------------------------------------------------
1 | import { bilerp, ibilerp } from "./lerp.js";
2 | import { getZeroedCorners } from "./corners.js";
3 |
4 | //======//
5 | // VIEW //
6 | //======//
7 | export const getViewPosition = (context, canvasPosition) => {
8 | const { canvas } = context;
9 | const [x, y] = canvasPosition;
10 | const viewPosition = [x / canvas.width, y / canvas.height];
11 | return viewPosition;
12 | };
13 |
14 | export const getCanvasPosition = (context, viewPosition) => {
15 | const { canvas } = context;
16 | const [x, y] = viewPosition;
17 | const canvasPosition = [x * canvas.width, y * canvas.height];
18 | return canvasPosition;
19 | };
20 |
21 | export const getCanvasPositions = (context, viewPositions) => {
22 | const canvasPositions = viewPositions.map((viewPosition) =>
23 | getCanvasPosition(context, viewPosition)
24 | );
25 | return canvasPositions;
26 | };
27 |
28 | export const getMousePosition = (context, corners) => {
29 | const viewPosition = getViewPosition(context, Mouse.position);
30 | const position = getMappedPosition(viewPosition, corners);
31 | return position;
32 | };
33 |
34 | // HIGHER screen position -> DEEPER screen position
35 | //
36 | // Position... within a higher screen
37 | // Corners... of a deeper screen
38 | // Return: Where the position would be, if it was inside the deeper screen (instead of the higher screen)
39 | export const getRelativePosition = (position, corners) => {
40 | const relativePosition = bilerp(position, corners);
41 | return relativePosition;
42 | };
43 |
44 | export const getRelativePositions = (positions, corners) => {
45 | const relativePositions = positions.map((position) =>
46 | getRelativePosition(position, corners)
47 | );
48 | return relativePositions;
49 | };
50 |
51 | // DEEPER screen position -> HIGHER screen position
52 | //
53 | // Position... within a higher screen
54 | // Corners... of a deeper screen
55 | // Return: If we treat the deeper screen as the co-ordinates, where should we place the position?
56 | export const getMappedPosition = (position, corners, safe = true) => {
57 | const mappedPosition = ibilerp(position, corners);
58 | if (!safe) return mappedPosition;
59 | return mappedPosition.map((axis) => (isNaN(axis) ? 0 : axis));
60 | };
61 |
62 | export const getMappedPositions = (positions, corners) => {
63 | const mappedPositions = positions.map((position) =>
64 | getMappedPosition(position, corners)
65 | );
66 | return mappedPositions;
67 | };
68 |
69 | export const getRotatedPosition = (position, origin, angle) => {
70 | const [px, py] = position;
71 | const [ox, oy] = origin;
72 |
73 | const cos = Math.cos(angle);
74 | const sin = Math.sin(angle);
75 | const dy = py - oy;
76 | const dx = px - ox;
77 |
78 | const x = dx * cos + dy * sin + ox;
79 | const y = dy * cos - dx * sin + oy;
80 | return [x, y];
81 | };
82 |
83 | export const isPositionInCorners = (position, corners, pity = [0, 0]) => {
84 | const mappedPosition = getMappedPosition(position, corners);
85 | return isMappedPositionInCorners(mappedPosition, pity);
86 | };
87 |
88 | export const isMappedPositionInCorners = (position, pity = [0, 0]) => {
89 | const [x, y] = position;
90 | const [px, py] = pity;
91 | if (x <= 0.0 - px) return false;
92 | if (x >= 1.0 + px) return false;
93 | if (y <= 0.0 - py) return false;
94 | if (y >= 1.0 + py) return false;
95 | return true;
96 | };
97 |
98 | export const getScaledPosition = (position, corners) => {
99 | const zeroedCorners = getZeroedCorners(corners);
100 | const scaledPosition = getMappedPosition(position, zeroedCorners);
101 | return scaledPosition;
102 | };
103 |
104 | export const getZoomedPosition = (position, zoom, origin) => {
105 | const [x, y] = position;
106 | const [ox, oy] = origin;
107 | const originedPosition = [x - ox, y - oy];
108 |
109 | const zoomedPosition = originedPosition.map((axis) => axis * zoom);
110 |
111 | const [zx, zy] = zoomedPosition;
112 | const movedPosition = [zx + ox, zy + oy];
113 |
114 | return movedPosition;
115 | };
116 |
117 | export const getZoomedPositions = (positions, zoom, origin) => {
118 | const zoomedPositions = positions.map((position) =>
119 | getZoomedPosition(position, zoom, origin)
120 | );
121 | return zoomedPositions;
122 | };
123 |
--------------------------------------------------------------------------------
/source/preset.js:
--------------------------------------------------------------------------------
1 | import { makeWorld } from "./world.js";
2 | import { makeRectangleCorners, getRotatedCorners } from "./corners.js";
3 | import { addScreen, removeAllScreens, rotateScreenNumber } from "./colour.js";
4 | import { makeScreen } from "./screen.js";
5 | import { onkeydown } from "./keyboard.js";
6 | import { clearQueue } from "./draw.js";
7 |
8 | //========//
9 | // PRESET //
10 | //========//
11 | const PRESET = {};
12 | export const loadPresetName = (global, presetName) => {
13 | const preset = PRESET[presetName];
14 | if (preset === undefined) {
15 | throw new Error(`Could not find preset: '${presetName}'`);
16 | }
17 | return loadPreset(global, preset);
18 | };
19 |
20 | export const loadPreset = (global, preset) => {
21 | const { colours } = global;
22 | for (const colour of colours) {
23 | removeAllScreens(colour);
24 | }
25 |
26 | for (const colourName in preset.colours) {
27 | const colour = colours[colourName];
28 | const screenPresets = preset.colours[colourName];
29 | for (const screenPreset of screenPresets) {
30 | const screenColour = colours[screenPreset.hex];
31 | const screen = makeScreen(screenColour, screenPreset.corners);
32 | addScreen(colour, screen);
33 | }
34 | }
35 |
36 | global.world = makeWorld(colours, preset.world?.hex ?? GREY);
37 | global.update = preset.update ?? (() => {});
38 |
39 | const { show, queue, world } = global;
40 | const { context } = show;
41 | if (context !== undefined) {
42 | clearQueue(context, queue, world);
43 | }
44 | };
45 |
46 | const createPreset = ({ key, world, colours = {}, update = () => {} } = {}) => {
47 | const preset = { world, colours, update };
48 | if (key !== undefined) {
49 | onkeydown(key, () => loadPreset(global, preset));
50 | }
51 | return preset;
52 | };
53 |
54 | //=========//
55 | // PRESETS //
56 | //=========//
57 | PRESET.EMPTY = createPreset({
58 | key: "c",
59 | });
60 |
61 | PRESET.SINGLE = createPreset({
62 | key: "s",
63 | colours: {
64 | [GREY]: [
65 | {
66 | hex: GREEN,
67 | corners: getRotatedCorners(
68 | makeRectangleCorners(0.1, 0.1, 0.8, 0.8),
69 | 0.0
70 | ),
71 | },
72 | ],
73 | [GREEN]: [
74 | {
75 | hex: GREEN,
76 | corners: getRotatedCorners(
77 | makeRectangleCorners(0.0, 0.0, 0.8, 0.8),
78 | 0.0
79 | ),
80 | },
81 | ],
82 | [BLUE]: [
83 | {
84 | hex: GREEN,
85 | corners: getRotatedCorners(
86 | makeRectangleCorners(0.05, 0.05, 0.9, 0.9),
87 | 0.0
88 | ),
89 | },
90 | ],
91 | },
92 | });
93 |
94 | PRESET.DOUBLE = createPreset({
95 | key: "d",
96 | colours: {
97 | [GREY]: [
98 | {
99 | hex: BLUE,
100 | corners: [
101 | [0.4, 0.4],
102 | [0.1, 0.4],
103 |
104 | [0.4, 0.1],
105 | [0.1, 0.1],
106 | ],
107 | },
108 | // { hex: RED, corners: makeRectangleCorners(0.525, 0.05, 0.425, 0.9) },
109 | ],
110 | // [BLUE]: [],
111 | // [RED]: [{ hex: RED, corners: makeRectangleCorners(0.1, 0.1, 0.8, 0.8) }],
112 | },
113 | update: ({ colours, queue, show, world }) => {
114 | // rotateScreenNumber(colours[RED], 0, 0.002);
115 | // const { context } = show;
116 | // clearQueue(context, queue, world);
117 | },
118 | });
119 |
120 | PRESET.INFINITE = createPreset({
121 | key: "f",
122 | colours: {
123 | [GREY]: [
124 | { hex: GREEN, corners: makeRectangleCorners(0.05, 0.05, 0.9, 0.9) },
125 | ],
126 | [GREEN]: [
127 | {
128 | hex: GREEN,
129 | corners: getRotatedCorners(
130 | makeRectangleCorners(0.05, 0.05, 0.9, 0.9),
131 | 0.0
132 | ),
133 | },
134 | ],
135 | },
136 | update: ({ colours, queue, show, hand, world }) => {
137 | const s1 = colours[GREEN].screens[0];
138 | s1.corners = getRotatedCorners(s1.corners, 0.001);
139 | const { context } = show;
140 | clearQueue(context, queue, world);
141 | },
142 | });
143 |
144 | PRESET.GRID = createPreset({
145 | key: "v",
146 | colours: {
147 | [GREY]: [
148 | { hex: GREEN, corners: makeRectangleCorners(0, 0, 1 / 3, 1 / 3) },
149 | { hex: BLUE, corners: makeRectangleCorners(1 / 3, 0, 1 / 3, 1 / 3) },
150 | { hex: RED, corners: makeRectangleCorners(2 / 3, 0, 1 / 3, 1 / 3) },
151 | { hex: YELLOW, corners: makeRectangleCorners(0, 1 / 3, 1 / 3, 1 / 3) },
152 | {
153 | hex: GREY,
154 | corners: getRotatedCorners(
155 | makeRectangleCorners(1 / 3, 1 / 3, 1 / 3, 1 / 3),
156 | 0.0
157 | ),
158 | },
159 | {
160 | hex: PURPLE,
161 | corners: makeRectangleCorners(2 / 3, 1 / 3, 1 / 3, 1 / 3),
162 | },
163 | { hex: ROSE, corners: makeRectangleCorners(0, 2 / 3, 1 / 3, 1 / 3) },
164 | { hex: CYAN, corners: makeRectangleCorners(1 / 3, 2 / 3, 1 / 3, 1 / 3) },
165 | {
166 | hex: ORANGE,
167 | corners: getRotatedCorners(
168 | makeRectangleCorners(2 / 3 - 0, 2 / 3 - 0, 1 / 3 - 0.0, 1 / 3 - 0.0),
169 | -0.0
170 | ),
171 | },
172 | ],
173 | },
174 | });
175 |
176 | PRESET.MINI_GRID = createPreset({
177 | key: "b",
178 | colours: {
179 | [GREY]: [{ hex: RED, corners: makeRectangleCorners(0.1, 0.1, 0.8, 0.8) }],
180 | [RED]: [
181 | {
182 | hex: RED,
183 | corners: getRotatedCorners(
184 | makeRectangleCorners(0, 0, 1 / 2, 1 / 2),
185 | 0.0
186 | ),
187 | },
188 | {
189 | hex: RED,
190 | corners: getRotatedCorners(
191 | makeRectangleCorners(0, 1 / 2, 1 / 2, 1 / 2),
192 | 0.0
193 | ),
194 | },
195 | {
196 | hex: RED,
197 | corners: getRotatedCorners(
198 | makeRectangleCorners(1 / 2, 1 / 2, 1 / 2, 1 / 2),
199 | -0.0
200 | ),
201 | },
202 | // { hex: BLUE, corners: makeRectangleCorners(1 / 2, 0.0, 1 / 2, 1 / 2) },
203 | ],
204 | },
205 | update: (colours) => {
206 | //const s = colours[RED].screens[3]
207 | //s.corners = getRotatedCorners(s.corners, 0.001)
208 | //resetColourCanvas(colours[GREY])
209 | },
210 | });
211 |
212 | PRESET.GRID2 = createPreset({
213 | key: "g",
214 | colours: {
215 | [GREY]: [
216 | {
217 | hex: GREY,
218 | corners: getRotatedCorners(
219 | makeRectangleCorners(0.25, 0.2, 0.5, 0.5),
220 | 0.0
221 | ),
222 | },
223 | //{hex: RED, corners: getRotatedCorners(makeRectangleCorners(0.1, 0.1, 0.3, 0.3), 0.0)},
224 | //{hex: RED, corners: getRotatedCorners(makeRectangleCorners(0.6, 0.1, 0.3, 0.3), 0.0)},
225 | //{hex: RED, corners: getRotatedCorners(makeRectangleCorners(0.1, 0.6, 0.3, 0.3), 0.0)},
226 | ],
227 | // [RED]: [
228 | // {
229 | // hex: BLUE,
230 | // corners: getRotatedCorners(
231 | // makeRectangleCorners(0.25, 0.25, 0.3, 0.3),
232 | // 0.0
233 | // ),
234 | // },
235 | // ],
236 | },
237 | update: ({ colours, queue, world, show, hand }) => {
238 | rotateScreenNumber(colours[GREY], 0, 0.0005);
239 | const { context } = show;
240 | clearQueue(context, queue, world);
241 | },
242 | });
243 |
244 | const IMPOSSIBLE_SCREEN_A = {
245 | hex: GREEN,
246 | corners: makeRectangleCorners(0.4, 0.5, 0.4, 0.4),
247 | };
248 |
249 | PRESET.IMPOSSIBLE = createPreset({
250 | key: "i",
251 | colours: {
252 | [GREY]: [
253 | {
254 | hex: GREEN,
255 | corners: makeRectangleCorners(0.1, 0.1, 0.7, 0.7),
256 | },
257 | ],
258 | [GREEN]: [
259 | IMPOSSIBLE_SCREEN_A,
260 | {
261 | hex: RED,
262 | corners: makeRectangleCorners(0.2, 0.1, 0.3, 0.3),
263 | },
264 | ],
265 | [RED]: [
266 | IMPOSSIBLE_SCREEN_A,
267 | {
268 | hex: BLUE,
269 | corners: makeRectangleCorners(0.1, 0.1, 0.3, 0.3),
270 | },
271 | ],
272 | [BLUE]: [
273 | {
274 | hex: YELLOW,
275 | corners: makeRectangleCorners(0.1, 0.5, 0.3, 0.3),
276 | },
277 | ],
278 | },
279 | });
280 |
281 | PRESET.TREE = createPreset({
282 | key: "t",
283 | colours: {
284 | [GREY]: [
285 | {
286 | hex: GREEN,
287 | corners: makeRectangleCorners(0.1, 0.1, 0.1, 0.1),
288 | },
289 | ],
290 | [GREEN]: [
291 | {
292 | hex: GREEN,
293 | corners: makeRectangleCorners(0.6, 1.2, 1, 1),
294 | },
295 | {
296 | hex: RED,
297 | corners: makeRectangleCorners(1.1, 0.1, 1, 1),
298 | },
299 | ],
300 | [RED]: [
301 | {
302 | hex: GREEN,
303 | corners: makeRectangleCorners(-0.5, 1.1, 1, 1),
304 | },
305 | {
306 | hex: BLUE,
307 | corners: makeRectangleCorners(1.1, 0.1, 1, 1),
308 | },
309 | ],
310 | [BLUE]: [
311 | {
312 | hex: YELLOW,
313 | corners: makeRectangleCorners(1.1, -0.1, 1, 1),
314 | },
315 | ],
316 | },
317 | });
318 |
319 | PRESET.EXPORT = createPreset({
320 | key: "p",
321 | ...JSON.parse(
322 | `{"world":{"hex":"#4680ff","corners":[[-0.3575603892456025,-0.43701263825246073],[1.7461078182554122,-0.43701263825246073],[-0.3575603892456025,1.719508651689953],[1.7461078182554122,1.719508651689953]]},"colours":{"#46ff80":[],"#4680ff":[{"hex":"#374362","corners":[[0.15265138424912664,0.2208257843043769],[0.6950044157739126,0.2208257843043769],[0.15265138424912664,0.7547058262610854],[0.6950044157739126,0.7547058262610854]]}],"#ff4346":[],"#ffcc46":[],"#ff8046":[],"#ff80cc":[],"#46ccff":[],"#8043f7":[],"#374362":[{"hex":"#4680ff","corners":[[0.42942222902747573,0.3199990986656474],[0.781510332802464,0.3199990986656474],[0.42942222902747573,0.6744463536178704],[0.781510332802464,0.6744463536178704]]}]}}`
323 | ),
324 | });
325 |
326 | export function getPresetFromCurrentState() {
327 | const { colours } = global;
328 | const preset = {
329 | world: { hex: global.world.colour.hex, corners: global.world.corners },
330 | colours: {},
331 | };
332 | for (const colourName in colours) {
333 | const colour = colours[colourName];
334 | preset.colours[colourName] = [];
335 | for (const screen of colour.screens) {
336 | preset.colours[colourName].push({
337 | hex: screen.colour.hex,
338 | corners: screen.corners,
339 | });
340 | }
341 | }
342 | return preset;
343 | }
344 |
345 | window.PRESET = PRESET;
346 |
--------------------------------------------------------------------------------
/source/route.js:
--------------------------------------------------------------------------------
1 | import { LinkedList } from "./list.js";
2 | import { getRelativePositions } from "./position.js";
3 | import { makeScreen } from "./screen.js";
4 |
5 | //=======//
6 | // ROUTE //
7 | //=======//
8 | export const makeRoute = (start) => {
9 | const steps = new LinkedList();
10 | const route = { start, steps, length: 1 };
11 | return route;
12 | };
13 |
14 | export const addStep = (route, number) => {
15 | route.steps.push(number);
16 | route.length++;
17 | };
18 |
19 | export const popStep = (route) => {
20 | if (!route.steps.isEmpty) {
21 | route.length--;
22 | }
23 | route.steps.pop();
24 | };
25 |
26 | export const getDrawnScreenFromRoute = (route, stepNumber = route.length) => {
27 | const { start, steps } = route;
28 | let screen = start;
29 | let i = 0;
30 | for (const step of steps) {
31 | if (i >= stepNumber) return screen;
32 |
33 | const number = step.item;
34 | const child = screen.colour.screens[number];
35 | const relativeCorners = getRelativePositions(child.corners, screen.corners);
36 | screen = makeScreen(child.colour, relativeCorners);
37 |
38 | i++;
39 | }
40 | return screen;
41 | };
42 |
43 | export const getAddressedScreenFromRoute = (
44 | route,
45 | stepNumber = route.length
46 | ) => {
47 | const { start, steps } = route;
48 | let screen = start;
49 | let i = 0;
50 | for (const step of steps) {
51 | if (i >= stepNumber) return screen;
52 |
53 | const number = step.item;
54 | screen = screen.colour.screens[number];
55 |
56 | i++;
57 | }
58 | return screen;
59 | };
60 |
61 | export const areRoutesEqual = (a, b) => {
62 | if (a.start !== b.start) return false;
63 | if (a.length !== b.length) return false;
64 | const aSteps = [...a.steps];
65 | let i = 0;
66 | for (const bStep of b.steps) {
67 | const aStep = aSteps[i];
68 | if (aStep.item !== bStep.item) return false;
69 | i++;
70 | }
71 | return true;
72 | };
73 |
--------------------------------------------------------------------------------
/source/screen.js:
--------------------------------------------------------------------------------
1 | //========//
2 | // SCREEN //
3 | //========//
4 | export const makeScreen = (colour, corners) => {
5 | return { colour, corners };
6 | };
7 |
--------------------------------------------------------------------------------
/source/vector.js:
--------------------------------------------------------------------------------
1 | //========//
2 | // VECTOR //
3 | //========//
4 | export const scaleVector = ([x, y], n) => [x * n, y * n];
5 |
6 | export const addVector = (a, b) => {
7 | const [ax, ay] = a;
8 | const [bx, by] = b;
9 | const x = ax + bx;
10 | const y = ay + by;
11 | return [x, y];
12 | };
13 |
14 | export const subtractVector = (a, b) => {
15 | const [ax, ay] = a;
16 | const [bx, by] = b;
17 | const x = ax - bx;
18 | const y = ay - by;
19 | return [x, y];
20 | };
21 |
22 | export const crossProductVector = (a, b) => {
23 | const [ax, ay] = a;
24 | const [bx, by] = b;
25 | return ax * by - ay * bx;
26 | };
27 |
28 | export const distanceBetweenVectors = (a, b) => {
29 | const displacement = subtractVector(a, b);
30 | const [dx, dy] = displacement;
31 | const distance = Math.hypot(dx, dy);
32 | return distance;
33 | };
34 |
35 | export const angleBetweenVectors = (a, b) => {
36 | const displacement = subtractVector(a, b);
37 | const [dx, dy] = displacement;
38 | const angle = Math.atan2(dy, dx);
39 | return angle;
40 | };
41 |
--------------------------------------------------------------------------------
/source/world.js:
--------------------------------------------------------------------------------
1 | import { getColourParents } from "./colour.js";
2 | import {
3 | getAddedCorners,
4 | getClonedCorners,
5 | getCornersPosition,
6 | getMovedCorners,
7 | getSubtractedCorners,
8 | makeRectangleCorners,
9 | VIEW_CORNERS,
10 | } from "./corners.js";
11 | import { getMappedPositionPart, PART_TYPE } from "./part.js";
12 | import { getMappedPositions, getRelativePositions } from "./position.js";
13 | import { addStep, getDrawnScreenFromRoute, makeRoute } from "./route.js";
14 | import { makeScreen } from "./screen.js";
15 |
16 | //=======//
17 | // WORLD //
18 | //=======//
19 | export const makeWorld = (colours, hex = GREY) => {
20 | const colour = colours[hex];
21 | const corners = makeRectangleCorners(0, 0, 1, 1);
22 | const world = makeScreen(colour, corners);
23 | return world;
24 | };
25 |
26 | export const setWorldCorners = (world, corners, colours) => {
27 | world.corners = corners;
28 |
29 | // Check if any children fill the whole world
30 | for (const child of world.colour.screens) {
31 | const relativeChildCorners = getRelativePositions(
32 | child.corners,
33 | world.corners
34 | );
35 | const mappedViewCorners = getMappedPositions(
36 | VIEW_CORNERS,
37 | relativeChildCorners
38 | );
39 |
40 | const parts = mappedViewCorners.map((corner) =>
41 | getMappedPositionPart(corner)
42 | );
43 | if (parts.every((part) => part.type === PART_TYPE.INSIDE)) {
44 | world.colour = child.colour;
45 | world.corners = relativeChildCorners;
46 | return;
47 | }
48 | }
49 |
50 | // Check that all world corners are outside the view
51 | const mappedViewCorners = getMappedPositions(VIEW_CORNERS, corners);
52 | const parts = mappedViewCorners.map((corner) =>
53 | getMappedPositionPart(corner)
54 | );
55 | if (parts.every((part) => part.type === PART_TYPE.INSIDE)) {
56 | return;
57 | }
58 |
59 | const parents = getColourParents(world.colour, colours);
60 | if (parents.length === 0) return;
61 |
62 | world.colour.parentNumber++;
63 | if (world.colour.parentNumber >= parents.length) {
64 | world.colour.parentNumber = 0;
65 | }
66 |
67 | const parent = parents[world.colour.parentNumber];
68 | const parentCorners = getRelativePositions(parent.corners, world.corners);
69 |
70 | const child = parent.colour.screens[parent.number];
71 | const relativeChildCorners = getRelativePositions(
72 | child.corners,
73 | parentCorners
74 | );
75 | const viewChildCorners = getMappedPositions(
76 | VIEW_CORNERS,
77 | relativeChildCorners
78 | );
79 |
80 | const difference = getSubtractedCorners(mappedViewCorners, viewChildCorners);
81 | const yankedCorners = getAddedCorners(parentCorners, difference);
82 |
83 | world.corners = yankedCorners;
84 | world.colour = parent.colour;
85 | };
86 |
--------------------------------------------------------------------------------
/source/zoom.js:
--------------------------------------------------------------------------------
1 | import { VIEW_CORNERS } from "./corners.js";
2 | import { clearQueue } from "./draw.js";
3 | import { onkeydown } from "./keyboard.js";
4 | import { getMousePosition, getZoomedPositions } from "./position.js";
5 | import { setWorldCorners } from "./world.js";
6 |
7 | //======//
8 | // ZOOM //
9 | //======//
10 | export const makeZoomer = () => {
11 | const zoomer = {
12 | speed: 0.0,
13 | desiredSpeed: 0.0,
14 | smoothMode: false,
15 | };
16 | return zoomer;
17 | };
18 |
19 | export const registerMouseWheel = (zoomer) => {
20 | onkeydown("z", () => (zoomer.smoothMode = !zoomer.smoothMode));
21 | onkeydown("r", () => (zoomer.desiredSpeed = 0.0));
22 |
23 | on.wheel((e) => {
24 | const dspeed = Math.sign(e.deltaY);
25 | zoomer.desiredSpeed += dspeed * (zoomer.smoothMode ? 0.1 : 3);
26 | });
27 | };
28 |
29 | export const updateZoom = (context, queue, zoomer, world, colours) => {
30 | const missingSpeed = zoomer.desiredSpeed - zoomer.speed;
31 |
32 | zoomer.speed += Math.sign(missingSpeed) * 0.15;
33 | if (!zoomer.smoothMode) zoomer.speed = zoomer.desiredSpeed;
34 |
35 | if (Math.abs(zoomer.speed) < 0.001) {
36 | zoomer.speed = 0.0;
37 | }
38 | if (zoomer.speed === 0.0) return;
39 |
40 | if (!zoomer.smoothMode) {
41 | zoomer.desiredSpeed *= 0.8;
42 | if (Math.abs(zoomer.desiredSpeed) < 0.001) {
43 | zoomer.desiredSpeed = 0.0;
44 | }
45 | }
46 |
47 | const mousePosition = getMousePosition(context, VIEW_CORNERS);
48 | const zoomedCorners = getZoomedPositions(
49 | world.corners,
50 | 1.0 + zoomer.speed * -0.002,
51 | mousePosition
52 | );
53 | setWorldCorners(world, zoomedCorners, colours);
54 | clearQueue(context, queue, world);
55 | };
56 |
--------------------------------------------------------------------------------