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

excaliburjs / Excalibur / 14804036802

02 May 2025 09:58PM UTC coverage: 5.927% (-83.4%) from 89.28%
14804036802

Pull #3404

github

web-flow
Merge 5c103d7f8 into 0f2ccaeb2
Pull Request #3404: feat: added Graph module to Math

234 of 8383 branches covered (2.79%)

229 of 246 new or added lines in 1 file covered. (93.09%)

13145 existing lines in 208 files now uncovered.

934 of 15759 relevant lines covered (5.93%)

4.72 hits per line

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

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

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

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

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

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

UNCOV
59
  private _zHasChanged = false;
×
UNCOV
60
  private _zIndexUpdate = () => {
×
UNCOV
61
    this._zHasChanged = true;
×
62
  };
63

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

75
  public update(elapsed: number): void {
UNCOV
76
    this._token++;
×
77
    let graphics: GraphicsComponent;
UNCOV
78
    FontCache.checkAndClearCache();
×
79

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

90
      // If the entity is offscreen skip
UNCOV
91
      if (entity.hasTag('ex.offscreen')) {
×
UNCOV
92
        continue;
×
93
      }
94

UNCOV
95
      graphics = entity.get(GraphicsComponent);
×
96
      // Exit if graphics set to not visible
UNCOV
97
      if (!graphics.isVisible) {
×
UNCOV
98
        continue;
×
99
      }
100

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

107
      // This optionally sets our camera based on the entity coord plan (world vs. screen)
UNCOV
108
      if (transform.coordPlane === CoordPlane.Screen) {
×
UNCOV
109
        this._graphicsContext.restore();
×
110
      }
111

UNCOV
112
      this._graphicsContext.save();
×
UNCOV
113
      if (transform.coordPlane === CoordPlane.Screen) {
×
UNCOV
114
        this._graphicsContext.translate(this._engine.screen.contentArea.left, this._engine.screen.contentArea.top);
×
115
      }
116

117
      // Tick any graphics state (but only once) for animations and graphics groups
UNCOV
118
      graphics.update(elapsed, this._token);
×
119

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

131
      // Position the entity + estimate lag
UNCOV
132
      this._applyTransform(entity);
×
133

134
      // If there is a material enable it on the context
UNCOV
135
      if (graphics.material) {
×
UNCOV
136
        this._graphicsContext.material = graphics.material;
×
137
      }
138

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

145
      // this._graphicsContext.opacity *= graphics.opacity;
UNCOV
146
      this._applyOpacity(entity);
×
147

148
      // Draw the graphics component
UNCOV
149
      this._drawGraphicsComponent(graphics, transform);
×
150

151
      // Optionally run the onPostDraw graphics lifecycle draw
UNCOV
152
      if (graphics.onPostDraw) {
×
UNCOV
153
        graphics.onPostDraw(this._graphicsContext, elapsed);
×
154
      }
UNCOV
155
      entity.events.emit('postdraw', new PostDrawEvent(this._graphicsContext, elapsed, entity));
×
156

UNCOV
157
      this._graphicsContext.restore();
×
158

159
      // Reset the transform back to the original world space
UNCOV
160
      if (transform.coordPlane === CoordPlane.Screen) {
×
UNCOV
161
        this._graphicsContext.save();
×
UNCOV
162
        if (this._camera) {
×
UNCOV
163
          this._camera.draw(this._graphicsContext);
×
164
        }
165
      }
166

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

176
  private _drawGraphicsComponent(graphicsComponent: GraphicsComponent, transformComponent: TransformComponent) {
UNCOV
177
    if (graphicsComponent.isVisible) {
×
UNCOV
178
      const flipHorizontal = graphicsComponent.flipHorizontal;
×
UNCOV
179
      const flipVertical = graphicsComponent.flipVertical;
×
180

UNCOV
181
      const graphic = graphicsComponent.current;
×
UNCOV
182
      const options = graphicsComponent.currentOptions ?? {};
×
183

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

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

UNCOV
204
        const oldFlipHorizontal = graphic.flipHorizontal;
×
UNCOV
205
        const oldFlipVertical = graphic.flipVertical;
×
UNCOV
206
        if (flipHorizontal || flipVertical) {
×
207
          // flip any currently flipped graphics
UNCOV
208
          graphic.flipHorizontal = flipHorizontal ? !oldFlipHorizontal : oldFlipHorizontal;
×
UNCOV
209
          graphic.flipVertical = flipVertical ? !oldFlipVertical : oldFlipVertical;
×
210
        }
211

UNCOV
212
        graphic?.draw(this._graphicsContext, offsetX, offsetY);
×
213

UNCOV
214
        if (flipHorizontal || flipVertical) {
×
UNCOV
215
          graphic.flipHorizontal = oldFlipHorizontal;
×
UNCOV
216
          graphic.flipVertical = oldFlipVertical;
×
217
        }
218

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

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

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

276
  private _applyOpacity(entity: Entity): void {
UNCOV
277
    const ancestors = entity.getAncestors();
×
UNCOV
278
    for (let i = 0; i < ancestors.length; i++) {
×
UNCOV
279
      const ancestor = ancestors[i];
×
UNCOV
280
      const maybeGraphics = ancestor?.get(GraphicsComponent);
×
UNCOV
281
      this._graphicsContext.opacity *= maybeGraphics?.opacity ?? 1;
×
282
    }
283
  }
284
}
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