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.
392 lines
12 KiB
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();
|
|
}
|
|
}
|
|
|