cb/assets/Script/module/Tool/GenieEffect.ts

367 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Genie Effect 动画工具
* 使用 RenderTexture 捕获整个节点,然后应用 Mac 风格最小化效果
* Cocos Creator 2.4.15
*/
const { ccclass, property } = cc._decorator;
@ccclass
export default class GenieEffect extends cc.Component {
@property(cc.Material)
genieMaterial: cc.Material = null;
@property
duration: number = 0.4;
@property
bendStrength: number = 0.5;
private _renderTexture: cc.RenderTexture = null;
private _captureSprite: cc.Sprite = null;
private _isPlaying: boolean = false;
onLoad() {
// 材质可以通过编辑器设置或动态传入
}
/**
* 捕获整个节点为 RenderTexture
*/
captureNode(): cc.SpriteFrame {
const node = this.node;
const size = node.getContentSize();
// 创建 RenderTexture
const renderTexture = new cc.RenderTexture();
renderTexture.initWithSize(size.width, size.height);
// 创建相机
const cameraNode = new cc.Node('CaptureCamera');
cameraNode.parent = node;
const camera = cameraNode.addComponent(cc.Camera);
// 设置相机
camera.targetTexture = renderTexture;
camera.clearFlags = cc.Camera.ClearFlags.COLOR | cc.Camera.ClearFlags.DEPTH;
camera.backgroundColor = cc.color(0, 0, 0, 0);
camera.zoomRatio = 1;
camera.cullingMask = 0xffffffff;
// 渲染一帧
camera.render();
// 销毁相机节点
cameraNode.destroy();
// 创建 SpriteFrame
const spriteFrame = new cc.SpriteFrame(renderTexture);
this._renderTexture = renderTexture;
return spriteFrame;
}
/**
* 创建捕获用的 Sprite 节点
*/
createCaptureSprite(): cc.Node {
// 捕获节点画面
const spriteFrame = this.captureNode();
// 创建 Sprite 节点
const spriteNode = new cc.Node('GenieCaptureSprite');
spriteNode.parent = this.node.parent;
spriteNode.setContentSize(this.node.getContentSize());
spriteNode.position = this.node.position;
spriteNode.anchorX = this.node.anchorX;
spriteNode.anchorY = this.node.anchorY;
// 添加 Sprite 组件
const sprite = spriteNode.addComponent(cc.Sprite);
sprite.spriteFrame = spriteFrame;
// 隐藏原节点
this.node.active = false;
this._captureSprite = sprite;
return spriteNode;
}
/**
* 播放关闭动画(吸入到目标点)
* @param targetNode 目标节点(吸入位置)
* @param callback 动画完成回调
*/
playClose(targetNode: cc.Node, callback?: Function) {
if (this._isPlaying) {
console.warn('GenieEffect: 动画正在播放中');
return;
}
if (!this.genieMaterial) {
console.warn('GenieEffect: 材质未加载,使用默认动画');
this.playDefaultClose(targetNode, callback);
return;
}
// 创建捕获的 Sprite 节点
const captureNode = this.createCaptureSprite();
if (!captureNode) {
console.warn('GenieEffect: 无法创建捕获节点');
this.playDefaultClose(targetNode, callback);
return;
}
this._isPlaying = true;
// 获取 Sprite 组件
const sprite = captureNode.getComponent(cc.Sprite);
// 获取起始位置和目标位置(世界坐标)
const startWorldPos = this.node.convertToWorldSpaceAR(cc.v2(0, 0));
const targetWorldPos = targetNode.convertToWorldSpaceAR(cc.v2(0, 0));
// 设置材质参数
const material = this.genieMaterial;
material.setProperty('u_targetPos', [targetWorldPos.x, targetWorldPos.y]);
material.setProperty('u_progress', 0);
material.setProperty('u_bendStrength', this.bendStrength);
material.setProperty('u_shrinkWidth', 0.2);
// 应用材质
sprite.setMaterial(0, material);
// 播放动画 - 同时移动节点和更新 Shader
const animData = { progress: 0 };
const startPos = captureNode.position;
const targetPos = captureNode.parent.convertToNodeSpaceAR(targetWorldPos);
cc.tween(animData)
.to(this.duration, { progress: 1 }, {
easing: 'quadInOut',
onUpdate: (target, ratio) => {
if (material && sprite) {
material.setProperty('u_progress', animData.progress);
}
// 同时移动捕获节点向目标点靠近
if (captureNode) {
const t = animData.progress;
// 使用缓动函数让移动更自然
const easeT = t * t * (3 - 2 * t); // smoothstep
captureNode.x = startPos.x + (targetPos.x - startPos.x) * easeT;
captureNode.y = startPos.y + (targetPos.y - startPos.y) * easeT;
}
}
})
.call(() => {
this._isPlaying = false;
// 销毁捕获节点
if (captureNode) {
captureNode.destroy();
}
if (callback) callback();
})
.start();
}
/**
* 播放打开动画(从目标点展开)
* @param fromNode 起始节点(展开位置)
* @param callback 动画完成回调
*/
playOpen(fromNode: cc.Node, callback?: Function) {
if (this._isPlaying) {
console.warn('GenieEffect: 动画正在播放中');
return;
}
if (!this.genieMaterial) {
console.warn('GenieEffect: 材质未加载,使用默认动画');
this.playDefaultOpen(fromNode, callback);
return;
}
// 创建捕获的 Sprite 节点
const captureNode = this.createCaptureSprite();
if (!captureNode) {
console.warn('GenieEffect: 无法创建捕获节点');
this.playDefaultOpen(fromNode, callback);
return;
}
this._isPlaying = true;
// 获取 Sprite 组件
const sprite = captureNode.getComponent(cc.Sprite);
// 计算起始位置(世界坐标)
const fromWorldPos = fromNode.convertToWorldSpaceAR(cc.v2(0, 0));
// 设置材质参数(从完全吸入状态开始)
const material = this.genieMaterial;
material.setProperty('u_targetPos', [fromWorldPos.x, fromWorldPos.y]);
material.setProperty('u_progress', 1);
material.setProperty('u_bendStrength', this.bendStrength);
material.setProperty('u_shrinkWidth', 0.2);
// 应用材质
sprite.setMaterial(0, material);
// 播放动画progress 从 1 到 0
const animData = { progress: 1 };
cc.tween(animData)
.to(this.duration, { progress: 0 }, {
easing: 'quadOut',
onUpdate: (target, ratio) => {
if (material && sprite) {
material.setProperty('u_progress', animData.progress);
}
}
})
.call(() => {
this._isPlaying = false;
// 显示原节点
this.node.active = true;
// 销毁捕获节点
if (captureNode) {
captureNode.destroy();
}
if (callback) callback();
})
.start();
}
/**
* 默认关闭动画(当 Shader 不可用时使用)
*/
private playDefaultClose(targetNode: cc.Node, callback?: Function) {
const startPos = this.node.position; const targetWorldPos = targetNode.convertToWorldSpaceAR(cc.v2(0, 0));
const targetLocalPos = this.node.parent.convertToNodeSpaceAR(targetWorldPos);
cc.tween(this.node)
.to(this.duration * 0.5, {
position: cc.v3(
startPos.x + (targetLocalPos.x - startPos.x) * 0.5,
startPos.y + (targetLocalPos.y - startPos.y) * 0.5,
0
),
scale: 0.6
}, { easing: 'quadIn' })
.to(this.duration * 0.5, {
position: cc.v3(targetLocalPos.x, targetLocalPos.y, 0),
scale: 0.01,
opacity: 0
}, { easing: 'expoIn' })
.call(() => {
if (callback) callback();
})
.start();
}
/**
* 默认打开动画(当 Shader 不可用时使用)
*/
private playDefaultOpen(fromNode: cc.Node, callback?: Function) {
const fromWorldPos = fromNode.convertToWorldSpaceAR(cc.v2(0, 0));
const fromLocalPos = this.node.parent.convertToNodeSpaceAR(fromWorldPos);
const endPos = this.node.position;
this.node.scale = 0.01;
this.node.opacity = 0;
this.node.position = cc.v3(fromLocalPos.x, fromLocalPos.y, 0);
cc.tween(this.node)
.to(this.duration * 0.5, {
position: cc.v3(
fromLocalPos.x + (endPos.x - fromLocalPos.x) * 0.5,
fromLocalPos.y + (endPos.y - fromLocalPos.y) * 0.5,
0
),
scale: 0.6,
opacity: 128
}, { easing: 'quadOut' })
.to(this.duration * 0.5, {
position: endPos,
scale: 1,
opacity: 255
}, { easing: 'expoOut' })
.call(() => {
if (callback) callback();
})
.start();
}
}
/**
* 静态工具函数 - 方便外部调用
*/
export namespace GenieEffectUtil {
/**
* 播放关闭动画(静态方法)
* @param node 目标节点(动画作用的节点)
* @param targetNode 吸入目标节点
* @param callback 完成回调
* @param material 材质(可选)
* @param duration 动画时长
*/
export function playClose(
node: cc.Node,
targetNode: cc.Node,
callback?: Function,
material?: cc.Material,
duration: number = 0.4
) {
const genieEffect = node.getComponent(GenieEffect);
if (genieEffect) {
genieEffect.playClose(targetNode, callback);
} else {
// 动态添加组件并播放
const effect = node.addComponent(GenieEffect);
effect.duration = duration;
if (material) {
effect.genieMaterial = material;
}
// 延迟一帧等待组件初始化
cc.director.once(cc.Director.EVENT_AFTER_UPDATE, () => {
effect.playClose(targetNode, callback);
});
}
}
/**
* 播放打开动画(静态方法)
* @param node 目标节点(动画作用的节点)
* @param fromNode 展开起始节点
* @param callback 完成回调
* @param material 材质(可选)
* @param duration 动画时长
*/
export function playOpen(
node: cc.Node,
fromNode: cc.Node,
callback?: Function,
material?: cc.Material,
duration: number = 0.4
) {
const genieEffect = node.getComponent(GenieEffect);
if (genieEffect) {
genieEffect.playOpen(fromNode, callback);
} else {
const effect = node.addComponent(GenieEffect);
effect.duration = duration;
if (material) {
effect.genieMaterial = material;
}
cc.director.once(cc.Director.EVENT_AFTER_UPDATE, () => {
effect.playOpen(fromNode, callback);
});
}
}
}