• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 15354777440

30 May 2025 08:03PM UTC coverage: 87.858% (-1.5%) from 89.344%
15354777440

Pull #3385

github

web-flow
Merge a00f57733 into e6ec66358
Pull Request #3385: updated Meet action to add tolerance

5002 of 6948 branches covered (71.99%)

3 of 5 new or added lines in 2 files covered. (60.0%)

872 existing lines in 83 files now uncovered.

13661 of 15549 relevant lines covered (87.86%)

25187.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.68
/src/engine/Graphics/GraphicsSystem.ts
1
import type { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext';
2
import type { Scene } from '../Scene';
3
import { GraphicsComponent } from './GraphicsComponent';
4
import { vec, Vector } from '../Math/vector';
5
import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent';
6
import type { Entity } from '../EntityComponentSystem/Entity';
7
import type { Camera } from '../Camera';
8
import type { Query, World } from '../EntityComponentSystem';
9
import { System, SystemPriority, SystemType } from '../EntityComponentSystem';
10
import type { Engine } from '../Engine';
11
import { GraphicsGroup } from './GraphicsGroup';
12
import { ParallaxComponent } from './ParallaxComponent';
13
import { CoordPlane } from '../Math/coord-plane';
14
import { BodyComponent } from '../Collision/BodyComponent';
15
import { FontCache } from './FontCache';
16
import { PostDrawEvent, PostTransformDrawEvent, PreDrawEvent, PreTransformDrawEvent } from '../Events';
17
import { Transform } from '../Math/transform';
18
import { blendTransform } from './TransformInterpolation';
19
import { Graphic } from './Graphic';
20

21
export class GraphicsSystem extends System {
117✔
22
  static priority = SystemPriority.Average;
23

24
  public readonly systemType = SystemType.Draw;
1,285✔
25
  private _token = 0;
1,285✔
26
  // Set in the initialize
27
  private _graphicsContext!: ExcaliburGraphicsContext;
28
  private _camera!: Camera;
29
  private _engine!: Engine;
30
  private _sortedTransforms: TransformComponent[] = [];
1,285✔
31
  query: Query<typeof TransformComponent | typeof GraphicsComponent>;
32
  public get sortedTransforms() {
33
    return this._sortedTransforms;
1✔
34
  }
35

36
  constructor(public world: World) {
1,285✔
37
    super();
1,285✔
38
    this.query = this.world.query([TransformComponent, GraphicsComponent]);
1,285✔
39
    this.query.entityAdded$.subscribe((e) => {
1,285✔
40
      const tx = e.get(TransformComponent);
1,956✔
41
      this._sortedTransforms.push(tx);
1,956✔
42
      tx.zIndexChanged$.subscribe(this._zIndexUpdate);
1,956✔
43
      this._zHasChanged = true;
1,956✔
44
    });
45
    this.query.entityRemoved$.subscribe((e) => {
1,285✔
46
      const tx = e.get(TransformComponent);
34✔
47
      tx.zIndexChanged$.unsubscribe(this._zIndexUpdate);
34✔
48
      const index = this._sortedTransforms.indexOf(tx);
34✔
49
      if (index > -1) {
34!
50
        this._sortedTransforms.splice(index, 1);
34✔
51
      }
52
    });
53
  }
54

55
  public initialize(world: World, scene: Scene): void {
56
    this._camera = scene.camera;
674✔
57
    this._engine = scene.engine;
674✔
58
  }
59

60
  private _zHasChanged = false;
1,285✔
61
  private _zIndexUpdate = () => {
1,285✔
62
    this._zHasChanged = true;
898✔
63
  };
64

65
  public preupdate(): void {
66
    // Graphics context could be switched to fallback in a new frame
67
    this._graphicsContext = this._engine.graphicsContext;
944✔
68
    if (this._zHasChanged) {
944✔
69
      this._sortedTransforms.sort((a, b) => {
293✔
70
        return a.globalZ - b.globalZ;
5,294✔
71
      });
72
      this._zHasChanged = false;
293✔
73
    }
74
  }
75

76
  public update(elapsed: number): void {
77
    this._token++;
943✔
78
    let graphics: GraphicsComponent;
79
    FontCache.checkAndClearCache();
943✔
80

81
    // This is a performance enhancement, most things are in world space
82
    // so if we can only do this once saves a ton of transform updates
83
    this._graphicsContext.save();
943✔
84
    if (this._camera) {
943!
85
      this._camera.draw(this._graphicsContext);
943✔
86
    }
87
    for (let transformIndex = 0; transformIndex < this._sortedTransforms.length; transformIndex++) {
943✔
88
      const transform = this._sortedTransforms[transformIndex];
2,383✔
89
      const entity = transform.owner as Entity;
2,383✔
90

91
      // If the entity is offscreen skip
92
      if (entity.hasTag('ex.offscreen')) {
2,383✔
93
        continue;
77✔
94
      }
95

96
      graphics = entity.get(GraphicsComponent);
2,306✔
97
      // Exit if graphics set to not visible
98
      if (!graphics.isVisible) {
2,306✔
99
        continue;
5✔
100
      }
101

102
      // Optionally run the onPreTransformDraw graphics lifecycle draw
103
      if (graphics.onPreTransformDraw) {
2,301!
UNCOV
104
        graphics.onPreTransformDraw(this._graphicsContext, elapsed);
×
105
      }
106
      entity.events.emit('pretransformdraw', new PreTransformDrawEvent(this._graphicsContext, elapsed, entity));
2,301✔
107

108
      // This optionally sets our camera based on the entity coord plan (world vs. screen)
109
      if (transform.coordPlane === CoordPlane.Screen) {
2,301✔
110
        this._graphicsContext.restore();
19✔
111
      }
112

113
      this._graphicsContext.save();
2,301✔
114
      if (transform.coordPlane === CoordPlane.Screen) {
2,301✔
115
        this._graphicsContext.translate(this._engine.screen.contentArea.left, this._engine.screen.contentArea.top);
19✔
116
      }
117

118
      // Tick any graphics state (but only once) for animations and graphics groups
119
      graphics.update(elapsed, this._token);
2,301✔
120

121
      // Apply parallax
122
      const parallax = entity.get(ParallaxComponent);
2,301✔
123
      if (parallax) {
2,301✔
124
        // We use the Tiled formula
125
        // https://doc.mapeditor.org/en/latest/manual/layers/#parallax-scrolling-factor
126
        // cameraPos * (1 - parallaxFactor)
127
        const oneMinusFactor = Vector.One.sub(parallax.parallaxFactor);
5✔
128
        const parallaxOffset = this._camera.drawPos.scale(oneMinusFactor);
5✔
129
        this._graphicsContext.translate(parallaxOffset.x, parallaxOffset.y);
5✔
130
      }
131

132
      // Position the entity + estimate lag
133
      this._applyTransform(entity);
2,301✔
134

135
      // If there is a material enable it on the context
136
      if (graphics.material) {
2,301✔
137
        this._graphicsContext.material = graphics.material;
3✔
138
      }
139

140
      // Optionally run the onPreDraw graphics lifecycle draw
141
      if (graphics.onPreDraw) {
2,301!
UNCOV
142
        graphics.onPreDraw(this._graphicsContext, elapsed);
×
143
      }
144
      entity.events.emit('predraw', new PreDrawEvent(this._graphicsContext, elapsed, entity));
2,301✔
145

146
      // this._graphicsContext.opacity *= graphics.opacity;
147
      this._applyOpacity(entity);
2,301✔
148

149
      // Draw the graphics component
150
      this._drawGraphicsComponent(graphics, transform);
2,301✔
151

152
      // Optionally run the onPostDraw graphics lifecycle draw
153
      if (graphics.onPostDraw) {
2,301✔
154
        graphics.onPostDraw(this._graphicsContext, elapsed);
978✔
155
      }
156
      entity.events.emit('postdraw', new PostDrawEvent(this._graphicsContext, elapsed, entity));
2,301✔
157

158
      this._graphicsContext.restore();
2,301✔
159

160
      // Reset the transform back to the original world space
161
      if (transform.coordPlane === CoordPlane.Screen) {
2,301✔
162
        this._graphicsContext.save();
19✔
163
        if (this._camera) {
19!
164
          this._camera.draw(this._graphicsContext);
19✔
165
        }
166
      }
167

168
      // Optionally run the onPreTransformDraw graphics lifecycle draw
169
      if (graphics.onPostTransformDraw) {
2,301!
UNCOV
170
        graphics.onPostTransformDraw(this._graphicsContext, elapsed);
×
171
      }
172
      entity.events.emit('posttransformdraw', new PostTransformDrawEvent(this._graphicsContext, elapsed, entity));
2,301✔
173
    }
174
    this._graphicsContext.restore();
943✔
175
  }
176

177
  private _drawGraphicsComponent(graphicsComponent: GraphicsComponent, transformComponent: TransformComponent) {
178
    if (graphicsComponent.isVisible) {
2,301!
179
      const flipHorizontal = graphicsComponent.flipHorizontal;
2,301✔
180
      const flipVertical = graphicsComponent.flipVertical;
2,301✔
181

182
      const graphic = graphicsComponent.current;
2,301✔
183
      const options = graphicsComponent.currentOptions ?? {};
2,301!
184

185
      if (graphic) {
2,301✔
186
        let anchor = graphicsComponent.anchor;
1,068✔
187
        let offset = graphicsComponent.offset;
1,068✔
188
        let scaleX = 1;
1,068✔
189
        let scaleY = 1;
1,068✔
190
        // handle specific overrides
191
        if (options?.anchor) {
1,068!
UNCOV
192
          anchor = options.anchor;
×
193
        }
194
        if (options?.offset) {
1,068!
UNCOV
195
          offset = options.offset;
×
196
        }
197
        const globalScale = transformComponent.globalScale;
1,068✔
198
        scaleX *= graphic.scale.x * globalScale.x;
1,068✔
199
        scaleY *= graphic.scale.y * globalScale.y;
1,068✔
200

201
        // See https://github.com/excaliburjs/Excalibur/pull/619 for discussion on this formula
202
        const offsetX = -graphic.width * anchor.x + offset.x * scaleX;
1,068✔
203
        const offsetY = -graphic.height * anchor.y + offset.y * scaleY;
1,068✔
204

205
        const oldFlipHorizontal = graphic.flipHorizontal;
1,068✔
206
        const oldFlipVertical = graphic.flipVertical;
1,068✔
207
        if (flipHorizontal || flipVertical) {
1,068✔
208
          // flip any currently flipped graphics
209
          graphic.flipHorizontal = flipHorizontal ? !oldFlipHorizontal : oldFlipHorizontal;
4✔
210
          graphic.flipVertical = flipVertical ? !oldFlipVertical : oldFlipVertical;
4✔
211
        }
212

213
        graphic?.draw(this._graphicsContext, offsetX, offsetY);
1,068!
214

215
        if (flipHorizontal || flipVertical) {
1,068✔
216
          graphic.flipHorizontal = oldFlipHorizontal;
4✔
217
          graphic.flipVertical = oldFlipVertical;
4✔
218
        }
219

220
        // TODO move debug code out?
221
        if (this._engine?.isDebug && this._engine.debug.graphics.showBounds) {
1,068!
222
          const offset = vec(offsetX, offsetY);
×
223
          if (graphic instanceof GraphicsGroup) {
×
UNCOV
224
            for (const member of graphic.members) {
×
225
              let g: Graphic;
226
              let pos: Vector = Vector.Zero;
×
227
              if (member instanceof Graphic) {
×
UNCOV
228
                g = member;
×
229
              } else {
230
                g = member.graphic;
×
UNCOV
231
                pos = member.offset;
×
232
              }
233

234
              if (graphic.useAnchor) {
×
UNCOV
235
                g?.localBounds.translate(offset.add(pos)).draw(this._graphicsContext, this._engine.debug.graphics.boundsColor);
×
236
              } else {
UNCOV
237
                g?.localBounds.translate(pos).draw(this._graphicsContext, this._engine.debug.graphics.boundsColor);
×
238
              }
239
            }
240
          } else {
241
            /* istanbul ignore next */
UNCOV
242
            graphic?.localBounds.translate(offset).draw(this._graphicsContext, this._engine.debug.graphics.boundsColor);
×
243
          }
244
        }
245
      }
246
    }
247
  }
248

249
  private _targetInterpolationTransform = new Transform();
1,285✔
250
  /**
251
   * This applies the current entity transform to the graphics context
252
   * @param entity
253
   */
254
  private _applyTransform(entity: Entity): void {
255
    const ancestors = entity.getAncestors();
2,301✔
256
    for (let i = 0; i < ancestors.length; i++) {
2,301✔
257
      const ancestor = ancestors[i];
3,207✔
258
      const transform = ancestor?.get(TransformComponent);
3,207!
259
      const optionalBody = ancestor?.get(BodyComponent);
3,207!
260
      if (transform) {
3,207✔
261
        let tx = transform.get();
3,206✔
262
        if (optionalBody) {
3,206✔
263
          if (this._engine.fixedUpdateTimestep && optionalBody.__oldTransformCaptured && optionalBody.enableFixedUpdateInterpolate) {
2,224✔
264
            // Interpolate graphics if needed
265
            const blend = this._engine.currentFrameLagMs / this._engine.fixedUpdateTimestep;
392✔
266
            tx = blendTransform(optionalBody.oldTransform, transform.get(), blend, this._targetInterpolationTransform);
392✔
267
          }
268
        }
269
        this._graphicsContext.z = transform.globalZ;
3,206✔
270
        this._graphicsContext.translate(tx.pos.x, tx.pos.y);
3,206✔
271
        this._graphicsContext.scale(tx.scale.x, tx.scale.y);
3,206✔
272
        this._graphicsContext.rotate(tx.rotation);
3,206✔
273
      }
274
    }
275
  }
276

277
  private _applyOpacity(entity: Entity): void {
278
    const ancestors = entity.getAncestors();
2,301✔
279
    for (let i = 0; i < ancestors.length; i++) {
2,301✔
280
      const ancestor = ancestors[i];
3,207✔
281
      const maybeGraphics = ancestor?.get(GraphicsComponent);
3,207!
282
      this._graphicsContext.opacity *= maybeGraphics?.opacity ?? 1;
3,207✔
283
    }
284
  }
285
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc