diff --git a/CHANGELOG.md b/CHANGELOG.md index 77e5ffd7..46489853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,14 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes - `ex.Util.extend()` is removed, modern js spread operator `{...someobject, ...someotherobject}` handles this better. - +- Excalibur post processing is now moved to the `engine.graphicsContext.addPostProcessor()` +- Breaking change to `ex.PostProcessor`, all post processors must now now implement this interface + ```typescript + export interface PostProcessor { + intialize(gl: WebGLRenderingContext): void; + getShader(): Shader; + } + ``` ### Deprecated - The static `Engine.createMainLoop` is now marked deprecated and will be removed in v0.26.0, it is replaced by the `Clock` api @@ -24,7 +31,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Pointers can now be configured to use the collider or the graphics bounds as the target for pointers with the `ex.PointerComponent` - `useColliderShape` - (default true) uses the collider component geometry for pointer events - `useGraphicsBounds` - (default false) uses the graphics bounds for pointer events -- + ### Fixed @@ -45,6 +52,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). - The following Engine's pieces: `Collision` `Graphics` `Resources` `Trigger` are updated to reflect the new EventDispatcher behavior. ### Changed + +- `ex.ColorBlindCorrector` is renamed to `ex.ColorBlindnessPostProcessor`, and `ex.ColorBlindness` is renamed to `ex.ColorBlindnessMode` + - Color blindness can still be corrected or simulated: + * `game.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Deuteranope)` + * `game.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Deuteranope)` +- Excalibur now uses pre-multiplied alpha automatically, images will be unpacked into memory using `gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true)` - Excalibur FPS is now sampled over 100ms blocks, this gives a more usable fps in the stats. The sampler is available off of the engine clock `engine.clock.fpsSampler.fps` - Pointer Events: * Event types (up, down, move, etc) now all exist in 2 types `ex.Input.PointerEvent` and `ex.Input.WheelEvent` diff --git a/sandbox/src/game.ts b/sandbox/src/game.ts index bd3e6b66..7cedfaf0 100644 --- a/sandbox/src/game.ts +++ b/sandbox/src/game.ts @@ -130,6 +130,9 @@ var game = new ex.Engine({ maxFps: 60 }); +var colorblind = new ex.ColorBlindnessPostProcessor(ex.ColorBlindnessMode.Deuteranope); +game.graphicsContext.addPostProcessor(colorblind); + fullscreenButton.addEventListener('click', () => { if (game.screen.isFullScreen) { game.screen.exitFullScreen(); diff --git a/src/engine/Debug/DebugFlags.ts b/src/engine/Debug/DebugFlags.ts index ad628332..7ca4a4d7 100644 --- a/src/engine/Debug/DebugFlags.ts +++ b/src/engine/Debug/DebugFlags.ts @@ -1,5 +1,7 @@ -import { Engine } from '../Engine'; -import { ColorBlindCorrector, ColorBlindness } from '../PostProcessing/Index'; +import { ColorBlindnessMode } from '../Graphics/PostProcessor/ColorBlindnessMode'; +import { ColorBlindnessPostProcessor } from '../Graphics/PostProcessor/ColorBlindnessPostProcessor'; +import { Engine } from '../Engine'; +import { ExcaliburGraphicsContextWebGL } from '..'; export interface DebugFlags { colorBlindMode: ColorBlindFlags; @@ -12,11 +14,15 @@ export class ColorBlindFlags { this._engine = engine; } - public correct(colorBlindness: ColorBlindness) { - this._engine.postProcessors.push(new ColorBlindCorrector(this._engine, false, colorBlindness)); + public correct(colorBlindness: ColorBlindnessMode) { + if (this._engine.graphicsContext instanceof ExcaliburGraphicsContextWebGL) { + this._engine.graphicsContext.addPostProcessor(new ColorBlindnessPostProcessor(colorBlindness, false)); + } } - public simulate(colorBlindness: ColorBlindness) { - this._engine.postProcessors.push(new ColorBlindCorrector(this._engine, true, colorBlindness)); + public simulate(colorBlindness: ColorBlindnessMode) { + if (this._engine.graphicsContext instanceof ExcaliburGraphicsContextWebGL) { + this._engine.graphicsContext.addPostProcessor(new ColorBlindnessPostProcessor(colorBlindness, true)); + } } } diff --git a/src/engine/Engine.ts b/src/engine/Engine.ts index c69474ef..e03ec42e 100644 --- a/src/engine/Engine.ts +++ b/src/engine/Engine.ts @@ -33,7 +33,6 @@ import { Logger, LogLevel } from './Util/Log'; import { Color } from './Color'; import { Scene } from './Scene'; import { Entity } from './EntityComponentSystem/Entity'; -import { PostProcessor } from './PostProcessing/PostProcessor'; import { Debug, DebugStats } from './Debug/Debug'; import { Class } from './Class'; import * as Input from './Input/Index'; @@ -311,11 +310,6 @@ export class Engine extends Class implements CanInitialize, CanUpdate, CanDraw { return this.debug.stats; } - /** - * Gets or sets the list of post processors to apply at the end of drawing a frame (such as [[ColorBlindCorrector]]) - */ - public postProcessors: PostProcessor[] = []; - /** * The current [[Scene]] being drawn and updated on screen */ @@ -1110,11 +1104,6 @@ O|===|* >________________>\n\ this.ctx.fillText('FPS:' + this.stats.currFrame.fps.toFixed(2).toString(), 10, 10); } - // Post processing - for (let i = 0; i < this.postProcessors.length; i++) { - this.postProcessors[i].process(this.ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight), this.ctx); - } - this._postdraw(ctx, delta); } diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts index 584962f2..b3e12800 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContext.ts @@ -1,5 +1,6 @@ import { Vector } from '../../Math/vector'; import { Color } from '../../Color'; +import { PostProcessor } from '../PostProcessor/PostProcessor'; export type HTMLImageSource = HTMLImageElement | HTMLCanvasElement; @@ -179,6 +180,25 @@ export interface ExcaliburGraphicsContext { */ scale(x: number, y: number): void; + /** + * Add a post processor to the graphics context + * + * Post processors are run in the order they were added. + * @param postprocessor + */ + addPostProcessor(postprocessor: PostProcessor): void; + + /** + * Remove a specific post processor from the graphics context + * @param postprocessor + */ + removePostProcessor(postprocessor: PostProcessor): void; + + /** + * Remove all post processors from the graphics context + */ + clearPostProcessors(): void; + /** * Clears the screen with the current background color */ diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts index b272322a..5fb4cd3b 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContext2DCanvas.ts @@ -11,6 +11,7 @@ import { Color } from '../../Color'; import { StateStack } from './state-stack'; import { GraphicsDiagnostics } from '../GraphicsDiagnostics'; import { DebugText } from './debug-text'; +import { PostProcessor } from '../PostProcessor/PostProcessor'; class ExcaliburGraphicsContext2DCanvasDebug implements DebugDraw { private _debugText = new DebugText(); @@ -250,6 +251,18 @@ export class ExcaliburGraphicsContext2DCanvas implements ExcaliburGraphicsContex this.__ctx.scale(x, y); } + public addPostProcessor(_postprocessor: PostProcessor) { + throw Error('Not implemented'); + } + + public removePostProcessor(_postprocessor: PostProcessor) { + throw Error('Not implemented'); + } + + public clearPostProcessors() { + throw Error('Not implemented'); + } + clear(): void { // Clear frame this.__ctx.clearRect(0, 0, this.width, this.height); diff --git a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts index 6d7f7826..b9fbc464 100644 --- a/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts +++ b/src/engine/Graphics/Context/ExcaliburGraphicsContextWebGL.ts @@ -21,6 +21,9 @@ import { Canvas } from '../Canvas'; import { GraphicsDiagnostics } from '../GraphicsDiagnostics'; import { DebugText } from './debug-text'; import { ScreenDimension } from '../../Screen'; +import { RenderTarget } from './render-target'; +import { ScreenRenderer } from './screen-renderer'; +import { PostProcessor } from '../PostProcessor/PostProcessor'; class ExcaliburGraphicsContextWebGLDebug implements DebugDraw { private _debugText = new DebugText(); @@ -71,6 +74,12 @@ export interface WebGLGraphicsContextInfo { } export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { + private _renderTarget: RenderTarget; + + private _postProcessTargets: RenderTarget[] = []; + + private _screenRenderer: ScreenRenderer; + private _postprocessors: PostProcessor[] = []; /** * Meant for internal use only. Access the internal context at your own risk and no guarantees this will exist in the future. * @internal @@ -173,12 +182,37 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { gl.clear(gl.COLOR_BUFFER_BIT); // Enable alpha blending + // https://www.realtimerendering.com/blog/gpus-prefer-premultiplication/ gl.enable(gl.BLEND); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); + gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); this.__pointRenderer = new PointRenderer(gl, { matrix: this._ortho, transform: this._transform, state: this._state }); this.__lineRenderer = new LineRenderer(gl, { matrix: this._ortho, transform: this._transform, state: this._state }); this.__imageRenderer = new ImageRenderer(gl, { matrix: this._ortho, transform: this._transform, state: this._state }); + this._screenRenderer = new ScreenRenderer(gl); + + this._renderTarget = new RenderTarget({ + gl, + width: gl.canvas.width, + height: gl.canvas.height + }); + + + this._postProcessTargets = [ + new RenderTarget({ + gl, + width: gl.canvas.width, + height: gl.canvas.height + }), + new RenderTarget({ + gl, + width: gl.canvas.width, + height: gl.canvas.height + }) + ]; // 2D ctx shim this._canvas = new Canvas({ @@ -199,6 +233,10 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { this.__lineRenderer.shader.addUniformMatrix('u_matrix', this._ortho.data); this.__imageRenderer.shader.addUniformMatrix('u_matrix', this._ortho.data); + this._renderTarget.setResolution(gl.canvas.width, gl.canvas.height); + this._postProcessTargets[0].setResolution(gl.canvas.width, gl.canvas.height); + this._postProcessTargets[1].setResolution(gl.canvas.width, gl.canvas.height); + // 2D ctx shim this._canvas.width = gl.canvas.width; this._canvas.height = gl.canvas.height; @@ -288,12 +326,30 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { this._transform.current = matrix; } + public addPostProcessor(postprocessor: PostProcessor) { + this._postprocessors.push(postprocessor); + postprocessor.intialize(this.__gl); + } + + public removePostProcessor(postprocessor: PostProcessor) { + const index = this._postprocessors.indexOf(postprocessor); + if (index !== -1) { + this._postprocessors.splice(index, 1); + } + } + + public clearPostProcessors() { + this._postprocessors.length = 0; + } + clear() { const gl = this.__gl; + this._renderTarget.use(); gl.clearColor(this.backgroundColor.r / 255, this.backgroundColor.g / 255, this.backgroundColor.b / 255, this.backgroundColor.a); // Clear the context with the newly set color. This is // the function call that actually does the drawing. gl.clear(gl.COLOR_BUFFER_BIT); + this._renderTarget.disable(); GraphicsDiagnostics.clear(); } @@ -302,10 +358,27 @@ export class ExcaliburGraphicsContextWebGL implements ExcaliburGraphicsContext { */ flush() { const gl = this.__gl; - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + // render target captures all draws and redirects to the render target + this._renderTarget.use(); this.__imageRenderer.render(); this.__lineRenderer.render(); this.__pointRenderer.render(); + this._renderTarget.disable(); + + // post process step + const source = this._renderTarget.toRenderSource(); + source.use(); + + // flip flop render targets + for (let i = 0; i < this._postprocessors.length; i++) { + this._postProcessTargets[i % 2].use(); + this._screenRenderer.renderWithShader(this._postprocessors[i].getShader()); + this._postProcessTargets[i % 2].toRenderSource().use(); + } + + // passing null switches renderering back to the canvas + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + this._screenRenderer.render(); } } diff --git a/src/engine/Graphics/Context/render-source.ts b/src/engine/Graphics/Context/render-source.ts new file mode 100644 index 00000000..18216839 --- /dev/null +++ b/src/engine/Graphics/Context/render-source.ts @@ -0,0 +1,16 @@ +export class RenderSource { + constructor( + private _gl: WebGLRenderingContext, + private _texture: WebGLTexture) {} + + public use() { + const gl = this._gl; + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, this._texture); + } + + public disable() { + const gl = this._gl; + gl.bindTexture(gl.TEXTURE_2D, null); + } +} \ No newline at end of file diff --git a/src/engine/Graphics/Context/render-target.ts b/src/engine/Graphics/Context/render-target.ts new file mode 100644 index 00000000..7fb6593e --- /dev/null +++ b/src/engine/Graphics/Context/render-target.ts @@ -0,0 +1,80 @@ +import { RenderSource } from './render-source'; + +export class RenderTarget { + width: number; + height: number; + private _gl: WebGLRenderingContext; + constructor(options: {gl: WebGLRenderingContext, width: number, height: number}) { + this.width = options.width; + this.height = options.height; + this._gl = options.gl; + this._setupFramebuffer(); + } + + setResolution(width: number, height: number) { + const gl = this._gl; + this.width = width; + this.height = height; + gl.bindTexture(gl.TEXTURE_2D, this._frameTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + } + + private _frameBuffer: WebGLFramebuffer; + public get frameBuffer() { + return this._frameBuffer; + } + private _frameTexture: WebGLTexture; + public get frameTexture() { + return this._frameTexture; + } + private _setupFramebuffer() { + // Allocates frame buffer + const gl = this._gl; + this._frameTexture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this._frameTexture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + // gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + + // set the filtering so we don't need mips + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + 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); + + + // attach the texture as the first color attachment + const attachmentPoint = gl.COLOR_ATTACHMENT0; + + // After this bind all draw calls will draw to this framebuffer texture + this._frameBuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer); + gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, this._frameTexture, 0); + // Reset after initialized + this.disable(); + } + + public toRenderSource() { + const source = new RenderSource(this._gl, this._frameTexture); + return source; + } + + /** + * When called, all drawing gets redirected to this render target + */ + public use() { + const gl = this._gl; + gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer); + // very important to set the viewport to the size of the framebuffer texture + gl.viewport(0, 0, this.width, this.height); + } + + /** + * When called, all drawing is sent back to the canvas + */ + public disable() { + const gl = this._gl; + // passing null switches renderering back to the canvas + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.bindTexture(gl.TEXTURE_2D, null); + } +} \ No newline at end of file diff --git a/src/engine/Graphics/Context/screen-renderer.ts b/src/engine/Graphics/Context/screen-renderer.ts new file mode 100644 index 00000000..66644900 --- /dev/null +++ b/src/engine/Graphics/Context/screen-renderer.ts @@ -0,0 +1,49 @@ +import { Renderer } from './renderer'; +import { Shader } from './shader'; + +import screenVertex from './shaders/screen-vertex.glsl'; +import screenFragment from './shaders/screen-fragment.glsl'; + +export class ScreenRenderer implements Renderer { + private _gl: WebGLRenderingContext; + private _screenQuadBuffer: WebGLBuffer; + private _screenQuad: Float32Array; + private _shader: Shader; + constructor(gl: WebGLRenderingContext) { + this._gl = gl; + this._shader = new Shader(gl, screenVertex, screenFragment); + this._shader.addAttribute('a_position', 2, gl.FLOAT); + this._shader.addAttribute('a_texcoord', 2, gl.FLOAT); + this._screenQuadBuffer = gl.createBuffer(); + // clip space quad + uv since we don't need a camera + this._screenQuad = new Float32Array([ + -1, -1, 0, 0, + -1, 1, 0, 1, + 1, -1, 1, 0, + + 1, -1, 1, 0, + -1, 1, 0, 1, + 1, 1, 1, 1 + ]); + gl.bindBuffer(gl.ARRAY_BUFFER, this._screenQuadBuffer); + gl.bufferData(gl.ARRAY_BUFFER, this._screenQuad, gl.STATIC_DRAW); + }; + + renderWithShader(customShader: Shader): void { + const gl = this._gl; + gl.bindBuffer(gl.ARRAY_BUFFER, this._screenQuadBuffer); + + customShader.use(); + + gl.drawArrays(gl.TRIANGLES, 0, 6); + } + + render(): void { + const gl = this._gl; + gl.bindBuffer(gl.ARRAY_BUFFER, this._screenQuadBuffer); + + this._shader.use(); + + gl.drawArrays(gl.TRIANGLES, 0, 6); + } +} \ No newline at end of file diff --git a/src/engine/Graphics/Context/shader.ts b/src/engine/Graphics/Context/shader.ts index 9d57e3b3..3fb58304 100644 --- a/src/engine/Graphics/Context/shader.ts +++ b/src/engine/Graphics/Context/shader.ts @@ -117,6 +117,26 @@ export class Shader { }; } + public addUniformBool(name: string, data: boolean) { + const gl = this._gl; + this.uniforms[name] = { + name, + type: 'boolean', + location: gl.getUniformLocation(this.program, name), + data: data + }; + } + + public addUniformInt(name: string, data: number) { + const gl = this._gl; + this.uniforms[name] = { + name, + type: 'int', + location: gl.getUniformLocation(this.program, name), + data: data + }; + } + /** * Add attributes in the order they appear in the VBO * @param name Name of the attribute in the shader source @@ -214,6 +234,14 @@ export class Shader { gl.uniform1iv(uniform.location, uniform.data); break; } + case 'int': { + gl.uniform1i(uniform.location, uniform.data); + break; + } + case 'boolean': { + gl.uniform1i(uniform.location, uniform.data); + break; + } } } } diff --git a/src/engine/Graphics/Context/shaders/point-fragment.glsl b/src/engine/Graphics/Context/shaders/point-fragment.glsl index ece3ed91..cbbbf7e4 100644 --- a/src/engine/Graphics/Context/shaders/point-fragment.glsl +++ b/src/engine/Graphics/Context/shaders/point-fragment.glsl @@ -9,7 +9,7 @@ void main() { float r = 0.0, delta = 0.0, alpha = 1.0; vec2 cxy = 2.0 * gl_PointCoord - 1.0; r = dot(cxy, cxy); - + #ifdef GL_OES_standard_derivatives delta = fwidth(r); alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, r); @@ -19,4 +19,6 @@ void main() { } #endif gl_FragColor = vec4(v_color.rgb, v_color.a * alpha); + // "premultiply" the color by alpha + gl_FragColor.rgb *= gl_FragColor.a; } \ No newline at end of file diff --git a/src/engine/Graphics/Context/shaders/screen-fragment.glsl b/src/engine/Graphics/Context/shaders/screen-fragment.glsl new file mode 100644 index 00000000..d24723a2 --- /dev/null +++ b/src/engine/Graphics/Context/shaders/screen-fragment.glsl @@ -0,0 +1,11 @@ +precision mediump float; + +// Passed in from the vertex shader. +varying vec2 v_texcoord; + +// The texture. +uniform sampler2D u_texture; + +void main() { + gl_FragColor = texture2D(u_texture, v_texcoord); +} \ No newline at end of file diff --git a/src/engine/Graphics/Context/shaders/screen-vertex.glsl b/src/engine/Graphics/Context/shaders/screen-vertex.glsl new file mode 100644 index 00000000..f5cd6a50 --- /dev/null +++ b/src/engine/Graphics/Context/shaders/screen-vertex.glsl @@ -0,0 +1,12 @@ +attribute vec4 a_position; +attribute vec2 a_texcoord; + +varying vec2 v_texcoord; + +void main() { + // Multiply the position by the matrix. + gl_Position = a_position; + + // Pass the texcoord to the fragment shader. + v_texcoord = a_texcoord; +} \ No newline at end of file diff --git a/src/engine/Graphics/Context/texture-loader.ts b/src/engine/Graphics/Context/texture-loader.ts index 5f72343a..4f115a79 100644 --- a/src/engine/Graphics/Context/texture-loader.ts +++ b/src/engine/Graphics/Context/texture-loader.ts @@ -66,6 +66,7 @@ export class TextureLoader { gl.bindTexture(gl.TEXTURE_2D, tex); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); 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); diff --git a/src/engine/Graphics/PostProcessor/ColorBlindnessMode.ts b/src/engine/Graphics/PostProcessor/ColorBlindnessMode.ts new file mode 100644 index 00000000..60da6b31 --- /dev/null +++ b/src/engine/Graphics/PostProcessor/ColorBlindnessMode.ts @@ -0,0 +1,7 @@ + + +export enum ColorBlindnessMode { + Protanope = 'Protanope', + Deuteranope = 'Deuteranope', + Tritanope = 'Tritanope' +} diff --git a/src/engine/Graphics/PostProcessor/ColorBlindnessPostProcessor.ts b/src/engine/Graphics/PostProcessor/ColorBlindnessPostProcessor.ts new file mode 100644 index 00000000..5eafc41c --- /dev/null +++ b/src/engine/Graphics/PostProcessor/ColorBlindnessPostProcessor.ts @@ -0,0 +1,49 @@ +import screenVertexSource from '../Context/shaders/screen-vertex.glsl'; +import { Shader } from '../Context/shader'; +import colorBlindCorrectSource from './color-blind-fragment.glsl'; +import { PostProcessor } from './PostProcessor'; +import { ColorBlindnessMode } from './ColorBlindnessMode'; + +export class ColorBlindnessPostProcessor implements PostProcessor { + private _shader: Shader; + private _simulate = false; + constructor(private _colorBlindnessMode: ColorBlindnessMode, simulate = false) { + this._simulate = simulate; + } + + intialize(gl: WebGLRenderingContext): void { + this._shader = new Shader(gl, screenVertexSource, colorBlindCorrectSource); + this._shader.addAttribute('a_position', 2, gl.FLOAT); + this._shader.addAttribute('a_texcoord', 2, gl.FLOAT); + this.simulate = this._simulate; + this.colorBlindnessMode = this._colorBlindnessMode; + } + + getShader(): Shader { + return this._shader; + } + + set colorBlindnessMode(colorBlindMode: ColorBlindnessMode) { + if (this._colorBlindnessMode === ColorBlindnessMode.Protanope) { + this._shader.addUniformInt('u_type', 0); + } else if (this._colorBlindnessMode === ColorBlindnessMode.Deuteranope) { + this._shader.addUniformInt('u_type', 1); + } else if (this._colorBlindnessMode === ColorBlindnessMode.Tritanope) { + this._shader.addUniformInt('u_type', 2); + } + this._colorBlindnessMode = colorBlindMode; + } + + get colorBlindnessMode(): ColorBlindnessMode { + return this._colorBlindnessMode; + } + + set simulate(value: boolean) { + this._simulate = value; + this._shader.addUniformBool('u_simulate', value); + } + + get simulate(): boolean { + return this._simulate; + } +} diff --git a/src/engine/Graphics/PostProcessor/PostProcessor.ts b/src/engine/Graphics/PostProcessor/PostProcessor.ts new file mode 100644 index 00000000..91377f16 --- /dev/null +++ b/src/engine/Graphics/PostProcessor/PostProcessor.ts @@ -0,0 +1,8 @@ + +import { Shader } from '../Context/shader'; + + +export interface PostProcessor { + intialize(gl: WebGLRenderingContext): void; + getShader(): Shader; +} diff --git a/src/engine/Graphics/PostProcessor/color-blind-fragment.glsl b/src/engine/Graphics/PostProcessor/color-blind-fragment.glsl new file mode 100644 index 00000000..bd4c5139 --- /dev/null +++ b/src/engine/Graphics/PostProcessor/color-blind-fragment.glsl @@ -0,0 +1,62 @@ +precision mediump float; +// our texture +uniform sampler2D u_image; +// the texCoords passed in from the vertex shader. +varying vec2 v_texcoord; + +// color blind type +uniform int u_type; + +// simulation? +uniform bool u_simulate; + +void main() { + vec4 o = texture2D(u_image, v_texcoord); + // RGB to LMS matrix conversion + float L = (17.8824 * o.r) + (43.5161 * o.g) + (4.11935 * o.b); + float M = (3.45565 * o.r) + (27.1554 * o.g) + (3.86714 * o.b); + float S = (0.0299566 * o.r) + (0.184309 * o.g) + (1.46709 * o.b); + // Simulate color blindness + float l; + float m; + float s; + //MODE CODE// + if (u_type == 0) { + // Protanope + l = 0.0 * L + 2.02344 * M + -2.52581 * S; + m = 0.0 * L + 1.0 * M + 0.0 * S; + s = 0.0 * L + 0.0 * M + 1.0 * S;; + } else if (u_type == 1) { + // Deuteranope + l = 1.0 * L + 0.0 * M + 0.0 * S; + m = 0.494207 * L + 0.0 * M + 1.24827 * S; + s = 0.0 * L + 0.0 * M + 1.0 * S; + } else if (u_type == 2) { + // Tritanope + l = 1.0 * L + 0.0 * M + 0.0 * S; + m = 0.0 * L + 1.0 * M + 0.0 * S; + s = -0.395913 * L + 0.801109 * M + 0.0 * S; + } + + // LMS to RGB matrix conversion + vec4 error; // simulate the colors + error.r = (0.0809444479 * l) + (-0.130504409 * m) + (0.116721066 * s); + error.g = (-0.0102485335 * l) + (0.0540193266 * m) + (-0.113614708 * s); + error.b = (-0.000365296938 * l) + (-0.00412161469 * m) + (0.693511405 * s); + error.a = 1.0; + vec4 diff = o - error; + vec4 correction; // correct the colors + correction.r = 0.0; + correction.g = (diff.r * 0.7) + (diff.g * 1.0); + correction.b = (diff.r * 0.7) + (diff.b * 1.0); + correction = o + correction; + correction.a = o.a; + //SIMULATE// + + // sim + if (u_simulate) { + gl_FragColor = error.rgba; + } else { + gl_FragColor = correction.rgba; + } +} \ No newline at end of file diff --git a/src/engine/Graphics/index.ts b/src/engine/Graphics/index.ts index b2c41cb2..1ed1873d 100644 --- a/src/engine/Graphics/index.ts +++ b/src/engine/Graphics/index.ts @@ -26,3 +26,8 @@ export * from './Context/ExcaliburGraphicsContext2DCanvas'; export * from './Context/ExcaliburGraphicsContextWebGL'; export * from './Context/debug-text'; + +// Post Processor +export * from './PostProcessor/PostProcessor'; +export * from './PostProcessor/ColorBlindnessMode'; +export * from './PostProcessor/ColorBlindnessPostProcessor'; diff --git a/src/engine/PostProcessing/ColorBlindCorrector.ts b/src/engine/PostProcessing/ColorBlindCorrector.ts deleted file mode 100644 index d6422a3f..00000000 --- a/src/engine/PostProcessing/ColorBlindCorrector.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { Logger } from './../Util/Log'; -import { PostProcessor } from './PostProcessor'; -import { Engine } from '../Engine'; - -export enum ColorBlindness { - Protanope, - Deuteranope, - Tritanope -} - -/** - * This post processor can correct colors and simulate color blindness. - * It is possible to use this on every game, but the game's performance - * will suffer measurably. It's better to use it as a helpful tool while developing your game. - * Remember, the best practice is to design with color blindness in mind. - */ -export class ColorBlindCorrector implements PostProcessor { - /*eslint-disable */ - private _vertexShader = - '' + - 'attribute vec2 a_position;' + - 'attribute vec2 a_texCoord;' + - 'uniform vec2 u_resolution;' + - 'varying vec2 v_texCoord;' + - 'void main() {' + - // convert the rectangle from pixels to 0.0 to 1.0 - 'vec2 zeroToOne = a_position / u_resolution;' + - // convert from 0->1 to 0->2 - 'vec2 zeroToTwo = zeroToOne * 2.0;' + - // convert from 0->2 to -1->+1 (clipspace) - 'vec2 clipSpace = zeroToTwo - 1.0;' + - 'gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);' + - // pass the texCoord to the fragment shader - // The GPU will interpolate this value between points. - 'v_texCoord = a_texCoord;' + - '}'; - - private _fragmentShader = - 'precision mediump float;' + - // our texture - 'uniform sampler2D u_image;' + - // the texCoords passed in from the vertex shader. - 'varying vec2 v_texCoord;' + - // Color blind conversions - /*'mat3 m[9] =' + - '{' + - 'mat3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ),' + // normal - 'mat3(0.567, 0.433, 0.0, 0.558, 0.442, 0.0, 0.0, 0.242, 0.758),' + // protanopia - 'mat3(0.817, 0.183, 0.0, 0.333, 0.667, 0.0, 0.0, 0.125,0.875),' + // protanomaly - 'mat3(0.625, 0.375, 0.0, 0.7, 0.3, 0.0, 0.0, 0.3,0.7 ),' + // deuteranopia - 'mat3(0.8, 0.2, 0.0, 0.258, 0.742, 0.0, 0.0, 0.142,0.858),' + // deuteranomaly - 'mat3(0.95, 0.05, 0.0, 0.0, 0.433, 0.567, 0.0, 0.475,0.525),' + // tritanopia - 'mat3(0.967, 0.033, 0.0, 0.0, 0.733, 0.267, 0.0, 0.183,0.817),' + // tritanomaly - 'mat3(0.299, 0.587, 0.114, 0.299, 0.587, 0.114, 0.299, 0.587,0.114),' + // achromatopsia - 'mat3(0.618, 0.320, 0.062, 0.163, 0.775, 0.062, 0.163, 0.320,0.516)' + // achromatomaly - '};' +*/ - - 'void main() {' + - 'vec4 o = texture2D(u_image, v_texCoord);' + - // RGB to LMS matrix conversion - 'float L = (17.8824 * o.r) + (43.5161 * o.g) + (4.11935 * o.b);' + - 'float M = (3.45565 * o.r) + (27.1554 * o.g) + (3.86714 * o.b);' + - 'float S = (0.0299566 * o.r) + (0.184309 * o.g) + (1.46709 * o.b);' + - // Simulate color blindness - - '//MODE CODE//' + - /* Deuteranope for testing - 'float l = 1.0 * L + 0.0 * M + 0.0 * S;' + - 'float m = 0.494207 * L + 0.0 * M + 1.24827 * S;' + - 'float s = 0.0 * L + 0.0 * M + 1.0 * S;' +*/ - - // LMS to RGB matrix conversion - 'vec4 error;' + - 'error.r = (0.0809444479 * l) + (-0.130504409 * m) + (0.116721066 * s);' + - 'error.g = (-0.0102485335 * l) + (0.0540193266 * m) + (-0.113614708 * s);' + - 'error.b = (-0.000365296938 * l) + (-0.00412161469 * m) + (0.693511405 * s);' + - 'error.a = 1.0;' + - 'vec4 diff = o - error;' + - 'vec4 correction;' + - 'correction.r = 0.0;' + - 'correction.g = (diff.r * 0.7) + (diff.g * 1.0);' + - 'correction.b = (diff.r * 0.7) + (diff.b * 1.0);' + - 'correction = o + correction;' + - 'correction.a = o.a;' + - '//SIMULATE//' + - '}'; - - /*eslint-enable */ - private _internalCanvas: HTMLCanvasElement; - private _gl: WebGLRenderingContext; - private _program: WebGLProgram; - - constructor(public engine: Engine, public simulate: boolean = false, public colorMode: ColorBlindness = ColorBlindness.Protanope) { - this._internalCanvas = document.createElement('canvas'); - this._internalCanvas.width = engine.drawWidth; - this._internalCanvas.height = engine.drawHeight; - - // eslint-disable-next-line - this._gl = this._internalCanvas.getContext('webgl', { preserveDrawingBuffer: true }); - - this._program = this._gl.createProgram(); - const fragmentShader = this._getShader('Fragment', this._getFragmentShaderByMode(colorMode)); - const vertextShader = this._getShader('Vertex', this._vertexShader); - - this._gl.attachShader(this._program, vertextShader); - this._gl.attachShader(this._program, fragmentShader); - this._gl.linkProgram(this._program); - - if (!this._gl.getProgramParameter(this._program, this._gl.LINK_STATUS)) { - Logger.getInstance().error('Unable to link shader program!'); - } - - this._gl.useProgram(this._program); - } - - private _getFragmentShaderByMode(colorMode: ColorBlindness) { - let code = ''; - if (colorMode === ColorBlindness.Protanope) { - code = - 'float l = 0.0 * L + 2.02344 * M + -2.52581 * S;' + - 'float m = 0.0 * L + 1.0 * M + 0.0 * S;' + - 'float s = 0.0 * L + 0.0 * M + 1.0 * S;'; - } else if (colorMode === ColorBlindness.Deuteranope) { - code = - 'float l = 1.0 * L + 0.0 * M + 0.0 * S;' + - 'float m = 0.494207 * L + 0.0 * M + 1.24827 * S;' + - 'float s = 0.0 * L + 0.0 * M + 1.0 * S;'; - } else if (colorMode === ColorBlindness.Tritanope) { - code = - 'float l = 1.0 * L + 0.0 * M + 0.0 * S;' + - 'float m = 0.0 * L + 1.0 * M + 0.0 * S;' + - 'float s = -0.395913 * L + 0.801109 * M + 0.0 * S;'; - } - - if (this.simulate) { - this._fragmentShader = this._fragmentShader.replace('//SIMULATE//', 'gl_FragColor = error.rgba;'); - } else { - this._fragmentShader = this._fragmentShader.replace('//SIMULATE//', 'gl_FragColor = correction.rgba;'); - } - - return this._fragmentShader.replace('//MODE CODE//', code); - } - - private _setRectangle(x: number, y: number, width: number, height: number) { - const x1 = x; - const x2 = x + width; - const y1 = y; - const y2 = y + height; - this._gl.bufferData(this._gl.ARRAY_BUFFER, new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]), this._gl.STATIC_DRAW); - } - - private _getShader(type: string, program: string): WebGLShader { - let shader: WebGLShader; - if (type === 'Fragment') { - shader = this._gl.createShader(this._gl.FRAGMENT_SHADER); - } else if (type === 'Vertex') { - shader = this._gl.createShader(this._gl.VERTEX_SHADER); - } else { - Logger.getInstance().error('Error unknown shader type', type); - } - - this._gl.shaderSource(shader, program); - this._gl.compileShader(shader); - - if (!this._gl.getShaderParameter(shader, this._gl.COMPILE_STATUS)) { - Logger.getInstance().error('Unable to compile shader!', this._gl.getShaderInfoLog(shader)); - return null; - } - - return shader; - } - - public process(image: ImageData, out: CanvasRenderingContext2D) { - // look up where the vertex data needs to go. - const positionLocation = this._gl.getAttribLocation(this._program, 'a_position'); - const texCoordLocation = this._gl.getAttribLocation(this._program, 'a_texCoord'); - - const texCoordBuffer = this._gl.createBuffer(); - this._gl.bindBuffer(this._gl.ARRAY_BUFFER, texCoordBuffer); - this._gl.bufferData( - this._gl.ARRAY_BUFFER, - new Float32Array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0]), - this._gl.STATIC_DRAW - ); - this._gl.enableVertexAttribArray(texCoordLocation); - this._gl.vertexAttribPointer(texCoordLocation, 2, this._gl.FLOAT, false, 0, 0); - - // Create a texture. - const texture = this._gl.createTexture(); - this._gl.bindTexture(this._gl.TEXTURE_2D, texture); - - // Set the parameters so we can render any size image. - this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_S, this._gl.CLAMP_TO_EDGE); - this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_WRAP_T, this._gl.CLAMP_TO_EDGE); - this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MIN_FILTER, this._gl.NEAREST); - this._gl.texParameteri(this._gl.TEXTURE_2D, this._gl.TEXTURE_MAG_FILTER, this._gl.NEAREST); - // Flip the texture when unpacking into the gl context, gl reads textures in the opposite order as everything else :/ - this._gl.pixelStorei(this._gl.UNPACK_FLIP_Y_WEBGL, 1); - // Upload the image into the texture. - this._gl.texImage2D(this._gl.TEXTURE_2D, 0, this._gl.RGBA, this._gl.RGBA, this._gl.UNSIGNED_BYTE, image); - - // lookup uniforms - const resolutionLocation = this._gl.getUniformLocation(this._program, 'u_resolution'); - - // set the resolution - this._gl.uniform2f(resolutionLocation, this._internalCanvas.width, this._internalCanvas.height); - - // Create a buffer for the position of the rectangle corners. - const positionBuffer = this._gl.createBuffer(); - this._gl.bindBuffer(this._gl.ARRAY_BUFFER, positionBuffer); - this._gl.enableVertexAttribArray(positionLocation); - this._gl.vertexAttribPointer(positionLocation, 2, this._gl.FLOAT, false, 0, 0); - - // Set a rectangle the same size as the image. - this._setRectangle(0, 0, image.width, image.height); - - // Draw the rectangle. - this._gl.drawArrays(this._gl.TRIANGLES, 0, 6); - - // Grab transformed image from internal canvas - const pixelData = new Uint8Array(image.width * image.height * 4); - this._gl.readPixels(0, 0, image.width, image.height, this._gl.RGBA, this._gl.UNSIGNED_BYTE, pixelData); - - (image.data).set(pixelData); - - out.putImageData(image, 0, 0); - } -} diff --git a/src/engine/PostProcessing/Index.ts b/src/engine/PostProcessing/Index.ts deleted file mode 100644 index a79efd8c..00000000 --- a/src/engine/PostProcessing/Index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ColorBlindCorrector'; -export * from './PostProcessor'; diff --git a/src/engine/PostProcessing/PostProcessor.ts b/src/engine/PostProcessing/PostProcessor.ts deleted file mode 100644 index b4d40c2f..00000000 --- a/src/engine/PostProcessing/PostProcessor.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Adds post processing support for the engine, can process raw pixel data and manipulate canvas directly. - */ -export interface PostProcessor { - process(image: ImageData, out: CanvasRenderingContext2D): void; -} diff --git a/src/engine/index.ts b/src/engine/index.ts index 6323b6f4..b47e4277 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -39,7 +39,6 @@ export * from './Collision/Index'; import * as legacyDrawing from './Drawing/Index'; export { legacyDrawing as LegacyDrawing }; export * from './Interfaces/Index'; -export * from './PostProcessing/Index'; export * from './Resources/Index'; export * from './EntityComponentSystem/index'; diff --git a/src/spec/ColorBlindCorrectorSpec.ts b/src/spec/ColorBlindCorrectorSpec.ts index a3792eff..2d2ba93c 100644 --- a/src/spec/ColorBlindCorrectorSpec.ts +++ b/src/spec/ColorBlindCorrectorSpec.ts @@ -1,18 +1,29 @@ import * as ex from '@excalibur'; import { TestUtils } from './util/TestUtils'; import { ExcaliburMatchers, ensureImagesLoaded } from 'excalibur-jasmine'; -import { ColorBlindness } from '@excalibur'; + +/** + * + */ +function flushWebGLCanvasTo2D(source: HTMLCanvasElement): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.width = source.width; + canvas.height = source.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(source, 0, 0); + return canvas; +} describe('A ColorBlindCorrector', () => { - let bg: ex.LegacyDrawing.Texture; + let bg: ex.ImageSource; let engine: ex.Engine; let clock: ex.TestClock; beforeEach(async () => { jasmine.addMatchers(ExcaliburMatchers); - engine = TestUtils.engine({ width: 800, height: 200 }); - bg = new ex.LegacyDrawing.Texture('src/spec/images/ColorBlindCorrectorSpec/actor.png', true); + engine = TestUtils.engine({ width: 800, height: 200 }, []); + bg = new ex.ImageSource('src/spec/images/ColorBlindCorrectorSpec/actor.png'); const loader = new ex.Loader([bg]); clock = engine.clock as ex.TestClock; await TestUtils.runToReady(engine, loader); @@ -24,10 +35,11 @@ describe('A ColorBlindCorrector', () => { it('is normal', (done) => { const actor = new ex.Actor(); - actor.addDrawing(bg); + actor.graphics.use(bg.toSprite()); engine.add(actor); engine.once('postdraw', (ev: ex.PostDrawEvent) => { - ensureImagesLoaded(engine.canvas, 'src/spec/images/ColorBlindCorrectorSpec/normal.png').then(([canvas, image]) => { + ensureImagesLoaded(flushWebGLCanvasTo2D(engine.canvas), + 'src/spec/images/ColorBlindCorrectorSpec/normal.png').then(([canvas, image]) => { expect(canvas).toEqualImage(image); done(); }); @@ -37,11 +49,12 @@ describe('A ColorBlindCorrector', () => { it('corrects deuteranopia', (done) => { const actor = new ex.Actor(); - actor.addDrawing(bg); + actor.graphics.use(bg.toSprite()); engine.add(actor); - engine.debug.colorBlindMode.correct(ColorBlindness.Deuteranope); + engine.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Deuteranope); engine.once('postdraw', (ev: ex.PostDrawEvent) => { - ensureImagesLoaded(engine.canvas, 'src/spec/images/ColorBlindCorrectorSpec/deuteranope_correct.png').then(([canvas, image]) => { + ensureImagesLoaded(flushWebGLCanvasTo2D(engine.canvas), + 'src/spec/images/ColorBlindCorrectorSpec/deuteranope_correct.png').then(([canvas, image]) => { expect(canvas).toEqualImage(image); done(); }); @@ -51,11 +64,12 @@ describe('A ColorBlindCorrector', () => { it('simulates deuteranopia', (done) => { const actor = new ex.Actor(); - actor.addDrawing(bg); + actor.graphics.use(bg.toSprite()); engine.add(actor); - engine.debug.colorBlindMode.simulate(ColorBlindness.Deuteranope); + engine.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Deuteranope); engine.once('postdraw', (ev: ex.PostDrawEvent) => { - ensureImagesLoaded(engine.canvas, 'src/spec/images/ColorBlindCorrectorSpec/deuteranope_simulate.png').then(([canvas, image]) => { + ensureImagesLoaded(flushWebGLCanvasTo2D(engine.canvas), + 'src/spec/images/ColorBlindCorrectorSpec/deuteranope_simulate.png').then(([canvas, image]) => { expect(canvas).toEqualImage(image); done(); }); @@ -65,11 +79,12 @@ describe('A ColorBlindCorrector', () => { it('corrects protanopia', (done) => { const actor = new ex.Actor(); - actor.addDrawing(bg); + actor.graphics.use(bg.toSprite()); engine.add(actor); - engine.debug.colorBlindMode.correct(ColorBlindness.Protanope); + engine.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Protanope); engine.once('postdraw', (ev: ex.PostDrawEvent) => { - ensureImagesLoaded(engine.canvas, 'src/spec/images/ColorBlindCorrectorSpec/protanope_correct.png').then(([canvas, image]) => { + ensureImagesLoaded(flushWebGLCanvasTo2D(engine.canvas), + 'src/spec/images/ColorBlindCorrectorSpec/protanope_correct.png').then(([canvas, image]) => { expect(canvas).toEqualImage(image); done(); }); @@ -79,11 +94,12 @@ describe('A ColorBlindCorrector', () => { it('simulates protanopia', (done) => { const actor = new ex.Actor(); - actor.addDrawing(bg); + actor.graphics.use(bg.toSprite()); engine.add(actor); - engine.debug.colorBlindMode.simulate(ColorBlindness.Protanope); + engine.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Protanope); engine.once('postdraw', (ev: ex.PostDrawEvent) => { - ensureImagesLoaded(engine.canvas, 'src/spec/images/ColorBlindCorrectorSpec/protanope_simulate.png').then(([canvas, image]) => { + ensureImagesLoaded(flushWebGLCanvasTo2D(engine.canvas), + 'src/spec/images/ColorBlindCorrectorSpec/protanope_simulate.png').then(([canvas, image]) => { expect(canvas).toEqualImage(image); done(); }); @@ -93,11 +109,12 @@ describe('A ColorBlindCorrector', () => { it('corrects tritanopia', (done) => { const actor = new ex.Actor(); - actor.addDrawing(bg); + actor.graphics.use(bg.toSprite()); engine.add(actor); - engine.debug.colorBlindMode.correct(ColorBlindness.Tritanope); + engine.debug.colorBlindMode.correct(ex.ColorBlindnessMode.Tritanope); engine.once('postdraw', (ev: ex.PostDrawEvent) => { - ensureImagesLoaded(engine.canvas, 'src/spec/images/ColorBlindCorrectorSpec/tritanope_correct.png').then(([canvas, image]) => { + ensureImagesLoaded(flushWebGLCanvasTo2D(engine.canvas), + 'src/spec/images/ColorBlindCorrectorSpec/tritanope_correct.png').then(([canvas, image]) => { expect(canvas).toEqualImage(image); done(); }); @@ -107,11 +124,12 @@ describe('A ColorBlindCorrector', () => { it('simulates tritanopia', (done) => { const actor = new ex.Actor(); - actor.addDrawing(bg); + actor.graphics.use(bg.toSprite()); engine.add(actor); - engine.debug.colorBlindMode.simulate(ColorBlindness.Tritanope); + engine.debug.colorBlindMode.simulate(ex.ColorBlindnessMode.Tritanope); engine.once('postdraw', (ev: ex.PostDrawEvent) => { - ensureImagesLoaded(engine.canvas, 'src/spec/images/ColorBlindCorrectorSpec/tritanope_simulate.png').then(([canvas, image]) => { + ensureImagesLoaded(flushWebGLCanvasTo2D(engine.canvas), + 'src/spec/images/ColorBlindCorrectorSpec/tritanope_simulate.png').then(([canvas, image]) => { expect(canvas).toEqualImage(image); done(); }); diff --git a/src/spec/ParticleSpec.ts b/src/spec/ParticleSpec.ts index ac5ef24b..ba6f3c98 100644 --- a/src/spec/ParticleSpec.ts +++ b/src/spec/ParticleSpec.ts @@ -2,15 +2,6 @@ import { ExcaliburMatchers, ExcaliburAsyncMatchers } from 'excalibur-jasmine'; import * as ex from '@excalibur'; import { TestUtils } from './util/TestUtils'; -const drawWithTransform = (ctx: CanvasRenderingContext2D, actor: ex.Actor, delta: number = 1) => { - ctx.save(); - ctx.translate(actor.pos.x, actor.pos.y); - ctx.rotate(actor.rotation); - ctx.scale(actor.scale.x, actor.scale.y); - actor.draw(ctx, delta); - ctx.restore(); -}; - /** * */ @@ -26,7 +17,6 @@ function flushWebGLCanvasTo2D(source: HTMLCanvasElement): HTMLCanvasElement { describe('A particle', () => { let engine: ex.Engine; let scene: ex.Scene; - let texture: ex.LegacyDrawing.Texture; beforeEach(() => { jasmine.addMatchers(ExcaliburMatchers); jasmine.addAsyncMatchers(ExcaliburAsyncMatchers); @@ -34,9 +24,9 @@ describe('A particle', () => { { width: 800, height: 200, - backgroundColor: ex.Color.Transparent + backgroundColor: ex.Color.Black }, - ['use-webgl'] + [] ); scene = new ex.Scene(); engine.addScene('root', scene); @@ -45,7 +35,6 @@ describe('A particle', () => { const clock = engine.clock as ex.TestClock; clock.step(1); - texture = new ex.LegacyDrawing.Texture('src/spec/images/SpriteFontSpec/SpriteFont.png', true); }); afterEach(() => { engine.stop(); diff --git a/src/spec/images/ParticleSpec/Particles.png b/src/spec/images/ParticleSpec/Particles.png index a8986d60bfea2a34fa6baef2da39902fac90678c..eb77885b9aec0c457adb3b432bc48210304e9454 100644 GIT binary patch literal 11580 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

3$^Z!>P6D-)^<}xF-GPlKGo=@w@Eigs0jsd4Cqi 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 literal 13508 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