diff --git a/src/core/PaperScript.js b/src/core/PaperScript.js index fbe88800..e64ef2a7 100644 --- a/src/core/PaperScript.js +++ b/src/core/PaperScript.js @@ -657,7 +657,9 @@ Base.exports.PaperScript = function() { compile: compile, execute: execute, load: load, - parse: parse + parse: parse, + calculateBinary: __$__, + calculateUnary: $__ }; // Pass on `this` as the binding object, so we can reference Acorn both in // development and in the built library. diff --git a/src/item/Item.js b/src/item/Item.js index d3a8d587..e9e4b69d 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -4667,4 +4667,157 @@ new function() { // Injection scope for hit-test functions shared with project } return this; } -})); +}), /** @lends Item# */{ + /** + * {@grouptitle Tweening Functions} + * + * Tween item between two states + * + * @name Item#tween + * + * @option options.duration {Number} the duration of the tweening + * @option options.easing {Function|String} an easing function or the type + * of the easing: {@values 'linear' 'easeInQuad' 'easeOutQuad' + * 'easeInOutQuad' 'easeInCubic' 'easeOutCubic' 'easeInOutCubic' + * 'easeInQuart' 'easeOutQuart' 'easeInOutQuart' 'easeInQuint' + * 'easeOutQuint' 'easeInOutQuint'} + * @option options.start {Boolean} Whether to start tweening automatically + * + * @function + * @param {Object} from the state at the start of the tweening + * @param {Object} to the state at the end of the tweening + * @param {Object|Number} options the options or the duration + * @return {Tween} + * + * @example {@paperscript height=100} + * // Tween fillColor: + * var path = new Path.Circle({ + * radius: view.bounds.height * 0.4, + * center: view.center + * }) + * path.tween({ fillColor: 'blue' }, { fillColor: 'red' }, 3000) + */ + /** + * Tween item to a state + * + * @name Item#tween + * + * @function + * @param {Object} to the state at the end of the tweening + * @param {Object|Number} options or duration + * @return {Tween} + * + * @example {@paperscript height=200} + * // Tween a nested property with relative values + * var path = new Path.Rectangle({ + * size: [100, 100], + * position: view.center, + * fillColor: 'red', + * }) + * + * var delta = { x: path.bounds.width / 2, y: 0 } + * + * path.tweenTo({ + * 'segments[1].point': ['+=', delta], + * 'segments[2].point.x': '-= 50' + * }, 3000) + * + * @see Item#tween(from, to, options) + */ + /** + * Tween item + * + * @name Item#tween + * + * @function + * @param {Object|Number} options options or duration + * @return {Tween} + * + * @see Item#tween(from, to, options) + * + * @example {@paperscript height=100} + * // Start an empty tween and just use the update callback: + * var path = new Path.Circle({ + * fillColor: 'blue', + * radius: view.bounds.height * 0.4, + * center: view.center, + * }) + * var pathFrom = path.clone({ insert: false }) + * var pathTo = new Path.Rectangle({ + * position: view.center, + * rectangle: path.bounds, + * insert: false + * }) + * path.tween(2000).on('update', function(event) { + * path.interpolate(pathFrom, pathTo, event.factor) + * }) + */ + tween: function(from, to, options) { + if (!options) { + // If there are only two or one arguments, shift arguments to the + // left by one (omit `from`): + options = to; + to = from; + from = null; + if (!options) { + options = to; + to = null; + } + } + var easing = options && options.easing, + start = options && options.start, + duration = options != null && ( + typeof options === 'number' ? options : options.duration + ), + tween = new Tween(this, from, to, duration, easing, start); + function onFrame(event) { + tween.handleFrame(event.time * 1000); + if (!tween.running) { + this.off('frame', onFrame); + } + } + if (duration) { + this.on('frame', onFrame); + } + return tween; + }, + + /** + * + * Tween item to a state + * + * @function + * @param {Object} state the state at the end of the tweening + * @param {Object|Number} options the options or the duration + * @return {Tween} + * + * @see Item#tween(to, options) + */ + tweenTo: function(state, options) { + return this.tween(null, state, options); + }, + + /** + * + * Tween from a state to it's state before the tweening + * + * @function + * @param {Object} state the state at the end of the tweening + * @param {Object|Number} options the options or the duration + * @return {Tween} + * + * @see Item#tween(from, to, options) + * + * @example {@paperscript height=100} + * // Tween fillColor from red to the path's initial fillColor: + * var path = new Path.Circle({ + * fillColor: 'blue', + * radius: view.bounds.height * 0.4, + * center: view.center + * }) + * path.tweenFrom({ fillColor: 'blue' }, { duration: 1000 }) + */ + tweenFrom: function(state, options) { + return this.tween(state, null, options); + } +}); diff --git a/src/item/Tween.js b/src/item/Tween.js new file mode 100644 index 00000000..63d9cbf8 --- /dev/null +++ b/src/item/Tween.js @@ -0,0 +1,301 @@ +/* + * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. + * http://paperjs.org/ + * + * Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey + * http://scratchdisk.com/ & https://puckey.studio/ + * + * Distributed under the MIT license. See LICENSE file for details. + * + * All rights reserved. + */ + +/** + * @name Tween + * + * @class Allows to tween properties of an Item between two values + */ +var Tween = Base.extend(Emitter, /** @lends Tween# */{ + _class: 'Tween', + + statics: { + easings: { + // no easing, no acceleration + linear: function(t) { + return t; + }, + + // accelerating from zero velocity + easeInQuad: function(t) { + return t * t; + }, + + // decelerating to zero velocity + easeOutQuad: function(t) { + return t * (2 - t); + }, + + // acceleration until halfway, then deceleration + easeInOutQuad: function(t) { + return t < 0.5 + ? 2 * t * t + : -1 + 2 * (2 - t) * t; + }, + + // accelerating from zero velocity + easeInCubic: function(t) { + return t * t * t; + }, + + // decelerating to zero velocity + easeOutCubic: function(t) { + return --t * t * t + 1; + }, + + // acceleration until halfway, then deceleration + easeInOutCubic: function(t) { + return t < 0.5 + ? 4 * t * t * t + : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; + }, + + // accelerating from zero velocity + easeInQuart: function(t) { + return t * t * t * t; + }, + + // decelerating to zero velocity + easeOutQuart: function(t) { + return 1 - (--t) * t * t * t; + }, + + // acceleration until halfway, then deceleration + easeInOutQuart: function(t) { + return t < 0.5 + ? 8 * t * t * t * t + : 1 - 8 * (--t) * t * t * t; + }, + + // accelerating from zero velocity + easeInQuint: function(t) { + return t * t * t * t * t; + }, + + // decelerating to zero velocity + easeOutQuint: function(t) { + return 1 + --t * t * t * t * t; + }, + + // acceleration until halfway, then deceleration + easeInOutQuint: function(t) { + return t < 0.5 + ? 16 * t * t * t * t * t + : 1 + 16 * (--t) * t * t * t * t; + } + } + }, + + /** + * {@grouptitle Event Handling} + * + * Attaches an event handler to the tween. + * + * @name Tween#on + * @function + * @param {String} type the type of event (currently only 'update') + * @param {Function} function the function to be called when the event + * occurs, receiving an object as its + * sole argument, containing the current progress of the + * tweening and the factor calculated by the easing function + * @return {Tween} this tween itself, so calls can be chained + */ + /** + * Creates a new tween + * + * @name Path#initialize + * @param {Item} item The Item to be tweened + * @param {Object} from State at the start of the tweening + * @param {Object} to State at the end of the tweening + * @param {Number} duration Duration of the tweening + * @param {String|Function} easing Type of the easing function or the easing + * function + * @param {Boolean} start Whether to start tweening automatically + * @return {Tween} the newly created tween + */ + initialize: function Tween(item, from, to, duration, easing, start) { + this.item = item; + var type = typeof easing + var isFunction = type === 'function'; + this.type = isFunction + ? type + : type === 'string' + ? easing + : 'linear'; + this.easing = isFunction ? easing : Tween.easings[this.type]; + this.duration = duration; + this.running = false; + + this._then = null; + this._startTime = null; + var state = from || to; + this._keys = state ? Object.keys(state) : []; + this._parsedKeys = this._parseKeys(this._keys); + this._from = state && this._getState(from); + this._to = state && this._getState(to); + if (start !== false) { + this.start(); + } + }, + + handleFrame: function(time) { + var startTime = this._startTime, + progress = startTime + ? (time - startTime) / this.duration + : 0; + if (!startTime) { + this._startTime = time; + } + this.update(progress); + }, + + then: function(then) { + this._then = then; + return this; + }, + + start: function() { + this._startTime = null; + this.running = true; + return this; + }, + + stop: function() { + this.running = false; + return this; + }, + + update: function(progress) { + if (this.running) { + if (progress > 1) { + // always finish the animation + progress = 1; + this.running = false; + } + + var factor = this.easing(progress), + keys = this._keys, + getValue = function(value) { + return typeof value === 'function' + ? value(factor, progress) + : value; + }; + for (var i = 0, l = keys && keys.length; i < l; i++) { + var key = keys[i], + from = getValue(this._from[key]), + to = getValue(this._to[key]), + // Some paper objects have math functions (e.g.: Point, + // Color) which can directly be used to do the tweening. + value = (from && to && from.__add && to.__add) + ? to.__subtract(from).__multiply(factor).__add(from) + : ((to - from) * factor) + from; + this._setItemProperty(this._parsedKeys[key], value); + } + + if (!this.running && this._then) { + this._then(this.item); + } + if (this.responds('update')) { + this.emit('update', new Base({ + progress: progress, + factor: factor + })); + } + } + return this; + }, + + _getState: function(state) { + var keys = this._keys, + result = {}; + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i], + path = this._parsedKeys[key], + current = this._getItemProperty(path), + value; + if (state) { + var resolved = this._resolveValue(current, state[key]); + // Temporarily set the resolved value, so we can retrieve the + // coerced value from paper's internal magic. + this._setItemProperty(path, resolved); + value = this._getItemProperty(path); + // Clone the value if possible to prevent future changes. + value = value.clone ? value.clone() : value + this._setItemProperty(path, current); + } else { + // We want to get the current state at the time of the call, so + // we have to clone if possible to prevent future changes. + value = current.clone ? current.clone() : current; + } + result[key] = value; + } + return result; + }, + + _resolveValue: function(current, value) { + if (value) { + if (Array.isArray(value) && value.length === 2) { + var operator = value[0]; + return ( + operator && + operator.match && + operator.match(/^[+\-*/]=/) + ) + ? this._calculate(current, operator[0], value[1]) + : value; + } else if (typeof value === 'string') { + var match = value.match(/^[+\-*/]=(.*)/); + if (match) { + var parsed = JSON.parse(match[1].replace( + /(['"])?([a-zA-Z0-9_]+)(['"])?:/g, + '"$2": ' + )); + return this._calculate(current, value[0], parsed); + } + } + } + return value; + }, + + _calculate: function(left, operator, right) { + return paper.PaperScript.calculateBinary(left, operator, right); + }, + + _parseKeys: function(keys) { + var parsed = {}; + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i], + path = key + // Convert from JS property access notation to JSON pointer: + .replace(/\.([^.]*)/g, '/$1') + // Expand array property access notation ([]) + .replace(/\[['"]?([^'"\]]*)['"]?\]/g, '/$1'); + parsed[key] = path.split('/'); + } + return parsed; + }, + + _getItemProperty: function(path, offset) { + var obj = this.item; + for (var i = 0, l = path.length - (offset || 0); i < l && obj; i++) { + obj = obj[path[i]]; + } + return obj; + }, + + _setItemProperty: function(path, value) { + var dest = this._getItemProperty(path, 1); + if (dest) { + dest[path[path.length - 1]] = value; + } + } +}); diff --git a/src/paper.js b/src/paper.js index 0fcf9ff1..1f5c153f 100644 --- a/src/paper.js +++ b/src/paper.js @@ -65,6 +65,7 @@ var paper = function(self, undefined) { /*#*/ include('item/SymbolItem.js'); /*#*/ include('item/SymbolDefinition.js'); /*#*/ include('item/HitResult.js'); +/*#*/ include('item/Tween.js'); /*#*/ include('path/Segment.js'); /*#*/ include('path/SegmentPoint.js');