Implement tweening

This commit is contained in:
arnoson 2018-11-28 20:26:25 +01:00 committed by Jürg Lehni
parent d46b6cbef4
commit 684f504930
4 changed files with 459 additions and 2 deletions

View file

@ -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.

View file

@ -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);
}
});

301
src/item/Tween.js Normal file
View 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;
}
}
});

View file

@ -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');