/** * 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); }); } } }