import { Node, Tween, tween, UIOpacity, Vec3 } from 'cc'; import { popExitScale, popExitYOffset, popImpactDropY, popImpactLaunch, popImpactLock, popImpactOpacity, popImpactRecoil, popImpactTailBounce, } from './PopAnimCurves'; import { adaptivePopDuration, AdaptivePopParams, computeAdaptivePopParams } from './PopAnimSizing'; import type { IPopAnimService } from './IPopAnimService'; import { IPopAnimSession, POP_MASK_OPACITY, PopCenterPreset, PopMaskPreset } from './PopAnimTypes'; type DoneFn = () => void; const CONTENT_DELAY_S = 0.02; interface CenterPose { x: number; y: number; z: number; angle: number; /** 预制体 design 缩放 X */ scaleX: number; /** 预制体 design 缩放 Y */ scaleY: number; } function ensureOpacity(node: Node): UIOpacity { return node.getComponent(UIOpacity) || node.addComponent(UIOpacity); } function stopNode(node: Node | null): void { if (node?.isValid) Tween.stopAllByTarget(node); } function applyEnterPose(center: Node, params: AdaptivePopParams, pose: CenterPose): void { center.setScale(0, 0, 1); center.setPosition(pose.x, pose.y + params.lift, pose.z); center.angle = pose.angle; ensureOpacity(center).opacity = params.opacityStart; } function applyEnterFrame( center: Node, params: AdaptivePopParams, pose: CenterPose, ratio: number, op: UIOpacity, ): void { const total = adaptivePopDuration(params); const elapsed = ratio * total; if (elapsed <= params.launchDur) { const t = elapsed / params.launchDur; const s = popImpactLaunch(t, params.peak); const yOff = popImpactDropY(t, params.lift); center.setScale(s * pose.scaleX, s * pose.scaleY, 1); center.setPosition(pose.x, pose.y + yOff, pose.z); op.opacity = popImpactOpacity(t, params.opacityStart); return; } const afterLaunch = elapsed - params.launchDur; if (afterLaunch <= params.recoilDur) { const t = afterLaunch / params.recoilDur; const s = popImpactRecoil(t, params.peak, params.undershoot); center.setScale(s * pose.scaleX, s * pose.scaleY, 1); center.setPosition(pose.x, pose.y, pose.z); op.opacity = 255; return; } const afterRecoil = afterLaunch - params.recoilDur; if (afterRecoil <= params.lockDur) { const t = afterRecoil / params.lockDur; const s = popImpactLock(t, params.undershoot); center.setScale(s * pose.scaleX, s * pose.scaleY, 1); center.setPosition(pose.x, pose.y, pose.z); op.opacity = 255; return; } if (params.jellyDur > 0) { const t = (afterRecoil - params.lockDur) / params.jellyDur; const { scale, yOff } = popImpactTailBounce(t, params.jellyAmp); center.setScale(scale * pose.scaleX, scale * pose.scaleY, 1); center.setPosition(pose.x, pose.y + yOff, pose.z); op.opacity = 255; return; } center.setScale(pose.scaleX, pose.scaleY, 1); center.setPosition(pose.x, pose.y, pose.z); op.opacity = 255; } /** 弹窗入场/退场动效实现 */ export class PopAnimPlayer implements IPopAnimService { private readonly _poseMap = new WeakMap(); resolveMaskNode(root: Node): Node | null { return root.getChildByName('bgMask') || root.getChildByName('ui_bgMask') || root.getChildByName('ui_mask'); } resolveCenterNode(root: Node): Node | null { return root.getChildByName('center') || root.getChildByName('ui_center'); } snapCenterOpen(root: Node): void { const c = this.resolveCenterNode(root); if (!c?.isValid) return; Tween.stopAllByTarget(c); this._resetCenter(c); } prepareOpen(root: Node, mask: PopMaskPreset, center: PopCenterPreset): void { this._prepareMask(root, mask); if (center === 'none') return; const c = this.resolveCenterNode(root); if (!c) return; const params = computeAdaptivePopParams(c, center); if (params) { applyEnterPose(c, params, this._ensureHomePose(c)); return; } const pose = this._ensureHomePose(c); if (center === 'fadeUp') { c.setPosition(pose.x, pose.y - 32, pose.z); ensureOpacity(c).opacity = 0; return; } if (center === 'sheet') { c.setScale(pose.scaleX, pose.scaleY, 1); c.setPosition(pose.x, pose.y - 420, pose.z); ensureOpacity(c).opacity = 0; } } openMask(root: Node, preset: PopMaskPreset): boolean { if (preset === 'none') return false; this._prepareMask(root, preset); if (preset === 'root') { const o = ensureOpacity(root); stopNode(root); tween(o).to(0.18, { opacity: 255 }).start(); return true; } const m = this.resolveMaskNode(root); if (!m) return false; stopNode(m); const o = ensureOpacity(m); const dur = preset === 'dimFast' ? 0.14 : 0.18; tween(o).to(dur, { opacity: POP_MASK_OPACITY }).start(); return true; } closeMask(root: Node, preset: PopMaskPreset, done?: DoneFn): void { if (preset === 'none') { done?.(); return; } if (preset === 'root') { const o = root.getComponent(UIOpacity); if (o) { tween(o).to(0.12, { opacity: 0 }).call(() => done?.()).start(); return; } done?.(); return; } const m = this.resolveMaskNode(root); if (!m) { done?.(); return; } const o = m.getComponent(UIOpacity) || m.addComponent(UIOpacity); tween(o).to(0.12, { opacity: 0 }).call(() => done?.()).start(); } openCenter(center: Node, preset: PopCenterPreset, done?: DoneFn): boolean { if (preset === 'none') { done?.(); return false; } stopNode(center); if (preset === 'scale') { this._playImpactEnter(center, 'scale', done); return true; } if (preset === 'bounce') { this._playImpactEnter(center, 'bounce', done); return true; } if (preset === 'fadeUp') { this._openFadeUp(center, done); return true; } if (preset === 'sheet') { this._openSheet(center, done); return true; } done?.(); return false; } closeCenter(center: Node, preset: PopCenterPreset, done?: DoneFn): boolean { if (preset === 'none') { done?.(); return false; } stopNode(center); if (preset === 'scale' || preset === 'bounce') { this._playExit(center, preset === 'bounce' ? 0.16 : 0.14, done); return true; } if (preset === 'fadeUp' || preset === 'sheet') { const pose = this._ensureHomePose(center); const op = ensureOpacity(center); tween(center) .parallel( tween(center).to(0.14, { position: new Vec3(pose.x, pose.y - 16, pose.z) }), tween(op).to(0.12, { opacity: 0 }), ) .call(() => { this._resetCenter(center); op.opacity = 255; done?.(); }) .start(); return true; } done?.(); return false; } playOpen(root: Node, mask: PopMaskPreset, center: PopCenterPreset, done?: DoneFn): IPopAnimSession { this.prepareOpen(root, mask, center); this.openMask(root, mask); const c = this.resolveCenterNode(root); if (c) this.openCenter(c, center, done); else done?.(); return { mask, center }; } playClose(root: Node, session: IPopAnimSession, tweenType: 1 | 2 | 3, done: DoneFn): void { if (tweenType === 1) { done(); return; } if (tweenType === 3) { const c = this.resolveCenterNode(root); if (c && session.center !== 'none') { this.closeMask(root, session.mask); this.closeCenter(c, session.center, done); return; } const o = root.getComponent(UIOpacity) || root.addComponent(UIOpacity); tween(o).to(0.12, { opacity: 0 }).call(done).start(); return; } this.closeMask(root, session.mask); const c = this.resolveCenterNode(root); if (c && session.center !== 'none') { this.closeCenter(c, session.center, done); } else { done(); } } private _ensureHomePose(center: Node): CenterPose { let pose = this._poseMap.get(center); if (!pose) { const p = center.position; const s = center.scale; pose = { x: p.x, y: p.y, z: p.z, angle: center.angle, scaleX: s.x, scaleY: s.y, }; this._poseMap.set(center, pose); } return pose; } private _resetCenter(center: Node): void { const pose = this._poseMap.get(center); if (pose) { center.setPosition(pose.x, pose.y, pose.z); center.angle = pose.angle; center.setScale(pose.scaleX, pose.scaleY, 1); } else { center.setScale(Vec3.ONE); } const op = center.getComponent(UIOpacity); if (op) op.opacity = 255; } private _prepareMask(root: Node, preset: PopMaskPreset): void { if (preset === 'none') return; if (preset === 'root') { ensureOpacity(root).opacity = 0; return; } const m = this.resolveMaskNode(root); if (m) ensureOpacity(m).opacity = 0; } private _playImpactEnter(center: Node, preset: PopCenterPreset, done?: DoneFn): void { const params = computeAdaptivePopParams(center, preset)!; const pose = this._ensureHomePose(center); const op = ensureOpacity(center); applyEnterPose(center, params, pose); Tween.stopAllByTarget(center); Tween.stopAllByTarget(op); tween(center) .delay(CONTENT_DELAY_S) .to(adaptivePopDuration(params), {}, { easing: 'linear', onUpdate: (_target: Node, ratio: number) => { applyEnterFrame(center, params, pose, ratio ?? 0, op); }, }) .call(() => { this._resetCenter(center); done?.(); }) .start(); } private _openFadeUp(center: Node, done?: DoneFn): void { const pose = this._ensureHomePose(center); const op = ensureOpacity(center); op.opacity = 0; center.setPosition(pose.x, pose.y - 32, pose.z); tween(center) .delay(CONTENT_DELAY_S) .parallel( tween(center).to(0.22, { position: new Vec3(pose.x, pose.y, pose.z) }, { easing: 'backOut' }), tween(op).to(0.18, { opacity: 255 }), ) .call(() => done?.()) .start(); } private _openSheet(center: Node, done?: DoneFn): void { const pose = this._ensureHomePose(center); const op = ensureOpacity(center); op.opacity = 0; center.setScale(pose.scaleX, pose.scaleY, 1); center.setPosition(pose.x, pose.y - 420, pose.z); tween(center) .parallel( tween(center).to(0.26, { position: new Vec3(pose.x, pose.y, pose.z) }, { easing: 'backOut' }), tween(op).to(0.18, { opacity: 255 }), ) .call(() => done?.()) .start(); } private _playExit(center: Node, duration: number, done?: DoneFn): void { const pose = this._ensureHomePose(center); Tween.stopAllByTarget(center); tween(center) .to(duration, {}, { easing: 'linear', onUpdate: (_target: Node, ratio: number) => { const t = ratio ?? 0; const s = popExitScale(t); center.setScale(s * pose.scaleX, s * pose.scaleY, 1); center.setPosition(pose.x, pose.y - popExitYOffset(t), pose.z); }, }) .call(() => { this._resetCenter(center); done?.(); }) .start(); } }