MatchMaster/assets/Script/ActivityPopupAnimator.ts

247 lines
7.6 KiB
TypeScript

interface ActivityPopupAnimationState {
x: number;
y: number;
scaleX: number;
scaleY: number;
opacity: number;
mask: cc.Node;
maskOpacity: number;
}
const ANIMATION_STATE_KEY = "__activityPopupAnimationState";
export enum ActivityPopupAnimationType {
"左侧滑入" = 0,
"Q弹弹出" = 1
}
/**
* Common enter/exit animation for activity popups.
* Only the content panel slides and scales; the full-screen mask fades separately.
*/
export default class ActivityPopupAnimator {
public static open(
content: cc.Node,
mask?: cc.Node,
animationType: ActivityPopupAnimationType = ActivityPopupAnimationType.左侧滑入,
onComplete?: () => void
): void {
if (!content || !cc.isValid(content, true)) {
if (onComplete) onComplete();
return;
}
const state = this.getState(content, mask);
content.stopAllActions();
content.active = true;
this.openMask(mask, state.maskOpacity);
if (animationType === ActivityPopupAnimationType.Q弹弹出) {
this.openElastic(content, state, onComplete);
} else {
this.openFromLeft(content, state, onComplete);
}
}
public static close(
content: cc.Node,
mask?: cc.Node,
animationType: ActivityPopupAnimationType = ActivityPopupAnimationType.左侧滑入,
onComplete?: () => void
): void {
if (!content || !cc.isValid(content, true)) {
if (onComplete) onComplete();
return;
}
const state = this.getState(content, mask);
content.stopAllActions();
this.closeMask(mask);
if (animationType === ActivityPopupAnimationType.Q弹弹出) {
this.closeElastic(content, mask, state, onComplete);
} else {
this.closeToLeft(content, mask, state, onComplete);
}
}
private static openFromLeft(
content: cc.Node,
state: ActivityPopupAnimationState,
onComplete?: () => void
): void {
const startX = state.x - this.getSlideDistance(content);
content.setPosition(startX, state.y);
content.scaleX = state.scaleX * 0.72;
content.scaleY = state.scaleY * 0.72;
content.opacity = 0;
cc.tween(content)
.to(0.24, {
x: state.x,
y: state.y,
scaleX: state.scaleX * 0.94,
scaleY: state.scaleY * 0.94,
opacity: state.opacity
}, { easing: "cubicOut" })
.to(0.10, {
scaleX: state.scaleX * 1.06,
scaleY: state.scaleY * 1.06
}, { easing: "backOut" })
.to(0.12, {
scaleX: state.scaleX,
scaleY: state.scaleY
}, { easing: "sineOut" })
.call(() => {
if (onComplete) onComplete();
})
.start();
}
private static openElastic(
content: cc.Node,
state: ActivityPopupAnimationState,
onComplete?: () => void
): void {
content.setPosition(state.x, state.y);
content.scaleX = state.scaleX * 0.36;
content.scaleY = state.scaleY * 0.36;
content.opacity = 0;
cc.tween(content)
.to(0.18, {
scaleX: state.scaleX * 1.12,
scaleY: state.scaleY * 0.88,
opacity: state.opacity
}, { easing: "backOut" })
.to(0.09, {
scaleX: state.scaleX * 0.94,
scaleY: state.scaleY * 1.08
}, { easing: "sineOut" })
.to(0.08, {
scaleX: state.scaleX * 1.04,
scaleY: state.scaleY * 0.97
}, { easing: "sineInOut" })
.to(0.08, {
scaleX: state.scaleX,
scaleY: state.scaleY
}, { easing: "sineOut" })
.call(() => {
if (onComplete) onComplete();
})
.start();
}
private static closeToLeft(
content: cc.Node,
mask: cc.Node,
state: ActivityPopupAnimationState,
onComplete?: () => void
): void {
const endX = state.x - this.getSlideDistance(content);
cc.tween(content)
.to(0.08, {
scaleX: state.scaleX * 1.06,
scaleY: state.scaleY * 1.06
}, { easing: "sineOut" })
.to(0.22, {
x: endX,
scaleX: state.scaleX * 0.72,
scaleY: state.scaleY * 0.72,
opacity: 0
}, { easing: "cubicIn" })
.call(() => this.finishClose(content, mask, state, onComplete))
.start();
}
private static closeElastic(
content: cc.Node,
mask: cc.Node,
state: ActivityPopupAnimationState,
onComplete?: () => void
): void {
cc.tween(content)
.to(0.07, {
scaleX: state.scaleX * 0.94,
scaleY: state.scaleY * 1.06
}, { easing: "sineOut" })
.to(0.08, {
scaleX: state.scaleX * 1.08,
scaleY: state.scaleY * 0.90
}, { easing: "sineInOut" })
.to(0.15, {
scaleX: state.scaleX * 0.36,
scaleY: state.scaleY * 0.36,
opacity: 0
}, { easing: "backIn" })
.call(() => this.finishClose(content, mask, state, onComplete))
.start();
}
private static openMask(mask: cc.Node, opacity: number): void {
if (!mask || !cc.isValid(mask, true)) return;
mask.stopAllActions();
mask.active = true;
mask.opacity = 0;
cc.tween(mask)
.to(0.24, { opacity }, { easing: "sineOut" })
.start();
}
private static closeMask(mask: cc.Node): void {
if (!mask || !cc.isValid(mask, true)) return;
mask.stopAllActions();
cc.tween(mask)
.to(0.30, { opacity: 0 }, { easing: "sineIn" })
.start();
}
private static finishClose(
content: cc.Node,
mask: cc.Node,
state: ActivityPopupAnimationState,
onComplete?: () => void
): void {
this.restore(content, state);
content.active = false;
if (mask && cc.isValid(mask, true)) {
mask.stopAllActions();
mask.opacity = 0;
mask.active = false;
}
if (onComplete) onComplete();
}
private static getState(content: cc.Node, mask?: cc.Node): ActivityPopupAnimationState {
let state = (content as any)[ANIMATION_STATE_KEY] as ActivityPopupAnimationState;
if (!state) {
state = {
x: content.x,
y: content.y,
scaleX: content.scaleX,
scaleY: content.scaleY,
opacity: content.opacity,
mask: mask || null,
maskOpacity: mask ? mask.opacity : 0
};
(content as any)[ANIMATION_STATE_KEY] = state;
} else if (mask && state.mask !== mask) {
state.mask = mask;
state.maskOpacity = mask.opacity;
}
return state;
}
private static getSlideDistance(content: cc.Node): number {
const parentWidth = content.parent ? content.parent.width : 0;
return Math.max(content.width, parentWidth * 0.92);
}
private static restore(content: cc.Node, state: ActivityPopupAnimationState): void {
content.setPosition(state.x, state.y);
content.scaleX = state.scaleX;
content.scaleY = state.scaleY;
content.opacity = state.opacity;
}
}