255 lines
8.4 KiB
TypeScript
255 lines
8.4 KiB
TypeScript
interface LoadingNodeState {
|
||
x: number;
|
||
y: number;
|
||
scaleX: number;
|
||
scaleY: number;
|
||
opacity: number;
|
||
}
|
||
|
||
interface LoadingAnimationState {
|
||
runId: number;
|
||
cat: LoadingNodeState;
|
||
blocks: LoadingNodeState[];
|
||
}
|
||
|
||
const STATE_KEY = "__loadingCatAnimationState";
|
||
|
||
/** 统一管理各界面的猫跳 Loading,避免每个业务脚本复制一套动画。 */
|
||
export default class LoadingCatAnimation {
|
||
static play(loading: cc.Node): boolean {
|
||
if (!loading || !cc.isValid(loading)) {
|
||
return false;
|
||
}
|
||
|
||
const cat = loading.getChildByName("cat");
|
||
const blocks = [1, 2, 3, 4].map(index => loading.getChildByName(`block${index}`));
|
||
if (!cat || blocks.some(block => !block)) {
|
||
cc.warn("Loading 缺少 cat 或 block1~4,无法播放猫跳动画:", loading.name);
|
||
loading.active = true;
|
||
return false;
|
||
}
|
||
|
||
let state = this.getState(loading);
|
||
if (!state) {
|
||
state = {
|
||
runId: 0,
|
||
cat: this.capture(cat),
|
||
blocks: blocks.map(block => this.capture(block)),
|
||
};
|
||
(loading as any)[STATE_KEY] = state;
|
||
}
|
||
|
||
this.stopTargets(loading, cat, blocks);
|
||
this.reset(cat, blocks, state);
|
||
const labelNode = loading.getChildByName("New Label");
|
||
const label = labelNode && labelNode.getComponent(cc.Label);
|
||
if (label) {
|
||
label.string = "加载中,请稍等喵~";
|
||
}
|
||
|
||
loading.active = true;
|
||
const runId = ++state.runId;
|
||
this.playCycle(loading, cat, blocks, state, runId);
|
||
return true;
|
||
}
|
||
|
||
static stop(loading: cc.Node): void {
|
||
if (!loading || !cc.isValid(loading)) {
|
||
return;
|
||
}
|
||
|
||
const cat = loading.getChildByName("cat");
|
||
const blocks = [1, 2, 3, 4]
|
||
.map(index => loading.getChildByName(`block${index}`))
|
||
.filter(block => !!block);
|
||
const state = this.getState(loading);
|
||
if (state) {
|
||
state.runId++;
|
||
}
|
||
this.stopTargets(loading, cat, blocks);
|
||
if (cat && blocks.length === 4 && state) {
|
||
this.reset(cat, blocks, state);
|
||
}
|
||
loading.active = false;
|
||
}
|
||
|
||
private static playCycle(
|
||
loading: cc.Node,
|
||
cat: cc.Node,
|
||
orderedBlocks: cc.Node[],
|
||
state: LoadingAnimationState,
|
||
runId: number,
|
||
): void {
|
||
if (!this.isRunning(loading, state, runId)) {
|
||
return;
|
||
}
|
||
|
||
const base = state.cat;
|
||
const spacing = Math.abs(orderedBlocks[1].x - orderedBlocks[0].x) || 150;
|
||
const landingBlock = orderedBlocks[2];
|
||
const footOffsetY = cat.height * cat.anchorY;
|
||
const squashScaleY = base.scaleY * 0.86;
|
||
const stretchScaleY = base.scaleY * 1.10;
|
||
|
||
cc.tween(cat)
|
||
.delay(0.10)
|
||
.to(0.09, {
|
||
y: base.y - 12,
|
||
scaleX: base.scaleX * 1.18,
|
||
scaleY: base.scaleY * 0.80,
|
||
}, { easing: "quadIn" })
|
||
.call(() => this.moveBlocks(orderedBlocks, spacing))
|
||
.to(0.08, {
|
||
y: base.y + 26,
|
||
scaleX: base.scaleX * 0.84,
|
||
scaleY: base.scaleY * 1.22,
|
||
}, { easing: "quadOut" })
|
||
.to(0.19, {
|
||
y: base.y + 108,
|
||
scaleX: base.scaleX * 0.98,
|
||
scaleY: base.scaleY * 1.03,
|
||
}, { easing: "quadOut" })
|
||
.to(0.19, {
|
||
y: base.y + 38,
|
||
scaleX: base.scaleX * 0.94,
|
||
scaleY: base.scaleY * 1.09,
|
||
}, { easing: "quadIn" })
|
||
.to(0.08, {
|
||
y: base.y,
|
||
scaleX: base.scaleX,
|
||
scaleY: base.scaleY,
|
||
}, { easing: "quadIn" })
|
||
.call(() => {
|
||
if (this.isRunning(loading, state, runId)) {
|
||
this.flashBlock(landingBlock);
|
||
}
|
||
})
|
||
.delay(0.03)
|
||
.to(0.10, {
|
||
y: base.y + footOffsetY * (squashScaleY - base.scaleY),
|
||
scaleX: base.scaleX * 1.12,
|
||
scaleY: squashScaleY,
|
||
}, { easing: "quadInOut" })
|
||
.to(0.11, {
|
||
y: base.y + footOffsetY * (stretchScaleY - base.scaleY),
|
||
scaleX: base.scaleX * 0.94,
|
||
scaleY: stretchScaleY,
|
||
}, { easing: "quadOut" })
|
||
.to(0.08, {
|
||
y: base.y,
|
||
scaleX: base.scaleX,
|
||
scaleY: base.scaleY,
|
||
}, { easing: "sineOut" })
|
||
.delay(0.05)
|
||
.call(() => {
|
||
if (!this.isRunning(loading, state, runId)) {
|
||
return;
|
||
}
|
||
|
||
const recycledBlock = orderedBlocks.shift();
|
||
const rightmostBlock = orderedBlocks[orderedBlocks.length - 1];
|
||
const recycledState = this.getBlockState(recycledBlock, state);
|
||
recycledBlock.x = rightmostBlock.x + spacing;
|
||
recycledBlock.y = recycledState.y;
|
||
recycledBlock.scaleX = recycledState.scaleX;
|
||
recycledBlock.scaleY = recycledState.scaleY;
|
||
recycledBlock.opacity = 0;
|
||
const light = recycledBlock.getChildByName("light");
|
||
if (light) {
|
||
light.opacity = 0;
|
||
}
|
||
orderedBlocks.push(recycledBlock);
|
||
this.playCycle(loading, cat, orderedBlocks, state, runId);
|
||
})
|
||
.start();
|
||
}
|
||
|
||
private static moveBlocks(blocks: cc.Node[], spacing: number): void {
|
||
const lastIndex = blocks.length - 1;
|
||
blocks.forEach((block, index) => {
|
||
const tween = cc.tween(block);
|
||
if (index === lastIndex) {
|
||
tween.delay(0.06);
|
||
}
|
||
tween.to(0.47, {
|
||
x: block.x - spacing,
|
||
opacity: index === 0 ? 0 : 255,
|
||
}, { easing: "sineInOut" }).start();
|
||
});
|
||
}
|
||
|
||
private static flashBlock(block: cc.Node): void {
|
||
const light = block && block.getChildByName("light");
|
||
if (!light) {
|
||
return;
|
||
}
|
||
|
||
cc.Tween.stopAllByTarget(light);
|
||
light.opacity = 255;
|
||
light.scaleX = 1.04;
|
||
light.scaleY = 1.04;
|
||
cc.tween(light)
|
||
.to(0.10, { opacity: 220, scaleX: 1.12, scaleY: 1.12 }, { easing: "quadOut" })
|
||
.to(0.24, { opacity: 0, scaleX: 1, scaleY: 1 }, { easing: "sineOut" })
|
||
.start();
|
||
}
|
||
|
||
private static reset(cat: cc.Node, blocks: cc.Node[], state: LoadingAnimationState): void {
|
||
this.apply(cat, state.cat);
|
||
blocks.forEach((block, index) => {
|
||
this.apply(block, state.blocks[index]);
|
||
const light = block.getChildByName("light");
|
||
if (light) {
|
||
light.opacity = 0;
|
||
light.scaleX = 1;
|
||
light.scaleY = 1;
|
||
}
|
||
});
|
||
}
|
||
|
||
private static stopTargets(loading: cc.Node, cat: cc.Node, blocks: cc.Node[]): void {
|
||
const targets = [cat, loading.getChildByName("load")];
|
||
blocks.forEach(block => {
|
||
targets.push(block);
|
||
targets.push(block.getChildByName("light"));
|
||
});
|
||
targets.forEach(target => {
|
||
if (target) {
|
||
cc.Tween.stopAllByTarget(target);
|
||
target.stopAllActions();
|
||
}
|
||
});
|
||
}
|
||
|
||
private static capture(node: cc.Node): LoadingNodeState {
|
||
return {
|
||
x: node.x,
|
||
y: node.y,
|
||
scaleX: node.scaleX,
|
||
scaleY: node.scaleY,
|
||
opacity: node.opacity,
|
||
};
|
||
}
|
||
|
||
private static apply(node: cc.Node, state: LoadingNodeState): void {
|
||
node.x = state.x;
|
||
node.y = state.y;
|
||
node.scaleX = state.scaleX;
|
||
node.scaleY = state.scaleY;
|
||
node.opacity = state.opacity;
|
||
}
|
||
|
||
private static getBlockState(block: cc.Node, state: LoadingAnimationState): LoadingNodeState {
|
||
const index = Number(block.name.replace("block", "")) - 1;
|
||
return state.blocks[index] || state.blocks[0];
|
||
}
|
||
|
||
private static getState(loading: cc.Node): LoadingAnimationState {
|
||
return (loading as any)[STATE_KEY] || null;
|
||
}
|
||
|
||
private static isRunning(loading: cc.Node, state: LoadingAnimationState, runId: number): boolean {
|
||
return state.runId === runId && cc.isValid(loading) && loading.active;
|
||
}
|
||
}
|