]>
git.r.bdr.sh - rbdr/heart/blob - js/lib/heart_renderer.js
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 console
.log(red
, green
, blue
);
252 this._currentColor
.red
= red
;
253 this._currentColor
.green
= green
;
254 this._currentColor
.blue
= blue
;
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
;
266 this._colorDirection
[component
] = 1;
274 _drawHeart(context
, delta
) {
276 const canvasHeight
= this.canvas
.height
;
277 const canvasWidth
= this.canvas
.width
;
279 const referenceDimension
= canvasWidth
< canvasHeight
? canvasWidth : canvasHeight
;
281 this.heartSize
+= Math
.sign(this._targetHeartSize
- this.heartSize
) * this._resizeSpeed
;
283 const heartSize
= Math
.round(referenceDimension
* this.heartSize
* .01);
284 const radius
= heartSize
/ 2;
288 this._center
.x
= Math
.round(canvasWidth
/ 2);
289 this._center
.y
= Math
.round(canvasHeight
/ 2);
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
);
296 // Move towards the target
297 this._center
.x
+= Math
.cos(angle
) * this._trackingSpeed
;
298 this._center
.y
+= Math
.sin(angle
) * this._trackingSpeed
;
300 const canvasCenterX
= this._center
.x
;
301 const canvasCenterY
= this._center
.y
;
302 const centerX
= -radius
;
303 const centerY
= -radius
;
306 // translate and rotate, adjusting for weight of the heart.
307 context
.translate(canvasCenterX
, canvasCenterY
+ radius
/ 4);
308 context
.rotate(-45 * Math
.PI
/ 180);
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
);
316 context
.arc(centerX
+ radius
, centerY
, radius
, 0, 2 * Math
.PI
, false);
322 context
.arc(centerX
+ heartSize
, centerY
+ radius
, radius
, 0, 2 * Math
.PI
, false);
326 context
.setTransform(1, 0, 0, 1, 0, 0);
329 // Sets the center from mouse
330 _setCenterFromMouse(event
) {
333 this._targetCenter
.x
= event
.offsetX
;
334 this._targetCenter
.y
= event
.offsetY
;
335 setTimeout(this._hideCursor
.bind(this), this._cursorTimeout
);
338 // Binds the wheel event to resize the heart
341 this.canvas
.addEventListener('wheel', this._onWheel
.bind(this));
344 // Handle the mouse wheel movement
347 if (!this._ticking
) {
348 this._ticking
= true;
349 window
.requestAnimationFrame(this._resizeHeartFromDelta
.bind(this, event
.deltaY
));
353 // Use delta to resize the heart
354 _resizeHeartFromDelta(delta
) {
356 let heartSize
= this.heartSize
+ (this._resizeMagnitude
* delta
);
358 if (heartSize
> this.maxHeartSize
) {
359 heartSize
= this.maxHeartSize
;
362 if (heartSize
< this.minHeartSize
) {
363 heartSize
= this.minHeartSize
;
366 this._targetHeartSize
= heartSize
;
367 this._ticking
= false;
370 // Apply a class to show the cursor.
372 this.canvas
.classList
.add('mouse-moving');
375 // Remove class to hide the cursor.
377 this.canvas
.classList
.remove('mouse-moving');
382 window
.HeartRenderer
= HeartRenderer
;