mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-01 02:38:43 -05:00
Implement tweening
This commit is contained in:
parent
d46b6cbef4
commit
684f504930
4 changed files with 459 additions and 2 deletions
|
@ -657,7 +657,9 @@ Base.exports.PaperScript = function() {
|
||||||
compile: compile,
|
compile: compile,
|
||||||
execute: execute,
|
execute: execute,
|
||||||
load: load,
|
load: load,
|
||||||
parse: parse
|
parse: parse,
|
||||||
|
calculateBinary: __$__,
|
||||||
|
calculateUnary: $__
|
||||||
};
|
};
|
||||||
// Pass on `this` as the binding object, so we can reference Acorn both in
|
// Pass on `this` as the binding object, so we can reference Acorn both in
|
||||||
// development and in the built library.
|
// development and in the built library.
|
||||||
|
|
155
src/item/Item.js
155
src/item/Item.js
|
@ -4667,4 +4667,157 @@ new function() { // Injection scope for hit-test functions shared with project
|
||||||
}
|
}
|
||||||
return this;
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
301
src/item/Tween.js
Normal file
301
src/item/Tween.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -65,6 +65,7 @@ var paper = function(self, undefined) {
|
||||||
/*#*/ include('item/SymbolItem.js');
|
/*#*/ include('item/SymbolItem.js');
|
||||||
/*#*/ include('item/SymbolDefinition.js');
|
/*#*/ include('item/SymbolDefinition.js');
|
||||||
/*#*/ include('item/HitResult.js');
|
/*#*/ include('item/HitResult.js');
|
||||||
|
/*#*/ include('item/Tween.js');
|
||||||
|
|
||||||
/*#*/ include('path/Segment.js');
|
/*#*/ include('path/Segment.js');
|
||||||
/*#*/ include('path/SegmentPoint.js');
|
/*#*/ include('path/SegmentPoint.js');
|
||||||
|
|
Loading…
Reference in a new issue