From d3c8c916d28ebe5851bc96fb71f64fe73144fc00 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sun, 15 May 2022 13:18:36 -0500 Subject: [PATCH] feat: [#2313] Add New Line Graphics Object (#2314) Related #2313 This PR adds a new graphics object `Line` for drawing lines using the graphics component! ```typescript const lineActor = new ex.Actor({ pos: ex.vec(100, 0) }); lineActor.graphics.anchor = ex.Vector.Zero; lineActor.graphics.use(new ex.Line({ start: ex.vec(0, 0), end: ex.vec(200, 200), color: ex.Color.Green, thickness: 10 })); game.add(lineActor); ``` --- CHANGELOG.md | 15 +++ .../Collision/Colliders/CircleCollider.ts | 4 +- .../Colliders/ClosestLineJumpTable.ts | 12 +- src/engine/Collision/Colliders/Collider.ts | 4 +- .../Collision/Colliders/CollisionJumpTable.ts | 12 +- .../Collision/Colliders/CompositeCollider.ts | 6 +- .../Collision/Colliders/EdgeCollider.ts | 12 +- .../Collision/Colliders/PolygonCollider.ts | 22 ++-- .../Collision/Colliders/SeparatingAxis.ts | 8 +- src/engine/Graphics/Line.ts | 42 +++++++ src/engine/Graphics/index.ts | 2 +- src/engine/Math/Index.ts | 2 +- src/engine/Math/{line.ts => line-segment.ts} | 10 +- src/engine/Math/ray.ts | 6 +- src/spec/AlgebraSpec.ts | 46 ++++---- src/spec/CompositeColliderSpec.ts | 14 +-- src/spec/LineSpec.ts | 103 ++++++++++++++++++ src/spec/images/LineSpec/line.png | Bin 0 -> 667 bytes 18 files changed, 240 insertions(+), 80 deletions(-) create mode 100644 src/engine/Graphics/Line.ts rename src/engine/Math/{line.ts => line-segment.ts} (96%) create mode 100644 src/spec/LineSpec.ts create mode 100644 src/spec/images/LineSpec/line.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 78b9d0c0..17afc4d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Breaking Changes +- `ex.Line` has be replaced with a new Graphics type, the old geometric behavior is now under the type `ex.LineSegment` - Notable deprecated types removed - `ex.SortedList` old sorted list is removed - `ex.Collection` old collection type is removed @@ -65,6 +66,20 @@ This project adheres to [Semantic Versioning](http://semver.org/). - ### Added +- Added new `ex.Line` graphics object for drawing lines! + ```typescript + const lineActor = new ex.Actor({ + pos: ex.vec(100, 0) + }); + lineActor.graphics.anchor = ex.Vector.Zero; + lineActor.graphics.use(new ex.Line({ + start: ex.vec(0, 0), + end: ex.vec(200, 200), + color: ex.Color.Green, + thickness: 10 + })); + game.add(lineActor); + ``` - Added new performance fallback configuration to `ex.Engine` for developers to help players experiencing poor performance in non-standard browser configurations * This will fallback to the Canvas2D rendering graphics context which usually performs better on non hardware accelerated browsers, currently postprocessing effects are unavailable in this fallback. * By default if a game is running at 20fps or lower for 100 frames or more after the game has started it will be triggered, the developer can optionally show a player message that is off by default. diff --git a/src/engine/Collision/Colliders/CircleCollider.ts b/src/engine/Collision/Colliders/CircleCollider.ts index 39bdb37a..b2cfccc2 100644 --- a/src/engine/Collision/Colliders/CircleCollider.ts +++ b/src/engine/Collision/Colliders/CircleCollider.ts @@ -5,7 +5,7 @@ import { PolygonCollider } from './PolygonCollider'; import { EdgeCollider } from './EdgeCollider'; import { Projection } from '../../Math/projection'; -import { Line } from '../../Math/line'; +import { LineSegment } from '../../Math/line-segment'; import { Vector } from '../../Math/vector'; import { Ray } from '../../Math/ray'; import { Color } from '../../Color'; @@ -152,7 +152,7 @@ export class CircleCollider extends Collider { } } - public getClosestLineBetween(shape: Collider): Line { + public getClosestLineBetween(shape: Collider): LineSegment { if (shape instanceof CircleCollider) { return ClosestLineJumpTable.CircleCircleClosestLine(this, shape); } else if (shape instanceof PolygonCollider) { diff --git a/src/engine/Collision/Colliders/ClosestLineJumpTable.ts b/src/engine/Collision/Colliders/ClosestLineJumpTable.ts index 2dd93e1b..4fe9a905 100644 --- a/src/engine/Collision/Colliders/ClosestLineJumpTable.ts +++ b/src/engine/Collision/Colliders/ClosestLineJumpTable.ts @@ -1,4 +1,4 @@ -import { Line } from '../../Math/line'; +import { LineSegment } from '../../Math/line-segment'; import { Vector } from '../../Math/vector'; import { Ray } from '../../Math/ray'; import { PolygonCollider } from './PolygonCollider'; @@ -53,7 +53,7 @@ export function ClosestLine(p0: Vector, u: Vector, q0: Vector, v: Vector) { // if denom is 0 they are parallel, use any point from either as the start in this case p0 if (denom === 0 || denom <= 0.01) { const tClosestParallel = d / b; - return new Line(p0, q0.add(v.scale(tClosestParallel))); + return new LineSegment(p0, q0.add(v.scale(tClosestParallel))); } // Solve for sClosest for infinite line @@ -97,7 +97,7 @@ export function ClosestLine(p0: Vector, u: Vector, q0: Vector, v: Vector) { sClosest = Math.abs(sClosest) < 0.001 ? 0 : sClosest / sDenom; tClosest = Math.abs(tClosest) < 0.001 ? 0 : tClosest / tDenom; - return new Line(p0.add(u.scale(sClosest)), q0.add(v.scale(tClosest))); + return new LineSegment(p0.add(u.scale(sClosest)), q0.add(v.scale(tClosest))); } export const ClosestLineJumpTable = { @@ -183,7 +183,7 @@ export const ClosestLineJumpTable = { const circlex = ((p0.x + u.x * t - otherWorldPos.x) * circle.radius) / (circle.radius + d); const circley = ((p0.y + u.y * t - otherWorldPos.y) * circle.radius) / (circle.radius + d); - return new Line(u.scale(t).add(p0), new Vector(otherWorldPos.x + circlex, otherWorldPos.y + circley)); + return new LineSegment(u.scale(t).add(p0), new Vector(otherWorldPos.x + circlex, otherWorldPos.y + circley)); }, CircleCircleClosestLine(circleA: CircleCollider, circleB: CircleCollider) { @@ -200,7 +200,7 @@ export const ClosestLineJumpTable = { const thisPoint = circleA.rayCast(rayTowardsOther); const otherPoint = circleB.rayCast(rayTowardsThis); - return new Line(thisPoint, otherPoint); + return new LineSegment(thisPoint, otherPoint); }, CircleEdgeClosestLine(circle: CircleCollider, edge: EdgeCollider) { @@ -229,7 +229,7 @@ export const ClosestLineJumpTable = { const circlex = ((p0.x + u.x * t - circleWorlPos.x) * circle.radius) / (circle.radius + d); const circley = ((p0.y + u.y * t - circleWorlPos.y) * circle.radius) / (circle.radius + d); - return new Line(u.scale(t).add(p0), new Vector(circleWorlPos.x + circlex, circleWorlPos.y + circley)); + return new LineSegment(u.scale(t).add(p0), new Vector(circleWorlPos.x + circlex, circleWorlPos.y + circley)); }, EdgeEdgeClosestLine(edgeA: EdgeCollider, edgeB: EdgeCollider) { diff --git a/src/engine/Collision/Colliders/Collider.ts b/src/engine/Collision/Colliders/Collider.ts index b654248b..21de6197 100644 --- a/src/engine/Collision/Colliders/Collider.ts +++ b/src/engine/Collision/Colliders/Collider.ts @@ -2,7 +2,7 @@ import { Color } from '../../Color'; import { CollisionContact } from '../Detection/CollisionContact'; import { BoundingBox } from '../BoundingBox'; import { Projection } from '../../Math/projection'; -import { Line } from '../../Math/line'; +import { LineSegment } from '../../Math/line-segment'; import { Vector } from '../../Math/vector'; import { Ray } from '../../Math/ray'; import { Clonable } from '../../Interfaces/Clonable'; @@ -89,7 +89,7 @@ export abstract class Collider implements Clonable { * Returns the closest line between the surfaces this collider and another * @param collider */ - abstract getClosestLineBetween(collider: Collider): Line; + abstract getClosestLineBetween(collider: Collider): LineSegment; /** * Return wether the collider contains a point inclusive to it's border diff --git a/src/engine/Collision/Colliders/CollisionJumpTable.ts b/src/engine/Collision/Colliders/CollisionJumpTable.ts index 534aa1b0..bb635c22 100644 --- a/src/engine/Collision/Colliders/CollisionJumpTable.ts +++ b/src/engine/Collision/Colliders/CollisionJumpTable.ts @@ -3,7 +3,7 @@ import { CollisionContact } from '../Detection/CollisionContact'; import { PolygonCollider } from './PolygonCollider'; import { EdgeCollider } from './EdgeCollider'; import { SeparatingAxis, SeparationInfo } from './SeparatingAxis'; -import { Line } from '../../Math/line'; +import { LineSegment } from '../../Math/line-segment'; import { Vector } from '../../Math/vector'; import { TransformComponent } from '../../EntityComponentSystem'; import { Pair } from '../Detection/Pair'; @@ -234,7 +234,7 @@ export const CollisionJumpTable = { // The incident side is the most opposite from the axes of collision on the other collider const other = separation.collider === polyA ? polyB : polyA; - const incident = other.findSide(separation.axis.negate()) as Line; + const incident = other.findSide(separation.axis.negate()) as LineSegment; // Clip incident side by the perpendicular lines at each end of the reference side // https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm @@ -243,7 +243,7 @@ export const CollisionJumpTable = { // Find our contact points by clipping the incident by the collision side const clipRight = incident.clip(refDir.negate(), -refDir.dot(reference.begin)); - let clipLeft: Line | null = null; + let clipLeft: LineSegment | null = null; if (clipRight) { clipLeft = clipRight.clip(refDir, refDir.dot(reference.end)); } @@ -294,13 +294,13 @@ export const CollisionJumpTable = { // both are polygons if (shapeA instanceof PolygonCollider && shapeB instanceof PolygonCollider) { if (contact.info.localSide) { - let side: Line; + let side: LineSegment; let worldPoint: Vector; if (contact.info.collider === shapeA) { - side = new Line(txA.apply(contact.info.localSide.begin), txA.apply(contact.info.localSide.end)); + side = new LineSegment(txA.apply(contact.info.localSide.begin), txA.apply(contact.info.localSide.end)); worldPoint = txB.apply(localPoint); } else { - side = new Line(txB.apply(contact.info.localSide.begin), txB.apply(contact.info.localSide.end)); + side = new LineSegment(txB.apply(contact.info.localSide.begin), txB.apply(contact.info.localSide.end)); worldPoint = txA.apply(localPoint); } diff --git a/src/engine/Collision/Colliders/CompositeCollider.ts b/src/engine/Collision/Colliders/CompositeCollider.ts index 94d33bed..a85d169c 100644 --- a/src/engine/Collision/Colliders/CompositeCollider.ts +++ b/src/engine/Collision/Colliders/CompositeCollider.ts @@ -3,7 +3,7 @@ import { Pair } from '../Detection/Pair'; import { Color } from '../../Color'; import { Transform } from '../../EntityComponentSystem'; import { ExcaliburGraphicsContext } from '../../Graphics/Context/ExcaliburGraphicsContext'; -import { Line } from '../../Math/line'; +import { LineSegment } from '../../Math/line-segment'; import { Projection } from '../../Math/projection'; import { Ray } from '../../Math/ray'; import { Vector } from '../../Math/vector'; @@ -137,9 +137,9 @@ export class CompositeCollider extends Collider { return contacts; } - getClosestLineBetween(other: Collider): Line { + getClosestLineBetween(other: Collider): LineSegment { const colliders = this.getColliders(); - const lines: Line[] = []; + const lines: LineSegment[] = []; if (other instanceof CompositeCollider) { const otherColliders = other.getColliders(); for (const colliderA of colliders) { diff --git a/src/engine/Collision/Colliders/EdgeCollider.ts b/src/engine/Collision/Colliders/EdgeCollider.ts index ffee13b6..6f3316b7 100644 --- a/src/engine/Collision/Colliders/EdgeCollider.ts +++ b/src/engine/Collision/Colliders/EdgeCollider.ts @@ -5,7 +5,7 @@ import { CircleCollider } from './CircleCollider'; import { PolygonCollider } from './PolygonCollider'; import { Projection } from '../../Math/projection'; -import { Line } from '../../Math/line'; +import { LineSegment } from '../../Math/line-segment'; import { Vector } from '../../Math/vector'; import { Ray } from '../../Math/ray'; import { Color } from '../../Color'; @@ -147,7 +147,7 @@ export class EdgeCollider extends Collider { * Returns the closes line between this and another collider, from this -> collider * @param shape */ - public getClosestLineBetween(shape: Collider): Line { + public getClosestLineBetween(shape: Collider): LineSegment { if (shape instanceof CircleCollider) { return ClosestLineJumpTable.CircleEdgeClosestLine(shape, this); } else if (shape instanceof PolygonCollider) { @@ -217,15 +217,15 @@ export class EdgeCollider extends Collider { /** * Returns this edge represented as a line in world coordinates */ - public asLine(): Line { - return new Line(this._getTransformedBegin(), this._getTransformedEnd()); + public asLine(): LineSegment { + return new LineSegment(this._getTransformedBegin(), this._getTransformedEnd()); } /** * Return this edge as a line in local line coordinates (relative to the position) */ - public asLocalLine(): Line { - return new Line(this.begin, this.end); + public asLocalLine(): LineSegment { + return new LineSegment(this.begin, this.end); } /** diff --git a/src/engine/Collision/Colliders/PolygonCollider.ts b/src/engine/Collision/Colliders/PolygonCollider.ts index e2001806..7ce7181f 100644 --- a/src/engine/Collision/Colliders/PolygonCollider.ts +++ b/src/engine/Collision/Colliders/PolygonCollider.ts @@ -5,7 +5,7 @@ import { CollisionJumpTable } from './CollisionJumpTable'; import { CircleCollider } from './CircleCollider'; import { CollisionContact } from '../Detection/CollisionContact'; import { Projection } from '../../Math/projection'; -import { Line } from '../../Math/line'; +import { LineSegment } from '../../Math/line-segment'; import { Vector } from '../../Math/vector'; import { Matrix } from '../../Math/matrix'; import { Ray } from '../../Math/ray'; @@ -59,8 +59,8 @@ export class PolygonCollider extends Collider { private _transformedPoints: Vector[] = []; private _axes: Vector[] = []; - private _sides: Line[] = []; - private _localSides: Line[] = []; + private _sides: LineSegment[] = []; + private _localSides: LineSegment[] = []; constructor(options: PolygonColliderOptions) { super(); @@ -301,7 +301,7 @@ export class PolygonCollider extends Collider { /** * Gets the sides of the polygon in world space */ - public getSides(): Line[] { + public getSides(): LineSegment[] { if (this._sides.length) { return this._sides; } @@ -310,7 +310,7 @@ export class PolygonCollider extends Collider { const len = points.length; for (let i = 0; i < len; i++) { // This winding is important - lines.push(new Line(points[i], points[(i + 1) % len])); + lines.push(new LineSegment(points[i], points[(i + 1) % len])); } this._sides = lines; return this._sides; @@ -319,7 +319,7 @@ export class PolygonCollider extends Collider { /** * Returns the local coordinate space sides */ - public getLocalSides(): Line[] { + public getLocalSides(): LineSegment[] { if (this._localSides.length) { return this._localSides; } @@ -328,7 +328,7 @@ export class PolygonCollider extends Collider { const len = points.length; for (let i = 0; i < len; i++) { // This winding is important - lines.push(new Line(points[i], points[(i + 1) % len])); + lines.push(new LineSegment(points[i], points[(i + 1) % len])); } this._localSides = lines; return this._localSides; @@ -338,7 +338,7 @@ export class PolygonCollider extends Collider { * Given a direction vector find the world space side that is most in that direction * @param direction */ - public findSide(direction: Vector): Line { + public findSide(direction: Vector): LineSegment { const sides = this.getSides(); let bestSide = sides[0]; let maxDistance = -Number.MAX_VALUE; @@ -358,7 +358,7 @@ export class PolygonCollider extends Collider { * Given a direction vector find the local space side that is most in that direction * @param direction */ - public findLocalSide(direction: Vector): Line { + public findLocalSide(direction: Vector): LineSegment { const sides = this.getLocalSides(); let bestSide = sides[0]; let maxDistance = -Number.MAX_VALUE; @@ -427,7 +427,7 @@ export class PolygonCollider extends Collider { return true; } - public getClosestLineBetween(collider: Collider): Line { + public getClosestLineBetween(collider: Collider): LineSegment { if (collider instanceof CircleCollider) { return ClosestLineJumpTable.PolygonCircleClosestLine(this, collider); } else if (collider instanceof PolygonCollider) { @@ -495,7 +495,7 @@ export class PolygonCollider extends Collider { * Finds the closes face to the point using perpendicular distance * @param point point to test against polygon */ - public getClosestFace(point: Vector): { distance: Vector; face: Line } { + public getClosestFace(point: Vector): { distance: Vector; face: LineSegment } { const sides = this.getSides(); let min = Number.POSITIVE_INFINITY; let faceIndex = -1; diff --git a/src/engine/Collision/Colliders/SeparatingAxis.ts b/src/engine/Collision/Colliders/SeparatingAxis.ts index d8398796..01a32ab7 100644 --- a/src/engine/Collision/Colliders/SeparatingAxis.ts +++ b/src/engine/Collision/Colliders/SeparatingAxis.ts @@ -1,4 +1,4 @@ -import { Line } from '../../Math/line'; +import { LineSegment } from '../../Math/line-segment'; import { Vector } from '../../Math/vector'; import { Collider } from './Collider'; import { CircleCollider } from './CircleCollider'; @@ -27,12 +27,12 @@ export interface SeparationInfo { * Side of separation (reference) from the collider's perspective */ - side?: Line; + side?: LineSegment; /** * Local side of separation (reference) from the collider's perspective */ - localSide?: Line; + localSide?: LineSegment; /** * Index of the separation side (reference) from the collider's perspective @@ -53,7 +53,7 @@ export interface SeparationInfo { export class SeparatingAxis { static findPolygonPolygonSeparation(polyA: PolygonCollider, polyB: PolygonCollider): SeparationInfo { let bestSeparation = -Number.MAX_VALUE; - let bestSide: Line | null = null; + let bestSide: LineSegment | null = null; let bestAxis: Vector | null = null; let bestSideIndex: number = -1; let bestOtherPoint: Vector | null = null; diff --git a/src/engine/Graphics/Line.ts b/src/engine/Graphics/Line.ts new file mode 100644 index 00000000..701d87c5 --- /dev/null +++ b/src/engine/Graphics/Line.ts @@ -0,0 +1,42 @@ +import { BoundingBox } from '../Collision/BoundingBox'; +import { Color } from '../Color'; +import { Vector } from '../Math/vector'; +import { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext'; +import { Graphic } from './Graphic'; + +export interface LineOptions { + start: Vector; + end: Vector; + color?: Color; + thickness?: number; +} +export class Line extends Graphic { + readonly start: Vector; + readonly end: Vector; + color: Color = Color.Black; + thickness: number = 1; + constructor(options: LineOptions) { + super(); + const { start, end, color, thickness } = options; + this.start = start; + this.end = end; + this.color = color ?? this.color; + this.thickness = thickness ?? this.thickness; + const { width, height } = BoundingBox.fromPoints([start, end]); + this.width = width; + this.height = height; + } + + protected _drawImage(ctx: ExcaliburGraphicsContext, _x: number, _y: number): void { + ctx.drawLine(this.start, this.end, this.color, this.thickness); + } + + clone(): Line { + return new Line({ + start: this.start, + end: this.end, + color: this.color, + thickness: this.thickness + }); + } +} \ No newline at end of file diff --git a/src/engine/Graphics/index.ts b/src/engine/Graphics/index.ts index 9ef6691c..c04e88b9 100644 --- a/src/engine/Graphics/index.ts +++ b/src/engine/Graphics/index.ts @@ -5,6 +5,7 @@ export * from './SpriteSheet'; export * from './GraphicsGroup'; export * from './ImageSource'; export * from './Animation'; +export * from './Line'; // Graphics ECS export * from './GraphicsComponent'; @@ -13,7 +14,6 @@ export * from './GraphicsSystem'; export * from './OffscreenSystem'; export * from './ParallaxComponent'; - // Raster graphics export * from './Raster'; export * from './Circle'; diff --git a/src/engine/Math/Index.ts b/src/engine/Math/Index.ts index 49af487d..981764c2 100644 --- a/src/engine/Math/Index.ts +++ b/src/engine/Math/Index.ts @@ -3,7 +3,7 @@ export * from './vector-view'; export * from './matrix'; export * from './Random'; export * from './global-coordinates'; -export * from './line'; +export * from './line-segment'; export * from './projection'; export * from './ray'; export * from './util'; \ No newline at end of file diff --git a/src/engine/Math/line.ts b/src/engine/Math/line-segment.ts similarity index 96% rename from src/engine/Math/line.ts rename to src/engine/Math/line-segment.ts index b72f83d6..f104aed5 100644 --- a/src/engine/Math/line.ts +++ b/src/engine/Math/line-segment.ts @@ -4,7 +4,7 @@ import { Vector } from './vector'; * A 2D line segment */ -export class Line { +export class LineSegment { /** * @param begin The starting point of the line segment * @param end The ending point of the line segment @@ -79,8 +79,8 @@ export class Line { /** * Flips the direction of the line segment */ - public flip(): Line { - return new Line(this.end, this.begin); + public flip(): LineSegment { + return new LineSegment(this.end, this.begin); } /** @@ -97,7 +97,7 @@ export class Line { * @param sideVector Vector that traces the line * @param length Length to clip along side */ - public clip(sideVector: Vector, length: number): Line { + public clip(sideVector: Vector, length: number): LineSegment { let dir = sideVector; dir = dir.normalize(); @@ -120,7 +120,7 @@ export class Line { return null; } - return new Line(results[0], results[1]); + return new LineSegment(results[0], results[1]); } /** diff --git a/src/engine/Math/ray.ts b/src/engine/Math/ray.ts index f56e426e..44989fc8 100644 --- a/src/engine/Math/ray.ts +++ b/src/engine/Math/ray.ts @@ -1,4 +1,4 @@ -import { Line } from './line'; +import { LineSegment } from './line-segment'; import { Vector } from './vector'; /** @@ -23,7 +23,7 @@ export class Ray { * This number indicates the mathematical intersection time. * @param line The line to test */ - public intersect(line: Line): number { + public intersect(line: LineSegment): number { const numerator = line.begin.sub(this.pos); // Test is line and ray are parallel and non intersecting @@ -48,7 +48,7 @@ export class Ray { return -1; } - public intersectPoint(line: Line): Vector { + public intersectPoint(line: LineSegment): Vector { const time = this.intersect(line); if (time < 0) { return null; diff --git a/src/spec/AlgebraSpec.ts b/src/spec/AlgebraSpec.ts index 0958072f..8087f6d0 100644 --- a/src/spec/AlgebraSpec.ts +++ b/src/spec/AlgebraSpec.ts @@ -274,7 +274,7 @@ describe('Rays', () => { it('can intersect with lines', () => { const ray = new ex.Ray(ex.Vector.Zero.clone(), new ex.Vector(1, 0)); - const line = new ex.Line(new ex.Vector(1, -1), new ex.Vector(1, 1)); + const line = new ex.LineSegment(new ex.Vector(1, -1), new ex.Vector(1, 1)); const intersection = ray.intersect(line); expect(intersection).toBeGreaterThan(0); @@ -287,67 +287,67 @@ describe('Rays', () => { describe('Lines', () => { it('exist', () => { - expect(ex.Line).toBeDefined(); + expect(ex.LineSegment).toBeDefined(); }); it('can be constructed', () => { - const line = new ex.Line(new ex.Vector(1, -1), new ex.Vector(1, 1)); + const line = new ex.LineSegment(new ex.Vector(1, -1), new ex.Vector(1, 1)); expect(line).toBeTruthy(); }); it('can have a slope Vector', () => { - const line = new ex.Line(new ex.Vector(1, -1), new ex.Vector(1, 1)); + const line = new ex.LineSegment(new ex.Vector(1, -1), new ex.Vector(1, 1)); const slope = line.getSlope(); expect(slope.equals(new ex.Vector(0, 1))).toBeTruthy(); }); it('can have a slope value of infinity', () => { - const line = new ex.Line(new ex.Vector(1, 0), new ex.Vector(1, 1)); + const line = new ex.LineSegment(new ex.Vector(1, 0), new ex.Vector(1, 1)); const slope = line.slope; expect(line.slope).toBe(Infinity); }); it('can have a slope value of 0', () => { - const line = new ex.Line(new ex.Vector(1, 1), new ex.Vector(2, 1)); + const line = new ex.LineSegment(new ex.Vector(1, 1), new ex.Vector(2, 1)); const slope = line.slope; expect(line.slope).toBe(0); }); it('can have a positive slope value', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(1, 1)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(1, 1)); expect(line.slope).toBe(1); }); it('can have a negative slope value', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(-1, 1)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(-1, 1)); expect(line.slope).toBe(-1); }); it('can have a y intercept of infinity', () => { - const line = new ex.Line(new ex.Vector(1, 0), new ex.Vector(1, 1)); + const line = new ex.LineSegment(new ex.Vector(1, 0), new ex.Vector(1, 1)); expect(line.intercept).toBe(-Infinity); }); it('can have a positive y intercept', () => { - const line = new ex.Line(new ex.Vector(1, 2), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(1, 2), new ex.Vector(2, 2)); expect(line.intercept).toBe(2); }); it('can have a negative y intercept', () => { - const line = new ex.Line(new ex.Vector(1, 1), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(1, 1), new ex.Vector(2, 2)); expect(line.intercept).toBe(0); }); it('can have a normal', () => { - const line = new ex.Line(new ex.Vector(1, 1), new ex.Vector(2, 1)); + const line = new ex.LineSegment(new ex.Vector(1, 1), new ex.Vector(2, 1)); const normal = line.normal(); expect(normal.x).toBe(0); @@ -355,73 +355,73 @@ describe('Lines', () => { }); it('can have a length', () => { - const line = new ex.Line(new ex.Vector(1, -1), new ex.Vector(1, 1)); + const line = new ex.LineSegment(new ex.Vector(1, -1), new ex.Vector(1, 1)); expect(line.getLength()).toBe(2); }); it('can find a point by X value', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(2, 2)); const t = line.findPoint(1); expect(t.y).toBe(1); }); it('can find a point by Y value', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(2, 2)); const t = line.findPoint(null, 1); expect(t.x).toBe(1); }); it('can calculate the perpendicular distance from a point', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(200, 0)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(200, 0)); const point = new ex.Vector(100, 100); expect(line.distanceToPoint(point)).toBe(100); }); it('can calculate the perpendicular distance from a point not above the line', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(200, 0)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(200, 0)); const point = new ex.Vector(-100, 100); expect(line.distanceToPoint(point)).toBe(100); }); it('can determine if point lies on the line by x and y', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(2, 2)); const t = line.hasPoint(1, 1); expect(t).toBe(true); }); it('can determine if point lies on the line by Vector', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(2, 2)); const t = line.hasPoint(new ex.Vector(1, 1)); expect(t).toBe(true); }); it('can determine if point lies on the line by x and y with absolute precision', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(2, 2)); const t = line.hasPoint(1.005, 1); expect(t).toBe(false); }); it('can determine if point lies on the line by Vector with absolute precision', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(2, 2)); const t = line.hasPoint(new ex.Vector(1.005, 1)); expect(t).toBe(false); }); it('can determine if point lies on the line by x and y at a certain threshold', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(2, 2)); const t = line.hasPoint(1.005, 1, 0.1); expect(t).toBe(true); }); it('can determine if point lies on the line by Vector at a certain threshold', () => { - const line = new ex.Line(new ex.Vector(0, 0), new ex.Vector(2, 2)); + const line = new ex.LineSegment(new ex.Vector(0, 0), new ex.Vector(2, 2)); const t = line.hasPoint(new ex.Vector(1.005, 1), 0.1); expect(t).toBe(true); diff --git a/src/spec/CompositeColliderSpec.ts b/src/spec/CompositeColliderSpec.ts index b6d00f52..fec39dd6 100644 --- a/src/spec/CompositeColliderSpec.ts +++ b/src/spec/CompositeColliderSpec.ts @@ -1,5 +1,5 @@ import * as ex from '@excalibur'; -import { BoundingBox, GameEvent, Line, Projection, Ray, TransformComponent, vec, Vector } from '@excalibur'; +import { BoundingBox, GameEvent, LineSegment, Projection, Ray, TransformComponent, vec, Vector } from '@excalibur'; import { ExcaliburAsyncMatchers, ExcaliburMatchers } from 'excalibur-jasmine'; describe('A CompositeCollider', () => { beforeAll(() => { @@ -79,22 +79,22 @@ describe('A CompositeCollider', () => { xf.pos = vec(300, 0); circle.update(xf); const lineRight = compCollider.getClosestLineBetween(circle); - expect(lineRight).toEqual(new Line(vec(250, 0), vec(100, 0))); + expect(lineRight).toEqual(new LineSegment(vec(250, 0), vec(100, 0))); xf.pos = vec(0, -300); circle.update(xf); const lineTop = compCollider.getClosestLineBetween(circle); - expect(lineTop).toEqual(new Line(vec(0, -250), vec(0, -50))); + expect(lineTop).toEqual(new LineSegment(vec(0, -250), vec(0, -50))); xf.pos = vec(0, 300); circle.update(xf); const lineBottom = compCollider.getClosestLineBetween(circle); - expect(lineBottom).toEqual(new Line(vec(0, 250), vec(0, 50))); + expect(lineBottom).toEqual(new LineSegment(vec(0, 250), vec(0, 50))); xf.pos = vec(-300, 0); circle.update(xf); const lineLeft = compCollider.getClosestLineBetween(circle); - expect(lineLeft).toEqual(new Line(vec(-250, 0), vec(-100, 0))); + expect(lineLeft).toEqual(new LineSegment(vec(-250, 0), vec(-100, 0))); }); it('can get the closest line between other composite colliders', () => { @@ -107,13 +107,13 @@ describe('A CompositeCollider', () => { compCollider2.update(xf); const line = compCollider1.getClosestLineBetween(compCollider2); - expect(line).toEqual(new Line(vec(100, -5), vec(400, -5))); + expect(line).toEqual(new LineSegment(vec(100, -5), vec(400, -5))); xf.pos = vec(0, 500); compCollider2.update(xf); const line2 = compCollider1.getClosestLineBetween(compCollider2); - expect(line2).toEqual(new Line(vec(0, 50), vec(0, 450))); + expect(line2).toEqual(new LineSegment(vec(0, 50), vec(0, 450))); }); it('can collide with normal colliders', () => { diff --git a/src/spec/LineSpec.ts b/src/spec/LineSpec.ts new file mode 100644 index 00000000..60dadf7e --- /dev/null +++ b/src/spec/LineSpec.ts @@ -0,0 +1,103 @@ +import { ExcaliburAsyncMatchers, ExcaliburMatchers } from 'excalibur-jasmine'; +import * as ex from '@excalibur'; +import { TestUtils } from './util/TestUtils'; +describe('A Line', () => { + beforeAll(() => { + jasmine.addMatchers(ExcaliburMatchers); + jasmine.addAsyncMatchers(ExcaliburAsyncMatchers); + }); + it('exists', () => { + expect(ex.Line).toBeDefined(); + }); + + it('can be constructed', () => { + const sut = new ex.Line({ + start: ex.vec(0, 0), + end: ex.vec(50, 70) + }); + + expect(sut.start).toBeVector(ex.vec(0, 0)); + expect(sut.end).toBeVector(ex.vec(50, 70)); + expect(sut.color).toEqual(ex.Color.Black); + expect(sut.thickness).toBe(1); + }); + + it('can be cloned', () => { + const sut = new ex.Line({ + start: ex.vec(0, 0), + end: ex.vec(50, 70), + color: ex.Color.Green, + thickness: 5 + }); + + const clone = sut.clone(); + + expect(clone.start).toBeVector(ex.vec(0, 0)); + expect(clone.end).toBeVector(ex.vec(50, 70)); + expect(clone.color).toEqual(ex.Color.Green); + expect(clone.thickness).toBe(5); + }); + + it('has correct local bounds', () => { + const sut = new ex.Line({ + start: ex.vec(0, 0), + end: ex.vec(50, 70), + color: ex.Color.Green, + thickness: 5 + }); + + expect(sut.width).toBe(50); + expect(sut.height).toBe(70); + }); + + it('can draw a line', async () => { + const sut = new ex.Line({ + start: ex.vec(0, 0), + end: ex.vec(50, 50), + color: ex.Color.Green, + thickness: 5 + }); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContextWebGL({ canvasElement }); + + ctx.clear(); + sut.draw(ctx, 0, 0); + ctx.flush(); + + await expectAsync(TestUtils.flushWebGLCanvasTo2D(canvasElement)).toEqualImage('src/spec/images/LineSpec/line.png'); + }); + + it('can draw a line when added to a graphics component', async () => { + const game = TestUtils.engine({ + width: 100, + height: 100, + backgroundColor: ex.Color.ExcaliburBlue + }); + + const testClock = game.clock as ex.TestClock; + + TestUtils.runToReady(game); + + const sut = new ex.Line({ + start: ex.vec(0, 0), + end: ex.vec(50, 50), + color: ex.Color.Green, + thickness: 5 + }); + + const actor = new ex.Actor({ + pos: ex.vec(0, 0) + }); + actor.graphics.anchor = ex.Vector.Zero; + actor.graphics.use(sut); + game.add(actor); + + testClock.step(16); + + await expectAsync(TestUtils.flushWebGLCanvasTo2D(game.canvas)).toEqualImage('src/spec/images/LineSpec/line.png'); + + }); +}); \ No newline at end of file diff --git a/src/spec/images/LineSpec/line.png b/src/spec/images/LineSpec/line.png new file mode 100644 index 0000000000000000000000000000000000000000..d1af78784056dcd516aa2f3ec59c8879b7a16149 GIT binary patch literal 667 zcmeAS@N?(olHy`uVBq!ia0y~yU`PRB4mJh`hJr^^Ll_ts7>k44ofy`glX=O&z!d7~ z;uum9_x8?3U#38jmc$iJ=W?7*Z{)Jt%*7*gCP^$M;s5UEMfLMdX3jDFochgCc-!p5 z&o<1gd29FWvi#qDh0M42D;$5`eJLXG(~d}iK(3~aO?;=yzVEZIsofV_yKDXWZ3gxV z$KAUwZTpe!|C?2CsX~Xw>%}oTPyW3A@hgfyP3*{05f;Hz*(s~JKYk4rIkI$1>(6pU zy`>r*-kHqB-<@K-0tKcYI&_4)P}9{hhKu?02Z+!noimpqVyBD@x^<5%6=5yZJbzGU zpTP7Wkt0iO*qWhYk~~nMJM5QpAO`Q`y<7uf&1{d%g|K$EMAkxBmItRqc6e)Y8T|@6 zrJM3)=@eGUQq84~ZZ4YqMp9nl3d##hn2or+!WEPkl&}|ZKNYo`zSMhy<&^LP8b6m$ z{{8P_rPo~h!bhP7yVyNHRGlpQed^WH2VcI6sO0jT<7N_{Gf#ZDv+9{|t