avg-core-ts
Version:
Develop Adventure Game (or ADV, Visual Novel, Galgame, etc.) on your own!
738 lines (593 loc) • 19.1 kB
JavaScript
/**
* @file Tween performance
* @author MicroMatrix <yangpan.1985@bytedance.com>
* @copyright 2012-2020 bytedance
* @link git@code.byted.org:yangpan.1985/avg.git
*/
import core from 'core/core';
import Logger from 'core/logger';
import Color from '../../utils/color.js';
const logger = Logger.create('Tween');
class Ticker {
constructor() {
const ticker = core.ticker;
this._update = () => {
this.update(ticker.elapsedTime);
};
}
start() {
core.getTicker().add(this._update);
}
stop() {
core.getTicker().remove(this._update);
}
update() {
}
}
class AbstractAction extends Ticker {
constructor(target, params, duration, easing, repeat, yoyo) {
super();
this.target = target;
this.params = params;
this.duration = duration;
this.easing = easing;
this.repeat = repeat;
this.yoyo = yoyo;
this.reset();
}
reset() {
this.finished = false;
this.times = 0;
this.lastProgress = 0;
this.progress = 0;
this.lastTime = 0;
// positive: 1, negative: -1
this.direction = 1;
this._direction = 1;
}
reverse() {
this.finished = false;
this._direction = -this._direction;
this.lastTime = 0;
}
update(time, autoFinish = true) {
if (this.finished) {
return true;
}
if (!this.lastTime) {
this.lastTime = time;
return false;
}
const _direction = this._direction;
const delta = time - this.lastTime;
const deltaProgress = delta / this.duration;
this.lastTime = time;
this.lastProgress = this.progress;
this.progress += deltaProgress * this.direction * _direction;
// avoid Infinity caused by small duration value.
this.progress = isFinite(this.progress) ? this.progress : Math.sign(this.progress);
if ((this.progress <= 0 || this.progress >= 1)
&& ((this.times === this.repeat && _direction === 1) || (this.times === 0 && _direction === -1))) {
autoFinish && (this.finished = true);
this.progress = Math.max(0, Math.min(this.progress, 1));
} else if ((this.progress <= 0 || this.progress >= 1)) {
this.times += Number(_direction);
if (this.yoyo) {
// change direction
this.direction = -this.direction;
this.progress = Math.max(0, Math.min(this.progress, 1)) + ((this.progress - (this.progress << 0)) * this.direction);
} else {
this.progress = 0 + (_direction === -1 ? 1 : 0);
}
}
this.updateTransform(this.easing(this.progress), this.easing(this.lastProgress), this.target, this.params);
return this.finished;
}
/* eslint-disable */
updateTransform(progress, lastProgress, target, params) {
}
/* eslint-enable */
}
class Sequence extends Ticker {
constructor(actions, repeat, yoyo) {
super();
this.actions = actions;
this.repeat = repeat;
this.yoyo = yoyo;
this.reset();
}
reset() {
this.times = 0;
this.finished = false;
this.position = 0;
// positive: 1, negative: -1
this.direction = 1;
this._direction = 1;
for (const action of this.actions) {
action.reset();
}
}
reverse() {
this.finished = false;
this._direction = -this._direction;
for (const action of this.actions) {
action.reverse();
}
}
update(time) {
// console.log(time)
if (this.finished) {
return true;
}
const action = this.actions[this.position];
const finished = action.update(time);
const _direction = this._direction;
if (finished) {
this.position += this.direction * _direction;
const length = this.actions.length;
if ((this.position < 0 || this.position >= length)
&& ((this.times === this.repeat && _direction === 1) || (this.times === 0 && _direction === -1))) {
this.finished = true;
return true;
} else if (this.position < 0 || this.position >= length) {
this.times += Number(_direction);
if (this.yoyo) {
// change direction
this.direction = -this.direction;
const maxPosition = length - 1;
this.position = (((this.position / maxPosition) << 0) * length) + ((this.position % maxPosition) * this.direction);
for (const action of this.actions) {
action.reverse();
}
} else {
this.position = 0 + (_direction === -1 ? length - 1 : 0);
for (const action of this.actions) {
action.reset();
}
}
}
}
return false;
}
}
class Parallel extends Ticker {
constructor(actions, repeat, yoyo) {
super();
this.actions = actions;
this.repeat = repeat;
this.yoyo = yoyo;
this.duration = this.actions.reduce((a, b) => Math.max(a, b.duration * (b.repeat + 1)), 0);
this.reset();
}
reset() {
this.times = 0;
this.finished = false;
this.startTime = 0;
// positive: 1, negative: -1
this.direction = 1;
this._direction = 1;
for (const action of this.actions) {
action.reset();
}
}
reverse() {
this.finished = false;
this._direction = -this._direction;
this.startTime = 0;
for (const action of this.actions) {
action.reverse();
}
}
update(time) {
if (this.finished) {
return true;
}
if (!this.startTime) {
this.startTime = time;
return false;
}
const _direction = this._direction;
const direction = _direction * this.direction;
// records of delays's accuracy: ± 1/fps ms
let totalFinished = true;
for (const action of this.actions) {
let finished = false;
if (direction > 0) {
finished = action.update(time);
} else {
const passed = time - this.startTime;
if (this.duration - passed <= action.duration * action.repeat) {
finished = action.update(time);
}
}
totalFinished = totalFinished && finished;
}
if (totalFinished && ((this.times === this.repeat && _direction === 1) || (this.times === 0 && _direction === -1))) {
this.finished = true;
return true;
} else if (totalFinished) {
this.times += Number(_direction);
if (this.yoyo) {
// change direction
this.direction = -this.direction;
for (const action of this.actions) {
action.reverse();
}
} else {
this.position = 0;
for (const action of this.actions) {
action.reset();
}
}
}
return false;
}
}
class MoveToAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
if (!this.initialled) {
this.lastDeltaX = 0;
this.lastDeltaY = 0;
this.initialled = true;
}
const { x = target.x, y = target.y } = params;
if (progress === 0) {
target.x = target.x - this.lastDeltaX;
target.y = target.y - this.lastDeltaY;
this.lastDeltaX = 0;
this.lastDeltaY = 0;
return;
}
if (progress === 1) {
this.lastDeltaX = x - target.x + this.lastDeltaX;
this.lastDeltaY = y - target.y + this.lastDeltaY;
target.x = x;
target.y = y;
return;
}
if (x != null) {
target.x -= this.lastDeltaX;
const deltaX = (x - target.x) * progress;
target.x += deltaX;
this.lastDeltaX = deltaX;
}
if (y != null) {
target.y -= this.lastDeltaY;
const deltaY = (y - target.y) * progress;
target.y += deltaY;
this.lastDeltaY = deltaY;
}
}
}
class MoveByAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
const deltaProgress = progress - lastProgress;
const { x, y } = params;
(x != null) && (target.x += x * deltaProgress);
(y != null) && (target.y += y * deltaProgress);
}
}
class FadeToAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
if (!this.initialled) {
this.alpha = target.alpha;
this.lastDeltaAlpha = 0;
this.initialled = true;
}
const alpha = params;
if (progress === 0) {
target.alpha = target.alpha - this.lastDeltaAlpha;
this.lastDeltaAlpha = 0;
return;
}
if (progress === 1) {
this.lastDeltaAlpha = alpha - target.alpha + this.lastDeltaAlpha;
target.alpha = alpha;
return;
}
target.alpha -= this.lastDeltaAlpha;
const deltaAlpha = (alpha - target.alpha) * progress;
target.alpha += deltaAlpha;
this.lastDeltaAlpha = deltaAlpha;
}
}
class FadeByAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
const deltaProgress = progress - lastProgress;
target.alpha += params * deltaProgress;
}
}
class DelayAction extends AbstractAction {
updateTransform() {
// do nothing :)
}
}
class RotateToAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
if (!this.initialled) {
this.lastDeltaRotation = 0;
this.initialled = true;
}
const rotation = params;
if (progress === 0) {
target.rotation = target.rotation - this.lastDeltaRotation;
this.lastDeltaRotation = 0;
return;
}
if (progress === 1) {
this.lastDeltaRotation = rotation - target.rotation + this.lastDeltaRotation;
target.rotation = rotation;
return;
}
target.rotation -= this.lastDeltaRotation;
const deltaAlpha = (rotation - target.rotation) * progress;
target.rotation += deltaAlpha;
this.lastDeltaRotation = deltaAlpha;
}
}
class RotateByAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
const deltaProgress = progress - lastProgress;
target.rotation += params * deltaProgress;
}
}
class ScaleToAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
if (!this.initialled) {
this.lastDeltaScaleX = 0;
this.lastDeltaScaleY = 0;
this.initialled = true;
}
const { x = target.scale.x, y = target.scale.y } = params;
if (progress === 0) {
target.scale.x = target.scale.x - this.lastDeltaScaleX;
target.scale.y = target.scale.y - this.lastDeltaScaleY;
this.lastDeltaScaleX = 0;
this.lastDeltaScaleY = 0;
return;
}
if (progress === 1) {
this.lastDeltaScaleX = x - target.scale.x + this.lastDeltaScaleX;
this.lastDeltaScaleY = y - target.scale.y + this.lastDeltaScaleY;
target.scale.x = x;
target.scale.y = y;
return;
}
if (x != null) {
target.scale.x -= this.lastDeltaScaleX;
const deltaX = (x - target.scale.x) * progress;
target.scale.x += deltaX;
this.lastDeltaScaleX = deltaX;
}
if (y != null) {
target.scale.y -= this.lastDeltaScaleY;
const deltaY = (y - target.scale.y) * progress;
target.scale.y += deltaY;
this.lastDeltaScaleY = deltaY;
}
}
}
class ScaleByAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
const deltaProgress = progress - lastProgress;
const { x, y } = params;
(x != null) && (target.scale.x += x * deltaProgress);
(y != null) && (target.scale.y += y * deltaProgress);
}
}
class TintToAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
if (!this.initialled) {
this.targetColor = new Color(params);
this.lastDeltaColor = new Color(0);
this.initialled = true;
}
const currentColor = new Color(target.tint || 0xffffff);
if (progress === 0) {
currentColor.r = currentColor.r - this.lastDeltaColor.r;
currentColor.g = currentColor.g - this.lastDeltaColor.g;
currentColor.b = currentColor.b - this.lastDeltaColor.b;
target.tint = currentColor.toNumber();
this.lastDeltaColor = new Color(0);
return;
}
if (progress === 1) {
this.lastDeltaColor.r = this.targetColor.r - currentColor.r + this.lastDeltaColor.r;
this.lastDeltaColor.g = this.targetColor.g - currentColor.g + this.lastDeltaColor.g;
this.lastDeltaColor.b = this.targetColor.b - currentColor.b + this.lastDeltaColor.b;
target.tint = this.targetColor.toNumber();
return;
}
currentColor.r -= this.lastDeltaColor.r;
currentColor.g -= this.lastDeltaColor.g;
currentColor.b -= this.lastDeltaColor.b;
const deltaColor = new Color(0);
deltaColor.r = (this.targetColor.r - currentColor.r) * progress;
deltaColor.g = (this.targetColor.g - currentColor.g) * progress;
deltaColor.b = (this.targetColor.b - currentColor.b) * progress;
currentColor.r += deltaColor.r;
currentColor.g += deltaColor.g;
currentColor.b += deltaColor.b;
target.tint = currentColor.toNumber();
this.lastDeltaColor = deltaColor;
}
}
class TintByAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
const deltaProgress = progress - lastProgress;
const currentColor = new Color(target.tint || 0xffffff);
const targetColor = new Color(params);
currentColor.r = (currentColor.r + (targetColor.r * deltaProgress)) % 256;
currentColor.g = (currentColor.g + (targetColor.g * deltaProgress)) % 256;
currentColor.b = (currentColor.b + (targetColor.b * deltaProgress)) % 256;
target.tint = currentColor.toNumber();
}
}
class ShakeAction extends AbstractAction {
getState(_progress, offset) {
let progress = _progress;
let firstPart = 0;
let secondPart = 0;
let thirdPart = 0;
let forthPart = 0;
if (progress > 0.75) {
forthPart = progress % 0.75;
progress -= forthPart;
}
if (progress > 0.5) {
thirdPart = progress % 0.5;
progress -= thirdPart;
}
if (progress > 0.25) {
secondPart = progress % 0.250001;
progress -= secondPart;
}
firstPart = progress;
return (firstPart - secondPart - thirdPart + forthPart) * offset * 4;
}
updateTransform(progress, lastProgress, target, params) {
// const deltaProgress = progress - lastProgress;
const { offsetX, offsetY } = params;
if (!this.initialled) {
this.lastDeltaX = 0;
this.lastDeltaY = 0;
this.initialled = true;
}
if (progress === 1 || progress === 0) {
target.x = target.x - this.lastDeltaX;
target.y = target.y - this.lastDeltaY;
this.lastDeltaX = 0;
this.lastDeltaY = 0;
}
if (offsetX) {
const deltaX = this.getState(progress, offsetX);
target.x = (target.x - this.lastDeltaX) + deltaX;
this.lastDeltaX = deltaX;
}
if (offsetY) {
const deltaY = this.getState(progress, offsetY);
target.y = (target.y - this.lastDeltaY) + deltaY;
this.lastDeltaY = deltaY;
}
}
}
class QuakeAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
const deltaProgress = progress - lastProgress;
const { offsetX, offsetY, speed = 10 } = params;
if (!this.initialled) {
this.lastDeltaX = 0;
this.lastDeltaY = 0;
this.elapsedProgress = 0;
this.initialled = true;
}
this.elapsedProgress += Math.abs(deltaProgress);
if (this.elapsedProgress < 1 / speed && (this.progress !== 1 && this.progress !== 0)) {
return;
}
this.elapsedProgress = this.elapsedProgress % (1 / speed);
if (progress === 1 || progress === 0) {
target.x = target.x - this.lastDeltaX;
target.y = target.y - this.lastDeltaY;
this.lastDeltaX = 0;
this.lastDeltaY = 0;
this.elapsedProgress = 0;
return;
}
if (offsetX) {
const deltaX = (Math.random() * offsetX * 2) - offsetX;
target.x = (target.x - this.lastDeltaX) + deltaX;
this.lastDeltaX = deltaX;
}
if (offsetY) {
const deltaY = (Math.random() * offsetY * 2) - offsetY;
target.y = (target.y - this.lastDeltaY) + deltaY;
this.lastDeltaY = deltaY;
}
}
}
class SetPropertyAction extends AbstractAction {
updateTransform(progress, lastProgress, target, params) {
const keys = Object.keys(params);
if (!this.initialled) {
this.params = {};
for (const key of keys) {
this.params[key] = target[key];
}
this.initialled = true;
}
const deltaProgress = progress - lastProgress;
if (deltaProgress > 0) {
for (const key of keys) {
target[key] = params[key];
}
} else {
for (const key of keys) {
target[key] = this.params[key];
}
}
}
}
class CallbackAction extends AbstractAction {
constructor(...args) {
super(...args);
/**
* autoFinish is designed to control whether a action should be setting to `finished`
* when after the specific `duration`.
*
* So, if duration was setting to 0 or undefined, `autoFinish` should be `false`,
* thus a callback `() => (this.finished = true)` must be passed to users
* so that they can finish it by themselves.
*/
if (this.duration) {
this.autoFinish = true;
} else {
this.autoFinish = false;
}
}
update(time) {
return super.update(time, this.autoFinish);
}
updateTransform(progress, lastProgress, target, params) {
params.call(target, progress, lastProgress, target, () => (this.finished = true));
}
}
export default function tweenGenerator(scheme, targetMap) {
if (scheme.type === 'action') {
let target;
if (typeof scheme.target === 'string') {
target = targetMap[scheme.target];
} else {
target = scheme.target;
}
const args = [target, scheme.params, scheme.duration, scheme.easing, scheme.repeat, scheme.yoyo];
switch (scheme.name) {
case 'moveTo': return new MoveToAction(...args);
case 'moveBy': return new MoveByAction(...args);
case 'delay': return new DelayAction(...args);
case 'fadeTo': return new FadeToAction(...args);
case 'fadeBy': return new FadeByAction(...args);
case 'rotateTo': return new RotateToAction(...args);
case 'rotateBy': return new RotateByAction(...args);
case 'scaleTo': return new ScaleToAction(...args);
case 'scaleBy': return new ScaleByAction(...args);
case 'tintTo': return new TintToAction(...args);
case 'tintBy': return new TintByAction(...args);
case 'shake': return new ShakeAction(...args);
case 'quake': return new QuakeAction(...args);
case 'setProperty': return new SetPropertyAction(...args);
case 'callback': return new CallbackAction(...args);
default: logger.warn(`Unknown action '${scheme.name}', ignored.`);
}
}
const actions = [];
for (const action of scheme.actions) {
actions.push(tweenGenerator(action, targetMap));
}
if (scheme.type === 'sequence') {
return new Sequence(actions, scheme.repeat, scheme.yoyo);
} else if (scheme.type === 'parallel') {
return new Parallel(actions, scheme.repeat, scheme.yoyo);
}
return new Sequence([], scheme.repeat, scheme.yoyo);
}