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 () => {