Last active
August 18, 2020 07:46
-
-
Save jamesliu96/463e08b7c1ade8a9d02a38fc665cda74 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Vec2 = { x: number; y: number }; | |
type Rect = Vec2 & { width: number; height: number }; | |
class Strip { | |
public rect: Rect; | |
public pos: Vec2 = { x: 0, y: 0 }; | |
public energy = 0; | |
public constructor(rect: Rect) { | |
if (rect.height < 0) rect.height = 0; | |
this.rect = rect; | |
this.updatePos(); | |
} | |
/** proxy to `this.rect` */ | |
public get width() { | |
return this.rect.width; | |
} | |
/** proxy to `this.rect`, update node position once set */ | |
public get height() { | |
return this.rect.height; | |
} | |
public set height(height: number) { | |
if (height < 0) height = 0; | |
this.rect.height = height; | |
this.updatePos(); | |
} | |
/** horizontal position of the top center point */ | |
public get x() { | |
return this.pos.x; | |
} | |
/** vertical position of the top center point */ | |
public get y() { | |
return this.pos.y; | |
} | |
private updatePos() { | |
this.pos.x = this.rect.x + this.rect.width / 2; | |
this.pos.y = this.rect.y + this.rect.height; | |
} | |
} | |
type StripsManagerContext = { | |
targetWidth: number; | |
targetOffsetX: number; | |
targetOffsetY: number; | |
height: number; | |
n: number; | |
spread: number; | |
propagation: number; | |
initialHeights: number[]; | |
}; | |
class StripsManager { | |
public strips: Strip[] = []; | |
private context: StripsManagerContext; | |
public constructor(context: StripsManagerContext) { | |
this.context = context; | |
const stripWidth = this.context.targetWidth / (this.context.n - 1); | |
for (let i = 0; i < this.context.n; i++) { | |
let initialHeight = this.context.initialHeights[i]; | |
if (typeof initialHeight !== 'number') { | |
initialHeight = this.context.height; | |
} | |
const strip = new Strip({ | |
x: stripWidth * i - stripWidth / 2 + this.context.targetOffsetX, | |
y: this.context.targetOffsetY, | |
width: stripWidth, | |
height: initialHeight, | |
}); | |
this.strips.push(strip); | |
} | |
} | |
/** | |
* Apply an impulse to the surface of the wave | |
* | |
* @param x horizontal position where the force applies | |
* @param force numeric value of the force | |
*/ | |
public impulse(x: number, force: number) { | |
const strip = this.getStripAt(x); | |
if (strip) { | |
strip.energy += force; | |
} | |
} | |
/** | |
* Push the surface of the wave to the height | |
* | |
* @param x horizontal position where the push applies | |
* @param height height to be pushed to | |
*/ | |
public pushTo(x: number, height: number) { | |
const strip = this.getStripAt(x); | |
if (strip) { | |
strip.height = height; | |
} | |
} | |
/** | |
* Find the strip at the given position | |
* | |
* @param x given horizontal position | |
*/ | |
public getStripAt(x: number) { | |
for (const strip of this.strips) { | |
if (x >= strip.rect.x && x < strip.rect.x + strip.rect.width) { | |
return strip; | |
} | |
} | |
} | |
/** | |
* Update the strips according to renderer's delta time factor | |
* | |
* @param dt delta time | |
*/ | |
public update(dt: number) { | |
for (let k = 0; k < this.context.propagation; k++) { | |
for (let i = 0; i < this.strips.length; i++) { | |
const thisStrip = this.strips[i]; | |
if (i > 0) { | |
const leftStrip = this.strips[i - 1]; | |
leftStrip.energy += | |
this.context.spread * (thisStrip.height - leftStrip.height); | |
} | |
if (i < this.strips.length - 1) { | |
const rightStrip = this.strips[i + 1]; | |
rightStrip.energy += | |
this.context.spread * (thisStrip.height - rightStrip.height); | |
} | |
} | |
} | |
for (const strip of this.strips) { | |
strip.energy *= this.context.spread; | |
strip.height += | |
strip.energy * dt + (this.context.height - strip.height) * dt; | |
} | |
} | |
} | |
enum Mode { | |
Graphics, | |
Mask, | |
} | |
const { ccclass, property } = cc._decorator; | |
/** | |
* Wave component | |
* | |
* @example | |
* ```ts | |
* // get the component | |
* const wave = node.getComponent(Wave); | |
* | |
* // apply impulse to wave surface | |
* wave.impulse(10, -100); | |
* // push the wave surface | |
* wave.pushTo(50, wave.height + 20); | |
* | |
* // set new height of wave surface | |
* wave.height += 40; | |
* | |
* // get strip at a horizontal position | |
* const strip = wave.getStripAt(80); | |
* ``` | |
*/ | |
@ccclass | |
export default class Wave extends cc.Component implements StripsManagerContext { | |
/** the mask node to be manipulated, default to `this.node` if not set */ | |
@property(cc.Node) | |
public target: cc.Node = null; | |
public get targetWidth() { | |
return this.target.width; | |
} | |
public get targetOffsetX() { | |
return -this.target.anchorX * this.target.width; | |
} | |
public get targetOffsetY() { | |
return -this.target.anchorY * this.target.height; | |
} | |
/** use which graphics to draw wave */ | |
@property({ type: cc.Enum(Mode) }) | |
public mode = Mode.Graphics; | |
private graphics: cc.Graphics; | |
/** debug flag */ | |
@property(cc.Boolean) | |
public debug = false; | |
private debugNode: cc.Node; | |
private debugGraphics: cc.Graphics; | |
/** the height of the surface of the wave */ | |
@property({ | |
type: cc.Float, | |
tooltip: 'the height of the surface of the wave', | |
min: 0, | |
}) | |
public height = 80; | |
/** the number of strips, become immutable once set before load */ | |
@property({ | |
type: cc.Integer, | |
displayName: 'Number', | |
tooltip: 'the number of strips, become immutable once set before load', | |
range: [0, 100, 1], | |
}) | |
public n = 20; | |
/** the spread factor of delta wave energy */ | |
@property({ | |
type: cc.Float, | |
tooltip: 'the spread factor of delta wave energy', | |
range: [0, 1, 0.001], | |
}) | |
public spread = 0.98; | |
/** the propagation iteration times */ | |
@property({ | |
type: cc.Integer, | |
tooltip: 'the propagation iteration times', | |
range: [1, 10, 1], | |
}) | |
public propagation = 1; | |
/** the initial heights of each wave strip, default to surface height if not set */ | |
@property({ | |
type: cc.Float, | |
tooltip: | |
'the initial heights of each wave strip, default to surface height if not set', | |
min: 0, | |
}) | |
public initialHeights: number[] = []; | |
/** color of gradient */ | |
@property({ | |
tooltip: 'color of gradient', | |
}) | |
public gradientColor = cc.color(); | |
/** the height of gradient, no gradient needed if set to 0 */ | |
@property({ | |
type: cc.Float, | |
tooltip: 'the height of gradient, no gradient needed if set to 0', | |
min: 0, | |
}) | |
public gradientHeight = 10; | |
public stripsManager: StripsManager; | |
public onLoad() { | |
if (this.n < 2) { | |
throw new Error('N must be 2 at minimum.'); | |
} | |
if (!this.target) { | |
this.target = this.node; | |
} | |
this.graphics = | |
this.mode === Mode.Mask | |
? this.target.addComponent(cc.Mask)['_graphics'] | |
: this.target.addComponent(cc.Graphics); | |
this.stripsManager = new StripsManager(this); | |
} | |
public update(dt: number) { | |
this.stripsManager.update(dt); | |
this.paintStripCurve(); | |
this.paintDebug(); | |
} | |
/** | |
* Apply an impulse to the surface of the wave | |
* | |
* @param x horizontal position where the force applies | |
* @param force numeric value of the force | |
*/ | |
public impulse(x: number, force: number) { | |
this.stripsManager.impulse(x, force); | |
} | |
/** | |
* Push the surface of the wave to the height | |
* | |
* @param x horizontal position where the push applies | |
* @param height height to be pushed to | |
*/ | |
public pushTo(x: number, height: number) { | |
this.stripsManager.pushTo(x, height); | |
} | |
/** | |
* Find the strip at the given position | |
* | |
* @param x given horizontal position | |
*/ | |
public getStripAt(x: number) { | |
return this.stripsManager.getStripAt(x); | |
} | |
private paintStripCurve() { | |
this.graphics.clear(); | |
const { strips } = this.stripsManager; | |
const [{ x: x0, y: y0 }] = strips; | |
this.graphics.fillColor = this.target.color | |
.clone() | |
.setA(this.target.opacity); | |
this.graphics.moveTo(x0, y0); | |
let i = 1; | |
for (; i < strips.length - 2; i++) { | |
const { x: x1, y: y1 } = strips[i]; | |
const { x: x2, y: y2 } = strips[i + 1]; | |
this.graphics.quadraticCurveTo(x1, y1, (x1 + x2) / 2, (y1 + y2) / 2); | |
} | |
const { x: x1, y: y1 } = strips[i]; | |
const { x: x2, y: y2 } = strips[i + 1]; | |
this.graphics.quadraticCurveTo(x1, y1, x2, y2); | |
this.graphics.lineTo( | |
this.target.width + this.targetOffsetX, | |
this.height + this.targetOffsetY | |
); | |
this.graphics.lineTo( | |
this.target.width + this.targetOffsetX, | |
this.targetOffsetY | |
); | |
this.graphics.lineTo(this.targetOffsetX, this.targetOffsetY); | |
this.graphics.close(); | |
this.graphics.fill(); | |
this.graphics.lineWidth = 1; | |
let offsetY = 0; | |
if (!this.useMask) { | |
for (; offsetY > -this.gradientHeight; offsetY--) { | |
this.graphics.strokeColor = this.getColorGradient( | |
offsetY / -this.gradientHeight | |
); | |
this.graphics.moveTo(x0, y0 + offsetY); | |
let i = 1; | |
for (; i < strips.length - 2; i++) { | |
let { x: x1, y: y1 } = strips[i]; | |
let { x: x2, y: y2 } = strips[i + 1]; | |
y1 += offsetY; | |
y2 += offsetY; | |
this.graphics.quadraticCurveTo(x1, y1, (x1 + x2) / 2, (y1 + y2) / 2); | |
} | |
let { x: x1, y: y1 } = strips[i]; | |
let { x: x2, y: y2 } = strips[i + 1]; | |
y1 += offsetY; | |
y2 += offsetY; | |
this.graphics.quadraticCurveTo(x1, y1, x2, y2); | |
this.graphics.stroke(); | |
} | |
} | |
} | |
private getColorGradient(r: number) { | |
return this.gradientColor.lerp(this.target.color.clone().setA(0), r); | |
} | |
private paintDebug() { | |
if (this.debug) { | |
if (!cc.isValid(this.debugNode)) { | |
this.debugNode = new cc.Node('MaskWave Debug'); | |
this.debugNode.setAnchorPoint(this.target.getAnchorPoint()); | |
this.debugNode.position = this.target.position; | |
this.debugNode.width = this.target.width; | |
this.debugNode.height = this.target.height; | |
this.debugGraphics = this.debugNode.addComponent(cc.Graphics); | |
this.target.parent.addChild(this.debugNode); | |
} | |
this.debugGraphics.clear(); | |
this.debugGraphics.fillColor = cc.Color.RED; | |
const { strips } = this.stripsManager; | |
for (const strip of strips) { | |
this.debugGraphics.strokeColor = cc.Color.MAGENTA; | |
this.debugGraphics.lineWidth = 1; | |
const { x, y, rect } = strip; | |
this.debugGraphics.rect(rect.x, rect.y, rect.width, rect.height); | |
this.debugGraphics.stroke(); | |
this.debugGraphics.strokeColor = cc.Color.GREEN; | |
this.debugGraphics.lineWidth = 2; | |
this.debugGraphics.moveTo(x, rect.y); | |
this.debugGraphics.lineTo(x, y); | |
this.debugGraphics.close(); | |
this.debugGraphics.stroke(); | |
this.debugGraphics.fillRect(x - 2, y - 2, 4, 4); | |
} | |
} else { | |
if (cc.isValid(this.debugNode)) { | |
this.debugNode.destroy(); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment