'use strict'; ((window) => { const kColorIteratorLimit = 256; /** * Renders a Heart, has its own canvas, which will be placed in the element * set by calling #render. * * @class HeartRenderer * @param {object} config configuration, extends any member of the renderer */ const HeartRenderer = class HeartRenderer { constructor(config) { /** * The instance of the heart renderer being used * * @memberof HeartRenderer * @instance * @name canvas * @type HTMLCanvasElement * @default A brand new canvas */ this.canvas = window.document.createElement('canvas'); /** * The maximum fps that will be used * * @memberof HeartRenderer * @instance * @name fps * @type Number * @default 60 */ this.fps = 60; /** * The size of the heart as a percentage of the canvas smallest dimension * * @memberof HeartRenderer * @instance * @name heartSize * @type Number * @default 40 */ this.heartSize = 40; /** * The max size of the heart as a percentage of the canvas smallest dimension * * @memberof HeartRenderer * @instance * @name maxHeartSize * @type Number * @default 75 */ this.maxHeartSize = 75; /** * The min size of the heart as a percentage of the canvas smallest dimension * * @memberof HeartRenderer * @instance * @name minHeartSize * @type Number * @default 10 */ this.minHeartSize = 10; this._ticking = false; // Lock for wheel event. this._resizeMagnitude = 0.1; // Multiplies the wheel delta to resize the heart this._resizeSpeed = 1; // How many percent points per frame we'll resize to match target this._trackingSpeed = 10; // How many pixels per frame will we move to match target this._following = null; // The status of mouse follow. this._center = null; // The actual center this._targetHeartSize = this.heartSize; this._targetCenter = { x: 0, y: 0 }; // the target coordinates this._animating = false; // The status of the animation. this._previousFrameTime = Date.now(); // The timestamp of the last frame for fps control this._cursorTimeout = 500; // Timeout to hide the cursor in milliseconds this._currentColor = { // The current color that will be painted red: 100, blue: 0, green: 50 }; this._colorSpeed = { red: 0.1, blue: 0.2, green: 0.15 }; this._colorDirection = { red: 1, blue: 1, green: 1 }; this._detectWheel(); this.startFollowingMouse(); Object.assign(this, config); } /** * Attaches the canvas to an HTML element * * @memberof HeartRenderer * @function render * @instance * @param {HTMLElement} element the element where we will attach our canvas */ render(element) { element.appendChild(this.canvas); this._targetCenter = { x: Math.round(this.canvas.width / 2), y: Math.round(this.canvas.height / 2) }; // the target coordinates this.resize(); } /** * Resizes the canvas * * @memberof HeartRenderer * @function render * @instance */ resize() { if (this.canvas.parentElement) { this.canvas.width = this.canvas.parentElement.offsetWidth; this.canvas.height = this.canvas.parentElement.offsetHeight; } } /** * Follows the mouse * * @memberof HeartRenderer * @function startFollowingMouse * @instance */ startFollowingMouse() { if (!this._following) { this._following = this._setCenterFromMouse.bind(this); this.canvas.addEventListener('mousemove', this._following); } } /** * Stop following the mouse * * @memberof HeartRenderer * @function stopFollowingMouse * @instance */ stopFollowingMouse() { if (this._following) { this.canvas.removeEventListener('mouseover', this._following); this._following = null; this._targetCenter = { x: Math.round(this.canvas.width / 2), y: Math.round(this.canvas.height / 2) }; // the target coordinates } } /** * Gets the context from the current canvas and starts the animation process * * @memberof HeartRenderer * @function activate * @instance */ activate() { const context = this.canvas.getContext('2d'); this._startAnimating(context); } /** * Stops the animation process * * @memberof HeartRenderer * @function deactivate * @instance */ deactivate() { this._stopAnimating(); } // Starts the animation loop _startAnimating(context) { this._frameDuration = 1000 / this.fps; this._animating = true; this._animate(context); } // Stops the animation on the next frame. _stopAnimating() { this._animating = false; } // Runs the animation step controlling the FPS _animate(context) { if (!this._animating) { return; } window.requestAnimationFrame(this._animate.bind(this, context)); const currentFrameTime = Date.now(); const delta = currentFrameTime - this._previousFrameTime; if (delta > this._frameDuration) { this._previousFrameTime = Date.now(); this._animateStep(context, delta); } } // The actual animation processing function. _animateStep(context, delta) { this._updateColor(delta); this._drawHeart(context, delta); } // Updates the current color _updateColor(delta) { const red = this._updateColorComponent('red', delta); const green = this._updateColorComponent('green', delta); const blue = this._updateColorComponent('blue', delta); this._currentColor.red = red; this._currentColor.green = green; this._currentColor.blue = blue; } // Updates a single color component. _updateColorComponent(component, delta) { let color = Math.round(this._currentColor[component] + (delta * this._colorSpeed[component] * this._colorDirection[component])); if (color >= kColorIteratorLimit) { this._colorDirection[component] = -1; color = kColorIteratorLimit; } if (color <= 0) { this._colorDirection[component] = 1; color = 0; } return color; } // Draws a heart _drawHeart(context, delta) { const canvasHeight = this.canvas.height; const canvasWidth = this.canvas.width; const referenceDimension = canvasWidth < canvasHeight ? canvasWidth : canvasHeight; this.heartSize += Math.sign(this._targetHeartSize - this.heartSize) * this._resizeSpeed; const heartSize = Math.round(referenceDimension * this.heartSize * .01); const radius = heartSize / 2; if (!this._center) { this._center = {}; this._center.x = Math.round(canvasWidth / 2); this._center.y = Math.round(canvasHeight / 2); } const deltaY = this._targetCenter.y - this._center.y; const deltaX = this._targetCenter.x - this._center.x; const angle = Math.atan2(deltaY, deltaX); // Move towards the target this._center.x += Math.cos(angle) * this._trackingSpeed; this._center.y += Math.sin(angle) * this._trackingSpeed; const canvasCenterX = this._center.x; const canvasCenterY = this._center.y; const centerX = -radius; const centerY = -radius; // translate and rotate, adjusting for weight of the heart. context.translate(canvasCenterX, canvasCenterY + radius / 4); context.rotate(-45 * Math.PI / 180); // Fill the ventricles of the heart context.fillStyle = `rgb(${this._currentColor.red}, ${this._currentColor.green}, ${this._currentColor.blue})`; context.fillRect(centerX, centerY, heartSize, heartSize); // Left atrium context.beginPath(); context.arc(centerX + radius, centerY, radius, 0, 2 * Math.PI, false); context.fill(); context.closePath(); // Right atrium context.beginPath(); context.arc(centerX + heartSize, centerY + radius, radius, 0, 2 * Math.PI, false); context.fill(); context.closePath(); context.setTransform(1, 0, 0, 1, 0, 0); } // Sets the center from mouse _setCenterFromMouse(event) { this._showCursor(); this._targetCenter.x = event.offsetX; this._targetCenter.y = event.offsetY; setTimeout(this._hideCursor.bind(this), this._cursorTimeout); } // Binds the wheel event to resize the heart _detectWheel() { this.canvas.addEventListener('wheel', this._onWheel.bind(this)); } // Handle the mouse wheel movement _onWheel(event) { if (!this._ticking) { this._ticking = true; window.requestAnimationFrame(this._resizeHeartFromDelta.bind(this, event.deltaY)); } } // Use delta to resize the heart _resizeHeartFromDelta(delta) { let heartSize = this.heartSize + (this._resizeMagnitude * delta); if (heartSize > this.maxHeartSize) { heartSize = this.maxHeartSize; } if (heartSize < this.minHeartSize) { heartSize = this.minHeartSize; } this._targetHeartSize = heartSize; this._ticking = false; } // Apply a class to show the cursor. _showCursor() { this.canvas.classList.add('mouse-moving'); } // Remove class to hide the cursor. _hideCursor() { this.canvas.classList.remove('mouse-moving'); } }; window.HeartRenderer = HeartRenderer; })(window);