486 lines
17 KiB
TypeScript
486 lines
17 KiB
TypeScript
// 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("藤蔓圖片å<E280A1><C3A5>å—", 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} é…<C3A9>ç½®ä¸<C3A4>å˜åœ¨`);
|
||
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();
|
||
}
|
||
}
|