From 6d0d6ee50ec2be666aae986a97e006dc8b21d5d7 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Mon, 11 Mar 2024 23:32:27 -0500 Subject: [PATCH 1/6] implement image wrapping configuration --- package-lock.json | 44 +++++++++---------- package.json | 2 +- .../Context/image-renderer/image-renderer.ts | 19 +++++--- .../material-renderer/material-renderer.ts | 20 +++++---- src/engine/Graphics/Context/material.ts | 19 +++++--- src/engine/Graphics/Context/texture-loader.ts | 39 +++++++++++++--- src/engine/Graphics/Filtering.ts | 11 +++++ src/engine/Graphics/FontTextInstance.ts | 4 +- src/engine/Graphics/ImageSource.ts | 43 +++++++++++++++++- src/engine/Graphics/Wrapping.ts | 24 ++++++++++ src/engine/Graphics/index.ts | 1 + src/spec/ImageSourceSpec.ts | 20 ++++++++- 12 files changed, 190 insertions(+), 56 deletions(-) create mode 100644 src/engine/Graphics/Wrapping.ts diff --git a/package-lock.json b/package-lock.json index b2f6d03db..077a6247d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "eslint-config-prettier": "9.1.0", "eslint-plugin-jsdoc": "46.10.1", "eslint-plugin-storybook": "0.8.0", - "excalibur-jasmine": "0.3.2", + "excalibur-jasmine": "0.3.4", "istanbul": "0.4.5", "istanbul-instrumenter-loader": "3.0.1", "jasmine": "5.1.0", @@ -15381,31 +15381,31 @@ } }, "node_modules/excalibur": { - "version": "0.28.7", - "resolved": "https://registry.npmjs.org/excalibur/-/excalibur-0.28.7.tgz", - "integrity": "sha512-ws4hT4xE4+EaICdxHvI9pZV3L+RdtJK+H36KUfCoYQK3uaZNlnIO4JoeqMvKYbcCOZybKzW73W9+DyIjAiBvnA==", + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/excalibur/-/excalibur-0.29.1.tgz", + "integrity": "sha512-VVgFzuRTBJjn2Za3KtrYPUN0SgkoS+KrRfx7KxjROAO55FwV1N4S6bFBaCb09YC0mG9MbXTZx8WSG2CGvPm/BA==", "dev": true, "peer": true, "dependencies": { - "core-js": "3.33.3" + "core-js": "3.35.1" } }, "node_modules/excalibur-jasmine": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/excalibur-jasmine/-/excalibur-jasmine-0.3.2.tgz", - "integrity": "sha512-EOv0hswqSZ/9h1pzUaC6WEzMxWcqGMCAtMHDP0tuLIjpUp0fgDjGWWW5qefqYyMgED4Ei4Q7pWL9iX5AlagmyQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/excalibur-jasmine/-/excalibur-jasmine-0.3.4.tgz", + "integrity": "sha512-91BvPdpQDgsvyJHrwkYT0Ryog174d2qvlkHoVvp+Acv7L7oEQCGusUUuPRZh4n/VzeSl/v0Nuuy/4ThXsCZy3A==", "dev": true, "dependencies": { "pixelmatch": "5.3.0" }, "peerDependencies": { - "excalibur": "0.28.7" + "excalibur": "^0.29.0" } }, "node_modules/excalibur/node_modules/core-js": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.3.tgz", - "integrity": "sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz", + "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==", "dev": true, "hasInstallScript": true, "peer": true, @@ -35934,28 +35934,28 @@ "dev": true }, "excalibur": { - "version": "0.28.7", - "resolved": "https://registry.npmjs.org/excalibur/-/excalibur-0.28.7.tgz", - "integrity": "sha512-ws4hT4xE4+EaICdxHvI9pZV3L+RdtJK+H36KUfCoYQK3uaZNlnIO4JoeqMvKYbcCOZybKzW73W9+DyIjAiBvnA==", + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/excalibur/-/excalibur-0.29.1.tgz", + "integrity": "sha512-VVgFzuRTBJjn2Za3KtrYPUN0SgkoS+KrRfx7KxjROAO55FwV1N4S6bFBaCb09YC0mG9MbXTZx8WSG2CGvPm/BA==", "dev": true, "peer": true, "requires": { - "core-js": "3.33.3" + "core-js": "3.35.1" }, "dependencies": { "core-js": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.3.tgz", - "integrity": "sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz", + "integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==", "dev": true, "peer": true } } }, "excalibur-jasmine": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/excalibur-jasmine/-/excalibur-jasmine-0.3.2.tgz", - "integrity": "sha512-EOv0hswqSZ/9h1pzUaC6WEzMxWcqGMCAtMHDP0tuLIjpUp0fgDjGWWW5qefqYyMgED4Ei4Q7pWL9iX5AlagmyQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/excalibur-jasmine/-/excalibur-jasmine-0.3.4.tgz", + "integrity": "sha512-91BvPdpQDgsvyJHrwkYT0Ryog174d2qvlkHoVvp+Acv7L7oEQCGusUUuPRZh4n/VzeSl/v0Nuuy/4ThXsCZy3A==", "dev": true, "requires": { "pixelmatch": "5.3.0" diff --git a/package.json b/package.json index 407eabc0b..559f57b23 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "eslint-config-prettier": "9.1.0", "eslint-plugin-jsdoc": "46.10.1", "eslint-plugin-storybook": "0.8.0", - "excalibur-jasmine": "0.3.2", + "excalibur-jasmine": "0.3.4", "istanbul": "0.4.5", "istanbul-instrumenter-loader": "3.0.1", "jasmine": "5.1.0", diff --git a/src/engine/Graphics/Context/image-renderer/image-renderer.ts b/src/engine/Graphics/Context/image-renderer/image-renderer.ts index a520d79c7..f90b91273 100644 --- a/src/engine/Graphics/Context/image-renderer/image-renderer.ts +++ b/src/engine/Graphics/Context/image-renderer/image-renderer.ts @@ -1,7 +1,8 @@ import { sign } from '../../../Math/util'; import { vec } from '../../../Math/vector'; -import { ImageFiltering } from '../../Filtering'; +import { parseImageFiltering } from '../../Filtering'; import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; +import { parseImageWrapping } from '../../Wrapping'; import { HTMLImageSource } from '../ExcaliburGraphicsContext'; import { ExcaliburGraphicsContextWebGL, pixelSnapEpsilon } from '../ExcaliburGraphicsContextWebGL'; import { QuadIndexBuffer } from '../quad-index-buffer'; @@ -120,14 +121,18 @@ export class ImageRenderer 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 filtering = maybeFiltering ? parseImageFiltering(image.getAttribute('filtering')) : null; + const wrapX = parseImageWrapping(image.getAttribute('wrapping-x')); + const wrapY = parseImageWrapping(image.getAttribute('wrapping-y')); 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..8db103265 100644 --- a/src/engine/Graphics/Context/material-renderer/material-renderer.ts +++ b/src/engine/Graphics/Context/material-renderer/material-renderer.ts @@ -1,6 +1,7 @@ import { vec } from '../../../Math/vector'; -import { ImageFiltering } from '../../Filtering'; +import { parseImageFiltering } from '../../Filtering'; import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; +import { parseImageWrapping } from '../../Wrapping'; import { HTMLImageSource } from '../ExcaliburGraphicsContext'; import { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL'; import { QuadIndexBuffer } from '../quad-index-buffer'; @@ -205,14 +206,18 @@ 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 filtering = maybeFiltering ? parseImageFiltering(image.getAttribute('filtering')) : null; + const wrapX = parseImageWrapping(image.getAttribute('wrapping-x')); + const wrapY = parseImageWrapping(image.getAttribute('wrapping-y')); 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 +233,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..8c3e83ea3 100644 --- a/src/engine/Graphics/Context/material.ts +++ b/src/engine/Graphics/Context/material.ts @@ -4,7 +4,8 @@ import { ExcaliburGraphicsContextWebGL } from './ExcaliburGraphicsContextWebGL'; import { Shader } from './shader'; import { Logger } from '../../Util/Log'; import { ImageSource } from '../ImageSource'; -import { ImageFiltering } from '../Filtering'; +import { ImageFiltering, parseImageFiltering } from '../Filtering'; +import { parseImageWrapping } from '../Wrapping'; export interface MaterialOptions { /** @@ -192,14 +193,18 @@ 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 filtering = maybeFiltering ? parseImageFiltering(imageElement.getAttribute('filtering')) : null; + const wrapX = parseImageWrapping(imageElement.getAttribute('wrapping-x')); + const wrapY = parseImageWrapping(imageElement.getAttribute('wrapping-y')); 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..3824509ef 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,33 @@ 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); + const { x: xWrap, y: yWrap} = wrapping ?? 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..6e4ad89e3 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' +} + +/** + * + */ +export function parseImageFiltering(val: string) { + switch (val) { + case ImageFiltering.Pixel: return ImageFiltering.Pixel; + case ImageFiltering.Blended: return ImageFiltering.Blended; + default: return ImageFiltering.Blended; + } } \ 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..981d8471f 100644 --- a/src/engine/Graphics/ImageSource.ts +++ b/src/engine/Graphics/ImageSource.ts @@ -5,16 +5,27 @@ 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?: { + x: ImageWrapping, + y: ImageWrapping + }; bustCache?: boolean; } +export interface ImageWrapConfiguration { + x: ImageWrapping; + y: ImageWrapping; +} + 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 +68,33 @@ export class ImageSource implements Loadable { */ public ready: Promise = this._readyFuture.promise; + public readonly path: string; + + /** + * + * @param path + * @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; + 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; + 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`); } @@ -87,6 +116,14 @@ export class ImageSource implements Loadable { imageSource.data.setAttribute('filtering', ImageFiltering.Blended); } + if (options?.wrapping) { + imageSource.data.setAttribute('wrapping-x', options?.wrapping.x); + imageSource.data.setAttribute('wrapping-y', options?.wrapping.y); + } else { + imageSource.data.setAttribute('wrapping-x', ImageWrapping.Clamp); + imageSource.data.setAttribute('wrapping-y', ImageWrapping.Clamp); + } + TextureLoader.checkImageSizeSupportedAndLog(image); imageSource._readyFuture.resolve(image); return imageSource; @@ -146,6 +183,8 @@ export class ImageSource implements Loadable { } // Do a bad thing to pass the filtering as an attribute this.data.setAttribute('filtering', this.filtering); + this.data.setAttribute('wrapping-x', this.wrapping?.x); + this.data.setAttribute('wrapping-y', this.wrapping?.y); // 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..0ccc18a98 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,15 @@ 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 convert to a Sprite', async () => { From 6f87d7598af66d8e3295e16a002b0bea0f92c627 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sat, 6 Apr 2024 17:31:43 -0500 Subject: [PATCH 2/6] fix tests --- .../Context/image-renderer/image-renderer.ts | 9 +++---- .../material-renderer/material-renderer.ts | 9 +++---- src/engine/Graphics/Context/material.ts | 10 ++++---- src/engine/Graphics/Context/texture-loader.ts | 2 +- src/engine/Graphics/Filtering.ts | 6 ++--- src/engine/Graphics/ImageSource.ts | 24 ++++++++++++------- 6 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/engine/Graphics/Context/image-renderer/image-renderer.ts b/src/engine/Graphics/Context/image-renderer/image-renderer.ts index 4f3df8c07..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,7 @@ import { sign } from '../../../Math/util'; 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'; @@ -125,10 +126,10 @@ export class ImageRenderer implements RendererPlugin { if (this._images.has(image)) { return; } - const maybeFiltering = image.getAttribute('filtering'); - const filtering = maybeFiltering ? parseImageFiltering(image.getAttribute('filtering')) : null; - const wrapX = parseImageWrapping(image.getAttribute('wrapping-x')); - const wrapY = parseImageWrapping(image.getAttribute('wrapping-y')); + 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( diff --git a/src/engine/Graphics/Context/material-renderer/material-renderer.ts b/src/engine/Graphics/Context/material-renderer/material-renderer.ts index 8db103265..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,7 @@ import { vec } from '../../../Math/vector'; import { parseImageFiltering } from '../../Filtering'; import { GraphicsDiagnostics } from '../../GraphicsDiagnostics'; +import { ImageSourceAttributeConstants } from '../../ImageSource'; import { parseImageWrapping } from '../../Wrapping'; import { HTMLImageSource } from '../ExcaliburGraphicsContext'; import { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL'; @@ -205,10 +206,10 @@ export class MaterialRenderer implements RendererPlugin { } private _addImageAsTexture(image: HTMLImageSource) { - const maybeFiltering = image.getAttribute('filtering'); - const filtering = maybeFiltering ? parseImageFiltering(image.getAttribute('filtering')) : null; - const wrapX = parseImageWrapping(image.getAttribute('wrapping-x')); - const wrapY = parseImageWrapping(image.getAttribute('wrapping-y')); + 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( diff --git a/src/engine/Graphics/Context/material.ts b/src/engine/Graphics/Context/material.ts index 8c3e83ea3..2d1bf836a 100644 --- a/src/engine/Graphics/Context/material.ts +++ b/src/engine/Graphics/Context/material.ts @@ -3,7 +3,7 @@ import { ExcaliburGraphicsContext } from './ExcaliburGraphicsContext'; import { ExcaliburGraphicsContextWebGL } from './ExcaliburGraphicsContextWebGL'; import { Shader } from './shader'; import { Logger } from '../../Util/Log'; -import { ImageSource } from '../ImageSource'; +import { ImageSource, ImageSourceAttributeConstants } from '../ImageSource'; import { ImageFiltering, parseImageFiltering } from '../Filtering'; import { parseImageWrapping } from '../Wrapping'; @@ -192,10 +192,10 @@ export class Material { private _loadImageSource(image: ImageSource) { const imageElement = image.image; - const maybeFiltering = imageElement.getAttribute('filtering'); - const filtering = maybeFiltering ? parseImageFiltering(imageElement.getAttribute('filtering')) : null; - const wrapX = parseImageWrapping(imageElement.getAttribute('wrapping-x')); - const wrapY = parseImageWrapping(imageElement.getAttribute('wrapping-y')); + 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( diff --git a/src/engine/Graphics/Context/texture-loader.ts b/src/engine/Graphics/Context/texture-loader.ts index 3824509ef..de2290137 100644 --- a/src/engine/Graphics/Context/texture-loader.ts +++ b/src/engine/Graphics/Context/texture-loader.ts @@ -90,7 +90,7 @@ export class TextureLoader { gl.bindTexture(gl.TEXTURE_2D, tex); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - const { x: xWrap, y: yWrap} = wrapping ?? TextureLoader.wrapping; + const { x: xWrap, y: yWrap} = (wrapping ?? TextureLoader.wrapping); switch (xWrap) { case ImageWrapping.Clamp: gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); diff --git a/src/engine/Graphics/Filtering.ts b/src/engine/Graphics/Filtering.ts index 6e4ad89e3..0801692e2 100644 --- a/src/engine/Graphics/Filtering.ts +++ b/src/engine/Graphics/Filtering.ts @@ -18,12 +18,12 @@ export enum ImageFiltering { } /** - * + * Parse the image filtering attribute value, if it doesn't match returns null */ -export function parseImageFiltering(val: string) { +export function parseImageFiltering(val: string): ImageFiltering | null { switch (val) { case ImageFiltering.Pixel: return ImageFiltering.Pixel; case ImageFiltering.Blended: return ImageFiltering.Blended; - default: return ImageFiltering.Blended; + default: return null; } } \ No newline at end of file diff --git a/src/engine/Graphics/ImageSource.ts b/src/engine/Graphics/ImageSource.ts index 981d8471f..b1d3b3d39 100644 --- a/src/engine/Graphics/ImageSource.ts +++ b/src/engine/Graphics/ImageSource.ts @@ -21,6 +21,12 @@ export interface ImageWrapConfiguration { 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; @@ -111,17 +117,17 @@ 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('filtering', ImageFiltering.Blended); + imageSource.data.setAttribute(ImageSourceAttributeConstants.Filtering, ImageFiltering.Blended); } if (options?.wrapping) { - imageSource.data.setAttribute('wrapping-x', options?.wrapping.x); - imageSource.data.setAttribute('wrapping-y', options?.wrapping.y); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, options?.wrapping.x); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, options?.wrapping.y); } else { - imageSource.data.setAttribute('wrapping-x', ImageWrapping.Clamp); - imageSource.data.setAttribute('wrapping-y', ImageWrapping.Clamp); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, ImageWrapping.Clamp); + imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, ImageWrapping.Clamp); } TextureLoader.checkImageSizeSupportedAndLog(image); @@ -182,9 +188,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('wrapping-x', this.wrapping?.x); - this.data.setAttribute('wrapping-y', this.wrapping?.y); + 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); From 6e1928c33075a296cf8709f9f3205b185529e7f1 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sat, 6 Apr 2024 18:34:16 -0500 Subject: [PATCH 3/6] update changelog --- CHANGELOG.md | 10 ++++++++++ src/engine/Graphics/ImageSource.ts | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) 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/src/engine/Graphics/ImageSource.ts b/src/engine/Graphics/ImageSource.ts index b1d3b3d39..b59e7ea31 100644 --- a/src/engine/Graphics/ImageSource.ts +++ b/src/engine/Graphics/ImageSource.ts @@ -77,9 +77,9 @@ export class ImageSource implements Loadable { public readonly path: string; /** - * - * @param path - * @param options + * 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); /** From aa81c0f5180c0449dddeb485afbbe516bf2527cf Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sat, 6 Apr 2024 18:41:01 -0500 Subject: [PATCH 4/6] fix lint --- src/engine/Graphics/ImageSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/Graphics/ImageSource.ts b/src/engine/Graphics/ImageSource.ts index b59e7ea31..a66556598 100644 --- a/src/engine/Graphics/ImageSource.ts +++ b/src/engine/Graphics/ImageSource.ts @@ -79,7 +79,7 @@ export class ImageSource implements Loadable { /** * 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 + * @param options */ constructor(path: string, options?: ImageSourceOptions); /** From e9e3422595a335b1db026d063420023291d79f76 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sat, 6 Apr 2024 20:52:06 -0500 Subject: [PATCH 5/6] add image wrapping example --- sandbox/tests/imagewrapping/index.html | 13 ++++ sandbox/tests/imagewrapping/index.ts | 59 ++++++++++++++++++ sandbox/tests/imagewrapping/noise.png | Bin 0 -> 8068 bytes src/engine/Graphics/Context/texture-loader.ts | 17 ++++- src/engine/Graphics/ImageSource.ts | 32 +++++++--- 5 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 sandbox/tests/imagewrapping/index.html create mode 100644 sandbox/tests/imagewrapping/index.ts create mode 100644 sandbox/tests/imagewrapping/noise.png 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 0000000000000000000000000000000000000000..98ab46077a94bd74d1ab03a94ba56023f9a9d8cf GIT binary patch literal 8068 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 Sg)}k%0000 { constructor(path: string, bustCacheOrOptions: boolean | ImageSourceOptions, filtering?: ImageFiltering) { this.path = path; let bustCache = false; - let wrapping: ImageWrapConfiguration; + let wrapping: ImageWrapConfiguration | ImageWrapping; if (typeof bustCacheOrOptions === 'boolean') { bustCache = bustCacheOrOptions; } else { @@ -100,7 +97,14 @@ export class ImageSource implements Loadable { } this._resource = new Resource(path, 'blob', bustCache); this.filtering = filtering ?? this.filtering; - this.wrapping = wrapping ?? this.wrapping; + 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`); } @@ -123,8 +127,20 @@ export class ImageSource implements Loadable { } if (options?.wrapping) { - imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingX, options?.wrapping.x); - imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, options?.wrapping.y); + 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(ImageSourceAttributeConstants.WrappingX, ImageWrapping.Clamp); imageSource.data.setAttribute(ImageSourceAttributeConstants.WrappingY, ImageWrapping.Clamp); From 482e5300706ccd27be9c6e69aa449818567c9f2a Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Sat, 6 Apr 2024 21:10:46 -0500 Subject: [PATCH 6/6] add test --- src/spec/ImageSourceSpec.ts | 103 ++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/spec/ImageSourceSpec.ts b/src/spec/ImageSourceSpec.ts index 0ccc18a98..000edaace 100644 --- a/src/spec/ImageSourceSpec.ts +++ b/src/spec/ImageSourceSpec.ts @@ -139,6 +139,109 @@ describe('A ImageSource', () => { }, 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 () => { const spriteFontImage = new ex.ImageSource('src/spec/images/GraphicsTextSpec/spritefont.png'); const sprite = spriteFontImage.toSprite();