]> git.r.bdr.sh - rbdr/heart/blob - js/lib/heart_renderer.js
4680fb93a187864e09149584e9e6cdfd5532f7ef
[rbdr/heart] / js / lib / heart_renderer.js
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);