• 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

4.76
/src/engine/Graphics/Context/texture-loader.ts
1
import { GarbageCollector } from '../../GarbageCollector';
2
import { Logger } from '../../Util/Log';
3
import { ImageFiltering } from '../Filtering';
4
import { ImageSourceOptions, ImageWrapConfiguration } from '../ImageSource';
5
import { ImageWrapping } from '../Wrapping';
6
import { HTMLImageSource } from './ExcaliburGraphicsContext';
7

8
/**
9
 * Manages loading image sources into webgl textures, a unique id is associated with all sources
10
 */
11
export class TextureLoader {
12
  private static _LOGGER = Logger.getInstance();
1✔
13

14
  constructor(
15
    gl: WebGL2RenderingContext,
UNCOV
16
    private _garbageCollector?: {
×
17
      garbageCollector: GarbageCollector;
18
      collectionInterval: number;
19
    }
20
  ) {
UNCOV
21
    this._gl = gl;
×
UNCOV
22
    TextureLoader._MAX_TEXTURE_SIZE = gl.getParameter(gl.MAX_TEXTURE_SIZE);
×
UNCOV
23
    if (this._garbageCollector) {
×
UNCOV
24
      TextureLoader._LOGGER.debug('WebGL Texture collection interval:', this._garbageCollector.collectionInterval);
×
UNCOV
25
      this._garbageCollector.garbageCollector?.registerCollector('texture', this._garbageCollector.collectionInterval, this._collect);
×
26
    }
27
  }
28

29
  public dispose() {
UNCOV
30
    for (const [image] of this._textureMap) {
×
UNCOV
31
      this.delete(image);
×
32
    }
UNCOV
33
    this._textureMap.clear();
×
UNCOV
34
    this._gl = null as any;
×
35
  }
36

37
  /**
38
   * Sets the default filtering for the Excalibur texture loader, default {@apilink ImageFiltering.Blended}
39
   */
40
  public static filtering: ImageFiltering = ImageFiltering.Blended;
1✔
41
  public static wrapping: ImageWrapConfiguration = { x: ImageWrapping.Clamp, y: ImageWrapping.Clamp };
1✔
42

43
  private _gl: WebGL2RenderingContext;
44

UNCOV
45
  private _textureMap = new Map<HTMLImageSource, WebGLTexture>();
×
46

47
  private static _MAX_TEXTURE_SIZE: number = 4096;
1✔
48

49
  /**
50
   * Get the WebGL Texture from a source image
51
   * @param image
52
   */
53
  public get(image: HTMLImageSource): WebGLTexture {
UNCOV
54
    return this._textureMap.get(image)!;
×
55
  }
56

57
  /**
58
   * Returns whether a source image has been loaded as a texture
59
   * @param image
60
   */
61
  public has(image: HTMLImageSource): boolean {
UNCOV
62
    return this._textureMap.has(image)!;
×
63
  }
64

65
  /**
66
   * Loads a graphic into webgl and returns it's texture info, a webgl context must be previously registered
67
   * @param image Source graphic
68
   * @param options {ImageSourceOptions} Optionally configure the ImageFiltering and ImageWrapping mode to apply to the loaded texture
69
   * @param forceUpdate Optionally force a texture to be reloaded, useful if the source graphic has changed
70
   */
71
  public load(image: HTMLImageSource, options?: ImageSourceOptions, forceUpdate = false): WebGLTexture | null {
×
72
    // Ignore loading if webgl is not registered
UNCOV
73
    const gl = this._gl;
×
UNCOV
74
    if (!gl) {
×
75
      return null;
×
76
    }
77

UNCOV
78
    const { filtering, wrapping } = { ...options };
×
79

UNCOV
80
    let tex: WebGLTexture | null = null;
×
81
    // If reuse the texture if it's from the same source
UNCOV
82
    if (this.has(image)) {
×
UNCOV
83
      tex = this.get(image);
×
84
    }
85

86
    // Update existing webgl texture and return early
UNCOV
87
    if (tex) {
×
UNCOV
88
      if (forceUpdate) {
×
UNCOV
89
        gl.bindTexture(gl.TEXTURE_2D, tex);
×
UNCOV
90
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
×
91
      }
UNCOV
92
      this._garbageCollector?.garbageCollector.touch(image);
×
UNCOV
93
      return tex;
×
94
    }
95

96
    // No texture exists create a new one
UNCOV
97
    tex = gl.createTexture();
×
98

99
    // TODO implement texture gc with weakmap and timer
UNCOV
100
    TextureLoader.checkImageSizeSupportedAndLog(image);
×
101

UNCOV
102
    gl.bindTexture(gl.TEXTURE_2D, tex);
×
103

UNCOV
104
    gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
×
105

106
    let wrappingConfig: ImageWrapConfiguration | undefined;
UNCOV
107
    if (wrapping) {
×
UNCOV
108
      if (typeof wrapping === 'string') {
×
109
        wrappingConfig = {
×
110
          x: wrapping,
111
          y: wrapping
112
        };
113
      } else {
UNCOV
114
        wrappingConfig = {
×
115
          x: wrapping.x,
116
          y: wrapping.y
117
        };
118
      }
119
    }
UNCOV
120
    const { x: xWrap, y: yWrap } = wrappingConfig ?? TextureLoader.wrapping;
×
UNCOV
121
    switch (xWrap) {
×
122
      case ImageWrapping.Clamp:
×
UNCOV
123
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
×
UNCOV
124
        break;
×
125
      case ImageWrapping.Repeat:
UNCOV
126
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
×
UNCOV
127
        break;
×
128
      case ImageWrapping.Mirror:
UNCOV
129
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
×
UNCOV
130
        break;
×
131
      default:
132
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
×
133
    }
UNCOV
134
    switch (yWrap) {
×
135
      case ImageWrapping.Clamp:
×
UNCOV
136
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
×
UNCOV
137
        break;
×
138
      case ImageWrapping.Repeat:
UNCOV
139
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
×
UNCOV
140
        break;
×
141
      case ImageWrapping.Mirror:
UNCOV
142
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
×
UNCOV
143
        break;
×
144
      default:
145
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
×
146
    }
147

148
    // NEAREST for pixel art, LINEAR for hi-res
UNCOV
149
    const filterMode = filtering ?? TextureLoader.filtering;
×
150

UNCOV
151
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterMode === ImageFiltering.Pixel ? gl.NEAREST : gl.LINEAR);
×
UNCOV
152
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterMode === ImageFiltering.Pixel ? gl.NEAREST : gl.LINEAR);
×
153

UNCOV
154
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
×
155

UNCOV
156
    this._textureMap.set(image, tex!);
×
UNCOV
157
    this._garbageCollector?.garbageCollector.addCollectableResource('texture', image);
×
UNCOV
158
    return tex;
×
159
  }
160

161
  public delete(image: HTMLImageSource): void {
162
    // Ignore loading if webgl is not registered
UNCOV
163
    const gl = this._gl;
×
UNCOV
164
    if (!gl) {
×
165
      return;
×
166
    }
167

UNCOV
168
    if (this.has(image)) {
×
UNCOV
169
      const texture = this.get(image);
×
UNCOV
170
      if (texture) {
×
UNCOV
171
        this._textureMap.delete(image);
×
UNCOV
172
        gl.deleteTexture(texture);
×
173
      }
174
    }
175
  }
176

177
  /**
178
   * Takes an image and returns if it meets size criteria for hardware
179
   * @param image
180
   * @returns if the image will be supported at runtime
181
   */
182
  public static checkImageSizeSupportedAndLog(image: HTMLImageSource) {
UNCOV
183
    const originalSrc = image.dataset.originalSrc ?? 'internal canvas bitmap';
×
UNCOV
184
    if (image.width > TextureLoader._MAX_TEXTURE_SIZE || image.height > TextureLoader._MAX_TEXTURE_SIZE) {
×
UNCOV
185
      TextureLoader._LOGGER.error(
×
186
        `The image [${originalSrc}] provided to Excalibur is too large for the device's maximum texture size of ` +
187
          `(${TextureLoader._MAX_TEXTURE_SIZE}x${TextureLoader._MAX_TEXTURE_SIZE}) please resize to an image ` +
188
          `for excalibur to render properly.\n\nImages will likely render as black rectangles.\n\n` +
189
          `Read more here: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#understand_system_limits`
190
      );
UNCOV
191
      return false;
×
UNCOV
192
    } else if (image.width > 4096 || image.height > 4096) {
×
193
      // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#understand_system_limits
UNCOV
194
      TextureLoader._LOGGER.warn(
×
195
        `The image [${originalSrc}] provided to excalibur is too large may not work on all mobile devices, ` +
196
          `it is recommended you resize images to a maximum (4096x4096).\n\n` +
197
          `Images will likely render as black rectangles on some mobile platforms.\n\n` +
198
          `Read more here: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#understand_system_limits`
199
      );
200
    }
UNCOV
201
    return true;
×
202
  }
203

204
  /**
205
   * Looks for textures that haven't been drawn in a while
206
   */
UNCOV
207
  private _collect = (image: HTMLImageSource) => {
×
UNCOV
208
    if (this._gl) {
×
UNCOV
209
      const name = image.dataset.originalSrc ?? image.constructor.name;
×
UNCOV
210
      TextureLoader._LOGGER.debug(`WebGL Texture for ${name} collected`);
×
UNCOV
211
      this.delete(image);
×
UNCOV
212
      return true;
×
213
    }
214
    return false;
×
215
  };
216
}
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