• 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.0
/src/engine/Graphics/GraphicsComponent.ts
1
import { Vector, vec } from '../Math/vector';
2
import { Graphic } from './Graphic';
3
import { HasTick } from './Animation';
4
import { ExcaliburGraphicsContext } from './Context/ExcaliburGraphicsContext';
5
import { BoundingBox } from '../Collision/Index';
6
import { Component } from '../EntityComponentSystem/Component';
7
import { Material } from './Context/material';
8
import { Logger } from '../Util/Log';
9
import { WatchVector } from '../Math/watch-vector';
10
import { TransformComponent } from '../EntityComponentSystem';
11
import { GraphicsGroup } from '../Graphics/GraphicsGroup';
12
import { Color } from '../Color';
13
import { Raster } from './Raster';
14
import { Text } from './Text';
15

16
/**
17
 * Type guard for checking if a Graphic HasTick (used for graphics that change over time like animations)
18
 * @param graphic
19
 */
20
export function hasGraphicsTick(graphic: Graphic): graphic is Graphic & HasTick {
UNCOV
21
  return !!(graphic as unknown as HasTick).tick;
×
22
}
23
export interface GraphicsShowOptions {
24
  offset?: Vector;
25
  anchor?: Vector;
26
}
27

28
export interface GraphicsComponentOptions {
29
  onPostDraw?: (ex: ExcaliburGraphicsContext, elapsed: number) => void;
30
  onPreDraw?: (ex: ExcaliburGraphicsContext, elapsed: number) => void;
31
  onPreTransformDraw?: (ex: ExcaliburGraphicsContext, elapsed: number) => void;
32
  onPostTransformDraw?: (ex: ExcaliburGraphicsContext, elapsed: number) => void;
33

34
  /**
35
   * Name of current graphic to use
36
   */
37
  current?: string;
38

39
  /**
40
   * Optionally set the color of the graphics component
41
   */
42
  color?: Color;
43

44
  /**
45
   * Optionally set a material to use on the graphic
46
   */
47
  material?: Material;
48

49
  /**
50
   * Optionally copy instances of graphics by calling .clone(), you may set this to false to avoid sharing graphics when added to the
51
   * component for performance reasons. By default graphics are not copied and are shared when added to the component.
52
   */
53
  copyGraphics?: boolean;
54

55
  /**
56
   * Optional visible flag, if the graphics component is not visible it will not be displayed
57
   */
58
  visible?: boolean;
59

60
  /**
61
   * Optional opacity
62
   */
63
  opacity?: number;
64

65
  /**
66
   * List of graphics and optionally the options per graphic
67
   */
68
  graphics?: { [graphicName: string]: Graphic | { graphic: Graphic; options?: GraphicsShowOptions | undefined } };
69

70
  /**
71
   * Optional offset in absolute pixels to shift all graphics in this component from each graphic's anchor (default is top left corner)
72
   */
73
  offset?: Vector;
74

75
  /**
76
   * Optional anchor
77
   */
78
  anchor?: Vector;
79
}
80

81
/**
82
 * Component to manage drawings, using with the position component
83
 */
84
export class GraphicsComponent extends Component {
UNCOV
85
  private _logger = Logger.getInstance();
×
86

UNCOV
87
  private _current: string = 'default';
×
UNCOV
88
  private _graphics: Record<string, Graphic> = {};
×
UNCOV
89
  private _options: Record<string, GraphicsShowOptions | undefined> = {};
×
90

UNCOV
91
  public material: Material | null = null;
×
92

93
  /**
94
   * Draws after the entity transform has been applied, but before graphics component graphics have been drawn
95
   */
96
  public onPreDraw?: (ctx: ExcaliburGraphicsContext, elapsed: number) => void;
97

98
  /**
99
   * Draws after the entity transform has been applied, and after graphics component graphics has been drawn
100
   */
101
  public onPostDraw?: (ctx: ExcaliburGraphicsContext, elapsed: number) => void;
102

103
  /**
104
   * Draws before the entity transform has been applied before any any graphics component drawing
105
   */
106
  public onPreTransformDraw?: (ctx: ExcaliburGraphicsContext, elapsed: number) => void;
107

108
  /**
109
   * Draws after the entity transform has been applied, and after all graphics component drawing
110
   */
111
  public onPostTransformDraw?: (ctx: ExcaliburGraphicsContext, elapsed: number) => void;
112
  private _color?: Color;
113

114
  /**
115
   * Sets or gets wether any drawing should be visible in this component
116
   * @deprecated use isVisible
117
   */
118
  public get visible(): boolean {
UNCOV
119
    return this.isVisible;
×
120
  }
121

122
  /**
123
   * Sets or gets wether any drawing should be visible in this component
124
   * @deprecated use isVisible
125
   */
126
  public set visible(val: boolean) {
UNCOV
127
    this.isVisible = val;
×
128
  }
129

130
  /**
131
   * Sets or gets wether any drawing should be visible in this component
132
   */
UNCOV
133
  public isVisible: boolean = true;
×
134

135
  /**
136
   * Optionally force the graphic onscreen, default false. Not recommend to use for perf reasons, only if you known what you're doing.
137
   */
UNCOV
138
  public forceOnScreen: boolean = false;
×
139

140
  /**
141
   * Sets or gets wither all drawings should have an opacity applied
142
   */
UNCOV
143
  public opacity: number = 1;
×
144

UNCOV
145
  private _offset: Vector = new WatchVector(Vector.Zero, () => this.recalculateBounds());
×
146

147
  /**
148
   * Offset to apply to graphics by default
149
   */
150
  public get offset(): Vector {
UNCOV
151
    return this._offset;
×
152
  }
153
  public set offset(value: Vector) {
UNCOV
154
    this._offset = new WatchVector(value, () => this.recalculateBounds());
×
UNCOV
155
    this.recalculateBounds();
×
156
  }
157

UNCOV
158
  private _anchor: Vector = new WatchVector(Vector.Half, () => this.recalculateBounds());
×
159

160
  /**
161
   * Anchor to apply to graphics by default
162
   */
163
  public get anchor(): Vector {
UNCOV
164
    return this._anchor;
×
165
  }
166
  public set anchor(value: Vector) {
UNCOV
167
    this._anchor = new WatchVector(value, () => this.recalculateBounds());
×
UNCOV
168
    this.recalculateBounds();
×
169
  }
170

171
  /**
172
   * Sets the color of the actor's current graphic
173
   */
174
  public get color(): Color | undefined {
UNCOV
175
    return this._color;
×
176
  }
177
  public set color(v: Color | undefined) {
UNCOV
178
    if (v) {
×
UNCOV
179
      this._color = v.clone();
×
UNCOV
180
      const currentGraphic = this.graphics.current;
×
UNCOV
181
      if (currentGraphic instanceof Raster || currentGraphic instanceof Text) {
×
182
        currentGraphic.color = this._color;
×
183
      }
184
    }
185
  }
186

187
  /**
188
   * Flip all graphics horizontally along the y-axis
189
   */
UNCOV
190
  public flipHorizontal: boolean = false;
×
191

192
  /**
193
   * Flip all graphics vertically along the x-axis
194
   */
UNCOV
195
  public flipVertical: boolean = false;
×
196

197
  /**
198
   * If set to true graphics added to the component will be copied. This can effect performance, but is useful if you don't want
199
   * changes to a graphic to effect all the places it is used.
200
   */
UNCOV
201
  public copyGraphics: boolean = false;
×
202

203
  constructor(options?: GraphicsComponentOptions) {
UNCOV
204
    super();
×
205
    // Defaults
UNCOV
206
    options = {
×
207
      visible: this.isVisible,
208
      graphics: {},
209
      ...options
210
    };
211

212
    const {
213
      current,
214
      anchor,
215
      color,
216
      opacity,
217
      visible,
218
      graphics,
219
      offset,
220
      copyGraphics,
221
      onPreDraw,
222
      onPostDraw,
223
      onPreTransformDraw,
224
      onPostTransformDraw
UNCOV
225
    } = options;
×
226

UNCOV
227
    for (const [key, graphicOrOptions] of Object.entries(graphics as GraphicsComponentOptions)) {
×
UNCOV
228
      if (graphicOrOptions instanceof Graphic) {
×
UNCOV
229
        this._graphics[key] = graphicOrOptions;
×
230
      } else {
231
        this._graphics[key] = graphicOrOptions.graphic;
×
232
        this._options[key] = graphicOrOptions.options;
×
233
      }
234
    }
235

UNCOV
236
    this.offset = offset ?? this.offset;
×
UNCOV
237
    this.opacity = opacity ?? this.opacity;
×
UNCOV
238
    this.anchor = anchor ?? this.anchor;
×
UNCOV
239
    this.color = color ?? this.color;
×
UNCOV
240
    this.copyGraphics = copyGraphics ?? this.copyGraphics;
×
UNCOV
241
    this.onPreDraw = onPreDraw ?? this.onPreDraw;
×
UNCOV
242
    this.onPostDraw = onPostDraw ?? this.onPostDraw;
×
UNCOV
243
    this.onPreDraw = onPreTransformDraw ?? this.onPreTransformDraw;
×
UNCOV
244
    this.onPostTransformDraw = onPostTransformDraw ?? this.onPostTransformDraw;
×
UNCOV
245
    this.isVisible = !!visible;
×
UNCOV
246
    this._current = current ?? this._current;
×
UNCOV
247
    if (current && this._graphics[current]) {
×
UNCOV
248
      this.use(current);
×
249
    }
250
  }
251

252
  public getGraphic(name: string): Graphic | undefined {
UNCOV
253
    return this._graphics[name];
×
254
  }
255
  public getOptions(name: string): GraphicsShowOptions | undefined {
256
    return this._options[name];
×
257
  }
258

259
  /**
260
   * Get registered graphics names
261
   */
262
  public getNames(): string[] {
UNCOV
263
    return Object.keys(this._graphics);
×
264
  }
265

266
  /**
267
   * Returns the currently displayed graphic
268
   */
269
  public get current(): Graphic | undefined {
UNCOV
270
    return this._graphics[this._current];
×
271
  }
272

273
  /**
274
   * Returns the currently displayed graphic offsets
275
   */
276
  public get currentOptions(): GraphicsShowOptions | undefined {
UNCOV
277
    return this._options[this._current];
×
278
  }
279

280
  /**
281
   * Returns all graphics associated with this component
282
   */
283
  public get graphics(): { [graphicName: string]: Graphic } {
UNCOV
284
    return this._graphics;
×
285
  }
286

287
  /**
288
   * Returns all graphics options associated with this component
289
   */
290
  public get options(): { [graphicName: string]: GraphicsShowOptions | undefined } {
291
    return this._options;
×
292
  }
293

294
  /**
295
   * 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
296
   * @param graphic
297
   */
298
  public add(graphic: Graphic, options?: GraphicsShowOptions): Graphic;
299
  public add(name: string, graphic: Graphic, options?: GraphicsShowOptions): Graphic;
300
  public add(nameOrGraphic: string | Graphic, graphicOrOptions?: Graphic | GraphicsShowOptions, options?: GraphicsShowOptions): Graphic {
UNCOV
301
    let name = 'default';
×
UNCOV
302
    let graphicToSet: Graphic | null = null;
×
UNCOV
303
    let optionsToSet: GraphicsShowOptions | undefined = undefined;
×
UNCOV
304
    if (typeof nameOrGraphic === 'string' && graphicOrOptions instanceof Graphic) {
×
UNCOV
305
      name = nameOrGraphic;
×
UNCOV
306
      graphicToSet = graphicOrOptions;
×
UNCOV
307
      optionsToSet = options;
×
308
    }
UNCOV
309
    if (nameOrGraphic instanceof Graphic && !(graphicOrOptions instanceof Graphic)) {
×
UNCOV
310
      graphicToSet = nameOrGraphic;
×
UNCOV
311
      optionsToSet = graphicOrOptions;
×
312
    }
313

UNCOV
314
    if (!graphicToSet) {
×
315
      throw new Error('Need to provide a graphic or valid graphic string');
×
316
    }
UNCOV
317
    this._graphics[name] = this.copyGraphics ? graphicToSet.clone() : graphicToSet;
×
UNCOV
318
    this._options[name] = this.copyGraphics ? { ...optionsToSet } : optionsToSet;
×
UNCOV
319
    if (name === 'default') {
×
UNCOV
320
      this.use('default');
×
321
    }
UNCOV
322
    return graphicToSet;
×
323
  }
324

325
  /**
326
   * Removes a registered graphic, if the removed graphic is the current it will switch to the default
327
   * @param name
328
   */
329
  public remove(name: string) {
UNCOV
330
    delete this._graphics[name];
×
UNCOV
331
    delete this._options[name];
×
UNCOV
332
    if (this._current === name) {
×
UNCOV
333
      this._current = 'default';
×
UNCOV
334
      this.recalculateBounds();
×
335
    }
336
  }
337

338
  /**
339
   * Use a graphic only, will set the default graphic. Returns the new {@apilink Graphic}
340
   *
341
   * Optionally override the stored options
342
   * @param nameOrGraphic
343
   * @param options
344
   */
345
  public use<T extends Graphic = Graphic>(nameOrGraphic: string | T, options?: GraphicsShowOptions): T {
UNCOV
346
    if (nameOrGraphic instanceof Graphic) {
×
UNCOV
347
      let graphic = nameOrGraphic as Graphic;
×
UNCOV
348
      if (this.copyGraphics) {
×
UNCOV
349
        graphic = nameOrGraphic.clone();
×
350
      }
UNCOV
351
      this._current = 'default';
×
UNCOV
352
      this._graphics[this._current] = graphic;
×
UNCOV
353
      this._options[this._current] = options;
×
354
    } else {
UNCOV
355
      this._current = nameOrGraphic;
×
UNCOV
356
      this._options[this._current] = options;
×
UNCOV
357
      if (!(this._current in this._graphics)) {
×
UNCOV
358
        this._logger.warn(
×
359
          `Graphic ${this._current} is not registered with the graphics component owned by ${this.owner?.name}. Nothing will be drawn.`
×
360
        );
361
      }
362
    }
UNCOV
363
    this.recalculateBounds();
×
UNCOV
364
    return this.current as T;
×
365
  }
366

367
  /**
368
   * Hide currently shown graphic
369
   */
370
  public hide(): void {
UNCOV
371
    this._current = 'ex.none';
×
372
  }
373

374
  private _localBounds?: BoundingBox;
375
  public set localBounds(bounds: BoundingBox) {
UNCOV
376
    this._localBounds = bounds;
×
377
  }
378

379
  public recalculateBounds() {
UNCOV
380
    let bb = new BoundingBox();
×
UNCOV
381
    const graphic = this._graphics[this._current];
×
UNCOV
382
    const options = this._options[this._current];
×
383

UNCOV
384
    if (!graphic) {
×
UNCOV
385
      this._localBounds = bb;
×
UNCOV
386
      return;
×
387
    }
388

UNCOV
389
    let anchor = this.anchor;
×
UNCOV
390
    let offset = this.offset;
×
UNCOV
391
    if (options?.anchor) {
×
UNCOV
392
      anchor = options.anchor;
×
393
    }
UNCOV
394
    if (options?.offset) {
×
UNCOV
395
      offset = options.offset;
×
396
    }
UNCOV
397
    const bounds = graphic.localBounds;
×
UNCOV
398
    const offsetX = -bounds.width * anchor.x + offset.x;
×
UNCOV
399
    const offsetY = -bounds.height * anchor.y + offset.y;
×
UNCOV
400
    if (graphic instanceof GraphicsGroup && !graphic.useAnchor) {
×
401
      bb = graphic?.localBounds.combine(bb);
×
402
    } else {
UNCOV
403
      bb = graphic?.localBounds.translate(vec(offsetX, offsetY)).combine(bb);
×
404
    }
UNCOV
405
    this._localBounds = bb;
×
406
  }
407

408
  /**
409
   * Get local bounds of graphics component
410
   */
411
  public get localBounds(): BoundingBox {
UNCOV
412
    if (!this._localBounds || this._localBounds.hasZeroDimensions()) {
×
UNCOV
413
      this.recalculateBounds();
×
414
    }
UNCOV
415
    return this._localBounds as BoundingBox; // recalc guarantees type
×
416
  }
417

418
  /**
419
   * Get world bounds of graphics component
420
   */
421
  public get bounds(): BoundingBox {
UNCOV
422
    let bounds = this.localBounds;
×
UNCOV
423
    if (this.owner) {
×
UNCOV
424
      const tx = this.owner.get(TransformComponent);
×
UNCOV
425
      if (tx) {
×
UNCOV
426
        bounds = bounds.transform(tx.get().matrix);
×
427
      }
428
    }
UNCOV
429
    return bounds;
×
430
  }
431

432
  /**
433
   * Update underlying graphics if necessary, called internally
434
   * @param elapsed
435
   * @internal
436
   */
437
  public update(elapsed: number, idempotencyToken: number = 0) {
×
UNCOV
438
    const graphic = this.current;
×
UNCOV
439
    if (graphic && hasGraphicsTick(graphic)) {
×
UNCOV
440
      graphic.tick(elapsed, idempotencyToken);
×
441
    }
442
  }
443

444
  public clone(): GraphicsComponent {
UNCOV
445
    const graphics = new GraphicsComponent();
×
UNCOV
446
    graphics._graphics = { ...this._graphics };
×
UNCOV
447
    graphics._options = { ...this._options };
×
UNCOV
448
    graphics.offset = this.offset.clone();
×
UNCOV
449
    if (this.color) {
×
UNCOV
450
      graphics.color = this.color.clone();
×
451
    }
UNCOV
452
    graphics.opacity = this.opacity;
×
UNCOV
453
    graphics.anchor = this.anchor.clone();
×
UNCOV
454
    graphics.copyGraphics = this.copyGraphics;
×
UNCOV
455
    graphics.onPreDraw = this.onPreDraw;
×
UNCOV
456
    graphics.onPostDraw = this.onPostDraw;
×
UNCOV
457
    graphics.isVisible = this.isVisible;
×
458

UNCOV
459
    return graphics;
×
460
  }
461
}
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