Skip to content

Instantly share code, notes, and snippets.

@jamesliu96
Last active August 18, 2020 07:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamesliu96/463e08b7c1ade8a9d02a38fc665cda74 to your computer and use it in GitHub Desktop.
Save jamesliu96/463e08b7c1ade8a9d02a38fc665cda74 to your computer and use it in GitHub Desktop.
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