MatchMaster/assets/Script/LoadingCatAnimation.ts
COMPUTER\EDY 63feac549d 更新
2026-08-13 15:28:06 +08:00

255 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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