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