• 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

94.52
/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts
1
import type {
2
  ExcaliburGraphicsContext,
3
  LineGraphicsOptions,
4
  RectGraphicsOptions,
5
  PointGraphicsOptions,
6
  ExcaliburGraphicsContextOptions,
7
  DebugDraw,
8
  HTMLImageSource
9
} from './ExcaliburGraphicsContext';
10

11
import { Matrix } from '../../Math/matrix';
12
import { TransformStack } from './transform-stack';
13
import type { Vector } from '../../Math/vector';
14
import { vec } from '../../Math/vector';
15
import { Color } from '../../Color';
16
import { StateStack } from './state-stack';
17
import { Logger } from '../../Util/Log';
18
import { DebugText } from './debug-text';
19
import type { Resolution } from '../../Screen';
20
import { RenderTarget } from './render-target';
21
import type { PostProcessor } from '../PostProcessor/PostProcessor';
22
import { TextureLoader } from './texture-loader';
23
import type { RendererPlugin } from './renderer';
24

25
// renderers
26
import { LineRenderer } from './line-renderer/line-renderer';
27
import { PointRenderer } from './point-renderer/point-renderer';
28
import { ScreenPassPainter } from './screen-pass-painter/screen-pass-painter';
29
import { ImageRenderer } from './image-renderer/image-renderer';
30
import { RectangleRenderer } from './rectangle-renderer/rectangle-renderer';
31
import { CircleRenderer } from './circle-renderer/circle-renderer';
32
import { Pool } from '../../Util/Pool';
33
import { DrawCall } from './draw-call';
34
import type { AffineMatrix } from '../../Math/affine-matrix';
35
import type { MaterialOptions } from './material';
36
import { Material } from './material';
37
import { MaterialRenderer } from './material-renderer/material-renderer';
38
import type { ShaderOptions } from './shader';
39
import { Shader } from './shader';
40
import type { GarbageCollector } from '../../GarbageCollector';
41
import { ParticleRenderer } from './particle-renderer/particle-renderer';
42
import { ImageRendererV2 } from './image-renderer-v2/image-renderer-v2';
43
import { Flags } from '../../Flags';
44

45
export const pixelSnapEpsilon = 0.0001;
117✔
46

47
class ExcaliburGraphicsContextWebGLDebug implements DebugDraw {
48
  private _debugText = new DebugText();
850✔
49
  constructor(private _webglCtx: ExcaliburGraphicsContextWebGL) {}
850✔
50

51
  /**
52
   * Draw a debugging rectangle to the context
53
   * @param x
54
   * @param y
55
   * @param width
56
   * @param height
57
   */
58
  drawRect(x: number, y: number, width: number, height: number, rectOptions: RectGraphicsOptions = { color: Color.Black }): void {
×
59
    this.drawLine(vec(x, y), vec(x + width, y), { ...rectOptions });
108✔
60
    this.drawLine(vec(x + width, y), vec(x + width, y + height), { ...rectOptions });
108✔
61
    this.drawLine(vec(x + width, y + height), vec(x, y + height), { ...rectOptions });
108✔
62
    this.drawLine(vec(x, y + height), vec(x, y), { ...rectOptions });
108✔
63
  }
64

65
  /**
66
   * Draw a debugging line to the context
67
   * @param start
68
   * @param end
69
   * @param lineOptions
70
   */
71
  drawLine(start: Vector, end: Vector, lineOptions?: LineGraphicsOptions): void {
72
    this._webglCtx.draw<LineRenderer>('ex.line', start, end, lineOptions?.color ?? Color.Black);
442!
73
  }
74

75
  /**
76
   * Draw a debugging point to the context
77
   * @param point
78
   * @param pointOptions
79
   */
80
  drawPoint(point: Vector, pointOptions: PointGraphicsOptions = { color: Color.Black, size: 5 }): void {
×
81
    this._webglCtx.draw<PointRenderer>('ex.point', point, pointOptions.color, pointOptions.size);
284✔
82
  }
83

84
  drawText(text: string, pos: Vector) {
85
    this._debugText.write(this._webglCtx, text, pos);
16✔
86
  }
87
}
88

89
export interface WebGLGraphicsContextInfo {
90
  transform: TransformStack;
91
  state: StateStack;
92
  ortho: Matrix;
93
  context: ExcaliburGraphicsContextWebGL;
94
}
95

96
export interface ExcaliburGraphicsContextWebGLOptions extends ExcaliburGraphicsContextOptions {
97
  context?: WebGL2RenderingContext;
98
  garbageCollector?: { garbageCollector: GarbageCollector; collectionInterval: number };
99
  handleContextLost?: (e: Event) => void;
100
  handleContextRestored?: (e: Event) => void;
101
}
102

103
export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext {
104
  private _logger = Logger.getInstance();
850✔
105
  private _renderers: Map<string, RendererPlugin> = new Map<string, RendererPlugin>();
850✔
106
  private _lazyRenderersFactory: Map<string, () => RendererPlugin> = new Map<string, () => RendererPlugin>();
850✔
107
  public imageRenderer: 'ex.image' | 'ex.image-v2' = Flags.isEnabled('use-legacy-image-renderer') ? 'ex.image' : 'ex.image-v2';
850!
108
  private _isDrawLifecycle = false;
850✔
109
  public useDrawSorting = true;
850✔
110

111
  private _drawCallPool = new Pool<DrawCall>(() => new DrawCall(), undefined, 4000);
3,400,000✔
112

113
  private _drawCallIndex = 0;
850✔
114
  private _drawCalls: DrawCall[] = new Array(4000).fill(null);
850✔
115

116
  // Main render target
117
  private _renderTarget!: RenderTarget;
118

119
  // Quad boundary MSAA
120
  private _msaaTarget!: RenderTarget;
121

122
  // Postprocessing is a tuple with 2 render targets, these are flip-flopped during the postprocessing process
123
  private _postProcessTargets: RenderTarget[] = [];
850✔
124

125
  private _screenRenderer!: ScreenPassPainter;
126

127
  private _postprocessors: PostProcessor[] = [];
850✔
128

129
  /**
130
   * Meant for internal use only. Access the internal context at your own risk and no guarantees this will exist in the future.
131
   * @internal
132
   */
133
  public __gl: WebGL2RenderingContext;
134

135
  private _transform = new TransformStack();
850✔
136
  private _state = new StateStack();
850✔
137
  private _ortho!: Matrix;
138

139
  /**
140
   * Snaps the drawing x/y coordinate to the nearest whole pixel
141
   */
142
  public snapToPixel: boolean = false;
850✔
143

144
  /**
145
   * Native context smoothing
146
   */
147
  public readonly smoothing: boolean = false;
850✔
148

149
  /**
150
   * Whether the pixel art sampler is enabled for smooth sub pixel anti-aliasing
151
   */
152
  public readonly pixelArtSampler: boolean = false;
850✔
153

154
  /**
155
   * UV padding in pixels to use in internal image rendering to prevent texture bleed
156
   *
157
   */
158
  public uvPadding = 0.01;
850✔
159

160
  public backgroundColor: Color = Color.ExcaliburBlue;
850✔
161

162
  public textureLoader: TextureLoader;
163

164
  public materialScreenTexture!: WebGLTexture | null;
165

166
  public get z(): number {
UNCOV
167
    return this._state.current.z;
×
168
  }
169

170
  public set z(value: number) {
171
    this._state.current.z = value;
3,686✔
172
  }
173

174
  public get opacity(): number {
175
    return this._state.current.opacity;
11,431✔
176
  }
177

178
  public set opacity(value: number) {
179
    this._state.current.opacity = value;
7,034✔
180
  }
181

182
  public get tint(): Color | undefined | null {
183
    return this._state.current.tint;
3,798✔
184
  }
185

186
  public set tint(color: Color | undefined | null) {
187
    this._state.current.tint = color;
1✔
188
  }
189

190
  public get width() {
191
    return this.__gl.canvas.width;
876✔
192
  }
193

194
  public get height() {
195
    return this.__gl.canvas.height;
876✔
196
  }
197

198
  public get ortho(): Matrix {
199
    return this._ortho;
6,338✔
200
  }
201

202
  /**
203
   * Checks the underlying webgl implementation if the requested internal resolution is supported
204
   * @param dim
205
   */
206
  public checkIfResolutionSupported(dim: Resolution): boolean {
207
    // Slight hack based on this thread https://groups.google.com/g/webgl-dev-list/c/AHONvz3oQTo
208
    let supported = true;
762✔
209
    if (dim.width > 4096 || dim.height > 4096) {
762✔
210
      supported = false;
1✔
211
    }
212
    return supported;
762✔
213
  }
214

215
  public readonly multiSampleAntialiasing: boolean = true;
850✔
216
  public readonly samples?: number;
217
  public readonly transparency: boolean = true;
850✔
218
  private _isContextLost = false;
850✔
219

220
  constructor(options: ExcaliburGraphicsContextWebGLOptions) {
221
    const {
222
      canvasElement,
223
      context,
224
      enableTransparency,
225
      antialiasing,
226
      uvPadding,
227
      multiSampleAntialiasing,
228
      pixelArtSampler,
229
      powerPreference,
230
      snapToPixel,
231
      backgroundColor,
232
      useDrawSorting,
233
      garbageCollector,
234
      handleContextLost,
235
      handleContextRestored
236
    } = options;
850✔
237
    this.__gl =
850✔
238
      context ??
850!
239
      (canvasElement.getContext('webgl2', {
240
        antialias: antialiasing ?? this.smoothing,
850✔
241
        premultipliedAlpha: false,
242
        alpha: enableTransparency ?? this.transparency,
850✔
243
        depth: false,
244
        powerPreference: powerPreference ?? 'high-performance'
850✔
245
      }) as WebGL2RenderingContext);
246
    if (!this.__gl) {
850!
UNCOV
247
      throw Error('Failed to retrieve webgl context from browser');
×
248
    }
249

250
    if (handleContextLost) {
850✔
251
      this.__gl.canvas.addEventListener('webglcontextlost', handleContextLost, false);
726✔
252
    }
253

254
    if (handleContextRestored) {
850!
UNCOV
255
      this.__gl.canvas.addEventListener('webglcontextrestored', handleContextRestored, false);
×
256
    }
257

258
    this.__gl.canvas.addEventListener('webglcontextlost', () => {
850✔
259
      this._isContextLost = true;
642✔
260
    });
261

262
    this.__gl.canvas.addEventListener('webglcontextrestored', () => {
850✔
263
      this._isContextLost = false;
266✔
264
    });
265

266
    this.textureLoader = new TextureLoader(this.__gl, garbageCollector);
850✔
267
    this.snapToPixel = snapToPixel ?? this.snapToPixel;
850✔
268
    this.smoothing = antialiasing ?? this.smoothing;
850✔
269
    this.transparency = enableTransparency ?? this.transparency;
850✔
270
    this.pixelArtSampler = pixelArtSampler ?? this.pixelArtSampler;
850✔
271
    this.uvPadding = uvPadding ?? this.uvPadding;
850✔
272
    this.multiSampleAntialiasing = typeof multiSampleAntialiasing === 'boolean' ? multiSampleAntialiasing : this.multiSampleAntialiasing;
850✔
273
    this.samples = typeof multiSampleAntialiasing === 'object' ? multiSampleAntialiasing.samples : undefined;
850!
274
    this.backgroundColor = backgroundColor ?? this.backgroundColor;
850✔
275
    this.useDrawSorting = useDrawSorting ?? this.useDrawSorting;
850✔
276
    this._drawCallPool.disableWarnings = true;
850✔
277
    this._drawCallPool.preallocate();
850✔
278
    this._init();
850✔
279
  }
280

281
  private _disposed = false;
850✔
282
  public dispose() {
283
    if (!this._disposed) {
758!
284
      this._disposed = true;
758✔
285
      this.textureLoader.dispose();
758✔
286
      for (const renderer of this._renderers.values()) {
758✔
287
        renderer.dispose();
5,309✔
288
      }
289
      this._renderers.clear();
758✔
290
      this._drawCallPool.dispose();
758✔
291
      this._drawCalls.length = 0;
758✔
292
      this.__gl = null as any;
758✔
293
    }
294
  }
295

296
  private _init() {
297
    const gl = this.__gl;
850✔
298
    // Setup viewport and view matrix
299
    this._ortho = Matrix.ortho(0, gl.canvas.width, gl.canvas.height, 0, 400, -400);
850✔
300
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
850✔
301

302
    // Clear background
303
    gl.clearColor(this.backgroundColor.r / 255, this.backgroundColor.g / 255, this.backgroundColor.b / 255, this.backgroundColor.a);
850✔
304
    gl.clear(gl.COLOR_BUFFER_BIT);
850✔
305

306
    // Enable alpha blending
307
    // https://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
308
    gl.enable(gl.BLEND);
850✔
309
    gl.blendEquation(gl.FUNC_ADD);
850✔
310
    gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
850✔
311
    gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
850✔
312
    gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
850✔
313
    gl.depthMask(false);
850✔
314
    // Setup builtin renderers
315
    this.register(
850✔
316
      new ImageRenderer({
317
        uvPadding: this.uvPadding,
318
        pixelArtSampler: this.pixelArtSampler
319
      })
320
    );
321
    this.register(new MaterialRenderer());
850✔
322
    this.register(new RectangleRenderer());
850✔
323
    this.register(new CircleRenderer());
850✔
324
    this.register(new PointRenderer());
850✔
325
    this.register(new LineRenderer());
850✔
326
    this.lazyRegister<ParticleRenderer>('ex.particle', () => new ParticleRenderer());
850✔
327
    this.register(
850✔
328
      new ImageRendererV2({
329
        uvPadding: this.uvPadding,
330
        pixelArtSampler: this.pixelArtSampler
331
      })
332
    );
333

334
    this.materialScreenTexture = gl.createTexture();
850✔
335
    if (!this.materialScreenTexture) {
850!
UNCOV
336
      throw new Error('Could not create screen texture!');
×
337
    }
338
    gl.bindTexture(gl.TEXTURE_2D, this.materialScreenTexture);
850✔
339
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
850✔
340
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
850✔
341
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
850✔
342
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
850✔
343
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
850✔
344
    gl.bindTexture(gl.TEXTURE_2D, null);
850✔
345

346
    this._screenRenderer = new ScreenPassPainter(this);
850✔
347

348
    this._renderTarget = new RenderTarget({
850✔
349
      gl,
350
      transparency: this.transparency,
351
      width: gl.canvas.width,
352
      height: gl.canvas.height
353
    });
354

355
    this._postProcessTargets = [
850✔
356
      new RenderTarget({
357
        gl,
358
        transparency: this.transparency,
359
        width: gl.canvas.width,
360
        height: gl.canvas.height
361
      }),
362
      new RenderTarget({
363
        gl,
364
        transparency: this.transparency,
365
        width: gl.canvas.width,
366
        height: gl.canvas.height
367
      })
368
    ];
369

370
    this._msaaTarget = new RenderTarget({
850✔
371
      gl,
372
      transparency: this.transparency,
373
      width: gl.canvas.width,
374
      height: gl.canvas.height,
375
      antialias: this.multiSampleAntialiasing,
376
      samples: this.samples
377
    });
378
  }
379

380
  public register<T extends RendererPlugin>(renderer: T) {
381
    this._renderers.set(renderer.type, renderer);
5,953✔
382
    renderer.initialize(this.__gl, this);
5,953✔
383
  }
384

385
  public lazyRegister<TRenderer extends RendererPlugin>(type: TRenderer['type'], renderer: () => TRenderer) {
386
    this._lazyRenderersFactory.set(type, renderer);
850✔
387
  }
388

389
  public get(rendererName: string): RendererPlugin | undefined {
390
    let maybeRenderer = this._renderers.get(rendererName);
6,077✔
391
    if (!maybeRenderer) {
6,077✔
392
      const lazyFactory = this._lazyRenderersFactory.get(rendererName);
4✔
393
      if (lazyFactory) {
4✔
394
        this._logger.debug('lazy init renderer:', rendererName);
3✔
395
        maybeRenderer = lazyFactory();
3✔
396
        this.register(maybeRenderer);
3✔
397
      }
398
    }
399
    return maybeRenderer;
6,077✔
400
  }
401

402
  private _currentRenderer: RendererPlugin | undefined;
403

404
  private _isCurrentRenderer(renderer: RendererPlugin): boolean {
405
    if (!this._currentRenderer || this._currentRenderer === renderer) {
8✔
406
      return true;
2✔
407
    }
408
    return false;
6✔
409
  }
410

411
  public beginDrawLifecycle() {
412
    this._isDrawLifecycle = true;
1,742✔
413
  }
414

415
  public endDrawLifecycle() {
416
    this._isDrawLifecycle = false;
1,742✔
417
  }
418

419
  public draw<TRenderer extends RendererPlugin>(rendererName: TRenderer['type'], ...args: Parameters<TRenderer['draw']>) {
420
    if (process.env.NODE_ENV === 'development') {
4,854!
421
      if (args.length > 9) {
4,854!
UNCOV
422
        throw new Error('Only 10 or less renderer arguments are supported!;');
×
423
      }
424
    }
425
    if (!this._isDrawLifecycle) {
4,854✔
426
      this._logger.warnOnce(
1,032✔
427
        `Attempting to draw outside the the drawing lifecycle (preDraw/postDraw) is not supported and is a source of bugs/errors.\n` +
428
          `If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPostDraw handler.`
429
      );
430
    }
431
    if (this._isContextLost) {
4,854!
UNCOV
432
      this._logger.errorOnce(`Unable to draw ${rendererName}, the webgl context is lost`);
×
UNCOV
433
      return;
×
434
    }
435

436
    const renderer = this.get(rendererName);
4,854✔
437
    if (renderer) {
4,854✔
438
      if (this.useDrawSorting) {
4,853✔
439
        const drawCall = this._drawCallPool.get();
4,845✔
440
        drawCall.z = this._state.current.z;
4,845✔
441
        drawCall.priority = renderer.priority;
4,845✔
442
        drawCall.renderer = rendererName;
4,845✔
443
        this.getTransform().clone(drawCall.transform);
4,845✔
444
        drawCall.state.z = this._state.current.z;
4,845✔
445
        drawCall.state.opacity = this._state.current.opacity;
4,845✔
446
        drawCall.state.tint = this._state.current.tint;
4,845✔
447
        drawCall.state.material = this._state.current.material;
4,845✔
448
        drawCall.args[0] = args[0];
4,845✔
449
        drawCall.args[1] = args[1];
4,845✔
450
        drawCall.args[2] = args[2];
4,845✔
451
        drawCall.args[3] = args[3];
4,845✔
452
        drawCall.args[4] = args[4];
4,845✔
453
        drawCall.args[5] = args[5];
4,845✔
454
        drawCall.args[6] = args[6];
4,845✔
455
        drawCall.args[7] = args[7];
4,845✔
456
        drawCall.args[8] = args[8];
4,845✔
457
        drawCall.args[9] = args[9];
4,845✔
458
        this._drawCalls[this._drawCallIndex++] = drawCall;
4,845✔
459
      } else {
460
        // Set the current renderer if not defined
461
        if (!this._currentRenderer) {
8✔
462
          this._currentRenderer = renderer;
2✔
463
        }
464

465
        if (!this._isCurrentRenderer(renderer)) {
8✔
466
          // switching graphics means we must flush the previous
467
          this._currentRenderer.flush();
6✔
468
        }
469

470
        // If we are still using the same renderer we can add to the current batch
471
        renderer.draw(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
8✔
472

473
        this._currentRenderer = renderer;
8✔
474
      }
475
    } else {
476
      throw Error(`No renderer with name ${rendererName} has been registered`);
1✔
477
    }
478
  }
479

480
  public resetTransform(): void {
481
    this._transform.reset();
762✔
482
  }
483

484
  public updateViewport(resolution: Resolution): void {
485
    const gl = this.__gl;
762✔
486
    this._ortho = this._ortho = Matrix.ortho(0, resolution.width, resolution.height, 0, 400, -400);
762✔
487

488
    this._renderTarget.setResolution(gl.canvas.width, gl.canvas.height);
762✔
489
    this._msaaTarget.setResolution(gl.canvas.width, gl.canvas.height);
762✔
490
    this._postProcessTargets[0].setResolution(gl.canvas.width, gl.canvas.height);
762✔
491
    this._postProcessTargets[1].setResolution(gl.canvas.width, gl.canvas.height);
762✔
492
  }
493

494
  private _imageToWidth = new Map<HTMLImageSource, number>();
850✔
495
  private _getImageWidth(image: HTMLImageSource) {
496
    let maybeWidth = this._imageToWidth.get(image);
3,809✔
497
    if (maybeWidth === undefined) {
3,809✔
498
      maybeWidth = image.width;
1,956✔
499
      this._imageToWidth.set(image, maybeWidth);
1,956✔
500
    }
501
    return maybeWidth;
3,809✔
502
  }
503

504
  private _imageToHeight = new Map<HTMLImageSource, number>();
850✔
505
  private _getImageHeight(image: HTMLImageSource) {
506
    let maybeHeight = this._imageToHeight.get(image);
3,808✔
507
    if (maybeHeight === undefined) {
3,808✔
508
      maybeHeight = image.height;
1,955✔
509
      this._imageToHeight.set(image, maybeHeight);
1,955✔
510
    }
511
    return maybeHeight;
3,808✔
512
  }
513

514
  drawImage(image: HTMLImageSource, x: number, y: number): void;
515
  drawImage(image: HTMLImageSource, x: number, y: number, width: number, height: number): void;
516
  drawImage(
517
    image: HTMLImageSource,
518
    sx: number,
519
    sy: number,
520
    swidth?: number,
521
    sheight?: number,
522
    dx?: number,
523
    dy?: number,
524
    dwidth?: number,
525
    dheight?: number
526
  ): void;
527
  drawImage(
528
    image: HTMLImageSource,
529
    sx: number,
530
    sy: number,
531
    swidth?: number,
532
    sheight?: number,
533
    dx?: number,
534
    dy?: number,
535
    dwidth?: number,
536
    dheight?: number
537
  ): void {
538
    if (swidth === 0 || sheight === 0) {
3,811✔
539
      return; // zero dimension dest exit early
1✔
540
    } else if (dwidth === 0 || dheight === 0) {
3,810✔
541
      return; // zero dimension dest exit early
1✔
542
    } else if (this._getImageWidth(image) === 0 || this._getImageHeight(image) === 0) {
3,809✔
543
      return; // zero dimension source exit early
1✔
544
    }
545

546
    if (!image) {
3,808!
UNCOV
547
      Logger.getInstance().warn('Cannot draw a null or undefined image');
×
548
      // tslint:disable-next-line: no-console
UNCOV
549
      if (console.trace) {
×
550
        // tslint:disable-next-line: no-console
UNCOV
551
        console.trace();
×
552
      }
UNCOV
553
      return;
×
554
    }
555

556
    if (this._state.current.material) {
3,808✔
557
      this.draw<MaterialRenderer>('ex.material', image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
7✔
558
    } else {
559
      if (this.imageRenderer === 'ex.image') {
3,801✔
560
        this.draw<ImageRenderer>(this.imageRenderer, image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
2✔
561
      } else {
562
        this.draw<ImageRendererV2>(this.imageRenderer, image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
3,799✔
563
      }
564
    }
565
  }
566

567
  public drawLine(start: Vector, end: Vector, color: Color, thickness = 1) {
×
568
    this.draw<RectangleRenderer>('ex.rectangle', start, end, color, thickness);
67✔
569
  }
570

571
  public drawRectangle(pos: Vector, width: number, height: number, color: Color, stroke?: Color, strokeThickness?: number) {
572
    this.draw<RectangleRenderer>('ex.rectangle', pos, width, height, color, stroke, strokeThickness);
7✔
573
  }
574

575
  public drawCircle(pos: Vector, radius: number, color: Color, stroke?: Color, thickness?: number) {
576
    this.draw<CircleRenderer>('ex.circle', pos, radius, color, stroke, thickness);
239✔
577
  }
578

579
  debug = new ExcaliburGraphicsContextWebGLDebug(this);
850✔
580

581
  public save(): void {
582
    this._transform.save();
8,796✔
583
    this._state.save();
8,796✔
584
  }
585

586
  public restore(): void {
587
    this._transform.restore();
8,796✔
588
    this._state.restore();
8,796✔
589
  }
590

591
  public translate(x: number, y: number): void {
592
    this._transform.translate(this.snapToPixel ? ~~(x + pixelSnapEpsilon) : x, this.snapToPixel ? ~~(y + pixelSnapEpsilon) : y);
8,428✔
593
  }
594

595
  public rotate(angle: number): void {
596
    this._transform.rotate(angle);
3,674✔
597
  }
598

599
  public scale(x: number, y: number): void {
600
    this._transform.scale(x, y);
5,618✔
601
  }
602

603
  public transform(matrix: AffineMatrix) {
UNCOV
604
    this._transform.current = matrix;
×
605
  }
606

607
  public getTransform(): AffineMatrix {
608
    return this._transform.current;
9,693✔
609
  }
610

611
  public multiply(m: AffineMatrix) {
612
    this._transform.current.multiply(m, this._transform.current);
5,042✔
613
  }
614

615
  public addPostProcessor(postprocessor: PostProcessor) {
616
    this._postprocessors.push(postprocessor);
8✔
617
    postprocessor.initialize(this);
8✔
618
  }
619

620
  public removePostProcessor(postprocessor: PostProcessor) {
621
    const index = this._postprocessors.indexOf(postprocessor);
6✔
622
    if (index !== -1) {
6!
UNCOV
623
      this._postprocessors.splice(index, 1);
×
624
    }
625
  }
626

627
  public clearPostProcessors() {
UNCOV
628
    this._postprocessors.length = 0;
×
629
  }
630

631
  private _totalPostProcessorTime = 0;
850✔
632
  public updatePostProcessors(elapsed: number) {
633
    for (const postprocessor of this._postprocessors) {
913✔
634
      const shader = postprocessor.getShader();
10✔
635
      shader.use();
10✔
636
      const uniforms = shader.getUniformDefinitions();
10✔
637
      this._totalPostProcessorTime += elapsed;
10✔
638

639
      if (uniforms.find((u) => u.name === 'u_time_ms')) {
26✔
640
        shader.setUniformFloat('u_time_ms', this._totalPostProcessorTime);
4✔
641
      }
642
      if (uniforms.find((u) => u.name === 'u_elapsed_ms')) {
30✔
643
        shader.setUniformFloat('u_elapsed_ms', elapsed);
4✔
644
      }
645
      if (uniforms.find((u) => u.name === 'u_resolution')) {
22✔
646
        shader.setUniformFloatVector('u_resolution', vec(this.width, this.height));
4✔
647
      }
648

649
      if (postprocessor.onUpdate) {
10✔
650
        postprocessor.onUpdate(elapsed);
4✔
651
      }
652
    }
653
  }
654

655
  public set material(material: Material | null | undefined) {
656
    this._state.current.material = material;
7✔
657
  }
658

659
  public get material(): Material | null | undefined {
660
    return this._state.current.material;
20✔
661
  }
662

663
  /**
664
   * Creates and initializes the material which compiles the internal shader
665
   * @param options
666
   * @returns Material
667
   */
668
  public createMaterial(options: Omit<MaterialOptions, 'graphicsContext'>): Material {
669
    const material = new Material({ ...options, graphicsContext: this });
2✔
670
    return material;
2✔
671
  }
672

673
  public createShader(options: Omit<ShaderOptions, 'graphicsContext'>): Shader {
674
    const { name, vertexSource, fragmentSource, uniforms, images, startingTextureSlot } = options;
10✔
675
    const shader = new Shader({
10✔
676
      name,
677
      graphicsContext: this,
678
      vertexSource,
679
      fragmentSource,
680
      uniforms,
681
      images,
682
      startingTextureSlot
683
    });
684
    shader.compile();
10✔
685
    return shader;
10✔
686
  }
687

688
  clear() {
689
    const gl = this.__gl;
1,809✔
690
    const currentTarget = this.multiSampleAntialiasing ? this._msaaTarget : this._renderTarget;
1,809✔
691
    currentTarget.use();
1,809✔
692
    gl.clearColor(this.backgroundColor.r / 255, this.backgroundColor.g / 255, this.backgroundColor.b / 255, this.backgroundColor.a);
1,809✔
693
    // Clear the context with the newly set color. This is
694
    // the function call that actually does the drawing.
695
    gl.clear(gl.COLOR_BUFFER_BIT);
1,809✔
696
  }
697

698
  /**
699
   * Flushes all batched rendering to the screen
700
   */
701
  flush() {
702
    if (this._isContextLost) {
1,854!
UNCOV
703
      this._logger.errorOnce(`Unable to flush the webgl context is lost`);
×
UNCOV
704
      return;
×
705
    }
706

707
    // render target captures all draws and redirects to the render target
708
    let currentTarget = this.multiSampleAntialiasing ? this._msaaTarget : this._renderTarget;
1,854✔
709
    currentTarget.use();
1,854✔
710

711
    if (this.useDrawSorting) {
1,854✔
712
      // null out unused draw calls
713
      for (let i = this._drawCallIndex; i < this._drawCalls.length; i++) {
1,852✔
714
        this._drawCalls[i] = null as any;
7,403,165✔
715
      }
716
      // sort draw calls
717
      // Find the original order of the first instance of the draw call
718
      const originalSort = new Map<string, number>();
1,852✔
719
      for (const [name] of this._renderers) {
1,852✔
720
        let firstIndex = 0;
12,970✔
721
        for (firstIndex = 0; firstIndex < this._drawCallIndex; firstIndex++) {
12,970✔
722
          if (this._drawCalls[firstIndex].renderer === name) {
28,214✔
723
            break;
1,209✔
724
          }
725
        }
726
        originalSort.set(name, firstIndex);
12,970✔
727
      }
728

729
      this._drawCalls.sort((a, b) => {
1,852✔
730
        if (a === null || b === null) {
7,406,670✔
731
          return 0;
7,402,776✔
732
        }
733
        const zIndex = a.z - b.z;
3,894✔
734
        const originalSortOrder = originalSort.get(a.renderer)! - originalSort.get(b.renderer)!;
3,894✔
735
        const priority = a.priority - b.priority;
3,894✔
736
        if (zIndex === 0) {
3,894✔
737
          // sort by z first
738
          if (priority === 0) {
3,729!
739
            // sort by priority
740
            return originalSortOrder; // use the original order to inform draw call packing to maximally preserve painter order
3,729✔
741
          }
UNCOV
742
          return priority;
×
743
        }
744
        return zIndex;
165✔
745
      });
746

747
      const oldTransform = this._transform.current;
1,852✔
748
      const oldState = this._state.current;
1,852✔
749

750
      if (this._drawCalls.length && this._drawCallIndex) {
1,852✔
751
        let currentRendererName = this._drawCalls[0].renderer;
1,194✔
752
        let currentRenderer = this.get(currentRendererName);
1,194✔
753
        for (let i = 0; i < this._drawCallIndex; i++) {
1,194✔
754
          // hydrate the state for renderers
755
          this._transform.current = this._drawCalls[i].transform;
4,835✔
756
          this._state.current = this._drawCalls[i].state;
4,835✔
757

758
          if (this._drawCalls[i].renderer !== currentRendererName) {
4,835✔
759
            // switching graphics renderer means we must flush the previous
760
            currentRenderer!.flush();
17✔
761
            currentRendererName = this._drawCalls[i].renderer;
17✔
762
            currentRenderer = this.get(currentRendererName);
17✔
763
          }
764

765
          // ! hack to grab screen texture before materials run because they might want it
766
          if (currentRenderer instanceof MaterialRenderer && this.material?.isUsingScreenTexture) {
4,835!
767
            currentTarget.copyToTexture(this.materialScreenTexture!);
2✔
768
            currentTarget.use();
2✔
769
          }
770
          // If we are still using the same renderer we can add to the current batch
771
          currentRenderer!.draw(...this._drawCalls[i].args);
4,835✔
772
        }
773
        if (currentRenderer!.hasPendingDraws()) {
1,194✔
774
          currentRenderer!.flush();
1,182✔
775
        }
776
      }
777

778
      // reset state
779
      this._transform.current = oldTransform;
1,852✔
780
      this._state.current = oldState;
1,852✔
781

782
      // reclaim draw calls
783
      this._drawCallPool.done();
1,852✔
784
      this._drawCallIndex = 0;
1,852✔
785
      this._imageToHeight.clear();
1,852✔
786
      this._imageToWidth.clear();
1,852✔
787
    } else {
788
      // This is the final flush at the moment to draw any leftover pending draw
789
      for (const renderer of this._renderers.values()) {
2✔
790
        if (renderer.hasPendingDraws()) {
14✔
791
          renderer.flush();
2✔
792
        }
793
      }
794
    }
795

796
    currentTarget.disable();
1,854✔
797

798
    // post process step
799
    if (this._postprocessors.length > 0) {
1,854✔
800
      currentTarget.toRenderSource().use();
6✔
801
    }
802

803
    // flip flop render targets for post processing
804
    for (let i = 0; i < this._postprocessors.length; i++) {
1,854✔
805
      currentTarget = this._postProcessTargets[i % 2];
6✔
806
      this._postProcessTargets[i % 2].use();
6✔
807
      this._screenRenderer.renderWithPostProcessor(this._postprocessors[i]);
6✔
808
      this._postProcessTargets[i % 2].toRenderSource().use();
6✔
809
    }
810

811
    // Final blit to the screen
812
    currentTarget.blitToScreen();
1,854✔
813
  }
814
}
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