import { Flags } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { BaseComponent } from './BaseComponent';
import { LerpType, LoopType, EventType, SpaceType } from '../const';
const __ = {
private: Symbol('private'),
}
const cMoveToActionName = '__moveTo__';
const cMovePathActionName = '__movePath__';
const cScaleToActionName = '__scaleTo__';
const cRotateToActionName = '__rotateTo__';
const cFadingActionName = '__fading__';
const cFlyingActionNames = ['__positionFlying__', '__targetFlying__'];
const Flag = {
Flying: 1 << 0,
};
let _lerpResults = [];
// #region Private Functions
function _buildAttribute(keys, object, attribute, callback) {
if (Utils.isArray(attribute)) {
return attribute.slice(0);
}
let result = {};
keys.forEach(key => {
let value = attribute[key];
if (Utils.isNull(value)) {
value = object.getAttribute(key);
}
else {
if (object.hasAttribute(key)) {
if (callback) {
callback(key, value);
}
}
}
result[key] = value;
});
return result;
}
function _buildAsyncTrigger(resolve, reject) {
return {
start: () => {
},
stop: () => {
resolve('stop');
},
update: () => {
},
complete: () => {
resolve('complete');
},
onStart: () => {
},
onStop: () => {
resolve('onStop');
},
onUpdate: () => {
},
onComplete: () => {
resolve('onComplete');
},
};
}
function _invokeCallback(object, value, callback, trigger, key) {
if (callback) {
callback({ object, value });
}
if (trigger && trigger[key]) {
trigger[key]();
}
}
function _update(object, from, to, values, onUpdate, progress, direction, beforeCallback, afterCallback) {
let results;
// It's array
if (values.length) {
_lerpResults.length = 0;
results = _lerpResults;
}
// It's object
else {
results = {};
}
let index = 0;
// Update values
for (let key in values) {
let value;
// Use slerp to work with quaternion
if (key == 'quaternion' || key == 'localQuaternion') {
value = MathUtils.slerp(to[key + '_start'], to[key], progress);
}
else {
value = values[key];
}
if (beforeCallback) {
beforeCallback(key, value);
}
results[key] = value;
if (afterCallback) {
afterCallback(key, value, index);
}
index++;
}
// Notify outside
if (onUpdate) {
onUpdate({ object, from, to, progress, value: results, direction, });
}
}
// Parse lerp to arguments.
function _parseLerpArgs(param, name, trigger) {
let options;
// Check whether it's number->number lerping mode
if (Utils.isNumber(param) && Utils.isNumber(name)) {
let fromNumber = param;
let toNumber = name;
options = {
from: { 'current': fromNumber },
to: { 'current': toNumber }
};
// Use trigger as lerping name
if (Utils.isString(trigger)) {
name = trigger;
}
else {
name = '';
// Check whether trigger is options
if (Utils.isObject(trigger)) {
for (let key in trigger) {
options[key] = trigger[key];
}
trigger = {};
}
}
}
else {
options = Object.assign({}, param);
}
options['name'] = name;
options['trigger'] = trigger;
return options;
}
function _buildUVTransformValues(from, to) {
let uv = {};
if (Utils.isValid(to.rotation)) {
uv.rotation = from.rotation;
}
if (to.offset) {
uv.offset = from.offset.slice(0);
}
if (to.repeat) {
uv.repeat = from.repeat.slice(0);
}
if (to.center) {
uv.center = from.center.slice(0);
}
return uv;
}
function _buildUVTransformName(slotType) {
return `__UVTransform_${slotType.toLowerCase()}__`;
}
function _parseLerpingOptions(options = {}) {
return {
loopType: Utils.parseLoopType(options['loopType']),
lerpType: Utils.parseLerpType(options['lerpType']),
time: options['time'],
duration: options['duration'],
times: options['times'],
loop: options['loop']
};
}
// #endregion
/**
* @class LerpComponent
* 插值计算组件
* @memberof THING
* @extends THING.BaseComponent
* @public
*/
class LerpComponent extends BaseComponent {
static mustCopyWithInstance = true;
/**
* 提供物体插值计算的功能组件
* </br>用于执行平滑的变换操作的物体插值计算功能组件。
*/
constructor() {
super();
this[__.private] = {};
let _private = this[__.private];
_private.flags = new Flags();
_private.tweens = [];
}
// #region Private
_buildLerpParam(trigger, param) {
let { lerpType, loop, loopType, times, duration, time, delayTime } = param;
let onRepeat = Utils.parseValue(param['onRepeat'], param['repeat']);
let onStart = Utils.parseValue(param['onStart'], param['start']);
let onStop = Utils.parseValue(param['onStop'], param['stop']);
let onUpdate = Utils.parseValue(param['onUpdate'], param['update']);
let onComplete = Utils.parseValue(param['onComplete'], param['complete']);
let canTriggerRepeat = false;
return {
lerpType,
loop,
loopType,
times,
duration,
time,
delayTime,
onRepeat: () => {
canTriggerRepeat = true;
},
onStart: () => {
if (onStart) {
onStart({ object: this.object });
}
if (trigger && trigger.onStart) {
trigger.onStart();
}
},
onStop: () => {
if (onStop) {
onStop({ object: this.object });
}
if (trigger && trigger.onStop) {
trigger.onStop();
}
},
onUpdate: (ev) => {
if (canTriggerRepeat) {
canTriggerRepeat = false;
if (onRepeat) {
onRepeat({ object: this.object });
}
if (trigger && trigger.onRepeat) {
trigger.onRepeat();
}
}
if (onUpdate) {
onUpdate(ev, ev.progress);
}
if (trigger && trigger.onUpdate) {
trigger.onUpdate();
}
},
onComplete: () => {
if (onComplete) {
onComplete({ object: this.object });
}
if (trigger && trigger.onComplete) {
trigger.onComplete();
}
}
};
}
_stopTweenByName(name) {
let _private = this[__.private];
let tweens = _private.tweens;
let index = tweens.findIndex(a => { return a.name == name; });
if (index === -1) {
return false;
}
tweens[index].tween.stop();
return true;
}
_removeTweenByName(name) {
let _private = this[__.private];
let tweens = _private.tweens;
let index = tweens.findIndex(a => { return a.name == name; });
if (index === -1) {
return false;
}
tweens._removeAt(index);
return true;
}
_getTweenByName(name) {
let _private = this[__.private];
let tweens = _private.tweens;
let index = tweens.findIndex(a => { return a.name == name; });
if (index === -1) {
return null;
}
return tweens[index].tween;
}
_lerpPoints(value, param, name, trigger) {
// Parse arguments
let loopType = Utils.parseLoopType(param['loopType']);
let onRepeat = Utils.parseValue(param['onRepeat'], param['repeat']);
let onStart = Utils.parseValue(param['onStart'], param['start']);
let onStop = Utils.parseValue(param['onStop'], param['stop']);
let onNext = Utils.parseValue(param['onNext'], param['next']);
let onUpdate = Utils.parseValue(param['onUpdate'], param['update']);
let onComplete = Utils.parseValue(param['onComplete'], param['complete']);
// Copy path to move
let path = value.slice(0);
// Check whether it's closure path
if (param['closure']) {
path.push(value[0]);
}
// Move object to start position
let object = this.object;
// The reverse mode
let reverseMode = false;
// The current index of path
let curIndex = -1;
// The direction of path
let direction = 1
let fromIndex = 0, toIndex = 1;
// Build lerp params
let lerpParam = Object.assign({}, param);
lerpParam['onRepeat'] = () => {
// Reset start index
curIndex = -1;
if (loopType == LoopType.PingPong) {
reverseMode = !reverseMode;
direction = -direction;;
}
else {
// Set the initial position of the object as the starting point of the path.
object.position = path[0];
}
if (onRepeat) {
onRepeat({ object, from: path[0], to: path[1] });
}
};
lerpParam['onStart'] = () => {
// Set the initial position of the object as the starting point of the path.
object.position = path[0];
if (onStart) {
onStart({ object, from: path[0], to: path[1] });
}
};
lerpParam['onStop'] = () => {
if (onStop) {
onStop({ object });
}
};
lerpParam['onUpdate'] = (ev) => {
let progress = ev.value.progress;
// Get the position by progress
let { point, index, from, to } = MathUtils.lerpPoints(path, steps, progress);
if (progress !== 1) {
if (reverseMode) {
fromIndex = index + 1;
toIndex = index;
} else {
fromIndex = index;
toIndex = index + 1;
}
}
else {
fromIndex = index - 1;
toIndex = index;
}
// Exchange from and to if it's in reserve mode
if (reverseMode) {
let cur = from;
from = to;
to = cur;
}
// Check whether reach next path
if (curIndex != index) {
if (to) {
if (onNext) {
onNext({ object, from, to });
}
}
curIndex = index;
}
// Notify outside
if (onUpdate) {
onUpdate({ object, point, from, to, progress, direction, fromIndex, toIndex });
}
};
lerpParam['onComplete'] = () => {
if (onComplete) {
onComplete({ object, progress: 1 });
}
};
// Start to lerp points
let options = this._buildLerpParam(trigger, lerpParam);
// Get the total distance of path
let steps = MathUtils.getPointsSteps(path);
if (!steps.length) {
if (options.onComplete) {
options.onComplete();
}
return;
}
options['from'] = { progress: 0 };
options['to'] = { progress: 1 };
this.to(options, name);
}
_lookAt(object, from, to, up) {
up = up || object.up;
if (MathUtils.equalsVector3(from, to)) {
return;
}
object.quaternion = MathUtils.getQuatFromTarget(from, to, up);
}
// #endregion
// #region BaseComponent Interface
onImport(param) {
let uv = param['uv'];
if (uv) {
for (let key in uv) {
let value = uv[key]['value'];
let options = _parseLerpingOptions(uv[key]['options']);
this.uvTransformTo(key, value, options);
}
}
}
onRemove() {
this.stopAll();
super.onRemove();
}
// #endregion
// #region Common
/**
* 停止全部插值
* @public
* @example
* let lerp = box.lerp;
* lerp.stopAll();
*/
stopAll() {
let _private = this[__.private];
// We must copy tweens here to prevent missing stop tween action with 'tween.stop()' interface
let tweens = _private.tweens.slice(0);
tweens.forEach(object => {
object.tween.stop();
});
_private.tweens.length = 0;
}
/**
* 暂停全部插值
* @public
* @example
* let lerp = box.lerp;
* lerp.pauseAll();
*/
pauseAll() {
let _private = this[__.private];
_private.tweens.forEach(object => {
object.tween.pause();
});
}
/**
* 恢复全部插值
* @public
* @example
* let lerp = box.lerp;
* lerp.resumeAll();
*/
resumeAll() {
let _private = this[__.private];
_private.tweens.forEach(object => {
object.tween.resume();
});
}
/**
* 暂停插值
* @param {string} name 插值的名称,
* @public
* @example
* let lerp = box.lerp;
* lerp.pause('move');
*/
pause(name) {
if (!name) {
return;
}
if (Utils.isArray(name)) {
name.forEach(n => {
this.pause(n);
});
}
else {
let tween = this._getTweenByName(name);
if (tween) {
tween.pause();
}
}
}
/**
* 恢复插值
* @param {string} name 插值的名称,
* @public
* @example
* let lerp = box.lerp;
* lerp.resume('move');
*/
resume(name) {
if (!name) {
return;
}
if (Utils.isArray(name)) {
name.forEach(n => {
this.resume(n);
});
}
else {
let tween = this._getTweenByName(name);
if (tween) {
tween.resume();
}
}
}
/**
* 停止插值
* @param {string} name 插值的名称,
* @public
* @example
* let lerp = box.lerp;
* lerp.stop('move');
*/
stop(name) {
if (!name) {
return;
}
if (Utils.isArray(name)) {
name.forEach(n => {
this._stopTweenByName(n);
});
}
else {
this._stopTweenByName(name);
}
}
/**
* 开始插值
* @param {LerpArgs} param 插值参数
* @param {string} name 插值的名称,停止插值时需要传入该参数
* @param {Function} trigger 触发器函数
* @public
* @example
* box.lerp.to({
* from: {
* position: [0, 0, 0]
* },
* to: {
* position: [10, 10, 10]
* },
* time: 5000,
* loopType: THING.LoopType.PingPong,
* onUpdate: (e) => {
* console.log(e.from, e.to, e.direction);
* }
* })
*/
to(param = {}, name, trigger) {
let options = _parseLerpArgs(param, name, trigger);
// Get the binding object
let object = this.object;
// Parse arguments
let syncMode = Utils.parseValue(options['syncMode'], true);
let from = options['from'] || {};
let to = options['to'] || {};
let lerpType = Utils.parseLerpType(options['lerpType']) || LerpType.Linear.None;
let loopType = Utils.parseLoopType(options['loopType']);
let times = Utils.parseValue(options['times'], -1);
let duration = Utils.parseNumber(options['duration'], Utils.parseNumber(options['time'], 1000));
let delayTime = Utils.parseValue(options['delayTime'], 0);
let onRepeat = Utils.parseValue(options['onRepeat'], options['repeat']);
let onStart = Utils.parseValue(options['onStart'], options['start']);
let onStop = Utils.parseValue(options['onStop'], options['stop']);
let onResume = Utils.parseValue(options['onResume'], options['resume']);
let onPause = Utils.parseValue(options['onPause'], options['pause']);
let onUpdate = Utils.parseValue(options['onUpdate'], options['update']);
let onComplete = Utils.parseValue(options['onComplete'], options['complete']);
let noInterp = Utils.parseBoolean(options['noInterp'], false);
let loop = options['loop'];
if (Utils.isValid(loop)) {
// true indicates -1, false indicates 1
if (Utils.isBoolean(loop)) {
times = loop ? -1 : 1;
}
else {
times = loop;
}
if (!Utils.isValid(loopType)) {
loopType = LoopType.Repeat;
}
}
// Get unique keys
let keys = Utils.getUnionKeys(from, to);
const directUpdateKeys = []
keys.forEach(key => {
directUpdateKeys.push(key.indexOf('/') === -1);
})
// Build the source attributes
let fromAttribute = _buildAttribute(keys, object, from, (key, value) => {
if (syncMode) {
object.setAttribute(key, value);
}
});
// Build the target attributes
let toAttribute = _buildAttribute(keys, object, to);
// Copy quaterion for lerping
if (fromAttribute['quaternion']) {
toAttribute['quaternion_start'] = fromAttribute['quaternion'].slice(0);
}
else if (fromAttribute['localQuaternion']) {
toAttribute['localQuaternion_start'] = fromAttribute['localQuaternion'].slice(0);
}
// Clone original data
let origin = THING.Utils.cloneObject(fromAttribute, false)
let direction = 1;
// Stop current action
if (options['name']) {
this.stop(options['name']);
}
// Get the update attribute callback function
let _onBeforeUpdateAttributeCallback = options['onBeforeUpdateAttribute'];
let _onAfterUpdateAttributeCallback = syncMode ? (key, value, index) => {
if (directUpdateKeys[index]) {
object[key] = value;
}
else {
object.setAttribute(key, value);
}
} : options['onAfterUpdateAttribute'];
let _private = this[__.private];
// Do it now, because it not provide any time info
if (!duration && !delayTime) {
_invokeCallback(object, fromAttribute, onStart, options['trigger'], 'onStart');
_update(object, origin, toAttribute, toAttribute, onUpdate, 1, direction, _onBeforeUpdateAttributeCallback, _onAfterUpdateAttributeCallback);
_invokeCallback(object, fromAttribute, onComplete, options['trigger'], 'onComplete');
}
// Start to lerp
else {
let tween = this.app.tweenManager.lerpTo(fromAttribute, toAttribute, duration, delayTime, { noInterp })
.times(times)
.easing(lerpType)
.looping(loopType)
.onRepeat(ev => {
if (loopType === LoopType.PingPong) {
direction = -direction;
}
_invokeCallback(object, ev.value, onRepeat, options['trigger'], 'onRepeat');
})
.onStart(ev => {
_invokeCallback(object, ev.value, onStart, options['trigger'], 'onStart');
})
.onParseValue((start, end, key) => {
if (key === 'color') {
start[key] = Utils.parseColor(start[key]);
end[key] = Utils.parseColor(end[key]);
}
})
.onStop(ev => {
_invokeCallback(object, ev.value, onStop, options['trigger'], 'onStop');
this._removeTweenByName(options['name']);
})
.onResume(ev => {
_invokeCallback(object, ev.value, onResume, options['trigger'], 'onResume');
})
.onPause(ev => {
_invokeCallback(object, ev.value, onPause, options['trigger'], 'onPause');
})
.onUpdate((ev, progress) => {
if (!object.destroyed) {
_update(object, origin, toAttribute, ev.value, onUpdate, progress, direction, _onBeforeUpdateAttributeCallback, _onAfterUpdateAttributeCallback);
}
})
.onComplete(ev => {
this._removeTweenByName(options['name']);
_invokeCallback(object, ev.value, onComplete, options['trigger'], 'onComplete');
})
.start();
// Bind name and easy to debug
if (_DEBUG) {
tween._tween._name = options['name'];
}
// Update tweens
_private.tweens.push({ name: options['name'], tween });
}
}
/**
* Lerp to in duration (async).
* @param {LerpArgs} param The parameters.
* @param {string} name The lerp name, if user want to stop it later then need to provide it.
* @returns {Promise<any>} 返回一个Promise对象
* @private
* @example
* box.lerp.toAsync({
* from: {
* position: [0, 0, 0]
* },
* to: {
* position: [10, 10, 10]
* },
* })
*/
toAsync(param = {}, name) {
return new Promise((resolve, reject) => {
this.to(param, name, _buildAsyncTrigger(resolve, reject));
});
}
lerpPoints(value, param = {}, name = '', trigger = null) {
if (name) {
this.stop(name);
}
this._lerpPoints(value, param, name, trigger);
}
lerpPointsAsync(value, param = {}, name = '') {
return new Promise((resolve, reject) => {
this.lerpPoints(value, param, name, _buildAsyncTrigger(resolve, reject));
});
}
// #endregion
// #region Moving
stopMoving() {
this.stop(cMoveToActionName);
this.stop(cMovePathActionName);
}
pauseMoving() {
this.pause(cMoveToActionName);
this.pause(cMovePathActionName);
}
resumeMoving() {
this.resume(cMoveToActionName);
this.resume(cMovePathActionName);
}
moveForward(distance, param = {}) {
let object = this.object;
let target = MathUtils.getPositionOnDirection(object.position, object.forward, distance);
this.moveTo(target, param);
}
/**
* 将物体移动到指定位置
* @param {Array|object} value - 目标位置坐标数组[x,y,z]或包含移动参数的对象
* @param {object} param - 移动参数配置对象
* @param {Array<number>} param.to - 目标位置
* @param {Array<number>} param.target - 目标位置,与 to 参数作用相同
* @param {number} param.time - 移动时间(毫秒)
* @param {THING.SpaceType} param.spaceType - 坐标空间类型
* @param {boolean} param.orientToPath - 是否朝向移动方向
* @param {number} param.orientToPathDegree - 朝向偏移角度,在朝向路径方向的基础上偏移的角度,正值为顺时针,负值为逆时针
* @param {number} param.times - 循环次数
* @param {THING.LoopType} param.loopType - 循环类型
* @param {Function} param.onUpdate - 更新回调函数
* @param {Function} param.onComplete - 完成回调函数
* @param {Function} trigger - 触发器函数
* @returns {void}
* @example
* // 基础用法 - 移动到世界坐标系中的位置
* object.lerp.moveTo([10, 0, 10]);
*
* // 使用参数对象方式
* object.lerp.moveTo({
* to: [10, 0, 10], // 目标位置
* time: 2000, // 移动时间(毫秒)
* spaceType: THING.SpaceType.World, // 坐标空间类型
* orientToPath: true, // 是否朝向移动方向
* orientToPathDegree: 45, // 朝向偏移角度
* times: 4, // 循环次数
* loopType: THING.LoopType.PingPong, // 循环类型
* onUpdate: (ev) => { // 更新回调
* console.log(ev.from, ev.to, ev.direction);
* console.log('Progress:', ev.progress);
* },
* onComplete: () => { // 完成回调
* console.log('Movement completed');
* }
* });
* @public
*/
moveTo(value, param = {}, trigger = null) {
this.stopMoving();
if (!Utils.isArray(value)) {
param = value;
value = param['to'];
}
let object = this._object;
const originOnUpdate = param['onUpdate'];
const onUpdate = (ev) => {
const progress = ev.progress;
const direction = ev.direction;
const from = ev.from.localPosition || ev.from.position;
const to = ev.to.localPosition || ev.to.position;
const value = ev.value.localPosition || ev.value.position;
const param = {
object: ev.object,
from: direction === 1 ? from : to,
to: direction === 1 ? to : from,
value,
progress,
direction,
}
originOnUpdate && originOnUpdate(param);
}
param['onUpdate'] = onUpdate;
// Build lerp params
let lerpParam = this._buildLerpParam(trigger, param);
// Set the target position
let spaceType = Utils.parseValue(param['spaceType'], SpaceType.World);
switch (spaceType) {
case SpaceType.World:
lerpParam['to'] = { position: value };
break;
case SpaceType.Local:
lerpParam['to'] = { localPosition: value };
break;
case SpaceType.Self:
lerpParam['to'] = { position: object.selfToWorld(value) };
break;
}
// Set object's quaternion
let orientToPath = Utils.parseValue(param['orientToPath'], true);
let orientToPathDegree = Utils.parseValue(param['orientToPathDegree'], 0);
if (orientToPath) {
let from = spaceType == SpaceType.Local ? object.localPosition : object.position;
let to = lerpParam['to'];
let up = param['up'] || [0, 1, 0];
let target = value;
if (spaceType == SpaceType.Self) {
target = object.selfToWorld(value);
} else if (spaceType == SpaceType.Local) {
target = object.localToWorld(value);
}
// Apply orientToPathDegree if needed
if (orientToPathDegree !== 0) {
let direction = MathUtils.subVector(target, object.position);
direction = MathUtils.rotateVector(direction, up, orientToPathDegree);
target = MathUtils.addVector(object.position, direction);
}
this._lookAt(object, object.position, target, up);
if (lerpParam['loopType'] == LoopType.PingPong) {
let onRepeat = lerpParam['onRepeat'];
lerpParam['onRepeat'] = (ev) => {
// Swap from and to position
let temp = from;
from = to.position || to.localPosition || to;
to = temp;
let tempFrom = from, tempTo = to;
if (spaceType == SpaceType.Local) {
tempFrom = object.localToWorld(from);
tempTo = object.localToWorld(to);
}
// Apply orientToPathDegree for return journey
if (orientToPathDegree !== 0) {
let direction = MathUtils.subVector(tempTo, tempFrom);
direction = MathUtils.rotateVector(direction, up, orientToPathDegree);
tempTo = MathUtils.addVector(tempFrom, direction);
}
this._lookAt(object, tempFrom, tempTo, up);
if (onRepeat) {
onRepeat(ev);
}
};
}
}
// Start to move
this.to(lerpParam, cMoveToActionName);
}
moveToAsync(value, param = {}) {
return new Promise((resolve, reject) => {
this.moveTo(value, param, _buildAsyncTrigger(resolve, reject));
});
}
/**
* 将物体沿指定路径移动
* @param {Array|object} value - 目标路径坐标数组或包含移动参数的对象
* @param {object} param - 移动参数配置对象
* @param {Array<Array<number>>} param.path - 移动路径坐标数组
* @param {number} param.time - 移动时间(毫秒)
* @param {THING.SpaceType} param.spaceType - 坐标空间类型
* @param {boolean} param.orientToPath - 是否朝向移动方向
* @param {number} param.orientToPathDegree - 朝向偏移角度,在朝向路径方向的基础上偏移的角度,正值为顺时针,负值为逆时针
* @param {number} param.times - 循环次数
* @param {THING.LoopType} param.loopType - 循环类型
* @param {Function} param.onUpdate - 更新回调函数
* @param {Function} param.onComplete - 完成回调函数
* @param {Function} trigger - 触发器函数
* @returns {void}
* @example
* // 基础用法 - 直接传入路径数组
* object.lerp.movePath([
* [0, 0, 0],
* [10, 10, 0],
* [10, 10, 10],
* ]);
*
* // 使用参数对象方式
* object.lerp.movePath({
* path: [ // 移动路径
* [0, 0, 0],
* [10, 10, 0],
* [10, 10, 10],
* ],
* time: 2000, // 移动时间(毫秒)
* spaceType: THING.SpaceType.World, // 坐标空间类型
* orientToPath: true, // 是否朝向移动方向
* orientToPathDegree: 45, // 朝向偏移角度
* times: 4, // 循环次数
* loopType: THING.LoopType.PingPong, // 循环类型
* onUpdate: (ev) => { // 更新回调
* console.log(ev.from, ev.to, ev.fromIndex, ev.toIndex, ev.direction);
* console.log('Progress:', ev.progress);
* },
* onComplete: () => { // 完成回调
* console.log('Movement completed');
* }
* });
* @public
*/
movePath(value, param = {}, trigger = null) {
this.stopMoving();
if (Utils.isObject(value)) {
param = value;
value = param['path'];
}
let orientToPath = Utils.parseValue(param['orientToPath'], true);
let orientToPathDegree = Utils.parseValue(param['orientToPathDegree'], 0);
let spaceType = Utils.parseValue(param['spaceType'], SpaceType.World);
let up = param['up'] || [0, 1, 0];
let object = this._object;
let onUpdate = Utils.parseValue(param['onUpdate'], param['update']);
param['onUpdate'] = (ev) => {
let point = ev.point;
// Set the target position
if (spaceType == SpaceType.World) {
object.position = point;
}
else {
object.localPosition = point;
}
if (onUpdate) {
onUpdate(ev);
}
};
let onNext = Utils.parseValue(param['onNext'], param['next']);
param['onNext'] = (ev) => {
let { from, to } = ev;
if (spaceType == SpaceType.Self) {
from = object.parent.selfToWorld(from);
to = object.parent.selfToWorld(to);
} else if (spaceType == SpaceType.Local) {
from = object.localToWorld(from);
to = object.localToWorld(to);
}
// Set object's quaternion
if (orientToPath) {
// Apply orientToPathDegree if needed
if (orientToPathDegree !== 0) {
let direction = MathUtils.subVector(to, from);
direction = MathUtils.rotateVector(direction, up, orientToPathDegree);
to = MathUtils.addVector(from, direction);
}
this._lookAt(object, from, to, up);
}
if (onNext) {
onNext(ev);
}
};
if (orientToPath) {
const loopType = param['loopType'];
const quaternion = object.quaternion.slice();
if (loopType && loopType === THING.LoopType.Repeat) {
let oldRepeat = param['onRepeat'];
param['onRepeat'] = (ev) => {
object.quaternion = quaternion;
if (oldRepeat) {
oldRepeat(ev);
}
}
}
}
this._lerpPoints(value, param, cMovePathActionName, trigger);
}
movePathAsync(value, param = {}) {
return new Promise((resolve, reject) => {
this.movePath(value, param, _buildAsyncTrigger(resolve, reject));
});
}
// #endregion
// #region Scaling
stopScaling() {
this.stop(cScaleToActionName);
}
pauseScaling() {
this.pause(cScaleToActionName);
}
resumeScaling() {
this.resume(cScaleToActionName);
}
/**
* 将物体缩放到指定大小
* @param {Array|object} value - 目标缩放值数组[x,y,z]或包含缩放参数的对象
* @param {object} param - 缩放参数配置对象
* @param {Array<number>} param.to - 目标缩放值
* @param {number} param.time - 缩放时间(毫秒)
* @param {THING.SpaceType} param.spaceType - 坐标空间类型,默认为 World
* @param {number} param.times - 循环次数
* @param {THING.LoopType} param.loopType - 循环类型
* @param {Function} param.onUpdate - 更新回调函数
* @param {Function} param.onComplete - 完成回调函数
* @param {Function} trigger - 触发器函数
* @returns {void}
* @example
* // 基础用法 - 直接传入缩放值数组
* object.lerp.scaleTo([2, 2, 2]);
*
* // 使用参数对象方式
* object.lerp.scaleTo({
* to: [2, 2, 2], // 目标缩放值
* time: 2000, // 缩放时间(毫秒)
* spaceType: THING.SpaceType.World, // 坐标空间类型
* times: 4, // 循环次数
* loopType: THING.LoopType.PingPong, // 循环类型
* onUpdate: (ev) => { // 更新回调
* console.log(ev.from, ev.to, ev.direction);
* console.log('Progress:', ev.progress);
* },
* onComplete: () => { // 完成回调
* console.log('Scale completed');
* }
* });
*
* // 在本地坐标系中缩放
* object.lerp.scaleTo({
* to: [2, 2, 2],
* time: 2000,
* spaceType: THING.SpaceType.Local
* });
*
* // 相对于当前缩放值进行缩放
* object.lerp.scaleTo({
* to: [2, 2, 2],
* time: 2000,
* spaceType: THING.SpaceType.Self
* });
* @public
*/
scaleTo(value, param = {}, trigger) {
this.stopScaling();
if (!Utils.isArray(value)) {
param = value;
value = param['to'];
}
// Build lerp params
let lerpParam = this._buildLerpParam(trigger, param);
let object = this.object;
// Set the target scale
let spaceType = Utils.parseValue(param['spaceType'], SpaceType.World);
switch (spaceType) {
case SpaceType.World:
lerpParam['to'] = { scale: value };
break;
case SpaceType.Local:
lerpParam['to'] = { localScale: value };
break;
case SpaceType.Self:
lerpParam['to'] = { scale: MathUtils.scaleVector(object.scale, value) };
break;
}
// Start to scale
this.to(lerpParam, cScaleToActionName);
}
scaleToAsync(value, param = {}) {
return new Promise((resolve, reject) => {
this.scaleTo(value, param, _buildAsyncTrigger(resolve, reject));
});
}
// #endregion
// #region Rotating
stopRotating() {
this.stop(cRotateToActionName);
}
pauseRotating() {
this.pause(cRotateToActionName);
}
resumeRotating() {
this.resume(cRotateToActionName);
}
/**
* 将物体旋转到指定角度
* @param {Array|object} value - 目标旋转值。可以是:
* - 欧拉角数组[x,y,z]表示旋转角度
* - 四元数数组[x,y,z,w]表示旋转
* - 包含旋转参数的对象
* @param {object} param - 旋转参数配置对象
* @param {Array<number>} param.to - 目标旋转值,可以是欧拉角[x,y,z]或四元数[x,y,z,w]
* @param {number} param.time - 旋转时间(毫秒)
* @param {THING.SpaceType} param.spaceType - 坐标空间类型,默认为 Local
* @param {number} param.times - 循环次数
* @param {THING.LoopType} param.loopType - 循环类型
* @param {Function} param.onUpdate - 更新回调函数
* @param {Function} param.onComplete - 完成回调函数
* @param {Function} trigger - 触发器函数
* @returns {void}
* @example
* // 基础用法 - 使用欧拉角旋转(x,y,z)
* object.lerp.rotateTo([90, 0, 0]); // 绕x轴旋转90度
*
* // 使用四元数旋转(x,y,z,w)
* object.lerp.rotateTo([0, 0, 0, 1]);
*
* // 使用参数对象方式 - 欧拉角
* object.lerp.rotateTo({
* to: [90, 0, 0], // 目标旋转角度
* time: 2000, // 旋转时间(毫秒)
* spaceType: THING.SpaceType.World, // 坐标空间类型
* times: 4, // 循环次数
* loopType: THING.LoopType.PingPong, // 循环类型
* onUpdate: (ev) => { // 更新回调
* console.log(ev.from, ev.to, ev.direction);
* console.log('Progress:', ev.progress);
* },
* onComplete: () => { // 完成回调
* console.log('Rotation completed');
* }
* });
*
* // 在世界坐标系中旋转
* object.lerp.rotateTo({
* to: [90, 0, 0],
* time: 2000,
* spaceType: THING.SpaceType.World
* });
*
* // 在本地坐标系中旋转(默认)
* object.lerp.rotateTo({
* to: [90, 0, 0],
* time: 2000,
* spaceType: THING.SpaceType.Local
* });
*
* // 相对于当前旋转进行旋转
* object.lerp.rotateTo({
* to: [90, 0, 0],
* time: 2000,
* spaceType: THING.SpaceType.Self
* });
* @public
*/
rotateTo(value, param = {}, trigger) {
this.stopRotating();
if (!Utils.isArray(value)) {
param = value;
value = param['to'];
}
// Build lerp params
let lerpParam = this._buildLerpParam(trigger, param);
let object = this.object;
// Set the target angels
let spaceType = Utils.parseValue(param['spaceType'], SpaceType.Local);
switch (spaceType) {
case SpaceType.World:
if (value.length == 3) {
lerpParam['to'] = { angles: value };
}
else if (value.length == 4) {
lerpParam['to'] = { quaternion: value };
}
else {
lerpParam['to'] = { quaternion: MathUtils.getQuatFromAngles(value) };
}
break;
case SpaceType.Local:
if (value.length == 3) {
lerpParam['to'] = { localAngles: value };
}
else if (value.length == 4) {
lerpParam['to'] = { localQuaternion: value };
}
else {
lerpParam['to'] = { localQuaternion: MathUtils.getQuatFromAngles(value) };
}
break;
case SpaceType.Self:
if (value.length == 3) {
lerpParam['to'] = { localAngles: MathUtils.addVector(object.localAngles, value) };
}
else if (value.length == 4) {
lerpParam['to'] = { quaternion: MathUtils.multiplyQuat(object.quaternion, value) };
}
else {
lerpParam['to'] = { quaternion: MathUtils.multiplyQuat(object.quaternion, MathUtils.getQuatFromAngles(value)) };
}
break;
}
// Start to rotate
this.to(lerpParam, cRotateToActionName);
}
rotateToAsync(value, param = {}) {
return new Promise((resolve, reject) => {
this.rotateTo(value, param, _buildAsyncTrigger(resolve, reject));
});
}
// #endregion
// #region Fading
/**
* 停止淡入淡出动作。
* @public
* @example
* let lerp = box.lerp;
* lerp.stopFading();
*/
stopFading() {
this.stop(cFadingActionName);
}
/**
* 暂停淡入淡出动作。
* @public
* @example
* let lerp = box.lerp;
* lerp.pauseFading();
*/
pauseFading() {
this.pause(cFadingActionName);
}
/**
* 恢复淡入淡出动作。
* @public
* @example
* let lerp = box.lerp;
* lerp.resumeFading();
*/
resumeFading() {
this.resume(cFadingActionName);
}
/**
* 将对象淡入到完全不透明。
* @param {LerpArgs} param - 淡入动作的参数。
* @param {Function} trigger - 在淡入完成时调用的触发函数。
* @public
* @example
* let lerp = box.lerp;
* lerp.fadeIn({
* from: {
* style: {
* opacity: 0
* }
* }
*/
fadeIn(param = {}, trigger) {
this.stopFading();
// Get the binding object
let object = this.object;
if (!object.style) {
return;
}
// Build lerp params
let lerpParam = this._buildLerpParam(trigger, param);
lerpParam['from'] = {
style: {
opacity: 0
}
};
lerpParam['to'] = {
style: {
opacity: 1
}
};
// Start to fade
this.to(lerpParam, cFadingActionName);
}
/**
* 将对象淡出到完全透明。
* @param {LerpArgs} param - 淡出动作的参数。
* @param {Function} trigger - 在淡出完成时调用的触发函数。
* @public
* @example
* let lerp = box.lerp;
* lerp.fadeOut({
* from: {
* style: {
* opacity: 1
* }
*/
fadeOut(param = {}, trigger) {
this.stopFading();
// Get the binding object
let object = this.object;
if (!object.style) {
return;
}
// Build lerp params
let lerpParam = this._buildLerpParam(trigger, param);
lerpParam['from'] = {
style: {
opacity: 1
}
};
lerpParam['to'] = {
style: {
opacity: 0
}
};
// Start to fade
this.to(lerpParam, cFadingActionName);
}
/**
* 异步将对象淡入。
* @param {LerpArgs} param - 淡入动作的参数。
* @returns {Promise} 一个在淡入完成时解析的承诺。
* @public
* @example
* let lerp = box.lerp;
* lerp.fadeInAsync({
* from: {
* style: {
* opacity: 0
* }
*/
fadeInAsync(param = {}) {
return new Promise((resolve, reject) => {
this.fadeIn(param, _buildAsyncTrigger(resolve, reject));
});
}
/**
* 异步将对象淡出。
* @param {LerpArgs} param - 淡出动作的参数。
* @returns {Promise} 一个在淡出完成时解析的承诺。
* @public
* @example
* let lerp = box.lerp;
* lerp.fadeOutAsync({
* from: {
* style: {
* opacity: 1
* }
*/
fadeOutAsync(param = {}) {
return new Promise((resolve, reject) => {
this.fadeOut(param, _buildAsyncTrigger(resolve, reject));
});
}
// #endregion
// #region Flying
/**
* @public
* @typedef {object} FlyInfoParams 计算飞行插值参数的对象
* @property {Array<number>} [position] 要飞到的位置
* @property {Array<number>|THING.BaseObject} target 飞行的目标位置,如果是一个物体,则自动选择其边界框的最佳位置。
* </br>飞行到目标位置后,物体的target属性表示物体的方向,即target和position的差表示物体的方向。
* </br>如果传入一个BaseObject,则target是目标对象的位置。如果没有传入position,则会根据目标对象的距离、水平角度和垂直角度来计算position。
* </br>参见 https://en.wikipedia.org/wiki/Spherical_coordinate_system
* @property {Array<number>} up 飞行到目标位置后物体的上方向。
* @property {number} distance 距离(仅适用于对象目标模式)。如果目标位置是一个Object3D对象,则表示与目标对象的距离。
* @property {number} horzAngle 水平角度(仅适用于对象目标模式)。如果目标位置是一个Object3D对象,则表示与目标对象的水平夹角。
*/
/**
* 获取飞行信息。
* @param {FlyInfoParams} param 参数。
* @returns {object} 飞行信息。
* @private
* @example
* let lerp = box.lerp;
* let info = lerp.getFlyInfo({
* target: box,
* });
*/
getFlyInfo(param = {}) {
let position = param['position'];
let target = param['target'];
let up = param['up'];
let object = this.object;
// Build position info
let result = {
position: {
from: object.position,
to: position ? position : object.position,
}
};
// Build target info
let targetPosition = object.target;
if (targetPosition) {
result.target = {
from: targetPosition,
to: null,
};
}
// Build up info
if (up) {
result.up = {
from: object.up,
to: up,
};
}
// It's object
if (target.isBaseObject || target.isBox3) {
let aabb = target;
if (target.isBaseObject) {
aabb = target.getAABB();
}
// If we do not provide position then try to calculate by angles and distance
if (!position) {
// If do not provide any angles then use 45 as default degree
let defaultDegree = 0;
if (Utils.isNull(param['horzAngle']) && Utils.isNull(param['vertAngle'])) {
defaultDegree = 45;
}
// We make distance a little more far
let radiusFactor = Utils.parseValue(param['radiusFactor'], 2.5);
let distance = Utils.parseValue(param['distance'], aabb.radius * radiusFactor);
let horzAngle = Utils.parseValue(param['horzAngle'], defaultDegree);
let vertAngle = Utils.parseValue(param['vertAngle'], defaultDegree);
if (this.object.isCamera) {
// Calculate the distance by fov
const factor = 0.5773502691896257 / Math.tan(0.5 * this.object.fov * Math.PI / 180); // 0.5773502691896257 = Math.tan( 60 * 0.5 * Math.PI / 180 )
distance *= factor;
}
// Prevent horz angles calculation bug
if (vertAngle == 90) {
vertAngle -= 0.1;
}
let offset = MathUtils.getOffsetFromAngles(horzAngle, vertAngle, distance);
result.position.to = MathUtils.addVector(aabb.center, offset);
}
if (result.target) {
result.target.to = aabb.center;
}
}
// It's position
else {
if (result.target) {
result.target.to = target;
}
}
return result;
}
stopFlying() {
this.stop(cFlyingActionNames);
}
pauseFlying() {
this.pause(cFlyingActionNames);
}
resumeFlying() {
this.resume(cFlyingActionNames);
}
flyTo(param = {}, trigger) {
let _private = this[__.private];
param = Utils.parseFlyParam(param);
// Parse arguments
let duration = param['duration'];
let time = param['time'];
let delayTime = Utils.parseValue(param['delayTime'], 0);
let lerpType = param['lerpType'] || LerpType.Linear.None;
let positionLerpType = Utils.parseValue(param['positionLerpType'], lerpType);
let targetLerpType = Utils.parseValue(param['targetLerpType'], lerpType);
let upLerpType = Utils.parseValue(param['upLerpType'], lerpType);
let onStart = Utils.parseValue(param['onStart'], param['start']);
let onStop = Utils.parseValue(param['onStop'], param['stop']);
let onResume = Utils.parseValue(param['onResume'], param['resume']);
let onPause = Utils.parseValue(param['onPause'], param['pause']);
let onUpdate = Utils.parseValue(param['onUpdate'], param['update']);
let onComplete = Utils.parseValue(param['onComplete'], param['complete']);
// Prepare for flying
let that = this;
// Stop previous flying action
this.stop(cFlyingActionNames);
// Parse fly info
let info = this.getFlyInfo(param);
// Start to fly with position
this.to({
syncMode: false,
from: info.position.from,
to: info.position.to,
duration,
time,
delayTime,
lerpType: positionLerpType,
onStart: function () {
_private.flags.enable(Flag.Flying, true);
that.object.trigger(EventType.StartFlying);
if (onStart) {
onStart({ object: that.object });
}
if (trigger && trigger.onStart) {
trigger.onStart();
}
},
onStop: function () {
_private.flags.enable(Flag.Flying, false);
that.object.trigger(EventType.StopFlying);
if (onStop) {
onStop({ object: that.object });
}
if (trigger && trigger.onStop) {
trigger.onStop();
}
},
onResume: function () {
_private.flags.enable(Flag.Flying, true);
if (onResume) {
onResume({ object: that.object });
}
},
onPause: function () {
_private.flags.enable(Flag.Flying, false);
if (onPause) {
onPause({ object: that.object });
}
},
onUpdate: function (ev) {
that.object.position = ev.value;
that.object.trigger(EventType.Flying);
if (onUpdate) {
onUpdate({ progress: ev.progress, object: that.object });
}
if (trigger && trigger.onUpdate) {
trigger.onUpdate();
}
},
onComplete: function () {
_private.flags.enable(Flag.Flying, false);
that.object.trigger(EventType.CompleteFlying);
if (onComplete) {
onComplete({ object: that.object });
}
if (trigger && trigger.onComplete) {
trigger.onComplete();
}
}
}, cFlyingActionNames[0]);
// Start to fly with target
if (info.target) {
// Start to fly with up
if (info.up) {
this.to({
syncMode: false,
from: { up: info.up.from, target: info.target.from },
to: { up: info.up.to, target: info.target.to },
duration,
time,
delayTime,
lerpType: upLerpType,
onUpdate: function (ev) {
let up = ev.value.up;
that.object.lookAt(ev.value.target, { up });
}
}, cFlyingActionNames[1]);
}
else {
this.to({
syncMode: false,
from: info.target.from,
to: info.target.to,
duration,
time,
delayTime,
lerpType: targetLerpType,
onUpdate: function (ev) {
that.object.lookAt(ev.value);
}
}, cFlyingActionNames[1]);
}
}
}
flyToAsync(param = {}) {
return new Promise((resolve, reject) => {
this.flyTo(param, _buildAsyncTrigger(resolve, reject));
});
}
fit(param = {}) {
this.stopFlying();
param = Utils.parseFlyParam(param);
param['target'] = param['target'] || this.app.root;
let flyInfo = this.getFlyInfo(param);
this.object.position = flyInfo.position.to;
if (flyInfo.target) {
this.object.lookAt(flyInfo.target.to);
}
}
get isFlying() {
let _private = this[__.private];
return _private.flags.has(Flag.Flying);
}
// #endregion
// #region UV
stopUVTransform(slotType) {
if (!slotType) {
return;
}
this.stop(_buildUVTransformName(slotType));
}
pauseUVTransform(slotType) {
if (!slotType) {
return;
}
this.pause(_buildUVTransformName(slotType));
}
resumeUVTransform(slotType) {
if (!slotType) {
return;
}
this.resume(_buildUVTransformName(slotType));
}
uvTransformTo(slotType, value, param, trigger) {
if (!slotType) {
return;
}
// We can provide value as param to run
if (param === undefined) {
param = value;
}
this.stopUVTransform();
// Get the UV transform info
let object = this.object;
let style = object.style;
let from = param['from'] || style.getUV(slotType);
let to = param['to'] || value;
// Build lerp params
let lerpParam = this._buildLerpParam(trigger, param);
lerpParam['from'] = _buildUVTransformValues(from, to);
lerpParam['to'] = to;
lerpParam['syncMode'] = false;
lerpParam['onAfterUpdateAttribute'] = (key, value) => {
if (object.invisible) {
return;
}
style.setUV(slotType, key, value);
};
// Start to rotate
this.to(lerpParam, _buildUVTransformName(slotType));
}
uvTransformToAsync(slotType, value, param) {
if (!slotType) {
return;
}
return new Promise((resolve, reject) => {
this.uvTransformTo(slotType, value, param, _buildAsyncTrigger(resolve, reject));
});
}
// #endregion
}
export { LerpComponent }