import { Label, Node, Sprite, Tween, tween, UIOpacity, UITransform, v2, v3, Vec3, view, Widget } from "cc"; import { ch } from "../../ch/ch"; //单个节点进场动画,退场动画 export function toggleSlideOutAndBack(t: Node, offset: Vec3, duration: number = 0.5, onFinish?: () => void) { if (!t['__originPos']) { t['__originPos'] = t.position.clone(); // 初始位置 t['__isHidden'] = false; } const originPos: Vec3 = t['__originPos']; if (t['__targetHidden'] === undefined) { t['__targetHidden'] = t['__isHidden']; } const isHidden = t['__isHidden']; const targetHidden = !t['__targetHidden']; // 翻转目标 t['__targetHidden'] = targetHidden; // 标记目标 const targetPos = targetHidden ? originPos.clone().add(offset) : originPos.clone(); // 停止旧动画(彻底打断) if (t['__slideTween']) { t['__slideTween'].stop(); t['__slideTween'] = null; } const currentPos = t.position.clone(); const posDelta = targetPos.clone().subtract(currentPos).length(); const isReversing = posDelta > 1 && isHidden !== targetHidden; const actualDuration = isReversing ? duration * 0.4 : duration; const tweenInst = tween(t) .to(actualDuration, { position: new Vec3(targetPos) }, { easing: "quadInOut" }) .call(() => { // 动画未被中断才更新最终状态 if (t['__targetHidden'] === targetHidden) { t['__isHidden'] = targetHidden; t['__slideTween'] = null; onFinish?.(); } }); t['__slideTween'] = tweenInst; tweenInst.start(); } //节点下所有子节点进场动画,退场动画 export function toggleSlideOutAndBack_Nodes(t: Node, offset: Vec3, duration: number = 0.5, onFinish?: () => void) { const children = t.children; if (children.length === 0) { onFinish?.(); return; } let completedCount = 0; children.forEach((node) => { toggleSlideOutAndBack(node, offset, duration, () => { completedCount++; if (completedCount === children.length) { onFinish?.(); } }); }); } //向上滑动 export function toggleSlideInAndOut(t: Node, duration: number = 0.5, onFinish?: () => void) { if (!t['__originPos']) { t['__originPos'] = t.position.clone(); } const originPos: Vec3 = t['__originPos']; const showPos = originPos.clone().add(new Vec3(0, 1334, 0));//可优化 let uiOpacity = t.getComponent(UIOpacity); if (!uiOpacity) { uiOpacity = t.addComponent(UIOpacity); uiOpacity.opacity = 0; } // 初始化状态记录 if (t['__isHidden'] === undefined) { t['__isHidden'] = true; } if (t['__targetHidden'] === undefined) { t['__targetHidden'] = t['__isHidden']; } // 计算新的目标状态(翻转) const nextTargetHidden = !t['__targetHidden']; t['__targetHidden'] = nextTargetHidden; const targetPos = nextTargetHidden ? originPos : showPos; const targetOpacity = nextTargetHidden ? 0 : 255; // 当前状态 const currentPos = t.position.clone(); const currentOpacity = uiOpacity.opacity; const posDiff = targetPos.clone().subtract(currentPos).length(); const isMidTween = currentOpacity > 0 && currentOpacity < 255 && posDiff > 0; const actualDuration = isMidTween ? duration * 0.4 : duration; // 停止旧动画 Tween.stopAllByTarget(t); Tween.stopAllByTarget(uiOpacity); tween(t) .parallel( tween(t).to(actualDuration, { position: targetPos.clone() }, { easing: 'quadInOut' }), tween(uiOpacity).to(actualDuration, { opacity: targetOpacity }, { easing: 'quadInOut' }) ) .call(() => { // 只有目标状态未被再次更改时,才确认最终状态 if (t['__targetHidden'] === nextTargetHidden) { t['__isHidden'] = nextTargetHidden; onFinish?.(); } }) .start(); } //呼吸动画 export function toggleBreath(t: Node, duration: number = 0.5, onFinish?: () => void) { if (!t['__originalScale']) { t['__originalScale'] = t.scale.clone(); // 缓存初始缩放 } const originalScale: Vec3 = t['__originalScale']; //停止之前的 tween,并还原缩放 if (t['__breathTween']) { t['__breathTween'].stop(); t.scale = originalScale.clone(); //强制还原到初始scale } const scaleUp = originalScale.clone().multiplyScalar(1.1); const scaleDown = originalScale; const breathTween = tween(t) .repeatForever( tween(t) .to(duration, { scale: scaleUp }, { easing: 'sineInOut' }) .to(duration, { scale: scaleDown }, { easing: 'sineInOut' }) .call(() => { console.log('breath'); if (onFinish) onFinish(); }) ); t['__breathTween'] = breathTween; breathTween.start(); } //提醒动画 export function toggleRemind(t: Node, duration: number = 0.2, delay: number = 1.5, onFinish?: () => void) { if (!t['__originScale']) { t['__originScale'] = t.scale.clone(); // 记录初始缩放 } if (t['__remindTween']) { t['__remindTween'].stop(); } const originalScale = t['__originScale']; const scaleUp = originalScale.clone().multiplyScalar(1.2); const remindTween = tween(t) .repeatForever( tween() .to(duration, { scale: scaleUp }, { easing: 'sineOut' }) .to(duration, { scale: originalScale }, { easing: 'sineIn' }) .delay(delay) .call(() => { if (onFinish) onFinish(); }) ); t['__remindTween'] = remindTween; remindTween.start(); } //平移至指定点位动画 export function toggleMoveTo(t: Node, toPos?: Vec3, duration: number = 0.5, onFinish?: () => void) { if (!t['__originPos']) { t['__originPos'] = t.position.clone(); } if (t['__moveTween']) { t['__moveTween'].stop(); t['__moveTween'] = null; } // 没传目标位置时表示还原 const targetPos = toPos ? toPos : t['__originPos']; const moveTween = tween(t) .to(duration, { position: targetPos }, { easing: 'quadOut' }) .call(() => { if (onFinish) onFinish(); }); t['__moveTween'] = moveTween; moveTween.start(); } // 根据指定位移距离进行平移 export function toggleMoveBy(t: Node, offset: Vec3, duration: number = 0.5, onFinish?: () => void) { if (!t['__originPos']) { t['__originPos'] = t.position.clone(); } if (t['__moveTween']) { t['__moveTween'].stop(); t['__moveTween'] = null; } const originPos: Vec3 = t['__originPos']; const targetPos = t.position.equals(originPos) ? originPos.clone().add(offset) : originPos; t.position = targetPos; const moveTween = tween(t) .to(duration, { position: targetPos }, { easing: "quadInOut" }) .call(() => { if (onFinish) onFinish(); }); t['__moveTween'] = moveTween; moveTween.start(); } //图片填充动画 export function fillImage(t: Node, duration: number, start: number, target: number, onFinish?: () => {}) { Tween.stopAllByTarget(t); let sprite = t.getComponent(Sprite); sprite.fillRange = start; tween(sprite) .to(duration, { fillRange: target }, { easing: 'quadOut' }).call(onFinish) .start(); } //数字过渡动画 export function number_fraction_ani(t: Node, duration: number, startNumerator: number, endNumerator: number, denominator: number, onFinish?: () => void) { Tween.stopAllByTarget(t); const label = t.getComponent(Label); if (!label) return; const obj = { value: startNumerator }; tween(obj) .to(duration, { value: endNumerator }, { easing: 'quadOut', onUpdate: () => { const numerator = Math.floor(obj.value); label.string = `${numerator}/${denominator}`; } }) .call(() => { label.string = `${endNumerator}/${denominator}`; onFinish?.(); }) .start(); } //弹窗动画 export function ani_ui(node: Node, end: number = 1.0): void { Tween.stopAllByTarget(node); // 打断旧动画 //重置scale node.setScale(0.5, 0.5, 1); tween(node) .to(1.2, { scale: v3(end, end, 1) }, { easing: 'elasticOut' }) .start(); } //淡入淡出动画 export function fade_anim(node: Node, duration: number = 0.2, curOpacity: number = 0, targetOpacity: number = 255, delay: number = 0, onFinish?: () => void) { let uiOpacity = node.getComponent(UIOpacity); if (!uiOpacity) { node.addComponent(UIOpacity).opacity = curOpacity; uiOpacity = node.getComponent(UIOpacity); } tween(uiOpacity).to(duration, { opacity: targetOpacity }).delay(delay).call(onFinish).start(); } //放大缩小动画 export function scale_anim(node: Node, duration: number = 0.2, curScale: Vec3 = new Vec3(0, 0, 0), targetScale: Vec3 = new Vec3(1, 1, 1), delay: number = 0, onFinish?: () => void, loop: boolean = false, backDuration: number = 0.1) { Tween.stopAllByTarget(node); // 平滑缩回初始 scale tween(node) .to(backDuration, { scale: curScale }) .call(() => { const anim = tween(node).to(duration, { scale: targetScale }).delay(delay); if (loop) { anim .to(duration, { scale: curScale }) .union() .repeatForever() .start(); } else { anim.call(() => { if (onFinish) onFinish(); }).start(); } }) .start(); } export type RotateAnimType = 'once' | 'loop' | 'swing'; export interface RotateAnimOptions { duration?: number; angle?: number; delay?: number; onFinish?: () => void; swingCount?: number; direction?: 'y' | 'x' | 'z'; } // 旋转动画 export function rotate_anim_ext(node: Node, type: RotateAnimType, options: RotateAnimOptions = {}) { const { duration = 3.5, angle = 30, delay = 0, onFinish, swingCount = 3, direction = 'z', } = options; const getAngleVec = (value: number): Vec3 => { switch (direction) { case 'x': return new Vec3(value, 0, 0); case 'z': return new Vec3(0, 0, value); case 'y': default: return new Vec3(0, value, 0); } }; //停止旧动画 if ((node as any).__rotate_tween) { (node as any).__rotate_tween.stop(); } //保存原始角度 (node as any).__rotate_origin = node.eulerAngles.clone(); let rotateTween: Tween; switch (type) { case 'once': { const original = node.eulerAngles.clone(); const offset = getAngleVec(angle); // 例如 Vec3(0, 15, 0) const left = original.clone().subtract(offset); const right = original.clone().add(offset); const swingCount = 3; // 摆动次数(一次来回为一次) const singleSwingDuration = duration / (swingCount * 2); // 一次半摆(左或右)用的时间 let t = tween(node); for (let i = 0; i < swingCount; i++) { t = t .to(singleSwingDuration, { eulerAngles: left }, { easing: 'sineInOut' }) .to(singleSwingDuration, { eulerAngles: right }, { easing: 'sineInOut' }); } // 最后回中 t = t .to(singleSwingDuration, { eulerAngles: original }, { easing: 'sineOut' }) .delay(delay) .call(onFinish); rotateTween = t; break; } case 'loop': { const delta: any = {}; switch (direction) { case 'x': delta.eulerAngles = { x: angle }; break; case 'z': delta.eulerAngles = { z: angle }; break; case 'y': default: delta.eulerAngles = { y: angle }; break; } rotateTween = tween(node) .delay(delay) .by(duration, delta) .repeatForever(); break; } case 'swing': { const original = node.eulerAngles.clone(); const left = original.clone().subtract(getAngleVec(angle)); const right = original.clone().add(getAngleVec(angle)); let swing = tween(node); for (let i = 0; i < swingCount; i++) { swing = swing .to(duration / 2, { eulerAngles: right }, { easing: 'sineOut' }) .to(duration / 2, { eulerAngles: left }, { easing: 'sineIn' }); } swing = swing .to(duration / 2, { eulerAngles: original }, { easing: 'sineOut' }) .call(onFinish); rotateTween = tween(node) .delay(delay) .then(swing); break; } } //启动动画并保存引用 (node as any).__rotate_tween = rotateTween; rotateTween.start(); } //停止旋转动画(不还原) export function rotate_anim_stop(node: Node) { const t = (node as any).__rotate_tween; if (t) { t.stop(); (node as any).__rotate_tween = null; } } //还原旋转角度(平滑返回初始值) export function rotate_anim_restore(node: Node, duration: number = 0.3) { const origin = (node as any).__rotate_origin as Vec3; if (!origin) return; rotate_anim_stop(node); const backTween = tween(node) .to(duration, { eulerAngles: origin }, { easing: 'quadOut' }); (node as any).__rotate_tween = backTween; backTween.start(); } //上下抖动动画 export interface BounceYOptions { distance?: number; // 向上抖动的距离(默认20) durationMin?: number; // 最短时长(默认 0.3) durationMax?: number; // 最长时长(默认 0.5) delay?: number; // 延迟(默认0) onFinish?: () => void; // 动画完成回调 } export function bounce_y_anim(node: Node, options: BounceYOptions = {}) { const { distance = 20, durationMin = 0.3, durationMax = 0.5, delay = 0, onFinish, } = options; //在 min~max 范围内生成随机时长 const duration = ch.util.getRandom(0.2, 0.35); const originalPos = node.position.clone(); const upPos = originalPos.clone().add(new Vec3(0, distance, 0)); tween(node) .delay(delay) .to(duration, { position: upPos }, { easing: 'quadOut' }) .to(duration, { position: originalPos }, { easing: 'quadIn' }) .call(onFinish) .start(); } export interface JellyAnimOptions { scaleX?: number; scaleY?: number; duration?: number; delay?: number; onFinish?: () => void; loop?: boolean; } export function jelly_anim_ext(node: Node, options: JellyAnimOptions = {}) { const { scaleX = 1.2, scaleY = 0.8, duration = 0.4, delay = 0.5, onFinish, loop = true, } = options; // 停止旧动画 if ((node as any).__jelly_tween) { (node as any).__jelly_tween.stop(); } // 保存原始 scale const original = node.scale.clone(); (node as any).__jelly_origin = original; const stretch = new Vec3(scaleX, scaleY, original.z); const fullTween = tween(node).sequence( tween(node).to(duration / 2, { scale: stretch }, { easing: 'sineOut' }), tween(node).to(duration / 2, { scale: original }, { easing: 'backOut' }), tween(node).delay(delay) ); let finalTween: Tween; if (loop) { finalTween = fullTween.repeatForever(); } else { finalTween = tween(node) .then(fullTween) .call(onFinish || (() => { })); } (node as any).__jelly_tween = finalTween; finalTween.start(); } //直接打断 export function jelly_anim_stop(node: Node) { const t = (node as any).__jelly_tween; if (t) { t.stop(); (node as any).__jelly_tween = null; } } //打断且复原 export function jelly_anim_restore(node: Node, duration: number = 0.3) { const origin = (node as any).__jelly_origin as Vec3; if (!origin) return; jelly_anim_stop(node); const backTween = tween(node).to(duration, { scale: origin }, { easing: 'quadOut' }); (node as any).__jelly_tween = backTween; backTween.start(); } /** * 贝塞尔加缩放 * @param tweenObj * @param target * @param startPos * @param targetPos * @param time * @param scale * @param px1 * @param py1 * @param px2 * @param py2 * @returns */ export function targetScaleBezier(tweenObj: any, target: Node, startPos: Vec3, targetPos: Vec3, time: number, scale: number, px1: number, py1: number, px2: number, py2: number, delay: number, onHalf?: () => void, onFinish?: () => void) { let t1 = tween(tweenObj).to(time, { t: 1 }, { onUpdate: (tar, t) => { const p0 = v2(0, 0); const p1 = v2(px1, py1); const p2 = v2(px2, py2); const p3 = v2(1, 1); const x = p0.x * (1 - t) * (1 - t) * (1 - t) + 3 * p1.x * t * (1 - t) * (1 - t) + 3 * p2.x * t * t * (1 - t) + p3.x * t * t * t; const y = p0.y * (1 - t) * (1 - t) * (1 - t) + 3 * p1.y * t * (1 - t) * (1 - t) + 3 * p2.y * t * t * (1 - t) + p3.y * t * t * t; let posx = (targetPos.x - startPos.x) * x + startPos.x; let posy = (targetPos.y - startPos.y) * y + startPos.y; target.setPosition(new Vec3(posx, posy, 0)); } }); let t2 = tween(tweenObj).to(time, { scale: scale }, { onUpdate: (tar: any, t) => { target.setScale(tar.scale, tar.scale, tar.scale); } }); tween(tweenObj).parallel(t1, t2).call(onHalf).delay(delay).call(onFinish).start(); }