X-Git-Url: https://git.r.bdr.sh/rbdr/heart/blobdiff_plain/910278aad4a0b47dc40112839a18bc8e590302e5..abf60045c085a46fe80ba074cfd0670ac67a7677:/js/lib/heart_renderer.js?ds=inline diff --git a/js/lib/heart_renderer.js b/js/lib/heart_renderer.js index f6ece86..356ed72 100644 --- a/js/lib/heart_renderer.js +++ b/js/lib/heart_renderer.js @@ -3,9 +3,6 @@ ((window) => { const kColorIteratorLimit = 256; - const kRedSpeed = 0.1; - const kGreenSpeed = 0.2; - const kBlueSpeed = 0.15; /** * Renders a Heart, has its own canvas, which will be placed in the element @@ -25,11 +22,9 @@ * @instance * @name canvas * @type HTMLCanvasElement - * @default A brand new full width and height canvas + * @default A brand new canvas */ this.canvas = window.document.createElement('canvas'); - this.canvas.style.height = '100%'; - this.canvas.style.width = '100%'; /** * The maximum fps that will be used @@ -42,13 +37,72 @@ */ 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._ + 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); } @@ -64,6 +118,61 @@ 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 + } } /** @@ -127,12 +236,143 @@ // The actual animation processing function. _animateStep(context, delta) { - this._currentColor.red = Math.round(this._currentColor.red + delta * kRedSpeed) % kColorIteratorLimit; - this._currentColor.green = Math.round(this._currentColor.green + delta * kGreenSpeed) % kColorIteratorLimit; - this._currentColor.blue = Math.round(this._currentColor.blue + delta * kBlueSpeed) % kColorIteratorLimit; + 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(0, 0, this.canvas.scrollWidth, this.canvas.scrollHeight); + 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'); } };