diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..78bf304 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "NodeNext", + "moduleResolution": "nodenext" + } +} diff --git a/package.json b/package.json index e17a9cf..3373c2b 100644 --- a/package.json +++ b/package.json @@ -22,5 +22,10 @@ "devDependencies": { "uglify-js": "^2.4.19" }, - "license": "MIT" + "license": "MIT", + "prettier": { + "singleQuote": false, + "trailingComma": "none", + "semi": false + } } diff --git a/src/animations/animation.js b/src/animations/animation.js index 1c93fc5..246868e 100644 --- a/src/animations/animation.js +++ b/src/animations/animation.js @@ -1,160 +1,173 @@ -/** - * Creates new animation - * @param {Item} item Object to animate - * @param {Object} transform - * @param {number} duration - * @param {string} ease Timing function - * @param {number} delay - * @constructor - */ -function Animation(item, transform, duration, ease, delay) { - this.item = item - - this.transformation = transform - - this.start = null - this.diff = null - - this.duration = (transform.duration || duration) | 0 - this.delay = (transform.delay || delay) | 0 - ease = transform.ease || ease - this.ease = easings[ease] || easings.linear - this.easeName = transform.ease || ease || 'linear' -} - -Animation.skip = {duration: null, delay: null, ease: null} -Animation.transform = {translate: null, rotate: null, scale: null} - -Animation.getState = function (transform, item) { - var initial = {}, - computed - - for (var property in transform) { - if (property in Animation.skip) continue - if (transform.hasOwnProperty(property)) { - if (item.get(property) == null) { - if (!computed) { - computed = getComputedStyle(item.dom, null) - } - Animation.setItemState(item, property, computed) - } - initial[property] = new Tween(item.get(property), transform[property], property) - } - } - return initial -} - -Animation.setItemState = function (item, property, computed) { - if (property in Animation.transform) { - var value = computed[transformProperty] - if (value === 'none') { - value = { - translate: Vector.zero(), - rotate: Vector.zero(), - scale: Vector.set(1) - } - } else { - value = Matrix.decompose(Matrix.parse(value)) - } - item.set('translate', value.translate) - item.set('rotate', value.rotate) - item.set('scale', value.scale) - } else { - item.set(property, computed[property]) - } -} - -/** - * Starts animation timer - * @param {number} tick Timestamp - * @param {boolean=} seek Is used in seek mode - */ -Animation.prototype.init = function (tick, seek) { - if (this.start !== null && !seek) return - if (this.start === null) { - this.state = Animation.getState(this.transformation, this.item) - } - this.start = tick + this.delay -} - -/** - * Merges animation values - * @param {Object} transform - * @param {number} duration - * @param {string} ease Timing function - * @param {number} delay - */ -Animation.prototype.merge = function (transform, duration, ease, delay) { - this.duration = (transform.duration || duration) | 0 - this.delay = (transform.delay || delay) | 0 - ease = transform.ease || ease - this.ease = easings[ease] || easings.linear - this.easeName = transform.ease || ease || 'linear' - - merge(this.transformation, transform) - - this.start = null -} - -/** - * Gets values from state params - * @param {string} type - */ -Animation.prototype.get = function (type) { - return this.state[type] -} - -/** - * Runs one tick of animation - * @param {number} tick - * @param {boolean} seek Is used in seek mode - */ -Animation.prototype.run = function (tick, seek) { - if (tick < this.start && !seek) return - var percent = 0 - - if (tick >= this.start) { - percent = (tick - this.start) / this.duration - percent = this.ease(percent) - } - - this.transform(percent) -} - -/** - * Pauses animation - */ -Animation.prototype.pause = function () { - this.diff = performance.now() - this.start -} - -/** - * Resumes animation - */ -Animation.prototype.resume = function () { - this.start = performance.now() - this.diff -} - -Animation.prototype.interpolate = function (property, percent) { - return this.get(property).interpolate(percent) -} - -/** - * Transforms item - * @param {number} percent - */ -Animation.prototype.transform = function (percent) { - for (var property in this.state) { - this.item.set(property, this.interpolate(property, percent)) - } -} - -/** - * Ends animation - * @param {boolean} abort - * @param {boolean} seek Is used in seek mode - */ -Animation.prototype.end = function (abort, seek) { - !abort && this.transform(this.ease(1)) - !seek && (this.start = null) +import { Item } from "../item.js" +import { easings } from "./easings.js" +import { Vector } from "../math/vector.js" +import { Matrix } from "../math/matrix.js" +import { Tween } from "./tween.js" +import { merge, transformProperty } from "../utils.js" + +export class Animation { + /** + * Creates new animation + * @param {Item} item Object to animate + * @param {Object} transform + * @param {number} duration + * @param {string} ease Timing function + * @param {number} delay + * @constructor + */ + constructor(item, transform, duration, ease, delay) { + this.item = item + + this.transformation = transform + + this.start = null + this.diff = null + + this.duration = (transform.duration || duration) | 0 + this.delay = (transform.delay || delay) | 0 + ease = transform.ease || ease + this.ease = easings[ease] || easings.linear + this.easeName = transform.ease || ease || "linear" + } + + static skip = { duration: null, delay: null, ease: null } + static transform = { translate: null, rotate: null, scale: null } + + static getState(transform, item) { + const initial = {} + let computed + + for (const property in transform) { + if (property in Animation.skip) continue + if (transform.hasOwnProperty(property)) { + if (item.get(property) == null) { + if (!computed) { + computed = getComputedStyle(item.dom, null) + } + Animation.setItemState(item, property, computed) + } + initial[property] = new Tween( + item.get(property), + transform[property], + property + ) + } + } + return initial + } + + static setItemState = function (item, property, computed) { + if (property in Animation.transform) { + let value = computed[transformProperty] + if (value === "none") { + value = { + translate: Vector.zero(), + rotate: Vector.zero(), + scale: Vector.set(1) + } + } else { + value = Matrix.decompose(Matrix.parse(value)) + } + item.set("translate", value.translate) + item.set("rotate", value.rotate) + item.set("scale", value.scale) + } else { + item.set(property, computed[property]) + } + } + + /** + * Starts animation timer + * @param {number} tick Timestamp + * @param {boolean=} seek Is used in seek mode + */ + init(tick, seek = false) { + if (this.start !== null && !seek) return + if (this.start === null) { + this.state = Animation.getState(this.transformation, this.item) + } + this.start = tick + this.delay + } + + /** + * Merges animation values + * @param {Object} transform + * @param {number} duration + * @param {string} ease Timing function + * @param {number} delay + */ + merge(transform, duration, ease, delay) { + this.duration = (transform.duration || duration) | 0 + this.delay = (transform.delay || delay) | 0 + ease = transform.ease || ease + this.ease = easings[ease] || easings.linear + this.easeName = transform.ease || ease || "linear" + + merge(this.transformation, transform) + + this.start = null + } + + /** + * Gets values from state params + * @param {string} type + */ + get(type) { + return this.state[type] + } + + /** + * Runs one tick of animation + * @param {number} tick + * @param {boolean} seek Is used in seek mode + */ + run(tick, seek) { + if (tick < this.start && !seek) return + let percent = 0 + + if (tick >= this.start) { + percent = (tick - this.start) / this.duration + percent = this.ease(percent) + } + + this.transform(percent) + } + + /** + * Pauses animation + */ + pause() { + this.diff = performance.now() - this.start + } + + /** + * Resumes animation + */ + resume() { + this.start = performance.now() - this.diff + } + + interpolate(property, percent) { + return this.get(property).interpolate(percent) + } + + /** + * Transforms item + * @param {number} percent + */ + transform(percent) { + for (const property in this.state) { + this.item.set(property, this.interpolate(property, percent)) + } + } + + /** + * Ends animation + * @param {boolean} abort + * @param {boolean} seek Is used in seek mode + */ + end(abort, seek) { + !abort && this.transform(this.ease(1)) + !seek && (this.start = null) + } } diff --git a/src/animations/collection.js b/src/animations/collection.js index 3bad9d1..462cee2 100644 --- a/src/animations/collection.js +++ b/src/animations/collection.js @@ -1,119 +1,132 @@ -/** - * Creates a set of animations - * @param {Item} item - * @constructor - */ -function Collection(item) { - EventEmitter.call(this) - - this.start = null - this.item = item - this.delay = 0 - this.duration = 0 - this.ease = easings.linear - this.easeName = 'linear' - this.animations = [] -} - -Collection.prototype = Object.create(EventEmitter.prototype) -Collection.prototype.constructor = Collection - -/** - * Add item to the collection - * @param transform - * @param duration - * @param ease - * @param delay - * @param generated - */ -Collection.prototype.add = function (transform, duration, ease, delay, generated) { - if (Array.isArray(transform)) { - transform = parallel(this.item, transform) - } else if (typeof transform == 'string' || transform.name != undefined) { - transform = new CssAnimation(this.item, transform, duration, ease, delay, generated) - } else if (!(transform instanceof Collection)) { - transform = new Animation(this.item, transform, duration, ease, delay) - } - - this.animations.push(transform) - - duration = this.animations.map(function (a) { - return a.duration + a.delay - }) - - if (this instanceof Parallel) { - this.duration = Math.max.apply(null, duration) - } else { - this.duration = duration.reduce(function (a, b) { - return a + b - }, 0) - } - - return this - - function sequence(item, transforms) { - var sequence = new Sequence(item) - - transforms.forEach(function (t) { - sequence.add(t, duration, ease, delay) - }) - - return sequence - } - - function parallel(item, transforms) { - var parallel = new Parallel(item) - - transforms.forEach(function (t) { - if (Array.isArray(t)) { - parallel.add(sequence(item, t)) - } else { - parallel.add(t, duration, ease, delay) - } - }) - - return parallel - } -} - -/** - * Collection length - */ -Object.defineProperty(Collection.prototype, 'length', { - get: function () { - return this.animations.length - } -}); - -/** - * Get element by index - * @param {number} index - * @returns {Animation|Collection} - */ -Collection.prototype.get = function (index) { - return this.animations[index] -} - -/** - * Remove all elements from collection - */ -Collection.prototype.empty = function () { - this.animations = [] -} - -/** - * Add animation to collection - * chainable - * @returns {Sequence} - */ -Collection.prototype.animate = function (transform, duration, ease, delay) { - return this.add(transform, duration, ease, delay) -} - -/** - * Apply styles - * @returns {CSS} - */ -Collection.prototype.css = function () { - return this.item.css() +import { EventEmitter } from "../eventemitter.js" +import { easings } from "./easings.js" +import { Item } from "../item.js" +import { CssAnimation } from "./css_animation.js" +import { Animation } from "./animation.js" +import { Sequence } from "./sequence.js" +import { Parallel } from "./parallel.js" +import { CSS } from "../css.js" + +export class Collection extends EventEmitter { + /** + * Creates a set of animations + * @param {Item} item + * @constructor + */ + constructor(item) { + super() + + this.start = null + this.item = item + this.delay = 0 + this.duration = 0 + this.ease = easings.linear + this.easeName = "linear" + this.animations = [] + } + + /** + * Add item to the collection + * @param transform + * @param duration + * @param ease + * @param delay + * @param generated + */ + add(transform, duration, ease, delay, generated) { + if (Array.isArray(transform)) { + transform = parallel(this.item, transform) + } else if (typeof transform == "string" || transform.name != undefined) { + transform = new CssAnimation( + this.item, + transform, + duration, + ease, + delay, + generated + ) + } else if (!(transform instanceof Collection)) { + transform = new Animation(this.item, transform, duration, ease, delay) + } + + this.animations.push(transform) + + duration = this.animations.map(function (a) { + return a.duration + a.delay + }) + + if (this instanceof Parallel) { + this.duration = Math.max.apply(null, duration) + } else { + this.duration = duration.reduce(function (a, b) { + return a + b + }, 0) + } + + return this + + function sequence(item, transforms) { + const sequence = new Sequence(item) + + transforms.forEach(function (t) { + sequence.add(t, duration, ease, delay) + }) + + return sequence + } + + function parallel(item, transforms) { + const parallel = new Parallel(item) + + transforms.forEach(function (t) { + if (Array.isArray(t)) { + parallel.add(sequence(item, t)) + } else { + parallel.add(t, duration, ease, delay) + } + }) + + return parallel + } + } + + /** + * Collection length + */ + get length() { + return this.animations.length + } + + /** + * Get element by index + * @param {number} index + * @returns {Animation | Parallel} + */ + get(index) { + return this.animations[index] + } + + /** + * Remove all elements from collection + */ + empty() { + this.animations = [] + } + + /** + * Add animation to collection + * chainable + * @returns {Collection} + */ + animate(transform, duration, ease, delay) { + return this.add(transform, duration, ease, delay) + } + + /** + * Apply styles + * @returns {CSS} + */ + css() { + return this.item.css() + } } diff --git a/src/animations/css_animation.js b/src/animations/css_animation.js index 7f86eaa..80d0f8b 100644 --- a/src/animations/css_animation.js +++ b/src/animations/css_animation.js @@ -1,77 +1,96 @@ -/** - * Creates new animation - * @param {Item} item Object to animate - * @param {Object || string} animation - * @param {number} duration - * @param {string} ease Timing function - * @param {number} delay - * @param {boolean} generated - * @constructor - */ -function CssAnimation(item, animation, duration, ease, delay, generated) { - this.item = item +import { Item } from "../item.js" +import { easings } from "./easings.js" +import { Matrix } from "../math/matrix.js" +import { animationProperty, transformProperty } from "../utils.js" - this.name = animation.name || animation +export class CssAnimation { + /** + * Creates new animation + * @param {Item} item Object to animate + * @param {Object | string} animation + * @param {number} duration + * @param {string} ease Timing function + * @param {number} delay + * @param {boolean} generated + * @constructor + */ + constructor(item, animation, duration, ease, delay, generated) { + this.item = item - this.start = null - this.diff = null + this.name = animation.name || animation - this.duration = (animation.duration || duration) | 0 - this.delay = (animation.delay || delay) | 0 - this.ease = easings.css[animation.ease] || easings.css[ease] || easings.css.linear + this.start = null + this.diff = null - this._infinite = false - this._generated = generated -} + this.duration = (animation.duration || duration) | 0 + this.delay = (animation.delay || delay) | 0 + this.ease = + easings.css[animation.ease] || easings.css[ease] || easings.css.linear -/** - * Starts animation timer - * @param {number} tick Timestamp - * @param {boolean=} force Force initialization - */ -CssAnimation.prototype.init = function (tick, force) { - if (this.start !== null && !force) return - this.start = tick + this.delay + this._infinite = false + this._generated = generated + } - this.item.style(animationProperty, - this.name + ' ' + this.duration + 'ms' + ' ' + this.ease + ' ' + - this.delay + 'ms' + (this._infinite ? ' infinite' : '') + ' ' + 'forwards') -} + /** + * Starts animation timer + * @param {number} tick Timestamp + * @param {boolean=} force Force initialization + */ + init(tick, force) { + if (this.start !== null && !force) return + this.start = tick + this.delay -/** - * Runs one tick of animation - */ -CssAnimation.prototype.run = function () { -} + this.item.style( + animationProperty, + this.name + + " " + + this.duration + + "ms" + + " " + + this.ease + + " " + + this.delay + + "ms" + + (this._infinite ? " infinite" : "") + + " " + + "forwards" + ) + } -/** - * Pauses animation - */ -CssAnimation.prototype.pause = function () { - this.item.style(animationProperty + '-play-state', 'paused') - this.diff = performance.now() - this.start -} + /** + * Runs one tick of animation + */ + run() {} -/** - * Resumes animation - */ -CssAnimation.prototype.resume = function () { - this.item.style(animationProperty + '-play-state', 'running') - this.start = performance.now() - this.diff -} + /** + * Pauses animation + */ + pause() { + this.item.style(animationProperty + "-play-state", "paused") + this.diff = performance.now() - this.start + } + + /** + * Resumes animation + */ + resume() { + this.item.style(animationProperty + "-play-state", "running") + this.start = performance.now() - this.diff + } -/** - * Ends animation - */ -CssAnimation.prototype.end = function () { - if (this._generated) { - var computed = getComputedStyle(this.item.dom, null), - transform = computed[transformProperty] + /** + * Ends animation + */ + end() { + if (this._generated) { + const computed = getComputedStyle(this.item.dom, null) + const transform = computed[transformProperty] - this.item.style(animationProperty, '') - this.item.state = Matrix.decompose(Matrix.parse(transform)) - this.item.style() - } + this.item.style(animationProperty, "") + this.item.state = Matrix.decompose(Matrix.parse(transform)) + this.item.style() + } - this.start = null + this.start = null + } } diff --git a/src/animations/easings.js b/src/animations/easings.js index a3e8fdc..4fa90b8 100644 --- a/src/animations/easings.js +++ b/src/animations/easings.js @@ -3,80 +3,78 @@ * used for Animations * @type {Object} */ -var easings = (function () { - var fn = { - quad: function (p) { - return Math.pow(p, 2) - }, - cubic: function (p) { - return Math.pow(p, 3) - }, - quart: function (p) { - return Math.pow(p, 4) - }, - quint: function (p) { - return Math.pow(p, 5) - }, - expo: function (p) { - return Math.pow(p, 6) - }, - sine: function (p) { - return 1 - Math.cos(p * Math.PI / 2) - }, - circ: function (p) { - return 1 - Math.sqrt(1 - p * p) - }, - back: function (p) { - return p * p * (3 * p - 2) - } - } +export const easings = (function () { + const fn = { + quad: function (p) { + return Math.pow(p, 2) + }, + cubic: function (p) { + return Math.pow(p, 3) + }, + quart: function (p) { + return Math.pow(p, 4) + }, + quint: function (p) { + return Math.pow(p, 5) + }, + expo: function (p) { + return Math.pow(p, 6) + }, + sine: function (p) { + return 1 - Math.cos((p * Math.PI) / 2) + }, + circ: function (p) { + return 1 - Math.sqrt(1 - p * p) + }, + back: function (p) { + return p * p * (3 * p - 2) + } + } - var easings = { - linear: function (p) { - return p - } - } + const easings = { + linear: function (p) { + return p + } + } - Object.keys(fn).forEach(function (name) { - var ease = fn[name] - easings['ease-in-' + name] = ease - easings['ease-out-' + name] = function (p) { - return 1 - ease(1 - p) - } - easings['ease-in-out-' + name] = function (p) { - return p < 0.5 - ? ease(p * 2) / 2 - : 1 - ease(p * -2 + 2) / 2 - } - }) + Object.keys(fn).forEach(function (name) { + const ease = fn[name] + easings["ease-in-" + name] = ease + easings["ease-out-" + name] = function (p) { + return 1 - ease(1 - p) + } + easings["ease-in-out-" + name] = function (p) { + return p < 0.5 ? ease(p * 2) / 2 : 1 - ease(p * -2 + 2) / 2 + } + }) - easings.css = { - 'linear': 'cubic-bezier(0.000, 0.000, 1.000, 1.000)', - 'ease-in-quad': 'cubic-bezier(0.550, 0.085, 0.680, 0.530)', - 'ease-in-cubic': 'cubic-bezier(0.550, 0.055, 0.675, 0.190)', - 'ease-in-quart': 'cubic-bezier(0.895, 0.030, 0.685, 0.220)', - 'ease-in-quint': 'cubic-bezier(0.755, 0.050, 0.855, 0.060)', - 'ease-in-sine': 'cubic-bezier(0.470, 0.000, 0.745, 0.715)', - 'ease-in-expo': 'cubic-bezier(0.950, 0.050, 0.795, 0.035)', - 'ease-in-circ': 'cubic-bezier(0.600, 0.040, 0.980, 0.335)', - 'ease-in-back': 'cubic-bezier(0.600, -0.280, 0.735, 0.045)', - 'ease-out-quad': 'cubic-bezier(0.250, 0.460, 0.450, 0.940)', - 'ease-out-cubic': 'cubic-bezier(0.215, 0.610, 0.355, 1.000)', - 'ease-out-quart': 'cubic-bezier(0.165, 0.840, 0.440, 1.000)', - 'ease-out-quint': 'cubic-bezier(0.230, 1.000, 0.320, 1.000)', - 'ease-out-sine': 'cubic-bezier(0.390, 0.575, 0.565, 1.000)', - 'ease-out-expo': 'cubic-bezier(0.190, 1.000, 0.220, 1.000)', - 'ease-out-circ': 'cubic-bezier(0.075, 0.820, 0.165, 1.000)', - 'ease-out-back': 'cubic-bezier(0.175, 0.885, 0.320, 1.275)', - 'ease-in-out-quad': 'cubic-bezier(0.455, 0.030, 0.515, 0.955)', - 'ease-in-out-cubic': 'cubic-bezier(0.645, 0.045, 0.355, 1.000)', - 'ease-in-out-quart': 'cubic-bezier(0.770, 0.000, 0.175, 1.000)', - 'ease-in-out-quint': 'cubic-bezier(0.860, 0.000, 0.070, 1.000)', - 'ease-in-out-sine': 'cubic-bezier(0.445, 0.050, 0.550, 0.950)', - 'ease-in-out-expo': 'cubic-bezier(1.000, 0.000, 0.000, 1.000)', - 'ease-in-out-circ': 'cubic-bezier(0.785, 0.135, 0.150, 0.860)', - 'ease-in-out-back': 'cubic-bezier(0.680, -0.550, 0.265, 1.550)' - } + easings.css = { + linear: "cubic-bezier(0.000, 0.000, 1.000, 1.000)", + "ease-in-quad": "cubic-bezier(0.550, 0.085, 0.680, 0.530)", + "ease-in-cubic": "cubic-bezier(0.550, 0.055, 0.675, 0.190)", + "ease-in-quart": "cubic-bezier(0.895, 0.030, 0.685, 0.220)", + "ease-in-quint": "cubic-bezier(0.755, 0.050, 0.855, 0.060)", + "ease-in-sine": "cubic-bezier(0.470, 0.000, 0.745, 0.715)", + "ease-in-expo": "cubic-bezier(0.950, 0.050, 0.795, 0.035)", + "ease-in-circ": "cubic-bezier(0.600, 0.040, 0.980, 0.335)", + "ease-in-back": "cubic-bezier(0.600, -0.280, 0.735, 0.045)", + "ease-out-quad": "cubic-bezier(0.250, 0.460, 0.450, 0.940)", + "ease-out-cubic": "cubic-bezier(0.215, 0.610, 0.355, 1.000)", + "ease-out-quart": "cubic-bezier(0.165, 0.840, 0.440, 1.000)", + "ease-out-quint": "cubic-bezier(0.230, 1.000, 0.320, 1.000)", + "ease-out-sine": "cubic-bezier(0.390, 0.575, 0.565, 1.000)", + "ease-out-expo": "cubic-bezier(0.190, 1.000, 0.220, 1.000)", + "ease-out-circ": "cubic-bezier(0.075, 0.820, 0.165, 1.000)", + "ease-out-back": "cubic-bezier(0.175, 0.885, 0.320, 1.275)", + "ease-in-out-quad": "cubic-bezier(0.455, 0.030, 0.515, 0.955)", + "ease-in-out-cubic": "cubic-bezier(0.645, 0.045, 0.355, 1.000)", + "ease-in-out-quart": "cubic-bezier(0.770, 0.000, 0.175, 1.000)", + "ease-in-out-quint": "cubic-bezier(0.860, 0.000, 0.070, 1.000)", + "ease-in-out-sine": "cubic-bezier(0.445, 0.050, 0.550, 0.950)", + "ease-in-out-expo": "cubic-bezier(1.000, 0.000, 0.000, 1.000)", + "ease-in-out-circ": "cubic-bezier(0.785, 0.135, 0.150, 0.860)", + "ease-in-out-back": "cubic-bezier(0.680, -0.550, 0.265, 1.550)" + } - return easings -}()) + return easings +})() diff --git a/src/animations/parallel.js b/src/animations/parallel.js index b4bb83b..c91f902 100644 --- a/src/animations/parallel.js +++ b/src/animations/parallel.js @@ -1,92 +1,94 @@ -/** - * Creates a set of parallel animations - * @param {Item} item - * @constructor - */ -function Parallel(item) { - Collection.call(this, item) -} +import { Collection } from "./collection.js" +import { Item } from "../item.js" -Parallel.prototype = Object.create(Collection.prototype) -Parallel.prototype.constructor = Parallel +export class Parallel extends Collection { + /** + * Creates a set of parallel animations + * @param {Item} item + * @constructor + */ + constructor(item) { + super(item) + } -/** - * Calls a method on all animations - * @param {string} method - */ -Parallel.prototype.all = function (method) { - var args = Array.prototype.slice.call(arguments, 1) + /** + * Calls a method on all animations + * @param {string} method + */ + all(method) { + const args = Array.prototype.slice.call(arguments, 1) - for (var i = 0; i < this.animations.length; ++i) { - var a = this.animations[i] - a[method].apply(a, args) - } -} + for (let i = 0; i < this.animations.length; ++i) { + const a = this.animations[i] + a[method].apply(a, args) + } + } -/** - * Initializes all animations in a set - * @param {number} tick - * @param {boolean=} force Force initialization - * @fires Parallel#start - */ -Parallel.prototype.init = function (tick, force) { - if (this.start !== null && !force) return - this.start = tick - this.all('init', tick, force) - this.emit('start') -} + /** + * Initializes all animations in a set + * @param {number} tick + * @param {boolean=} force Force initialization + * @fires Parallel#start + */ + init(tick, force) { + if (this.start !== null && !force) return + this.start = tick + this.all("init", tick, force) + this.emit("start") + } -/** - * Runs one tick of animations - * @param {number} tick - */ -Parallel.prototype.run = function (tick) { - if (!this.animations.length) return + /** + * Runs one tick of animations + * @param {number} tick + */ + run(tick) { + if (!this.animations.length) return - for (var i = 0; i < this.animations.length; ++i) { - var a = this.animations[i] - if (a.start + a.duration <= tick) { - this.animations.splice(i--, 1) - a.end() - continue - } - a.run(tick) - } - this.item.style() + for (let i = 0; i < this.animations.length; ++i) { + const a = this.animations[i] + if (a.start + a.duration <= tick) { + this.animations.splice(i--, 1) + a.end() + continue + } + a.run(tick) + } + this.item.style() - if (!this.animations.length) { - this.end() - } -} + if (!this.animations.length) { + this.end() + } + } -/** - * Seeks to the animation tick - * @param {number} tick - */ -Parallel.prototype.seek = function (tick) { - this.run(tick) -} + /** + * Seeks to the animation tick + * @param {number} tick + */ + seek(tick) { + this.run(tick) + } -/** - * Pauses animations - */ -Parallel.prototype.pause = function () { - this.all('pause') -} + /** + * Pauses animations + */ + pause() { + this.all("pause") + } -/** - * Resumes animations - */ -Parallel.prototype.resume = function () { - this.all('resume') -} + /** + * Resumes animations + */ + resume() { + this.all("resume") + } -/** - * Ends all animations in a set - * @param {boolean} abort - * @fires Parallel#end - */ -Parallel.prototype.end = function (abort) { - this.all('end', abort) - this.emit('end') + /** + * Ends all animations in a set + * @param {boolean} abort + * @fires Parallel#end + */ + end(abort = false) { + this.all("end", abort) + this.emit("end") + } } diff --git a/src/animations/sequence.js b/src/animations/sequence.js index 32bf9b8..1e86f44 100644 --- a/src/animations/sequence.js +++ b/src/animations/sequence.js @@ -1,124 +1,127 @@ -/** - * Creates a set of parallel animations - * @param {Item} item - * @constructor - */ -function Sequence(item) { - Collection.call(this, item) +import { Collection } from "./collection" +import { CssAnimation } from "./css_animation" +import { Item } from "../item" - this._infinite = false -} +export class Sequence extends Collection { + /** + * Creates a set of parallel animations + * @param {Item} item + * @constructor + */ + constructor(item) { + super(item) -Sequence.prototype = Object.create(Collection.prototype) -Sequence.prototype.constructor = Sequence + this._infinite = false + } -/** - * Initializes all animations in a set - * @param {number} tick - * @param {boolean=} force Force initialization - * @fires Sequence#start - */ -Sequence.prototype.init = function (tick, force) { - if (this.start !== null && !force) return + /** + * Initializes all animations in a set + * @param {number} tick + * @param {boolean=} force Force initialization + * @fires Sequence#start + */ + init(tick, force) { + if (this.start !== null && !force) return - this.start = tick - this.animations[0].init(tick, force) - this.emit('start') -} + this.start = tick + this.animations[0].init(tick, force) + this.emit("start") + } -/** - * Runs one tick of animations - * @param {number} tick - */ -Sequence.prototype.run = function (tick, a) { - if (!this.animations.length) return + /** + * Runs one tick of animations + * @param {number} tick + */ + run(tick, a) { + if (!this.animations.length) return - while (this.animations.length !== 0) { - a = this.animations[0] - if (a instanceof CssAnimation) { - a._infinite = this._infinite - } - a.init(tick) - if (a.start + a.duration <= tick) { - if (!(this._infinite && a instanceof CssAnimation)) { - this.animations.shift() - a.end() - } else { - break - } - if (this._infinite && !(a instanceof CssAnimation)) { - this.animations.push(a) - } - continue - } - a.run(tick) - break - } + while (this.animations.length !== 0) { + a = this.animations[0] + if (a instanceof CssAnimation) { + a._infinite = this._infinite + } + a.init(tick) + if (a.start + a.duration <= tick) { + if (!(this._infinite && a instanceof CssAnimation)) { + this.animations.shift() + a.end() + } else { + break + } + if (this._infinite && !(a instanceof CssAnimation)) { + this.animations.push(a) + } + continue + } + a.run(tick) + break + } - if (!(a instanceof CssAnimation)) { - this.item.style() - } + if (!(a instanceof CssAnimation)) { + this.item.style() + } - if (!this.animations.length) { - this.end() - } -} + if (!this.animations.length) { + this.end() + } + } -/** - * Seeks animations - * @param {number} tick - */ -Sequence.prototype.seek = function (tick) { - if (this.animations.length === 0) return - var time = 0 - for (var i = 0; i < this.animations.length; ++i) { - var a = this.animations[i] - a.init(time, true) - if (a.start + a.duration <= tick) { - time += a.delay + a.duration - a.end(false, true) - continue - } else { - a.run(tick, true) - } - break - } - this.item.style() -} + /** + * Seeks animations + * @param {number} tick + */ + seek(tick) { + if (this.animations.length === 0) return + let time = 0 + for (let i = 0; i < this.animations.length; ++i) { + const a = this.animations[i] + a.init(time, true) + if (a.start + a.duration <= tick) { + time += a.delay + a.duration + a.end(false, true) + continue + } else { + a.run(tick, true) + } + break + } + this.item.style() + } -/** - * Play animation infinitely - * @returns {Sequence} - */ -Sequence.prototype.infinite = function () { - this._infinite = true - return this -} + /** + * Play animation infinitely + * @returns {Sequence} + */ + infinite() { + this._infinite = true + return this + } -/** - * Pauses animations - */ -Sequence.prototype.pause = function () { - this.animations.length && this.animations[0].pause() -} + /** + * Pauses animations + */ + pause() { + this.animations.length && this.animations[0].pause() + } -/** - * Resumes animations - */ -Sequence.prototype.resume = function () { - this.animations.length && this.animations[0].resume() -} + /** + * Resumes animations + */ + resume() { + this.animations.length && this.animations[0].resume() + } -/** - * Ends all animations in a set - * @param {boolean} abort - * @fires Sequence#end - */ -Sequence.prototype.end = function (abort) { - for (var i = 0; i < this.animations.length; ++i) { - this.animations[i].end(abort) - } - this.animations = [] - this._infinite = false - this.emit('end') + /** + * Ends all animations in a set + * @param {boolean} abort + * @fires Sequence#end + */ + end(abort = false) { + for (let i = 0; i < this.animations.length; ++i) { + this.animations[i].end(abort) + } + this.animations = [] + this._infinite = false + this.emit("end") + } } diff --git a/src/animations/tween.js b/src/animations/tween.js index 0081d01..790b2b8 100644 --- a/src/animations/tween.js +++ b/src/animations/tween.js @@ -1,109 +1,123 @@ -function Tween(start, end, property) { - var type = Tween.propTypes[property] || Tween.NUMERIC - this.type = type +export class Tween { + constructor(start, end, property) { + const type = Tween.propTypes[property] || Tween.NUMERIC + this.type = type - this.start = Tween.parseValue(start, type) - this.end = Tween.parseValue(end, type) - - this.suffix = Tween.px.indexOf(property) !== -1 ? 'px' : '' -} + this.start = Tween.parseValue(start, type) + this.end = Tween.parseValue(end, type) -Tween.NUMERIC = 'NUMERIC' -Tween.COLOR = 'COLOR' + this.suffix = Tween.px.indexOf(property) !== -1 ? "px" : "" + } -Tween.propTypes = { - color: Tween.COLOR, - backgroundColor: Tween.COLOR, - borderColor: Tween.COLOR -} + static NUMERIC = "NUMERIC" + static COLOR = "COLOR" + + static propTypes = { + color: Tween.COLOR, + backgroundColor: Tween.COLOR, + borderColor: Tween.COLOR + } -Tween.px = '\ + static px = "\ margin,marginTop,marginLeft,marginBottom,marginRight,\ padding,paddingTop,paddingLeft,paddingBottom,paddingRight,\ top,left,bottom,right,\ width,height,maxWidth,maxHeight,minWidth,minHeight,\ -borderRadius,borderWidth'.split(',') +borderRadius,borderWidth".split(",") -Tween.parseValue = function (value, type) { - return type === Tween.COLOR ? Tween.parseColor(value) : Tween.parseNumeric(value) -} + static parseValue(value, type) { + return type === Tween.COLOR + ? Tween.parseColor(value) + : Tween.parseNumeric(value) + } -Tween.parseNumeric = function (numeric) { - if (!Array.isArray(numeric)) { - numeric = String(numeric).split(/\s+/) - } - return Array.isArray(numeric) ? numeric.map(parseFloat) : Number(numeric) -} + static parseNumeric(numeric) { + if (!Array.isArray(numeric)) { + numeric = String(numeric).split(/\s+/) + } + return Array.isArray(numeric) ? numeric.map(parseFloat) : Number(numeric) + } -Tween.parseColor = function (color) { - var hex = color.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i) - if (hex) { - return { - r: parseInt(hex[1], 16), - g: parseInt(hex[2], 16), - b: parseInt(hex[3], 16), - a: 1 - } - } + static parseColor(color) { + const hex = color.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i) + if (hex) { + return { + r: parseInt(hex[1], 16), + g: parseInt(hex[2], 16), + b: parseInt(hex[3], 16), + a: 1 + } + } - var rgb = color.match(/^rgba?\(([0-9.]*), ?([0-9.]*), ?([0-9.]*)(?:, ?([0-9.]*))?\)$/) - if (rgb) { - return { - r: parseFloat(rgb[1]), - g: parseFloat(rgb[2]), - b: parseFloat(rgb[3]), - a: parseFloat(rgb[4] != null ? rgb[4] : 1) - } - } -} + const rgb = color.match( + /^rgba?\(([0-9.]*), ?([0-9.]*), ?([0-9.]*)(?:, ?([0-9.]*))?\)$/ + ) + if (rgb) { + return { + r: parseFloat(rgb[1]), + g: parseFloat(rgb[2]), + b: parseFloat(rgb[3]), + a: parseFloat(rgb[4] != null ? rgb[4] : 1) + } + } + } -Tween.prototype.interpolate = function (percent) { - if (this.type === Tween.NUMERIC) { - if (Array.isArray(this.end)) { - return this.array(percent) - } else if (this.end !== undefined) { - return this.absolute(percent) - } - } else if (this.type === Tween.COLOR) { - return this.color(percent) - } -} + interpolate(percent) { + if (this.type === Tween.NUMERIC) { + if (Array.isArray(this.end)) { + return this.array(percent) + } else if (this.end !== undefined) { + return this.absolute(percent) + } + } else if (this.type === Tween.COLOR) { + return this.color(percent) + } + } -Tween.prototype.array = function (percent) { - var value = [] - for (var i = 0; i < this.end.length; ++i) { - if (this.end[i]) { - value[i] = this.start[i] + this.end[i] * percent - if (this.suffix) { - value[i] += this.suffix - } - } - } - return value -} + array(percent) { + const value = [] + if (Array.isArray(this.end)) { + for (let i = 0; i < this.end.length; ++i) { + if (this.end[i]) { + value[i] = this.start[i] + this.end[i] * percent + if (this.suffix) { + value[i] += this.suffix + } + } + } + } + return value + } -Tween.prototype.absolute = function (percent) { - var value = Number(this.start) + (Number(this.end) - Number(this.start)) * percent - if (this.suffix) { - value += this.suffix - } - return value -} + absolute(percent) { + /** @type {number | string} */ + let value = + Number(this.start) + (Number(this.end) - Number(this.start)) * percent + if (this.suffix) { + value = value + this.suffix + } + return value + } -Tween.prototype.color = function (percent) { - var rgb = {r:0,g:0,b:0} - for (var spectra in rgb) { - var value = Math.round(this.start[spectra] + (this.end[spectra] - this.start[spectra]) * percent) - rgb[spectra] = clamp(value, 0, 255) - } - spectra = 'a' - value = Math.round(this.start[spectra] + (this.end[spectra] - this.start[spectra]) * percent) - rgb[spectra] = clamp(value, 0, 1) - return 'rgba(' + [rgb.r, rgb.g, rgb.b, rgb.a] + ')' + color(percent) { + const rgb = { r: 0, g: 0, b: 0 } + let spectra, value + for (spectra in rgb) { + const value = Math.round( + this.start[spectra] + + (this.end[spectra] - this.start[spectra]) * percent + ) + rgb[spectra] = clamp(value, 0, 255) + } + spectra = "a" + value = Math.round( + this.start[spectra] + (this.end[spectra] - this.start[spectra]) * percent + ) + rgb[spectra] = clamp(value, 0, 1) + return "rgba(" + [rgb.r, rgb.g, rgb.b, rgb.a] + ")" + } } function clamp(value, min, max) { - return Math.min(max, Math.max(min, value)); + return Math.min(max, Math.max(min, value)) } - - diff --git a/src/core.js b/src/core.js index a2a5cb6..0e73466 100644 --- a/src/core.js +++ b/src/core.js @@ -1,29 +1,23 @@ +import { Timeline } from "./timeline.js" +import { World } from "./world.js" + /** * Animatic * @type {Object} */ -var a = {} - -/** - * Creates and initializes world with frame loop - * @return {World} - */ -a.world = function () { - return new World -} - -/** - * Creates and initializes timeline - * @return {Timeline} - */ -a.timeline = function () { - return new Timeline -} - -if (typeof module === 'object' && typeof module.exports === 'object') { - module.exports = a -} else if (typeof define === 'function' && define.amd) { - define(a) -} else { - root.animatic = root.a = a +export default { + /** + * Creates and initializes world with frame loop + * @return {World} + */ + world() { + return new World() + }, + /** + * Creates and initializes timeline + * @return {Timeline} + */ + timeline() { + return new Timeline() + } } diff --git a/src/css.js b/src/css.js index b9e188c..bfae091 100644 --- a/src/css.js +++ b/src/css.js @@ -1,150 +1,178 @@ -/** - * CSSify animations - * @param {Item} item - * @param {boolean=} idle - * @constructor - */ -function CSS(item, idle) { - !document.styleSheets.length && this.createStyleSheet() - this.stylesheet = document.styleSheets[0] - - this.item = item - this.animation = item.animation - - !idle && this.style() -} - -CSS.skip = {translate: null, rotate: null, scale: null}; - -/** - * Creates new stylesheet and adds it to HEAD - */ -CSS.prototype.createStyleSheet = function () { - var style = document.createElement('style') - document.getElementsByTagName('head')[0].appendChild(style) -} - -/** - * Pauses CSS animation - */ -CSS.prototype.pause = function () { - this.animation.pause() -} - -/** - * Resumes CSS animation - */ -CSS.prototype.resume = function () { - this.animation.resume() -} - -/** - * Stops CSS animation - * parses current transformation matrix - * extracts values and sets item state - */ -CSS.prototype.stop = function () { - var computed = getComputedStyle(this.item.dom, null), - transform = computed[transformProperty] - - this.item.style(animationProperty, '') - this.item.state = Matrix.decompose(Matrix.parse(transform)) - this.item.style() - - return this -} - -/** - * Applies animations and sets item style - */ -CSS.prototype.style = function () { - var animation = 'a' + Date.now() + 'r' + Math.floor(Math.random() * 1000) - - var cssRules = this.stylesheet.cssRules - this.stylesheet.insertRule(this.keyframes(animation), cssRules ? cssRules.length : 0) - - this.animation.empty() - this.animation.add(animation, this.animation.duration, '', 0, true) -} - -/** - * Generates @keyframes based on animations - * @param {string} name Animation name - * @return {string} - */ -CSS.prototype.keyframes = function (name) { - var time = 0, - rule = ['@' + getProperty('keyframes') + ' ' + name + '{'] - - for (var i = 0; i < this.animation.length; ++i) { - var a = this.animation.get(i), - aNext = this.animation.get(i + 1) - - a.init() - - if (a instanceof Animation) { // Single - i === 0 && rule.push(this.frame(0, easings.css[a.easeName])) - - a.delay && rule.push(this.frame(time += a.delay)) - - a.transform(1) - - rule.push(this.frame(time += a.duration, aNext && easings.css[aNext.easeName])) - } else { // Parallel (it doesn't work with custom easings for now) - var frames = [] - a.animations.forEach(function frame(a) { - a.animations && a.animations.forEach(frame) - a.delay && frames.indexOf(a.delay) === -1 && frames.push(a.delay) - a.duration && frames.indexOf(a.delay + a.duration) === -1 && frames.push(a.delay + a.duration) - }) - - frames = frames.sort(function (a, b) { - return a - b - }) - - for (var k = 0; k < frames.length; ++k) { - var frame = frames[k] - for (var j = 0; j < a.animations.length; ++j) { - var pa = a.animations[j] - // it's animation start or it's already ended - if (pa.delay >= frame || pa.delay + pa.duration < frame) - continue - pa.transform(pa.ease((frame - pa.delay) / pa.duration)) - } - - rule.push(this.frame(time += frame)) - } - } - } - rule.push('}') - return rule.join('') -} - -/** - * Calcuates percent for keyframes - * @param {number} time - * @return {string} - */ -CSS.prototype.percent = function (time) { - return (time * 100 / this.animation.duration).toFixed(3) -} - -/** - * Generates one frame for @keyframes - * @param {number} time - * @param {string=} ease - * @return {string} - */ -CSS.prototype.frame = function (time, ease) { - var percent = this.percent(time), - props = [] - for (var property in this.item.state) { - if (property in CSS.skip) continue - props.push(percent ? property.replace(/([A-Z])/g, '-$1') + ':' + this.item.get(property) + ';' : '') - } - return percent + '% {' + - (percent ? transformProperty + ':' + this.item.transform() + ';' : '') + - (props.join('')) + - (ease ? getProperty('animation-timing-function') + ':' + ease + ';' : '') + - '}' +import { Collection } from "./animations/collection.js" +import { easings } from "./animations/easings.js" +import { Item } from "./item.js" +import { Matrix } from "./math/matrix.js" +import { animationProperty, getProperty, transformProperty } from "./utils.js" + +export class CSS { + /** + * CSSify animations + * @param {Item} item + * @param {boolean=} idle + * @constructor + */ + constructor(item, idle) { + !document.styleSheets.length && this.createStyleSheet() + this.stylesheet = document.styleSheets[0] + + this.item = item + this.animation = item.animation + + !idle && this.style() + } + + static skip = { translate: null, rotate: null, scale: null } + + /** + * Creates new stylesheet and adds it to HEAD + */ + createStyleSheet() { + const style = document.createElement("style") + document.getElementsByTagName("head")[0].appendChild(style) + } + + /** + * Pauses CSS animation + */ + pause() { + this.animation.pause() + } + + /** + * Resumes CSS animation + */ + resume() { + this.animation.resume() + } + + /** + * Stops CSS animation + * parses current transformation matrix + * extracts values and sets item state + */ + stop() { + const computed = getComputedStyle(this.item.dom, null), + transform = computed[transformProperty] + + this.item.style(animationProperty, "") + this.item.state = Matrix.decompose(Matrix.parse(transform)) + this.item.style() + + return this + } + + /** + * Applies animations and sets item style + */ + style() { + const animation = "a" + Date.now() + "r" + Math.floor(Math.random() * 1000) + + const cssRules = this.stylesheet.cssRules + this.stylesheet.insertRule( + this.keyframes(animation), + cssRules ? cssRules.length : 0 + ) + + this.animation.empty() + this.animation.add(animation, this.animation.duration, "", 0, true) + } + + /** + * Generates @keyframes based on animations + * @param {string} name Animation name + * @return {string} + */ + keyframes(name) { + let time = 0 + const rule = ["@" + getProperty("keyframes") + " " + name + "{"] + + for (let i = 0; i < this.animation.length; ++i) { + const a = this.animation.get(i) + const aNext = this.animation.get(i + 1) + + a.init(time) + + if (a instanceof Collection) { + // Parallel (it doesn't work with custom easings for now) + let frames = [] + a.animations.forEach(function frame(a) { + a.animations && a.animations.forEach(frame) + a.delay && frames.indexOf(a.delay) === -1 && frames.push(a.delay) + a.duration && + frames.indexOf(a.delay + a.duration) === -1 && + frames.push(a.delay + a.duration) + }) + + frames = frames.sort(function (a, b) { + return a - b + }) + + for (let k = 0; k < frames.length; ++k) { + const frame = frames[k] + for (let j = 0; j < a.animations.length; ++j) { + const pa = a.animations[j] + // it's animation start or it's already ended + if (pa.delay >= frame || pa.delay + pa.duration < frame) continue + pa.transform(pa.ease((frame - pa.delay) / pa.duration)) + } + + rule.push(this.frame((time += frame))) + } + } else { + // Single + i === 0 && rule.push(this.frame(0, easings.css[a.easeName])) + + a.delay && rule.push(this.frame((time += a.delay))) + + a.transform(1) + + rule.push( + this.frame((time += a.duration), aNext && easings.css[aNext.easeName]) + ) + } + } + rule.push("}") + return rule.join("") + } + + /** + * Calcuates percent for keyframes + * @param {number} time + * @return {string} + */ + percent(time) { + return ((time * 100) / this.animation.duration).toFixed(3) + } + + /** + * Generates one frame for @keyframes + * @param {number} time + * @param {string=} ease + * @return {string} + */ + frame(time, ease) { + const percent = this.percent(time) + const props = [] + for (const property in this.item.state) { + if (property in CSS.skip) continue + props.push( + percent + ? property.replace(/([A-Z])/g, "-$1") + + ":" + + this.item.get(property) + + ";" + : "" + ) + } + return ( + percent + + "% {" + + (percent ? transformProperty + ":" + this.item.transform() + ";" : "") + + props.join("") + + (ease + ? getProperty("animation-timing-function") + ":" + ease + ";" + : "") + + "}" + ) + } } diff --git a/src/eventemitter.js b/src/eventemitter.js index 853f4f2..692adb1 100644 --- a/src/eventemitter.js +++ b/src/eventemitter.js @@ -1,64 +1,61 @@ -/** - * EventEmitter - * @constructor - */ -function EventEmitter() { - this.handlers = {} -} - -/** - * Adds handler for event - * @param {string} event - * @param {Function} handler - * @return {EventEmitter} - */ -EventEmitter.prototype.on = function (event, handler) { - (this.handlers[event] = this.handlers[event] || []) - .push(handler) - return this -} - -/** - * Removes event handler - * @param {string} event - * @param {Function} handler - * @return {EventEmitter} - */ -EventEmitter.prototype.off = function (event, handler) { - var handlers = this.handlers[event] - - if (handler) { - handlers.splice(handlers.indexOf(handler), 1) - } else { - delete this.handlers[event] - } - - return this -} - -/** - * Triggers event - * @param {string} event - * @return {EventEmitter} - */ -EventEmitter.prototype.emit = function (event) { - var args = Array.prototype.slice.call(arguments, 1), - handlers = this.handlers[event] - - if (handlers) { - for (var i = 0; i < handlers.length; ++i) { - handlers[i].apply(this, args) - } - } - - return this -} - -/** - * List all event listeners - * @param {string} event - * @returns {Array} - */ -EventEmitter.prototype.listeners = function (event) { - return this.handlers[event] || [] +export class EventEmitter { + constructor() { + this.handlers = {} + } + + /** + * Adds handler for event + * @param {string} event + * @param {Function} handler + * @returns {EventEmitter} + */ + on(event, handler) { + this.handlers[event] ??= [] + this.handlers[event].push(handler) + return this + } + + /** + * Removes event handler + * @param {string} event + * @param {Function} handler + * @returns {EventEmitter} + */ + off(event, handler) { + const handlers = this.handlers[event] + + if (handler) { + handlers.splice(handlers.indexOf(handler), 1) + } else { + delete this.handlers[event] + } + + return this + } + + /** + * Triggers event + * @param {string} event + * @returns {EventEmitter} + */ + emit(event, ...args) { + const handlers = this.handlers[event] + + if (handlers) { + for (let i = 0; i < handlers.length; ++i) { + handlers[i].apply(this, args) + } + } + + return this + } + + /** + * List all event listeners + * @param {string} event + * @returns {Array} + */ + listeners(event) { + return this.handlers[event] || [] + } } diff --git a/src/item.js b/src/item.js index a1d6cd4..918641f 100644 --- a/src/item.js +++ b/src/item.js @@ -1,199 +1,205 @@ -/** - * Creates new animated item - * @param {HTMLElement} node - * @constructor - */ -function Item(node) { - EventEmitter.call(this) - - this.dom = node - - this.animation = new Sequence(this) - - this.running = true - this.state = {} -} - -Item.prototype = Object.create(EventEmitter.prototype) -Item.prototype.constructor = Item - -/** - * Updates item on frame - * @param {number} tick - */ -Item.prototype.update = function (tick) { - if (!this.running) return - this.animation.run(tick) -} - -/** - * Updates item on timeline - * @param {number} tick - */ -Item.prototype.timeline = function (tick) { - this.clear() - this.animation.seek(tick) -} - -/** - * Pauses item animation - */ -Item.prototype.pause = function () { - if (!this.running) return - this.animation.pause() - this.running = false -} - -/** - * Resumes item animation - */ -Item.prototype.resume = function () { - if (this.running) return - this.animation.resume() - this.running = true -} - -/** - * Sets style to the dom node - * @param {string=} property - * @param {string=} value - */ -Item.prototype.style = function (property, value) { - var style = this.dom.style; - if (property && value) { - style[property] = value - } else { - style[transformProperty] = this.transform() - for (var property in this.state) { - style[property] = this.get(property) - } - } -} - -/** - * Returns transform CSS value - * @return {string} - */ -Item.prototype.transform = function () { - return Matrix.stringify(this.matrix()) -} - -/** - * Calculates transformation matrix for the state - * @return {Object} - */ -Item.prototype.matrix = function () { - var state = this.state - return Matrix.compose( - state.translate, state.rotate, state.scale - ) -} - -/** - * Gets transformation needed to make Item in center - * @return {Object} - */ -Item.prototype.center = function () { - return Matrix.decompose(Matrix.inverse(this.matrix())) -} - -/** - * Rotates item to look at vector - * @param {Array} vector - */ -Item.prototype.lookAt = function (vector) { - var transform = Matrix.decompose(Matrix.lookAt( - vector, this.get('translate'), Vector.set(0, 1, 0) - )) - this.set('rotate', transform.rotate) -} - -/** - * Sets values to state params - * @param {string} type - * @param {Array|Number|String} value - * @return {Item} - */ -Item.prototype.set = function (type, value) { - if (Array.isArray(value)) { - this.state[type] || (this.state[type] = []) - for (var i = 0; i < value.length; ++i) { - if (value[i] !== undefined) { - this.state[type][i] = value[i] - } - } - } else { - this.state[type] = value - } - - return this -} - -/** - * Gets values from state params - * @param {string} type - */ -Item.prototype.get = function (type) { - return this.state[type] -} - -/** - * Clears item transform - */ -Item.prototype.clear = function () { - this.state.translate = Vector.zero() - this.state.rotate = Vector.zero() - this.state.scale = Vector.set(1) -} - -/** - * Adds animation - * @param {Object|Array} transform - * @param {number} duration - * @param {string} ease - * @param {number} delay - * @return {Sequence} - */ -Item.prototype.animate = function (transform, duration, ease, delay) { - return this.animation.add(transform, duration, ease, delay) -} - -/** - * Alternates current animation - * @param {Object|Array} transform - * @param {number} duration - * @param {string} ease - * @param {number} delay - */ -Item.prototype.alternate = function (transform, duration, ease, delay) { - if (this.animation.length) { - this.animation.get(0).merge(transform, duration, ease, delay) - } else { - this.animate.call(this, transform, duration, ease, delay) - } -} - -/** - * Finishes all Item animations - * @param {boolean} abort - */ -Item.prototype.finish = function (abort) { - this.animation.end(abort) - return this -} - -/** - * Stops all Item animations - */ -Item.prototype.stop = function () { - return this.finish(true) -} - -/** - * Generates CSS animation or transition - * @param {boolean=} idle - * @return {CSS} - */ -Item.prototype.css = function (idle) { - return new CSS(this, idle) +import { Vector } from './math/vector.js' +import { Sequence } from './animations/sequence.js' +import { Matrix } from './math/matrix.js' +import { EventEmitter } from './eventemitter.js' +import { CSS } from './css.js' +import { transformProperty } from './utils.js' +import { Collection } from "./animations/collection.js" + +export class Item extends EventEmitter { + /** + * Creates new animated item + * @param {HTMLElement} node + */ + constructor(node) { + super() + this.dom = node + + this.animation = new Sequence(this) + + this.running = true + this.state = {} + } + /** + * Updates item on frame + * @param {number} tick + */ + update(tick) { + if (!this.running) return + this.animation.run(tick) + } + + /** + * Updates item on timeline + * @param {number} tick + */ + timeline(tick) { + this.clear() + this.animation.seek(tick) + } + + /** + * Pauses item animation + */ + pause() { + if (!this.running) return + this.animation.pause() + this.running = false + } + + /** + * Resumes item animation + */ + resume() { + if (this.running) return + this.animation.resume() + this.running = true + } + + /** + * Sets style to the dom node + * @param {string=} property + * @param {string=} value + */ + style(property, value) { + const style = this.dom.style + if (property && value) { + style[property] = value + } else { + style[transformProperty] = this.transform() + for (const property in this.state) { + style[property] = this.get(property) + } + } + } + + /** + * Returns transform CSS value + * @return {string} + */ + transform() { + return Matrix.stringify(this.matrix()) + } + + /** + * Calculates transformation matrix for the state + * @return {Object} + */ + matrix() { + const state = this.state + return Matrix.compose(state.translate, state.rotate, state.scale) + } + + /** + * Gets transformation needed to make Item in center + * @return {Object} + */ + center() { + return Matrix.decompose(Matrix.inverse(this.matrix())) + } + + /** + * Rotates item to look at vector + * @param {Array} vector + */ + lookAt(vector) { + const transform = Matrix.decompose( + Matrix.lookAt(vector, this.get("translate"), Vector.set(0, 1, 0)) + ) + this.set("rotate", transform.rotate) + } + + /** + * Sets values to state params + * @param {string} type + * @param {Array|Number|String} value + * @return {Item} + */ + set(type, value) { + if (Array.isArray(value)) { + this.state[type] ||= [] + for (let i = 0; i < value.length; ++i) { + if (value[i] !== undefined) { + this.state[type][i] = value[i] + } + } + } else { + this.state[type] = value + } + + return this + } + + /** + * Gets values from state params + * @param {string} type + */ + get(type) { + return this.state[type] + } + + /** + * Clears item transform + */ + clear() { + this.state.translate = Vector.zero() + this.state.rotate = Vector.zero() + this.state.scale = Vector.set(1) + } + + /** + * Adds animation + * @param {Object|Array} transform + * @param {number} duration + * @param {string} ease + * @param {number} delay + * @return {Sequence} + */ + animate(transform, duration, ease, delay) { + return this.animation.add(transform, duration, ease, delay) + } + + /** + * Alternates current animation + * @param {Object|Array} transform + * @param {number} duration + * @param {string} ease + * @param {number} delay + */ + alternate(transform, duration, ease, delay) { + if (this.animation.length) { + const a = this.animation.get(0) + if (a instanceof Collection) { + return + } + a.merge(transform, duration, ease, delay) + } else { + this.animate.call(this, transform, duration, ease, delay) + } + } + + /** + * Finishes all Item animations + * @param {boolean=} abort + */ + finish(abort = false) { + this.animation.end(abort) + return this + } + + /** + * Stops all Item animations + */ + stop() { + return this.finish(true) + } + + /** + * Generates CSS animation or transition + * @param {boolean=} idle + * @return {CSS} + */ + css(idle = false) { + return new CSS(this, idle) + } } diff --git a/src/math/matrix.js b/src/math/matrix.js index 075cb27..bcdc41b 100644 --- a/src/math/matrix.js +++ b/src/math/matrix.js @@ -1,18 +1,20 @@ -var radians = Math.PI / 180 +import { Vector } from "./vector.js" + +const radians = Math.PI / 180 /** * Matrix object for transformation calculations * @type {Object} */ -var Matrix = { - identity: function () { +export const Matrix = { + identity() { return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] }, - multiply: function multiply(a, b) { // doesn't work for perspective - var c = this.identity() + multiply(a, b) { // doesn't work for perspective + const c = this.identity() c[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] c[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] @@ -32,14 +34,14 @@ var Matrix = { return 2 >= arguments.length ? c - : multiply.apply(this, [c].concat(Array.prototype.slice.call(arguments, 2))) + : this.multiply.apply(this, [c].concat(Array.prototype.slice.call(arguments, 2))) }, - translate: function (tx, ty, tz) { + translate(tx, ty, tz) { if (!(tx || ty || tz)) return this.identity() - tx || (tx = 0) - ty || (ty = 0) - tz || (tz = 0) + tx ||= 0 + ty ||= 0 + tz ||= 0 return [1, 0, 0, 0, 0, 1, 0, 0, @@ -47,22 +49,22 @@ var Matrix = { tx, ty, tz, 1] }, /* - translateX: function translateX(t) { + translateX(t) { return this.translate(t, 0, 0) }, - translateY: function translateY(t) { + translateY(t) { return this.translate(0, t, 0) }, - translateZ: function translateZ(t) { + translateZ(t) { return this.translate(0, 0, t) }, */ - scale: function (sx, sy, sz) { + scale(sx, sy, sz) { if (!(sx || sy || sz)) return this.identity() - sx || (sx = 1) - sy || (sy = 1) - sz || (sz = 1) + sx ||= 1 + sy ||= 1 + sz ||= 1 return [sx, 0, 0, 0, 0, sy, 0, 0, @@ -70,35 +72,35 @@ var Matrix = { 0, 0, 0, 1] }, /* - scaleX: function scaleX(s) { + scaleX(s) { return this.scale(s, 0, 0) }, - scaleY: function scaleY(s) { + scaleY(s) { return this.scale(0, s, 0) }, - scaleZ: function scaleZ(s) { + scaleZ(s) { return this.scale(0, 0, s) }, */ - rotate: function (ax, ay, az) { + rotate(ax, ay, az) { if (!(ax || ay || az)) return this.identity() - ax || (ax = 0) - ay || (ay = 0) - az || (az = 0) + ax ||= 0 + ay ||= 0 + az ||= 0 ax *= radians ay *= radians az *= radians - var sx = Math.sin(ax), - cx = Math.cos(ax), + const sx = Math.sin(ax) + const cx = Math.cos(ax) - sy = Math.sin(ay), - cy = Math.cos(ay), + const sy = Math.sin(ay) + const cy = Math.cos(ay) - sz = Math.sin(az), - cz = Math.cos(az) + const sz = Math.sin(az) + const cz = Math.cos(az) return [cy * cz, cx * sz + sx * sy * cz, sx * sz - cx * sy * cz, 0, -cy * sz, cx * cz - sx * sy * sz, sx * cz + cx * sy * sz, 0, @@ -106,33 +108,33 @@ var Matrix = { 0, 0, 0, 1] }, /* - rotateX: function rotateX(a) { + rotateX(a) { a *= radians - var s = Math.sin(a), - c = Math.cos(a) + const s = Math.sin(a) + const c = Math.cos(a) return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1] }, - rotateY: function rotateY(a) { + rotateY(a) { a *= radians - var s = Math.sin(a), - c = Math.cos(a) + const s = Math.sin(a) + const c = Math.cos(a) return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1] }, - rotateZ: function rotateZ(a) { + rotateZrotateZ(a) { a *= radians - var s = Math.sin(a), - c = Math.cos(a) + const s = Math.sin(a) + const c = Math.cos(a) return [c, s, 0, 0, -s, c, 0, 0, @@ -140,34 +142,32 @@ var Matrix = { 0, 0, 0, 1] }, */ - rotate3d: function (x, y, z, a) { - a || (a = 0) - + rotate3d(x, y, z, a = 0) { a *= radians - var s = Math.sin(a), - c = Math.cos(a), - norm = Vector.norm(x, y, z) + const s = Math.sin(a) + const c = Math.cos(a) + const norm = Vector.norm(x, y, z) x = norm[0] y = norm[1] z = norm[2] - var xx = x * x, - yy = y * y, - zz = z * z, - _c = 1 - c + const xx = x * x + const yy = y * y + const zz = z * z + const _c = 1 - c return [xx + (1 - xx) * c, x * y * _c + z * s, x * z * _c - y * s, 0, x * y * _c - z * s, yy + (1 - yy) * c, y * z * _c + x * s, 0, x * z * _c + y * s, y * z * _c - x * s, zz + (1 - zz) * c, 0, 0, 0, 0, 1] }, - skew: function (ax, ay) { + skew(ax, ay) { if (!(ax || ay)) return this.identity() - ax || (ax = 0) - ay || (ay = 0) + ax ||= 0 + ay ||= 0 ax *= radians ay *= radians @@ -178,14 +178,14 @@ var Matrix = { 0, 0, 0, 1] }, /* - skewX: function skewX(a) { + skewX(a) { return this.skew(a, 0) }, - skewY: function skewY(a) { + skewY(a) { return this.skew(0, a) }, */ - perspective: function (p) { + perspective(p) { p = -1 / p return [1, 0, 0, 0, @@ -193,8 +193,8 @@ var Matrix = { 0, 0, 1, p, 0, 0, 0, 1] }, - parse: function (s) { - var m = s.match(/\((.+)\)/)[1].split(/,\s?/) + parse(s) { + const m = s.match(/\((.+)\)/)[1].split(/,\s?/) if (m.length === 6) { m.splice(2, 0, 0, 0) m.splice(6, 0, 0, 0) @@ -204,22 +204,22 @@ var Matrix = { return m }, - inverse: function (m) { - var a = this.identity(), + inverse(m) { + const a = this.identity() - inv0 = m[5] * m[10] - m[6] * m[9], - inv1 = m[1] * m[10] - m[2] * m[9], - inv2 = m[1] * m[6] - m[2] * m[5], + const inv0 = m[5] * m[10] - m[6] * m[9] + const inv1 = m[1] * m[10] - m[2] * m[9] + const inv2 = m[1] * m[6] - m[2] * m[5] - inv4 = m[4] * m[10] - m[6] * m[8], - inv5 = m[0] * m[10] - m[2] * m[8], - inv6 = m[0] * m[6] - m[2] * m[4], + const inv4 = m[4] * m[10] - m[6] * m[8] + const inv5 = m[0] * m[10] - m[2] * m[8] + const inv6 = m[0] * m[6] - m[2] * m[4] - inv8 = m[4] * m[9] - m[5] * m[8], - inv9 = m[0] * m[9] - m[1] * m[8], - inv10 = m[0] * m[5] - m[1] * m[4], + const inv8 = m[4] * m[9] - m[5] * m[8] + const inv9 = m[0] * m[9] - m[1] * m[8] + const inv10 = m[0] * m[5] - m[1] * m[4] - det = 1 / (m[0] * inv0 - m[1] * inv4 + m[2] * inv8) + const det = 1 / (m[0] * inv0 - m[1] * inv4 + m[2] * inv8) a[0] = det * inv0 a[1] = -det * inv1 @@ -239,12 +239,8 @@ var Matrix = { return a }, - compose: function (translate, rotate, scale) { - translate || (translate = []) - rotate || (rotate = []) - scale || (scale = []) - - var a = this.rotate(rotate[0], rotate[1], rotate[2]) + compose(translate = [], rotate = [], scale = []) { + const a = this.rotate(rotate[0], rotate[1], rotate[2]) if (scale.length) { a[0] *= scale[0] @@ -268,14 +264,14 @@ var Matrix = { return a }, - decompose: function (m) { // supports only scale*rotate*translate matrix - var sX = Vector.length(m[0], m[1], m[2]), - sY = Vector.length(m[4], m[5], m[6]), - sZ = Vector.length(m[8], m[9], m[10]) + decompose(m) { // supports only scale*rotate*translate matrix + const sX = Vector.length(m[0], m[1], m[2]) + const sY = Vector.length(m[4], m[5], m[6]) + const sZ = Vector.length(m[8], m[9], m[10]) - var rX = Math.atan2(-m[9] / sZ, m[10] / sZ) / radians, - rY = Math.asin(m[8] / sZ) / radians, - rZ = Math.atan2(-m[4] / sY, m[0] / sX) / radians + let rX = Math.atan2(-m[9] / sZ, m[10] / sZ) / radians + let rY = Math.asin(m[8] / sZ) / radians + let rZ = Math.atan2(-m[4] / sY, m[0] / sX) / radians if (m[4] === 1 || m[4] === -1) { rX = 0 @@ -283,9 +279,9 @@ var Matrix = { rZ = m[4] * Math.atan2(m[6] / sY, m[5] / sY) / radians } - var tX = m[12], - tY = m[13], - tZ = m[14] + const tX = m[12] + const tY = m[13] + const tZ = m[14] return { translate: [tX, tY, tZ], @@ -293,8 +289,8 @@ var Matrix = { scale: [sX, sY, sZ] } }, - transpose: function (m) { - var t + transpose(m) { + let t t = m[1] m[1] = m[4] @@ -322,21 +318,21 @@ var Matrix = { return m }, - lookAt: function (eye, target, up) { - var z = Vector.sub(eye, target) + lookAt(eye, target, up) { + let z = Vector.sub(eye, target) z = Vector.norm(z) if (Vector.length(z) === 0) z[2] = 1 - var x = Vector.cross(up, z) + let x = Vector.cross(up, z) if (Vector.length(x) === 0) { z[0] += 0.0001 x = Vector.norm(Vector.cross(up, z)) } - var y = Vector.cross(z, x) + const y = Vector.cross(z, x) - var a = this.identity() + const a = this.identity() a[0] = x[0] a[1] = x[1] @@ -352,8 +348,8 @@ var Matrix = { return a }, - stringify: function (m) { - for (var i = 0; i < m.length; ++i) { + stringify(m) { + for (let i = 0; i < m.length; ++i) { if (Math.abs(m[i]) < 1e-5) m[i] = 0 } return 'matrix3d(' + m.join() + ')' diff --git a/src/math/vector.js b/src/math/vector.js index a2f7e9e..c16348a 100644 --- a/src/math/vector.js +++ b/src/math/vector.js @@ -1,5 +1,5 @@ -var Vector = { - set: function (x, y, z) { +export const Vector = { + set(x, y, z) { if (Array.isArray(x)) { y = x[1] z = x[2] @@ -14,7 +14,7 @@ var Vector = { } return [x, y, z] }, - length: function (x, y, z) { + length(x, y, z) { if (Array.isArray(x)) { y = x[1] z = x[2] @@ -22,27 +22,27 @@ var Vector = { } return Math.sqrt(x * x + y * y + z * z) }, - add: function (a, b) { + add(a, b) { return [ a[0] + b[0], a[1] + b[1], a[2] + b[2] ] }, - sub: function (a, b) { + sub(a, b) { return [ a[0] - b[0], a[1] - b[1], a[2] - b[2] ] }, - norm: function (x, y, z) { + norm(x, y, z) { if (Array.isArray(x)) { y = x[1] z = x[2] x = x[0] } - var len = this.length(x, y, z) + const len = this.length(x, y, z) if (len !== 0) { x /= len @@ -56,24 +56,24 @@ var Vector = { return [x, y, z] }, - dist: function (a, b) { - var dx = a[0] - b[0], - dy = a[1] - b[1], - dz = a[2] - b[2] + dist(a, b) { + const dx = a[0] - b[0] + const dy = a[1] - b[1] + const dz = a[2] - b[2] return Math.sqrt(dx * dx + dy * dy + dz + dz) }, - cross: function (a, b) { - var x = a[1] * b[2] - a[2] * b[1], - y = a[2] * b[0] - a[0] * b[2], - z = a[1] * b[1] - a[1] * b[0] + cross(a, b) { + const x = a[1] * b[2] - a[2] * b[1] + const y = a[2] * b[0] - a[0] * b[2] + const z = a[1] * b[1] - a[1] * b[0] return [x, y, z] }, - clone: function (v) { + clone(v) { return v.slice() }, - scale: function (x, y, z, f) { + scale(x, y, z, f) { if (Array.isArray(x)) { f = y y = x[1] @@ -82,7 +82,7 @@ var Vector = { } return [x * f, y * f, z * f] }, - zero: function () { + zero() { return [0, 0, 0] } } diff --git a/src/physics/forces/attraction.js b/src/physics/forces/attraction.js index 959047d..607b76d 100644 --- a/src/physics/forces/attraction.js +++ b/src/physics/forces/attraction.js @@ -1,19 +1,26 @@ +import { Vector } from "../../math/vector.js" +import { Particle } from "../particle.js" + /** * Attraction force + * @param {Particle} item * @param {number} radius * @param {number} strength * @constructor */ -function Attraction(radius, strength) { - radius || (radius = 1000) - strength || (strength = 100) - - var force = Vector.sub(this.state.translate, this.current.position), - distance = Vector.length(force) +export function Attraction(item, radius = 1000, strength = 100) { + let force = Vector.sub(item.state.translate, item.current.position) + const distance = Vector.length(force) - if (distance < radius) { - force = Vector.scale(Vector.norm(force), 1.0 - (distance * distance) / (radius * radius)) + if (distance < radius) { + force = Vector.scale( + Vector.norm(force), + 1.0 - (distance * distance) / (radius * radius) + ) - this.current.acceleration = Vector.add(this.current.acceleration, Vector.scale(force, strength)) - } + item.current.acceleration = Vector.add( + item.current.acceleration, + Vector.scale(force, strength) + ) + } } diff --git a/src/physics/forces/constant.js b/src/physics/forces/constant.js index a4187df..5ff0db1 100644 --- a/src/physics/forces/constant.js +++ b/src/physics/forces/constant.js @@ -1,9 +1,13 @@ +import { Vector } from "../../math/vector" +import { Particle } from "../particle" + /** * Constant force + * @param {Particle} item * @constructor */ -function Constant() { - var force = Vector.sub(this.state.translate, this.current.position) +export function Constant(item) { + const force = Vector.sub(item.state.translate, item.current.position) - this.current.acceleration = Vector.add(this.current.acceleration, force) + item.current.acceleration = Vector.add(item.current.acceleration, force) } diff --git a/src/physics/forces/edge.js b/src/physics/forces/edge.js index 9ad5d6b..ee98160 100644 --- a/src/physics/forces/edge.js +++ b/src/physics/forces/edge.js @@ -1,21 +1,34 @@ +import { Vector } from '../../math/vector.js' +import { Particle } from '../particle.js' + /** * Edge force - * @param {Vector} min - * @param {Vector} max + * @param {Particle} item + * @param {number[]} min + * @param {number[]} max + * @param {boolean} bounce * @constructor */ -function Edge(min, max, bounce) { - min || (min = Vector.set(0)) - max || (max = Vector.set(0)) - bounce || (bounce = true) - - for (var i = 0; i < 3; ++i) { - if (this.current.position[i] < min[i] || this.current.position[i] > max[i]) { - if (bounce) { - this.previous.position[i] = 2 * this.current.position[i] - this.previous.position[i] - } else { - this.current.position[i] = Math.max(min[i], Math.min(max[i], this.current.position[i])) - } - } - } +export function Edge( + item, + min = Vector.set(0), + max = Vector.set(0), + bounce = true +) { + for (let i = 0; i < 3; ++i) { + if ( + item.current.position[i] < min[i] || + item.current.position[i] > max[i] + ) { + if (bounce) { + item.previous.position[i] = + 2 * item.current.position[i] - item.previous.position[i] + } else { + item.current.position[i] = Math.max( + min[i], + Math.min(max[i], item.current.position[i]) + ) + } + } + } } diff --git a/src/physics/particle.js b/src/physics/particle.js index 9e3bf63..0db2bf6 100644 --- a/src/physics/particle.js +++ b/src/physics/particle.js @@ -1,102 +1,118 @@ -/** - * Creatites particle with physics - * @param {HTMLElement} node - * @param {number=} mass - * @param {number=} viscosity - * @constructor - */ -function Particle(node, mass, viscosity, edge) { - Item.call(this, node) - - if (mass === Object(mass)) { - viscosity = mass.viscosity - edge = mass.edge - mass = mass.mass - } - - mass /= 100 - - mass || (mass = 0.01) - viscosity || (viscosity = 0.1) - edge || (edge = false) - - this.mass = 1 / mass - this.viscosity = viscosity - this.edge = edge - - this.current = { - position: Vector.zero(), - velocity: Vector.zero(), - acceleration: Vector.zero() - } - - this.previous = { - position: Vector.zero(), - velocity: Vector.zero(), - acceleration: Vector.zero() - } - - this.clock = null -} - -Particle.prototype = Object.create(Item.prototype) -Particle.prototype.constructor = Particle - -/** - * Updates particle and applies integration - * @param {number} tick - */ -Particle.prototype.update = function (tick) { - this.animation.run(tick) - - this.integrate(tick) - - this.style() -} - -Particle.prototype.timeline = function (tick) { - this.clear() - this.animation.seek(tick) - - this.integrate(tick, true) - - this.style() -} - -/** - * Integrates particle - * @param {number} delta - */ -Particle.prototype.integrate = function (tick, clamp) { - this.clock || (this.clock = tick) - - var delta = tick - this.clock - - if (delta) { - clamp && (delta = Math.max(-16, Math.min(16, delta))) - - this.clock = tick - - delta *= 0.001 - - Constant.call(this) - this.edge && Edge.call(this, Vector.set(this.edge.min), Vector.set(this.edge.max), this.edge.bounce) - - Verlet.call(this, delta, 1.0 - this.viscosity) - } -} - -Particle.prototype.css = function () { - throw new Error('CSS is nor supported for physics'); -} - -/** - * Gets particle matrix - * @returns {Array} - */ -Particle.prototype.matrix = function () { - var state = this.state - return Matrix.compose( - this.current.position, state.rotate, state.scale - ) +import { Item } from "../item.js" +import { Constant } from "./forces/constant.js" +import { Edge } from "./forces/edge.js" +import { Verlet } from "./verlet.js" +import { Matrix } from "../math/matrix.js" +import { Vector } from "../math/vector.js" + +export class Particle extends Item { + /** + * Creates particle with physics + * @param {HTMLElement} node + * @param {number | {mass:number, viscosity:number, edge: {min: number, max: number, bounce: boolean}}} mass + * @param {number} viscosity + * @param {null | {min: number, max: number, bounce: boolean}} edge + * @constructor + */ + constructor(node, mass, viscosity, edge) { + super(node) + + if (typeof mass === "object") { + viscosity = mass.viscosity + edge = mass.edge + mass = mass.mass + } else { + mass /= 100 + } + + mass ||= 0.01 + viscosity ||= 0.1 + edge ||= null + + this.mass = 1 / mass + this.viscosity = viscosity + this.edge = edge + + this.current = { + position: Vector.zero(), + velocity: Vector.zero(), + acceleration: Vector.zero() + } + + this.previous = { + position: Vector.zero(), + velocity: Vector.zero(), + acceleration: Vector.zero() + } + + this.clock = null + } + + /** + * Updates particle and applies integration + * @param {number} tick + */ + update(tick) { + this.animation.run(tick) + + this.integrate(tick) + + this.style() + } + + timeline(tick) { + this.clear() + this.animation.seek(tick) + + this.integrate(tick, true) + + this.style() + } + + /** + * Integrates particle + * @param {number} tick + * @param {boolean=} clamp + */ + integrate(tick, clamp) { + this.clock ||= tick + + let delta = tick - this.clock + + if (delta) { + clamp && (delta = Math.max(-16, Math.min(16, delta))) + + this.clock = tick + + delta *= 0.001 + + Constant.call(null, this) + this.edge && + Edge.call( + null, + this, + Vector.set(this.edge.min), + Vector.set(this.edge.max), + this.edge.bounce + ) + + Verlet.call(null, this, delta, 1.0 - this.viscosity) + } + } + + /** + * @return {ReturnType} + */ + css() { + throw new Error("CSS is nor supported for physics") + } + + /** + * Gets particle matrix + * @returns {Array} + */ + matrix() { + const state = this.state + return Matrix.compose(this.current.position, state.rotate, state.scale) + } } diff --git a/src/physics/verlet.js b/src/physics/verlet.js index e419365..d18c258 100644 --- a/src/physics/verlet.js +++ b/src/physics/verlet.js @@ -1,24 +1,34 @@ +import { Vector } from '../math/vector.js' +import { Particle } from './particle.js' + /** * Velocity Verlet Integrator + * @param {Particle} self * @param {number} delta * @param {number} drag * @constructor */ -function Verlet(delta, drag) { - // velocity = position - old_position - // position = position + (velocity + acceleration * delta * delta) - var current = this.current, - previous = this.previous +export function Verlet(self, delta, drag) { + // velocity = position - old_position + // position = position + (velocity + acceleration * delta * delta) + const current = self.current + const previous = self.previous - current.acceleration = Vector.scale(current.acceleration, this.mass) - current.velocity = Vector.sub(current.position, previous.position) + current.acceleration = Vector.scale(current.acceleration, self.mass) + current.velocity = Vector.sub(current.position, previous.position) - if (drag !== undefined) { - current.velocity = Vector.scale(current.velocity, drag) - } + if (drag !== undefined) { + current.velocity = Vector.scale(current.velocity, drag) + } - previous.position = current.position - current.position = Vector.add(current.position, Vector.add(current.velocity, Vector.scale(current.acceleration, delta * delta))) + previous.position = current.position + current.position = Vector.add( + current.position, + Vector.add( + current.velocity, + Vector.scale(current.acceleration, delta * delta) + ) + ) - current.acceleration = Vector.zero() + current.acceleration = Vector.zero() } diff --git a/src/timeline.js b/src/timeline.js index 53867fc..2e9f601 100644 --- a/src/timeline.js +++ b/src/timeline.js @@ -1,82 +1,84 @@ -/** - * Creates new Timeline and start frame loop - * @constructor - */ -function Timeline() { - World.call(this, true) - this.currentTime = 0 - this.start = 0 -} +import { fixTick } from "./utils.js" +import { World } from "./world.js" -Timeline.prototype = Object.create(World.prototype) -Timeline.prototype.constructor = Timeline +export class Timeline extends World { + /** + * Creates new Timeline and start frame loop + * @constructor + */ + constructor() { + super() + this.currentTime = 0 + this.start = 0 + } -/** - * Starts new frame loop - */ -Timeline.prototype.run = function () { - this.frame = requestAnimationFrame(update) + /** + * Starts new frame loop + */ + run() { + this.frame = requestAnimationFrame(update) - var self = this + const self = this - function update(tick) { - if (fixTick) { - tick = performance.now() - } - if (self.running) { - self.currentTime = tick - self.start - } - self.update(self.currentTime) - self.frame = requestAnimationFrame(update) - } -} + function update(tick) { + if (fixTick) { + tick = performance.now() + } + if (self.running) { + self.currentTime = tick - self.start + } + self.update(self.currentTime) + self.frame = requestAnimationFrame(update) + } + } -/** - * Updates Items in Timeline - * @param {number} tick - * @fires Timeline#update - */ -Timeline.prototype.update = function (tick) { - for (var i = 0, length = this.items.length; i < length; ++i) { - var item = this.items[i] - if (this.changed < length || this.running) { - item.timeline(tick) - this.changed++ - this.emit('update', tick) - } else { - item.style() - } - } -} + /** + * Updates Items in Timeline + * @param {number} tick + * @fires Timeline#update + */ + update(tick) { + for (let i = 0, length = this.items.length; i < length; ++i) { + const item = this.items[i] + if (this.changed < length || this.running) { + item.timeline(tick) + this.changed++ + this.emit("update", tick) + } else { + item.style() + } + } + } -/** - * Plays/Resumes Timeline - */ -Timeline.prototype.play = function () { - this.running = true - this.start = performance.now() - this.currentTime -} + /** + * Plays/Resumes Timeline + */ + play() { + this.running = true + this.start = performance.now() - this.currentTime + } -/** - * Pauses Timeline - */ -Timeline.prototype.pause = function () { - this.running = false -} + /** + * Pauses Timeline + */ + pause() { + this.running = false + } -/** - * Stops Timeline - */ -Timeline.prototype.stop = function () { - this.currentTime = 0 - this.running = false -} + /** + * Stops Timeline + */ + stop() { + this.currentTime = 0 + this.running = false + } -/** - * Sets Timeline time - * @param {number} time - */ -Timeline.prototype.seek = function (time) { - this.changed = 0 - this.currentTime = time + /** + * Sets Timeline time + * @param {number} time + */ + seek(time) { + this.changed = 0 + this.currentTime = time + } } diff --git a/src/utils.js b/src/utils.js index 547c0d2..b6a2a9b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,48 +2,38 @@ * Vendor specific stuff */ -var requestAnimationFrame = root.requestAnimationFrame, - cancelAnimationFrame = root.cancelAnimationFrame, - vendors = ['moz', 'webkit', 'ms'] +export const prefix = ([].slice + .call(getComputedStyle(document.documentElement, null)) + .join("") + .match(/(-(moz|webkit|ms)-)transform/) || [])[1] +export const transformProperty = getProperty("transform") +export const animationProperty = getProperty("animation") +export let fixTick -for (var i = 0; i < vendors.length && !requestAnimationFrame; i++) { - requestAnimationFrame = root[vendors[i] + 'RequestAnimationFrame'] - cancelAnimationFrame = root[vendors[i] + 'CancelAnimationFrame'] - || root[vendors[i] + 'CancelRequestAnimationFrame'] +export function getProperty(name) { + return name } -var prefix = ([].slice.call(getComputedStyle(document.documentElement, null)) - .join('').match(/(-(moz|webkit|ms)-)transform/) || [])[1], - transformProperty = getProperty('transform'), - animationProperty = getProperty('animation'), - fixTick - -function getProperty(name) { - return prefix ? prefix + name : name -} - -var performance = root.performance && root.performance.now ? root.performance : Date - -requestAnimationFrame(function(tick) { - fixTick = tick > 1e12 != performance.now() > 1e12 +requestAnimationFrame(function (tick) { + fixTick = tick > 1e12 != performance.now() > 1e12 }) -function merge(obj) { - var i = 1 - while (i < arguments.length) { - var source = arguments[i++] - for (var property in source) { - if (Array.isArray(source[property])) { - for (var j = 0; j < source[property].length; ++j) { - var value = source[property][j] - if (value) { - obj[property][j] = value - } - } - } else { - obj[property] = source[property] - } - } - } - return obj +export function merge(obj) { + let i = 1 + while (i < arguments.length) { + const source = arguments[i++] + for (const property in source) { + if (Array.isArray(source[property])) { + for (let j = 0; j < source[property].length; ++j) { + const value = source[property][j] + if (value) { + obj[property][j] = value + } + } + } else { + obj[property] = source[property] + } + } + } + return obj } diff --git a/src/world.js b/src/world.js index 13e0b1e..c72fcb0 100644 --- a/src/world.js +++ b/src/world.js @@ -1,96 +1,101 @@ -/** - * Creates new world and start frame loop - * @constructor - */ -function World() { - EventEmitter.call(this) - this.items = [] - this.frame = null - this.run() -} +import { EventEmitter } from "./eventemitter.js" +import { Item } from "./item.js" +import { Particle } from "./physics/particle.js" +import { fixTick } from "./utils.js" -World.prototype = Object.create(EventEmitter.prototype) -World.prototype.constructor = World +export class World extends EventEmitter { + /** + * Creates new world and start frame loop + * @constructor + */ + constructor() { + super() + this.items = [] + this.frame = null + this.run() + } -/** - * Starts new frame loop - */ -World.prototype.run = function () { - var self = this + /** + * Starts new frame loop + */ + run() { + const self = this - this.frame = requestAnimationFrame(update) + this.frame = requestAnimationFrame(update) - function update(tick) { - if (fixTick) { - tick = performance.now() - } - self.update(tick) - self.frame = requestAnimationFrame(update) - } -} + function update(tick) { + if (fixTick) { + tick = performance.now() + } + self.update(tick) + self.frame = requestAnimationFrame(update) + } + } -/** - * Update the World on frame - * @param {number} tick - */ -World.prototype.update = function (tick) { - for (var i = 0; i < this.items.length; ++i) { - this.items[i].update(tick) - } -} + /** + * Update the World on frame + * @param {number} tick + */ + update(tick) { + for (let i = 0; i < this.items.length; ++i) { + this.items[i].update(tick) + } + } -/** - * Adds node to the animated world - * @param {HTMLElement} node - * @param {number=} mass - * @param {number=} viscosity - * @return {Item} - */ -World.prototype.add = function (node, mass, viscosity, edge) { - var item - if (mass) { - item = new Particle(node, mass, viscosity, edge) - } else { - item = new Item(node) - } - this.items.push(item) - return item -} + /** + * Adds node to the animated world + * @param {HTMLElement} node + * @param {number=} mass + * @param {number=} viscosity + * @param {any=} edge + * @return {Item | Particle} + */ + add(node, mass, viscosity, edge) { + let item + if (mass) { + item = new Particle(node, mass, viscosity, edge) + } else { + item = new Item(node) + } + this.items.push(item) + return item + } -/** - * Cancels next frame - */ -World.prototype.cancel = function () { - this.frame && cancelAnimationFrame(this.frame) - this.frame = 0 -} + /** + * Cancels next frame + */ + cancel() { + this.frame && cancelAnimationFrame(this.frame) + this.frame = 0 + } -/** - * Stops the World - */ -World.prototype.stop = function () { - this.cancel() - for (var i = 0; i < this.items.length; ++i) { - this.items[i].stop() - } -} + /** + * Stops the World + */ + stop() { + this.cancel() + for (let i = 0; i < this.items.length; ++i) { + this.items[i].stop() + } + } -/** - * Pauses all animations - */ -World.prototype.pause = function () { - this.cancel() - for (var i = 0; i < this.items.length; ++i) { - this.items[i].pause() - } -} + /** + * Pauses all animations + */ + pause() { + this.cancel() + for (let i = 0; i < this.items.length; ++i) { + this.items[i].pause() + } + } -/** - * Resumes all animations - */ -World.prototype.resume = function () { - for (var i = 0; i < this.items.length; ++i) { - this.items[i].resume() - } - this.run() + /** + * Resumes all animations + */ + resume() { + for (let i = 0; i < this.items.length; ++i) { + this.items[i].resume() + } + this.run() + } }