[READ-ONLY] Mirror of https://github.com/excaliburjs/Excalibur. 🎮 Your friendly TypeScript 2D game engine for the web 🗡️ excaliburjs.com
excalibur excaliburjs game-development game-engine game-framework gamedev games html5-canvas typescript
2

Configure Feed

Select the types of activity you want to include in your feed.

feat: Implement image wrapping configuration (#2963)

This PR allows users to configure the ImageSource wrapping mode: Clamp, Repeat, or Mirror.

Example of using the Repeat mode to repeat noise textures over time

https://github.com/excaliburjs/Excalibur/assets/612071/ca199c17-569e-4f38-9810-72227e2ebb52

authored by

Erik Onarheim and committed by
GitHub
(Apr 6, 2024, 10:18 PM -0500) 470439f0 88594e26

+398 -40
+10
CHANGELOG.md
··· 16 16 17 17 ### Added 18 18 19 + - Added ability to configure image wrapping on `ex.ImageSource` with the new `ex.ImageWrapping.Clamp` (default), `ex.ImageWrapping.Repeat`, and `ex.ImageWrapping.Mirror`. 20 + ```typescript 21 + const image = new ex.ImageSource('path/to/image.png', { 22 + filtering: ex.ImageFiltering.Pixel, 23 + wrapping: { 24 + x: ex.ImageWrapping.Repeat, 25 + y: ex.ImageWrapping.Repeat, 26 + } 27 + }); 28 + ``` 19 29 - Added pointer event support to `ex.TileMap`'s and individual `ex.Tile`'s 20 30 - Added pointer event support to `ex.IsometricMap`'s and individual `ex.IsometricTile`'s 21 31 - Added `useAnchor` parameter to `ex.GraphicsGroup` to allow users to opt out of anchor based positioning, if set to false all graphics members
+13
sandbox/tests/imagewrapping/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Image Wrapping</title> 7 + </head> 8 + <body> 9 + <canvas id="game"></canvas> 10 + <script src="../../lib/excalibur.js"></script> 11 + <script src="index.js"></script> 12 + </body> 13 + </html>
+59
sandbox/tests/imagewrapping/index.ts
··· 1 + /// <reference path="../../lib/excalibur.d.ts" /> 2 + 3 + // identity tagged template literal lights up glsl-literal vscode plugin 4 + var glsl = x => x[0]; 5 + var game = new ex.Engine({ 6 + canvasElementId: 'game', 7 + width: 800, 8 + height: 800 9 + }); 10 + 11 + var fireShader = glsl`#version 300 es 12 + precision mediump float; 13 + uniform float animation_speed; 14 + uniform float offset; 15 + uniform float u_time_ms; 16 + uniform sampler2D u_graphic; 17 + uniform sampler2D noise; 18 + in vec2 v_uv; 19 + out vec4 fragColor; 20 + 21 + void main() { 22 + vec2 animatedUV = vec2(v_uv.x, v_uv.y + (u_time_ms / 1000.) * 0.5); 23 + vec4 color = texture(noise, animatedUV); 24 + color.rgb += (v_uv.y - 0.5); 25 + color.rgb = step(color.rgb, vec3(0.5)); 26 + color.rgb = vec3(1.0) - color.rgb; 27 + 28 + fragColor.rgb = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), v_uv.y); 29 + fragColor.a = color.r; 30 + fragColor.rgb = fragColor.rgb * fragColor.a; 31 + } 32 + ` 33 + 34 + var noiseImage = new ex.ImageSource('./noise.png', { 35 + filtering: ex.ImageFiltering.Blended, 36 + wrapping: ex.ImageWrapping.Repeat 37 + }); 38 + 39 + var material = game.graphicsContext.createMaterial({ 40 + name: 'fire', 41 + fragmentSource: fireShader, 42 + images: { 43 + 'noise': noiseImage 44 + } 45 + }) 46 + 47 + var actor = new ex.Actor({ 48 + pos: ex.vec(0, 200), 49 + anchor: ex.vec(0, 0), 50 + width: 800, 51 + height: 600, 52 + color: ex.Color.Red 53 + }); 54 + actor.graphics.material = material; 55 + game.add(actor); 56 + 57 + var loader = new ex.Loader([noiseImage]); 58 + 59 + game.start(loader);
sandbox/tests/imagewrapping/noise.png

This is a binary file and will not be displayed.

+14 -8
src/engine/Graphics/Context/image-renderer/image-renderer.ts
··· 1 1 import { sign } from '../../../Math/util'; 2 - import { ImageFiltering } from '../../Filtering'; 2 + import { parseImageFiltering } from '../../Filtering'; 3 3 import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; 4 + import { ImageSourceAttributeConstants } from '../../ImageSource'; 5 + import { parseImageWrapping } from '../../Wrapping'; 4 6 import { HTMLImageSource } from '../ExcaliburGraphicsContext'; 5 7 import { ExcaliburGraphicsContextWebGL, pixelSnapEpsilon } from '../ExcaliburGraphicsContextWebGL'; 6 8 import { QuadIndexBuffer } from '../quad-index-buffer'; ··· 124 126 if (this._images.has(image)) { 125 127 return; 126 128 } 127 - const maybeFiltering = image.getAttribute('filtering'); 128 - let filtering: ImageFiltering = null; 129 - if (maybeFiltering === ImageFiltering.Blended || 130 - maybeFiltering === ImageFiltering.Pixel) { 131 - filtering = maybeFiltering; 132 - } 129 + const maybeFiltering = image.getAttribute(ImageSourceAttributeConstants.Filtering); 130 + const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : null; 131 + const wrapX = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingX)); 132 + const wrapY = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingY)); 133 133 134 134 const force = image.getAttribute('forceUpload') === 'true' ? true : false; 135 - const texture = this._context.textureLoader.load(image, filtering, force); 135 + const texture = this._context.textureLoader.load( 136 + image, 137 + { 138 + filtering, 139 + wrapping: { x: wrapX, y: wrapY } 140 + }, 141 + force); 136 142 // remove force attribute after upload 137 143 image.removeAttribute('forceUpload'); 138 144 if (this._textures.indexOf(texture) === -1) {
+14 -9
src/engine/Graphics/Context/material-renderer/material-renderer.ts
··· 1 1 import { vec } from '../../../Math/vector'; 2 - import { ImageFiltering } from '../../Filtering'; 2 + import { parseImageFiltering } from '../../Filtering'; 3 3 import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; 4 + import { ImageSourceAttributeConstants } from '../../ImageSource'; 5 + import { parseImageWrapping } from '../../Wrapping'; 4 6 import { HTMLImageSource } from '../ExcaliburGraphicsContext'; 5 7 import { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL'; 6 8 import { QuadIndexBuffer } from '../quad-index-buffer'; ··· 204 206 } 205 207 206 208 private _addImageAsTexture(image: HTMLImageSource) { 207 - const maybeFiltering = image.getAttribute('filtering'); 208 - let filtering: ImageFiltering = null; 209 - if (maybeFiltering === ImageFiltering.Blended || 210 - maybeFiltering === ImageFiltering.Pixel) { 211 - filtering = maybeFiltering; 212 - } 209 + const maybeFiltering = image.getAttribute(ImageSourceAttributeConstants.Filtering); 210 + const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : null; 211 + const wrapX = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingX)); 212 + const wrapY = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingY)); 213 213 214 214 const force = image.getAttribute('forceUpload') === 'true' ? true : false; 215 - const texture = this._context.textureLoader.load(image, filtering, force); 215 + const texture = this._context.textureLoader.load( 216 + image, 217 + { 218 + filtering, 219 + wrapping: { x: wrapX, y: wrapY } 220 + }, 221 + force); 216 222 // remove force attribute after upload 217 223 image.removeAttribute('forceUpload'); 218 224 if (this._textures.indexOf(texture) === -1) { ··· 228 234 flush(): void { 229 235 // flush does not do anything, material renderer renders immediately per draw 230 236 } 231 - 232 237 }
+14 -9
src/engine/Graphics/Context/material.ts
··· 3 3 import { ExcaliburGraphicsContextWebGL } from './ExcaliburGraphicsContextWebGL'; 4 4 import { Shader } from './shader'; 5 5 import { Logger } from '../../Util/Log'; 6 - import { ImageSource } from '../ImageSource'; 7 - import { ImageFiltering } from '../Filtering'; 6 + import { ImageSource, ImageSourceAttributeConstants } from '../ImageSource'; 7 + import { ImageFiltering, parseImageFiltering } from '../Filtering'; 8 + import { parseImageWrapping } from '../Wrapping'; 8 9 9 10 export interface MaterialOptions { 10 11 /** ··· 191 192 192 193 private _loadImageSource(image: ImageSource) { 193 194 const imageElement = image.image; 194 - const maybeFiltering = imageElement.getAttribute('filtering'); 195 - let filtering: ImageFiltering = null; 196 - if (maybeFiltering === ImageFiltering.Blended || 197 - maybeFiltering === ImageFiltering.Pixel) { 198 - filtering = maybeFiltering; 199 - } 195 + const maybeFiltering = imageElement.getAttribute(ImageSourceAttributeConstants.Filtering); 196 + const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : null; 197 + const wrapX = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingX)); 198 + const wrapY = parseImageWrapping(imageElement.getAttribute(ImageSourceAttributeConstants.WrappingY)); 200 199 201 200 const force = imageElement.getAttribute('forceUpload') === 'true' ? true : false; 202 - const texture = this._graphicsContext.textureLoader.load(imageElement, filtering, force); 201 + const texture = this._graphicsContext.textureLoader.load( 202 + imageElement, 203 + { 204 + filtering, 205 + wrapping: { x: wrapX, y: wrapY } 206 + }, 207 + force); 203 208 // remove force attribute after upload 204 209 imageElement.removeAttribute('forceUpload'); 205 210 if (!this._textures.has(image)) {
+49 -5
src/engine/Graphics/Context/texture-loader.ts
··· 1 1 import { Logger } from '../../Util/Log'; 2 2 import { ImageFiltering } from '../Filtering'; 3 + import { ImageSourceOptions, ImageWrapConfiguration } from '../ImageSource'; 4 + import { ImageWrapping } from '../Wrapping'; 3 5 import { HTMLImageSource } from './ExcaliburGraphicsContext'; 4 6 5 7 /** ··· 25 27 * Sets the default filtering for the Excalibur texture loader, default [[ImageFiltering.Blended]] 26 28 */ 27 29 public static filtering: ImageFiltering = ImageFiltering.Blended; 30 + public static wrapping: ImageWrapConfiguration = {x: ImageWrapping.Clamp, y: ImageWrapping.Clamp}; 28 31 29 32 private _gl: WebGL2RenderingContext; 30 33 ··· 51 54 /** 52 55 * Loads a graphic into webgl and returns it's texture info, a webgl context must be previously registered 53 56 * @param image Source graphic 54 - * @param filtering {ImageFiltering} The ImageFiltering mode to apply to the loaded texture 57 + * @param options {ImageSourceOptions} Optionally configure the ImageFiltering and ImageWrapping mode to apply to the loaded texture 55 58 * @param forceUpdate Optionally force a texture to be reloaded, useful if the source graphic has changed 56 59 */ 57 - public load(image: HTMLImageSource, filtering?: ImageFiltering, forceUpdate = false): WebGLTexture { 60 + public load(image: HTMLImageSource, options?: ImageSourceOptions, forceUpdate = false): WebGLTexture { 58 61 // Ignore loading if webgl is not registered 59 62 const gl = this._gl; 60 63 if (!gl) { 61 64 return null; 62 65 } 63 66 67 + const { filtering, wrapping } = {...options}; 68 + 64 69 let tex: WebGLTexture = null; 65 70 // If reuse the texture if it's from the same source 66 71 if (this.has(image)) { ··· 85 90 gl.bindTexture(gl.TEXTURE_2D, tex); 86 91 87 92 gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); 88 - // TODO make configurable 89 - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 90 - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 93 + 94 + let wrappingConfig: ImageWrapConfiguration; 95 + if (wrapping) { 96 + if (typeof wrapping === 'string') { 97 + wrappingConfig = { 98 + x: wrapping, 99 + y: wrapping 100 + }; 101 + } else { 102 + wrappingConfig = { 103 + x: wrapping.x, 104 + y: wrapping.y 105 + }; 106 + } 107 + } 108 + const { x: xWrap, y: yWrap} = (wrappingConfig ?? TextureLoader.wrapping); 109 + switch (xWrap) { 110 + case ImageWrapping.Clamp: 111 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 112 + break; 113 + case ImageWrapping.Repeat: 114 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); 115 + break; 116 + case ImageWrapping.Mirror: 117 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); 118 + break; 119 + default: 120 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 121 + } 122 + switch (yWrap) { 123 + case ImageWrapping.Clamp: 124 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 125 + break; 126 + case ImageWrapping.Repeat: 127 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); 128 + break; 129 + case ImageWrapping.Mirror: 130 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); 131 + break; 132 + default: 133 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 134 + } 91 135 92 136 // NEAREST for pixel art, LINEAR for hi-res 93 137 const filterMode = filtering ?? TextureLoader.filtering;
+11
src/engine/Graphics/Filtering.ts
··· 15 15 * Blended is useful when you have high resolution artwork and would like it blended and smoothed 16 16 */ 17 17 Blended = 'Blended' 18 + } 19 + 20 + /** 21 + * Parse the image filtering attribute value, if it doesn't match returns null 22 + */ 23 + export function parseImageFiltering(val: string): ImageFiltering | null { 24 + switch (val) { 25 + case ImageFiltering.Pixel: return ImageFiltering.Pixel; 26 + case ImageFiltering.Blended: return ImageFiltering.Blended; 27 + default: return null; 28 + } 18 29 }
+2 -2
src/engine/Graphics/FontTextInstance.ts
··· 40 40 const metrics = this.ctx.measureText(maxWidthLine); 41 41 let textHeight = Math.abs(metrics.actualBoundingBoxAscent) + Math.abs(metrics.actualBoundingBoxDescent); 42 42 43 - // TODO lineheight makes the text bounds wonky 43 + // TODO line height makes the text bounds wonky 44 44 const lineAdjustedHeight = textHeight * lines.length; 45 45 textHeight = lineAdjustedHeight; 46 46 const bottomBounds = lineAdjustedHeight - Math.abs(metrics.actualBoundingBoxAscent); ··· 209 209 210 210 if (ex instanceof ExcaliburGraphicsContextWebGL) { 211 211 for (const frag of this._textFragments) { 212 - ex.textureLoader.load(frag.canvas, this.font.filtering, true); 212 + ex.textureLoader.load(frag.canvas, { filtering: this.font.filtering }, true); 213 213 } 214 214 } 215 215 this._lastHashCode = hashCode;
+66 -5
src/engine/Graphics/ImageSource.ts
··· 5 5 import { ImageFiltering } from './Filtering'; 6 6 import { Future } from '../Util/Future'; 7 7 import { TextureLoader } from '../Graphics/Context/texture-loader'; 8 + import { ImageWrapping } from './Wrapping'; 8 9 9 10 export interface ImageSourceOptions { 10 11 filtering?: ImageFiltering; 12 + wrapping?: ImageWrapConfiguration | ImageWrapping; 11 13 bustCache?: boolean; 12 14 } 13 15 16 + export interface ImageWrapConfiguration { 17 + x: ImageWrapping; 18 + y: ImageWrapping; 19 + } 20 + 21 + export const ImageSourceAttributeConstants = { 22 + Filtering: 'filtering', 23 + WrappingX: 'wrapping-x', 24 + WrappingY: 'wrapping-y' 25 + } as const; 26 + 14 27 export class ImageSource implements Loadable<HTMLImageElement> { 15 28 private _logger = Logger.getInstance(); 16 29 private _resource: Resource<Blob>; 17 30 public filtering: ImageFiltering; 31 + public wrapping: ImageWrapConfiguration; 18 32 19 33 /** 20 34 * The original size of the source image in pixels ··· 57 71 */ 58 72 public ready: Promise<HTMLImageElement> = this._readyFuture.promise; 59 73 74 + public readonly path: string; 75 + 76 + /** 77 + * The path to the image, can also be a data url like 'data:image/' 78 + * @param path {string} Path to the image resource relative from the HTML document hosting the game, or absolute 79 + * @param options 80 + */ 81 + constructor(path: string, options?: ImageSourceOptions); 60 82 /** 61 83 * The path to the image, can also be a data url like 'data:image/' 62 84 * @param path {string} Path to the image resource relative from the HTML document hosting the game, or absolute 63 85 * @param bustCache {boolean} Should excalibur add a cache busting querystring? 64 86 * @param filtering {ImageFiltering} Optionally override the image filtering set by [[EngineOptions.antialiasing]] 65 87 */ 66 - constructor(public readonly path: string, bustCache: boolean = false, filtering?: ImageFiltering) { 88 + constructor(path: string, bustCache: boolean, filtering?: ImageFiltering); 89 + constructor(path: string, bustCacheOrOptions: boolean | ImageSourceOptions, filtering?: ImageFiltering) { 90 + this.path = path; 91 + let bustCache = false; 92 + let wrapping: ImageWrapConfiguration | ImageWrapping; 93 + if (typeof bustCacheOrOptions === 'boolean') { 94 + bustCache = bustCacheOrOptions; 95 + } else { 96 + ({ filtering, wrapping, bustCache } = {...bustCacheOrOptions}); 97 + } 67 98 this._resource = new Resource(path, 'blob', bustCache); 68 - this.filtering = filtering; 99 + this.filtering = filtering ?? this.filtering; 100 + if (typeof wrapping === 'string') { 101 + this.wrapping = { 102 + x: wrapping, 103 + y: wrapping 104 + }; 105 + } else { 106 + this.wrapping = wrapping ?? this.wrapping; 107 + } 69 108 if (path.endsWith('.svg') || path.endsWith('.gif')) { 70 109 this._logger.warn(`Image type is not fully supported, you may have mixed results ${path}. Fully supported: jpg, bmp, and png`); 71 110 } ··· 82 121 imageSource.data.setAttribute('data-original-src', 'image-element'); 83 122 84 123 if (options?.filtering) { 85 - imageSource.data.setAttribute('filtering', options?.filtering); 124 + imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, options?.filtering); 125 + } else { 126 + imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, ImageFiltering.Blended); 127 + } 128 + 129 + if (options?.wrapping) { 130 + let wrapping: ImageWrapConfiguration; 131 + if (typeof options.wrapping === 'string') { 132 + wrapping = { 133 + x: options.wrapping, 134 + y: options.wrapping 135 + }; 136 + } else { 137 + wrapping = { 138 + x: options.wrapping.x, 139 + y: options.wrapping.y 140 + }; 141 + } 142 + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, wrapping.x); 143 + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, wrapping.y); 86 144 } else { 87 - imageSource.data.setAttribute('filtering', ImageFiltering.Blended); 145 + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, ImageWrapping.Clamp); 146 + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, ImageWrapping.Clamp); 88 147 } 89 148 90 149 TextureLoader.checkImageSizeSupportedAndLog(image); ··· 145 204 throw `Error loading ImageSource from path '${this.path}' with error [${error.message}]`; 146 205 } 147 206 // Do a bad thing to pass the filtering as an attribute 148 - this.data.setAttribute('filtering', this.filtering); 207 + this.data.setAttribute(ImageSourceAttributeConstants.Filtering, this.filtering); 208 + this.data.setAttribute(ImageSourceAttributeConstants.WrappingX, this.wrapping?.x ?? ImageWrapping.Clamp); 209 + this.data.setAttribute(ImageSourceAttributeConstants.WrappingY, this.wrapping?.y ?? ImageWrapping.Clamp); 149 210 150 211 // todo emit complete 151 212 this._readyFuture.resolve(this.data);
+24
src/engine/Graphics/Wrapping.ts
··· 1 + 2 + /** 3 + * Describes the different image wrapping modes 4 + */ 5 + export enum ImageWrapping { 6 + 7 + Clamp = 'Clamp', 8 + 9 + Repeat = 'Repeat', 10 + 11 + Mirror = 'Mirror' 12 + } 13 + 14 + /** 15 + * 16 + */ 17 + export function parseImageWrapping(val: string): ImageWrapping { 18 + switch (val) { 19 + case ImageWrapping.Clamp: return ImageWrapping.Clamp; 20 + case ImageWrapping.Repeat: return ImageWrapping.Repeat; 21 + case ImageWrapping.Mirror: return ImageWrapping.Mirror; 22 + default: return ImageWrapping.Clamp; 23 + } 24 + }
+1
src/engine/Graphics/index.ts
··· 40 40 41 41 export * from './Context/texture-loader'; 42 42 export * from './Filtering'; 43 + export * from './Wrapping'; 43 44 44 45 45 46 // Rendering
+121 -2
src/spec/ImageSourceSpec.ts
··· 99 99 100 100 expect(image.src).not.toBeNull(); 101 101 expect(whenLoaded).toHaveBeenCalledTimes(1); 102 - expect(webgl.textureLoader.load).toHaveBeenCalledWith(image, ex.ImageFiltering.Blended, false); 102 + expect(webgl.textureLoader.load).toHaveBeenCalledWith( 103 + image, 104 + { 105 + filtering: ex.ImageFiltering.Blended, 106 + wrapping: { 107 + x: ex.ImageWrapping.Clamp, 108 + y: ex.ImageWrapping.Clamp 109 + } 110 + }, false); 103 111 }); 104 112 105 113 it('can load images with an image filtering Pixel', async () => { ··· 120 128 121 129 expect(image.src).not.toBeNull(); 122 130 expect(whenLoaded).toHaveBeenCalledTimes(1); 123 - expect(webgl.textureLoader.load).toHaveBeenCalledWith(image, ex.ImageFiltering.Pixel, false); 131 + expect(webgl.textureLoader.load).toHaveBeenCalledWith( 132 + image, 133 + { 134 + filtering: ex.ImageFiltering.Pixel, 135 + wrapping: { 136 + x: ex.ImageWrapping.Clamp, 137 + y: ex.ImageWrapping.Clamp 138 + } 139 + }, false); 140 + }); 141 + 142 + it('can load images with an image wrap repeat', async () => { 143 + const canvas = document.createElement('canvas'); 144 + const webgl = new ex.ExcaliburGraphicsContextWebGL({ 145 + canvasElement: canvas 146 + }); 147 + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); 148 + imageRenderer.initialize(webgl.__gl, webgl); 149 + spyOn(webgl.textureLoader, 'load').and.callThrough(); 150 + 151 + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png',{ 152 + filtering: ex.ImageFiltering.Pixel, 153 + wrapping: ex.ImageWrapping.Repeat 154 + }); 155 + const whenLoaded = jasmine.createSpy('whenLoaded'); 156 + const image = await spriteFontImage.load(); 157 + await spriteFontImage.ready.then(whenLoaded); 158 + 159 + imageRenderer.draw(image, 0, 0); 160 + 161 + expect(image.src).not.toBeNull(); 162 + expect(whenLoaded).toHaveBeenCalledTimes(1); 163 + expect(webgl.textureLoader.load).toHaveBeenCalledWith( 164 + image, 165 + { 166 + filtering: ex.ImageFiltering.Pixel, 167 + wrapping: { 168 + x: ex.ImageWrapping.Repeat, 169 + y: ex.ImageWrapping.Repeat 170 + } 171 + }, false); 172 + }); 173 + 174 + it('can load images with an image wrap repeat', async () => { 175 + const canvas = document.createElement('canvas'); 176 + const webgl = new ex.ExcaliburGraphicsContextWebGL({ 177 + canvasElement: canvas 178 + }); 179 + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); 180 + imageRenderer.initialize(webgl.__gl, webgl); 181 + spyOn(webgl.textureLoader, 'load').and.callThrough(); 182 + 183 + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png',{ 184 + filtering: ex.ImageFiltering.Pixel, 185 + wrapping: ex.ImageWrapping.Mirror 186 + }); 187 + const whenLoaded = jasmine.createSpy('whenLoaded'); 188 + const image = await spriteFontImage.load(); 189 + await spriteFontImage.ready.then(whenLoaded); 190 + 191 + imageRenderer.draw(image, 0, 0); 192 + 193 + expect(image.src).not.toBeNull(); 194 + expect(whenLoaded).toHaveBeenCalledTimes(1); 195 + expect(webgl.textureLoader.load).toHaveBeenCalledWith( 196 + image, 197 + { 198 + filtering: ex.ImageFiltering.Pixel, 199 + wrapping: { 200 + x: ex.ImageWrapping.Mirror, 201 + y: ex.ImageWrapping.Mirror 202 + } 203 + }, false); 204 + }); 205 + 206 + it('can load images with an image wrap mixed', async () => { 207 + const canvas = document.createElement('canvas'); 208 + const webgl = new ex.ExcaliburGraphicsContextWebGL({ 209 + canvasElement: canvas 210 + }); 211 + const imageRenderer = new ImageRenderer({pixelArtSampler: false, uvPadding: 0}); 212 + imageRenderer.initialize(webgl.__gl, webgl); 213 + spyOn(webgl.textureLoader, 'load').and.callThrough(); 214 + const texParameteri = spyOn(webgl.__gl, 'texParameteri').and.callThrough(); 215 + const gl = webgl.__gl; 216 + 217 + const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png',{ 218 + filtering: ex.ImageFiltering.Pixel, 219 + wrapping: { 220 + x: ex.ImageWrapping.Mirror, 221 + y: ex.ImageWrapping.Clamp 222 + } 223 + }); 224 + const whenLoaded = jasmine.createSpy('whenLoaded'); 225 + const image = await spriteFontImage.load(); 226 + await spriteFontImage.ready.then(whenLoaded); 227 + 228 + imageRenderer.draw(image, 0, 0); 229 + 230 + expect(image.src).not.toBeNull(); 231 + expect(whenLoaded).toHaveBeenCalledTimes(1); 232 + expect(texParameteri.calls.argsFor(0)).toEqual([gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT]); 233 + expect(texParameteri.calls.argsFor(1)).toEqual([gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE]); 234 + expect(webgl.textureLoader.load).toHaveBeenCalledWith( 235 + image, 236 + { 237 + filtering: ex.ImageFiltering.Pixel, 238 + wrapping: { 239 + x: ex.ImageWrapping.Mirror, 240 + y: ex.ImageWrapping.Clamp 241 + } 242 + }, false); 124 243 }); 125 244 126 245 it('can convert to a Sprite', async () => {