• 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

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

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

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

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

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

34
  /**
35
   * The original size of the source image in pixels
36
   */
37
  public get width() {
UNCOV
38
    return this.image.naturalWidth;
×
39
  }
40

41
  /**
42
   * The original height of the source image in pixels
43
   */
44
  public get height() {
UNCOV
45
    return this.image.naturalHeight;
×
46
  }
47

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

61
  /**
62
   * Access to the underlying html image element
63
   */
64
  public data: HTMLImageElement = new Image();
1✔
65
  public get image(): HTMLImageElement {
UNCOV
66
    return this.data;
×
67
  }
68

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

75
  public readonly path: string;
76

77
  /**
78
   * The path to the image, can also be a data url like 'data:image/'
79
   * @param pathOrBase64 {string} Path to the image resource relative from the HTML document hosting the game, or absolute
80
   * @param options
81
   */
82
  constructor(pathOrBase64: string, options?: ImageSourceOptions);
83
  /**
84
   * The path to the image, can also be a data url like 'data:image/'
85
   * @param pathOrBase64 {string} Path to the image resource relative from the HTML document hosting the game, or absolute
86
   * @param bustCache {boolean} Should excalibur add a cache busting querystring?
87
   * @param filtering {ImageFiltering} Optionally override the image filtering set by {@apilink EngineOptions.antialiasing}
88
   */
89
  constructor(pathOrBase64: string, bustCache: boolean, filtering?: ImageFiltering);
90
  constructor(pathOrBase64: string, bustCacheOrOptions: boolean | ImageSourceOptions | undefined, filtering?: ImageFiltering) {
91
    this.path = pathOrBase64;
1✔
92
    let bustCache: boolean | undefined = false;
1✔
93
    let wrapping: ImageWrapConfiguration | ImageWrapping | undefined;
94
    if (typeof bustCacheOrOptions === 'boolean') {
1!
UNCOV
95
      bustCache = bustCacheOrOptions;
×
96
    } else {
97
      ({ filtering, wrapping, bustCache } = { ...bustCacheOrOptions });
1✔
98
    }
99
    this._resource = new Resource(pathOrBase64, 'blob', bustCache);
1✔
100
    this.filtering = filtering ?? this.filtering;
1!
101
    if (typeof wrapping === 'string') {
1!
UNCOV
102
      this.wrapping = {
×
103
        x: wrapping,
104
        y: wrapping
105
      };
106
    } else {
107
      this.wrapping = wrapping ?? this.wrapping;
1!
108
    }
109
    if (pathOrBase64.endsWith('.gif')) {
1!
UNCOV
110
      this._logger.warn(
×
111
        `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`
112
      );
113
    }
114
  }
115

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

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

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

UNCOV
152
    TextureLoader.checkImageSizeSupportedAndLog(image);
×
UNCOV
153
    imageSource._readyFuture.resolve(image);
×
UNCOV
154
    return imageSource;
×
155
  }
156

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

UNCOV
162
    if (options?.filtering) {
×
UNCOV
163
      imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, options?.filtering);
×
164
    } else {
UNCOV
165
      imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, ImageFiltering.Blended);
×
166
    }
167

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

UNCOV
188
    TextureLoader.checkImageSizeSupportedAndLog(image);
×
189

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

UNCOV
202
    return imageSource;
×
203
  }
204

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

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

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

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

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

UNCOV
250
      await loadedFuture.promise;
×
251

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

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

268
    // todo emit complete
UNCOV
269
    this._readyFuture.resolve(this.data);
×
UNCOV
270
    return this.data;
×
271
  }
272

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

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