]>
git.r.bdr.sh - rbdr/heart/blob - js/lib/heart_renderer.js
4680fb93a187864e09149584e9e6cdfd5532f7ef
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
;
80 this._targetCenter
= {
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
97 this._colorDirection
= {
104 this.startFollowingMouse();
106 Object
.assign(this, config
);
110 * Attaches the canvas to an HTML element
112 * @memberof HeartRenderer
115 * @param {HTMLElement} element the element where we will attach our canvas
119 element
.appendChild(this.canvas
);
121 this._targetCenter
= {
122 x: Math
.round(this.canvas
.width
/ 2),
123 y: Math
.round(this.canvas
.height
/ 2)
124 }; // the target coordinates
131 * @memberof HeartRenderer
137 if (this.canvas
.parentElement
) {
138 this.canvas
.width
= this.canvas
.parentElement
.offsetWidth
;
139 this.canvas
.height
= this.canvas
.parentElement
.offsetHeight
;
146 * @memberof HeartRenderer
147 * @function startFollowingMouse
150 startFollowingMouse() {
152 if (!this._following
) {
153 this._following
= this._setCenterFromMouse
.bind(this);
154 this.canvas
.addEventListener('mousemove', this._following
);
159 * Stop following the mouse
161 * @memberof HeartRenderer
162 * @function stopFollowingMouse
165 stopFollowingMouse() {
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
178 * Gets the context from the current canvas and starts the animation process
180 * @memberof HeartRenderer
186 const context
= this.canvas
.getContext('2d');
187 this._startAnimating(context
);
191 * Stops the animation process
193 * @memberof HeartRenderer
194 * @function deactivate
199 this._stopAnimating();
202 // Starts the animation loop
203 _startAnimating(context
) {
205 this._frameDuration
= 1000 / this.fps
;
206 this._animating
= true;
208 this._animate(context
);
211 // Stops the animation on the next frame.
214 this._animating
= false;
217 // Runs the animation step controlling the FPS
220 if (!this._animating
) {
224 window
.requestAnimationFrame(this._animate
.bind(this, context
));
226 const currentFrameTime
= Date
.now();
227 const delta
= currentFrameTime
- this._previousFrameTime
;
229 if (delta
> this._frameDuration
) {
230 this._previousFrameTime
= Date
.now();
231 this._animateStep(context
, delta
);
235 // The actual animation processing function.
236 _animateStep(context
, delta
) {
238 this._updateColor(delta
);
239 this._drawHeart(context
, delta
);
242 // Updates the current color
243 _updateColor(delta
) {
245 const red
= this._updateColorComponent('red', delta
);
246 const green
= this._updateColorComponent('green', delta
);
247 const blue
= this._updateColorComponent('blue', delta
);
249 this._currentColor
.red
= red
;
250 this._currentColor
.green
= green
;
251 this._currentColor
.blue
= blue
;
254 // Updates a single color component.
255 _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.
371 this.canvas
.classList
.add('mouse-moving');
374 // Remove class to hide the cursor.
377 this.canvas
.classList
.remove('mouse-moving');
382 window
.HeartRenderer
= HeartRenderer
;