You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/mx/module/ui/popAnim/PopAnimPlayer.ts

392 lines
12 KiB

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<Node, CenterPose>();
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();
}
}