├── .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 | --------------------------------------------------------------------------------