]>
Commit | Line | Data |
---|---|---|
1 | 'use strict'; | |
2 | ||
3 | ((window) => { | |
4 | ||
5 | const kColorIteratorLimit = 256; | |
6 | ||
7 | /** | |
8 | * Renders a Heart, has its own canvas, which will be placed in the element | |
9 | * set by calling #render. | |
10 | * | |
11 | * @class HeartRenderer | |
12 | * @param {object} config configuration, extends any member of the renderer | |
13 | */ | |
14 | const HeartRenderer = class HeartRenderer { | |
15 | ||
16 | constructor(config) { | |
17 | ||
18 | /** | |
19 | * The instance of the heart renderer being used | |
20 | * | |
21 | * @memberof HeartRenderer | |
22 | * @instance | |
23 | * @name canvas | |
24 | * @type HTMLCanvasElement | |
25 | * @default A brand new canvas | |
26 | */ | |
27 | this.canvas = window.document.createElement('canvas'); | |
28 | ||
29 | /** | |
30 | * The maximum fps that will be used | |
31 | * | |
32 | * @memberof HeartRenderer | |
33 | * @instance | |
34 | * @name fps | |
35 | * @type Number | |
36 | * @default 60 | |
37 | */ | |
38 | this.fps = 60; | |
39 | ||
40 | /** | |
41 | * The size of the heart as a percentage of the canvas smallest dimension | |
42 | * | |
43 | * @memberof HeartRenderer | |
44 | * @instance | |
45 | * @name heartSize | |
46 | * @type Number | |
47 | * @default 40 | |
48 | */ | |
49 | this.heartSize = 40; | |
50 | ||
51 | /** | |
52 | * The max size of the heart as a percentage of the canvas smallest dimension | |
53 | * | |
54 | * @memberof HeartRenderer | |
55 | * @instance | |
56 | * @name maxHeartSize | |
57 | * @type Number | |
58 | * @default 75 | |
59 | */ | |
60 | this.maxHeartSize = 75; | |
61 | ||
62 | /** | |
63 | * The min size of the heart as a percentage of the canvas smallest dimension | |
64 | * | |
65 | * @memberof HeartRenderer | |
66 | * @instance | |
67 | * @name minHeartSize | |
68 | * @type Number | |
69 | * @default 10 | |
70 | */ | |
71 | this.minHeartSize = 10; | |
72 | ||
73 | this._ticking = false; // Lock for wheel event. | |
74 | this._resizeMagnitude = 0.1; // Multiplies the wheel delta to resize the heart | |
75 | this._resizeSpeed = 1; // How many percent points per frame we'll resize to match target | |
76 | this._trackingSpeed = 10; // How many pixels per frame will we move to match target | |
77 | this._following = null; // The status of mouse follow. | |
78 | this._center = null; // The actual center | |
79 | this._targetHeartSize = this.heartSize; | |
80 | this._ | |
81 | this._targetCenter = { | |
82 | x: 0, | |
83 | y: 0 | |
84 | }; // the target coordinates | |
85 | this._animating = false; // The status of the animation. | |
86 | this._previousFrameTime = Date.now(); // The timestamp of the last frame for fps control | |
87 | this._cursorTimeout = 500; // Timeout to hide the cursor in milliseconds | |
88 | this._currentColor = { // The current color that will be painted | |
89 | red: 100, | |
90 | blue: 0, | |
91 | green: 50 | |
92 | }; | |
93 | this._colorSpeed = { | |
94 | red: 0.1, | |
95 | blue: 0.2, | |
96 | green: 0.15 | |
97 | }; | |
98 | this._colorDirection = { | |
99 | red: 1, | |
100 | blue: 1, | |
101 | green: 1 | |
102 | }; | |
103 | ||
104 | this._detectWheel(); | |
105 | this.startFollowingMouse(); | |
106 | ||
107 | Object.assign(this, config); | |
108 | } | |
109 | ||
110 | /** | |
111 | * Attaches the canvas to an HTML element | |
112 | * | |
113 | * @memberof HeartRenderer | |
114 | * @function render | |
115 | * @instance | |
116 | * @param {HTMLElement} element the element where we will attach our canvas | |
117 | */ | |
118 | render(element) { | |
119 | ||
120 | element.appendChild(this.canvas); | |
121 | ||
122 | this._targetCenter = { | |
123 | x: Math.round(this.canvas.width / 2), | |
124 | y: Math.round(this.canvas.height / 2) | |
125 | }; // the target coordinates | |
126 | this.resize(); | |
127 | } | |
128 | ||
129 | /** | |
130 | * Resizes the canvas | |
131 | * | |
132 | * @memberof HeartRenderer | |
133 | * @function render | |
134 | * @instance | |
135 | */ | |
136 | resize() { | |
137 | ||
138 | if (this.canvas.parentElement) { | |
139 | this.canvas.width = this.canvas.parentElement.offsetWidth; | |
140 | this.canvas.height = this.canvas.parentElement.offsetHeight; | |
141 | } | |
142 | } | |
143 | ||
144 | /** | |
145 | * Follows the mouse | |
146 | * | |
147 | * @memberof HeartRenderer | |
148 | * @function startFollowingMouse | |
149 | * @instance | |
150 | */ | |
151 | startFollowingMouse() { | |
152 | ||
153 | if (!this._following) { | |
154 | this._following = this._setCenterFromMouse.bind(this); | |
155 | this.canvas.addEventListener('mousemove', this._following); | |
156 | } | |
157 | } | |
158 | ||
159 | /** | |
160 | * Stop following the mouse | |
161 | * | |
162 | * @memberof HeartRenderer | |
163 | * @function stopFollowingMouse | |
164 | * @instance | |
165 | */ | |
166 | stopFollowingMouse() { | |
167 | ||
168 | if (this._following) { | |
169 | this.canvas.removeEventListener('mouseover', this._following); | |
170 | this._following = null; | |
171 | this._targetCenter = { | |
172 | x: Math.round(this.canvas.width / 2), | |
173 | y: Math.round(this.canvas.height / 2) | |
174 | }; // the target coordinates | |
175 | } | |
176 | } | |
177 | ||
178 | /** | |
179 | * Gets the context from the current canvas and starts the animation process | |
180 | * | |
181 | * @memberof HeartRenderer | |
182 | * @function activate | |
183 | * @instance | |
184 | */ | |
185 | activate() { | |
186 | ||
187 | const context = this.canvas.getContext('2d'); | |
188 | this._startAnimating(context); | |
189 | } | |
190 | ||
191 | /** | |
192 | * Stops the animation process | |
193 | * | |
194 | * @memberof HeartRenderer | |
195 | * @function deactivate | |
196 | * @instance | |
197 | */ | |
198 | deactivate() { | |
199 | ||
200 | this._stopAnimating(); | |
201 | } | |
202 | ||
203 | // Starts the animation loop | |
204 | _startAnimating(context) { | |
205 | ||
206 | this._frameDuration = 1000 / this.fps; | |
207 | this._animating = true; | |
208 | ||
209 | this._animate(context); | |
210 | } | |
211 | ||
212 | // Stops the animation on the next frame. | |
213 | _stopAnimating() { | |
214 | ||
215 | this._animating = false; | |
216 | } | |
217 | ||
218 | // Runs the animation step controlling the FPS | |
219 | _animate(context) { | |
220 | ||
221 | if (!this._animating) { | |
222 | return; | |
223 | } | |
224 | ||
225 | window.requestAnimationFrame(this._animate.bind(this, context)); | |
226 | ||
227 | const currentFrameTime = Date.now(); | |
228 | const delta = currentFrameTime - this._previousFrameTime; | |
229 | ||
230 | if (delta > this._frameDuration) { | |
231 | this._previousFrameTime = Date.now(); | |
232 | this._animateStep(context, delta); | |
233 | } | |
234 | } | |
235 | ||
236 | // The actual animation processing function. | |
237 | _animateStep(context, delta) { | |
238 | ||
239 | this._updateColor(delta); | |
240 | this._drawHeart(context, delta); | |
241 | } | |
242 | ||
243 | // Updates the current color | |
244 | _updateColor(delta) { | |
245 | ||
246 | const red = this._updateColorComponent('red', delta); | |
247 | const green = this._updateColorComponent('green', delta); | |
248 | const blue = this._updateColorComponent('blue', delta); | |
249 | ||
250 | console.log(red, green, blue); | |
251 | ||
252 | this._currentColor.red = red; | |
253 | this._currentColor.green = green; | |
254 | this._currentColor.blue = blue; | |
255 | } | |
256 | ||
257 | // Updates a single color component. | |
258 | _updateColorComponent(component, delta) { | |
259 | let color = Math.round(this._currentColor[component] + (delta * this._colorSpeed[component] * this._colorDirection[component])); | |
260 | if (color >= kColorIteratorLimit) { | |
261 | this._colorDirection[component] = -1; | |
262 | color = kColorIteratorLimit; | |
263 | } | |
264 | ||
265 | if (color <= 0) { | |
266 | this._colorDirection[component] = 1; | |
267 | color = 0; | |
268 | } | |
269 | ||
270 | return color; | |
271 | } | |
272 | ||
273 | // Draws a heart | |
274 | _drawHeart(context, delta) { | |
275 | ||
276 | const canvasHeight = this.canvas.height; | |
277 | const canvasWidth = this.canvas.width; | |
278 | ||
279 | const referenceDimension = canvasWidth < canvasHeight ? canvasWidth : canvasHeight; | |
280 | ||
281 | this.heartSize += Math.sign(this._targetHeartSize - this.heartSize) * this._resizeSpeed; | |
282 | ||
283 | const heartSize = Math.round(referenceDimension * this.heartSize * .01); | |
284 | const radius = heartSize / 2; | |
285 | ||
286 | if (!this._center) { | |
287 | this._center = {}; | |
288 | this._center.x = Math.round(canvasWidth / 2); | |
289 | this._center.y = Math.round(canvasHeight / 2); | |
290 | } | |
291 | ||
292 | const deltaY = this._targetCenter.y - this._center.y; | |
293 | const deltaX = this._targetCenter.x - this._center.x; | |
294 | const angle = Math.atan2(deltaY, deltaX); | |
295 | ||
296 | // Move towards the target | |
297 | this._center.x += Math.cos(angle) * this._trackingSpeed; | |
298 | this._center.y += Math.sin(angle) * this._trackingSpeed; | |
299 | ||
300 | const canvasCenterX = this._center.x; | |
301 | const canvasCenterY = this._center.y; | |
302 | const centerX = -radius; | |
303 | const centerY = -radius; | |
304 | ||
305 | ||
306 | // translate and rotate, adjusting for weight of the heart. | |
307 | context.translate(canvasCenterX, canvasCenterY + radius / 4); | |
308 | context.rotate(-45 * Math.PI / 180); | |
309 | ||
310 | // Fill the ventricles of the heart | |
311 | context.fillStyle = `rgb(${this._currentColor.red}, ${this._currentColor.green}, ${this._currentColor.blue})`; | |
312 | context.fillRect(centerX, centerY, heartSize, heartSize); | |
313 | ||
314 | // Left atrium | |
315 | context.beginPath(); | |
316 | context.arc(centerX + radius, centerY, radius, 0, 2 * Math.PI, false); | |
317 | context.fill(); | |
318 | context.closePath(); | |
319 | ||
320 | // Right atrium | |
321 | context.beginPath(); | |
322 | context.arc(centerX + heartSize, centerY + radius, radius, 0, 2 * Math.PI, false); | |
323 | context.fill(); | |
324 | context.closePath(); | |
325 | ||
326 | context.setTransform(1, 0, 0, 1, 0, 0); | |
327 | } | |
328 | ||
329 | // Sets the center from mouse | |
330 | _setCenterFromMouse(event) { | |
331 | ||
332 | this._showCursor(); | |
333 | this._targetCenter.x = event.offsetX; | |
334 | this._targetCenter.y = event.offsetY; | |
335 | setTimeout(this._hideCursor.bind(this), this._cursorTimeout); | |
336 | } | |
337 | ||
338 | // Binds the wheel event to resize the heart | |
339 | _detectWheel() { | |
340 | ||
341 | this.canvas.addEventListener('wheel', this._onWheel.bind(this)); | |
342 | } | |
343 | ||
344 | // Handle the mouse wheel movement | |
345 | _onWheel(event) { | |
346 | ||
347 | if (!this._ticking) { | |
348 | this._ticking = true; | |
349 | window.requestAnimationFrame(this._resizeHeartFromDelta.bind(this, event.deltaY)); | |
350 | } | |
351 | } | |
352 | ||
353 | // Use delta to resize the heart | |
354 | _resizeHeartFromDelta(delta) { | |
355 | ||
356 | let heartSize = this.heartSize + (this._resizeMagnitude * delta); | |
357 | ||
358 | if (heartSize > this.maxHeartSize) { | |
359 | heartSize = this.maxHeartSize; | |
360 | } | |
361 | ||
362 | if (heartSize < this.minHeartSize) { | |
363 | heartSize = this.minHeartSize; | |
364 | } | |
365 | ||
366 | this._targetHeartSize = heartSize; | |
367 | this._ticking = false; | |
368 | } | |
369 | ||
370 | // Apply a class to show the cursor. | |
371 | _showCursor() { | |
372 | this.canvas.classList.add('mouse-moving'); | |
373 | } | |
374 | ||
375 | // Remove class to hide the cursor. | |
376 | _hideCursor() { | |
377 | this.canvas.classList.remove('mouse-moving'); | |
378 | } | |
379 | }; | |
380 | ||
381 | ||
382 | window.HeartRenderer = HeartRenderer; | |
383 | })(window); |