Neale Pickett
·
2023-09-13
background.mjs
1function randint(max) {
2 return Math.floor(Math.random() * max)
3}
4
5const Millisecond = 1
6const Second = Millisecond * 1000
7const FrameRate = 24 / Second // Fast enough for this tomfoolery
8
9class Point {
10 constructor(x, y) {
11 this.x = x
12 this.y = y
13 }
14
15 /**
16 * Add n to this.
17 *
18 * @param {Point} n What to add to this
19 * @returns {Point}
20 */
21 Add(n) {
22 return new Point(this.x + n.x, this.y + n.y)
23 }
24
25 /**
26 * Subtract n from this.
27 *
28 * @param {Point} n
29 * @returns {Point}
30 */
31 Subtract(n) {
32 return new Point(this.x - n.x, this.y - n.y)
33 }
34
35 /**
36 * Add velocity, then bounce point off box defined by points at min and max
37 * @param {Point} velocity
38 * @param {Point} min
39 * @param {Point} max
40 * @returns {Point}
41 */
42 Bounce(velocity, min, max) {
43 let p = this.Add(velocity)
44 if (p.x < min.x) {
45 p.x += (min.x - p.x) * 2
46 velocity.x *= -1
47 }
48 if (p.x > max.x) {
49 p.x += (max.x - p.x) * 2
50 velocity.x *= -1
51 }
52 if (p.y < min.y) {
53 p.y += (min.y - p.y) * 2
54 velocity.y *= -1
55 }
56 if (p.y > max.y) {
57 p.y += (max.y - p.y) * 2
58 velocity.y *= -1
59 }
60 return p
61 }
62
63 /**
64 *
65 * @param {Point} p
66 * @returns {Boolean}
67 */
68 Equal(p) {
69 return (this.x == p.x) && (this.y == p.y)
70 }
71}
72
73class QixLine {
74 /**
75 * @param {Number} hue
76 * @param {Point} a
77 * @param {Point} b
78 */
79 constructor(hue, a, b) {
80 this.hue = hue
81 this.a = a
82 this.b = b
83 }
84}
85
86/**
87 * Draw a line dancing around the screen,
88 * like the video game "qix"
89 */
90class QixBackground {
91 constructor(ctx, frameRate = 6/Second) {
92 this.ctx = ctx
93 this.min = new Point(0, 0)
94 this.max = new Point(this.ctx.canvas.width, this.ctx.canvas.height)
95 this.box = this.max.Subtract(this.min)
96
97 this.lines = [
98 new QixLine(
99 Math.random(),
100 new Point(randint(this.box.x), randint(this.box.y)),
101 new Point(randint(this.box.x), randint(this.box.y)),
102 )
103 ]
104 while (this.lines.length < 18) {
105 this.lines.push(this.lines[0])
106 }
107 this.velocity = new QixLine(
108 0.001,
109 new Point(1 + randint(this.box.x / 100), 1 + randint(this.box.y / 100)),
110 new Point(1 + randint(this.box.x / 100), 1 + randint(this.box.y / 100)),
111 )
112
113 this.frameInterval = Millisecond / frameRate
114 this.nextFrame = 0
115 }
116
117 /**
118 * Maybe draw a frame
119 */
120 Animate() {
121 let now = performance.now()
122 if (now < this.nextFrame) {
123 // Not today, satan
124 return
125 }
126 this.nextFrame = now + this.frameInterval
127
128 this.lines.shift()
129 let lastLine = this.lines[this.lines.length - 1]
130 let nextLine = new QixLine(
131 (lastLine.hue + this.velocity.hue) % 1.0,
132 lastLine.a.Bounce(this.velocity.a, this.min, this.max),
133 lastLine.b.Bounce(this.velocity.b, this.min, this.max),
134 )
135
136 this.lines.push(nextLine)
137
138 this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
139 for (let line of this.lines) {
140 this.ctx.save()
141 this.ctx.strokeStyle = `hwb(${line.hue}turn 0% 0%)`
142 this.ctx.beginPath()
143 this.ctx.moveTo(line.a.x, line.a.y)
144 this.ctx.lineTo(line.b.x, line.b.y)
145 this.ctx.stroke()
146 this.ctx.restore()
147 }
148 }
149}
150
151function init() {
152 // Don't like the background animation? You can disable it by setting a
153 // property in localStorage and reloading.
154 if (localStorage.disableBackgroundAnimation) {
155 return
156 }
157
158 let canvas = document.createElement("canvas")
159 canvas.width = 640
160 canvas.height = 640
161 canvas.classList.add("wallpaper")
162 document.body.insertBefore(canvas, document.body.firstChild)
163
164 let ctx = canvas.getContext("2d")
165
166 let qix = new QixBackground(ctx)
167 // window.requestAnimationFrame is overkill for something this silly
168 setInterval(() => qix.Animate(), Millisecond/FrameRate)
169}
170
171if (document.readyState === "loading") {
172 document.addEventListener("DOMContentLoaded", init)
173} else {
174 init()
175}