├── 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()
--------------------------------------------------------------------------------