Skip to content

Commit

Permalink
change: GraphicsGroup no longer centers with anchors (#2966)
Browse files Browse the repository at this point in the history
`useAnchor: false` The graphics members are positioned from the top left

  ```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),
      },
    ],
  });
  ```

![image](https://github.com/excaliburjs/Excalibur/assets/612071/effae398-7281-41a9-bdbc-e0ddc2328237)

`useAnchor: true` The graphics members' total combined bounds are centered by the actor's anchor (.5, .5) by default

  ```typescript
  const graphicGroup = new ex.GraphicsGroup({
    useAnchor: true,
    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),
      },
    ],
  });
  ```

![image](https://github.com/excaliburjs/Excalibur/assets/612071/c0ba5312-abc8-4f84-a2fb-5cf3e9e3ab96)
  • Loading branch information
eonarheim authored Mar 26, 2024
1 parent d7c97c2 commit 99c7a88
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 18 deletions.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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* () {...});
Expand Down
35 changes: 20 additions & 15 deletions sandbox/tests/graphics-group/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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<any>, delta: number): void {
Expand All @@ -50,4 +53,6 @@ class MyActor2 extends ex.Actor {

game.add(new MyActor2());

game.start(loader)
game.start(loader)
game.currentScene.camera.pos = ex.vec(200, 200);
game.currentScene.camera.zoom = 3;
7 changes: 6 additions & 1 deletion src/engine/Graphics/GraphicsComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}

Expand Down
11 changes: 10 additions & 1 deletion src/engine/Graphics/GraphicsGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
}

Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion src/engine/Graphics/GraphicsSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
37 changes: 37 additions & 0 deletions src/spec/GraphicsGroupSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 99c7a88

Please sign in to comment.