diff --git a/CHANGELOG.md b/CHANGELOG.md index 2227735e1..20b14aa60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,16 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added ability to configure image wrapping on `ex.ImageSource` with the new `ex.ImageWrapping.Clamp` (default), `ex.ImageWrapping.Repeat`, and `ex.ImageWrapping.Mirror`. + ```typescript + const image = new ex.ImageSource('path/to/image.png', { + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Repeat, + y: ex.ImageWrapping.Repeat, + } + }); + ``` - Added pointer event support to `ex.TileMap`'s and individual `ex.Tile`'s - Added pointer event support to `ex.IsometricMap`'s and individual `ex.IsometricTile`'s - Added `useAnchor` parameter to `ex.GraphicsGroup` to allow users to opt out of anchor based positioning, if set to false all graphics members diff --git a/sandbox/tests/imagewrapping/index.html b/sandbox/tests/imagewrapping/index.html new file mode 100644 index 000000000..45b4bf43e --- /dev/null +++ b/sandbox/tests/imagewrapping/index.html @@ -0,0 +1,13 @@ + + + + + + Image Wrapping + + + + + + + \ No newline at end of file diff --git a/sandbox/tests/imagewrapping/index.ts b/sandbox/tests/imagewrapping/index.ts new file mode 100644 index 000000000..d7cbcfe4c --- /dev/null +++ b/sandbox/tests/imagewrapping/index.ts @@ -0,0 +1,59 @@ +/// + +// identity tagged template literal lights up glsl-literal vscode plugin +var glsl = x => x[0]; +var game = new ex.Engine({ + canvasElementId: 'game', + width: 800, + height: 800 +}); + +var fireShader = glsl`#version 300 es + precision mediump float; + uniform float animation_speed; + uniform float offset; + uniform float u_time_ms; + uniform sampler2D u_graphic; + uniform sampler2D noise; + in vec2 v_uv; + out vec4 fragColor; + + void main() { + vec2 animatedUV = vec2(v_uv.x, v_uv.y + (u_time_ms / 1000.) * 0.5); + vec4 color = texture(noise, animatedUV); + color.rgb += (v_uv.y - 0.5); + color.rgb = step(color.rgb, vec3(0.5)); + color.rgb = vec3(1.0) - color.rgb; + + fragColor.rgb = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), v_uv.y); + fragColor.a = color.r; + fragColor.rgb = fragColor.rgb * fragColor.a; + } +` + +var noiseImage = new ex.ImageSource('./noise.png', { + filtering: ex.ImageFiltering.Blended, + wrapping: ex.ImageWrapping.Repeat +}); + +var material = game.graphicsContext.createMaterial({ + name: 'fire', + fragmentSource: fireShader, + images: { + 'noise': noiseImage + } +}) + +var actor = new ex.Actor({ + pos: ex.vec(0, 200), + anchor: ex.vec(0, 0), + width: 800, + height: 600, + color: ex.Color.Red +}); +actor.graphics.material = material; +game.add(actor); + +var loader = new ex.Loader([noiseImage]); + +game.start(loader); \ No newline at end of file diff --git a/sandbox/tests/imagewrapping/noise.png b/sandbox/tests/imagewrapping/noise.png new file mode 100644 index 000000000..98ab46077 Binary files /dev/null and b/sandbox/tests/imagewrapping/noise.png differ diff --git a/src/engine/Graphics/Context/image-renderer/image-renderer.ts b/src/engine/Graphics/Context/image-renderer/image-renderer.ts index ca4b9f078..68d07745c 100644 --- a/src/engine/Graphics/Context/image-renderer/image-renderer.ts +++ b/src/engine/Graphics/Context/image-renderer/image-renderer.ts @@ -1,6 +1,8 @@ import { sign } from '../../../Math/util'; -import { ImageFiltering } from '../../Filtering'; +import { parseImageFiltering } from '../../Filtering'; import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; +import { ImageSourceAttributeConstants } from '../../ImageSource'; +import { parseImageWrapping } from '../../Wrapping'; import { HTMLImageSource } from '../ExcaliburGraphicsContext'; import { ExcaliburGraphicsContextWebGL, pixelSnapEpsilon } from '../ExcaliburGraphicsContextWebGL'; import { QuadIndexBuffer } from '../quad-index-buffer'; @@ -124,15 +126,19 @@ export class ImageRenderer implements RendererPlugin { if (this._images.has(image)) { return; } - const maybeFiltering = image.getAttribute('filtering'); - let filtering: ImageFiltering = null; - if (maybeFiltering === ImageFiltering.Blended || - maybeFiltering === ImageFiltering.Pixel) { - filtering = maybeFiltering; - } + const maybeFiltering = image.getAttribute(ImageSourceAttributeConstants.Filtering); + const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : null; + const wrapX = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingX)); + const wrapY = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingY)); const force = image.getAttribute('forceUpload') === 'true' ? true : false; - const texture = this._context.textureLoader.load(image, filtering, force); + const texture = this._context.textureLoader.load( + image, + { + filtering, + wrapping: { x: wrapX, y: wrapY } + }, + force); // remove force attribute after upload image.removeAttribute('forceUpload'); if (this._textures.indexOf(texture) === -1) { diff --git a/src/engine/Graphics/Context/material-renderer/material-renderer.ts b/src/engine/Graphics/Context/material-renderer/material-renderer.ts index c23af65c8..627e1b348 100644 --- a/src/engine/Graphics/Context/material-renderer/material-renderer.ts +++ b/src/engine/Graphics/Context/material-renderer/material-renderer.ts @@ -1,6 +1,8 @@ import { vec } from '../../../Math/vector'; -import { ImageFiltering } from '../../Filtering'; +import { parseImageFiltering } from '../../Filtering'; import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; +import { ImageSourceAttributeConstants } from '../../ImageSource'; +import { parseImageWrapping } from '../../Wrapping'; import { HTMLImageSource } from '../ExcaliburGraphicsContext'; import { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL'; import { QuadIndexBuffer } from '../quad-index-buffer'; @@ -204,15 +206,19 @@ export class MaterialRenderer implements RendererPlugin { } private _addImageAsTexture(image: HTMLImageSource) { - const maybeFiltering = image.getAttribute('filtering'); - let filtering: ImageFiltering = null; - if (maybeFiltering === ImageFiltering.Blended || - maybeFiltering === ImageFiltering.Pixel) { - filtering = maybeFiltering; - } + const maybeFiltering = image.getAttribute(ImageSourceAttributeConstants.Filtering); + const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : null; + const wrapX = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingX)); + const wrapY = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingY)); const force = image.getAttribute('forceUpload') === 'true' ? true : false; - const texture = this._context.textureLoader.load(image, filtering, force); + const texture = this._context.textureLoader.load( + image, + { + filtering, + wrapping: { x: wrapX, y: wrapY } + }, + force); // remove force attribute after upload image.removeAttribute('forceUpload'); if (this._textures.indexOf(texture) === -1) { @@ -228,5 +234,4 @@ export class MaterialRenderer implements RendererPlugin { flush(): void { // flush does not do anything, material renderer renders immediately per draw } - } \ No newline at end of file diff --git a/src/engine/Graphics/Context/material.ts b/src/engine/Graphics/Context/material.ts index 8d212551f..2d1bf836a 100644 --- a/src/engine/Graphics/Context/material.ts +++ b/src/engine/Graphics/Context/material.ts @@ -3,8 +3,9 @@ import { ExcaliburGraphicsContext } from './ExcaliburGraphicsContext'; import { ExcaliburGraphicsContextWebGL } from './ExcaliburGraphicsContextWebGL'; import { Shader } from './shader'; import { Logger } from '../../Util/Log'; -import { ImageSource } from '../ImageSource'; -import { ImageFiltering } from '../Filtering'; +import { ImageSource, ImageSourceAttributeConstants } from '../ImageSource'; +import { ImageFiltering, parseImageFiltering } from '../Filtering'; +import { parseImageWrapping } from '../Wrapping'; export interface MaterialOptions { /** @@ -191,15 +192,19 @@ export class Material { private _loadImageSource(image: ImageSource) { const imageElement = image.image; - const maybeFiltering = imageElement.getAttribute('filtering'); - let filtering: ImageFiltering = null; - if (maybeFiltering === ImageFiltering.Blended || - maybeFiltering === ImageFiltering.Pixel) { - filtering = maybeFiltering; - } + const maybeFiltering = imageElement.getAttribute(ImageSourceAttributeConstants.Filtering); + const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : null; + const wrapX = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingX)); + const wrapY = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingY)); const force = imageElement.getAttribute('forceUpload') === 'true' ? true : false; - const texture = this._graphicsContext.textureLoader.load(imageElement, filtering, force); + const texture = this._graphicsContext.textureLoader.load( + imageElement, + { + filtering, + wrapping: { x: wrapX, y: wrapY } + }, + force); // remove force attribute after upload imageElement.removeAttribute('forceUpload'); if (!this._textures.has(image)) { diff --git a/src/engine/Graphics/Context/texture-loader.ts b/src/engine/Graphics/Context/texture-loader.ts index e7e9a331a..c437255a1 100644 --- a/src/engine/Graphics/Context/texture-loader.ts +++ b/src/engine/Graphics/Context/texture-loader.ts @@ -1,5 +1,7 @@ import { Logger } from '../../Util/Log'; import { ImageFiltering } from '../Filtering'; +import { ImageSourceOptions, ImageWrapConfiguration } from '../ImageSource'; +import { ImageWrapping } from '../Wrapping'; import { HTMLImageSource } from './ExcaliburGraphicsContext'; /** @@ -25,6 +27,7 @@ export class TextureLoader { * Sets the default filtering for the Excalibur texture loader, default [[ImageFiltering.Blended]] */ public static filtering: ImageFiltering = ImageFiltering.Blended; + public static wrapping: ImageWrapConfiguration = {x: ImageWrapping.Clamp, y: ImageWrapping.Clamp}; private _gl: WebGL2RenderingContext; @@ -51,16 +54,18 @@ export class TextureLoader { /** * Loads a graphic into webgl and returns it's texture info, a webgl context must be previously registered * @param image Source graphic - * @param filtering {ImageFiltering} The ImageFiltering mode to apply to the loaded texture + * @param options {ImageSourceOptions} Optionally configure the ImageFiltering and ImageWrapping mode to apply to the loaded texture * @param forceUpdate Optionally force a texture to be reloaded, useful if the source graphic has changed */ - public load(image: HTMLImageSource, filtering?: ImageFiltering, forceUpdate = false): WebGLTexture { + public load(image: HTMLImageSource, options?: ImageSourceOptions, forceUpdate = false): WebGLTexture { // Ignore loading if webgl is not registered const gl = this._gl; if (!gl) { return null; } + const { filtering, wrapping } = {...options}; + let tex: WebGLTexture = null; // If reuse the texture if it's from the same source if (this.has(image)) { @@ -85,9 +90,48 @@ export class TextureLoader { gl.bindTexture(gl.TEXTURE_2D, tex); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - // TODO make configurable - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + let wrappingConfig: ImageWrapConfiguration; + if (wrapping) { + if (typeof wrapping === 'string') { + wrappingConfig = { + x: wrapping, + y: wrapping + }; + } else { + wrappingConfig = { + x: wrapping.x, + y: wrapping.y + }; + } + } + const { x: xWrap, y: yWrap} = (wrappingConfig ?? TextureLoader.wrapping); + switch (xWrap) { + case ImageWrapping.Clamp: + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + break; + case ImageWrapping.Repeat: + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + break; + case ImageWrapping.Mirror: + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); + break; + default: + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + } + switch (yWrap) { + case ImageWrapping.Clamp: + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + break; + case ImageWrapping.Repeat: + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); + break; + case ImageWrapping.Mirror: + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); + break; + default: + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + } // NEAREST for pixel art, LINEAR for hi-res const filterMode = filtering ?? TextureLoader.filtering; diff --git a/src/engine/Graphics/Filtering.ts b/src/engine/Graphics/Filtering.ts index 67333df15..0801692e2 100644 --- a/src/engine/Graphics/Filtering.ts +++ b/src/engine/Graphics/Filtering.ts @@ -15,4 +15,15 @@ export enum ImageFiltering { * Blended is useful when you have high resolution artwork and would like it blended and smoothed */ Blended = 'Blended' +} + +/** + * Parse the image filtering attribute value, if it doesn't match returns null + */ +export function parseImageFiltering(val: string): ImageFiltering | null { + switch (val) { + case ImageFiltering.Pixel: return ImageFiltering.Pixel; + case ImageFiltering.Blended: return ImageFiltering.Blended; + default: return null; + } } \ No newline at end of file diff --git a/src/engine/Graphics/FontTextInstance.ts b/src/engine/Graphics/FontTextInstance.ts index 5593f732e..62c6e8eb7 100644 --- a/src/engine/Graphics/FontTextInstance.ts +++ b/src/engine/Graphics/FontTextInstance.ts @@ -40,7 +40,7 @@ export class FontTextInstance { const metrics = this.ctx.measureText(maxWidthLine); let textHeight = Math.abs(metrics.actualBoundingBoxAscent) + Math.abs(metrics.actualBoundingBoxDescent); - // TODO lineheight makes the text bounds wonky + // TODO line height makes the text bounds wonky const lineAdjustedHeight = textHeight * lines.length; textHeight = lineAdjustedHeight; const bottomBounds = lineAdjustedHeight - Math.abs(metrics.actualBoundingBoxAscent); @@ -209,7 +209,7 @@ export class FontTextInstance { if (ex instanceof ExcaliburGraphicsContextWebGL) { for (const frag of this._textFragments) { - ex.textureLoader.load(frag.canvas, this.font.filtering, true); + ex.textureLoader.load(frag.canvas, { filtering: this.font.filtering }, true); } } this._lastHashCode = hashCode; diff --git a/src/engine/Graphics/ImageSource.ts b/src/engine/Graphics/ImageSource.ts index aa0ebde24..083c51086 100644 --- a/src/engine/Graphics/ImageSource.ts +++ b/src/engine/Graphics/ImageSource.ts @@ -5,16 +5,30 @@ import { Logger } from '../Util/Log'; import { ImageFiltering } from './Filtering'; import { Future } from '../Util/Future'; import { TextureLoader } from '../Graphics/Context/texture-loader'; +import { ImageWrapping } from './Wrapping'; export interface ImageSourceOptions { filtering?: ImageFiltering; + wrapping?: ImageWrapConfiguration | ImageWrapping; bustCache?: boolean; } +export interface ImageWrapConfiguration { + x: ImageWrapping; + y: ImageWrapping; +} + +export const ImageSourceAttributeConstants = { + Filtering: 'filtering', + WrappingX: 'wrapping-x', + WrappingY: 'wrapping-y' +} as const; + export class ImageSource implements Loadable { private _logger = Logger.getInstance(); private _resource: Resource; public filtering: ImageFiltering; + public wrapping: ImageWrapConfiguration; /** * The original size of the source image in pixels @@ -57,15 +71,40 @@ export class ImageSource implements Loadable { */ public ready: Promise = this._readyFuture.promise; + public readonly path: string; + + /** + * The path to the image, can also be a data url like 'data:image/' + * @param path {string} Path to the image resource relative from the HTML document hosting the game, or absolute + * @param options + */ + constructor(path: string, options?: ImageSourceOptions); /** * The path to the image, can also be a data url like 'data:image/' * @param path {string} Path to the image resource relative from the HTML document hosting the game, or absolute * @param bustCache {boolean} Should excalibur add a cache busting querystring? * @param filtering {ImageFiltering} Optionally override the image filtering set by [[EngineOptions.antialiasing]] */ - constructor(public readonly path: string, bustCache: boolean = false, filtering?: ImageFiltering) { + constructor(path: string, bustCache: boolean, filtering?: ImageFiltering); + constructor(path: string, bustCacheOrOptions: boolean | ImageSourceOptions, filtering?: ImageFiltering) { + this.path = path; + let bustCache = false; + let wrapping: ImageWrapConfiguration | ImageWrapping; + if (typeof bustCacheOrOptions === 'boolean') { + bustCache = bustCacheOrOptions; + } else { + ({ filtering, wrapping, bustCache } = {...bustCacheOrOptions}); + } this._resource = new Resource(path, 'blob', bustCache); - this.filtering = filtering; + this.filtering = filtering ?? this.filtering; + if (typeof wrapping === 'string') { + this.wrapping = { + x: wrapping, + y: wrapping + }; + } else { + this.wrapping = wrapping ?? this.wrapping; + } if (path.endsWith('.svg') || path.endsWith('.gif')) { this._logger.warn(`Image type is not fully supported, you may have mixed results ${path}. Fully supported: jpg, bmp, and png`); } @@ -82,9 +121,29 @@ export class ImageSource implements Loadable { imageSource.data.setAttribute('data-original-src', 'image-element'); if (options?.filtering) { - imageSource.data.setAttribute('filtering', options?.filtering); + imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, options?.filtering); + } else { + imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, ImageFiltering.Blended); + } + + if (options?.wrapping) { + let wrapping: ImageWrapConfiguration; + if (typeof options.wrapping === 'string') { + wrapping = { + x: options.wrapping, + y: options.wrapping + }; + } else { + wrapping = { + x: options.wrapping.x, + y: options.wrapping.y + }; + } + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, wrapping.x); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, wrapping.y); } else { - imageSource.data.setAttribute('filtering', ImageFiltering.Blended); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, ImageWrapping.Clamp); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, ImageWrapping.Clamp); } TextureLoader.checkImageSizeSupportedAndLog(image); @@ -145,7 +204,9 @@ export class ImageSource implements Loadable { throw `Error loading ImageSource from path '${this.path}' with error [${error.message}]`; } // Do a bad thing to pass the filtering as an attribute - this.data.setAttribute('filtering', this.filtering); + this.data.setAttribute(ImageSourceAttributeConstants.Filtering, this.filtering); + this.data.setAttribute(ImageSourceAttributeConstants.WrappingX, this.wrapping?.x ?? ImageWrapping.Clamp); + this.data.setAttribute(ImageSourceAttributeConstants.WrappingY, this.wrapping?.y ?? ImageWrapping.Clamp); // todo emit complete this._readyFuture.resolve(this.data); diff --git a/src/engine/Graphics/Wrapping.ts b/src/engine/Graphics/Wrapping.ts new file mode 100644 index 000000000..324c21a31 --- /dev/null +++ b/src/engine/Graphics/Wrapping.ts @@ -0,0 +1,24 @@ + +/** + * Describes the different image wrapping modes + */ +export enum ImageWrapping { + + Clamp = 'Clamp', + + Repeat = 'Repeat', + + Mirror = 'Mirror' +} + +/** + * + */ +export function parseImageWrapping(val: string): ImageWrapping { + switch (val) { + case ImageWrapping.Clamp: return ImageWrapping.Clamp; + case ImageWrapping.Repeat: return ImageWrapping.Repeat; + case ImageWrapping.Mirror: return ImageWrapping.Mirror; + default: return ImageWrapping.Clamp; + } +} \ No newline at end of file diff --git a/src/engine/Graphics/index.ts b/src/engine/Graphics/index.ts index 2a23e1fa0..b3707a31c 100644 --- a/src/engine/Graphics/index.ts +++ b/src/engine/Graphics/index.ts @@ -40,6 +40,7 @@ export * from './PostProcessor/ColorBlindnessPostProcessor'; export * from './Context/texture-loader'; export * from './Filtering'; +export * from './Wrapping'; // Rendering diff --git a/src/spec/ImageSourceSpec.ts b/src/spec/ImageSourceSpec.ts index c0caea5da..000edaace 100644 --- a/src/spec/ImageSourceSpec.ts +++ b/src/spec/ImageSourceSpec.ts @@ -99,7 +99,15 @@ describe('A ImageSource', () => { expect(image.src).not.toBeNull(); expect(whenLoaded).toHaveBeenCalledTimes(1); - expect(webgl.textureLoader.load).toHaveBeenCalledWith(image, ex.ImageFiltering.Blended, false); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Blended, + wrapping: { + x: ex.ImageWrapping.Clamp, + y: ex.ImageWrapping.Clamp + } + }, false); }); it('can load images with an image filtering Pixel', async () => { @@ -120,7 +128,118 @@ describe('A ImageSource', () => { expect(image.src).not.toBeNull(); expect(whenLoaded).toHaveBeenCalledTimes(1); - expect(webgl.textureLoader.load).toHaveBeenCalledWith(image, ex.ImageFiltering.Pixel, false); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Clamp, + y: ex.ImageWrapping.Clamp + } + }, false); + }); + + it('can load images with an image wrap repeat', async () => { + const canvas = document.createElement('canvas'); + const webgl = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas + }); + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); + imageRenderer.initialize(webgl.__gl, webgl); + spyOn(webgl.textureLoader, 'load').and.callThrough(); + + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png',{ + filtering: ex.ImageFiltering.Pixel, + wrapping: ex.ImageWrapping.Repeat + }); + const whenLoaded = jasmine.createSpy('whenLoaded'); + const image = await spriteFontImage.load(); + await spriteFontImage.ready.then(whenLoaded); + + imageRenderer.draw(image, 0, 0); + + expect(image.src).not.toBeNull(); + expect(whenLoaded).toHaveBeenCalledTimes(1); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Repeat, + y: ex.ImageWrapping.Repeat + } + }, false); + }); + + it('can load images with an image wrap repeat', async () => { + const canvas = document.createElement('canvas'); + const webgl = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas + }); + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); + imageRenderer.initialize(webgl.__gl, webgl); + spyOn(webgl.textureLoader, 'load').and.callThrough(); + + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png',{ + filtering: ex.ImageFiltering.Pixel, + wrapping: ex.ImageWrapping.Mirror + }); + const whenLoaded = jasmine.createSpy('whenLoaded'); + const image = await spriteFontImage.load(); + await spriteFontImage.ready.then(whenLoaded); + + imageRenderer.draw(image, 0, 0); + + expect(image.src).not.toBeNull(); + expect(whenLoaded).toHaveBeenCalledTimes(1); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Mirror, + y: ex.ImageWrapping.Mirror + } + }, false); + }); + + it('can load images with an image wrap mixed', async () => { + const canvas = document.createElement('canvas'); + const webgl = new ex.ExcaliburGraphicsContextWebGL({ + canvasElement: canvas + }); + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); + imageRenderer.initialize(webgl.__gl, webgl); + spyOn(webgl.textureLoader, 'load').and.callThrough(); + const texParameteri = spyOn(webgl.__gl, 'texParameteri').and.callThrough(); + const gl = webgl.__gl; + + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png',{ + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Mirror, + y: ex.ImageWrapping.Clamp + } + }); + const whenLoaded = jasmine.createSpy('whenLoaded'); + const image = await spriteFontImage.load(); + await spriteFontImage.ready.then(whenLoaded); + + imageRenderer.draw(image, 0, 0); + + expect(image.src).not.toBeNull(); + expect(whenLoaded).toHaveBeenCalledTimes(1); + expect(texParameteri.calls.argsFor(0)).toEqual([gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT]); + expect(texParameteri.calls.argsFor(1)).toEqual([gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE]); + expect(webgl.textureLoader.load).toHaveBeenCalledWith( + image, + { + filtering: ex.ImageFiltering.Pixel, + wrapping: { + x: ex.ImageWrapping.Mirror, + y: ex.ImageWrapping.Clamp + } + }, false); }); it('can convert to a Sprite', async () => {