├── LICENSE ├── README.md ├── docs └── screenshot-mode.md ├── images ├── berd0.png ├── berd0.psd ├── berd1.png ├── bot0.png ├── bot0.psd ├── bot1.png ├── hat │ ├── berd0.png │ ├── berd0.psd │ ├── berd1.png │ ├── tode.png │ └── tode.psd ├── tode.png ├── tode.psd └── todecur.png ├── index.html ├── libraries ├── habitat-embed.js ├── perfect-freehand.js ├── show.js └── svgShow.js └── script.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lu[ke] 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 | 2 | 3 | # PaintPond 4 | Paint with a painter.
5 | I originally made PaintPond for this video: ✨ **[NEW Cellular Automata](https://youtu.be/WMJ1H3Ai-qs)**
6 | It was also used in this one: 🔮 **[Spellular Automata](https://youtu.be/xvlsJ3FqNYU?si=8PFznQkaGfdiOPJA)** 7 | 8 | Try it at [paintpond.cool](https://paintpond.cool)! 9 | 10 | ## Controls 11 | **Number keys**: Change colour!
12 | **Tab key**: Change painter!
13 | **Space bar**: Enter [screenshot mode](docs/screenshot-mode.md)! 14 | 15 | **X key**: Undo!
16 | **C key**: Clear the screen!
17 | 18 | ## Running Locally 19 | To run locally...
20 | you need to run a local server because it uses javascript modules.
21 | (ie: you can't just open `index.html` like most of my other projects)
22 | 23 | I recommend getting [deno](https://deno.land) 24 | and then installing `file_server` with this command: 25 | ``` 26 | deno install --allow-read --allow-net https://deno.land/std@0.142.0/http/file_server.ts 27 | ``` 28 | Then you can run this command to run a local server: 29 | ``` 30 | file_server 31 | ``` 32 | 33 | ## Special Thanks 34 | * [Magnogen](https://magnogen.net/) for giving the characters more life + movement 35 | * [Steve Ruiz](https://www.steveruiz.me/) for making [Perfect Freehand](https://github.com/steveruizok/perfect-freehand) (which this project uses) 36 | * [Linn Dahlgreen](https://github.com/SimplyLinn) for improving the stroke rendering, and getting it to work on iPhones 37 | -------------------------------------------------------------------------------- /docs/screenshot-mode.md: -------------------------------------------------------------------------------- 1 | # Screenshot Mode 2 | Press space to pause/unpause PaintPond.
3 | When paused, you can right/left click to copy the image to your clipboard.
4 | Left click copies what you see, right click copies a version with transparent background
5 | Click and drag to copy a region, just click to copy the entire canvas
6 | -------------------------------------------------------------------------------- /images/berd0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/berd0.png -------------------------------------------------------------------------------- /images/berd0.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/berd0.psd -------------------------------------------------------------------------------- /images/berd1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/berd1.png -------------------------------------------------------------------------------- /images/bot0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/bot0.png -------------------------------------------------------------------------------- /images/bot0.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/bot0.psd -------------------------------------------------------------------------------- /images/bot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/bot1.png -------------------------------------------------------------------------------- /images/hat/berd0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/hat/berd0.png -------------------------------------------------------------------------------- /images/hat/berd0.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/hat/berd0.psd -------------------------------------------------------------------------------- /images/hat/berd1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/hat/berd1.png -------------------------------------------------------------------------------- /images/hat/tode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/hat/tode.png -------------------------------------------------------------------------------- /images/hat/tode.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/hat/tode.psd -------------------------------------------------------------------------------- /images/tode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/tode.png -------------------------------------------------------------------------------- /images/tode.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/tode.psd -------------------------------------------------------------------------------- /images/todecur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TodePond/PaintPond/cf246126e9614683aafa18387e42ddb2092b00ba/images/todecur.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PaintPond 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /libraries/habitat-embed.js: -------------------------------------------------------------------------------- 1 | const Habitat = {} 2 | 3 | //=======// 4 | // Array // 5 | //=======// 6 | { 7 | 8 | const install = (global) => { 9 | 10 | Reflect.defineProperty(global.Array.prototype, "last", { 11 | get() { 12 | return this[this.length-1] 13 | }, 14 | set(value) { 15 | Reflect.defineProperty(this, "last", {value, configurable: true, writable: true, enumerable: true}) 16 | }, 17 | configurable: true, 18 | enumerable: false, 19 | }) 20 | 21 | Reflect.defineProperty(global.Array.prototype, "clone", { 22 | get() { 23 | return [...this] 24 | }, 25 | set(value) { 26 | Reflect.defineProperty(this, "clone", {value, configurable: true, writable: true, enumerable: true}) 27 | }, 28 | configurable: true, 29 | enumerable: false, 30 | }) 31 | 32 | Reflect.defineProperty(global.Array.prototype, "at", { 33 | value(position) { 34 | if (position >= 0) return this[position] 35 | return this[this.length + position] 36 | }, 37 | configurable: true, 38 | enumerable: false, 39 | writable: true, 40 | }) 41 | 42 | Reflect.defineProperty(global.Array.prototype, "shuffle", { 43 | value() { 44 | for (let i = this.length - 1; i > 0; i--) { 45 | const r = Math.floor(Math.random() * (i+1)) 46 | ;[this[i], this[r]] = [this[r], this[i]] 47 | } 48 | return this 49 | }, 50 | configurable: true, 51 | enumerable: false, 52 | writable: true, 53 | }) 54 | 55 | Reflect.defineProperty(global.Array.prototype, "trim", { 56 | value() { 57 | if (this.length == 0) return this 58 | let start = this.length - 1 59 | let end = 0 60 | for (let i = 0; i < this.length; i++) { 61 | const value = this[i] 62 | if (value !== undefined) { 63 | start = i 64 | break 65 | } 66 | } 67 | for (let i = this.length - 1; i >= 0; i--) { 68 | const value = this[i] 69 | if (value !== undefined) { 70 | end = i + 1 71 | break 72 | } 73 | } 74 | this.splice(end) 75 | this.splice(0, start) 76 | return this 77 | }, 78 | configurable: true, 79 | enumerable: false, 80 | writable: true, 81 | }) 82 | 83 | Reflect.defineProperty(global.Array.prototype, "repeat", { 84 | value(n) { 85 | if (n === 0) { 86 | this.splice(0) 87 | return this 88 | } 89 | if (n < 0) { 90 | this.reverse() 91 | n = n - n - n 92 | } 93 | const clone = [...this] 94 | for (let i = 1; i < n; i++) { 95 | this.push(...clone) 96 | } 97 | return this 98 | }, 99 | configurable: true, 100 | enumerable: false, 101 | writable: true, 102 | }) 103 | 104 | Habitat.Array.installed = true 105 | 106 | } 107 | 108 | Habitat.Array = {install} 109 | 110 | } 111 | 112 | //=======// 113 | // Async // 114 | //=======// 115 | { 116 | const sleep = (duration) => new Promise(resolve => setTimeout(resolve, duration)) 117 | const install = (global) => { 118 | global.sleep = sleep 119 | Habitat.Async.installed = true 120 | } 121 | 122 | Habitat.Async = {install, sleep} 123 | } 124 | 125 | //========// 126 | // Colour // 127 | //========// 128 | { 129 | 130 | Habitat.Colour = {} 131 | 132 | Habitat.Colour.make = (style) => { 133 | 134 | if (typeof style === "number") { 135 | let string = style.toString() 136 | while (string.length < 3) string = "0"+string 137 | 138 | const redId = parseInt(string[0]) 139 | const greenId = parseInt(string[1]) 140 | const blueId = parseInt(string[2]) 141 | 142 | const red = reds[redId] 143 | const green = greens[greenId] 144 | const blue = blues[blueId] 145 | 146 | const rgb = `rgb(${red}, ${green}, ${blue})` 147 | 148 | const colour = Habitat.Colour.make(rgb) 149 | colour.splash = style 150 | return colour 151 | } 152 | 153 | const canvas = document.createElement("canvas") 154 | const context = canvas.getContext("2d") 155 | canvas.width = 1 156 | canvas.height = 1 157 | context.fillStyle = style 158 | context.fillRect(0, 0, 1, 1) 159 | 160 | const data = context.getImageData(0, 0, 1, 1).data 161 | const [red, green, blue] = data 162 | const splash = getSplash(red, green, blue) 163 | const alpha = data[3] / 255 164 | const [hue, saturation, lightness] = getHSL(red, green, blue) 165 | const colour = new Uint8Array([red, green, blue, alpha]) 166 | colour.fullColour = true 167 | 168 | colour.red = red 169 | colour.green = green 170 | colour.blue = blue 171 | colour.alpha = alpha 172 | 173 | colour.splash = splash 174 | 175 | colour.hue = hue 176 | colour.saturation = saturation 177 | colour.lightness = lightness 178 | 179 | colour.r = red 180 | colour.g = green 181 | colour.b = blue 182 | colour.a = alpha 183 | 184 | const rgb = `rgb(${red}, ${green}, ${blue})` 185 | const rgba = `rgba(${red}, ${green}, ${blue}, ${alpha})` 186 | const hex = `#${hexify(red)}${hexify(green)}${hexify(blue)}` 187 | const hsl = `hsl(${hue}, ${saturation}%, ${lightness}%)` 188 | 189 | colour.toString = () => hex 190 | colour.rgb = rgb 191 | colour.rgba = rgba 192 | colour.hex = hex 193 | colour.hsl = hsl 194 | 195 | colour.brightness = (red*299 + green*587 + blue*114) / 1000 / 255 196 | colour.isBright = colour.brightness > 0.7 197 | colour.isDark = colour.brightness < 0.3 198 | 199 | return colour 200 | } 201 | 202 | const hexify = (number) => { 203 | const string = number.toString(16) 204 | if (string.length === 2) return string 205 | return "0"+string 206 | } 207 | 208 | const getSplash = (red, green, blue) => { 209 | const closestRed = getClosest(red, reds).toString() 210 | const closestGreen = getClosest(green, greens).toString() 211 | const closestBlue = getClosest(blue, blues).toString() 212 | const string = closestRed + closestGreen + closestBlue 213 | const splash = parseInt(string) 214 | return splash 215 | } 216 | 217 | const getClosest = (number, array) => { 218 | let highscore = Infinity 219 | let winner = 0 220 | for (let i = 0; i < array.length; i++) { 221 | const value = array[i] 222 | const difference = Math.abs(number - value) 223 | if (difference < highscore) { 224 | highscore = difference 225 | winner = i 226 | } 227 | } 228 | return winner 229 | } 230 | 231 | //https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB 232 | const getHSL = (red, green, blue) => { 233 | 234 | red /= 255 235 | green /= 255 236 | blue /= 255 237 | 238 | const max = Math.max(red, green, blue) 239 | const min = Math.min(red, green, blue) 240 | const chroma = max - min 241 | 242 | let lightness = (max + min) / 2 243 | let saturation = 0 244 | if (lightness !== 0 && lightness !== 1) { 245 | saturation = (max - lightness) / Math.min(lightness, 1-lightness) 246 | } 247 | 248 | let hue = 0 249 | if (max === red) hue = (green-blue)/chroma 250 | if (max === green) hue = 2 + (blue-red)/chroma 251 | if (max === blue) hue = 4 + (red-green)/chroma 252 | if (chroma === 0) hue = 0 253 | 254 | lightness *= 100 255 | saturation *= 100 256 | hue *= 60 257 | while (hue < 0) hue += 360 258 | 259 | return [hue, saturation, lightness] 260 | 261 | } 262 | 263 | Habitat.Colour.add = (colour, {red=0, green=0, blue=0, alpha=0, hue=0, saturation=0, lightness=0, r=0, g=0, b=0, a=0, h=0, s=0, l=0, splash, fullColour = false} = {}) => { 264 | 265 | const newRed = clamp(colour.red + r + red, 0, 255) 266 | const newGreen = clamp(colour.green + g + green, 0, 255) 267 | const newBlue = clamp(colour.blue + b + blue, 0, 255) 268 | const newAlpha = clamp(colour.alpha + a + alpha, 0, 1) 269 | const rgbaStyle = `rgba(${newRed}, ${newGreen}, ${newBlue}, ${newAlpha})` 270 | const rgbaColour = Habitat.Colour.make(rgbaStyle) 271 | 272 | if (fullColour) return rgbaColour 273 | 274 | const newHue = wrap(rgbaColour.hue + h + hue, 0, 360) 275 | const newSaturation = clamp(rgbaColour.saturation + s + saturation, 0, 100) 276 | const newLightness = clamp(rgbaColour.lightness + l + lightness, 0, 100) 277 | const hslStyle = `hsl(${newHue}, ${newSaturation}%, ${newLightness}%)` 278 | const hslColour = Habitat.Colour.make(hslStyle) 279 | 280 | if (splash !== undefined && splashed) { 281 | const newSplash = hslColour.splash + splash 282 | const splashColour = Habitat.Colour.make(newSplash) 283 | return splashColour 284 | } 285 | 286 | return hslColour 287 | 288 | } 289 | 290 | 291 | Habitat.Colour.multiply = (colour, {red=1, green=1, blue=1, alpha=1, hue=1, saturation=1, lightness=1, r=1, g=1, b=1, a=1, h=1, s=1, l=1, splash, fullColour = false} = {}) => { 292 | 293 | const newRed = clamp(colour.red * r * red, 0, 255) 294 | const newGreen = clamp(colour.green * g * green, 0, 255) 295 | const newBlue = clamp(colour.blue * b * blue, 0, 255) 296 | const newAlpha = clamp(colour.alpha * a * alpha, 0, 1) 297 | const rgbaStyle = `rgba(${newRed}, ${newGreen}, ${newBlue}, ${newAlpha})` 298 | const rgbaColour = Habitat.Colour.make(rgbaStyle) 299 | 300 | if (fullColour) return rgbaColour 301 | 302 | const newHue = wrap(rgbaColour.hue * h * hue, 0, 360) 303 | const newSaturation = clamp(rgbaColour.saturation * s * saturation, 0, 100) 304 | const newLightness = clamp(rgbaColour.lightness * l * lightness, 0, 100) 305 | const hslStyle = `hsl(${newHue}, ${newSaturation}%, ${newLightness}%)` 306 | const hslColour = Habitat.Colour.make(hslStyle) 307 | 308 | if (splash !== undefined) { 309 | const newSplash = hslColour.splash * splash 310 | const splashColour = Habitat.Colour.make(newSplash) 311 | return splashColour 312 | } 313 | 314 | return hslColour 315 | 316 | } 317 | 318 | const clamp = (number, min, max) => { 319 | if (number < min) return min 320 | if (number > max) return max 321 | return number 322 | } 323 | 324 | const wrap = (number, min, max) => { 325 | const difference = max - min 326 | while (number < min) number += difference 327 | while (number > max) number -= difference 328 | return number 329 | } 330 | 331 | const reds = [23, 55, 70, 98, 128, 159, 174, 204, 242, 255] 332 | const greens = [29, 67, 98, 128, 159, 174, 204, 222, 245, 255] 333 | const blues = [40, 70, 98, 128, 159, 174, 204, 222, 247, 255] 334 | 335 | Habitat.Colour.Void = Habitat.Colour.make("rgb(6, 7, 10)") 336 | Habitat.Colour.Black = Habitat.Colour.make(000) 337 | Habitat.Colour.Grey = Habitat.Colour.make(112) 338 | Habitat.Colour.Silver = Habitat.Colour.make(556) 339 | Habitat.Colour.White = Habitat.Colour.make(888) 340 | 341 | Habitat.Colour.Green = Habitat.Colour.make(293) 342 | Habitat.Colour.Red = Habitat.Colour.make(911) 343 | Habitat.Colour.Blue = Habitat.Colour.make(239) 344 | Habitat.Colour.Yellow = Habitat.Colour.make(961) 345 | Habitat.Colour.Orange = Habitat.Colour.make(931) 346 | Habitat.Colour.Pink = Habitat.Colour.make(933) 347 | Habitat.Colour.Rose = Habitat.Colour.make(936) 348 | Habitat.Colour.Cyan = Habitat.Colour.make(269) 349 | Habitat.Colour.Purple = Habitat.Colour.make(418) 350 | 351 | Habitat.Colour.cache = [] 352 | Habitat.Colour.splash = (number) => { 353 | if (Habitat.Colour.cache.length === 0) { 354 | for (let i = 0; i < 1000; i++) { 355 | const colour = Habitat.Colour.make(i) 356 | Habitat.Colour.cache.push(colour) 357 | } 358 | } 359 | 360 | return Habitat.Colour.cache[number] 361 | } 362 | 363 | Habitat.Colour.install = (global) => { 364 | global.Colour = Habitat.Colour 365 | Habitat.Colour.installed = true 366 | } 367 | 368 | } 369 | 370 | //=========// 371 | // Console // 372 | //=========// 373 | { 374 | const print = console.log.bind(console) 375 | const dir = (value) => console.dir(Object(value)) 376 | 377 | let print9Counter = 0 378 | const print9 = (message) => { 379 | if (print9Counter >= 9) return 380 | print9Counter++ 381 | console.log(message) 382 | } 383 | 384 | const install = (global) => { 385 | global.print = print 386 | global.dir = dir 387 | global.print9 = print9 388 | 389 | Reflect.defineProperty(global.Object.prototype, "d", { 390 | get() { 391 | const value = this.valueOf() 392 | console.log(value) 393 | return value 394 | }, 395 | set(value) { 396 | Reflect.defineProperty(this, "d", {value, configurable: true, writable: true, enumerable: true}) 397 | }, 398 | configurable: true, 399 | enumerable: false, 400 | }) 401 | 402 | Reflect.defineProperty(global.Object.prototype, "dir", { 403 | get() { 404 | console.dir(this) 405 | return this.valueOf() 406 | }, 407 | set(value) { 408 | Reflect.defineProperty(this, "dir", {value, configurable: true, writable: true, enumerable: true}) 409 | }, 410 | configurable: true, 411 | enumerable: false, 412 | }) 413 | 414 | let d9Counter = 0 415 | Reflect.defineProperty(global.Object.prototype, "d9", { 416 | get() { 417 | const value = this.valueOf() 418 | if (d9Counter < 9) { 419 | console.log(value) 420 | d9Counter++ 421 | } 422 | return value 423 | }, 424 | set(value) { 425 | Reflect.defineProperty(this, "d9", {value, configurable: true, writable: true, enumerable: true}) 426 | }, 427 | configurable: true, 428 | enumerable: false, 429 | }) 430 | 431 | Habitat.Console.installed = true 432 | 433 | } 434 | 435 | Habitat.Console = {install, print, dir, print9} 436 | } 437 | 438 | //==========// 439 | // Document // 440 | //==========// 441 | { 442 | 443 | const $ = (...args) => document.querySelector(...args) 444 | const $$ = (...args) => document.querySelectorAll(...args) 445 | 446 | const install = (global) => { 447 | 448 | 449 | global.$ = $ 450 | global.$$ = $$ 451 | 452 | if (global.Node === undefined) return 453 | 454 | Reflect.defineProperty(global.Node.prototype, "$", { 455 | value(...args) { 456 | return this.querySelector(...args) 457 | }, 458 | configurable: true, 459 | enumerable: false, 460 | writable: true, 461 | }) 462 | 463 | Reflect.defineProperty(global.Node.prototype, "$$", { 464 | value(...args) { 465 | return this.querySelectorAll(...args) 466 | }, 467 | configurable: true, 468 | enumerable: false, 469 | writable: true, 470 | }) 471 | 472 | Habitat.Document.installed = true 473 | 474 | } 475 | 476 | Habitat.Document = {install, $, $$} 477 | 478 | } 479 | 480 | 481 | //=======// 482 | // Event // 483 | //=======// 484 | { 485 | 486 | const install = (global) => { 487 | 488 | Reflect.defineProperty(global.EventTarget.prototype, "on", { 489 | get() { 490 | return new Proxy(this, { 491 | get: (element, eventName) => (...args) => { 492 | element.addEventListener(eventName, ...args) 493 | return () => element.removeEventListener(eventName, ...args) 494 | }, 495 | }) 496 | }, 497 | set(value) { 498 | Reflect.defineProperty(this, "on", {value, configurable: true, writable: true, enumerable: true}) 499 | }, 500 | configurable: true, 501 | enumerable: false, 502 | }) 503 | 504 | Reflect.defineProperty(global.EventTarget.prototype, "trigger", { 505 | value(name, options = {}) { 506 | const {bubbles = true, cancelable = true, ...data} = options 507 | const event = new Event(name, {bubbles, cancelable}) 508 | for (const key in data) event[key] = data[key] 509 | this.dispatchEvent(event) 510 | }, 511 | configurable: true, 512 | enumerable: false, 513 | writable: true, 514 | }) 515 | 516 | Habitat.Event.installed = true 517 | 518 | } 519 | 520 | Habitat.Event = {install} 521 | 522 | } 523 | 524 | 525 | //======// 526 | // HTML // 527 | //======// 528 | { 529 | 530 | Habitat.HTML = (...args) => { 531 | const source = String.raw(...args) 532 | const template = document.createElement("template") 533 | template.innerHTML = source 534 | return template.content 535 | } 536 | 537 | Habitat.HTML.install = (global) => { 538 | global.HTML = Habitat.HTML 539 | Habitat.HTML.installed = true 540 | } 541 | 542 | } 543 | 544 | 545 | //============// 546 | // JavaScript // 547 | //============// 548 | { 549 | 550 | Habitat.JavaScript = (...args) => { 551 | const source = String.raw(...args) 552 | const code = `return ${source}` 553 | const func = new Function(code)() 554 | return func 555 | } 556 | 557 | Habitat.JavaScript.install = (global) => { 558 | global.JavaScript = Habitat.JavaScript 559 | Habitat.JavaScript.installed = true 560 | } 561 | 562 | } 563 | 564 | 565 | //==========// 566 | // Keyboard // 567 | //==========// 568 | { 569 | 570 | const Keyboard = Habitat.Keyboard = {} 571 | Reflect.defineProperty(Keyboard, "install", { 572 | value(global) { 573 | global.Keyboard = Keyboard 574 | global.addEventListener("keydown", e => { 575 | Keyboard[e.key] = true 576 | }) 577 | 578 | global.addEventListener("keyup", e => { 579 | Keyboard[e.key] = false 580 | }) 581 | 582 | Reflect.defineProperty(Keyboard, "installed", { 583 | value: true, 584 | configurable: true, 585 | enumerable: false, 586 | writable: true, 587 | }) 588 | }, 589 | configurable: true, 590 | enumerable: false, 591 | writable: true, 592 | }) 593 | 594 | } 595 | 596 | 597 | //======// 598 | // Main // 599 | //======// 600 | Habitat.install = (global) => { 601 | 602 | if (Habitat.installed) return 603 | 604 | if (!Habitat.Array.installed) Habitat.Array.install(global) 605 | if (!Habitat.Async.installed) Habitat.Async.install(global) 606 | if (!Habitat.Colour.installed) Habitat.Colour.install(global) 607 | if (!Habitat.Console.installed) Habitat.Console.install(global) 608 | if (!Habitat.Document.installed) Habitat.Document.install(global) 609 | if (!Habitat.Event.installed) Habitat.Event.install(global) 610 | if (!Habitat.HTML.installed) Habitat.HTML.install(global) 611 | if (!Habitat.JavaScript.installed) Habitat.JavaScript.install(global) 612 | if (!Habitat.Keyboard.installed) Habitat.Keyboard.install(global) 613 | if (!Habitat.Math.installed) Habitat.Math.install(global) 614 | if (!Habitat.Mouse.installed) Habitat.Mouse.install(global) 615 | if (!Habitat.Number.installed) Habitat.Number.install(global) 616 | if (!Habitat.Object.installed) Habitat.Object.install(global) 617 | if (!Habitat.Property.installed) Habitat.Property.install(global) 618 | if (!Habitat.Random.installed) Habitat.Random.install(global) 619 | if (!Habitat.Stage.installed) Habitat.Stage.install(global) 620 | if (!Habitat.String.installed) Habitat.String.install(global) 621 | if (!Habitat.Touches.installed) Habitat.Touches.install(global) 622 | if (!Habitat.Tween.installed) Habitat.Tween.install(global) 623 | if (!Habitat.Type.installed) Habitat.Type.install(global) 624 | 625 | Habitat.installed = true 626 | 627 | } 628 | 629 | //======// 630 | // Math // 631 | //======// 632 | { 633 | 634 | const gcd = (...numbers) => { 635 | const [head, ...tail] = numbers 636 | if (numbers.length === 1) return head 637 | if (numbers.length > 2) return gcd(head, gcd(...tail)) 638 | 639 | let [a, b] = [head, ...tail] 640 | 641 | while (true) { 642 | if (b === 0) return a 643 | a = a % b 644 | if (a === 0) return b 645 | b = b % a 646 | } 647 | 648 | } 649 | 650 | const reduce = (...numbers) => { 651 | const divisor = gcd(...numbers) 652 | return numbers.map(n => n / divisor) 653 | } 654 | 655 | const wrap = (number, min, max) => { 656 | const difference = max - min 657 | while (number > max) { 658 | number -= difference 659 | } 660 | while (number < min) { 661 | number += difference 662 | } 663 | return number 664 | } 665 | 666 | const install = (global) => { 667 | global.Math.gcd = Habitat.Math.gcd 668 | global.Math.reduce = Habitat.Math.reduce 669 | global.Math.wrap = Habitat.Math.wrap 670 | Habitat.Math.installed = true 671 | } 672 | 673 | 674 | Habitat.Math = {install, gcd, reduce, wrap} 675 | 676 | } 677 | 678 | 679 | //=======// 680 | // Mouse // 681 | //=======// 682 | { 683 | 684 | const Mouse = Habitat.Mouse = { 685 | position: [undefined, undefined], 686 | } 687 | 688 | const buttonMap = ["Left", "Middle", "Right", "Back", "Forward"] 689 | 690 | Reflect.defineProperty(Mouse, "install", { 691 | value(global) { 692 | global.Mouse = Mouse 693 | global.addEventListener("mousedown", e => { 694 | const buttonName = buttonMap[e.button] 695 | Mouse[buttonName] = true 696 | }) 697 | 698 | global.addEventListener("mouseup", e => { 699 | const buttonName = buttonMap[e.button] 700 | Mouse[buttonName] = false 701 | }) 702 | 703 | global.addEventListener("mousemove", e => { 704 | Mouse.position[0] = event.clientX 705 | Mouse.position[1] = event.clientY 706 | }) 707 | 708 | Reflect.defineProperty(Mouse, "installed", { 709 | value: true, 710 | configurable: true, 711 | enumerable: false, 712 | writable: true, 713 | }) 714 | }, 715 | configurable: true, 716 | enumerable: false, 717 | writable: true, 718 | }) 719 | 720 | } 721 | 722 | 723 | //========// 724 | // Number // 725 | //========// 726 | { 727 | 728 | const install = (global) => { 729 | 730 | Reflect.defineProperty(global.Number.prototype, "to", { 731 | value: function* (v) { 732 | let i = this.valueOf() 733 | if (i <= v) { 734 | while (i <= v) { 735 | yield i 736 | i++ 737 | } 738 | } 739 | else { 740 | while (i >= v) { 741 | yield i 742 | i-- 743 | } 744 | } 745 | }, 746 | configurable: true, 747 | enumerable: false, 748 | writable: true, 749 | }) 750 | 751 | const numberToString = global.Number.prototype.toString 752 | Reflect.defineProperty(global.Number.prototype, "toString", { 753 | value(base, size) { 754 | if (size === undefined) return numberToString.call(this, base) 755 | if (size <= 0) return "" 756 | const string = numberToString.call(this, base) 757 | return string.slice(-size).padStart(size, "0") 758 | }, 759 | configurable: true, 760 | enumerable: false, 761 | writable: true, 762 | }) 763 | 764 | if (global.BigInt !== undefined) { 765 | const bigIntToString = global.BigInt.prototype.toString 766 | Reflect.defineProperty(global.BigInt.prototype, "toString", { 767 | value(base, size) { 768 | if (size === undefined) return bigIntToString.call(this, base) 769 | if (size <= 0) return "" 770 | const string = bigIntToString.call(this, base) 771 | return string.slice(-size).padStart(size, "0") 772 | }, 773 | configurable: true, 774 | enumerable: false, 775 | writable: true, 776 | }) 777 | } 778 | 779 | Habitat.Number.installed = true 780 | 781 | } 782 | 783 | Habitat.Number = {install} 784 | 785 | } 786 | 787 | //========// 788 | // Object // 789 | //========// 790 | { 791 | Habitat.Object = {} 792 | Habitat.Object.install = (global) => { 793 | 794 | Reflect.defineProperty(global.Object.prototype, Symbol.iterator, { 795 | value: function*() { 796 | for (const key in this) { 797 | yield this[key] 798 | } 799 | }, 800 | configurable: true, 801 | enumerable: false, 802 | writable: true, 803 | }) 804 | 805 | Reflect.defineProperty(global.Object.prototype, "keys", { 806 | value() { 807 | return Object.keys(this) 808 | }, 809 | configurable: true, 810 | enumerable: false, 811 | writable: true, 812 | }) 813 | 814 | Reflect.defineProperty(global.Object.prototype, "values", { 815 | value() { 816 | return Object.values(this) 817 | }, 818 | configurable: true, 819 | enumerable: false, 820 | writable: true, 821 | }) 822 | 823 | Reflect.defineProperty(global.Object.prototype, "entries", { 824 | value() { 825 | return Object.entries(this) 826 | }, 827 | configurable: true, 828 | enumerable: false, 829 | writable: true, 830 | }) 831 | 832 | Habitat.Object.installed = true 833 | 834 | } 835 | 836 | } 837 | 838 | //==========// 839 | // Property // 840 | //==========// 841 | { 842 | 843 | const install = (global) => { 844 | 845 | Reflect.defineProperty(global.Object.prototype, "_", { 846 | get() { 847 | return new Proxy(this, { 848 | set(object, propertyName, descriptor) { 849 | Reflect.defineProperty(object, propertyName, descriptor) 850 | }, 851 | get(object, propertyName) { 852 | const editor = { 853 | get value() { 854 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 855 | const {value} = descriptor 856 | return value 857 | }, 858 | set value(value) { 859 | const {enumerable, configurable, writable} = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, configurable: true, writable: true} 860 | const descriptor = {value, enumerable, configurable, writable} 861 | Reflect.defineProperty(object, propertyName, descriptor) 862 | }, 863 | get get() { 864 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 865 | const {get} = descriptor 866 | return get 867 | }, 868 | set get(get) { 869 | const {set, enumerable, configurable} = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, configurable: true} 870 | const descriptor = {get, set, enumerable, configurable} 871 | Reflect.defineProperty(object, propertyName, descriptor) 872 | }, 873 | get set() { 874 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 875 | const {set} = descriptor 876 | return set 877 | }, 878 | set set(set) { 879 | const {get, enumerable, configurable} = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, configurable: true} 880 | const descriptor = {get, set, enumerable, configurable} 881 | Reflect.defineProperty(object, propertyName, descriptor) 882 | }, 883 | get enumerable() { 884 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 885 | const {enumerable} = descriptor 886 | return enumerable 887 | }, 888 | set enumerable(v) { 889 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {configurable: true, writable: true} 890 | descriptor.enumerable = v 891 | Reflect.defineProperty(object, propertyName, descriptor) 892 | }, 893 | get configurable() { 894 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 895 | const {configurable} = descriptor 896 | return configurable 897 | }, 898 | set configurable(v) { 899 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, writable: true} 900 | descriptor.configurable = v 901 | Reflect.defineProperty(object, propertyName, descriptor) 902 | }, 903 | get writable() { 904 | const descriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {} 905 | const {writable} = descriptor 906 | return writable 907 | }, 908 | set writable(v) { 909 | const oldDescriptor = Reflect.getOwnPropertyDescriptor(object, propertyName) || {enumerable: true, configurable: true} 910 | const {get, set, writable, ...rest} = oldDescriptor 911 | const newDescriptor = {...rest, writable: v} 912 | Reflect.defineProperty(object, propertyName, newDescriptor) 913 | }, 914 | } 915 | return editor 916 | }, 917 | }) 918 | }, 919 | set(value) { 920 | Reflect.defineProperty(this, "_", {value, configurable: true, writable: true, enumerable: true}) 921 | }, 922 | configurable: true, 923 | enumerable: false, 924 | }) 925 | 926 | 927 | Habitat.Property.installed = true 928 | 929 | } 930 | 931 | Habitat.Property = {install} 932 | 933 | } 934 | 935 | //========// 936 | // Random // 937 | //========// 938 | { 939 | Habitat.Random = {} 940 | 941 | const maxId8 = 2 ** 16 942 | const u8s = new Uint8Array(maxId8) 943 | let id8 = maxId8 944 | const getRandomUint8 = () => { 945 | 946 | if (id8 >= maxId8) { 947 | crypto.getRandomValues(u8s) 948 | id8 = 0 949 | } 950 | 951 | const result = u8s[id8] 952 | id8++ 953 | return result 954 | } 955 | 956 | Reflect.defineProperty(Habitat.Random, "Uint8", { 957 | get: getRandomUint8, 958 | configurable: true, 959 | enumerable: true, 960 | }) 961 | 962 | const maxId32 = 2 ** 14 963 | const u32s = new Uint32Array(maxId32) 964 | let id32 = maxId32 965 | const getRandomUint32 = () => { 966 | 967 | if (id32 >= maxId32) { 968 | crypto.getRandomValues(u32s) 969 | id32 = 0 970 | } 971 | 972 | const result = u32s[id32] 973 | id32++ 974 | return result 975 | } 976 | 977 | Reflect.defineProperty(Habitat.Random, "Uint32", { 978 | get: getRandomUint32, 979 | configurable: true, 980 | enumerable: true, 981 | }) 982 | 983 | Habitat.Random.oneIn = (n) => { 984 | const result = getRandomUint32() 985 | return result % n === 0 986 | } 987 | 988 | Habitat.Random.maybe = (chance) => { 989 | return Habitat.Random.oneIn(1 / chance) 990 | } 991 | 992 | Habitat.Random.install = (global) => { 993 | global.Random = Habitat.Random 994 | global.oneIn = Habitat.Random.oneIn 995 | global.maybe = Habitat.Random.maybe 996 | Habitat.Random.installed = true 997 | } 998 | 999 | } 1000 | 1001 | //=======// 1002 | // Stage // 1003 | //=======// 1004 | { 1005 | 1006 | Habitat.Stage = {} 1007 | Habitat.Stage.make = () => { 1008 | 1009 | const canvas = document.createElement("canvas") 1010 | const context = canvas.getContext("2d") 1011 | 1012 | const stage = { 1013 | canvas, 1014 | context, 1015 | update: () => {}, 1016 | draw: () => {}, 1017 | tick: () => { 1018 | stage.update() 1019 | stage.draw() 1020 | requestAnimationFrame(stage.tick) 1021 | }, 1022 | } 1023 | 1024 | requestAnimationFrame(stage.tick) 1025 | return stage 1026 | } 1027 | 1028 | Habitat.Stage.install = (global) => { 1029 | global.Stage = Habitat.Stage 1030 | Habitat.Stage.installed = true 1031 | 1032 | } 1033 | 1034 | } 1035 | 1036 | //========// 1037 | // String // 1038 | //========// 1039 | { 1040 | 1041 | const install = (global) => { 1042 | 1043 | Reflect.defineProperty(global.String.prototype, "divide", { 1044 | value(n) { 1045 | const regExp = RegExp(`[^]{1,${n}}`, "g") 1046 | return this.match(regExp) 1047 | }, 1048 | configurable: true, 1049 | enumerable: false, 1050 | writable: true, 1051 | }) 1052 | 1053 | Reflect.defineProperty(global.String.prototype, "toNumber", { 1054 | value(base) { 1055 | return parseInt(this, base) 1056 | }, 1057 | configurable: true, 1058 | enumerable: false, 1059 | writable: true, 1060 | }) 1061 | 1062 | Habitat.String.installed = true 1063 | 1064 | } 1065 | 1066 | Habitat.String = {install} 1067 | 1068 | } 1069 | 1070 | //=======// 1071 | // Touch // 1072 | //=======// 1073 | { 1074 | 1075 | const Touches = Habitat.Touches = new Map() 1076 | Reflect.defineProperty(Touches, 'first', { 1077 | get() { 1078 | return this.values().next().value 1079 | }, 1080 | enumerable: true, 1081 | configurable: true, 1082 | }) 1083 | 1084 | 1085 | Reflect.defineProperty(Touches, "install", { 1086 | value(global) { 1087 | 1088 | global.Touches = Touches 1089 | global.addEventListener("touchstart", e => { 1090 | for (const changedTouch of e.changedTouches) { 1091 | const x = changedTouch.clientX 1092 | const y = changedTouch.clientY 1093 | const id = changedTouch.identifier 1094 | if (!Touches.has(id)) Touches.set(id, {position: [undefined, undefined]}) 1095 | const touch = Touches.get(id) 1096 | touch.position[0] = x 1097 | touch.position[1] = y 1098 | } 1099 | }) 1100 | 1101 | global.addEventListener("touchmove", e => { 1102 | try { 1103 | for (const changedTouch of e.changedTouches) { 1104 | const x = changedTouch.clientX 1105 | const y = changedTouch.clientY 1106 | const id = changedTouch.identifier 1107 | let touch = Touches.get(id) 1108 | if (touch == undefined) { 1109 | touch = {position: [undefined, undefined]} 1110 | Touches.set(id, touch) 1111 | } 1112 | 1113 | touch.position[0] = x 1114 | touch.position[1] = y 1115 | } 1116 | } 1117 | catch(e) { 1118 | console.error(e) 1119 | } 1120 | }) 1121 | 1122 | global.addEventListener("touchend", e => { 1123 | for (const changedTouch of e.changedTouches) { 1124 | const id = changedTouch.identifier 1125 | Touches.delete(id) 1126 | } 1127 | }) 1128 | 1129 | Reflect.defineProperty(Touches, "installed", { 1130 | value: true, 1131 | configurable: true, 1132 | enumerable: false, 1133 | writable: true, 1134 | }) 1135 | }, 1136 | configurable: true, 1137 | enumerable: false, 1138 | writable: true, 1139 | }) 1140 | 1141 | 1142 | } 1143 | 1144 | 1145 | //=======// 1146 | // Tween // 1147 | //=======// 1148 | 1149 | { 1150 | Habitat.Tween = {} 1151 | 1152 | // all from https://easings.net 1153 | Habitat.Tween.EASE_IN_LINEAR = (t) => t 1154 | Habitat.Tween.EASE_OUT_LINEAR = (t) => t 1155 | Habitat.Tween.EASE_IN_OUT_LINEAR = (t) => t 1156 | Habitat.Tween.EASE_IN_SINE = (t) => 1-Math.cos(t*Math.PI/2) 1157 | Habitat.Tween.EASE_OUT_SINE = (t) => Math.sin(t*Math.PI/2) 1158 | Habitat.Tween.EASE_IN_OUT_SINE = (t) => -(Math.cos(t*Math.PI)-1)/2 1159 | Habitat.Tween.EASE_IN_POWER = (p) => (t) => Math.pow(t, p) 1160 | Habitat.Tween.EASE_OUT_POWER = (p) => (t) => 1-Math.pow(1-t, p) 1161 | Habitat.Tween.EASE_IN_OUT_POWER = (p) => (t) => { 1162 | if (t < 0.5) return Math.pow(2, p-1)*Math.pow(t, p) 1163 | return 1 - Math.pow(2 - 2*t, p)/2 1164 | } 1165 | Habitat.Tween.EASE_IN_EXP = Habitat.Tween.EASE_IN_EXPONENTIAL = (e) => (t) => Math.pow(2, e*t - e) * t 1166 | Habitat.Tween.EASE_OUT_EXP = Habitat.Tween.EASE_OUT_EXPONENTIAL = (e) => (t) => 1 - Math.pow(2, -e*t) * (1-t) 1167 | Habitat.Tween.EASE_IN_OUT_EXP = Habitat.Tween.EASE_IN_OUT_EXPONENTIAL = (e) => (t) => { 1168 | let f; 1169 | if (t < 0.5) f = t => Math.pow(2, 2*e*t - e)/2 1170 | else f = t => (2 - Math.pow(2, -2*e*t + e))/2 1171 | return f(t) * ((1-t)*f(0) + t*(f(1)-1)) 1172 | } 1173 | Habitat.Tween.EASE_IN_CIRCULAR = (t) => 1 - Math.sqrt(1 - Math.pow(t, 2)) 1174 | Habitat.Tween.EASE_OUT_CIRCULAR = (t) => Math.sqrt(1 - Math.pow(t - 1, 2)) 1175 | Habitat.Tween.EASE_IN_OUT_CIRCULAR = (t) => { 1176 | if (t < 0.5) return 0.5 - Math.sqrt(1 - Math.pow(2*t, 2))/2 1177 | return 0.5 + Math.sqrt(1 - Math.pow(-2*t + 2, 2))/2 1178 | } 1179 | Habitat.Tween.EASE_IN_BACK = (t) => 2.70158*t*t*t - 1.70158*t*t 1180 | Habitat.Tween.EASE_OUT_BACK = (t) => 1 + 2.70158*Math.pow(t - 1, 3) + 1.70158*Math.pow(t - 1, 2) 1181 | Habitat.Tween.EASE_IN_OUT_BACK = (t) => { 1182 | if (t < 0.5) return (Math.pow(2*t, 2)*(3.59491*2*t - 2.59491))/2 1183 | return (Math.pow(2*t-2,2)*(3.59491*(t*2-2) + 2.59491)+2)/2 1184 | } 1185 | Habitat.Tween.EASE_IN_ELASTIC = (t) => -Math.pow(2,10*t-10)*Math.sin((t*10-10.75)*2*Math.PI/3) 1186 | Habitat.Tween.EASE_OUT_ELASTIC = (t) => Math.pow(2,-10*t)*Math.sin((t*10-0.75)*2*Math.PI/3)+1 1187 | Habitat.Tween.EASE_IN_OUT_ELASTIC = (t) => { 1188 | if (t < 0.5) return -(Math.pow(2, 20*t-10)*Math.sin((20*t-11.125)*2*Math.PI/4.5))/2 1189 | return (Math.pow(2, -20*t+10)*Math.sin((20*t-11.125)*2*Math.PI/4.5))/2+1 1190 | } 1191 | Habitat.Tween.EASE_OUT_BOUNCE = (t) => (t) => { 1192 | const n1 = 7.5625 1193 | const d1 = 2.75 1194 | 1195 | if (t < 1 / d1) return n1 * t * t 1196 | else if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75 1197 | else if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375 1198 | else return n1 * (t -= 2.625 / d1) * t + 0.984375 1199 | } 1200 | Habitat.Tween.EASE_IN_BOUNCE = (t) => 1-Habitat.Tween.EASE_OUT_BOUNCE(1-t) 1201 | Habitat.Tween.EASE_IN_OUT_BOUNCE = (t) => { 1202 | if (t < 0.5) return (1-Habitat.Tween.EASE_OUT_BOUNCE(1-2*t))/2 1203 | return (1+Habitat.Tween.EASE_OUT_BOUNCE(2*t-1))/2 1204 | } 1205 | 1206 | Habitat.Tween.install = (global) => { 1207 | Habitat.Tween.installed = true 1208 | 1209 | Reflect.defineProperty(global.Object.prototype, "tween", { 1210 | value(propertyName, {to, from, over = 1000, launch = 0.5, land = 0.5, strength = 1, ease = false} = {}) { 1211 | const before = this[propertyName] 1212 | if (from === undefined) from = before 1213 | if (to === undefined) to = before 1214 | 1215 | launch *= 2/3 1216 | land = 1/3 + (1 - land) * 2/3 1217 | 1218 | const start = performance.now() 1219 | 1220 | Reflect.defineProperty(this, propertyName, { 1221 | get() { 1222 | const now = performance.now() 1223 | 1224 | if (now > start + over) { 1225 | Reflect.defineProperty(this, propertyName, { 1226 | value: to, 1227 | writable: true, 1228 | configurable: true, 1229 | enumerable: true 1230 | }) 1231 | return to 1232 | } 1233 | 1234 | const t = (now - start) / over 1235 | 1236 | if (ease) { 1237 | const v = ease(strength) 1238 | if (typeof v == 'function') return v(t) * (to - from) + from 1239 | return ease(t) * (to - from) + from 1240 | } 1241 | 1242 | const v = 3*t*(1-t)*(1-t)*launch + 3*t*t*(1-t)*land + t*t*t 1243 | return v * (to - from) + from 1244 | 1245 | }, 1246 | set() { }, 1247 | 1248 | configurable: true, 1249 | enumerable: true 1250 | }) 1251 | }, 1252 | 1253 | configurable: true, 1254 | enumerable: false, 1255 | writable: true 1256 | }) 1257 | } 1258 | } 1259 | 1260 | 1261 | //======// 1262 | // Type // 1263 | //======// 1264 | { 1265 | 1266 | const Int = { 1267 | check: (n) => n % 1 == 0, 1268 | convert: (n) => parseInt(n), 1269 | } 1270 | 1271 | const Positive = { 1272 | check: (n) => n >= 0, 1273 | convert: (n) => Math.abs(n), 1274 | } 1275 | 1276 | const Negative = { 1277 | check: (n) => n <= 0, 1278 | convert: (n) => -Math.abs(n), 1279 | } 1280 | 1281 | const UInt = { 1282 | check: (n) => n % 1 == 0 && n >= 0, 1283 | convert: (n) => Math.abs(parseInt(n)), 1284 | } 1285 | 1286 | const UpperCase = { 1287 | check: (s) => s == s.toUpperCase(), 1288 | convert: (s) => s.toUpperCase(), 1289 | } 1290 | 1291 | const LowerCase = { 1292 | check: (s) => s == s.toLowerCase(), 1293 | convert: (s) => s.toLowerCase(), 1294 | } 1295 | 1296 | const WhiteSpace = { 1297 | check: (s) => /^[ | ]*$/.test(s), 1298 | } 1299 | 1300 | const PureObject = { 1301 | check: (o) => o.constructor == Object, 1302 | } 1303 | 1304 | const Primitive = { 1305 | check: p => p.is(Number) || p.is(String) || p.is(RegExp) || p.is(Symbol), 1306 | } 1307 | 1308 | const install = (global) => { 1309 | 1310 | global.Int = Int 1311 | global.Positive = Positive 1312 | global.Negative = Negative 1313 | global.UInt = UInt 1314 | global.UpperCase = UpperCase 1315 | global.LowerCase = LowerCase 1316 | global.WhiteSpace = WhiteSpace 1317 | global.PureObject = PureObject 1318 | global.Primitive = Primitive 1319 | 1320 | Reflect.defineProperty(global.Object.prototype, "is", { 1321 | value(type) { 1322 | if ("check" in type) { 1323 | try { return type.check(this) } 1324 | catch {} 1325 | } 1326 | try { return this instanceof type } 1327 | catch { return false } 1328 | }, 1329 | configurable: true, 1330 | enumerable: false, 1331 | writable: true, 1332 | }) 1333 | 1334 | Reflect.defineProperty(global.Object.prototype, "as", { 1335 | value(type) { 1336 | if ("convert" in type) { 1337 | try { return type.convert(this) } 1338 | catch {} 1339 | } 1340 | return type(this) 1341 | }, 1342 | configurable: true, 1343 | enumerable: false, 1344 | writable: true, 1345 | }) 1346 | 1347 | Habitat.Type.installed = true 1348 | 1349 | } 1350 | 1351 | Habitat.Type = {install, Int, Positive, Negative, UInt, UpperCase, LowerCase, WhiteSpace, PureObject, Primitive} 1352 | 1353 | } -------------------------------------------------------------------------------- /libraries/perfect-freehand.js: -------------------------------------------------------------------------------- 1 | function $(e,t,s,x=h=>h){return e*x(.5-t*(.5-s))}function ue(e){return[-e[0],-e[1]]}function l(e,t){return[e[0]+t[0],e[1]+t[1]]}function a(e,t){return[e[0]-t[0],e[1]-t[1]]}function b(e,t){return[e[0]*t,e[1]*t]}function he(e,t){return[e[0]/t,e[1]/t]}function R(e){return[e[1],-e[0]]}function B(e,t){return e[0]*t[0]+e[1]*t[1]}function se(e,t){return e[0]===t[0]&&e[1]===t[1]}function ge(e){return Math.hypot(e[0],e[1])}function de(e){return e[0]*e[0]+e[1]*e[1]}function A(e,t){return de(a(e,t))}function G(e){return he(e,ge(e))}function ie(e,t){return Math.hypot(e[1]-t[1],e[0]-t[0])}function L(e,t,s){let x=Math.sin(s),h=Math.cos(s),y=e[0]-t[0],n=e[1]-t[1],f=y*h-n*x,d=y*x+n*h;return[f+t[0],d+t[1]]}function K(e,t,s){return l(e,b(a(t,e),s))}function ee(e,t,s){return l(e,b(t,s))}var{min:C,PI:xe}=Math,pe=.275,V=xe+1e-4;function me(e,t={}){let{size:s=16,smoothing:x=.5,thinning:h=.5,simulatePressure:y=!0,easing:n=r=>r,start:f={},end:d={},last:D=!1}=t,{cap:S=!0,easing:j=r=>r*(2-r)}=f,{cap:q=!0,easing:m=r=>--r*r*r+1}=d;if(e.length===0||s<=0)return[];let p=e[e.length-1].runningLength,g=f.taper===!1?0:f.taper===!0?Math.max(s,p):f.taper,T=d.taper===!1?0:d.taper===!0?Math.max(s,p):d.taper,te=Math.pow(s*x,2),_=[],M=[],H=e.slice(0,10).reduce((r,i)=>{let o=i.pressure;if(y){let u=C(1,i.distance/s),W=C(1,1-u);o=C(1,r+(W-r)*(u*pe))}return(r+o)/2},e[0].pressure),c=$(s,h,e[e.length-1].pressure,n),U,X=e[0].vector,z=e[0].point,F=z,O=z,E=F,J=!1;for(let r=0;rte)&&(_.push(O),z=O),E=l(o,oe),(r<=1||A(F,E)>te)&&(M.push(E),F=E),H=i,X=u}let P=e[0].point.slice(0,2),k=e.length>1?e[e.length-1].point.slice(0,2):l(e[0].point,[1,1]),Q=[],N=[];if(e.length===1){if(!(g||T)||D){let r=ee(P,G(R(a(P,k))),-(U||c)),i=[];for(let o=1/13,u=o;u<=1;u+=o)i.push(L(r,P,V*2*u));return i}}else{if(!(g||T&&e.length===1))if(S)for(let i=1/13,o=i;o<=1;o+=i){let u=L(M[0],P,V*o);Q.push(u)}else{let i=a(_[0],M[0]),o=b(i,.5),u=b(i,.51);Q.push(a(P,o),a(P,u),l(P,u),l(P,o))}let r=R(ue(e[e.length-1].vector));if(T||g&&e.length===1)N.push(k);else if(q){let i=ee(k,r,c);for(let o=1/29,u=o;u<1;u+=o)N.push(L(i,k,V*3*u))}else N.push(l(k,b(r,c)),l(k,b(r,c*.99)),a(k,b(r,c*.99)),a(k,b(r,c)))}return _.concat(N,M.reverse(),Q)}function ce(e,t={}){var q;let{streamline:s=.5,size:x=16,last:h=!1}=t;if(e.length===0)return[];let y=.15+(1-s)*.85,n=Array.isArray(e[0])?e:e.map(({x:m,y:p,pressure:g=.5})=>[m,p,g]);if(n.length===2){let m=n[1];n=n.slice(0,-1);for(let p=1;p<5;p++)n.push(K(n[0],m,p/4))}n.length===1&&(n=[...n,[...l(n[0],[1,1]),...n[0].slice(2)]]);let f=[{point:[n[0][0],n[0][1]],pressure:n[0][2]>=0?n[0][2]:.25,vector:[1,1],distance:0,runningLength:0}],d=!1,D=0,S=f[0],j=n.length-1;for(let m=1;m=0?n[m][2]:.5,vector:G(a(S.point,p)),distance:g,runningLength:D},f.push(S)}return f[0].vector=((q=f[1])==null?void 0:q.vector)||[0,0],f}function ae(e,t={}){return me(ce(e,t),t)}var ze=ae;export{ze as default,ae as getStroke,me as getStrokeOutlinePoints,ce as getStrokePoints}; 2 | -------------------------------------------------------------------------------- /libraries/show.js: -------------------------------------------------------------------------------- 1 | const CanvasShow = {} 2 | 3 | { 4 | 5 | CanvasShow.make = ({canvas, context, paused = false, scale = 1.0, aspect, speed = 1.0, resize = () => {}, tick = () => {}, supertick = () => {}} = {}) => { 6 | 7 | const show = {canvas, context, paused, scale, speed, resize, tick, supertick} 8 | 9 | if (document.body === null) { 10 | addEventListener("load", () => start(show)) 11 | } else { 12 | start(show) 13 | } 14 | 15 | return show 16 | } 17 | 18 | const start = (show) => { 19 | 20 | // TODO: support canvases of different sizes. just for provided ones? or all? 21 | if (show.canvas === undefined) { 22 | document.body.style["margin"] = "0px" 23 | document.body.style["overflow"] = "hidden" 24 | document.body.style["background-color"] = Colour.Black 25 | 26 | show.canvas = document.createElement("canvas") 27 | show.canvas.style["background-color"] = Colour.Black 28 | //show.canvas.style["image-rendering"] = "pixelated" 29 | document.body.appendChild(show.canvas) 30 | } 31 | 32 | if (show.context === undefined) { 33 | show.context = show.canvas.getContext("2d") 34 | } 35 | 36 | const resize = () => { 37 | 38 | show.canvas.width = (innerWidth * 1) 39 | show.canvas.height = (innerHeight * 1) 40 | 41 | window.shrinkScore = 1 42 | /*if (show.canvas.width > 1920 || show.canvas.height > 1920) { 43 | window.shrinkScore++ 44 | show.canvas.width = Math.round(show.canvas.width / 2) 45 | show.canvas.height = Math.round(show.canvas.height / 2) 46 | }*/ 47 | 48 | show.canvas.style["width"] = (innerWidth) 49 | show.canvas.style["height"] = (innerHeight) 50 | 51 | show.resize(show.context, show.canvas) 52 | } 53 | 54 | let t = 0 55 | const tick = () => { 56 | 57 | t += show.speed 58 | while (t > 0) { 59 | if (!show.paused) show.tick(show.context, show.canvas) 60 | show.supertick(show.context, show.canvas) 61 | t-- 62 | } 63 | 64 | requestAnimationFrame(tick) 65 | } 66 | 67 | 68 | addEventListener("resize", resize) 69 | addEventListener("keydown", (e) => { 70 | if (e.key === " ") show.paused = !show.paused 71 | }) 72 | 73 | resize() 74 | requestAnimationFrame(tick) 75 | 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /libraries/svgShow.js: -------------------------------------------------------------------------------- 1 | const Show = {} 2 | 3 | { 4 | 5 | Show.make = ({layers, container, paused = false, scale = 1.0, aspect, speed = 1.0, resize = () => {}, tick = () => {}, supertick = () => {}, layerCount = 1} = {}, pause = () => {}) => { 6 | if (layers !== undefined) { 7 | layerCount = layers.length; 8 | } 9 | const show = {layers, layerCount, paused, scale, speed, resize, tick, supertick} 10 | 11 | if (document.body === null) { 12 | addEventListener("load", () => start(show)) 13 | } else { 14 | start(show) 15 | } 16 | 17 | return show 18 | } 19 | 20 | const start = (show) => { 21 | 22 | 23 | if (show.layers === undefined) { 24 | if (show.container === undefined) { 25 | show.container = document.body 26 | } 27 | document.body.style["margin"] = "0px" 28 | document.body.style["overflow"] = "hidden" 29 | document.body.style["background-color"] = Colour.Black 30 | 31 | show.layers = new Array(show.layerCount) 32 | for (let i = 0; i < show.layerCount; i++) { 33 | show.layers[i] = document.createElementNS("http://www.w3.org/2000/svg", "svg") 34 | show.layers[i].style["position"] = "absolute" 35 | document.body.appendChild(show.layers[i]) 36 | } 37 | //show.layers[0].style["background-color"] = Colour.Black 38 | } 39 | 40 | show.layers.first = show.layers[0] 41 | show.layers.last = show.layers[show.layers.length - 1] 42 | 43 | const resize = () => { 44 | show.layers.forEach((layer) => { 45 | layer.width = (innerWidth * 1) 46 | layer.height = (innerHeight * 1) 47 | 48 | window.shrinkScore = 1 49 | 50 | layer.style["width"] = (innerWidth) 51 | layer.style["height"] = (innerHeight) 52 | 53 | layer.setAttributeNS("http://www.w3.org/2000/svg", 'viewBox', `0 0 ${innerWidth} ${innerHeight}`) 54 | }) 55 | show.resize(show.layers) 56 | } 57 | 58 | let t = 0 59 | const tick = () => { 60 | 61 | t += show.speed 62 | while (t > 0) { 63 | if (!show.paused) show.tick(show.layers) 64 | show.supertick(show.layers) 65 | t-- 66 | } 67 | 68 | requestAnimationFrame(tick) 69 | } 70 | 71 | 72 | addEventListener("resize", resize) 73 | addEventListener("keydown", (e) => { 74 | if (e.key === " ") { 75 | show.paused = !show.paused 76 | show.pause(show.paused, show.layers) 77 | } 78 | }) 79 | 80 | resize() 81 | requestAnimationFrame(tick) 82 | 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | import { getStroke, getStrokeOutlinePoints } from "./libraries/perfect-freehand.js" 2 | 3 | //===========// 4 | // UTILITIES // 5 | //===========// 6 | const clamp = (n, min, max) => { 7 | if (n < min) return min 8 | if (n > max) return max 9 | return n 10 | } 11 | 12 | // https://stackoverflow.com/questions/17410809/how-to-calculate-rotation-in-2d-in-javascript 13 | function rotatePoint (cx, cy, x, y, angle) { 14 | const cos = Math.cos(-angle) 15 | const sin = Math.sin(-angle) 16 | const nx = (cos * (x - cx)) + (sin * (y - cy)) + cx 17 | const ny = (cos * (y - cy)) - (sin * (x - cx)) + cy 18 | return {x: nx, y: ny} 19 | } 20 | 21 | //=========// 22 | // PAINTER // 23 | //=========// 24 | const makePainter = ({ 25 | sources, 26 | offsetX = 0, 27 | offsetY = 0, 28 | centerX = 0.5, 29 | centerY = 0.5, 30 | scale = 1.0, 31 | speed = 0.1, 32 | maxSpeed = speed, 33 | minSpeed = maxSpeed * 0.3, 34 | acceleration = 0.001, 35 | dr = 0.05, 36 | frameRate = 24, 37 | speedR = 1.0, 38 | idleFadePower = 1.0, 39 | wobble = 1.0, 40 | idleFrequency = { x: 500, y: 750 }, 41 | strokeOptions = {}, 42 | lockAxis = false, 43 | } = {}) => { 44 | 45 | const images = sources.map(() => { 46 | const image = document.createElementNS("http://www.w3.org/2000/svg", "image") 47 | image.style.visibility = "hidden" 48 | return image 49 | }) 50 | 51 | const ready = (async () => { 52 | const c = document.createElement("canvas"); 53 | const ctx = c.getContext("2d"); 54 | 55 | // A promise that awaits the loading of all resources before resolving. 56 | await Promise.all(sources.map(async (src, i) => { 57 | const img = new Image() 58 | img.crossOrigin = "" 59 | img.src = src 60 | img.loading = "eager" 61 | if (!img.complete) { 62 | await new Promise((resolve, reject) => { 63 | img.addEventListener('load', () => { 64 | c.width = img.naturalWidth; // update canvas size to match image 65 | c.height = img.naturalHeight; 66 | ctx.drawImage(img, 0, 0); // draw in image 67 | const image = images[i] 68 | image.setAttribute("width", img.width * scale) 69 | image.setAttribute("height", img.height * scale) 70 | image.setAttribute('href', c.toDataURL()) // update image with new data 71 | resolve() 72 | }) 73 | img.addEventListener('error', reject) 74 | }) 75 | } 76 | })) 77 | })() 78 | 79 | const painter = { 80 | images, 81 | frame: 0, 82 | frameRate, 83 | age: 0, 84 | scale, 85 | x: 0, 86 | y: 0, 87 | dx: 0, 88 | dy: 0, 89 | dr, 90 | offsetX, 91 | offsetY, 92 | centerX, 93 | centerY, 94 | speed, 95 | maxSpeed, 96 | minSpeed, 97 | acceleration, 98 | isPainting: false, 99 | brushdx: 0, 100 | brushdy: 0, 101 | r: 0, 102 | targetR: 0, 103 | speedR, 104 | wobble, 105 | idleFadePower, 106 | idleFrequency, 107 | strokeOptions, 108 | ready, 109 | lockAxis, 110 | } 111 | 112 | return painter 113 | } 114 | 115 | const drawPainter = (painter) => { 116 | const {images, frame, r} = painter 117 | const image = images[frame] 118 | const width = image.width.baseVal.value 119 | const height = image.height.baseVal.value 120 | const cx = painter.x + width * painter.centerX 121 | const cy = painter.y + height * painter.centerY 122 | 123 | image.setAttribute("x", cx - width/2) 124 | image.setAttribute("y", cy - height/2) 125 | image.setAttribute("transform", `rotate(${r * 180 / Math.PI}, ${cx}, ${cy})`) 126 | } 127 | 128 | const getBrushPosition = (painter) => { 129 | const image = painter.images[painter.frame] 130 | const width = image.width.baseVal.value 131 | const height = image.height.baseVal.value 132 | const x = painter.x - painter.offsetX * painter.scale 133 | const y = painter.y - painter.offsetY * painter.scale 134 | let cx = painter.x + width*painter.centerX 135 | let cy = painter.y + height*painter.centerY 136 | 137 | const point = rotatePoint(cx, cy, x, y, painter.r) 138 | return point 139 | } 140 | 141 | let lastBrushWasTouch = false 142 | let restingPosition = [0, 0] 143 | on.mousemove(() => restingPosition = Mouse.position.map(v => v * 1 / (window.shrinkScore))) 144 | on.touchstart(e => e.preventDefault(), {passive: false}) 145 | 146 | let previousPosition = [0, 0] 147 | const updatePainter = (layers, strokeHistoryContainer, currentStrokeContainer, painter, paths, colour) => { 148 | if (painter.isPainting) { 149 | painter.idleFadePower -= 0.01 150 | } else { 151 | painter.idleFadePower += 0.01 152 | } 153 | 154 | painter.idleFadePower = clamp(painter.idleFadePower, 0.0, 1.0) 155 | 156 | painter.age++ 157 | if (painter.age > 255) { 158 | painter.age = 0 159 | } 160 | if (painter.age % (60 / painter.frameRate) === 0) { 161 | painter.images[painter.frame].style.visibility = "hidden" 162 | painter.frame++ 163 | if (painter.frame >= painter.images.length) { 164 | painter.frame = 0 165 | } 166 | painter.images[painter.frame].style.visibility = "visible" 167 | } 168 | 169 | const acceleration = painter.acceleration * (Mouse.Right? -1 : 1) 170 | painter.speed = clamp(painter.speed + acceleration, painter.minSpeed, painter.maxSpeed) 171 | 172 | const brush = getBrushPosition(painter) 173 | 174 | let [mx, my] = restingPosition 175 | const touch = Touches.first 176 | if (touch) { 177 | lastBrushWasTouch = true 178 | restingPosition = touch.position.map(v => v * 1 / (window.shrinkScore)) 179 | ;[mx, my] = restingPosition 180 | } 181 | 182 | if (Mouse.Left) lastBrushWasTouch = false 183 | 184 | if (!lastBrushWasTouch && mx !== undefined) { 185 | my -= (layers.last.height.baseVal.value - my)/3 186 | mx -= (layers.last.width.baseVal.value - mx)/3 187 | 188 | my += (layers.last.height.baseVal.value + my)/10 189 | mx += (layers.last.width.baseVal.value + mx)/10 190 | 191 | } else if (mx !== undefined) { 192 | my -= 200 193 | mx -= 20 194 | } 195 | const mouse = {x: mx, y: my} 196 | 197 | if (Mouse.Left || touch) { 198 | if (!painter.isPainting) { 199 | let isCloseEnough = true 200 | if (touch) { 201 | const displacement = [mx - painter.x, my - painter.y] 202 | const distance = Math.hypot(...displacement) 203 | if (distance > 50) isCloseEnough = false 204 | } 205 | 206 | if (isCloseEnough) { 207 | painter.isPainting = true 208 | const path = [] 209 | const element = document.createElementNS("http://www.w3.org/2000/svg", "path") 210 | path.element = element 211 | paths.push(path) 212 | path.colour = colour 213 | path.push([brush.x, brush.y]) 214 | const stroke = getStroke(path, painter.strokeOptions) 215 | element.setAttribute("d", getSvgPathFromStroke(stroke)) 216 | element.setAttribute("fill", path.colour) 217 | currentStrokeContainer.appendChild(element) 218 | } 219 | } 220 | } else if (painter.isPainting) { 221 | painter.isPainting = false 222 | strokeHistoryContainer.appendChild(paths.last.element) 223 | } 224 | 225 | for (const position of ["x", "y"]) { 226 | const speed = `d${position}` 227 | if (mouse[position] === undefined) continue 228 | painter[speed] = (mouse[position] - painter[position]) * painter.speed 229 | painter[position] += painter[speed] 230 | } 231 | 232 | global.painter.x += (2*Math.sin(performance.now() / global.painter.idleFrequency.x)) * global.painter.idleFadePower * global.painter.wobble 233 | global.painter.y += (2*Math.sin(performance.now() / global.painter.idleFrequency.y)) * global.painter.idleFadePower * global.painter.wobble 234 | 235 | previousPosition = [painter.x, painter.y] 236 | 237 | painter.targetR = painter.dx * painter.dr + painter.dy * -painter.dr 238 | painter.r += (painter.targetR - painter.r) * painter.speedR 239 | 240 | const newBrush = getBrushPosition(painter) 241 | 242 | painter.brushdx = newBrush.x - brush.x 243 | painter.brushdy = newBrush.y - brush.y 244 | 245 | if (painter.isPainting) { 246 | const path = paths.last 247 | const last = path.last 248 | const secondLast = path[path.length - 2] 249 | if (secondLast === undefined || last === undefined) { 250 | path.push([newBrush.x, newBrush.y]) 251 | const stroke = getStroke(path, painter.strokeOptions) 252 | path.element.setAttribute("d", getSvgPathFromStroke(stroke)) 253 | return 254 | } 255 | //const displacementLast = [newBrush.x - last[0], newBrush.y - last[1]] 256 | const displacementSecondLast = [newBrush.x - secondLast[0], newBrush.y - secondLast[1]] 257 | //const distanceLast = Math.hypot(...displacementLast) 258 | const distanceSecondLast = Math.hypot(...displacementSecondLast) 259 | if (distanceSecondLast <= 5.0 || painter.lockAxis) { 260 | path[path.length-1] = [newBrush.x, newBrush.y] 261 | const stroke = getStroke(path, painter.strokeOptions) 262 | path.element.setAttribute("d", getSvgPathFromStroke(stroke)) 263 | } else { 264 | path.push([newBrush.x, newBrush.y]) 265 | const stroke = getStroke(path, painter.strokeOptions) 266 | path.element.setAttribute("d", getSvgPathFromStroke(stroke)) 267 | } 268 | } 269 | 270 | } 271 | 272 | //======// 273 | // PATH // 274 | //======// 275 | function getSvgPathFromStroke(stroke) { 276 | if (!stroke.length) return '' 277 | 278 | const d = stroke.reduce( 279 | (acc, [x0, y0], i, arr) => { 280 | const [x1, y1] = arr[(i + 1) % arr.length] 281 | acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2) 282 | return acc 283 | }, 284 | ['M', ...stroke[0], 'Q'] 285 | ) 286 | 287 | d.push('Z') 288 | return d.join(' ') 289 | } 290 | 291 | //--------------------- NO GLOBAL STATE ABOVE THIS LINE ---------------------// 292 | 293 | //==========// 294 | // PAINTERS // 295 | //==========// 296 | const berd = makePainter({ 297 | sources: ["images/berd0.png", "images/berd1.png"], 298 | scale: 0.5, 299 | centerY: 0.35, 300 | centerX: 0.55, 301 | offsetX: -47, 302 | offsetY: -60, 303 | speed: 0.1, 304 | minSpeed: 0.035, 305 | maxSpeed: 0.2, 306 | dr: 0.05, 307 | speedR: 0.1, 308 | acceleration: 0.0002, 309 | strokeOptions: { 310 | smoothing: 1.0, 311 | streamline: 0.5, 312 | thinning: 0.5, 313 | last: true, 314 | //size: 10, 315 | }, 316 | }) 317 | 318 | const tode = makePainter({ 319 | sources: ["images/tode.png"], 320 | scale: 0.5, 321 | centerY: 0.35, 322 | centerX: 0.55, 323 | offsetX: -35, 324 | offsetY: -17.5, 325 | 326 | 327 | speed: 0.09, 328 | minSpeed: 0.01, 329 | maxSpeed: 0.15, 330 | dr: 0.05, 331 | speedR: 0.05, 332 | acceleration: 0.00001, 333 | wobble: 0.5, 334 | 335 | strokeOptions: berd.strokeOptions, 336 | }) 337 | 338 | const bot = makePainter({ 339 | sources: ["images/bot0.png", "images/bot1.png"], 340 | scale: 0.5, 341 | centerY: 0.11, 342 | centerX: 1.125, 343 | offsetX: -500, 344 | strokeOptions: berd.strokeOptions, 345 | 346 | 347 | speed: 0.05, 348 | minSpeed: 0.035, 349 | maxSpeed: 0.2, 350 | dr: 0.05, 351 | speedR: 0.1, 352 | wobble: 1.0, 353 | acceleration: 0.0001, 354 | lockAxis: true, 355 | }) 356 | 357 | const berdWitch = makePainter({ 358 | sources: ["images/hat/berd0.png", "images/hat/berd1.png"], 359 | scale: 0.5, 360 | centerY: 0.1, 361 | centerX: 0.55, 362 | offsetX: -47, 363 | offsetY: -60, 364 | speed: 0.1, 365 | minSpeed: 0.035, 366 | maxSpeed: 0.2, 367 | dr: 0.05, 368 | speedR: 0.1, 369 | acceleration: 0.0002, 370 | strokeOptions: { 371 | smoothing: 1.0, 372 | streamline: 0.5, 373 | thinning: 0.5, 374 | last: true, 375 | //size: 10, 376 | }, 377 | }) 378 | 379 | const todeWitch = makePainter({ 380 | sources: ["images/hat/tode.png"], 381 | scale: 0.25, 382 | centerY: 0.1, 383 | centerX: 0.525, 384 | offsetX: -35, 385 | offsetY: -17.5, 386 | speed: 0.09, 387 | minSpeed: 0.01, 388 | maxSpeed: 0.15, 389 | dr: 0.05, 390 | speedR: 0.05, 391 | acceleration: 0.00001, 392 | wobble: 0.5, 393 | strokeOptions: berdWitch.strokeOptions, 394 | }) 395 | 396 | const painters = [berd, tode, bot] 397 | 398 | //======// 399 | // SHOW // 400 | //======// 401 | const show = Show.make({ layerCount: 2 }) 402 | const undershow = CanvasShow.make() 403 | 404 | //==============// 405 | // GREEN SCREEN // 406 | //==============// 407 | //const GREEN_SCREEN_COLOUR = Colour.Void 408 | const GREEN_SCREEN_COLOUR = Colour.multiply(Colour.Blue, {lightness: 0.25}) 409 | //const PLATE_DIMENSIONS = [4400, 2253] 410 | //const PLATE_DIMENSIONS = [2200 + 33.92, 2253] 411 | const PLATE_SCALED_DIMENSIONS = [1080, 1080].map(v => v * 1) 412 | 413 | let debug = undefined 414 | undershow.tick = (context) => { 415 | const {canvas} = context 416 | const {width, height} = canvas 417 | 418 | const [plateWidth, plateHeight] = PLATE_SCALED_DIMENSIONS 419 | const margin = [width - plateWidth, height - plateHeight] 420 | const [mx, my] = margin 421 | const plate = { 422 | x: mx/2, 423 | y: my/2, 424 | width: plateWidth / devicePixelRatio, 425 | height: plateHeight / devicePixelRatio, 426 | } 427 | 428 | if (global.greenScreenEnabled) { 429 | 430 | context.fillStyle = GREEN_SCREEN_COLOUR 431 | context.fillRect(0, 0, canvas.width, canvas.height) 432 | 433 | if (!global.fullGreenScreenEnabled) { 434 | context.fillStyle = Colour.Black 435 | //context.fillRect(mx/2, my/2, ...PLATE_SCALED_DIMENSIONS) 436 | } 437 | 438 | const path = new Path2D() 439 | path.rect(mx/2, my/2, ...PLATE_SCALED_DIMENSIONS) 440 | context.strokeStyle = Colour.White 441 | context.lineWidth = 10 442 | //context.stroke(path) 443 | 444 | } else { 445 | context.clearRect(0, 0, canvas.width, canvas.height) 446 | } 447 | 448 | for (const box of global.layout) { 449 | if (box.direction > 0.0) { 450 | if (box.opacity < 1.0) { 451 | //box.opacity += 0.04 452 | box.opacity += 0.005 453 | } else { 454 | //box.direction = -1.0 455 | } 456 | } else { 457 | if (box.opacity > 0.0) { 458 | //box.opacity -= 0.04 459 | box.opacity -= 0.01 460 | } 461 | } 462 | } 463 | drawLayout(context, plate, global.layout) 464 | 465 | if (debug !== undefined) { 466 | undershow.context.globalAlpha = 1.0 467 | undershow.context.fillStyle = "red" 468 | undershow.context.fillRect(debug.x, debug.y, 10, 10) 469 | } 470 | 471 | } 472 | 473 | const resetLayout = () => { 474 | let opacity = 0.0 475 | for (const box of global.layout) { 476 | box.opacity = opacity 477 | box.direction = 1.0 478 | //opacity -= 0.08 479 | opacity -= 0.15 480 | } 481 | } 482 | 483 | const unsetLayout = () => { 484 | let opacity = 1.0 485 | for (const box of global.layout) { 486 | box.opacity = opacity 487 | box.direction = -1.0 488 | opacity += 0.08 489 | } 490 | } 491 | 492 | //=======// 493 | // BOXES // 494 | //=======// 495 | // https://www.wolframalpha.com/input?i=1320+%3D+4a+%2B+5b%3B+675.9+%3D+2a+%2B+3b%3B 496 | const SIZE = 309.6 497 | const MARGIN = 33.92 498 | const makeBox = ({colour, position = [MARGIN, MARGIN], dimensions = [SIZE, SIZE]} = {}) => { 499 | return {colour, position, dimensions, opacity: 1.0, direction: 1.0} 500 | } 501 | 502 | const layouts = [] 503 | 504 | layouts.push([]) 505 | 506 | layouts.push([ 507 | makeBox({colour: Colour.Green, position: [MARGIN, MARGIN]}), 508 | makeBox({colour: Colour.Green, position: [MARGIN + SIZE+MARGIN, MARGIN]}), 509 | makeBox({colour: Colour.Green, position: [MARGIN + 2*(SIZE+MARGIN), MARGIN]}), 510 | makeBox({colour: Colour.Green, position: [MARGIN + 3*(SIZE+MARGIN), MARGIN]}), 511 | makeBox({colour: Colour.Orange, position: [MARGIN, MARGIN+SIZE+MARGIN]}), 512 | makeBox({colour: Colour.Orange, position: [MARGIN + 1*(SIZE+MARGIN), MARGIN+SIZE+MARGIN]}), 513 | makeBox({colour: Colour.Orange, position: [MARGIN + 2*(SIZE+MARGIN), MARGIN+SIZE+MARGIN]}), 514 | makeBox({colour: Colour.Orange, position: [MARGIN + 3*(SIZE+MARGIN), MARGIN+SIZE+MARGIN]}), 515 | ]) 516 | 517 | layouts.push([ 518 | makeBox({colour: Colour.Orange, position: [MARGIN, MARGIN+SIZE+MARGIN]}), 519 | ]) 520 | 521 | layouts.push([ 522 | makeBox({colour: Colour.Green, position: [MARGIN, MARGIN]}), 523 | makeBox({colour: Colour.Green, position: [MARGIN + SIZE+MARGIN, MARGIN]}), 524 | makeBox({colour: Colour.Green, position: [MARGIN + 2*(SIZE+MARGIN), MARGIN]}), 525 | makeBox({colour: Colour.Green, position: [MARGIN + 3*(SIZE+MARGIN), MARGIN]}), 526 | makeBox({colour: Colour.Orange, position: [MARGIN, MARGIN+SIZE+MARGIN], dimensions: [SIZE+MARGIN+SIZE/2, SIZE]}), 527 | makeBox({colour: Colour.Orange, position: [SIZE+MARGIN+SIZE/2 + MARGIN + MARGIN + 0*(SIZE+MARGIN), MARGIN+SIZE+MARGIN], dimensions: [SIZE*0.795, SIZE]}), 528 | makeBox({colour: Colour.Orange, position: [SIZE+MARGIN+SIZE/2 + MARGIN + MARGIN + 1*(SIZE*0.795+MARGIN), MARGIN+SIZE+MARGIN], dimensions: [SIZE*0.795, SIZE]}), 529 | makeBox({colour: Colour.Orange, position: [SIZE+MARGIN+SIZE/2 + MARGIN + MARGIN + 2*(SIZE*0.795+MARGIN), MARGIN+SIZE+MARGIN], dimensions: [SIZE*0.795, SIZE]}), 530 | ]) 531 | 532 | layouts.push([ 533 | makeBox({colour: Colour.Green, position: [MARGIN + 2*(SIZE+MARGIN), MARGIN]}), 534 | ]) 535 | 536 | layouts.push([ 537 | makeBox({colour: Colour.Green, position: [MARGIN, MARGIN]}), 538 | makeBox({colour: Colour.Green, position: [MARGIN + SIZE+MARGIN, MARGIN]}), 539 | makeBox({colour: Colour.Orange, position: [MARGIN + 2*(SIZE+MARGIN), MARGIN]}), 540 | makeBox({colour: Colour.Green, position: [MARGIN + 3*(SIZE+MARGIN), MARGIN], dimensions: [SIZE/2 - MARGIN/2, SIZE]}), 541 | makeBox({colour: Colour.Green, position: [MARGIN + 3*(SIZE+MARGIN) + SIZE/2 + MARGIN/2, MARGIN], dimensions: [SIZE/2 - MARGIN/2, SIZE]}), 542 | makeBox({colour: Colour.Orange, position: [MARGIN, MARGIN+SIZE+MARGIN], dimensions: [SIZE+MARGIN+SIZE/2, SIZE]}), 543 | makeBox({colour: Colour.Orange, position: [SIZE+MARGIN+SIZE/2 + MARGIN + MARGIN, MARGIN+SIZE+MARGIN], dimensions: [(SIZE*0.795*3 + MARGIN*1) / 2, SIZE]}), 544 | makeBox({colour: Colour.Orange, position: [SIZE+MARGIN+SIZE/2 + MARGIN + MARGIN + (SIZE*0.795*3 + MARGIN*1) / 2 + MARGIN, MARGIN+SIZE+MARGIN], dimensions: [(SIZE*0.795*3 + MARGIN*1) / 2, SIZE]}), 545 | ]) 546 | 547 | layouts.push([ 548 | makeBox({colour: Colour.Green, position: [MARGIN, MARGIN]}), 549 | makeBox({colour: Colour.Green, position: [MARGIN + SIZE+MARGIN, MARGIN]}), 550 | makeBox({colour: Colour.Green, position: [MARGIN + 2*(SIZE+MARGIN), MARGIN]}), 551 | makeBox({colour: Colour.Green, position: [MARGIN + 3*(SIZE+MARGIN), MARGIN]}), 552 | ]) 553 | 554 | layouts.push([ 555 | makeBox({colour: Colour.Orange, position: [MARGIN, MARGIN+SIZE+MARGIN]}), 556 | makeBox({colour: Colour.Orange, position: [MARGIN + 1*(SIZE+MARGIN), MARGIN+SIZE+MARGIN]}), 557 | makeBox({colour: Colour.Orange, position: [MARGIN + 2*(SIZE+MARGIN), MARGIN+SIZE+MARGIN]}), 558 | makeBox({colour: Colour.Orange, position: [MARGIN + 3*(SIZE+MARGIN), MARGIN+SIZE+MARGIN]}), 559 | ]) 560 | 561 | layouts.push([ 562 | makeBox({colour: Colour.Green, position: [MARGIN, MARGIN]}), 563 | makeBox({colour: Colour.Green, position: [MARGIN + SIZE+MARGIN, MARGIN]}), 564 | makeBox({colour: Colour.Orange, position: [MARGIN, MARGIN+SIZE+MARGIN]}), 565 | makeBox({colour: Colour.Orange, position: [MARGIN + 1*(SIZE+MARGIN), MARGIN+SIZE+MARGIN]}), 566 | ]) 567 | 568 | layouts.push([ 569 | makeBox({colour: Colour.Green, position: [MARGIN, MARGIN]}), 570 | makeBox({colour: Colour.Green, position: [MARGIN + SIZE+MARGIN, MARGIN]}), 571 | makeBox({colour: Colour.Orange, position: [MARGIN, MARGIN+SIZE+MARGIN], dimensions: [SIZE+SIZE+MARGIN, SIZE]}), 572 | ]) 573 | 574 | const DEFAULT_LAYOUT = layouts[0] 575 | 576 | const drawLayout = (context, plate, layout) => { 577 | for (const box of layout) { 578 | drawBox(context, plate, box) 579 | } 580 | } 581 | 582 | const drawBox = (context, plate, box) => { 583 | const {position, dimensions} = box 584 | const [x, y] = position 585 | const [width, height] = dimensions 586 | context.globalAlpha = clamp(box.opacity, 0.0, 1.0) 587 | context.strokeStyle = box.colour 588 | context.lineWidth = 10 589 | context.strokeRect(x + plate.x, y + plate.y, width, height) 590 | context.globalAlpha = 1.0 591 | } 592 | 593 | //==============// 594 | // PICTURE MODE // 595 | //==============// 596 | const pictureMode = (() => { 597 | let listeners = null; 598 | let viewBoxX, viewBoxY, viewBoxW, viewBoxH; 599 | const dragLayer = document.createElementNS("http://www.w3.org/2000/svg", "svg") 600 | const dragPath = document.createElementNS("http://www.w3.org/2000/svg", "path") 601 | dragLayer.style.cursor = 'crosshair' 602 | dragPath.setAttribute("fill", "#00000055") 603 | 604 | function renderToCanvas(transparentBg, { x, y, w, h }) { 605 | const canvas = document.createElement("canvas") 606 | canvas.width = w 607 | canvas.height = h 608 | const ctx = canvas.getContext("2d") 609 | ctx.clearRect(0, 0, w, h) 610 | const layers = show.layers 611 | return Promise.all( 612 | layers.map((layer, i) => { 613 | layer = layer.cloneNode(true); 614 | layer.setAttribute("xmlns", "http://www.w3.org/2000/svg") 615 | if (i === 0 && transparentBg) { 616 | layer.style['background-color'] = 'transparent' 617 | } 618 | const outerHTML = layer.outerHTML; 619 | const blob = new Blob([outerHTML], {type:'image/svg+xml;charset=utf-8'}); 620 | const URL = window.URL || window.webkitURL || window; 621 | const blobURL = URL.createObjectURL(blob); 622 | const img = new Image(); 623 | img.width = viewBoxW; 624 | img.height = viewBoxH; 625 | return new Promise((resolve, reject) => { 626 | img.src = blobURL; 627 | img.onload = () => resolve(img) 628 | img.onerror = reject 629 | }) 630 | }) 631 | ).then(images => { 632 | images.forEach(img => ctx.drawImage(img, -x, -y, viewBoxW, viewBoxH)) 633 | }).then(() => canvas) 634 | } 635 | 636 | const pm = { 637 | start(layers) { 638 | if (listeners) return 639 | Array.prototype.filter.call(layers.last.style, key => key != 'cursor').forEach(key => dragLayer.style[key] = layers.last.style[key]); 640 | [viewBoxX, viewBoxY, viewBoxW, viewBoxH] = layers.last.getAttributeNS("http://www.w3.org/2000/svg", "viewBox").split(" ").filter(x => x != "") 641 | dragLayer.setAttributeNS("http://www.w3.org/2000/svg", "viewBox", `${viewBoxX} ${viewBoxY} ${viewBoxW} ${viewBoxH}`) 642 | let dragStart = null; 643 | let isDragging = false; 644 | let isRightClick = false; 645 | layers.last.insertAdjacentElement("afterend", dragLayer) 646 | listeners = [ 647 | on.mousedown((e) => { 648 | // We need focus in order to have permission 649 | // to modify the clipboard. 650 | // On certain OS'es, focus is granted after 651 | // mouseup is fired 652 | if (dragStart || !document.hasFocus()) { 653 | isDragging = false 654 | dragStart = null 655 | dragPath.remove() 656 | return 657 | } 658 | isRightClick = e.button === 2 659 | dragStart = [e.clientX, e.clientY] 660 | isDragging = false 661 | }), 662 | on.mousemove((e) => { 663 | if (dragStart && !isDragging) { 664 | isDragging = true 665 | dragLayer.appendChild(dragPath) 666 | } 667 | if (isDragging) { 668 | const [x0, y0] = dragStart 669 | let [x1, y1] = [e.clientX, e.clientY] 670 | const minX = Math.min(x0, x1) 671 | const minY = Math.min(y0, y1) 672 | const maxX = Math.max(x0, x1) 673 | const maxY = Math.max(y0, y1) 674 | dragPath.setAttribute("d", `M0,0L${viewBoxW},0 ${viewBoxW},${viewBoxH} 0,${viewBoxH}M${minX},${minY}L${minX},${maxY} ${maxX},${maxY} ${maxX},${minY}Z`) 675 | } 676 | }), 677 | on.mouseup((e) => { 678 | if (!dragStart) return 679 | const canvas = document.createElement("canvas") 680 | canvas.width = viewBoxW 681 | canvas.height = viewBoxH 682 | let canvasPromise 683 | if (isDragging) { 684 | const from = dragStart; 685 | const to = [e.clientX, e.clientY]; 686 | const minX = Math.min(from[0], to[0]); 687 | const minY = Math.min(from[1], to[1]); 688 | const maxX = Math.max(from[0], to[0]); 689 | const maxY = Math.max(from[1], to[1]); 690 | dragPath.remove() 691 | canvasPromise = renderToCanvas(isRightClick, { 692 | x: minX, 693 | y: minY, 694 | w: maxX - minX, 695 | h: maxY - minY 696 | }); 697 | } else { 698 | canvasPromise = renderToCanvas(isRightClick, { 699 | x: 0, 700 | y: 0, 701 | w: viewBoxW, 702 | h: viewBoxH, 703 | }); 704 | } 705 | navigator.clipboard.write([ 706 | new ClipboardItem({ 707 | "image/png": canvasPromise.then(canvas => new Promise(resolve => canvas.toBlob(resolve))), 708 | }) 709 | ]).catch(console.error) 710 | dragStart = null 711 | isDragging = false 712 | }), 713 | ] 714 | }, 715 | resize(layers) { 716 | Array.prototype.filter.call(layers.last.style, key => key != 'cursor').forEach(key => dragLayer.style[key] = layers.last.style[key]); 717 | [viewBoxX, viewBoxY, viewBoxW, viewBoxH] = layers.last.getAttributeNS("http://www.w3.org/2000/svg", "viewBox").split(" ").filter(x => x != "") 718 | dragLayer.setAttributeNS("http://www.w3.org/2000/svg", "viewBox", `${viewBoxX} ${viewBoxY} ${viewBoxW} ${viewBoxH}`) 719 | }, 720 | stop() { 721 | if (!listeners) return 722 | listeners.forEach(off => off()) 723 | listeners = null 724 | dragLayer.remove() 725 | } 726 | } 727 | 728 | return pm 729 | })() 730 | 731 | //==============// 732 | // GLOBAL STATE // 733 | //==============// 734 | const global = { 735 | painterId: 2, 736 | painter: painters[2], 737 | paths: [], 738 | colour: Colour.White, 739 | currentFrame: null, 740 | strokeHistoryContainer: show.layers.first.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "g")), 741 | currentStrokeContainer: show.layers.last.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "g")), 742 | painterContainer: show.layers.last.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "g")), 743 | greenScreenEnabled: true, 744 | fullGreenScreenEnabled: false, 745 | layout: DEFAULT_LAYOUT, 746 | } 747 | 748 | window.global = global 749 | 750 | show.resize = (layers) => { 751 | layers.forEach(layer => layer.style["cursor"] = "none") 752 | pictureMode.resize(layers) 753 | } 754 | 755 | show.tick = (layers) => { 756 | // Suspend redraw of the SVG image until all changes are made 757 | const suspendIds = layers.map(layer => [layer, layer.suspendRedraw(5000)]) 758 | updatePainter(layers, global.strokeHistoryContainer, global.currentStrokeContainer, global.painter, global.paths, global.colour) 759 | drawPainter(global.painter) 760 | // Resume redraw of the SVG image 761 | suspendIds.map(([layer, suspendId]) => layer.unsuspendRedraw(suspendId)) 762 | } 763 | 764 | show.pause = (paused, layers) => { 765 | if (paused) { 766 | pictureMode.start(layers) 767 | } else { 768 | pictureMode.stop() 769 | } 770 | } 771 | 772 | on.load(() => trigger("resize")) 773 | 774 | //================// 775 | // CHANGE PAINTER // 776 | //================// 777 | const changePainter = (painter) => { 778 | global.painter.images.forEach((el) => { 779 | if (el.parentNode != null) { 780 | global.painterContainer.removeChild(el) 781 | } 782 | }) 783 | global.painter = painter 784 | global.painter.images.forEach((el) => { 785 | el.style.visibility = "hidden" 786 | global.painterContainer.appendChild(el) 787 | }) 788 | global.painter.images[global.painter.frame].style.visibility = "hidden" 789 | } 790 | 791 | // Run it once at the start to trigger the initial painter 792 | changePainter(painters[2]) 793 | 794 | //=======// 795 | // EVENT // 796 | //=======// 797 | on.contextmenu((e) => e.preventDefault(), {passive: false}) 798 | 799 | const KEYDOWN = {} 800 | on.keydown(e => { 801 | const func = KEYDOWN[e.key] 802 | if (func === undefined) return 803 | func(e) 804 | }) 805 | 806 | KEYDOWN["x"] = () => { 807 | const path = global.paths.pop() 808 | if (path) { 809 | path.element.remove() 810 | } 811 | if (global.painter.isPainting) { 812 | global.painter.isPainting = false 813 | } 814 | } 815 | 816 | KEYDOWN["r"] = () => { 817 | global.paths.forEach((path) => { 818 | path.element.remove() 819 | }) 820 | global.paths = [] 821 | global.painter.isPainting = false 822 | } 823 | 824 | KEYDOWN["c"] = KEYDOWN["x"] 825 | 826 | KEYDOWN["1"] = () => global.colour = Colour.White 827 | KEYDOWN["2"] = () => global.colour = Colour.Red 828 | KEYDOWN["3"] = () => global.colour = Colour.Green 829 | KEYDOWN["4"] = () => global.colour = Colour.Blue 830 | KEYDOWN["5"] = () => global.colour = Colour.Yellow 831 | KEYDOWN["6"] = () => global.colour = Colour.Orange 832 | KEYDOWN["7"] = () => global.colour = Colour.Pink 833 | KEYDOWN["8"] = () => global.colour = Colour.Rose 834 | KEYDOWN["9"] = () => global.colour = Colour.Cyan 835 | KEYDOWN["0"] = () => global.colour = Colour.Purple 836 | 837 | KEYDOWN["Tab"] = (e) => { 838 | global.painterId++ 839 | if (global.painterId >= painters.length) { 840 | global.painterId = 0 841 | } 842 | changePainter(painters[global.painterId]) 843 | e.preventDefault() 844 | } 845 | 846 | KEYDOWN["g"] = () => global.greenScreenEnabled = !global.greenScreenEnabled 847 | KEYDOWN["f"] = () => global.fullGreenScreenEnabled = !global.fullGreenScreenEnabled 848 | 849 | KEYDOWN["d"] = () => { 850 | let index = layouts.indexOf(global.layout) + 1 851 | if (index >= layouts.length) { 852 | index = 0 853 | } 854 | global.layout = layouts[index] 855 | resetLayout() 856 | } 857 | 858 | KEYDOWN["a"] = () => { 859 | let index = layouts.indexOf(global.layout) - 1 860 | if (index < 0) { 861 | index = layouts.length - 1 862 | } 863 | global.layout = layouts[index] 864 | resetLayout() 865 | } 866 | 867 | KEYDOWN["w"] = () => resetLayout() 868 | KEYDOWN["s"] = () => unsetLayout() 869 | 870 | unsetLayout() --------------------------------------------------------------------------------