UNPKG

telesoft-waves

Version:

The Siri wave replicated in a JS library.

459 lines (451 loc) 16 kB
/*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } class ClassicCurve { constructor(ctrl, definition) { this.ATT_FACTOR = 4; this.GRAPH_X = 2; this.AMPLITUDE_FACTOR = 0.6; this.ctrl = ctrl; this.definition = definition; } globalAttFn(x) { return Math.pow(this.ATT_FACTOR / (this.ATT_FACTOR + Math.pow(x, this.ATT_FACTOR)), this.ATT_FACTOR); } xPos(i) { return this.ctrl.width * ((i + this.GRAPH_X) / (this.GRAPH_X * 2)); } yPos(i) { return (this.AMPLITUDE_FACTOR * (this.globalAttFn(i) * (this.ctrl.heightMax * this.ctrl.amplitude) * (1 / this.definition.attenuation) * Math.sin(this.ctrl.opt.frequency * i - this.ctrl.phase))); } draw() { const { ctx } = this.ctrl; ctx.moveTo(0, 0); ctx.beginPath(); const color = this.ctrl.color.replace(/rgb\(/g, "").replace(/\)/g, ""); ctx.strokeStyle = `rgba(${color},${this.definition.opacity})`; ctx.lineWidth = this.definition.lineWidth; // Cycle the graph from -X to +X every PX_DEPTH and draw the line for (let i = -this.GRAPH_X; i <= this.GRAPH_X; i += this.ctrl.opt.pixelDepth) { ctx.lineTo(this.xPos(i), this.ctrl.heightMax + this.yPos(i)); } ctx.stroke(); } static getDefinition() { return [ { attenuation: -2, lineWidth: 1, opacity: 0.1, }, { attenuation: -6, lineWidth: 1, opacity: 0.2, }, { attenuation: 4, lineWidth: 1, opacity: 0.4, }, { attenuation: 2, lineWidth: 1, opacity: 0.6, }, { attenuation: 1, lineWidth: 1.5, opacity: 1, }, ]; } } class iOS9Curve { constructor(ctrl, definition) { this.GRAPH_X = 25; this.AMPLITUDE_FACTOR = 0.8; this.SPEED_FACTOR = 1; this.DEAD_PX = 2; this.ATT_FACTOR = 4; this.DESPAWN_FACTOR = 0.02; this.NOOFCURVES_RANGES = [2, 5]; this.AMPLITUDE_RANGES = [0.3, 1]; this.OFFSET_RANGES = [-3, 3]; this.WIDTH_RANGES = [1, 3]; this.SPEED_RANGES = [0.5, 1]; this.DESPAWN_TIMEOUT_RANGES = [500, 2000]; this.ctrl = ctrl; this.definition = definition; this.noOfCurves = 0; this.spawnAt = 0; this.prevMaxY = 0; this.phases = []; this.offsets = []; this.speeds = []; this.finalAmplitudes = []; this.widths = []; this.amplitudes = []; this.despawnTimeouts = []; this.verses = []; } getRandomRange(e) { return e[0] + Math.random() * (e[1] - e[0]); } spawnSingle(ci) { this.phases[ci] = 0; this.amplitudes[ci] = 0; this.despawnTimeouts[ci] = this.getRandomRange(this.DESPAWN_TIMEOUT_RANGES); this.offsets[ci] = this.getRandomRange(this.OFFSET_RANGES); this.speeds[ci] = this.getRandomRange(this.SPEED_RANGES); this.finalAmplitudes[ci] = this.getRandomRange(this.AMPLITUDE_RANGES); this.widths[ci] = this.getRandomRange(this.WIDTH_RANGES); this.verses[ci] = this.getRandomRange([-1, 1]); } getEmptyArray(count) { return new Array(count); } spawn() { this.spawnAt = Date.now(); this.noOfCurves = Math.floor(this.getRandomRange(this.NOOFCURVES_RANGES)); this.phases = this.getEmptyArray(this.noOfCurves); this.offsets = this.getEmptyArray(this.noOfCurves); this.speeds = this.getEmptyArray(this.noOfCurves); this.finalAmplitudes = this.getEmptyArray(this.noOfCurves); this.widths = this.getEmptyArray(this.noOfCurves); this.amplitudes = this.getEmptyArray(this.noOfCurves); this.despawnTimeouts = this.getEmptyArray(this.noOfCurves); this.verses = this.getEmptyArray(this.noOfCurves); for (let ci = 0; ci < this.noOfCurves; ci++) { this.spawnSingle(ci); } } globalAttFn(x) { return Math.pow(this.ATT_FACTOR / (this.ATT_FACTOR + Math.pow(x, 2)), this.ATT_FACTOR); } sin(x, phase) { return Math.sin(x - phase); } yRelativePos(i) { let y = 0; for (let ci = 0; ci < this.noOfCurves; ci++) { // Generate a static T so that each curve is distant from each oterh let t = 4 * (-1 + (ci / (this.noOfCurves - 1)) * 2); // but add a dynamic offset t += this.offsets[ci]; const k = 1 / this.widths[ci]; const x = i * k - t; y += Math.abs(this.amplitudes[ci] * this.sin(this.verses[ci] * x, this.phases[ci]) * this.globalAttFn(x)); } // Divide for NoOfCurves so that y <= 1 return y / this.noOfCurves; } yPos(i) { return (this.AMPLITUDE_FACTOR * this.ctrl.heightMax * this.ctrl.amplitude * this.yRelativePos(i) * this.globalAttFn((i / this.GRAPH_X) * 2)); } xPos(i) { return this.ctrl.width * ((i + this.GRAPH_X) / (this.GRAPH_X * 2)); } drawSupportLine() { const { ctx } = this.ctrl; const coo = [0, this.ctrl.heightMax, this.ctrl.width, 1]; const gradient = ctx.createLinearGradient.apply(ctx, coo); gradient.addColorStop(0, "transparent"); gradient.addColorStop(0.1, "rgba(255,255,255,.5)"); gradient.addColorStop(1 - 0.1 - 0.1, "rgba(255,255,255,.5)"); gradient.addColorStop(1, "transparent"); ctx.fillStyle = gradient; ctx.fillRect.apply(ctx, coo); } draw() { const { ctx } = this.ctrl; ctx.globalAlpha = 0.7; ctx.globalCompositeOperation = "lighter"; if (this.spawnAt === 0) { this.spawn(); } if (this.definition.supportLine) { // Draw the support line return this.drawSupportLine(); } for (let ci = 0; ci < this.noOfCurves; ci++) { if (this.spawnAt + this.despawnTimeouts[ci] <= Date.now()) { this.amplitudes[ci] -= this.DESPAWN_FACTOR; } else { this.amplitudes[ci] += this.DESPAWN_FACTOR; } this.amplitudes[ci] = Math.min(Math.max(this.amplitudes[ci], 0), this.finalAmplitudes[ci]); this.phases[ci] = (this.phases[ci] + this.ctrl.speed * this.speeds[ci] * this.SPEED_FACTOR) % (2 * Math.PI); } let maxY = -Infinity; // Write two opposite waves for (const sign of [1, -1]) { ctx.beginPath(); for (let i = -this.GRAPH_X; i <= this.GRAPH_X; i += this.ctrl.opt.pixelDepth) { const x = this.xPos(i); const y = this.yPos(i); ctx.lineTo(x, this.ctrl.heightMax - sign * y); maxY = Math.max(maxY, y); } ctx.closePath(); ctx.fillStyle = `rgba(${this.definition.color}, 1)`; ctx.strokeStyle = `rgba(${this.definition.color}, 1)`; ctx.fill(); } if (maxY < this.DEAD_PX && this.prevMaxY > maxY) { this.spawnAt = 0; } this.prevMaxY = maxY; return null; } static getDefinition() { return [ { color: "255,255,255", supportLine: true, }, { // blue color: "15, 82, 169", }, { // red color: "173, 57, 76", }, { // green color: "48, 220, 155", }, ]; } } class SiriWave { constructor(_a) { var { container } = _a, rest = __rest(_a, ["container"]); // Phase of the wave (passed to Math.sin function) this.phase = 0; // Boolean value indicating the the animation is running this.run = false; // Curves objects to animate this.curves = []; const csStyle = window.getComputedStyle(container); this.opt = Object.assign({ container, style: "ios", ratio: window.devicePixelRatio || 1, speed: 0.2, amplitude: 1, frequency: 6, color: "#fff", cover: false, width: parseInt(csStyle.width.replace("px", ""), 10), height: parseInt(csStyle.height.replace("px", ""), 10), autostart: true, pixelDepth: 0.02, lerpSpeed: 0.1 }, rest); /** * Actual speed of the animation. Is not safe to change this value directly, use `setSpeed` instead. */ this.speed = Number(this.opt.speed); /** * Actual amplitude of the animation. Is not safe to change this value directly, use `setAmplitude` instead. */ this.amplitude = Number(this.opt.amplitude); /** * Width of the canvas multiplied by pixel ratio */ this.width = Number(this.opt.ratio * this.opt.width); /** * Height of the canvas multiplied by pixel ratio */ this.height = Number(this.opt.ratio * this.opt.height); /** * Maximum height for a single wave */ this.heightMax = Number(this.height / 2) - 6; /** * Color of the wave (used in Classic iOS) */ this.color = `rgb(${this.hex2rgb(this.opt.color)})`; /** * An object containing controller variables that need to be interpolated * to an another value before to be actually changed */ this.interpolation = { speed: this.speed, amplitude: this.amplitude, }; /** * Canvas DOM Element where curves will be drawn */ this.canvas = document.createElement("canvas"); /** * 2D Context from Canvas */ const ctx = this.canvas.getContext("2d"); if (ctx === null) { throw new Error("Unable to create 2D Context"); } this.ctx = ctx; // Set dimensions this.canvas.width = this.width; this.canvas.height = this.height; // By covering, we ensure the canvas is in the same size of the parent if (this.opt.cover === true) { this.canvas.style.width = this.canvas.style.height = "100%"; } else { this.canvas.style.width = `${this.width / this.opt.ratio}px`; this.canvas.style.height = `${this.height / this.opt.ratio}px`; } // Instantiate all curves based on the style switch (this.opt.style) { case "ios9": this.curves = (this.opt.curveDefinition || iOS9Curve.getDefinition()).map((def) => new iOS9Curve(this, def)); break; case "ios": default: this.curves = (this.opt.curveDefinition || ClassicCurve.getDefinition()).map((def) => new ClassicCurve(this, def)); break; } // Attach to the container this.opt.container.appendChild(this.canvas); // Start the animation if (this.opt.autostart) { this.start(); } } /** * Convert an HEX color to RGB */ hex2rgb(hex) { const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? `${parseInt(result[1], 16).toString()},${parseInt(result[2], 16).toString()},${parseInt(result[3], 16).toString()}` : null; } intLerp(v0, v1, t) { return v0 * (1 - t) + v1 * t; } /** * Interpolate a property to the value found in this.interpolation */ lerp(propertyStr) { const prop = this.interpolation[propertyStr]; if (prop !== null) { this[propertyStr] = this.intLerp(this[propertyStr], prop, this.opt.lerpSpeed); if (this[propertyStr] - prop === 0) { this.interpolation[propertyStr] = null; } } return this[propertyStr]; } /** * Clear the canvas */ clear() { this.ctx.globalCompositeOperation = "destination-out"; this.ctx.fillRect(0, 0, this.width, this.height); this.ctx.globalCompositeOperation = "source-over"; } /** * Draw all curves */ draw() { this.curves.forEach((curve) => curve.draw()); } /** * Clear the space, interpolate values, calculate new steps and draws * @returns */ startDrawCycle() { this.clear(); // Interpolate values this.lerp("amplitude"); this.lerp("speed"); this.draw(); this.phase = (this.phase + (Math.PI / 2) * this.speed) % (2 * Math.PI); if (window.requestAnimationFrame) { this.animationFrameId = window.requestAnimationFrame(this.startDrawCycle.bind(this)); } else { this.timeoutId = setTimeout(this.startDrawCycle.bind(this), 20); } } /* API */ /** * Start the animation */ start() { if (!this.canvas) { throw new Error("This instance of SiriWave has been disposed, please create a new instance"); } this.phase = 0; // Ensure we don't re-launch the draw cycle if (!this.run) { this.run = true; this.startDrawCycle(); } } /** * Stop the animation */ stop() { this.phase = 0; this.run = false; // Clear old draw cycle on stop if (this.animationFrameId) { window.cancelAnimationFrame(this.animationFrameId); } if (this.timeoutId) { clearTimeout(this.timeoutId); } } /** * Dispose */ dispose() { this.stop(); if (this.canvas) { this.canvas.remove(); this.canvas = null; } } /** * Set a new value for a property (interpolated) */ set(propertyStr, value) { this.interpolation[propertyStr] = value; } /** * Set a new value for the speed property (interpolated) */ setSpeed(value) { this.set("speed", value); } /** * Set a new value for the amplitude property (interpolated) */ setAmplitude(value) { this.set("amplitude", value); } } module.exports = SiriWave;