• 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

91.3
/src/engine/Graphics/ImageSource.ts
1
import { Resource } from '../Resources/Resource';
2
import type { SpriteOptions } from './Sprite';
3
import { Sprite } from './Sprite';
4
import type { Loadable } from '../Interfaces/Index';
5
import { Logger } from '../Util/Log';
6
import { ImageFiltering } from './Filtering';
7
import { Future } from '../Util/Future';
8
import { TextureLoader } from '../Graphics/Context/texture-loader';
9
import { ImageWrapping } from './Wrapping';
10
import type { GraphicOptions } from './Graphic';
11

12
export interface ImageSourceOptions {
13
  filtering?: ImageFiltering;
14
  wrapping?: ImageWrapConfiguration | ImageWrapping;
15
  bustCache?: boolean;
16
}
17

18
export interface ImageWrapConfiguration {
19
  x: ImageWrapping;
20
  y: ImageWrapping;
21
}
22

23
export const ImageSourceAttributeConstants = {
117✔
24
  Filtering: 'filtering',
25
  WrappingX: 'wrapping-x',
26
  WrappingY: 'wrapping-y'
27
} as const;
28

29
export class ImageSource implements Loadable<HTMLImageElement> {
30
  private _logger = Logger.getInstance();
2,062✔
31
  private _resource: Resource<Blob>;
32
  public filtering?: ImageFiltering;
33
  public wrapping?: ImageWrapConfiguration;
34

35
  /**
36
   * The original size of the source image in pixels
37
   */
38
  public get width() {
39
    return this.image.naturalWidth;
125,736✔
40
  }
41

42
  /**
43
   * The original height of the source image in pixels
44
   */
45
  public get height() {
46
    return this.image.naturalHeight;
125,736✔
47
  }
48

49
  private _src?: string;
50
  /**
51
   * Returns true if the Texture is completely loaded and is ready
52
   * to be drawn.
53
   */
54
  public isLoaded(): boolean {
55
    if (!this._src) {
5,849✔
56
      // this boosts speed of access
57
      this._src = this.data.src;
2,079✔
58
    }
59
    return !!this._src;
5,849✔
60
  }
61

62
  /**
63
   * Access to the underlying html image element
64
   */
65
  public data: HTMLImageElement = new Image();
2,062✔
66
  public get image(): HTMLImageElement {
67
    return this.data;
253,692✔
68
  }
69

70
  private _readyFuture = new Future<HTMLImageElement>();
2,062✔
71
  /**
72
   * Promise the resolves when the image is loaded and ready for use, does not initiate loading
73
   */
74
  public ready: Promise<HTMLImageElement> = this._readyFuture.promise;
2,062✔
75

76
  public readonly path: string;
77

78
  /**
79
   * The path to the image, can also be a data url like 'data:image/'
80
   * @param pathOrBase64 {string} Path to the image resource relative from the HTML document hosting the game, or absolute
81
   * @param options
82
   */
83
  constructor(pathOrBase64: string, options?: ImageSourceOptions);
84
  /**
85
   * The path to the image, can also be a data url like 'data:image/'
86
   * @param pathOrBase64 {string} Path to the image resource relative from the HTML document hosting the game, or absolute
87
   * @param bustCache {boolean} Should excalibur add a cache busting querystring?
88
   * @param filtering {ImageFiltering} Optionally override the image filtering set by {@apilink EngineOptions.antialiasing}
89
   */
90
  constructor(pathOrBase64: string, bustCache: boolean, filtering?: ImageFiltering);
91
  constructor(pathOrBase64: string, bustCacheOrOptions: boolean | ImageSourceOptions | undefined, filtering?: ImageFiltering) {
92
    this.path = pathOrBase64;
2,062✔
93
    let bustCache: boolean | undefined = false;
2,062✔
94
    let wrapping: ImageWrapConfiguration | ImageWrapping | undefined;
95
    if (typeof bustCacheOrOptions === 'boolean') {
2,062✔
96
      bustCache = bustCacheOrOptions;
101✔
97
    } else {
98
      ({ filtering, wrapping, bustCache } = { ...bustCacheOrOptions });
1,961✔
99
    }
100
    this._resource = new Resource(pathOrBase64, 'blob', bustCache);
2,062✔
101
    this.filtering = filtering ?? this.filtering;
2,062✔
102
    if (typeof wrapping === 'string') {
2,062✔
103
      this.wrapping = {
2✔
104
        x: wrapping,
105
        y: wrapping
106
      };
107
    } else {
108
      this.wrapping = wrapping ?? this.wrapping;
2,060✔
109
    }
110
    if (pathOrBase64.endsWith('.gif')) {
2,062✔
111
      this._logger.warn(
1✔
112
        `Use the ex.Gif type to load gifs, you may have mixed results with ${pathOrBase64} in ex.ImageSource. Fully supported: svg, jpg, bmp, and png`
113
      );
114
    }
115
  }
116

117
  /**
118
   * Create an ImageSource from and HTML <image> tag element
119
   * @param image
120
   */
121
  static fromHtmlImageElement(image: HTMLImageElement, options?: ImageSourceOptions) {
122
    const imageSource = new ImageSource('');
5✔
123
    imageSource._src = 'image-element';
5✔
124
    imageSource.data = image;
5✔
125
    imageSource.data.setAttribute('data-original-src', 'image-element');
5✔
126

127
    if (options?.filtering) {
5!
UNCOV
128
      imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, options?.filtering);
×
129
    } else {
130
      imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, ImageFiltering.Blended);
5✔
131
    }
132

133
    if (options?.wrapping) {
5!
134
      let wrapping: ImageWrapConfiguration;
135
      if (typeof options.wrapping === 'string') {
×
UNCOV
136
        wrapping = {
×
137
          x: options.wrapping,
138
          y: options.wrapping
139
        };
140
      } else {
UNCOV
141
        wrapping = {
×
142
          x: options.wrapping.x,
143
          y: options.wrapping.y
144
        };
145
      }
146
      imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, wrapping.x);
×
UNCOV
147
      imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, wrapping.y);
×
148
    } else {
149
      imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, ImageWrapping.Clamp);
5✔
150
      imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, ImageWrapping.Clamp);
5✔
151
    }
152

153
    TextureLoader.checkImageSizeSupportedAndLog(image);
5✔
154
    imageSource._readyFuture.resolve(image);
5✔
155
    return imageSource;
5✔
156
  }
157

158
  static fromHtmlCanvasElement(image: HTMLCanvasElement, options?: ImageSourceOptions): ImageSource {
159
    const imageSource = new ImageSource('');
63✔
160
    imageSource._src = 'canvas-element-blob';
63✔
161
    imageSource.data.setAttribute('data-original-src', 'canvas-element-blob');
63✔
162

163
    if (options?.filtering) {
63✔
164
      imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, options?.filtering);
2!
165
    } else {
166
      imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, ImageFiltering.Blended);
61✔
167
    }
168

169
    if (options?.wrapping) {
63✔
170
      let wrapping: ImageWrapConfiguration;
171
      if (typeof options.wrapping === 'string') {
62✔
172
        wrapping = {
58✔
173
          x: options.wrapping,
174
          y: options.wrapping
175
        };
176
      } else {
177
        wrapping = {
4✔
178
          x: options.wrapping.x,
179
          y: options.wrapping.y
180
        };
181
      }
182
      imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, wrapping.x);
62✔
183
      imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, wrapping.y);
62✔
184
    } else {
185
      imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, ImageWrapping.Clamp);
1✔
186
      imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, ImageWrapping.Clamp);
1✔
187
    }
188

189
    TextureLoader.checkImageSizeSupportedAndLog(image);
63✔
190

191
    image.toBlob((blob) => {
63✔
192
      // TODO throw? if blob null?
193
      const url = URL.createObjectURL(blob!);
63✔
194
      imageSource.image.onload = () => {
63✔
195
        // no longer need to read the blob so it's revoked
196
        URL.revokeObjectURL(url);
63✔
197
        imageSource.data = imageSource.image;
63✔
198
        imageSource._readyFuture.resolve(imageSource.image);
63✔
199
      };
200
      imageSource.image.src = url;
63✔
201
    });
202

203
    return imageSource;
63✔
204
  }
205

206
  static fromSvgString(svgSource: string, options?: ImageSourceOptions) {
207
    const blob = new Blob([svgSource], { type: 'image/svg+xml' });
1✔
208
    const url = URL.createObjectURL(blob);
1✔
209
    return new ImageSource(url, options);
1✔
210
  }
211

212
  /**
213
   * Should excalibur add a cache busting querystring? By default false.
214
   * Must be set before loading
215
   */
216
  public get bustCache() {
UNCOV
217
    return this._resource.bustCache;
×
218
  }
219

220
  public set bustCache(val: boolean) {
UNCOV
221
    this._resource.bustCache = val;
×
222
  }
223

224
  /**
225
   * Begins loading the image and returns a promise that resolves when the image is loaded
226
   */
227
  async load(): Promise<HTMLImageElement> {
228
    if (this.isLoaded()) {
1,981✔
229
      return this.data;
4✔
230
    }
231
    try {
1,977✔
232
      // Load base64 or blob if needed
233
      let url: string;
234
      if (!this.path.includes('data:image/')) {
1,977✔
235
        const blob = await this._resource.load();
1,080✔
236
        url = URL.createObjectURL(blob);
1,079✔
237
      } else {
238
        url = this.path;
897✔
239
      }
240

241
      // Decode the image
242
      const image = new Image();
1,976✔
243
      // Use Image.onload over Image.decode()
244
      // https://bugs.chromium.org/p/chromium/issues/detail?id=1055828#c7
245
      // Otherwise chrome will throw still Image.decode() failures for large textures
246
      const loadedFuture = new Future<void>();
1,976✔
247
      image.onload = () => loadedFuture.resolve();
1,976✔
248
      image.src = url;
1,976✔
249
      image.setAttribute('data-original-src', this.path);
1,976✔
250

251
      await loadedFuture.promise;
1,976✔
252

253
      // Set results
254
      // We defer loading the texture into webgl until the first draw that way we avoid a singleton
255
      // and for the multi-engine case the texture needs to be created in EACH webgl context to work
256
      // See image-renderer.ts draw()
257
      this.data = image;
1,949✔
258

259
      // emit warning if potentially too big
260
      TextureLoader.checkImageSizeSupportedAndLog(this.data);
1,949✔
261
    } catch (error: any) {
262
      throw `Error loading ImageSource from path '${this.path}' with error [${error.message}]`;
1✔
263
    }
264
    // Do a bad thing to pass the filtering as an attribute
265
    this.data.setAttribute(ImageSourceAttributeConstants.Filtering, this.filtering as any); // TODO fix type
1,949✔
266
    this.data.setAttribute(ImageSourceAttributeConstants.WrappingX, this.wrapping?.x ?? ImageWrapping.Clamp);
1,949✔
267
    this.data.setAttribute(ImageSourceAttributeConstants.WrappingY, this.wrapping?.y ?? ImageWrapping.Clamp);
1,949✔
268

269
    // todo emit complete
270
    this._readyFuture.resolve(this.data);
1,949✔
271
    return this.data;
1,949✔
272
  }
273

274
  /**
275
   * Build a sprite from this ImageSource
276
   */
277
  public toSprite(options?: Omit<GraphicOptions & SpriteOptions, 'image'>): Sprite {
278
    return Sprite.from(this, options);
131✔
279
  }
280

281
  /**
282
   * Unload images from memory
283
   */
284
  unload(): void {
285
    this.data = new Image();
1✔
286
  }
287
}
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