375 lines
12 KiB
TypeScript
375 lines
12 KiB
TypeScript
/**
|
||
* 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();
|
||
|
||
// 检查是否包含滚动视图,临时禁用裁剪
|
||
const scrollView = node.getComponent(cc.ScrollView);
|
||
const maskComponents = node.getComponentsInChildren(cc.Mask);
|
||
const originalMaskEnabled = maskComponents.map(m => m.enabled);
|
||
|
||
// 临时禁用所有 Mask 组件
|
||
maskComponents.forEach(mask => {
|
||
mask.enabled = false;
|
||
});
|
||
|
||
// 创建 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;
|
||
|
||
// 特别处理:如果节点包含滚动视图,调整渲染设置
|
||
if (scrollView) {
|
||
// 对于滚动视图,强制渲染所有内容
|
||
camera.cullingMask = 0xffffffff;
|
||
// 设置更清晰的渲染质量
|
||
camera.zoomRatio = 1.5;
|
||
}
|
||
|
||
// 渲染一帧
|
||
camera.render();
|
||
|
||
// 销毁相机节点
|
||
cameraNode.destroy();
|
||
|
||
// 恢复 Mask 组件状态
|
||
maskComponents.forEach((mask, index) => {
|
||
mask.enabled = originalMaskEnabled[index];
|
||
});
|
||
|
||
// 创建 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;
|
||
|
||
// 设置混合模式为透明
|
||
sprite.srcBlendFactor = cc.macro.BlendFactor.SRC_ALPHA;
|
||
sprite.dstBlendFactor = cc.macro.BlendFactor.ONE_MINUS_SRC_ALPHA;
|
||
|
||
// 隐藏原节点
|
||
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 targetWorldPos = this.node.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);
|
||
|
||
// 播放动画 - 同时移动节点和更新 Shader
|
||
const animData = { progress: 1 };
|
||
const startPos = captureNode.position;
|
||
const targetPos = captureNode.parent.convertToNodeSpaceAR(targetWorldPos);
|
||
|
||
cc.tween(animData)
|
||
.to(this.duration, { progress: 0 }, {
|
||
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();
|
||
}
|
||
// 显示原节点
|
||
this.node.active = true;
|
||
if (callback) callback();
|
||
})
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 默认关闭动画(不使用 Shader)
|
||
*/
|
||
playDefaultClose(targetNode: cc.Node, callback?: Function) {
|
||
if (this._isPlaying) {
|
||
console.warn('GenieEffect: 动画正在播放中');
|
||
return;
|
||
}
|
||
|
||
this._isPlaying = true;
|
||
|
||
// 获取起始位置和目标位置(世界坐标)
|
||
const startWorldPos = this.node.convertToWorldSpaceAR(cc.v2(0, 0));
|
||
const targetWorldPos = targetNode.convertToWorldSpaceAR(cc.v2(0, 0));
|
||
|
||
// 播放缩放和移动动画
|
||
cc.tween(this.node)
|
||
.to(this.duration, {
|
||
scale: 0,
|
||
position: new cc.Vec3(this.node.parent.convertToNodeSpaceAR(targetWorldPos).x, this.node.parent.convertToNodeSpaceAR(targetWorldPos).y, 0)
|
||
}, {
|
||
easing: 'quadInOut'
|
||
})
|
||
.call(() => {
|
||
this._isPlaying = false;
|
||
this.node.active = false;
|
||
if (callback) callback();
|
||
})
|
||
.start();
|
||
}
|
||
|
||
/**
|
||
* 默认打开动画(不使用 Shader)
|
||
*/
|
||
playDefaultOpen(fromNode: cc.Node, callback?: Function) {
|
||
if (this._isPlaying) {
|
||
console.warn('GenieEffect: 动画正在播放中');
|
||
return;
|
||
}
|
||
|
||
this._isPlaying = true;
|
||
|
||
// 获取起始位置和目标位置(世界坐标)
|
||
const fromWorldPos = fromNode.convertToWorldSpaceAR(cc.v2(0, 0));
|
||
const targetWorldPos = this.node.convertToWorldSpaceAR(cc.v2(0, 0));
|
||
|
||
// 设置初始状态
|
||
this.node.scale = 0;
|
||
const fromPos = this.node.parent.convertToNodeSpaceAR(fromWorldPos);
|
||
this.node.position = new cc.Vec3(fromPos.x, fromPos.y, 0);
|
||
this.node.active = true;
|
||
|
||
// 播放缩放和移动动画
|
||
cc.tween(this.node)
|
||
.to(this.duration, {
|
||
scale: 1,
|
||
position: new cc.Vec3(this.node.parent.convertToNodeSpaceAR(targetWorldPos).x, this.node.parent.convertToNodeSpaceAR(targetWorldPos).y, 0)
|
||
}, {
|
||
easing: 'quadInOut'
|
||
})
|
||
.call(() => {
|
||
this._isPlaying = false;
|
||
if (callback) callback();
|
||
})
|
||
.start();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GenieEffect 工具类
|
||
* 提供静态方法方便调用
|
||
*/
|
||
export class GenieEffectUtil {
|
||
/**
|
||
* 播放关闭动画
|
||
* @param node 要播放动画的节点
|
||
* @param targetNode 目标节点(吸入位置)
|
||
* @param callback 动画完成回调
|
||
* @param material 可选的材质
|
||
*/
|
||
static playClose(node: cc.Node, targetNode: cc.Node, callback?: Function, material?: cc.Material) {
|
||
const genieEffect = node.addComponent(GenieEffect);
|
||
genieEffect.genieMaterial = material;
|
||
genieEffect.playClose(targetNode, callback);
|
||
}
|
||
|
||
/**
|
||
* 播放打开动画
|
||
* @param node 要播放动画的节点
|
||
* @param fromNode 起始节点(展开位置)
|
||
* @param callback 动画完成回调
|
||
* @param material 可选的材质
|
||
*/
|
||
static playOpen(node: cc.Node, fromNode: cc.Node, callback?: Function, material?: cc.Material) {
|
||
const genieEffect = node.addComponent(GenieEffect);
|
||
genieEffect.genieMaterial = material;
|
||
genieEffect.playOpen(fromNode, callback);
|
||
}
|
||
}
|