src/tween.mjs
import { Logger } from './vendor/logger.min.mjs';
/**
* Class for creating a tweening animation
* @class Tween
* @license Tween does not have a license at this time. For licensing contact the author
* @author https://github.com/doubleactii
*/
class Tween {
/**
* @param {Object} [pOtions={}] - The options for the tween animation
* @param {Object} [pOtions.start={}] - The starting properties of the animation
* @param {Object} [pOtions.end={}] - The end properties of the animation
* @param {number} [pOtions.duration=1000] - The duration of the animation in milliseconds
* @param {string} [pOtions.easing="linear"] - The easing function to use for the animation
*/
constructor({ start = {}, end = {}, duration = 1000, easing = Tween.linear } = {}) {
this._build(start, end, duration, easing);
}
// Robert Penner's easing functions
static linear(t, b, c, d) {
return c * t / d + b;
}
static easeInQuad(t, b, c, d) {
return c * (t /= d) * t + b;
}
static easeOutQuad(t, b, c, d) {
return -c * (t /= d) * (t - 2) + b;
}
static easeInOutQuad(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t + b;
return -c / 2 * ((--t) * (t - 2) - 1) + b;
}
static easeInSine(t, b, c, d) {
return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
}
static easeOutSine(t, b, c, d) {
return c * Math.sin(t / d * (Math.PI / 2)) + b;
}
static easeInOutSine(t, b, c, d) {
return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
}
static easeInExpo(t, b, c, d) {
return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
}
static easeOutExpo(t, b, c, d) {
return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
}
static easeInOutExpo(t, b, c, d) {
if (t == 0) return b;
if (t == d) return b + c;
if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
}
static easeInCirc(t, b, c, d) {
return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
}
static easeOutCirc(t, b, c, d) {
return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
}
static easeInOutCirc(t, b, c, d) {
if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
}
static easeInCubic(t, b, c, d) {
return c * (t /= d) * t * t + b;
}
static easeOutCubic(t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
}
static easeInOutCubic(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
return c / 2 * ((t -= 2) * t * t + 2) + b;
}
static easeInQuart(t, b, c, d) {
return c * (t /= d) * t * t * t + b;
}
static easeOutQuart(t, b, c, d) {
return -c * ((t = t / d - 1) * t * t * t - 1) + b;
}
static easeInOutQuart(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
}
static easeInQuint(t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
}
static easeOutQuint(t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
}
static easeInOutQuint(t, b, c, d) {
if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b;
return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
}
static easeInElastic(t, b, c, d) {
var s = 1.70158;
var p = 0;
var a = c;
if (t == 0) return b;
if ((t /= d) == 1) return b + c;
if (!p) p = d * .3;
if (a < Math.abs(c)) {
a = c;
var s = p / 4;
} else {
// Handle the Math.asin(0 / 0) case
if (c === 0 && a === 0) {
s = p / (2 * Math.PI) * Math.asin(1);
} else {
var s = p / (2 * Math.PI) * Math.asin(c / a);
}
}
return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
}
static easeOutElastic(t, b, c, d) {
var s = 1.70158;
var p = 0;
var a = c;
if (t == 0) return b;
if ((t /= d) == 1) return b + c;
if (!p) p = d * .3;
if (a < Math.abs(c)) {
a = c;
var s = p / 4;
} else {
// Handle the Math.asin(0 / 0) case
if (c === 0 && a === 0) {
s = p / (2 * Math.PI) * Math.asin(1);
} else {
var s = p / (2 * Math.PI) * Math.asin(c / a);
}
}
return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
}
static easeInOutElastic(t, b, c, d) {
var s = 1.70158;
var p = 0;
var a = c;
if (t == 0) return b;
if ((t /= d / 2) == 2) return b + c;
if (!p) p = d * (.3 * 1.5);
if (a < Math.abs(c)) {
a = c;
var s = p / 4;
} else {
// Handle the Math.asin(0 / 0) case
if (c === 0 && a === 0) {
s = p / (2 * Math.PI) * Math.asin(1);
} else {
var s = p / (2 * Math.PI) * Math.asin(c / a);
}
}
if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * .5 + c + b;
}
static easeInBack(t, b, c, d) {
var s = 1.70158;
return c * (t /= d) * t * ((s + 1) * t - s) + b;
}
static easeOutBack(t, b, c, d) {
var s = 1.70158;
return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
}
static easeInOutBack(t, b, c, d) {
var s = 1.70158;
if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
}
static easeInBounce(t, b, c, d) {
return c - Tween.easeOutBounce(d - t, 0, c, d) + b;
}
static easeOutBounce(t, b, c, d) {
t /= d;
if (t < 1/2.75) {
return c * 7.5625 * t * t + b;
}
if (t < 2/2.75) {
t -= 1.5/2.75;
return c * (7.5625 * t * t + 0.75) + b;
}
if (t < 2.5/2.75) {
t -= 2.25/2.75;
return c * (7.5625 * t * t + 0.9375) + b;
} else {
t -= 2.625/2.75;
return c * (7.5625 * t * t + 0.984375) + b;
}
}
static easeInOutBounce(t, b, c, d) {
if (t < d*0.5) {
return (Tween.easeInBounce(t*2, 0, c, d)*0.5 + b);
}
return (Tween.easeOutBounce(t*2 - d, 0, c, d)*0.5 + c*0.5 + b);
}
/**
* @param {number} pNumber - The number to clamp
* @param {number} pMin - The minimum number
* @param {number} pMax - The maximum number
* @returns {number} The number clamped between the minimum and maximum values
*/
static _clamp(pNumber, pMin = 0, pMax = 1) {
return Math.max(pMin, Math.min(pNumber, pMax));
}
/**
* Converts an Hex color value to an array of [r, g, b].
*
* @private
* @param {string} pHex - The Hex color in the form "#fff" or "#ffffff" or tagless.
* @return {Array} The array [r, g, b].
*/
static _hexToRgb(pHex) {
pHex = pHex.replace('#', '');
if (pHex.length === 3) {
pHex = pHex.replace(new RegExp('(.)', 'g'), '$1$1');
}
pHex = pHex.match(new RegExp('..', 'g'));
const r = Tween._clamp(parseInt(pHex[0], 16), 0, 255);
const g = Tween._clamp(parseInt(pHex[1], 16), 0, 255);
const b = Tween._clamp(parseInt(pHex[2], 16), 0, 255);
return [r, g, b];
}
/**
* Converts an array of [r, g, b] to an Hex color code.
*
* @private
* @param {Array} pColorArray - The rgb color array to convert into a hex
* return {string} The hex color
*/
static _rgbToHex(pColorArray) {
if (Array.isArray(pColorArray)) {
return '#' + pColorArray.map((pColor) => Math.abs(Math.round(pColor)).toString(16).padStart(2, '0')).join('');
}
}
/**
* The version of the module.
*/
version = "VERSION_REPLACE_ME";
/**
* Builds/Rebuilds the tween object with new info
* @param {Object} pStart - The start object containing the start values
* @param {Object} pEnd - The end object containing the end values
* @param {number} pDuration - The duration of the effect
* @param {function} pEasing - The easing function to use
*/
_build(pStart, pEnd, pDuration, pEasing) {
this.start = pStart;
this.end = pEnd;
this.duration = pDuration;
this.easing = typeof(pEasing) === 'function' ? pEasing : Tween.linear;
this.events = {};
this.exportedValues = {};
this.tweening = false;
this.update = null;
this.paused = false;
this.lastTime = 0;
this.elapsed = 0;
}
/**
* @param {Object} [pOtions={}] - The options for the tween animation
* @param {Object} [pOtions.start={}] - The starting properties of the animation
* @param {Object} [pOtions.end={}] - The end properties of the animation
* @param {number} [pOtions.duration=1000] - The duration of the animation in milliseconds
* @param {string} [pOtions.easing="linear"] - The easing function to use for the animation
*/
build({ start = {}, end = {}, duration = 1000, easing = Tween.linear } = {}) {
this._build(start, end, duration, easing);
return this;
}
/**
* Attaches a callback to the specified event.
* @param {Object} pEvent - The event to attach the callback to
* @param {Function} pCallback - The function to be called when the event is triggered
* @return {Tween} The Tween instance
*/
on(pEvent, pCallback) {
if (typeof(pCallback) === "function") {
switch (pEvent) {
case "start":
case "end":
case "pause":
case "resume":
this.events[pEvent] = pCallback;
break;
default:
logger.prefix('Tween-Module').error(`The event "${pEvent}" is not supported.`);
}
} else {
logger.prefix('Tween-Module').error(`The callback for event "${pEvent}" is not a function.`);
}
return this;
}
/**
* Update each frame.
*/
animationFrame = () => {
if (!this.tweening || this.paused) return;
const now = Date.now();
if (!this.lastTime) this.lastTime = now;
this.elapsed += now - this.lastTime;
let progress = this.elapsed / this.duration;
if (this.oscillating) {
progress = (1 - Math.cos(progress * Math.PI)) / 2;
}
if (progress > 1) {
progress = 1;
}
for (let key in this.end) {
let startValue = this.start[key];
let endValue = this.end[key];
if (typeof(startValue) === "string" && (startValue.length === 3 || startValue.length === 6) || startValue.length === 4 || startValue.length === 7) {
startValue = Tween._hexToRgb(startValue);
endValue = Tween._hexToRgb(endValue);
const currentRGB = [
this.easing(progress, startValue[0], endValue[0] - startValue[0], 1),
this.easing(progress, startValue[1], endValue[1] - startValue[1], 1),
this.easing(progress, startValue[2], endValue[2] - startValue[2], 1)
];
this.exportedValues[key] = Tween._rgbToHex(currentRGB);
} else {
this.exportedValues[key] = this.easing(progress, startValue, endValue - startValue, 1);
}
}
this.update(this.exportedValues);
if (progress === 1 && !this.oscillating) {
this.stop();
if (this.events.end) {
this.events.end();
}
} else {
requestAnimationFrame(this.animationFrame);
}
this.lastTime = now;
}
/**
* Animates the tween by oscillating between the start and end properties.
* @param {function} pUpdate - A callback function to update the values during the oscillation.
* @param {boolean} [pOscillate=false] - A flag to indicate if the tween should oscillate.
* @return {Tween} - Returns the Tween instance for method chaining.
*/
animate(pUpdate, pOscillate = false) {
if (typeof(pUpdate) !== "function") {
logger.prefix('Tween-Module').error("The pUpdate parameter passed to animate is not a function.");
return;
}
if (this.tweening) return;
let startProperties = Object.keys(this.start);
let endProperties = Object.keys(this.end);
if (!startProperties.length || !endProperties.length) {
logger.prefix('Tween-Module').error("The start object or the end object has no properties.");
return;
}
if (!startProperties.every(prop => endProperties.includes(prop))) {
logger.prefix('Tween-Module').error("The end object is missing properties that the start object has.");
return;
}
this.update = pUpdate;
this.tweening = true;
this.elapsed = 0;
this.oscillating = pOscillate;
if (this.events.start) {
this.events.start();
}
requestAnimationFrame(this.animationFrame);
return this;
}
/**
* Resumes the tween animation
* @returns {Tween} The instance of the Tween object
*/
resume() {
if (this.paused) {
this.lastTime = Date.now();
this.paused = false;
if (this.events.resume) {
this.events.resume();
}
requestAnimationFrame(this.animationFrame);
}
return this;
}
/**
* Pauses the tween animation
* @returns {Tween} The instance of the Tween object
*/
pause() {
if (!this.paused) {
this.paused = true;
if (this.events.pause) {
this.events.pause();
}
}
return this;
}
/**
* Stops the tween and clears all data.
*/
stop() {
this.tweening = false;
this.oscillating = false;
this.update = null;
this.elapsed = 0;
this.lastTime = 0;
this.paused = false;
for (const prop in this.exportedValues) {
if (this.exportedValues.hasOwnProperty(prop)) {
delete this.exportedValues[prop];
}
}
}
}
/**
* The logger for this module.
* @ignore
*/
const logger = new Logger();
logger.registerType('Tween-Module', '#ff6600');
export { Tween };