diff --git a/.vscode/settings.json b/.vscode/settings.json
index d721f5bc7..670f6ffe5 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -25,5 +25,6 @@
"typescript.tsdk": "./node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
- }
+ },
+ "deno.enable": false
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00fdf3bd5..cdb561449 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -75,6 +75,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added
+- Added new `ex.BezierCurve` type for drawing cubic bezier curves
+- Added 2 new actions `actor.actions.curveTo(...)` and `actor.actions.curveBy(...)`
- Added new `ex.lerp(...)`, `ex.inverseLerp(...)`, and `ex.remap(...)` for numbers
- Added new `ex.lerpVector(...)`,` ex.inverseLerpVector(...)`, and `ex.remapVector(...)` for `ex.Vector`
- Added new `actor.actions.flash(...)` `Action` to flash a color for a period of time
diff --git a/sandbox/tests/bezier/index.html b/sandbox/tests/bezier/index.html
new file mode 100644
index 000000000..b4648dc7e
--- /dev/null
+++ b/sandbox/tests/bezier/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Bezier Curve
+
+
+
+
+
+
diff --git a/sandbox/tests/bezier/index.ts b/sandbox/tests/bezier/index.ts
new file mode 100644
index 000000000..f6f718489
--- /dev/null
+++ b/sandbox/tests/bezier/index.ts
@@ -0,0 +1,82 @@
+var game = new ex.Engine({
+ width: 600,
+ height: 400,
+ displayMode: ex.DisplayMode.FillScreen
+});
+game.toggleDebug();
+
+var curve = new ex.BezierCurve({
+ controlPoints: [ex.vec(0, 700), ex.vec(100, -300), ex.vec(150, 800), ex.vec(500, 100)],
+ quality: 10
+});
+
+var reverseCurve = curve.clone();
+reverseCurve.controlPoints = [...reverseCurve.controlPoints].reverse() as any;
+
+var actor = new ex.Actor({
+ pos: ex.vec(500, 500),
+ width: 100,
+ height: 100,
+ color: ex.Color.Red,
+ angularVelocity: 1
+});
+game.add(actor);
+
+actor.actions.repeatForever((ctx) => {
+ ctx.curveTo({
+ controlPoints: [ex.vec(100, -300), ex.vec(150, 800), ex.vec(500, 100)],
+ durationMs: 6000
+ });
+ ctx.curveBy({
+ controlPoints: [ex.vec(100, 0), ex.vec(-100, 0), ex.vec(0, 300)],
+ durationMs: 1000
+ });
+ ctx.curveTo({
+ controlPoints: [ex.vec(150, 800), ex.vec(100, -300), ex.vec(0, 700)],
+ durationMs: 6000
+ });
+});
+
+var time = 0;
+var points: ex.Vector[] = [];
+game.onPostDraw = (ctx: ex.ExcaliburGraphicsContext, elapsedMs) => {
+ if (time < 5000) {
+ var t = ex.clamp(ex.remap(0, 5000, 0, 1, time), 0, 1);
+
+ var p = curve.getPoint(t);
+ ctx.drawCircle(p, 20, ex.Color.Red);
+ var p2 = curve.getUniformPoint(t);
+ ctx.drawCircle(p2, 20, ex.Color.Purple);
+ points.push(p2);
+
+ var tangent = curve.getTangent(t);
+ var normal = curve.getNormal(t);
+ ex.Debug.drawRay(new ex.Ray(p, tangent), {
+ distance: 100,
+ color: ex.Color.Yellow
+ });
+ ex.Debug.drawRay(new ex.Ray(p, normal), {
+ distance: 100,
+ color: ex.Color.Green
+ });
+
+ var uTangent = curve.getUniformTangent(t);
+ var uNormal = curve.getUniformNormal(t);
+ ex.Debug.drawRay(new ex.Ray(p2, uTangent), {
+ distance: 100,
+ color: ex.Color.Yellow
+ });
+ ex.Debug.drawRay(new ex.Ray(p2, uNormal), {
+ distance: 100,
+ color: ex.Color.Green
+ });
+
+ time += elapsedMs;
+ }
+
+ for (let i = 0; i < points.length - 1; i++) {
+ ctx.drawLine(points[i], points[i + 1], ex.Color.Purple, 2);
+ }
+};
+
+game.start();
diff --git a/src/engine/Actions/Action/CurveBy.ts b/src/engine/Actions/Action/CurveBy.ts
new file mode 100644
index 000000000..9a56f7226
--- /dev/null
+++ b/src/engine/Actions/Action/CurveBy.ts
@@ -0,0 +1,86 @@
+import { Entity, TransformComponent } from '../../EntityComponentSystem';
+import { BezierCurve, clamp, remap, vec, Vector } from '../../Math';
+import { Action, nextActionId } from '../Action';
+
+export interface CurveByOptions {
+ /**
+ * Curve relative to the current actor position to move
+ */
+ controlPoints: [control1: Vector, control2: Vector, end: Vector];
+ /**
+ * Total duration for the action to run
+ */
+ durationMs: number;
+ /**
+ * Dynamic mode will speed up/slow down depending on the curve
+ *
+ * Uniform mode will animate at a consistent velocity across the curve
+ *
+ * Default: 'dynamic'
+ */
+ mode?: 'dynamic' | 'uniform';
+
+ quality?: number;
+}
+
+export class CurveBy implements Action {
+ id: number = nextActionId();
+
+ private _curve: BezierCurve;
+ private _durationMs: number;
+ private _entity: Entity;
+ private _tx: TransformComponent;
+ private _currentMs: number;
+ private _started = false;
+ private _stopped = false;
+ private _mode: 'dynamic' | 'uniform' = 'dynamic';
+ constructor(entity: Entity, options: CurveByOptions) {
+ this._entity = entity;
+ this._tx = this._entity.get(TransformComponent);
+ if (!this._tx) {
+ throw new Error(`Entity ${entity.name} has no TransformComponent, can only curveTo on Entities with TransformComponents.`);
+ }
+ this._curve = this._curve = new BezierCurve({
+ controlPoints: [vec(0, 0), ...options.controlPoints],
+ quality: options.quality
+ });
+ this._durationMs = options.durationMs;
+ this._mode = options.mode ?? this._mode;
+ this._currentMs = this._durationMs;
+ }
+
+ update(elapsedMs: number): void {
+ if (!this._started) {
+ this._curve.setControlPoint(0, this._tx.globalPos);
+ this._curve.setControlPoint(1, this._curve.controlPoints[1].add(this._tx.globalPos));
+ this._curve.setControlPoint(2, this._curve.controlPoints[2].add(this._tx.globalPos));
+ this._curve.setControlPoint(3, this._curve.controlPoints[3].add(this._tx.globalPos));
+ this._started = true;
+ }
+ const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1);
+ if (this._mode === 'dynamic') {
+ this._tx.pos = this._curve.getPoint(t);
+ } else {
+ this._tx.pos = this._curve.getUniformPoint(t);
+ }
+ this._currentMs -= elapsedMs;
+ if (this.isComplete(this._entity)) {
+ if (this._mode === 'dynamic') {
+ this._tx.pos = this._curve.getPoint(1);
+ } else {
+ this._tx.pos = this._curve.getUniformPoint(1);
+ }
+ }
+ }
+ isComplete(entity: Entity): boolean {
+ return this._stopped || this._currentMs < 0;
+ }
+ reset(): void {
+ this._currentMs = this._durationMs;
+ this._started = false;
+ this._stopped = false;
+ }
+ stop(): void {
+ this._stopped = true;
+ }
+}
diff --git a/src/engine/Actions/Action/CurveTo.ts b/src/engine/Actions/Action/CurveTo.ts
new file mode 100644
index 000000000..33236a797
--- /dev/null
+++ b/src/engine/Actions/Action/CurveTo.ts
@@ -0,0 +1,84 @@
+import { Entity, TransformComponent } from '../../EntityComponentSystem';
+import { BezierCurve, clamp, remap, vec, Vector } from '../../Math';
+import { Action, nextActionId } from '../Action';
+
+export interface CurveToOptions {
+ /**
+ * Curve in world coordinates to animate towards
+ *
+ * The start control point is assumed to be the actor's current position
+ */
+ controlPoints: [control1: Vector, control2: Vector, end: Vector];
+ /**
+ * Total duration for the action to run
+ */
+ durationMs: number;
+ /**
+ * Dynamic mode will speed up/slow down depending on the curve
+ *
+ * Uniform mode will animate at a consistent velocity across the curve
+ *
+ * Default: 'dynamic'
+ */
+ mode?: 'dynamic' | 'uniform';
+ quality?: number;
+}
+
+export class CurveTo implements Action {
+ id: number = nextActionId();
+
+ private _curve: BezierCurve;
+ private _durationMs: number;
+ private _entity: Entity;
+ private _tx: TransformComponent;
+ private _currentMs: number;
+ private _started = false;
+ private _stopped = false;
+ private _mode: 'dynamic' | 'uniform' = 'dynamic';
+ constructor(entity: Entity, options: CurveToOptions) {
+ this._entity = entity;
+ this._tx = this._entity.get(TransformComponent);
+ if (!this._tx) {
+ throw new Error(`Entity ${entity.name} has no TransformComponent, can only curveTo on Entities with TransformComponents.`);
+ }
+ this._curve = new BezierCurve({
+ controlPoints: [vec(0, 0), ...options.controlPoints],
+ quality: options.quality
+ });
+ this._durationMs = options.durationMs;
+ this._mode = options.mode ?? this._mode;
+ this._currentMs = this._durationMs;
+ }
+
+ update(elapsedMs: number): void {
+ if (!this._started) {
+ this._curve.setControlPoint(0, this._tx.globalPos.clone());
+ this._started = true;
+ }
+ const t = clamp(remap(0, this._durationMs, 0, 1, this._durationMs - this._currentMs), 0, 1);
+ if (this._mode === 'dynamic') {
+ this._tx.pos = this._curve.getPoint(t);
+ } else {
+ this._tx.pos = this._curve.getUniformPoint(t);
+ }
+ this._currentMs -= elapsedMs;
+ if (this.isComplete(this._entity)) {
+ if (this._mode === 'dynamic') {
+ this._tx.pos = this._curve.getPoint(1);
+ } else {
+ this._tx.pos = this._curve.getUniformPoint(1);
+ }
+ }
+ }
+ isComplete(entity: Entity): boolean {
+ return this._stopped || this._currentMs < 0;
+ }
+ reset(): void {
+ this._currentMs = this._durationMs;
+ this._started = false;
+ this._stopped = false;
+ }
+ stop(): void {
+ this._currentMs = 0;
+ }
+}
diff --git a/src/engine/Actions/ActionContext.ts b/src/engine/Actions/ActionContext.ts
index 0644cde1f..3026688a7 100644
--- a/src/engine/Actions/ActionContext.ts
+++ b/src/engine/Actions/ActionContext.ts
@@ -24,6 +24,8 @@ import { Entity } from '../EntityComponentSystem/Entity';
import { Action } from './Action';
import { Color } from '../Color';
import { Flash } from './Action/Flash';
+import { CurveTo, CurveToOptions } from './Action/CurveTo';
+import { CurveBy, CurveByOptions } from './Action/CurveBy';
/**
* The fluent Action API allows you to perform "actions" on
@@ -61,6 +63,26 @@ export class ActionContext {
return this;
}
+ /**
+ * Animates an actor with a specified bezier curve, overrides the first control point
+ * to be the actor's current position.
+ * @param options
+ */
+ public curveBy(options: CurveByOptions): ActionContext {
+ this._queue.add(new CurveBy(this._entity, options));
+ return this;
+ }
+
+ /**
+ * Animates an actor with a specified bezier curve, overrides the first control point
+ * to be the actor's current position.
+ * @param options
+ */
+ public curveTo(options: CurveToOptions): ActionContext {
+ this._queue.add(new CurveTo(this._entity, options));
+ return this;
+ }
+
/**
* This method will move an actor to the specified `x` and `y` position over the
* specified duration using a given {@apilink EasingFunctions} and return back the actor. This
diff --git a/src/engine/Actions/ActionsComponent.ts b/src/engine/Actions/ActionsComponent.ts
index 44a6c2591..c654a4748 100644
--- a/src/engine/Actions/ActionsComponent.ts
+++ b/src/engine/Actions/ActionsComponent.ts
@@ -10,6 +10,8 @@ import { ActionQueue } from './ActionQueue';
import { RotationType } from './RotationType';
import { Action } from './Action';
import { Color } from '../Color';
+import { CurveToOptions } from './Action/CurveTo';
+import { CurveByOptions } from './Action/CurveBy';
export interface ActionContextMethods extends Pick {}
@@ -65,6 +67,14 @@ export class ActionsComponent extends Component implements ActionContextMethods
this._ctx?.clearActions();
}
+ public curveBy(options: CurveByOptions): ActionContext {
+ return this._getCtx().curveBy.apply(this._ctx, [options]);
+ }
+
+ public curveTo(options: CurveToOptions): ActionContext {
+ return this._getCtx().curveTo.apply(this._ctx, [options]);
+ }
+
/**
* This method will move an actor to the specified `x` and `y` position over the
* specified duration using a given {@apilink EasingFunctions} and return back the actor. This
diff --git a/src/engine/Actions/Index.ts b/src/engine/Actions/Index.ts
index eff76c4c7..4408e5783 100644
--- a/src/engine/Actions/Index.ts
+++ b/src/engine/Actions/Index.ts
@@ -23,6 +23,7 @@ export * from './Action/ScaleBy';
export * from './Action/ScaleTo';
export * from './Action/Delay';
export * from './Action/Flash';
-
+export * from './Action/CurveTo';
+export * from './Action/CurveBy';
export * from './ActionsComponent';
export * from './ActionsSystem';
diff --git a/src/engine/Math/bezier-curve.ts b/src/engine/Math/bezier-curve.ts
new file mode 100644
index 000000000..c7fb1c13a
--- /dev/null
+++ b/src/engine/Math/bezier-curve.ts
@@ -0,0 +1,156 @@
+import { lerpVector, remap } from './lerp';
+import { Vector } from './vector';
+
+export interface BezierCurveOptions {
+ /**
+ * [start, control1, control2, end]
+ */
+ controlPoints: [start: Vector, control1: Vector, control2: Vector, end: Vector];
+ quality?: number;
+}
+
+/**
+ * BezierCurve that supports cubic Bezier curves.
+ */
+export class BezierCurve {
+ // Thanks Freya! https://www.youtube.com/watch?v=aVwxzDHniEw
+ private _distLookup: number[] = [];
+ private _controlPoints: [Vector, Vector, Vector, Vector];
+ private _arcLength: number;
+ readonly quality: number = 4;
+ constructor(options: BezierCurveOptions) {
+ if (options.controlPoints.length !== 4) {
+ throw new Error('Only cubic bezier curves are supported');
+ }
+ this._controlPoints = [...options.controlPoints];
+ this.quality = options.quality ?? this.quality;
+
+ this._calculateLookup();
+ }
+
+ public get arcLength(): number {
+ return this._arcLength;
+ }
+
+ public get controlPoints(): readonly [start: Vector, control1: Vector, control2: Vector, end: Vector] {
+ return this._controlPoints;
+ }
+
+ public set controlPoints(points: [start: Vector, control1: Vector, control2: Vector, end: Vector]) {
+ this._controlPoints = [...points];
+ this._calculateLookup();
+ }
+
+ setControlPoint(index: 0 | 1 | 2 | 3, point: Vector) {
+ this._controlPoints[index] = point;
+ this._calculateLookup();
+ }
+
+ private _calculateLookup() {
+ let totalLength = 0;
+ this._distLookup.length = 0;
+ let prev = this.controlPoints[0];
+ const n = this.controlPoints.length * this.quality;
+ for (let i = 0; i < n; i++) {
+ const t = i / (n - 1);
+ const pt = this.getPoint(t);
+ const diff = prev.distance(pt);
+ totalLength += diff;
+ this._distLookup.push(totalLength);
+ prev = pt;
+ }
+ this._arcLength = totalLength;
+ }
+
+ private _getTimeGivenDistance(distance: number): number {
+ const n = this._distLookup.length;
+ const arcLength = this.arcLength;
+
+ if (distance >= 0 && distance < arcLength) {
+ for (let i = 0; i < n - 1; i++) {
+ if (this._distLookup[i] <= distance && distance < this._distLookup[i + 1]) {
+ return remap(this._distLookup[i], this._distLookup[i + 1], i / (n - 1), (i + 1) / (n - 1), distance);
+ }
+ }
+ }
+ return distance / arcLength;
+ }
+
+ /**
+ * Get the point on the Bezier curve at a certain time
+ * @param time Between 0-1
+ */
+ getPoint(time: number): Vector {
+ const points = [...this.controlPoints];
+
+ for (let r = 1; r < points.length; r++) {
+ for (let i = 0; i < points.length - r; i++) {
+ points[i] = lerpVector(points[i], points[i + 1], time);
+ }
+ }
+
+ return points[0];
+ }
+
+ /**
+ * Get the tangent of the Bezier curve at a certain time
+ * @param time Between 0-1
+ */
+ getTangent(time: number): Vector {
+ const timeSquared = time * time;
+ const p0 = this.controlPoints[0];
+ const p1 = this.controlPoints[1];
+ const p2 = this.controlPoints[2];
+ const p3 = this.controlPoints[3];
+
+ // Derivative of Bernstein polynomial
+ const pPrime = p0
+ .scale(-3 * timeSquared + 6 * time - 3)
+ .add(p1.scale(9 * timeSquared - 12 * time + 3).add(p2.scale(-9 * timeSquared + 6 * time).add(p3.scale(3 * timeSquared))));
+
+ return pPrime.normalize();
+ }
+
+ /**
+ * Get the tangent of the Bezier curve where the distance is uniformly distributed over time
+ * @param time
+ */
+ getUniformTangent(time: number): Vector {
+ const desiredDistance = time * this.arcLength;
+ const uniformTime = this._getTimeGivenDistance(desiredDistance);
+ return this.getTangent(uniformTime);
+ }
+
+ /**
+ * Get the normal of the Bezier curve at a certain time
+ * @param time Between 0-1
+ */
+ getNormal(time: number): Vector {
+ return this.getTangent(time).normal();
+ }
+
+ /**
+ * Get the normal of the Bezier curve where the distance is uniformly distributed over time
+ * @param time
+ */
+ getUniformNormal(time: number): Vector {
+ return this.getUniformTangent(time).normal();
+ }
+
+ /**
+ * Points are spaced uniformly across the length of the curve over time
+ * @param time
+ */
+ getUniformPoint(time: number): Vector {
+ const desiredDistance = time * this.arcLength;
+ const uniformTime = this._getTimeGivenDistance(desiredDistance);
+ return this.getPoint(uniformTime);
+ }
+
+ clone(): BezierCurve {
+ return new BezierCurve({
+ controlPoints: [...this.controlPoints],
+ quality: this.quality
+ });
+ }
+}
diff --git a/src/engine/Math/index.ts b/src/engine/Math/index.ts
index b75deaeb3..bd6027610 100644
--- a/src/engine/Math/index.ts
+++ b/src/engine/Math/index.ts
@@ -10,4 +10,5 @@ export * from './line-segment';
export * from './projection';
export * from './ray';
export * from './lerp';
+export * from './bezier-curve';
export * from './util';
diff --git a/src/spec/BezierSpec.ts b/src/spec/BezierSpec.ts
new file mode 100644
index 000000000..7c885d5a7
--- /dev/null
+++ b/src/spec/BezierSpec.ts
@@ -0,0 +1,63 @@
+import * as ex from '@excalibur';
+import { ExcaliburMatchers } from 'excalibur-jasmine';
+
+describe('A BezierCurve', () => {
+ beforeAll(() => {
+ jasmine.addMatchers(ExcaliburMatchers);
+ });
+ it('exists', () => {
+ expect(ex.BezierCurve).toBeDefined();
+ });
+
+ it('only supports cubic', () => {
+ const throws = () => {
+ const curve = new ex.BezierCurve({ controlPoints: [] as any });
+ };
+ expect(throws).toThrowError('Only cubic bezier curves are supported');
+ });
+
+ it('can build a cubic bezier curve', () => {
+ const curve = new ex.BezierCurve({
+ controlPoints: [ex.vec(100, 100), ex.vec(0, 200), ex.vec(0, -200), ex.vec(800, 0)]
+ });
+
+ expect(curve.getPoint(0)).toBeVector(ex.vec(100, 100));
+ expect(curve.getPoint(0.5)).toBeVector(ex.vec(112.5, 12.5));
+ expect(curve.getNormal(0.5)).toBeVector(ex.vec(-0.5812, -0.8137));
+ expect(curve.getTangent(0.5)).toBeVector(ex.vec(0.8137, -0.5812));
+ expect(curve.getPoint(1)).toBeVector(ex.vec(800, 0));
+
+ expect(curve.getUniformPoint(0)).toBeVector(ex.vec(100, 100));
+ expect(curve.getUniformPoint(0.5)).toBeVector(ex.vec(366.77, -56.17));
+ expect(curve.getUniformNormal(0.5)).toBeVector(ex.vec(-0.0375, -0.9992));
+ expect(curve.getUniformTangent(0.5)).toBeVector(ex.vec(0.9992, -0.0375));
+ expect(curve.getUniformPoint(1)).toBeVector(ex.vec(800, 0));
+ });
+
+ it('can get and set control points', () => {
+ const curve = new ex.BezierCurve({
+ controlPoints: [ex.vec(100, 100), ex.vec(0, 200), ex.vec(0, -200), ex.vec(800, 0)]
+ });
+
+ expect(curve.controlPoints).toEqual([ex.vec(100, 100), ex.vec(0, 200), ex.vec(0, -200), ex.vec(800, 0)]);
+
+ curve.setControlPoint(2, ex.vec(99, 99));
+
+ expect(curve.controlPoints).toEqual([ex.vec(100, 100), ex.vec(0, 200), ex.vec(99, 99), ex.vec(800, 0)]);
+
+ curve.controlPoints = [ex.vec(0, 100), ex.vec(0, 200), ex.vec(0, -200), ex.vec(0, 0)];
+
+ expect(curve.controlPoints).toEqual([ex.vec(0, 100), ex.vec(0, 200), ex.vec(0, -200), ex.vec(0, 0)]);
+ });
+
+ it('can be cloned', () => {
+ const curve = new ex.BezierCurve({
+ controlPoints: [ex.vec(100, 100), ex.vec(0, 200), ex.vec(0, -200), ex.vec(800, 0)],
+ quality: 12
+ });
+
+ const clone = curve.clone();
+
+ expect(clone).toEqual(curve);
+ });
+});
diff --git a/src/spec/CurveActionSpec.ts b/src/spec/CurveActionSpec.ts
new file mode 100644
index 000000000..7d36b504f
--- /dev/null
+++ b/src/spec/CurveActionSpec.ts
@@ -0,0 +1,102 @@
+import * as ex from '@excalibur';
+import { ExcaliburMatchers } from 'excalibur-jasmine';
+import { TestUtils } from './util/TestUtils';
+
+describe('A actor can curve', () => {
+ beforeAll(() => {
+ jasmine.addMatchers(ExcaliburMatchers);
+ });
+
+ let engine: ex.Engine;
+ let clock: ex.TestClock;
+ beforeEach(async () => {
+ engine = TestUtils.engine();
+ await TestUtils.runToReady(engine);
+ await engine.start();
+ clock = engine.clock as ex.TestClock;
+ });
+
+ afterEach(() => {
+ engine.stop();
+ engine.dispose();
+ engine = null;
+ });
+
+ it('exists', () => {
+ expect(ex.CurveTo).toBeDefined();
+ });
+
+ it('an actor can curveTo ', async () => {
+ const actor = new ex.Actor({
+ pos: ex.vec(200, 202)
+ });
+ const scene = engine.currentScene;
+ scene.add(actor);
+
+ actor.actions.curveTo({
+ controlPoints: [ex.vec(100, 100), ex.vec(0, 0), ex.vec(100, 200)],
+ durationMs: 1000
+ });
+
+ clock.step(0);
+ clock.run(10, 100);
+
+ expect(actor.pos).toBeVector(ex.vec(100, 200));
+ });
+
+ it('an actor can curveBy ', async () => {
+ const actor = new ex.Actor({
+ pos: ex.vec(200, 202)
+ });
+ const scene = engine.currentScene;
+ scene.add(actor);
+
+ actor.actions.curveBy({
+ controlPoints: [ex.vec(0, 100), ex.vec(0, 100), ex.vec(100, 200)],
+ durationMs: 1000
+ });
+
+ clock.step(0);
+ clock.run(10, 100);
+
+ expect(actor.pos).toBeVector(ex.vec(300, 402));
+ });
+
+ it('curveBy can be stopped', () => {
+ const actor = new ex.Actor({
+ pos: ex.vec(200, 202)
+ });
+ const scene = engine.currentScene;
+ scene.add(actor);
+
+ actor.actions.curveBy({
+ controlPoints: [ex.vec(0, 100), ex.vec(0, 100), ex.vec(100, 200)],
+ durationMs: 1000
+ });
+
+ clock.step(0);
+ clock.run(5, 100);
+ const pos = actor.pos.clone();
+ actor.actions.getQueue().getActions()[0].stop();
+ expect(actor.pos).toBeVector(pos);
+ });
+
+ it('curveTo can be stopped', () => {
+ const actor = new ex.Actor({
+ pos: ex.vec(200, 202)
+ });
+ const scene = engine.currentScene;
+ scene.add(actor);
+
+ actor.actions.curveTo({
+ controlPoints: [ex.vec(0, 100), ex.vec(0, 100), ex.vec(100, 200)],
+ durationMs: 1000
+ });
+
+ clock.step(0);
+ clock.run(5, 100);
+ const pos = actor.pos.clone();
+ actor.actions.getQueue().getActions()[0].stop();
+ expect(actor.pos).toBeVector(pos);
+ });
+});