diff --git a/CHANGELOG.md b/CHANGELOG.md index ee88db719..44bd29124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,31 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added `useAnchor` parameter to `ex.GraphicsGroup` to allow users to opt out of anchor based positioning, if set to false all graphics members +will be positioned with the top left of the graphic at the actor's position. + ```typescript + const graphicGroup = new ex.GraphicsGroup({ + useAnchor: false, + members: [ + { + graphic: heartImage.toSprite(), + offset: ex.vec(0, 0), + }, + { + graphic: heartImage.toSprite(), + offset: ex.vec(0, 16), + }, + { + graphic: heartImage.toSprite(), + offset: ex.vec(16, 16), + }, + { + graphic: heartImage.toSprite(), + offset: ex.vec(16, 0), + }, + ], + }); + ``` - Added simplified `ex.coroutine` overloads, you need not pass engine as long as you are in an Excalibur lifecycle ```typescript const result = ex.coroutine(function* () {...}); diff --git a/sandbox/tests/graphics-group/index.ts b/sandbox/tests/graphics-group/index.ts index bcaba603d..c19c2946b 100644 --- a/sandbox/tests/graphics-group/index.ts +++ b/sandbox/tests/graphics-group/index.ts @@ -3,7 +3,11 @@ var game = new ex.Engine({ width: 1000, height: 1000, + pixelArt: true }); +game.toggleDebug(); +game.debug.graphics.showBounds = true; +game.debug.transform.showPosition = true; var heartImage = new ex.ImageSource('./heart.png'); @@ -19,28 +23,27 @@ class MyActor2 extends ex.Actor { this.graphics.add( "interactive", new ex.GraphicsGroup({ + useAnchor: false, members: [ { - graphic: undefined, - offset: ex.vec(8, 8), + graphic: heartImage.toSprite(), + offset: ex.vec(0, 0), + }, + { + graphic: heartImage.toSprite(), + offset: ex.vec(0, 16), }, { graphic: heartImage.toSprite(), - offset: ex.vec(8, -16), + offset: ex.vec(16, 16), + }, + { + graphic: heartImage.toSprite(), + offset: ex.vec(16, 0), }, ], - }), - { - anchor: ex.vec(0, 0), - } + }) ); - this.graphics.add( - "noninteractive", - heartImage.toSprite(), - { - anchor: ex.vec(8, 8), - } - ) } onPreUpdate(engine: ex.Engine, delta: number): void { @@ -50,4 +53,6 @@ class MyActor2 extends ex.Actor { game.add(new MyActor2()); -game.start(loader) \ No newline at end of file +game.start(loader) +game.currentScene.camera.pos = ex.vec(200, 200); +game.currentScene.camera.zoom = 3; \ No newline at end of file diff --git a/src/engine/Graphics/GraphicsComponent.ts b/src/engine/Graphics/GraphicsComponent.ts index 6559d940b..7fa047d8e 100644 --- a/src/engine/Graphics/GraphicsComponent.ts +++ b/src/engine/Graphics/GraphicsComponent.ts @@ -8,6 +8,7 @@ import { Material } from './Context/material'; import { Logger } from '../Util/Log'; import { WatchVector } from '../Math/watch-vector'; import { TransformComponent } from '../EntityComponentSystem'; +import { GraphicsGroup } from '../Graphics/GraphicsGroup'; /** * Type guard for checking if a Graphic HasTick (used for graphics that change over time like animations) @@ -359,7 +360,11 @@ export class GraphicsComponent extends Component { const bounds = graphic.localBounds; const offsetX = -bounds.width * anchor.x + offset.x; const offsetY = -bounds.height * anchor.y + offset.y; - bb = graphic?.localBounds.translate(vec(offsetX, offsetY)).combine(bb); + if (graphic instanceof GraphicsGroup && !graphic.useAnchor) { + bb = graphic?.localBounds.combine(bb); + } else { + bb = graphic?.localBounds.translate(vec(offsetX, offsetY)).combine(bb); + } this._localBounds = bb; } diff --git a/src/engine/Graphics/GraphicsGroup.ts b/src/engine/Graphics/GraphicsGroup.ts index 27898e693..7c3bbe0ed 100644 --- a/src/engine/Graphics/GraphicsGroup.ts +++ b/src/engine/Graphics/GraphicsGroup.ts @@ -7,6 +7,13 @@ import { Logger } from '../Util/Log'; export interface GraphicsGroupingOptions { members: (GraphicsGrouping | Graphic)[]; + /** + * Default true, GraphicsGroup will use the anchor to position all the graphics based on their combined bounds + * + * Setting to false will ignore anchoring and position the top left of all graphics at the actor's position, + * positioning graphics in the group is done with the `offset` property. + */ + useAnchor?: boolean; } export interface GraphicsGrouping { @@ -24,11 +31,13 @@ export interface GraphicsGrouping { export class GraphicsGroup extends Graphic implements HasTick { private _logger = Logger.getInstance(); + public useAnchor: boolean = true; public members: (GraphicsGrouping | Graphic)[] = []; constructor(options: GraphicsGroupingOptions & GraphicOptions) { super(options); this.members = options.members; + this.useAnchor = options.useAnchor ?? this.useAnchor; this._updateDimensions(); } @@ -100,7 +109,7 @@ export class GraphicsGroup extends Graphic implements HasTick { protected _preDraw(ex: ExcaliburGraphicsContext, x: number, y: number) { this._updateDimensions(); - super._preDraw(ex, x, y); + super._preDraw(ex, this.useAnchor ? x : 0, this.useAnchor ? y : 0); } protected _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number) { diff --git a/src/engine/Graphics/GraphicsSystem.ts b/src/engine/Graphics/GraphicsSystem.ts index 5915850fc..88ab13066 100644 --- a/src/engine/Graphics/GraphicsSystem.ts +++ b/src/engine/Graphics/GraphicsSystem.ts @@ -232,7 +232,12 @@ export class GraphicsSystem extends System { g = member.graphic; pos = member.offset; } - g?.localBounds.translate(offset.add(pos)).draw(this._graphicsContext, this._engine.debug.graphics.boundsColor); + + if (graphic.useAnchor) { + g?.localBounds.translate(offset.add(pos)).draw(this._graphicsContext, this._engine.debug.graphics.boundsColor); + } else { + g?.localBounds.translate(pos).draw(this._graphicsContext, this._engine.debug.graphics.boundsColor); + } } } else { /* istanbul ignore next */ diff --git a/src/spec/GraphicsGroupSpec.ts b/src/spec/GraphicsGroupSpec.ts index dbf345e8f..bafd033b5 100644 --- a/src/spec/GraphicsGroupSpec.ts +++ b/src/spec/GraphicsGroupSpec.ts @@ -47,6 +47,43 @@ describe('A Graphics Group', () => { await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsGroupSpec/graphics-group.png'); }); + it('can be created and drawn without anchor', async () => { + const rect1 = new ex.Rectangle({ + width: 25, + height: 25, + color: ex.Color.Blue + }); + + const rect2 = new ex.Rectangle({ + width: 25, + height: 25, + color: ex.Color.Yellow + }); + + const group = new ex.GraphicsGroup({ + useAnchor: false, + members: [ + { offset: ex.vec(0, 0), graphic: rect1 }, + { offset: ex.vec(25, 25), graphic: rect2 } + ] + }); + + expect(group.width).toBe(50); + expect(group.height).toBe(50); + expect(group.localBounds.width).toBe(50); + expect(group.localBounds.height).toBe(50); + + const canvasElement = document.createElement('canvas'); + canvasElement.width = 100; + canvasElement.height = 100; + const ctx = new ex.ExcaliburGraphicsContext2DCanvas({ canvasElement }); + + ctx.clear(); + group.draw(ctx, 100, 100); + + await expectAsync(canvasElement).toEqualImage('src/spec/images/GraphicsGroupSpec/graphics-group-without-anchor.png'); + }); + it('can be cloned', () => { const animation = new ex.Animation({ frames: [] diff --git a/src/spec/images/GraphicsGroupSpec/graphics-group-without-anchor.png b/src/spec/images/GraphicsGroupSpec/graphics-group-without-anchor.png new file mode 100644 index 000000000..c4e16e0d7 Binary files /dev/null and b/src/spec/images/GraphicsGroupSpec/graphics-group-without-anchor.png differ