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