From 4e081bfe7769b60192d5a3a6d3fc4929804771a6 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Fri, 19 Jan 2024 13:39:15 -0600 Subject: [PATCH] refactor: Graphics Component simplification --- CHANGELOG.md | 3 +- sandbox/src/game.ts | 26 +- sandbox/tests/material/index.ts | 1 + sandbox/tests/sprite-tint/index.ts | 10 +- .../04-graphics/04.2-graphics-component.mdx | 81 +++- src/engine/Actor.ts | 7 +- src/engine/Graphics/GraphicsComponent.ts | 399 ++++++------------ src/engine/Graphics/GraphicsGroup.ts | 54 ++- src/engine/Graphics/GraphicsSystem.ts | 89 ++-- src/spec/ActorSpec.ts | 16 +- src/spec/GraphicsComponentSpec.ts | 184 +++----- src/spec/GraphicsGroupSpec.ts | 10 +- src/spec/GraphicsSystemSpec.ts | 8 +- src/spec/OffscreenSystemSpec.ts | 2 +- 14 files changed, 382 insertions(+), 508 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf30bcd26..47b3c098d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes -- +- Remove confusing Graphics Layering from `ex.GraphicsComponent`, recommend we use the `ex.GraphicsGroup` to manage this behavior + * Update `ex.GraphicsGroup` to be consistent and use `offset` instead of `pos` for graphics relative positioning ### Deprecated diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts index 390313989..40b193a19 100644 --- a/sandbox/src/game.ts +++ b/sandbox/src/game.ts @@ -618,12 +618,6 @@ var healthbar2 = new ex.Rectangle({ color: new ex.Color(0, 255, 0) }); -var backroundLayer = player.graphics.layers.create({ - name: 'background', - order: -1 -}); - -backroundLayer.show(healthbar2, { offset: ex.vec(0, -70) }); var playerText = new ex.Text({ text: 'A long piece of text is long', font: new ex.Font({ @@ -631,8 +625,24 @@ var playerText = new ex.Text({ family: 'Times New Roman' }) }); -// playerText.showDebug = true; -backroundLayer.show(playerText, { offset: ex.vec(0, -70) }); + +var group = new ex.GraphicsGroup({ + members: [ + { graphic: healthbar2, pos: ex.vec(0, -70)}, + { graphic: playerText, pos: ex.vec(0, -70)} + ] +}) +healthbar.graphics.use(group); + +// var backgroundLayer = player.graphics.layers.create({ +// name: 'background', +// order: -1 +// }); + +// backgroundLayer.show(healthbar2, { offset: ex.vec(0, -70) }); + +// // playerText.showDebug = true; +// backgroundLayer.show(playerText, { offset: ex.vec(0, -70) }); // Retrieve animations for player from sprite sheet var left = ex.Animation.fromSpriteSheet(spriteSheetRun, ex.range(1, 10), 50); diff --git a/sandbox/tests/material/index.ts b/sandbox/tests/material/index.ts index dce2702ee..7254a0e85 100644 --- a/sandbox/tests/material/index.ts +++ b/sandbox/tests/material/index.ts @@ -266,6 +266,7 @@ void main() { fragColor.rgb = mix(screen_color.rgb, mixColor, u_color.a)*fragColor.a + (wave_crest_color.rgb * wave_crest); fragColor.rgb = texture(u_noise, v_uv).rgb * fragColor.a; + fragColor.rgb = vec3(gl_FragCoord.xy/u_resolution, 0.0); }`; const noise = new ex.ImageSource('./noise.avif', false, ex.ImageFiltering.Pixel); diff --git a/sandbox/tests/sprite-tint/index.ts b/sandbox/tests/sprite-tint/index.ts index 16d2b52d9..6a161d81c 100644 --- a/sandbox/tests/sprite-tint/index.ts +++ b/sandbox/tests/sprite-tint/index.ts @@ -38,8 +38,14 @@ actor.onInitialize = () => { }) ); - actor.graphics.show(shadow, {offset: ex.vec(10, 10)}); - actor.graphics.show(sprite); + const group = new ex.GraphicsGroup({ + members: [ + {graphic: shadow, pos: ex.vec(10, 10)}, + sprite + ] + }) + + actor.graphics.use(group); }; diff --git a/site/docs/04-graphics/04.2-graphics-component.mdx b/site/docs/04-graphics/04.2-graphics-component.mdx index cfe94967b..260bd5adc 100644 --- a/site/docs/04-graphics/04.2-graphics-component.mdx +++ b/site/docs/04-graphics/04.2-graphics-component.mdx @@ -84,27 +84,78 @@ actor.graphics.onPostDraw = (ctx: ex.ExcaliburGraphicsContext) => { } ``` -### Layers +### Multiple Graphics at Once -The layer's component adds a way for multiple [graphics](#graphics) to be on top or behind each other for a certain actor or entity. +Sometimes you want to draw multiple pieces of graphics at once! There are two recommended ways to accomplish this! -Layers can be ordered numerically, larger negative layers behind, and positive layers in front. +#### GraphicsGroup -```typescript -actor.graphics.layers.create({ name: 'background', order: -1 }) -actor.graphics.layers.create({ name: 'foreground', order: 1 }) - -actor.graphics.layers.get('background').show(myBackground) -actor.graphics.layers.get('foreground').show(myForeground) +[[GraphicsGroup|Graphics groups]] allow you to compose multiple graphics together into 1. This can be useful if you have multi layered or complex graphics requirements. One limitation however is you can only influence the relative offset from you Actor. -// no longer display the background -actor.graphics.layers.get('background').hide() +```typescript +const healthBarActor = new ex.Actor({...}) + +const healthBarRectangle = new ex.Rectangle({ + width: 140, + height: 5, + color: new ex.Color(0, 255, 0) +}); + +const healthBarText = new ex.Text({ + text: 'A long piece of text is long', + font: new ex.Font({ + size: 20, + family: 'Times New Roman' + }) +}); + +const group = new ex.GraphicsGroup({ + members: [ + { graphic: healthbarRectangle, pos: ex.vec(0, -70)}, + { graphic: healthBarText, pos: ex.vec(0, -70)} + ] +}); + +healthBarActor.graphics.use(group); ``` -There is always a layer named `'default'` at `order: 0` +#### Child Actor or Entity + +If you need independent articulation and a lot of control over positioning, rotation, and scale this is this strategy to reach for. One example is you might have a main actor, and a child actor for every limb of a paper doll. ```typescript -actor.graphics.show(myAnimation) -// is equivalent to -actor.graphics.layers.get('default').show(myAnimation) + +import { Resources } from './Resources'; + +class PaperDoll extends ex.Actor { + this.leftArm = new ex.Actor({ + pos: ex.vec(-10, 10) + }); + this.rightArm = new ex.Actor({ + pos: ex.vec(10, 10) + }); + this.head = new ex.Actor({ + pos: ex.vec(0, -10) + }); + this.body = new ex.Actor({ + pos: ex.vec(0, 20) + }); + this.leftLeg = new ex.Actor({ + pos: ex.vec(-10, 30) + }); + this.rightLeg = new ex.Actor({ + pos: ex.vec(10, 30) + }); + + constructor() { + this.leftArm.graphics.use(Resources.LeftArm); + this.rightArm.graphics.use(Resources.RightArm); + + this.head.graphics.use(Resources.Head); + this.body.graphics.use(Resources.Body); + + this.leftLeg.graphics.use(Resources.LeftLeg); + this.rightLeg.graphics.use(Resources.RightLeg); + } +} ``` diff --git a/src/engine/Actor.ts b/src/engine/Actor.ts index 27bda1d6a..0abc09a79 100644 --- a/src/engine/Actor.ts +++ b/src/engine/Actor.ts @@ -56,7 +56,7 @@ export function isActor(x: any): x is Actor { } /** - * Actor contructor options + * Actor constructor options */ export interface ActorArgs { /** @@ -137,7 +137,7 @@ export interface ActorArgs { */ collider?: Collider; /** - * Optionally suppy a [[CollisionGroup]] + * Optionally supply a [[CollisionGroup]] */ collisionGroup?: CollisionGroup; } @@ -492,8 +492,7 @@ export class Actor extends Entity implements Eventable, PointerEvents, CanInitia } public set color(v: Color) { this._color = v.clone(); - const defaultLayer = this.graphics.layers.default; - const currentGraphic = defaultLayer.graphics[0]?.graphic; + const currentGraphic = this.graphics.current; if (currentGraphic instanceof Raster || currentGraphic instanceof Text) { currentGraphic.color = this._color; } diff --git a/src/engine/Graphics/GraphicsComponent.ts b/src/engine/Graphics/GraphicsComponent.ts index 137422bbd..b810e3bc5 100644 --- a/src/engine/Graphics/GraphicsComponent.ts +++ b/src/engine/Graphics/GraphicsComponent.ts @@ -2,10 +2,10 @@ import { Vector, vec } from '../Math/vector'; import { Graphic } from './Graphic'; import { HasTick } from './Animation'; import { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext'; -import { Logger } from '../Util/Log'; import { BoundingBox } from '../Collision/Index'; import { Component } from '../EntityComponentSystem/Component'; import { Material } from './Context/material'; +import { Logger } from '../Util/Log'; /** * Type guard for checking if a Graphic HasTick (used for graphics that change over time like animations) @@ -22,12 +22,19 @@ export interface GraphicsShowOptions { export interface GraphicsComponentOptions { onPostDraw?: (ex: ExcaliburGraphicsContext, elapsed: number) => void; onPreDraw?: (ex: ExcaliburGraphicsContext, elapsed: number) => void; + onPreTransformDraw?: (ex: ExcaliburGraphicsContext, elapsed: number) => void; + onPostTransformDraw?: (ex: ExcaliburGraphicsContext, elapsed: number) => void; /** * Name of current graphic to use */ current?: string; + /** + * Optionally set a material to use on the graphic + */ + material?: Material; + /** * Optionally copy instances of graphics by calling .clone(), you may set this to false to avoid sharing graphics when added to the * component for performance reasons. By default graphics are not copied and are shared when added to the component. @@ -45,9 +52,9 @@ export interface GraphicsComponentOptions { opacity?: number; /** - * List of graphics + * List of graphics and optionally the options per graphic */ - graphics?: { [graphicName: string]: Graphic }; + graphics?: { [graphicName: string]: Graphic | { graphic: Graphic, options: GraphicsShowOptions } }; /** * Optional offset in absolute pixels to shift all graphics in this component from each graphic's anchor (default is top left corner) @@ -60,222 +67,20 @@ export interface GraphicsComponentOptions { anchor?: Vector; } -export interface GraphicsLayerOptions { - /** - * Name of the layer required, for example 'background' - */ - name: string; - /** - * Order of the layer, a layer with order -1 will be below a layer with order of 1 - */ - order: number; - /** - * Offset to shift the entire layer - */ - offset?: Vector; -} -export class GraphicsLayer { - public graphics: { graphic: Graphic; options: GraphicsShowOptions }[] = []; - constructor(private _options: GraphicsLayerOptions, private _graphics: GraphicsComponent) {} - public get name(): string { - return this._options.name; - } - - /** - * Remove any instance(s) of a graphic currently being shown in this layer - */ - public hide(nameOrGraphic: string | Graphic): void; - /** - * Remove all currently shown graphics in this layer - */ - public hide(): void; - public hide(nameOrGraphic?: string | Graphic): void { - if (!nameOrGraphic) { - this.graphics.length = 0; - } else { - let gfx: Graphic = null; - if (nameOrGraphic instanceof Graphic) { - gfx = nameOrGraphic; - } else { - gfx = this._graphics.getGraphic(nameOrGraphic); - } - this.graphics = this.graphics.filter((g) => g.graphic !== gfx); - this._graphics.recalculateBounds(); - } - } - - /** - * Show a graphic by name or instance at an offset, graphics are shown in the order in which `show()` is called. - * - * If `show()` is called multiple times for the same graphic it will be shown multiple times. - * @param nameOrGraphic - * @param options - */ - public show(nameOrGraphic: string | T, options?: GraphicsShowOptions): T { - options = { ...options }; - let gfx: Graphic; - if (nameOrGraphic instanceof Graphic) { - gfx = this._graphics.copyGraphics ? nameOrGraphic.clone() : nameOrGraphic; - } else { - gfx = this._graphics.getGraphic(nameOrGraphic); - if (!gfx) { - Logger.getInstance().error( - `No such graphic added to component named ${nameOrGraphic}. These named graphics are available: `, - this._graphics.getNames() - ); - } - } - if (gfx) { - this.graphics.push({ graphic: gfx, options }); - this._graphics.recalculateBounds(); - return gfx as T; - } else { - return null; - } - } - - /** - * Use a specific graphic, swap out any current graphics being shown - * @param nameOrGraphic - * @param options - */ - public use(nameOrGraphic: string | T, options?: GraphicsShowOptions): T { - options = { ...options }; - this.hide(); - return this.show(nameOrGraphic, options); - } - - /** - * Current order of the layer, higher numbers are on top, lower numbers are on the bottom. - * - * For example a layer with `order = -1` would be under a layer of `order = 1` - */ - public get order(): number { - return this._options.order; - } - - /** - * Set the order of the layer, higher numbers are on top, lower numbers are on the bottom. - * - * For example a layer with `order = -1` would be under a layer of `order = 1` - */ - public set order(order: number) { - this._options.order = order; - } - - /** - * Get or set the pixel offset from the layer anchor for all graphics in the layer - */ - public get offset(): Vector { - return this._options.offset ?? Vector.Zero; - } - - public set offset(value: Vector) { - this._options.offset = value; - } - - public get currentKeys(): string { - return this.name ?? 'anonymous'; - } - - public clone(graphicsComponent: GraphicsComponent): GraphicsLayer { - const layer = new GraphicsLayer({...this._options}, graphicsComponent); - layer.graphics = [...this.graphics.map(g => ({graphic: g.graphic.clone(), options: {...g.options}}))]; - return layer; - } -} - -export class GraphicsLayers { - private _layers: GraphicsLayer[] = []; - private _layerMap: { [layerName: string]: GraphicsLayer } = {}; - public default: GraphicsLayer; - constructor(private _component: GraphicsComponent) { - this.default = new GraphicsLayer({ name: 'default', order: 0 }, _component); - this._maybeAddLayer(this.default); - } - public create(options: GraphicsLayerOptions): GraphicsLayer { - const layer = new GraphicsLayer(options, this._component); - return this._maybeAddLayer(layer); - } - - /** - * Retrieve a single layer by name - * @param name - */ - public get(name: string): GraphicsLayer; - /** - * Retrieve all layers - */ - public get(): readonly GraphicsLayer[]; - public get(name?: string): GraphicsLayer | readonly GraphicsLayer[] { - if (name) { - return this._getLayer(name); - } - return this._layers; - } - - public currentKeys() { - const graphicsLayerKeys = []; - for (const layer of this._layers) { - graphicsLayerKeys.push(layer.currentKeys); - } - return graphicsLayerKeys; - } - - public has(name: string): boolean { - return name in this._layerMap; - } - - private _maybeAddLayer(layer: GraphicsLayer) { - if (this._layerMap[layer.name]) { - // todo log warning - return this._layerMap[layer.name]; - } - this._layerMap[layer.name] = layer; - this._layers.push(layer); - this._layers.sort((a, b) => a.order - b.order); - return layer; - } - - private _getLayer(name: string): GraphicsLayer | undefined { - return this._layerMap[name]; - } - - public clone(graphicsComponent: GraphicsComponent): GraphicsLayers { - const layers = new GraphicsLayers(graphicsComponent); - layers._layerMap = {}; - layers._layers = []; - layers.default = this.default.clone(graphicsComponent); - layers._maybeAddLayer(layers.default); - // Remove the default layer out of the clone - const clonedLayers = this._layers.filter(l => l.name !== 'default').map(l => l.clone(graphicsComponent)); - clonedLayers.forEach(layer => layers._maybeAddLayer(layer)); - return layers; - } -} - /** * Component to manage drawings, using with the position component */ export class GraphicsComponent extends Component<'ex.graphics'> { + private _logger = Logger.getInstance(); readonly type = 'ex.graphics'; - private _graphics: { [graphicName: string]: Graphic } = {}; - - public layers: GraphicsLayers; + private _current: string = 'default'; + private _graphics: Record = {}; + private _options: Record = {}; public material: Material | null = null; - public getGraphic(name: string): Graphic | undefined { - return this._graphics[name]; - } - /** - * Get registered graphics names - */ - public getNames(): string[] { - return Object.keys(this._graphics); - } /** * Draws after the entity transform has been applied, but before graphics component graphics have been drawn @@ -328,7 +133,8 @@ export class GraphicsComponent extends Component<'ex.graphics'> { public flipVertical: boolean = false; /** - * If set to true graphics added to the component will be copied. This can affect performance + * If set to true graphics added to the component will be copied. This can effect performance, but is useful if you don't want + * changes to a graphic to effect all the places it is used. */ public copyGraphics: boolean = false; @@ -337,31 +143,74 @@ export class GraphicsComponent extends Component<'ex.graphics'> { // Defaults options = { visible: this.visible, + graphics: {}, ...options }; - const { current, anchor, opacity, visible, graphics, offset, copyGraphics, onPreDraw, onPostDraw } = options; + const { + current, + anchor, + opacity, + visible, + graphics, + offset, + copyGraphics, + onPreDraw, + onPostDraw, + onPreTransformDraw, + onPostTransformDraw + } = options; + + for (const [key, graphicOrOptions] of Object.entries(graphics)) { + if (graphicOrOptions instanceof Graphic) { + this._graphics[key] = graphicOrOptions; + } else { + this._graphics[key] = graphicOrOptions.graphic; + this._options[key] = graphicOrOptions.options; + } + } - this._graphics = graphics || {}; this.offset = offset ?? this.offset; this.opacity = opacity ?? this.opacity; this.anchor = anchor ?? this.anchor; this.copyGraphics = copyGraphics ?? this.copyGraphics; this.onPreDraw = onPreDraw ?? this.onPreDraw; this.onPostDraw = onPostDraw ?? this.onPostDraw; + this.onPreDraw = onPreTransformDraw ?? this.onPreTransformDraw; + this.onPostTransformDraw = onPostTransformDraw ?? this.onPostTransformDraw; this.visible = !!visible; - - this.layers = new GraphicsLayers(this); + this._current = current ?? this._current; if (current && this._graphics[current]) { - this.show(this._graphics[current]); + this.use(current); } } + public getGraphic(name: string): Graphic | undefined { + return this._graphics[name]; + } + public getOptions(name: string): GraphicsShowOptions | undefined { + return this._options[name]; + } + + /** + * Get registered graphics names + */ + public getNames(): string[] { + return Object.keys(this._graphics); + } + + /** + * Returns the currently displayed graphic + */ + public get current(): Graphic | undefined { + return this._graphics[this._current]; + } + /** - * Returns the currently displayed graphics and their offsets, empty array if hidden + * Returns the currently displayed graphic offsets */ - public get current(): { graphic: Graphic; options: GraphicsShowOptions }[] { - return this.layers.default.graphics; + public get currentOptions(): GraphicsShowOptions | undefined { + return this._options[this._current]; } /** @@ -371,60 +220,72 @@ export class GraphicsComponent extends Component<'ex.graphics'> { return this._graphics; } + /** + * Returns all graphics options associated with this component + */ + public get options(): { [graphicName: string]: GraphicsShowOptions } { + return this._options; + } + /** * Adds a named graphic to this component, if the name is "default" or not specified, it will be shown by default without needing to call - * `show("default")` * @param graphic */ - public add(graphic: Graphic): Graphic; - public add(name: string, graphic: Graphic): Graphic; - public add(nameOrGraphic: string | Graphic, graphic?: Graphic): Graphic { + public add(graphic: Graphic, options?: GraphicsShowOptions): Graphic; + public add(name: string, graphic: Graphic, options?: GraphicsShowOptions): Graphic; + public add(nameOrGraphic: string | Graphic, graphicOrOptions?: Graphic | GraphicsShowOptions, options?: GraphicsShowOptions): Graphic { let name = 'default'; let graphicToSet: Graphic = null; - if (typeof nameOrGraphic === 'string') { + let optionsToSet: GraphicsShowOptions | undefined = undefined; + if (typeof nameOrGraphic === 'string' && graphicOrOptions instanceof Graphic) { name = nameOrGraphic; - graphicToSet = graphic; - } else { + graphicToSet = graphicOrOptions; + optionsToSet = options; + } + if (nameOrGraphic instanceof Graphic && !(graphicOrOptions instanceof Graphic)) { graphicToSet = nameOrGraphic; + optionsToSet = graphicOrOptions; } this._graphics[name] = this.copyGraphics ? graphicToSet.clone() : graphicToSet; + this._options[name] = this.copyGraphics ? {...optionsToSet} : optionsToSet; if (name === 'default') { - this.show('default'); + this.use('default'); } return graphicToSet; } - /** - * Show a graphic by name on the **default** layer, returns the new [[Graphic]] - */ - public show(nameOrGraphic: string | T, options?: GraphicsShowOptions): T { - const result = this.layers.default.show(nameOrGraphic, options); - this.recalculateBounds(); - return result; - } - /** * Use a graphic only, swap out any graphics on the **default** layer, returns the new [[Graphic]] * @param nameOrGraphic * @param options */ public use(nameOrGraphic: string | T, options?: GraphicsShowOptions): T { - const result = this.layers.default.use(nameOrGraphic, options); + if (nameOrGraphic instanceof Graphic) { + let graphic = nameOrGraphic as Graphic; + if (this.copyGraphics) { + graphic = nameOrGraphic.clone(); + } + this._current = 'default'; + this._graphics[this._current] = graphic; + this._options[this._current] = options; + } else { + this._current = nameOrGraphic; + this._options[this._current] = options; + if (!(this._current in this._graphics)) { + this._logger.warn( + `Graphic ${this._current} is not registered with the graphics component owned by ${this.owner?.name}. Nothing will be drawn.`); + } + } this.recalculateBounds(); - return result; + return this.current as T; } /** - * Remove any instance(s) of a graphic currently being shown in the **default** layer + * Hide currently shown graphic */ - public hide(nameOrGraphic: string | Graphic): void; - /** - * Remove all currently shown graphics in the **default** layer - */ - public hide(): void; - public hide(nameOrGraphic?: string | Graphic): void { - this.layers.default.hide(nameOrGraphic); + public hide(): void { + this._current = 'ex.none'; } private _localBounds: BoundingBox = null; @@ -434,22 +295,26 @@ export class GraphicsComponent extends Component<'ex.graphics'> { public recalculateBounds() { let bb = new BoundingBox(); - for (const layer of this.layers.get()) { - for (const { graphic, options } of layer.graphics) { - let anchor = this.anchor; - let offset = this.offset; - if (options?.anchor) { - anchor = options.anchor; - } - if (options?.offset) { - offset = options.offset; - } - 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 + layer.offset.x, offsetY + layer.offset.y)).combine(bb); - } + const graphic = this._graphics[this._current]; + const options = this._options[this._current]; + + if (!graphic) { + this._localBounds = bb; + return; + } + + let anchor = this.anchor; + let offset = this.offset; + if (options?.anchor) { + anchor = options.anchor; + } + if (options?.offset) { + offset = options.offset; } + 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); this._localBounds = bb; } @@ -461,23 +326,22 @@ export class GraphicsComponent extends Component<'ex.graphics'> { } /** - * Update underlying graphics if necesary, called internally + * Update underlying graphics if necessary, called internally * @param elapsed * @internal */ public update(elapsed: number, idempotencyToken: number = 0) { - for (const layer of this.layers.get()) { - for (const { graphic } of layer.graphics) { - if (hasGraphicsTick(graphic)) { - graphic?.tick(elapsed, idempotencyToken); - } - } + const graphic = this.current; + if (graphic && hasGraphicsTick(graphic)) { + graphic.tick(elapsed, idempotencyToken); } } + public clone(): GraphicsComponent { const graphics = new GraphicsComponent(); graphics._graphics = { ...this._graphics }; + graphics._options = {... this._options}; graphics.offset = this.offset.clone(); graphics.opacity = this.opacity; graphics.anchor = this.anchor.clone(); @@ -485,7 +349,6 @@ export class GraphicsComponent extends Component<'ex.graphics'> { graphics.onPreDraw = this.onPreDraw; graphics.onPostDraw = this.onPostDraw; graphics.visible = this.visible; - graphics.layers = this.layers.clone(graphics); return graphics; } diff --git a/src/engine/Graphics/GraphicsGroup.ts b/src/engine/Graphics/GraphicsGroup.ts index afc7ebd8b..1486021e7 100644 --- a/src/engine/Graphics/GraphicsGroup.ts +++ b/src/engine/Graphics/GraphicsGroup.ts @@ -5,16 +5,16 @@ import { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext'; import { BoundingBox } from '../Collision/Index'; export interface GraphicsGroupingOptions { - members: GraphicsGrouping[]; + members: (GraphicsGrouping | Graphic)[]; } export interface GraphicsGrouping { - pos: Vector; + offset: Vector; graphic: Graphic; } export class GraphicsGroup extends Graphic implements HasTick { - public members: GraphicsGrouping[] = []; + public members: (GraphicsGrouping | Graphic)[] = []; constructor(options: GraphicsGroupingOptions & GraphicOptions) { super(options); @@ -30,21 +30,21 @@ export class GraphicsGroup extends Graphic implements HasTick { } private _updateDimensions(): BoundingBox { - let bb = new BoundingBox(); - for (const { graphic, pos } of this.members) { - bb = graphic.localBounds.translate(pos).combine(bb); - } - + const bb = this.localBounds; this.width = bb.width; this.height = bb.height; - return bb; } public get localBounds(): BoundingBox { let bb = new BoundingBox(); - for (const { graphic, pos } of this.members) { - bb = graphic.localBounds.translate(pos).combine(bb); + for (const member of this.members) { + if (member instanceof Graphic) { + bb = member.localBounds.combine(bb); + } else { + const { graphic, offset: pos } = member; + bb = graphic.localBounds.translate(pos).combine(bb); + } } return bb; } @@ -55,18 +55,28 @@ export class GraphicsGroup extends Graphic implements HasTick { public tick(elapsedMilliseconds: number, idempotencyToken?: number) { for (const member of this.members) { - const maybeAnimation = member.graphic; - if (this._isAnimationOrGroup(maybeAnimation)) { - maybeAnimation.tick(elapsedMilliseconds, idempotencyToken); + let graphic: Graphic; + if (member instanceof Graphic) { + graphic = member; + } else { + graphic = member.graphic; + } + if (this._isAnimationOrGroup(graphic)) { + graphic.tick(elapsedMilliseconds, idempotencyToken); } } } public reset() { for (const member of this.members) { - const maybeAnimation = member.graphic; - if (this._isAnimationOrGroup(maybeAnimation)) { - maybeAnimation.reset(); + let graphic: Graphic; + if (member instanceof Graphic) { + graphic = member; + } else { + graphic = member.graphic; + } + if (this._isAnimationOrGroup(graphic)) { + graphic.reset(); } } } @@ -77,10 +87,18 @@ export class GraphicsGroup extends Graphic implements HasTick { } protected _drawImage(ex: ExcaliburGraphicsContext, x: number, y: number) { + const pos = Vector.Zero; for (const member of this.members) { + let graphic: Graphic; + if (member instanceof Graphic) { + graphic = member; + } else { + graphic = member.graphic; + member.offset.clone(pos); + } ex.save(); ex.translate(x, y); - member.graphic.draw(ex, member.pos.x, member.pos.y); + graphic.draw(ex, pos.x, pos.y); if (this.showDebug) { /* istanbul ignore next */ ex.debug.drawRect(0, 0, this.width, this.height); diff --git a/src/engine/Graphics/GraphicsSystem.ts b/src/engine/Graphics/GraphicsSystem.ts index bb9e597c8..1fb0f8eb8 100644 --- a/src/engine/Graphics/GraphicsSystem.ts +++ b/src/engine/Graphics/GraphicsSystem.ts @@ -16,6 +16,7 @@ import { FontCache } from './FontCache'; import { PostDrawEvent, PostTransformDrawEvent, PreDrawEvent, PreTransformDrawEvent } from '../Events'; import { Transform } from '../Math/transform'; import { blendTransform } from './TransformInterpolation'; +import { Graphic } from './Graphic'; export class GraphicsSystem extends System { public readonly types = ['ex.transform', 'ex.graphics'] as const; @@ -175,51 +176,59 @@ export class GraphicsSystem extends System { it('can be created with a radius with default circle collider and graphic', () => { const actor = new ex.Actor({ x: 50, y: 50, color: ex.Color.Red, radius: 10 }); - expect(actor.graphics.current[0].graphic).toBeInstanceOf(ex.Circle); - expect((actor.graphics.current[0].graphic as ex.Circle).radius).toBe(10); - expect((actor.graphics.current[0].graphic as ex.Circle).color).toEqual(ex.Color.Red); + expect(actor.graphics.current).toBeInstanceOf(ex.Circle); + expect((actor.graphics.current as ex.Circle).radius).toBe(10); + expect((actor.graphics.current as ex.Circle).color).toEqual(ex.Color.Red); expect(actor.collider.get()).toBeInstanceOf(ex.CircleCollider); expect(actor.collider.get().offset).toBeVector(ex.vec(0, 0)); }); it('can be created with a width/height with default rectangle collider and graphic', () => { const actor = new ex.Actor({ x: 50, y: 50, color: ex.Color.Red, width: 10, height: 10 }); - expect(actor.graphics.current[0].graphic).toBeInstanceOf(ex.Rectangle); - expect((actor.graphics.current[0].graphic as ex.Rectangle).width).toBe(10); - expect((actor.graphics.current[0].graphic as ex.Rectangle).height).toBe(10); - expect((actor.graphics.current[0].graphic as ex.Rectangle).color).toEqual(ex.Color.Red); + expect(actor.graphics.current).toBeInstanceOf(ex.Rectangle); + expect((actor.graphics.current as ex.Rectangle).width).toBe(10); + expect((actor.graphics.current as ex.Rectangle).height).toBe(10); + expect((actor.graphics.current as ex.Rectangle).color).toEqual(ex.Color.Red); expect(actor.collider.get()).toBeInstanceOf(ex.PolygonCollider); }); @@ -599,7 +599,7 @@ describe('A game actor', () => { expect(grandChildActor.getGlobalPos().y).toBe(75); }); - it('can find its global coordinates if it doesnt have a parent', () => { + it('can find its global coordinates if it doesn\'t have a parent', () => { expect(actor.pos.x).toBe(0); expect(actor.pos.y).toBe(0); diff --git a/src/spec/GraphicsComponentSpec.ts b/src/spec/GraphicsComponentSpec.ts index 06e12c6f8..b8c632a4b 100644 --- a/src/spec/GraphicsComponentSpec.ts +++ b/src/spec/GraphicsComponentSpec.ts @@ -18,7 +18,7 @@ describe('A Graphics ECS Component', () => { expect(sut.offset).toBeVector(ex.vec(0, 0)); expect(sut.opacity).toBe(1); expect(sut.visible).toBe(true); - expect(sut.current).toEqual([]); + expect(sut.current).toBeUndefined(); expect(sut.graphics).toEqual({}); }); @@ -43,7 +43,6 @@ describe('A Graphics ECS Component', () => { graphics.onPreDraw = () => { /* do nothing */ }; graphics.onPostDraw = () => { /* do nothing */}; graphics.use(rect); - graphics.layers.create({name: 'background', order: -1}).use(rect2); const clone = owner.clone(); @@ -57,21 +56,11 @@ describe('A Graphics ECS Component', () => { expect(sut.copyGraphics).toEqual(graphics.copyGraphics); expect(sut.onPreDraw).toBe(sut.onPreDraw); expect(sut.onPostDraw).toBe(sut.onPostDraw); - expect(sut.layers.get().length).toEqual(graphics.layers.get().length); - expect((sut.layers.get('background').graphics[0].graphic as ex.Rectangle).color) - .toEqual((graphics.layers.get('background').graphics[0].graphic as ex.Rectangle).color); - expect((sut.layers.get('background').graphics[0].graphic as ex.Rectangle).width) - .toEqual((graphics.layers.get('background').graphics[0].graphic as ex.Rectangle).width); - expect((sut.layers.get('background').graphics[0].graphic as ex.Rectangle).height) - .toEqual((graphics.layers.get('background').graphics[0].graphic as ex.Rectangle).height); - expect(sut.layers.get('background').graphics[0].options).toEqual(graphics.layers.get('background').graphics[0].options); // Should be new refs expect(sut).not.toBe(graphics); expect(sut.offset).not.toBe(graphics.offset); expect(sut.anchor).not.toBe(graphics.anchor); - expect(sut.layers.get()).not.toBe(graphics.layers.get()); - expect(sut.layers.get('background').graphics).not.toBe(graphics.layers.get('background').graphics); // Should have a new owner expect(sut.owner).toBe(clone); @@ -114,7 +103,7 @@ describe('A Graphics ECS Component', () => { const sut = new ex.GraphicsComponent({ copyGraphics: true }); - const shownRect = sut.show(rect); + const shownRect = sut.use(rect); expect(shownRect.id).not.toEqual(rect.id); }); @@ -125,25 +114,21 @@ describe('A Graphics ECS Component', () => { }); const sut = new ex.GraphicsComponent(); - sut.show(rect, { offset: ex.vec(1, 2), anchor: ex.vec(1, 1) }); - sut.show(rect, { offset: ex.vec(-1, -2), anchor: ex.vec(0, 0) }); + sut.use(rect, { offset: ex.vec(1, 2), anchor: ex.vec(1, 1) }); - expect(sut.current).toEqual([ - { - graphic: rect, - options: { - offset: ex.vec(1, 2), - anchor: ex.vec(1, 1) - } - }, - { - graphic: rect, - options: { - offset: ex.vec(-1, -2), - anchor: ex.vec(0, 0) - } - } - ]); + expect(sut.current).toEqual(rect); + expect(sut.currentOptions).toEqual({ + offset: ex.vec(1, 2), + anchor: ex.vec(1, 1) + }); + + sut.use(rect, { offset: ex.vec(-1, -2), anchor: ex.vec(0, 0) }); + + expect(sut.current).toEqual(rect); + expect(sut.currentOptions).toEqual({ + offset: ex.vec(-1, -2), + anchor: ex.vec(0, 0) + }); }); it('can show graphics by name if it exists', () => { @@ -158,20 +143,16 @@ describe('A Graphics ECS Component', () => { }); const logger = ex.Logger.getInstance(); - spyOn(logger, 'error'); - - expect(sut.current).toEqual([]); - sut.show('some-gfx-2'); - expect(sut.current).toEqual([ - { - graphic: rect, - options: {} - } - ]); + spyOn(logger, 'warn'); + + expect(sut.current).toBeUndefined(); + sut.use('some-gfx-2'); + expect(sut.current).toEqual(rect); + expect(sut.currentOptions).toBeUndefined(); - const none = sut.show('made-up-name'); - expect(none).toBeNull(); - expect(logger.error).toHaveBeenCalled(); + const none = sut.use('made-up-name'); + expect(none).toBeUndefined(); + expect(logger.warn).toHaveBeenCalled(); }); it('can swap all the graphics for a graphic', () => { @@ -185,26 +166,18 @@ describe('A Graphics ECS Component', () => { }); const sut = new ex.GraphicsComponent(); - sut.show(rect, { offset: ex.vec(1, 2), anchor: ex.vec(1, 1) }); + sut.use(rect, { offset: ex.vec(1, 2), anchor: ex.vec(1, 1) }); - expect(sut.current).toEqual([ - { - graphic: rect, - options: { - offset: ex.vec(1, 2), - anchor: ex.vec(1, 1) - } - } - ]); + expect(sut.current).toEqual(rect); + expect(sut.currentOptions).toEqual({ + offset: ex.vec(1, 2), + anchor: ex.vec(1, 1) + }); sut.use(rect2); - expect(sut.current).toEqual([ - { - graphic: rect2, - options: {} - } - ]); + expect(sut.current).toEqual(rect2); + expect(sut.currentOptions).toBeUndefined(); }); it('can hide graphics', () => { @@ -214,12 +187,11 @@ describe('A Graphics ECS Component', () => { }); const sut = new ex.GraphicsComponent(); - sut.show(rect, { offset: ex.vec(1, 2), anchor: ex.vec(1, 1) }); - sut.show(rect, { offset: ex.vec(-1, -2), anchor: ex.vec(0, 0) }); + sut.use(rect, { offset: ex.vec(1, 2), anchor: ex.vec(1, 1) }); sut.hide(); - expect(sut.current).toEqual([]); + expect(sut.current).toBeUndefined(); }); it('can hide graphics by reference or name', () => { @@ -238,16 +210,13 @@ describe('A Graphics ECS Component', () => { } }); - const shown = sut.show('gfx-1'); + const shown = sut.use('gfx-1'); expect(shown).not.toBeNull(); - const shown2 = sut.show('gfx-2'); + const shown2 = sut.use('gfx-2'); expect(shown2).not.toBeNull(); - sut.hide(shown); - expect(sut.current.length).toBe(1); - - sut.hide('gfx-2'); - expect(sut.current.length).toBe(0); + sut.hide(); + expect(sut.current).toBeUndefined(); }); it('can have graphics added to it', () => { @@ -264,27 +233,6 @@ describe('A Graphics ECS Component', () => { expect(sut.graphics.default).toBe(rect); }); - it('can have multiple layers', () => { - const rect = new ex.Rectangle({ - width: 40, - height: 40 - }); - const sut = new ex.GraphicsComponent(); - - sut.show(rect); - sut.layers.create({ name: 'background', order: -1 }).show(rect); - - const layers = sut.layers.get(); - - expect(sut.layers.has('background')).toBeTrue(); - expect(sut.layers.has('default')).toBeTrue(); - expect(layers.length).toBe(2); - expect(layers[0].name).toBe('background'); - expect(layers[0].order).toBe(-1); - expect(layers[1].name).toBe('default'); - expect(layers[1].order).toBe(0); - }); - it('ticks graphics that need ticking', () => { const animation = new ex.Animation({ frames: [] @@ -299,27 +247,9 @@ describe('A Graphics ECS Component', () => { expect(animation.tick).toHaveBeenCalledWith(123, 4); }); - it('currentKeys should return names of graphics show in all layers', () => { - const rect = new ex.Rectangle({ - width: 40, - height: 40 - }); - const sut = new ex.GraphicsComponent(); - sut.layers.create({ name: 'background', order: -1 }).show(rect); - const layers = sut.layers.currentKeys(); - expect(typeof layers).toBe('object'); - expect(layers.length).toBe(2); - }); it('correctly calculates graphics bounds (rasters)', () => { const sut = new ex.GraphicsComponent(); - const rec = new ex.Rectangle({ - width: 40, - height: 40 - }); - rec.scale = ex.vec(3, 3); - sut.add(rec); - const rec2 = new ex.Rectangle({ width: 200, height: 10 @@ -330,56 +260,42 @@ describe('A Graphics ECS Component', () => { expect(sut.localBounds).toEqual(new ex.BoundingBox({ left: -200, right: 200, - top: -60, - bottom: 60 + top: -10, + bottom: 10 })); }); it('correctly calculates graphics bounds (rasters + offset)', () => { const sut = new ex.GraphicsComponent(); - const rec = new ex.Rectangle({ - width: 40, - height: 40 - }); - rec.scale = ex.vec(3, 3); - sut.add(rec); - const rec2 = new ex.Rectangle({ width: 200, height: 10 }); - rec2.scale = ex.vec(2, 2); - sut.show(rec2, { offset: ex.vec(100, 0)}); + rec2.scale = ex.vec(2, 2); // width 400, height 20 + sut.use(rec2, { offset: ex.vec(100, 0)}); // offset 100 to the right expect(sut.localBounds).toEqual(new ex.BoundingBox({ left: -100, right: 300, - top: -60, - bottom: 60 + top: -10, + bottom: 10 })); }); it('correctly calculates graphics bounds (rasters + anchor)', () => { const sut = new ex.GraphicsComponent(); - const rec = new ex.Rectangle({ - width: 40, - height: 40 - }); - rec.scale = ex.vec(3, 3); - sut.add(rec); - const rec2 = new ex.Rectangle({ width: 200, height: 10 }); - rec2.scale = ex.vec(2, 2); - sut.show(rec2, { anchor: ex.vec(1, 1)}); + rec2.scale = ex.vec(2, 2); // width 400, height 20 + sut.use(rec2, { anchor: ex.vec(1, 1)}); // anchor at the bottom right expect(sut.localBounds).toEqual(new ex.BoundingBox({ left: -400, - right: 60, - top: -60, - bottom: 60 + right: 0, + top: -20, + bottom: 0 })); }); diff --git a/src/spec/GraphicsGroupSpec.ts b/src/spec/GraphicsGroupSpec.ts index 90d7da93d..dbf345e8f 100644 --- a/src/spec/GraphicsGroupSpec.ts +++ b/src/spec/GraphicsGroupSpec.ts @@ -26,8 +26,8 @@ describe('A Graphics Group', () => { const group = new ex.GraphicsGroup({ members: [ - { pos: ex.vec(0, 0), graphic: rect1 }, - { pos: ex.vec(25, 25), graphic: rect2 } + { offset: ex.vec(0, 0), graphic: rect1 }, + { offset: ex.vec(25, 25), graphic: rect2 } ] }); @@ -52,7 +52,7 @@ describe('A Graphics Group', () => { frames: [] }); const group = new ex.GraphicsGroup({ - members: [{ pos: ex.vec(0, 0), graphic: animation }] + members: [{ offset: ex.vec(0, 0), graphic: animation }] }); const clone = group.clone(); @@ -68,7 +68,7 @@ describe('A Graphics Group', () => { }); const group = new ex.GraphicsGroup({ - members: [{ pos: ex.vec(0, 0), graphic: animation }] + members: [{ offset: ex.vec(0, 0), graphic: animation }] }); group.tick(100, 1234); @@ -84,7 +84,7 @@ describe('A Graphics Group', () => { }); const group = new ex.GraphicsGroup({ - members: [{ pos: ex.vec(0, 0), graphic: animation }] + members: [{ offset: ex.vec(0, 0), graphic: animation }] }); group.reset(); diff --git a/src/spec/GraphicsSystemSpec.ts b/src/spec/GraphicsSystemSpec.ts index 013264c24..3cd6e3836 100644 --- a/src/spec/GraphicsSystemSpec.ts +++ b/src/spec/GraphicsSystemSpec.ts @@ -64,19 +64,19 @@ describe('A Graphics ECS System', () => { entities[0].get(TransformComponent).pos = ex.vec(25, 25); entities[0].get(TransformComponent).rotation = Math.PI / 4; - entities[0].get(GraphicsComponent).show(rect); + entities[0].get(GraphicsComponent).use(rect); entities[1].get(TransformComponent).pos = ex.vec(75, 75); - entities[1].get(GraphicsComponent).show(circle); + entities[1].get(GraphicsComponent).use(circle); entities[2].get(TransformComponent).pos = ex.vec(75, 25); entities[2].get(TransformComponent).scale = ex.vec(2, 2); - entities[2].get(GraphicsComponent).show(rect2); + entities[2].get(GraphicsComponent).use(rect2); const offscreenRect = rect.clone(); const offscreen = new ex.Entity().addComponent(new TransformComponent()).addComponent(new GraphicsComponent()); offscreen.get(TransformComponent).pos = ex.vec(112.5, 112.5); - offscreen.get(GraphicsComponent).show(offscreenRect); + offscreen.get(GraphicsComponent).use(offscreenRect); spyOn(rect, 'draw').and.callThrough(); spyOn(circle, 'draw').and.callThrough(); diff --git a/src/spec/OffscreenSystemSpec.ts b/src/spec/OffscreenSystemSpec.ts index e270b6de0..02d343631 100644 --- a/src/spec/OffscreenSystemSpec.ts +++ b/src/spec/OffscreenSystemSpec.ts @@ -40,7 +40,7 @@ describe('The OffscreenSystem', () => { const offscreen = new ex.Entity([new TransformComponent(), new GraphicsComponent()]); - offscreen.get(GraphicsComponent).show(rect); + offscreen.get(GraphicsComponent).use(rect); offscreen.get(TransformComponent).pos = ex.vec(112.5, 112.5); const offscreenSpy = jasmine.createSpy('offscreenSpy');