From 213124539c2dd5196616099baad322a43fb85b9f Mon Sep 17 00:00:00 2001 From: Yehor Lvivski Date: Sun, 30 Jul 2023 19:17:18 -0700 Subject: [PATCH 1/2] esm --- jsconfig.json | 7 + src/animations/animation.js | 326 +++++++++++++------------ src/animations/collection.js | 242 ++++++++++--------- src/animations/css_animation.js | 135 ++++++----- src/animations/easings.js | 8 +- src/animations/parallel.js | 162 ++++++------- src/animations/sequence.js | 223 ++++++++--------- src/animations/tween.js | 181 +++++++------- src/core.js | 30 +-- src/css.js | 306 ++++++++++++------------ src/eventemitter.js | 122 +++++----- src/item.js | 399 ++++++++++++++++--------------- src/math/matrix.js | 184 +++++++------- src/math/vector.js | 36 +-- src/physics/forces/attraction.js | 15 +- src/physics/forces/constant.js | 10 +- src/physics/forces/edge.js | 23 +- src/physics/particle.js | 211 ++++++++-------- src/physics/verlet.js | 12 +- src/timeline.js | 147 ++++++------ src/utils.js | 38 +-- src/world.js | 175 +++++++------- 22 files changed, 1523 insertions(+), 1469 deletions(-) create mode 100644 jsconfig.json diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..9f9a9a2 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "ES2020", + "moduleResolution": "nodenext" + } +} diff --git a/src/animations/animation.js b/src/animations/animation.js index 1c93fc5..c721896 100644 --- a/src/animations/animation.js +++ b/src/animations/animation.js @@ -1,160 +1,170 @@ -/** - * 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)) - } -} +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) + } -/** - * 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) } diff --git a/src/animations/collection.js b/src/animations/collection.js index 3bad9d1..cd75546 100644 --- a/src/animations/collection.js +++ b/src/animations/collection.js @@ -1,119 +1,125 @@ -/** - * 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..c6df9da 100644 --- a/src/animations/css_animation.js +++ b/src/animations/css_animation.js @@ -1,77 +1,84 @@ -/** - * 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..88e18cf 100644 --- a/src/animations/easings.js +++ b/src/animations/easings.js @@ -3,8 +3,8 @@ * used for Animations * @type {Object} */ -var easings = (function () { - var fn = { +export const easings = (function () { + const fn = { quad: function (p) { return Math.pow(p, 2) }, @@ -31,14 +31,14 @@ var easings = (function () { } } - var easings = { + const easings = { linear: function (p) { return p } } Object.keys(fn).forEach(function (name) { - var ease = fn[name] + const ease = fn[name] easings['ease-in-' + name] = ease easings['ease-out-' + name] = function (p) { return 1 - ease(1 - p) diff --git a/src/animations/parallel.js b/src/animations/parallel.js index b4bb83b..83ecf7d 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..0760981 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..b2be99a 100644 --- a/src/animations/tween.js +++ b/src/animations/tween.js @@ -1,109 +1,114 @@ -function Tween(start, end, property) { - var 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' : '' -} +export class Tween { + constructor(start, end, property) { + const type = Tween.propTypes[property] || Tween.NUMERIC + this.type = type -Tween.NUMERIC = 'NUMERIC' -Tween.COLOR = 'COLOR' + this.start = Tween.parseValue(start, type) + this.end = Tween.parseValue(end, type) -Tween.propTypes = { - color: Tween.COLOR, - backgroundColor: Tween.COLOR, - borderColor: Tween.COLOR -} + this.suffix = Tween.px.indexOf(property) !== -1 ? 'px' : '' + } + + static NUMERIC = 'NUMERIC' + static COLOR = 'COLOR' -Tween.px = '\ + static propTypes = { + color: Tween.COLOR, + backgroundColor: Tween.COLOR, + borderColor: Tween.COLOR + } + + 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(',') -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 - } - } - - 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) - } - } -} + 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 + } + } -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) - } -} + 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.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 -} + 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.absolute = function (percent) { - var value = Number(this.start) + (Number(this.end) - Number(this.start)) * percent - if (this.suffix) { - value += 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 + } + + absolute(percent) { + let value = Number(this.start) + (Number(this.end) - Number(this.start)) * percent + if (this.suffix) { + 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..71dd41a 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 = {} - -/** +export default { + /** * Creates and initializes world with frame loop * @return {World} */ -a.world = function () { - return new World -} - -/** + world() { + 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 + timeline() { + return new Timeline + } } diff --git a/src/css.js b/src/css.js index b9e188c..88df0ad 100644 --- a/src/css.js +++ b/src/css.js @@ -1,150 +1,158 @@ -/** - * 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 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 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)) + } + } + } + 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..d407ad1 100644 --- a/src/eventemitter.js +++ b/src/eventemitter.js @@ -1,64 +1,62 @@ -/** - * 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 -} +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] || [] + } -/** - * List all event listeners - * @param {string} event - * @returns {Array} - */ -EventEmitter.prototype.listeners = function (event) { - return this.handlers[event] || [] } diff --git a/src/item.js b/src/item.js index a1d6cd4..a9a6ffc 100644 --- a/src/item.js +++ b/src/item.js @@ -1,199 +1,202 @@ -/** - * 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' + +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) { + 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 + */ + 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..9e615f0 100644 --- a/src/physics/forces/attraction.js +++ b/src/physics/forces/attraction.js @@ -1,19 +1,20 @@ +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)) - 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..8da9384 100644 --- a/src/physics/forces/edge.js +++ b/src/physics/forces/edge.js @@ -1,20 +1,21 @@ +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]) { +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) { - this.previous.position[i] = 2 * this.current.position[i] - this.previous.position[i] + item.previous.position[i] = 2 * item.current.position[i] - item.previous.position[i] } else { - this.current.position[i] = Math.max(min[i], Math.min(max[i], this.current.position[i])) + 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..b4324fa 100644 --- a/src/physics/particle.js +++ b/src/physics/particle.js @@ -1,102 +1,111 @@ -/** - * 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) + } + } + + 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..7af3af6 100644 --- a/src/physics/verlet.js +++ b/src/physics/verlet.js @@ -1,16 +1,20 @@ +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) { +export function Verlet(self, delta, drag) { // velocity = position - old_position // position = position + (velocity + acceleration * delta * delta) - var current = this.current, - previous = this.previous + const current = self.current + const previous = self.previous - current.acceleration = Vector.scale(current.acceleration, this.mass) + current.acceleration = Vector.scale(current.acceleration, self.mass) current.velocity = Vector.sub(current.position, previous.position) if (drag !== undefined) { diff --git a/src/timeline.js b/src/timeline.js index 53867fc..a3d6f3b 100644 --- a/src/timeline.js +++ b/src/timeline.js @@ -1,82 +1,85 @@ -/** - * 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 { -/** - * Starts new frame loop - */ -Timeline.prototype.run = function () { - this.frame = requestAnimationFrame(update) + /** + * Creates new Timeline and start frame loop + * @constructor + */ + constructor() { + super() + this.currentTime = 0 + this.start = 0 + } - var self = this + /** + * Starts new frame loop + */ + run() { + this.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) - } -} + const self = this -/** - * 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() - } - } -} + function update(tick) { + if (fixTick) { + tick = performance.now() + } + if (self.running) { + self.currentTime = tick - self.start + } + self.update(self.currentTime) + self.frame = requestAnimationFrame(update) + } + } -/** - * Plays/Resumes Timeline - */ -Timeline.prototype.play = function () { - this.running = true - this.start = performance.now() - this.currentTime -} + /** + * 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() + } + } + } -/** - * Pauses Timeline - */ -Timeline.prototype.pause = function () { - this.running = false -} + /** + * Plays/Resumes Timeline + */ + play() { + this.running = true + this.start = performance.now() - this.currentTime + } -/** - * Stops Timeline - */ -Timeline.prototype.stop = function () { - this.currentTime = 0 - this.running = false -} + /** + * Pauses Timeline + */ + pause() { + 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..64c63ed 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,40 +2,28 @@ * 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 }) -function merge(obj) { - var i = 1 +export function merge(obj) { + let i = 1 while (i < arguments.length) { - var source = arguments[i++] - for (var property in source) { + const source = arguments[i++] + for (const property in source) { if (Array.isArray(source[property])) { - for (var j = 0; j < source[property].length; ++j) { - var value = source[property][j] + for (let j = 0; j < source[property].length; ++j) { + const value = source[property][j] if (value) { obj[property][j] = value } 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() + } } From 0c579acf9906085c0e246c7a0e5fa4fcd98a3fa7 Mon Sep 17 00:00:00 2001 From: Yehor Lvivski Date: Wed, 1 Jan 2025 22:23:28 -0800 Subject: [PATCH 2/2] format --- jsconfig.json | 2 +- package.json | 7 +- src/animations/animation.js | 31 +++---- src/animations/collection.js | 29 ++++--- src/animations/css_animation.js | 30 +++++-- src/animations/easings.js | 136 +++++++++++++++---------------- src/animations/parallel.js | 12 +-- src/animations/sequence.js | 10 +-- src/animations/tween.js | 41 ++++++---- src/core.js | 16 ++-- src/css.js | 80 +++++++++++------- src/eventemitter.js | 35 ++++---- src/item.js | 35 ++++---- src/physics/forces/attraction.js | 14 +++- src/physics/forces/edge.js | 30 +++++-- src/physics/particle.js | 33 +++++--- src/physics/verlet.js | 24 ++++-- src/timeline.js | 3 +- src/utils.js | 38 +++++---- 19 files changed, 347 insertions(+), 259 deletions(-) diff --git a/jsconfig.json b/jsconfig.json index 9f9a9a2..78bf304 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "checkJs": true, - "module": "ES2020", + "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 c721896..246868e 100644 --- a/src/animations/animation.js +++ b/src/animations/animation.js @@ -1,9 +1,9 @@ -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' +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 { /** @@ -27,7 +27,7 @@ export class Animation { this.delay = (transform.delay || delay) | 0 ease = transform.ease || ease this.ease = easings[ease] || easings.linear - this.easeName = transform.ease || ease || 'linear' + this.easeName = transform.ease || ease || "linear" } static skip = { duration: null, delay: null, ease: null } @@ -46,7 +46,11 @@ export class Animation { } Animation.setItemState(item, property, computed) } - initial[property] = new Tween(item.get(property), transform[property], property) + initial[property] = new Tween( + item.get(property), + transform[property], + property + ) } } return initial @@ -55,7 +59,7 @@ export class Animation { static setItemState = function (item, property, computed) { if (property in Animation.transform) { let value = computed[transformProperty] - if (value === 'none') { + if (value === "none") { value = { translate: Vector.zero(), rotate: Vector.zero(), @@ -64,9 +68,9 @@ export class Animation { } else { value = Matrix.decompose(Matrix.parse(value)) } - item.set('translate', value.translate) - item.set('rotate', value.rotate) - item.set('scale', value.scale) + item.set("translate", value.translate) + item.set("rotate", value.rotate) + item.set("scale", value.scale) } else { item.set(property, computed[property]) } @@ -97,7 +101,7 @@ export class Animation { this.delay = (transform.delay || delay) | 0 ease = transform.ease || ease this.ease = easings[ease] || easings.linear - this.easeName = transform.ease || ease || 'linear' + this.easeName = transform.ease || ease || "linear" merge(this.transformation, transform) @@ -166,5 +170,4 @@ export class Animation { !abort && this.transform(this.ease(1)) !seek && (this.start = null) } - } diff --git a/src/animations/collection.js b/src/animations/collection.js index cd75546..462cee2 100644 --- a/src/animations/collection.js +++ b/src/animations/collection.js @@ -1,11 +1,11 @@ -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' +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 { /** @@ -21,7 +21,7 @@ export class Collection extends EventEmitter { this.delay = 0 this.duration = 0 this.ease = easings.linear - this.easeName = 'linear' + this.easeName = "linear" this.animations = [] } @@ -36,8 +36,15 @@ export class Collection extends EventEmitter { 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 (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) } diff --git a/src/animations/css_animation.js b/src/animations/css_animation.js index c6df9da..80d0f8b 100644 --- a/src/animations/css_animation.js +++ b/src/animations/css_animation.js @@ -24,7 +24,8 @@ export class CssAnimation { 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.ease = + easings.css[animation.ease] || easings.css[ease] || easings.css.linear this._infinite = false this._generated = generated @@ -39,22 +40,33 @@ export class CssAnimation { if (this.start !== null && !force) return this.start = tick + this.delay - this.item.style(animationProperty, - this.name + ' ' + this.duration + 'ms' + ' ' + this.ease + ' ' + - this.delay + 'ms' + (this._infinite ? ' infinite' : '') + ' ' + 'forwards') + this.item.style( + animationProperty, + this.name + + " " + + this.duration + + "ms" + + " " + + this.ease + + " " + + this.delay + + "ms" + + (this._infinite ? " infinite" : "") + + " " + + "forwards" + ) } /** * Runs one tick of animation */ - run() { - } + run() {} /** * Pauses animation */ pause() { - this.item.style(animationProperty + '-play-state', 'paused') + this.item.style(animationProperty + "-play-state", "paused") this.diff = performance.now() - this.start } @@ -62,7 +74,7 @@ export class CssAnimation { * Resumes animation */ resume() { - this.item.style(animationProperty + '-play-state', 'running') + this.item.style(animationProperty + "-play-state", "running") this.start = performance.now() - this.diff } @@ -74,7 +86,7 @@ export class CssAnimation { const computed = getComputedStyle(this.item.dom, null) const transform = computed[transformProperty] - this.item.style(animationProperty, '') + this.item.style(animationProperty, "") this.item.state = Matrix.decompose(Matrix.parse(transform)) this.item.style() } diff --git a/src/animations/easings.js b/src/animations/easings.js index 88e18cf..4fa90b8 100644 --- a/src/animations/easings.js +++ b/src/animations/easings.js @@ -5,78 +5,76 @@ */ 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) - } - } + 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) + } + } const easings = { - linear: function (p) { - return p - } - } + linear: function (p) { + return p + } + } - Object.keys(fn).forEach(function (name) { + 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["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 83ecf7d..c91f902 100644 --- a/src/animations/parallel.js +++ b/src/animations/parallel.js @@ -33,8 +33,8 @@ export class Parallel extends Collection { init(tick, force) { if (this.start !== null && !force) return this.start = tick - this.all('init', tick, force) - this.emit('start') + this.all("init", tick, force) + this.emit("start") } /** @@ -72,14 +72,14 @@ export class Parallel extends Collection { * Pauses animations */ pause() { - this.all('pause') + this.all("pause") } /** * Resumes animations */ resume() { - this.all('resume') + this.all("resume") } /** @@ -88,7 +88,7 @@ export class Parallel extends Collection { * @fires Parallel#end */ end(abort = false) { - this.all('end', abort) - this.emit('end') + this.all("end", abort) + this.emit("end") } } diff --git a/src/animations/sequence.js b/src/animations/sequence.js index 0760981..1e86f44 100644 --- a/src/animations/sequence.js +++ b/src/animations/sequence.js @@ -1,6 +1,6 @@ -import { Collection } from './collection' -import { CssAnimation } from './css_animation' -import { Item } from '../item' +import { Collection } from "./collection" +import { CssAnimation } from "./css_animation" +import { Item } from "../item" export class Sequence extends Collection { /** @@ -25,7 +25,7 @@ export class Sequence extends Collection { this.start = tick this.animations[0].init(tick, force) - this.emit('start') + this.emit("start") } /** @@ -122,6 +122,6 @@ export class Sequence extends Collection { } this.animations = [] this._infinite = false - this.emit('end') + this.emit("end") } } diff --git a/src/animations/tween.js b/src/animations/tween.js index b2be99a..790b2b8 100644 --- a/src/animations/tween.js +++ b/src/animations/tween.js @@ -6,11 +6,11 @@ export class Tween { this.start = Tween.parseValue(start, type) this.end = Tween.parseValue(end, type) - this.suffix = Tween.px.indexOf(property) !== -1 ? 'px' : '' + this.suffix = Tween.px.indexOf(property) !== -1 ? "px" : "" } - static NUMERIC = 'NUMERIC' - static COLOR = 'COLOR' + static NUMERIC = "NUMERIC" + static COLOR = "COLOR" static propTypes = { color: Tween.COLOR, @@ -18,15 +18,17 @@ export class Tween { borderColor: Tween.COLOR } - static 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(",") static parseValue(value, type) { - return type === Tween.COLOR ? Tween.parseColor(value) : Tween.parseNumeric(value) + return type === Tween.COLOR + ? Tween.parseColor(value) + : Tween.parseNumeric(value) } static parseNumeric(numeric) { @@ -47,7 +49,9 @@ borderRadius,borderWidth'.split(',') } } - const rgb = color.match(/^rgba?\(([0-9.]*), ?([0-9.]*), ?([0-9.]*)(?:, ?([0-9.]*))?\)$/) + const rgb = color.match( + /^rgba?\(([0-9.]*), ?([0-9.]*), ?([0-9.]*)(?:, ?([0-9.]*))?\)$/ + ) if (rgb) { return { r: parseFloat(rgb[1]), @@ -86,9 +90,11 @@ borderRadius,borderWidth'.split(',') } absolute(percent) { - let value = Number(this.start) + (Number(this.end) - Number(this.start)) * percent + /** @type {number | string} */ + let value = + Number(this.start) + (Number(this.end) - Number(this.start)) * percent if (this.suffix) { - value += this.suffix + value = value + this.suffix } return value } @@ -97,18 +103,21 @@ borderRadius,borderWidth'.split(',') 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) + 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) + 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] + ')' + 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 71dd41a..0e73466 100644 --- a/src/core.js +++ b/src/core.js @@ -7,17 +7,17 @@ import { World } from "./world.js" */ export default { /** - * Creates and initializes world with frame loop - * @return {World} - */ + * Creates and initializes world with frame loop + * @return {World} + */ world() { - return new World + return new World() }, /** - * Creates and initializes timeline - * @return {Timeline} - */ + * Creates and initializes timeline + * @return {Timeline} + */ timeline() { - return new Timeline + return new Timeline() } } diff --git a/src/css.js b/src/css.js index 88df0ad..bfae091 100644 --- a/src/css.js +++ b/src/css.js @@ -21,14 +21,14 @@ export class CSS { !idle && this.style() } - static skip = { translate: null, rotate: null, scale: null }; + 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) + const style = document.createElement("style") + document.getElementsByTagName("head")[0].appendChild(style) } /** @@ -54,7 +54,7 @@ export class CSS { const computed = getComputedStyle(this.item.dom, null), transform = computed[transformProperty] - this.item.style(animationProperty, '') + this.item.style(animationProperty, "") this.item.state = Matrix.decompose(Matrix.parse(transform)) this.item.style() @@ -65,13 +65,16 @@ export class CSS { * Applies animations and sets item style */ style() { - const animation = 'a' + Date.now() + 'r' + Math.floor(Math.random() * 1000) + 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.stylesheet.insertRule( + this.keyframes(animation), + cssRules ? cssRules.length : 0 + ) this.animation.empty() - this.animation.add(animation, this.animation.duration, '', 0, true) + this.animation.add(animation, this.animation.duration, "", 0, true) } /** @@ -81,7 +84,7 @@ export class CSS { */ keyframes(name) { let time = 0 - const rule = ['@' + getProperty('keyframes') + ' ' + name + '{'] + const rule = ["@" + getProperty("keyframes") + " " + name + "{"] for (let i = 0; i < this.animation.length; ++i) { const a = this.animation.get(i) @@ -89,20 +92,15 @@ export class CSS { a.init(time) - 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 if (a instanceof Collection) { // Parallel (it doesn't work with custom easings for now) + 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) + a.duration && + frames.indexOf(a.delay + a.duration) === -1 && + frames.push(a.delay + a.duration) }) frames = frames.sort(function (a, b) { @@ -114,17 +112,27 @@ export class CSS { 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 + 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(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('') + rule.push("}") + return rule.join("") } /** @@ -133,7 +141,7 @@ export class CSS { * @return {string} */ percent(time) { - return (time * 100 / this.animation.duration).toFixed(3) + return ((time * 100) / this.animation.duration).toFixed(3) } /** @@ -147,12 +155,24 @@ export class CSS { 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) + ';' : '') + 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 + ';' : '') + - '}' + 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 d407ad1..692adb1 100644 --- a/src/eventemitter.js +++ b/src/eventemitter.js @@ -3,24 +3,24 @@ export class EventEmitter { this.handlers = {} } - /** - * Adds handler for event - * @param {string} event - * @param {Function} handler - * @returns {EventEmitter} - */ + /** + * 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} - */ + /** + * Removes event handler + * @param {string} event + * @param {Function} handler + * @returns {EventEmitter} + */ off(event, handler) { const handlers = this.handlers[event] @@ -33,11 +33,11 @@ export class EventEmitter { return this } - /** - * Triggers event - * @param {string} event - * @returns {EventEmitter} - */ + /** + * Triggers event + * @param {string} event + * @returns {EventEmitter} + */ emit(event, ...args) { const handlers = this.handlers[event] @@ -58,5 +58,4 @@ export class EventEmitter { listeners(event) { return this.handlers[event] || [] } - } diff --git a/src/item.js b/src/item.js index a9a6ffc..918641f 100644 --- a/src/item.js +++ b/src/item.js @@ -4,12 +4,13 @@ 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 - */ + /** + * Creates new animated item + * @param {HTMLElement} node + */ constructor(node) { super() this.dom = node @@ -86,9 +87,7 @@ export class Item extends EventEmitter { */ matrix() { const state = this.state - return Matrix.compose( - state.translate, state.rotate, state.scale - ) + return Matrix.compose(state.translate, state.rotate, state.scale) } /** @@ -104,10 +103,10 @@ export class Item extends EventEmitter { * @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) + const transform = Matrix.decompose( + Matrix.lookAt(vector, this.get("translate"), Vector.set(0, 1, 0)) + ) + this.set("rotate", transform.rotate) } /** @@ -169,16 +168,20 @@ export class Item extends EventEmitter { */ alternate(transform, duration, ease, delay) { if (this.animation.length) { - this.animation.get(0).merge(transform, duration, ease, delay) + 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 - */ + /** + * Finishes all Item animations + * @param {boolean=} abort + */ finish(abort = false) { this.animation.end(abort) return this diff --git a/src/physics/forces/attraction.js b/src/physics/forces/attraction.js index 9e615f0..607b76d 100644 --- a/src/physics/forces/attraction.js +++ b/src/physics/forces/attraction.js @@ -12,9 +12,15 @@ 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) + ) - item.current.acceleration = Vector.add(item.current.acceleration, Vector.scale(force, strength)) - } + item.current.acceleration = Vector.add( + item.current.acceleration, + Vector.scale(force, strength) + ) + } } diff --git a/src/physics/forces/edge.js b/src/physics/forces/edge.js index 8da9384..ee98160 100644 --- a/src/physics/forces/edge.js +++ b/src/physics/forces/edge.js @@ -9,14 +9,26 @@ import { Particle } from '../particle.js' * @param {boolean} bounce * @constructor */ -export function Edge(item, min = Vector.set(0), max = Vector.set(0), bounce = true) { +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])) - } - } - } + 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 b4324fa..0db2bf6 100644 --- a/src/physics/particle.js +++ b/src/physics/particle.js @@ -1,12 +1,11 @@ -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' +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 @@ -18,7 +17,7 @@ export class Particle extends Item { constructor(node, mass, viscosity, edge) { super(node) - if (typeof mass === 'object') { + if (typeof mass === "object") { viscosity = mass.viscosity edge = mass.edge mass = mass.mass @@ -88,14 +87,24 @@ export class Particle extends Item { 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) + 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'); + throw new Error("CSS is nor supported for physics") } /** @@ -104,8 +113,6 @@ export class Particle extends Item { */ matrix() { const state = this.state - return Matrix.compose( - this.current.position, state.rotate, state.scale - ) + return Matrix.compose(this.current.position, state.rotate, state.scale) } } diff --git a/src/physics/verlet.js b/src/physics/verlet.js index 7af3af6..d18c258 100644 --- a/src/physics/verlet.js +++ b/src/physics/verlet.js @@ -9,20 +9,26 @@ import { Particle } from './particle.js' * @constructor */ export function Verlet(self, delta, drag) { - // velocity = position - old_position - // position = position + (velocity + acceleration * delta * delta) + // velocity = position - old_position + // position = position + (velocity + acceleration * delta * delta) const current = self.current const previous = self.previous current.acceleration = Vector.scale(current.acceleration, self.mass) - current.velocity = Vector.sub(current.position, previous.position) + 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 a3d6f3b..2e9f601 100644 --- a/src/timeline.js +++ b/src/timeline.js @@ -2,7 +2,6 @@ import { fixTick } from "./utils.js" import { World } from "./world.js" export class Timeline extends World { - /** * Creates new Timeline and start frame loop * @constructor @@ -44,7 +43,7 @@ export class Timeline extends World { if (this.changed < length || this.running) { item.timeline(tick) this.changed++ - this.emit('update', tick) + this.emit("update", tick) } else { item.style() } diff --git a/src/utils.js b/src/utils.js index 64c63ed..b6a2a9b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,36 +2,38 @@ * Vendor specific stuff */ -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 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 export function getProperty(name) { return name } -requestAnimationFrame(function(tick) { - fixTick = tick > 1e12 != performance.now() > 1e12 +requestAnimationFrame(function (tick) { + fixTick = tick > 1e12 != performance.now() > 1e12 }) export function merge(obj) { let i = 1 - while (i < arguments.length) { + while (i < arguments.length) { const source = arguments[i++] for (const property in source) { - if (Array.isArray(source[property])) { + 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 + if (value) { + obj[property][j] = value + } + } + } else { + obj[property] = source[property] + } + } + } + return obj }