]>
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._targetCenter = { | |
81 | x: 0, | |
82 | y: 0 | |
83 | }; // the target coordinates | |
84 | this._animating = false; // The status of the animation. | |
85 | this._previousFrameTime = Date.now(); // The timestamp of the last frame for fps control | |
86 | this._cursorTimeout = 500; // Timeout to hide the cursor in milliseconds | |
87 | this._currentColor = { // The current color that will be painted | |
88 | red: 100, | |
89 | blue: 0, | |
90 | green: 50 | |
91 | }; | |
92 | this._colorSpeed = { | |
93 | red: 0.1, | |
94 | blue: 0.2, | |
95 | green: 0.15 | |
96 | }; | |
97 | this._colorDirection = { | |
98 | red: 1, | |
99 | blue: 1, | |
100 | green: 1 | |
101 | }; | |
102 | ||
103 | this._detectWheel(); | |
104 | this.startFollowingMouse(); | |
105 | ||
106 | Object.assign(this, config); | |
107 | } | |
108 | ||
109 | /** | |
110 | * Attaches the canvas to an HTML element | |
111 | * | |
112 | * @memberof HeartRenderer | |
113 | * @function render | |
114 | * @instance | |
115 | * @param {HTMLElement} element the element where we will attach our canvas | |
116 | */ | |
117 | render(element) { | |
118 | ||
119 | element.appendChild(this.canvas); | |
120 | ||
121 | this._targetCenter = { | |
122 | x: Math.round(this.canvas.width / 2), | |
123 | y: Math.round(this.canvas.height / 2) | |
124 | }; // the target coordinates | |
125 | this.resize(); | |
126 | } | |
127 | ||
128 | /** | |
129 | * Resizes the canvas | |
130 | * | |
131 | * @memberof HeartRenderer | |
132 | * @function render | |
133 | * @instance | |
134 | */ | |
135 | resize() { | |
136 | ||
137 | if (this.canvas.parentElement) { | |
138 | this.canvas.width = this.canvas.parentElement.offsetWidth; | |
139 | this.canvas.height = this.canvas.parentElement.offsetHeight; | |
140 | } | |
141 | } | |
142 | ||
143 | /** | |
144 | * Follows the mouse | |
145 | * | |
146 | * @memberof HeartRenderer | |
147 | * @function startFollowingMouse | |
148 | * @instance | |
149 | */ | |
150 | startFollowingMouse() { | |
151 | ||
152 | if (!this._following) { | |
153 | this._following = this._setCenterFromMouse.bind(this); | |
154 | this.canvas.addEventListener('mousemove', this._following); | |
155 | } | |
156 | } | |
157 | ||
158 | /** | |
159 | * Stop following the mouse | |
160 | * | |
161 | * @memberof HeartRenderer | |
162 | * @function stopFollowingMouse | |
163 | * @instance | |
164 | */ | |
165 | stopFollowingMouse() { | |
166 | ||
167 | if (this._following) { | |
168 | this.canvas.removeEventListener('mouseover', this._following); | |
169 | this._following = null; | |
170 | this._targetCenter = { | |
171 | x: Math.round(this.canvas.width / 2), | |
172 | y: Math.round(this.canvas.height / 2) | |
173 | }; // the target coordinates | |
174 | } | |
175 | } | |
176 | ||
177 | /** | |
178 | * Gets the context from the current canvas and starts the animation process | |
179 | * | |
180 | * @memberof HeartRenderer | |
181 | * @function activate | |
182 | * @instance | |
183 | */ | |
184 | activate() { | |
185 | ||
186 | const context = this.canvas.getContext('2d'); | |
187 | this._startAnimating(context); | |
188 | } | |
189 | ||
190 | /** | |
191 | * Stops the animation process | |
192 | * | |
193 | * @memberof HeartRenderer | |
194 | * @function deactivate | |
195 | * @instance | |
196 | */ | |
197 | deactivate() { | |
198 | ||
199 | this._stopAnimating(); | |
200 | } | |
201 | ||
202 | // Starts the animation loop | |
203 | _startAnimating(context) { | |
204 | ||
205 | this._frameDuration = 1000 / this.fps; | |
206 | this._animating = true; | |
207 | ||
208 | this._animate(context); | |
209 | } | |
210 | ||
211 | // Stops the animation on the next frame. | |
212 | _stopAnimating() { | |
213 | ||
214 | this._animating = false; | |
215 | } | |
216 | ||
217 | // Runs the animation step controlling the FPS | |
218 | _animate(context) { | |
219 | ||
220 | if (!this._animating) { | |
221 | return; | |
222 | } | |
223 | ||
224 | window.requestAnimationFrame(this._animate.bind(this, context)); | |
225 | ||
226 | const currentFrameTime = Date.now(); | |
227 | const delta = currentFrameTime - this._previousFrameTime; | |
228 | ||
229 | if (delta > this._frameDuration) { | |
230 | this._previousFrameTime = Date.now(); | |
231 | this._animateStep(context, delta); | |
232 | } | |
233 | } | |
234 | ||
235 | // The actual animation processing function. | |
236 | _animateStep(context, delta) { | |
237 | ||
238 | this._updateColor(delta); | |
239 | this._drawHeart(context, delta); | |
240 | } | |
241 | ||
242 | // Updates the current color | |
243 | _updateColor(delta) { | |
244 | ||
245 | const red = this._updateColorComponent('red', delta); | |
246 | const green = this._updateColorComponent('green', delta); | |
247 | const blue = this._updateColorComponent('blue', delta); | |
248 | ||
249 | this._currentColor.red = red; | |
250 | this._currentColor.green = green; | |
251 | this._currentColor.blue = blue; | |
252 | } | |
253 | ||
254 | // Updates a single color component. | |
255 | _updateColorComponent(component, delta) { | |
256 | ||
257 | let color = Math.round(this._currentColor[component] + (delta * this._colorSpeed[component] * this._colorDirection[component])); | |
258 | if (color >= kColorIteratorLimit) { | |
259 | this._colorDirection[component] = -1; | |
260 | color = kColorIteratorLimit; | |
261 | } | |
262 | ||
263 | if (color <= 0) { | |
264 | this._colorDirection[component] = 1; | |
265 | color = 0; | |
266 | } | |
267 | ||
268 | return color; | |
269 | } | |
270 | ||
271 | // Draws a heart | |
272 | _drawHeart(context, delta) { | |
273 | ||
274 | const canvasHeight = this.canvas.height; | |
275 | const canvasWidth = this.canvas.width; | |
276 | ||
277 | const referenceDimension = canvasWidth < canvasHeight ? canvasWidth : canvasHeight; | |
278 | ||
279 | this.heartSize += Math.sign(this._targetHeartSize - this.heartSize) * this._resizeSpeed; | |
280 | ||
281 | const heartSize = Math.round(referenceDimension * this.heartSize * .01); | |
282 | const radius = heartSize / 2; | |
283 | ||
284 | if (!this._center) { | |
285 | this._center = {}; | |
286 | this._center.x = Math.round(canvasWidth / 2); | |
287 | this._center.y = Math.round(canvasHeight / 2); | |
288 | } | |
289 | ||
290 | const deltaY = this._targetCenter.y - this._center.y; | |
291 | const deltaX = this._targetCenter.x - this._center.x; | |
292 | const angle = Math.atan2(deltaY, deltaX); | |
293 | ||
294 | // Move towards the target | |
295 | this._center.x += Math.cos(angle) * this._trackingSpeed; | |
296 | this._center.y += Math.sin(angle) * this._trackingSpeed; | |
297 | ||
298 | const canvasCenterX = this._center.x; | |
299 | const canvasCenterY = this._center.y; | |
300 | const centerX = -radius; | |
301 | const centerY = -radius; | |
302 | ||
303 | ||
304 | // translate and rotate, adjusting for weight of the heart. | |
305 | context.translate(canvasCenterX, canvasCenterY + radius / 4); | |
306 | context.rotate(-45 * Math.PI / 180); | |
307 | ||
308 | // Fill the ventricles of the heart | |
309 | context.fillStyle = `rgb(${this._currentColor.red}, ${this._currentColor.green}, ${this._currentColor.blue})`; | |
310 | context.fillRect(centerX, centerY, heartSize, heartSize); | |
311 | ||
312 | // Left atrium | |
313 | context.beginPath(); | |
314 | context.arc(centerX + radius, centerY, radius, 0, 2 * Math.PI, false); | |
315 | context.fill(); | |
316 | context.closePath(); | |
317 | ||
318 | // Right atrium | |
319 | context.beginPath(); | |
320 | context.arc(centerX + heartSize, centerY + radius, radius, 0, 2 * Math.PI, false); | |
321 | context.fill(); | |
322 | context.closePath(); | |
323 | ||
324 | context.setTransform(1, 0, 0, 1, 0, 0); | |
325 | } | |
326 | ||
327 | // Sets the center from mouse | |
328 | _setCenterFromMouse(event) { | |
329 | ||
330 | this._showCursor(); | |
331 | this._targetCenter.x = event.offsetX; | |
332 | this._targetCenter.y = event.offsetY; | |
333 | setTimeout(this._hideCursor.bind(this), this._cursorTimeout); | |
334 | } | |
335 | ||
336 | // Binds the wheel event to resize the heart | |
337 | _detectWheel() { | |
338 | ||
339 | this.canvas.addEventListener('wheel', this._onWheel.bind(this)); | |
340 | } | |
341 | ||
342 | // Handle the mouse wheel movement | |
343 | _onWheel(event) { | |
344 | ||
345 | if (!this._ticking) { | |
346 | this._ticking = true; | |
347 | window.requestAnimationFrame(this._resizeHeartFromDelta.bind(this, event.deltaY)); | |
348 | } | |
349 | } | |
350 | ||
351 | // Use delta to resize the heart | |
352 | _resizeHeartFromDelta(delta) { | |
353 | ||
354 | let heartSize = this.heartSize + (this._resizeMagnitude * delta); | |
355 | ||
356 | if (heartSize > this.maxHeartSize) { | |
357 | heartSize = this.maxHeartSize; | |
358 | } | |
359 | ||
360 | if (heartSize < this.minHeartSize) { | |
361 | heartSize = this.minHeartSize; | |
362 | } | |
363 | ||
364 | this._targetHeartSize = heartSize; | |
365 | this._ticking = false; | |
366 | } | |
367 | ||
368 | // Apply a class to show the cursor. | |
369 | _showCursor() { | |
370 | ||
371 | this.canvas.classList.add('mouse-moving'); | |
372 | } | |
373 | ||
374 | // Remove class to hide the cursor. | |
375 | _hideCursor() { | |
376 | ||
377 | this.canvas.classList.remove('mouse-moving'); | |
378 | } | |
379 | }; | |
380 | ||
381 | ||
382 | window.HeartRenderer = HeartRenderer; | |
383 | })(window); |