]> git.r.bdr.sh - rbdr/heart/blob - js/lib/heart_renderer.js
356ed72d94d0c64456271ac0d494b461dda406dc
[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._
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 this._currentColor.red = red;
251 this._currentColor.green = green;
252 this._currentColor.blue = blue;
253 }
254
255 // Updates a single color component.
256 _updateColorComponent(component, delta) {
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 this.canvas.classList.add('mouse-moving');
371 }
372
373 // Remove class to hide the cursor.
374 _hideCursor() {
375 this.canvas.classList.remove('mouse-moving');
376 }
377 };
378
379
380 window.HeartRenderer = HeartRenderer;
381 })(window);