((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
* @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
*/
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);
}
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
+ }
}
/**
// 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');
}
};