• 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.32
/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts
1
import {
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 { Vector, vec } from '../../Math/vector';
14
import { Color } from '../../Color';
15
import { StateStack } from './state-stack';
16
import { Logger } from '../../Util/Log';
17
import { DebugText } from './debug-text';
18
import { Resolution } from '../../Screen';
19
import { RenderTarget } from './render-target';
20
import { PostProcessor } from '../PostProcessor/PostProcessor';
21
import { TextureLoader } from './texture-loader';
22
import { RendererPlugin } from './renderer';
23

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

42
export const pixelSnapEpsilon = 0.0001;
1✔
43

44
class ExcaliburGraphicsContextWebGLDebug implements DebugDraw {
UNCOV
45
  private _debugText = new DebugText();
×
UNCOV
46
  constructor(private _webglCtx: ExcaliburGraphicsContextWebGL) {}
×
47

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

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

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

81
  drawText(text: string, pos: Vector) {
UNCOV
82
    this._debugText.write(this._webglCtx, text, pos);
×
83
  }
84
}
85

86
export interface WebGLGraphicsContextInfo {
87
  transform: TransformStack;
88
  state: StateStack;
89
  ortho: Matrix;
90
  context: ExcaliburGraphicsContextWebGL;
91
}
92

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

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

UNCOV
108
  private _drawCallPool = new Pool<DrawCall>(() => new DrawCall(), undefined, 4000);
×
109

UNCOV
110
  private _drawCallIndex = 0;
×
UNCOV
111
  private _drawCalls: DrawCall[] = new Array(4000).fill(null);
×
112

113
  // Main render target
114
  private _renderTarget!: RenderTarget;
115

116
  // Quad boundary MSAA
117
  private _msaaTarget!: RenderTarget;
118

119
  // Postprocessing is a tuple with 2 render targets, these are flip-flopped during the postprocessing process
UNCOV
120
  private _postProcessTargets: RenderTarget[] = [];
×
121

122
  private _screenRenderer!: ScreenPassPainter;
123

UNCOV
124
  private _postprocessors: PostProcessor[] = [];
×
125

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

UNCOV
132
  private _transform = new TransformStack();
×
UNCOV
133
  private _state = new StateStack();
×
134
  private _ortho!: Matrix;
135

136
  /**
137
   * Snaps the drawing x/y coordinate to the nearest whole pixel
138
   */
UNCOV
139
  public snapToPixel: boolean = false;
×
140

141
  /**
142
   * Native context smoothing
143
   */
UNCOV
144
  public readonly smoothing: boolean = false;
×
145

146
  /**
147
   * Whether the pixel art sampler is enabled for smooth sub pixel anti-aliasing
148
   */
UNCOV
149
  public readonly pixelArtSampler: boolean = false;
×
150

151
  /**
152
   * UV padding in pixels to use in internal image rendering to prevent texture bleed
153
   *
154
   */
UNCOV
155
  public uvPadding = 0.01;
×
156

UNCOV
157
  public backgroundColor: Color = Color.ExcaliburBlue;
×
158

159
  public textureLoader: TextureLoader;
160

161
  public materialScreenTexture!: WebGLTexture | null;
162

163
  public get z(): number {
164
    return this._state.current.z;
×
165
  }
166

167
  public set z(value: number) {
UNCOV
168
    this._state.current.z = value;
×
169
  }
170

171
  public get opacity(): number {
UNCOV
172
    return this._state.current.opacity;
×
173
  }
174

175
  public set opacity(value: number) {
UNCOV
176
    this._state.current.opacity = value;
×
177
  }
178

179
  public get tint(): Color | undefined | null {
UNCOV
180
    return this._state.current.tint;
×
181
  }
182

183
  public set tint(color: Color | undefined | null) {
UNCOV
184
    this._state.current.tint = color;
×
185
  }
186

187
  public get width() {
UNCOV
188
    return this.__gl.canvas.width;
×
189
  }
190

191
  public get height() {
UNCOV
192
    return this.__gl.canvas.height;
×
193
  }
194

195
  public get ortho(): Matrix {
UNCOV
196
    return this._ortho;
×
197
  }
198

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

UNCOV
212
  public readonly multiSampleAntialiasing: boolean = true;
×
213
  public readonly samples?: number;
UNCOV
214
  public readonly transparency: boolean = true;
×
UNCOV
215
  private _isContextLost = false;
×
216

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

UNCOV
247
    if (handleContextLost) {
×
UNCOV
248
      this.__gl.canvas.addEventListener('webglcontextlost', handleContextLost, false);
×
249
    }
250

UNCOV
251
    if (handleContextRestored) {
×
252
      this.__gl.canvas.addEventListener('webglcontextrestored', handleContextRestored, false);
×
253
    }
254

UNCOV
255
    this.__gl.canvas.addEventListener('webglcontextlost', () => {
×
UNCOV
256
      this._isContextLost = true;
×
257
    });
258

UNCOV
259
    this.__gl.canvas.addEventListener('webglcontextrestored', () => {
×
UNCOV
260
      this._isContextLost = false;
×
261
    });
262

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

UNCOV
278
  private _disposed = false;
×
279
  public dispose() {
UNCOV
280
    if (!this._disposed) {
×
UNCOV
281
      this._disposed = true;
×
UNCOV
282
      this.textureLoader.dispose();
×
UNCOV
283
      for (const renderer of this._renderers.values()) {
×
UNCOV
284
        renderer.dispose();
×
285
      }
UNCOV
286
      this._renderers.clear();
×
UNCOV
287
      this._drawCallPool.dispose();
×
UNCOV
288
      this._drawCalls.length = 0;
×
UNCOV
289
      this.__gl = null as any;
×
290
    }
291
  }
292

293
  private _init() {
UNCOV
294
    const gl = this.__gl;
×
295
    // Setup viewport and view matrix
UNCOV
296
    this._ortho = Matrix.ortho(0, gl.canvas.width, gl.canvas.height, 0, 400, -400);
×
UNCOV
297
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
×
298

299
    // Clear background
UNCOV
300
    gl.clearColor(this.backgroundColor.r / 255, this.backgroundColor.g / 255, this.backgroundColor.b / 255, this.backgroundColor.a);
×
UNCOV
301
    gl.clear(gl.COLOR_BUFFER_BIT);
×
302

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

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

UNCOV
343
    this._screenRenderer = new ScreenPassPainter(this);
×
344

UNCOV
345
    this._renderTarget = new RenderTarget({
×
346
      gl,
347
      transparency: this.transparency,
348
      width: gl.canvas.width,
349
      height: gl.canvas.height
350
    });
351

UNCOV
352
    this._postProcessTargets = [
×
353
      new RenderTarget({
354
        gl,
355
        transparency: this.transparency,
356
        width: gl.canvas.width,
357
        height: gl.canvas.height
358
      }),
359
      new RenderTarget({
360
        gl,
361
        transparency: this.transparency,
362
        width: gl.canvas.width,
363
        height: gl.canvas.height
364
      })
365
    ];
366

UNCOV
367
    this._msaaTarget = new RenderTarget({
×
368
      gl,
369
      transparency: this.transparency,
370
      width: gl.canvas.width,
371
      height: gl.canvas.height,
372
      antialias: this.multiSampleAntialiasing,
373
      samples: this.samples
374
    });
375
  }
376

377
  public register<T extends RendererPlugin>(renderer: T) {
UNCOV
378
    this._renderers.set(renderer.type, renderer);
×
UNCOV
379
    renderer.initialize(this.__gl, this);
×
380
  }
381

382
  public lazyRegister<TRenderer extends RendererPlugin>(type: TRenderer['type'], renderer: () => TRenderer) {
UNCOV
383
    this._lazyRenderersFactory.set(type, renderer);
×
384
  }
385

386
  public get(rendererName: string): RendererPlugin | undefined {
UNCOV
387
    let maybeRenderer = this._renderers.get(rendererName);
×
UNCOV
388
    if (!maybeRenderer) {
×
UNCOV
389
      const lazyFactory = this._lazyRenderersFactory.get(rendererName);
×
UNCOV
390
      if (lazyFactory) {
×
UNCOV
391
        this._logger.debug('lazy init renderer:', rendererName);
×
UNCOV
392
        maybeRenderer = lazyFactory();
×
UNCOV
393
        this.register(maybeRenderer);
×
394
      }
395
    }
UNCOV
396
    return maybeRenderer;
×
397
  }
398

399
  private _currentRenderer: RendererPlugin | undefined;
400

401
  private _isCurrentRenderer(renderer: RendererPlugin): boolean {
UNCOV
402
    if (!this._currentRenderer || this._currentRenderer === renderer) {
×
UNCOV
403
      return true;
×
404
    }
UNCOV
405
    return false;
×
406
  }
407

408
  public beginDrawLifecycle() {
UNCOV
409
    this._isDrawLifecycle = true;
×
410
  }
411

412
  public endDrawLifecycle() {
UNCOV
413
    this._isDrawLifecycle = false;
×
414
  }
415

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

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

UNCOV
462
        if (!this._isCurrentRenderer(renderer)) {
×
463
          // switching graphics means we must flush the previous
UNCOV
464
          this._currentRenderer.flush();
×
465
        }
466

467
        // If we are still using the same renderer we can add to the current batch
UNCOV
468
        renderer.draw(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
×
469

UNCOV
470
        this._currentRenderer = renderer;
×
471
      }
472
    } else {
UNCOV
473
      throw Error(`No renderer with name ${rendererName} has been registered`);
×
474
    }
475
  }
476

477
  public resetTransform(): void {
UNCOV
478
    this._transform.reset();
×
479
  }
480

481
  public updateViewport(resolution: Resolution): void {
UNCOV
482
    const gl = this.__gl;
×
UNCOV
483
    this._ortho = this._ortho = Matrix.ortho(0, resolution.width, resolution.height, 0, 400, -400);
×
484

UNCOV
485
    this._renderTarget.setResolution(gl.canvas.width, gl.canvas.height);
×
UNCOV
486
    this._msaaTarget.setResolution(gl.canvas.width, gl.canvas.height);
×
UNCOV
487
    this._postProcessTargets[0].setResolution(gl.canvas.width, gl.canvas.height);
×
UNCOV
488
    this._postProcessTargets[1].setResolution(gl.canvas.width, gl.canvas.height);
×
489
  }
490

UNCOV
491
  private _imageToWidth = new Map<HTMLImageSource, number>();
×
492
  private _getImageWidth(image: HTMLImageSource) {
UNCOV
493
    let maybeWidth = this._imageToWidth.get(image);
×
UNCOV
494
    if (maybeWidth === undefined) {
×
UNCOV
495
      maybeWidth = image.width;
×
UNCOV
496
      this._imageToWidth.set(image, maybeWidth);
×
497
    }
UNCOV
498
    return maybeWidth;
×
499
  }
500

UNCOV
501
  private _imageToHeight = new Map<HTMLImageSource, number>();
×
502
  private _getImageHeight(image: HTMLImageSource) {
UNCOV
503
    let maybeHeight = this._imageToHeight.get(image);
×
UNCOV
504
    if (maybeHeight === undefined) {
×
UNCOV
505
      maybeHeight = image.height;
×
UNCOV
506
      this._imageToHeight.set(image, maybeHeight);
×
507
    }
UNCOV
508
    return maybeHeight;
×
509
  }
510

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

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

UNCOV
553
    if (this._state.current.material) {
×
UNCOV
554
      this.draw<MaterialRenderer>('ex.material', image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
×
555
    } else {
UNCOV
556
      if (this.imageRenderer === 'ex.image') {
×
UNCOV
557
        this.draw<ImageRenderer>(this.imageRenderer, image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
×
558
      } else {
UNCOV
559
        this.draw<ImageRendererV2>(this.imageRenderer, image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
×
560
      }
561
    }
562
  }
563

564
  public drawLine(start: Vector, end: Vector, color: Color, thickness = 1) {
×
UNCOV
565
    this.draw<RectangleRenderer>('ex.rectangle', start, end, color, thickness);
×
566
  }
567

568
  public drawRectangle(pos: Vector, width: number, height: number, color: Color, stroke?: Color, strokeThickness?: number) {
UNCOV
569
    this.draw<RectangleRenderer>('ex.rectangle', pos, width, height, color, stroke, strokeThickness);
×
570
  }
571

572
  public drawCircle(pos: Vector, radius: number, color: Color, stroke?: Color, thickness?: number) {
UNCOV
573
    this.draw<CircleRenderer>('ex.circle', pos, radius, color, stroke, thickness);
×
574
  }
575

UNCOV
576
  debug = new ExcaliburGraphicsContextWebGLDebug(this);
×
577

578
  public save(): void {
UNCOV
579
    this._transform.save();
×
UNCOV
580
    this._state.save();
×
581
  }
582

583
  public restore(): void {
UNCOV
584
    this._transform.restore();
×
UNCOV
585
    this._state.restore();
×
586
  }
587

588
  public translate(x: number, y: number): void {
UNCOV
589
    this._transform.translate(this.snapToPixel ? ~~(x + pixelSnapEpsilon) : x, this.snapToPixel ? ~~(y + pixelSnapEpsilon) : y);
×
590
  }
591

592
  public rotate(angle: number): void {
UNCOV
593
    this._transform.rotate(angle);
×
594
  }
595

596
  public scale(x: number, y: number): void {
UNCOV
597
    this._transform.scale(x, y);
×
598
  }
599

600
  public transform(matrix: AffineMatrix) {
601
    this._transform.current = matrix;
×
602
  }
603

604
  public getTransform(): AffineMatrix {
UNCOV
605
    return this._transform.current;
×
606
  }
607

608
  public multiply(m: AffineMatrix) {
UNCOV
609
    this._transform.current.multiply(m, this._transform.current);
×
610
  }
611

612
  public addPostProcessor(postprocessor: PostProcessor) {
UNCOV
613
    this._postprocessors.push(postprocessor);
×
UNCOV
614
    postprocessor.initialize(this);
×
615
  }
616

617
  public removePostProcessor(postprocessor: PostProcessor) {
UNCOV
618
    const index = this._postprocessors.indexOf(postprocessor);
×
UNCOV
619
    if (index !== -1) {
×
620
      this._postprocessors.splice(index, 1);
×
621
    }
622
  }
623

624
  public clearPostProcessors() {
625
    this._postprocessors.length = 0;
×
626
  }
627

UNCOV
628
  private _totalPostProcessorTime = 0;
×
629
  public updatePostProcessors(elapsed: number) {
UNCOV
630
    for (const postprocessor of this._postprocessors) {
×
UNCOV
631
      const shader = postprocessor.getShader();
×
UNCOV
632
      shader.use();
×
UNCOV
633
      const uniforms = shader.getUniformDefinitions();
×
UNCOV
634
      this._totalPostProcessorTime += elapsed;
×
635

UNCOV
636
      if (uniforms.find((u) => u.name === 'u_time_ms')) {
×
UNCOV
637
        shader.setUniformFloat('u_time_ms', this._totalPostProcessorTime);
×
638
      }
UNCOV
639
      if (uniforms.find((u) => u.name === 'u_elapsed_ms')) {
×
UNCOV
640
        shader.setUniformFloat('u_elapsed_ms', elapsed);
×
641
      }
UNCOV
642
      if (uniforms.find((u) => u.name === 'u_resolution')) {
×
UNCOV
643
        shader.setUniformFloatVector('u_resolution', vec(this.width, this.height));
×
644
      }
645

UNCOV
646
      if (postprocessor.onUpdate) {
×
UNCOV
647
        postprocessor.onUpdate(elapsed);
×
648
      }
649
    }
650
  }
651

652
  public set material(material: Material | null | undefined) {
UNCOV
653
    this._state.current.material = material;
×
654
  }
655

656
  public get material(): Material | null | undefined {
UNCOV
657
    return this._state.current.material;
×
658
  }
659

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

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

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

695
  /**
696
   * Flushes all batched rendering to the screen
697
   */
698
  flush() {
UNCOV
699
    if (this._isContextLost) {
×
700
      this._logger.errorOnce(`Unable to flush the webgl context is lost`);
×
701
      return;
×
702
    }
703

704
    // render target captures all draws and redirects to the render target
UNCOV
705
    let currentTarget = this.multiSampleAntialiasing ? this._msaaTarget : this._renderTarget;
×
UNCOV
706
    currentTarget.use();
×
707

UNCOV
708
    if (this.useDrawSorting) {
×
709
      // null out unused draw calls
UNCOV
710
      for (let i = this._drawCallIndex; i < this._drawCalls.length; i++) {
×
UNCOV
711
        this._drawCalls[i] = null as any;
×
712
      }
713
      // sort draw calls
714
      // Find the original order of the first instance of the draw call
UNCOV
715
      const originalSort = new Map<string, number>();
×
UNCOV
716
      for (const [name] of this._renderers) {
×
UNCOV
717
        let firstIndex = 0;
×
UNCOV
718
        for (firstIndex = 0; firstIndex < this._drawCallIndex; firstIndex++) {
×
UNCOV
719
          if (this._drawCalls[firstIndex].renderer === name) {
×
UNCOV
720
            break;
×
721
          }
722
        }
UNCOV
723
        originalSort.set(name, firstIndex);
×
724
      }
725

UNCOV
726
      this._drawCalls.sort((a, b) => {
×
UNCOV
727
        if (a === null || b === null) {
×
UNCOV
728
          return 0;
×
729
        }
UNCOV
730
        const zIndex = a.z - b.z;
×
UNCOV
731
        const originalSortOrder = originalSort.get(a.renderer)! - originalSort.get(b.renderer)!;
×
UNCOV
732
        const priority = a.priority - b.priority;
×
UNCOV
733
        if (zIndex === 0) {
×
734
          // sort by z first
UNCOV
735
          if (priority === 0) {
×
736
            // sort by priority
UNCOV
737
            return originalSortOrder; // use the original order to inform draw call packing to maximally preserve painter order
×
738
          }
739
          return priority;
×
740
        }
UNCOV
741
        return zIndex;
×
742
      });
743

UNCOV
744
      const oldTransform = this._transform.current;
×
UNCOV
745
      const oldState = this._state.current;
×
746

UNCOV
747
      if (this._drawCalls.length && this._drawCallIndex) {
×
UNCOV
748
        let currentRendererName = this._drawCalls[0].renderer;
×
UNCOV
749
        let currentRenderer = this.get(currentRendererName);
×
UNCOV
750
        for (let i = 0; i < this._drawCallIndex; i++) {
×
751
          // hydrate the state for renderers
UNCOV
752
          this._transform.current = this._drawCalls[i].transform;
×
UNCOV
753
          this._state.current = this._drawCalls[i].state;
×
754

UNCOV
755
          if (this._drawCalls[i].renderer !== currentRendererName) {
×
756
            // switching graphics renderer means we must flush the previous
UNCOV
757
            currentRenderer!.flush();
×
UNCOV
758
            currentRendererName = this._drawCalls[i].renderer;
×
UNCOV
759
            currentRenderer = this.get(currentRendererName);
×
760
          }
761

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

775
      // reset state
UNCOV
776
      this._transform.current = oldTransform;
×
UNCOV
777
      this._state.current = oldState;
×
778

779
      // reclaim draw calls
UNCOV
780
      this._drawCallPool.done();
×
UNCOV
781
      this._drawCallIndex = 0;
×
UNCOV
782
      this._imageToHeight.clear();
×
UNCOV
783
      this._imageToWidth.clear();
×
784
    } else {
785
      // This is the final flush at the moment to draw any leftover pending draw
UNCOV
786
      for (const renderer of this._renderers.values()) {
×
UNCOV
787
        if (renderer.hasPendingDraws()) {
×
UNCOV
788
          renderer.flush();
×
789
        }
790
      }
791
    }
792

UNCOV
793
    currentTarget.disable();
×
794

795
    // post process step
UNCOV
796
    if (this._postprocessors.length > 0) {
×
UNCOV
797
      currentTarget.toRenderSource().use();
×
798
    }
799

800
    // flip flop render targets for post processing
UNCOV
801
    for (let i = 0; i < this._postprocessors.length; i++) {
×
UNCOV
802
      currentTarget = this._postProcessTargets[i % 2];
×
UNCOV
803
      this._postProcessTargets[i % 2].use();
×
UNCOV
804
      this._screenRenderer.renderWithPostProcessor(this._postprocessors[i]);
×
UNCOV
805
      this._postProcessTargets[i % 2].toRenderSource().use();
×
806
    }
807

808
    // Final blit to the screen
UNCOV
809
    currentTarget.blitToScreen();
×
810
  }
811
}
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