]>
git.r.bdr.sh - rbdr/heart/blob - js/lib/heart_renderer.js
356ed72d94d0c64456271ac0d494b461dda406dc
5 const kColorIteratorLimit
= 256;
8 * Renders a Heart, has its own canvas, which will be placed in the element
9 * set by calling #render.
11 * @class HeartRenderer
12 * @param {object} config configuration, extends any member of the renderer
14 const HeartRenderer
= class HeartRenderer
{
19 * The instance of the heart renderer being used
21 * @memberof HeartRenderer
24 * @type HTMLCanvasElement
25 * @default A brand new canvas
27 this.canvas
= window
.document
.createElement('canvas');
30 * The maximum fps that will be used
32 * @memberof HeartRenderer
41 * The size of the heart as a percentage of the canvas smallest dimension
43 * @memberof HeartRenderer
52 * The max size of the heart as a percentage of the canvas smallest dimension
54 * @memberof HeartRenderer
60 this.maxHeartSize
= 75;
63 * The min size of the heart as a percentage of the canvas smallest dimension
65 * @memberof HeartRenderer
71 this.minHeartSize
= 10;
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
;
81 this._targetCenter
= {
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
98 this._colorDirection
= {
105 this.startFollowingMouse();
107 Object
.assign(this, config
);
111 * Attaches the canvas to an HTML element
113 * @memberof HeartRenderer
116 * @param {HTMLElement} element the element where we will attach our canvas
120 element
.appendChild(this.canvas
);
122 this._targetCenter
= {
123 x: Math
.round(this.canvas
.width
/ 2),
124 y: Math
.round(this.canvas
.height
/ 2)
125 }; // the target coordinates
132 * @memberof HeartRenderer
138 if (this.canvas
.parentElement
) {
139 this.canvas
.width
= this.canvas
.parentElement
.offsetWidth
;
140 this.canvas
.height
= this.canvas
.parentElement
.offsetHeight
;
147 * @memberof HeartRenderer
148 * @function startFollowingMouse
151 startFollowingMouse() {
153 if (!this._following
) {
154 this._following
= this._setCenterFromMouse
.bind(this);
155 this.canvas
.addEventListener('mousemove', this._following
);
160 * Stop following the mouse
162 * @memberof HeartRenderer
163 * @function stopFollowingMouse
166 stopFollowingMouse() {
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
179 * Gets the context from the current canvas and starts the animation process
181 * @memberof HeartRenderer
187 const context
= this.canvas
.getContext('2d');
188 this._startAnimating(context
);
192 * Stops the animation process
194 * @memberof HeartRenderer
195 * @function deactivate
200 this._stopAnimating();
203 // Starts the animation loop
204 _startAnimating(context
) {
206 this._frameDuration
= 1000 / this.fps
;
207 this._animating
= true;
209 this._animate(context
);
212 // Stops the animation on the next frame.
215 this._animating
= false;
218 // Runs the animation step controlling the FPS
221 if (!this._animating
) {
225 window
.requestAnimationFrame(this._animate
.bind(this, context
));
227 const currentFrameTime
= Date
.now();
228 const delta
= currentFrameTime
- this._previousFrameTime
;
230 if (delta
> this._frameDuration
) {
231 this._previousFrameTime
= Date
.now();
232 this._animateStep(context
, delta
);
236 // The actual animation processing function.
237 _animateStep(context
, delta
) {
239 this._updateColor(delta
);
240 this._drawHeart(context
, delta
);
243 // Updates the current color
244 _updateColor(delta
) {
246 const red
= this._updateColorComponent('red', delta
);
247 const green
= this._updateColorComponent('green', delta
);
248 const blue
= this._updateColorComponent('blue', delta
);
250 this._currentColor
.red
= red
;
251 this._currentColor
.green
= green
;
252 this._currentColor
.blue
= blue
;
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
;
264 this._colorDirection
[component
] = 1;
272 _drawHeart(context
, delta
) {
274 const canvasHeight
= this.canvas
.height
;
275 const canvasWidth
= this.canvas
.width
;
277 const referenceDimension
= canvasWidth
< canvasHeight
? canvasWidth : canvasHeight
;
279 this.heartSize
+= Math
.sign(this._targetHeartSize
- this.heartSize
) * this._resizeSpeed
;
281 const heartSize
= Math
.round(referenceDimension
* this.heartSize
* .01);
282 const radius
= heartSize
/ 2;
286 this._center
.x
= Math
.round(canvasWidth
/ 2);
287 this._center
.y
= Math
.round(canvasHeight
/ 2);
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
);
294 // Move towards the target
295 this._center
.x
+= Math
.cos(angle
) * this._trackingSpeed
;
296 this._center
.y
+= Math
.sin(angle
) * this._trackingSpeed
;
298 const canvasCenterX
= this._center
.x
;
299 const canvasCenterY
= this._center
.y
;
300 const centerX
= -radius
;
301 const centerY
= -radius
;
304 // translate and rotate, adjusting for weight of the heart.
305 context
.translate(canvasCenterX
, canvasCenterY
+ radius
/ 4);
306 context
.rotate(-45 * Math
.PI
/ 180);
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
);
314 context
.arc(centerX
+ radius
, centerY
, radius
, 0, 2 * Math
.PI
, false);
320 context
.arc(centerX
+ heartSize
, centerY
+ radius
, radius
, 0, 2 * Math
.PI
, false);
324 context
.setTransform(1, 0, 0, 1, 0, 0);
327 // Sets the center from mouse
328 _setCenterFromMouse(event
) {
331 this._targetCenter
.x
= event
.offsetX
;
332 this._targetCenter
.y
= event
.offsetY
;
333 setTimeout(this._hideCursor
.bind(this), this._cursorTimeout
);
336 // Binds the wheel event to resize the heart
339 this.canvas
.addEventListener('wheel', this._onWheel
.bind(this));
342 // Handle the mouse wheel movement
345 if (!this._ticking
) {
346 this._ticking
= true;
347 window
.requestAnimationFrame(this._resizeHeartFromDelta
.bind(this, event
.deltaY
));
351 // Use delta to resize the heart
352 _resizeHeartFromDelta(delta
) {
354 let heartSize
= this.heartSize
+ (this._resizeMagnitude
* delta
);
356 if (heartSize
> this.maxHeartSize
) {
357 heartSize
= this.maxHeartSize
;
360 if (heartSize
< this.minHeartSize
) {
361 heartSize
= this.minHeartSize
;
364 this._targetHeartSize
= heartSize
;
365 this._ticking
= false;
368 // Apply a class to show the cursor.
370 this.canvas
.classList
.add('mouse-moving');
373 // Remove class to hide the cursor.
375 this.canvas
.classList
.remove('mouse-moving');
380 window
.HeartRenderer
= HeartRenderer
;