// Learn TypeScript: // - https://docs.cocos.com/creator/2.4/manual/en/scripting/typescript.html // Learn Attribute: // - https://docs.cocos.com/creator/2.4/manual/en/scripting/reference/attributes.html // Learn life-cycle callbacks: // - https://docs.cocos.com/creator/2.4/manual/en/scripting/life-cycle-callbacks.html const { ccclass, property } = cc._decorator; @ccclass export default class NewClass extends cc.Component { @property(cc.Label) label: cc.Label = null; @property({ tooltip: "Vine shrink animation speed (3 = one third second per segment)" }) animationSpeed: number = 3; @property(cc.SpriteFrame) start_up: cc.SpriteFrame = null; @property(cc.SpriteFrame) start_down: cc.SpriteFrame = null; @property(cc.SpriteFrame) start_left: cc.SpriteFrame = null; @property(cc.SpriteFrame) start_right: cc.SpriteFrame = null; @property(cc.SpriteFrame) up: cc.SpriteFrame = null; @property(cc.SpriteFrame) down: cc.SpriteFrame = null; @property(cc.SpriteFrame) left: cc.SpriteFrame = null; @property(cc.SpriteFrame) right: cc.SpriteFrame = null; @property(cc.SpriteFrame) up_left: cc.SpriteFrame = null; @property(cc.SpriteFrame) up_right: cc.SpriteFrame = null; @property(cc.SpriteFrame) down_left: cc.SpriteFrame = null; @property(cc.SpriteFrame) down_right: cc.SpriteFrame = null; @property(cc.SpriteFrame) left_up: cc.SpriteFrame = null; @property(cc.SpriteFrame) left_down: cc.SpriteFrame = null; @property(cc.SpriteFrame) right_up: cc.SpriteFrame = null; @property(cc.SpriteFrame) right_down: cc.SpriteFrame = null; @property(cc.SpriteFrame) end_up: cc.SpriteFrame = null; @property(cc.SpriteFrame) end_down: cc.SpriteFrame = null; @property(cc.SpriteFrame) end_left: cc.SpriteFrame = null; @property(cc.SpriteFrame) end_right: cc.SpriteFrame = null; private iconName: string = ""; setIcon(iconName: string): boolean { console.log("藤蔓圖片名字", iconName); const iconNode = this.node.getChildByName("icon"); const iconSprite = iconNode && iconNode.getComponent(cc.Sprite); const spriteFrame = (this as any)[iconName] as cc.SpriteFrame; if (!iconSprite || !spriteFrame) { cc.warn(`藤蔓图片 ${iconName} 配置不存在`); return false; } console.log("藤蔓圖片", spriteFrame); this.iconName = iconName; iconSprite.spriteFrame = spriteFrame; iconSprite.type = cc.Sprite.Type.FILLED; iconSprite.fillRange = 1; iconSprite.node.opacity = 255; return true; } getShrinkStepDuration(): number { return 1 / Math.max(this.animationSpeed, 0.01); } playShrinkTransition( nextNode: cc.Node, towardStartNode: cc.Node, removeNext: boolean, complete: () => void ): void { let finished = false; const finish = () => { if (finished) return; finished = true; if (complete) complete(); }; if (!nextNode || !cc.isValid(nextNode)) { this.playSingleShrink(finish); return; } const nextVine = nextNode.getComponent(NewClass); const parent = this.node.parent; if (!nextVine || !parent) { if (cc.isValid(this.node)) this.node.destroy(); if (removeNext && cc.isValid(nextNode)) nextNode.destroy(); finish(); return; } const duration = this.getShrinkStepDuration(); const tailPosition = this.copyPosition(this.node.position); const nextPosition = this.copyPosition(nextNode.position); const actualTowardStart = towardStartNode && cc.isValid(towardStartNode); const towardStartPosition = actualTowardStart ? this.copyPosition(towardStartNode.position) : cc.v3( nextPosition.x + nextPosition.x - tailPosition.x, nextPosition.y + nextPosition.y - tailPosition.y ); const tailTowardStart = this.getDirection(tailPosition, nextPosition); const tailTowardEnd = cc.v2(-tailTowardStart.x, -tailTowardStart.y); const nextTowardEnd = this.getDirection(nextPosition, tailPosition); const nextTowardStart = this.getDirection(nextPosition, towardStartPosition); const initialGlowPosition = this.hasVisibleEnd() ? tailPosition : cc.v3( tailPosition.x + (tailPosition.x - nextPosition.x) / 2, tailPosition.y + (tailPosition.y - nextPosition.y) / 2 ); const glow = this.createRetractGlow(parent, initialGlowPosition); this.animateCurrentTail(tailTowardStart, tailTowardEnd, duration); let tween = cc.tween(glow) .to(duration, { position: this.getBoundaryPosition(tailPosition, nextPosition) }, { easing: "linear" }) .call(() => { nextVine.animateBecomeTail(nextTowardStart, nextTowardEnd, duration); }); if (actualTowardStart) { if (this.isCorner(nextTowardEnd, nextTowardStart)) { tween .to(duration / 2, { position: nextPosition }, { easing: "linear" }) .to(duration / 2, { position: this.getBoundaryPosition(nextPosition, towardStartPosition) }, { easing: "linear" }); } else { tween.to(duration, { position: this.getBoundaryPosition(nextPosition, towardStartPosition) }, { easing: "linear" }); } } else { tween.to(duration, { position: nextPosition }, { easing: "linear" }); } if (removeNext) { tween .call(() => { nextVine.animateCurrentTail(nextTowardStart, nextTowardEnd, duration); nextVine.animateBackgroundDisappear(duration); }) .to(duration, { position: this.getBoundaryPosition(nextPosition, towardStartPosition) }, { easing: "linear" }); } tween .call(() => { this.destroyGlow(glow); if (cc.isValid(this.node)) this.node.destroy(); if (removeNext && cc.isValid(nextNode)) nextNode.destroy(); finish(); }) .start(); } private playSingleShrink(complete: () => void): void { const duration = this.getShrinkStepDuration(); const position = this.copyPosition(this.node.position); const towardStart = this.getSingleExitDirection(); const towardEnd = cc.v2(-towardStart.x, -towardStart.y); const distance = Math.max(this.node.width, this.node.height, 120) / 2; const target = cc.v3( position.x + towardStart.x * distance, position.y + towardStart.y * distance ); const parent = this.node.parent; this.animateCurrentTail(towardStart, towardEnd, duration); this.animateBackgroundDisappear(duration); if (!parent) { if (cc.isValid(this.node)) this.node.destroy(); complete(); return; } const glow = this.createRetractGlow(parent, position); cc.tween(glow) .to(duration, { position: target }, { easing: "linear" }) .call(() => { this.destroyGlow(glow); if (cc.isValid(this.node)) this.node.destroy(); complete(); }) .start(); } private animateBecomeTail( towardStart: cc.Vec2, towardEnd: cc.Vec2, duration: number ): void { this.animateBodyDisappear(towardStart, towardEnd, duration); const endSprite = this.getEndSprite(true); const spriteFrame = (this as any)[this.getEndIconName(towardStart)] as cc.SpriteFrame; if (!endSprite || !spriteFrame) return; endSprite.spriteFrame = spriteFrame; endSprite.type = cc.Sprite.Type.FILLED; const range = this.configureLinearFill(endSprite, towardStart); endSprite.fillRange = 0; endSprite.node.opacity = 0; cc.Tween.stopAllByTarget(endSprite); cc.Tween.stopAllByTarget(endSprite.node); cc.tween(endSprite) .delay(duration / 3) .to(duration / 2, { fillRange: range.fullRange }, { easing: "linear" }) .start(); cc.tween(endSprite.node) .delay(duration / 3) .to(duration / 2, { opacity: 255 }, { easing: "linear" }) .start(); } private animateCurrentTail( towardStart: cc.Vec2, towardEnd: cc.Vec2, duration: number ): void { const endSprite = this.getEndSprite(false); if (endSprite && endSprite.spriteFrame && Math.abs(endSprite.fillRange) > 0.001) { cc.Tween.stopAllByTarget(endSprite); cc.tween(endSprite) .to(duration, { fillRange: 0 }, { easing: "linear" }) .start(); return; } this.animateBodyDisappear(towardStart, towardEnd, duration); } private animateBodyDisappear( towardStart: cc.Vec2, towardEnd: cc.Vec2, duration: number ): void { const iconNode = this.node.getChildByName("icon"); const sprite = iconNode && iconNode.getComponent(cc.Sprite); if (!sprite) return; const range = this.configureBodyFill(sprite, towardStart, towardEnd); sprite.fillRange = range.fullRange; cc.Tween.stopAllByTarget(sprite); cc.tween(sprite) .to(duration, { fillRange: range.targetRange }, { easing: "linear" }) .call(() => { // Keep a final visibility guard for pixels exactly on the radial boundary. if (cc.isValid(iconNode)) iconNode.opacity = 0; }) .start(); } private animateBackgroundDisappear(duration: number): void { const background = this.node.getChildByName("bg"); if (!background || !background.active) return; cc.Tween.stopAllByTarget(background); cc.tween(background) .to(duration, { opacity: 0 }, { easing: "linear" }) .start(); } private configureBodyFill( sprite: cc.Sprite, towardStart: cc.Vec2, towardEnd: cc.Vec2 ) { const start = this.normalizeDirection(towardStart); const end = this.normalizeDirection(towardEnd); const cross = end.x * start.y - end.y * start.x; sprite.type = cc.Sprite.Type.FILLED; if (Math.abs(cross) < 0.01) { return this.configureLinearFill(sprite, start); } sprite.fillType = cc.Sprite.FillType.RADIAL; // Independent corner images are not atlas-rotated: their radial center is // the actual corner shared by the two connected sides. sprite.fillCenter = cc.v2( 0.5 + 0.5 * (end.x + start.x), 0.5 + 0.5 * (end.y + start.y) ); // From the corner center, the endpoint on the end side points opposite // to the start direction. let fillStart = Math.atan2(-start.y, -start.x) / (Math.PI * 2); if (fillStart < 0) fillStart += 1; sprite.fillStart = fillStart; const fullRange = cross > 0 ? 1 : -1; return { fullRange: fullRange, targetRange: fullRange * 0.75 }; } private configureLinearFill(sprite: cc.Sprite, direction: cc.Vec2) { const towardStart = this.normalizeDirection(direction); let fullRange = 1; if (Math.abs(towardStart.x) >= Math.abs(towardStart.y)) { sprite.fillType = cc.Sprite.FillType.HORIZONTAL; sprite.fillStart = towardStart.x < 0 ? 0 : 1; fullRange = towardStart.x < 0 ? 1 : -1; } else { sprite.fillType = cc.Sprite.FillType.VERTICAL; sprite.fillStart = towardStart.y < 0 ? 0 : 1; fullRange = towardStart.y < 0 ? 1 : -1; } return { fullRange: fullRange, targetRange: 0 }; } private getEndSprite(create: boolean): cc.Sprite { let endNode = this.node.getChildByName("end"); if (!endNode && create) { endNode = new cc.Node("end"); this.node.addChild(endNode); endNode.setSiblingIndex(0); } if (!endNode) return null; let sprite = endNode.getComponent(cc.Sprite); if (!sprite && create) sprite = endNode.addComponent(cc.Sprite); if (!sprite) return null; const icon = this.node.getChildByName("icon"); if (icon) { endNode.width = icon.width; endNode.height = icon.height; } endNode.active = true; return sprite; } private hasVisibleEnd(): boolean { const sprite = this.getEndSprite(false); return !!sprite && !!sprite.spriteFrame && sprite.node.opacity > 0 && Math.abs(sprite.fillRange) > 0.001; } private getEndIconName(direction: cc.Vec2): string { const normalized = this.normalizeDirection(direction); if (Math.abs(normalized.x) >= Math.abs(normalized.y)) { return normalized.x < 0 ? "end_left" : "end_right"; } return normalized.y < 0 ? "end_down" : "end_up"; } private getSingleExitDirection(): cc.Vec2 { const parts = this.iconName.split("_"); if (parts.length >= 2 && parts[0] === "start") { const towardEnd = this.directionFromName(parts[1]); return cc.v2(-towardEnd.x, -towardEnd.y); } if (parts.length >= 2 && parts[0] === "end") { return this.directionFromName(parts[1]); } return cc.v2(0, -1); } private directionFromName(name: string): cc.Vec2 { if (name === "left") return cc.v2(-1, 0); if (name === "right") return cc.v2(1, 0); if (name === "up") return cc.v2(0, 1); return cc.v2(0, -1); } private normalizeDirection(direction: cc.Vec2): cc.Vec2 { if (Math.abs(direction.x) >= Math.abs(direction.y) && Math.abs(direction.x) > 0.01) { return cc.v2(direction.x < 0 ? -1 : 1, 0); } if (Math.abs(direction.y) > 0.01) { return cc.v2(0, direction.y < 0 ? -1 : 1); } return cc.v2(0, -1); } private getDirection(from: cc.Vec3, to: cc.Vec3): cc.Vec2 { return this.normalizeDirection(cc.v2(to.x - from.x, to.y - from.y)); } private isCorner(first: cc.Vec2, second: cc.Vec2): boolean { return Math.abs(first.x * second.y - first.y * second.x) > 0.01; } private getBoundaryPosition(from: cc.Vec3, toward: cc.Vec3): cc.Vec3 { return cc.v3((from.x + toward.x) / 2, (from.y + toward.y) / 2); } private copyPosition(position: cc.Vec3): cc.Vec3 { return cc.v3(position.x, position.y, position.z || 0); } private createRetractGlow(parent: cc.Node, position: cc.Vec3): cc.Node { const root = new cc.Node("retract_glow"); const pulse = new cc.Node("pulse"); const graphics = pulse.addComponent(cc.Graphics); const orbit = new cc.Node("orbit"); graphics.fillColor = cc.color(145, 255, 45, 45); graphics.circle(0, 0, 30); graphics.fill(); graphics.fillColor = cc.color(190, 255, 70, 95); graphics.circle(0, 0, 19); graphics.fill(); graphics.fillColor = cc.color(245, 255, 170, 240); graphics.circle(0, 0, 7); graphics.fill(); for (let index = 0; index < 4; index++) { const spark = new cc.Node("spark"); const sparkGraphics = spark.addComponent(cc.Graphics); const angle = Math.PI * index / 2; spark.setPosition(Math.cos(angle) * 24, Math.sin(angle) * 24); sparkGraphics.fillColor = cc.color(220, 255, 95, 210); sparkGraphics.circle(0, 0, 3); sparkGraphics.fill(); orbit.addChild(spark); } root.setPosition(position); root.zIndex = 2000; root.addChild(pulse); root.addChild(orbit); parent.addChild(root); cc.tween(pulse) .repeatForever( cc.tween() .to(0.4, { scale: 1.18, opacity: 150 }, { easing: "sineInOut" }) .to(0.4, { scale: 0.9, opacity: 255 }, { easing: "sineInOut" }) ) .start(); cc.tween(orbit) .by(1.2, { angle: 360 }) .repeatForever() .start(); return root; } private destroyGlow(glow: cc.Node): void { if (!glow || !cc.isValid(glow)) return; cc.Tween.stopAllByTarget(glow); for (let i = 0; i < glow.children.length; i++) { cc.Tween.stopAllByTarget(glow.children[i]); } glow.destroy(); } }