diff --git a/package.json b/package.json index 44adbd8..b986b1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sdf-2d", - "version": "0.6.0", + "version": "0.6.1", "description": "Graphics framework for efficiently rendering 2D signed distance fields.", "keywords": [ "webgl", @@ -27,6 +27,11 @@ "url": "git://github.com/schmelczerandras/sdf-2d.git" }, "license": "ISC", + "exports": { + ".": { + "import": "./dist/main.js" + } + }, "scripts": { "start": "rm -rf lib/* && webpack --mode development -w", "build": "rm -rf lib/* docs/* && typedoc && webpack --mode production" @@ -40,7 +45,8 @@ "src/main.ts" ], "dependencies": { - "gl-matrix": "^3.3.0" + "gl-matrix": "^3.3.0", + "resize-observer-polyfill": "^1.5.1" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^3.10.1", diff --git a/src/graphics/graphics-library/frame-buffer/default-frame-buffer.ts b/src/graphics/graphics-library/frame-buffer/default-frame-buffer.ts index 7517d10..67e75b6 100644 --- a/src/graphics/graphics-library/frame-buffer/default-frame-buffer.ts +++ b/src/graphics/graphics-library/frame-buffer/default-frame-buffer.ts @@ -1,16 +1,17 @@ +import { ReadonlyVec2 } from 'gl-matrix'; import { UniversalRenderingContext } from '../universal-rendering-context'; import { FrameBuffer } from './frame-buffer'; /** @internal */ export class DefaultFrameBuffer extends FrameBuffer { - constructor(gl: UniversalRenderingContext) { + constructor(gl: UniversalRenderingContext, canvasSize: ReadonlyVec2) { super(gl); this.frameBuffer = null; - this.setSize(); + this.setSize(canvasSize); } - public setSize(): boolean { - const hasChanged = super.setSize(); + public setSize(canvasSize: ReadonlyVec2): boolean { + const hasChanged = super.setSize(canvasSize); if (hasChanged) { this.gl.canvas.width = this.size.x; diff --git a/src/graphics/graphics-library/frame-buffer/frame-buffer.ts b/src/graphics/graphics-library/frame-buffer/frame-buffer.ts index 44a9071..99cbf45 100644 --- a/src/graphics/graphics-library/frame-buffer/frame-buffer.ts +++ b/src/graphics/graphics-library/frame-buffer/frame-buffer.ts @@ -1,4 +1,4 @@ -import { vec2 } from 'gl-matrix'; +import { ReadonlyVec2, vec2 } from 'gl-matrix'; import { Texture } from '../texture/texture'; import { UniversalRenderingContext } from '../universal-rendering-context'; @@ -26,15 +26,12 @@ export abstract class FrameBuffer { this.gl.deleteFramebuffer(this.frameBuffer); } - public setSize(): boolean { + public setSize(canvasSize: ReadonlyVec2): boolean { const realToCssPixels = (this.enableHighDpiRendering ? devicePixelRatio : 1) * this.renderScale; - const canvasWidth = (this.gl.canvas as HTMLCanvasElement).clientWidth; - const canvasHeight = (this.gl.canvas as HTMLCanvasElement).clientHeight; - - const displayWidth = Math.floor(canvasWidth * realToCssPixels); - const displayHeight = Math.floor(canvasHeight * realToCssPixels); + const displayWidth = Math.floor(canvasSize.x * realToCssPixels); + const displayHeight = Math.floor(canvasSize.y * realToCssPixels); const oldSize = vec2.clone(this.getSize()); this.size = vec2.fromValues(displayWidth, displayHeight); diff --git a/src/graphics/graphics-library/frame-buffer/intermediate-frame-buffer.ts b/src/graphics/graphics-library/frame-buffer/intermediate-frame-buffer.ts index 1fcc988..0e4d483 100644 --- a/src/graphics/graphics-library/frame-buffer/intermediate-frame-buffer.ts +++ b/src/graphics/graphics-library/frame-buffer/intermediate-frame-buffer.ts @@ -1,3 +1,4 @@ +import { ReadonlyVec2 } from 'gl-matrix'; import { ColorTexture } from '../texture/color-texture'; import { DistanceTexture } from '../texture/distance-texture'; import { Texture } from '../texture/texture'; @@ -9,7 +10,7 @@ export class IntermediateFrameBuffer extends FrameBuffer { private distanceTexture?: DistanceTexture; private colorTexture: ColorTexture; - constructor(gl: UniversalRenderingContext) { + constructor(gl: UniversalRenderingContext, canvasSize: ReadonlyVec2) { super(gl); this.colorTexture = new ColorTexture(gl); @@ -21,7 +22,7 @@ export class IntermediateFrameBuffer extends FrameBuffer { this.frameBuffer = this.gl.createFramebuffer()!; this.configureFrameBuffer(); - this.setSize(); + this.setSize(canvasSize); } public destroy(): void { @@ -38,8 +39,8 @@ export class IntermediateFrameBuffer extends FrameBuffer { : [this.colorTexture]; } - public setSize(): boolean { - const hasChanged = super.setSize(); + public setSize(canvasSize: ReadonlyVec2): boolean { + const hasChanged = super.setSize(canvasSize); if (hasChanged) { this.colorTexture.setSize(this.size); diff --git a/src/graphics/rendering/renderer/context-aware-renderer.ts b/src/graphics/rendering/renderer/context-aware-renderer.ts index 1a67bdc..954191c 100644 --- a/src/graphics/rendering/renderer/context-aware-renderer.ts +++ b/src/graphics/rendering/renderer/context-aware-renderer.ts @@ -1,4 +1,4 @@ -import { vec2 } from 'gl-matrix'; +import { ReadonlyVec2, vec2 } from 'gl-matrix'; import { Drawable } from '../../../drawables/drawable'; import { DrawableDescriptor } from '../../../drawables/drawable-descriptor'; import { RuntimeSettings } from '../settings/runtime-settings'; @@ -14,8 +14,8 @@ export class ContextAwareRenderer implements Renderer { private readyPromise!: Promise; private runtimeOverrides: Partial = {}; private ignoreWebGL2?: boolean; - private previousViewAreaTopLeft?: vec2; - private previousViewAreaSize?: vec2; + private previousViewAreaTopLeft?: ReadonlyVec2; + private previousViewAreaSize?: ReadonlyVec2; private contextRestoredHandler = this.handleContextRestored.bind(this); private contextLostHandler = this.handleContextLost.bind(this); @@ -102,11 +102,11 @@ export class ContextAwareRenderer implements Renderer { } } - public get canvasSize(): vec2 { + public get canvasSize(): ReadonlyVec2 { return this.handle(() => this.renderer.canvasSize, vec2.create()); } - public get viewAreaSize(): vec2 { + public get viewAreaSize(): ReadonlyVec2 { return this.handle(() => this.renderer.viewAreaSize, vec2.create()); } @@ -114,7 +114,7 @@ export class ContextAwareRenderer implements Renderer { return this.handle(() => this.renderer.insights, null); } - public setViewArea(topLeft: vec2, size: vec2): void { + public setViewArea(topLeft: ReadonlyVec2, size: ReadonlyVec2): void { this.previousViewAreaTopLeft = topLeft; this.previousViewAreaSize = size; return this.handle(() => this.renderer.setViewArea(topLeft, size), undefined); @@ -133,14 +133,14 @@ export class ContextAwareRenderer implements Renderer { return this.handle(() => this.renderer.addDrawable(drawable), undefined); } - public displayToWorldCoordinates(displayCoordinates: vec2): vec2 { + public displayToWorldCoordinates(displayCoordinates: ReadonlyVec2): vec2 { return this.handle( () => this.renderer.displayToWorldCoordinates(displayCoordinates), vec2.create() ); } - public worldToDisplayCoordinates(worldCoordinates: vec2): vec2 { + public worldToDisplayCoordinates(worldCoordinates: ReadonlyVec2): vec2 { return this.handle( () => this.renderer.worldToDisplayCoordinates(worldCoordinates), vec2.create() @@ -159,9 +159,7 @@ export class ContextAwareRenderer implements Renderer { ); this.canvas.removeEventListener('webglcontextlost', this.contextLostHandler, false); - this.isRendererReady = false; - this.handle(() => this.renderer.destroy(), undefined); } } diff --git a/src/graphics/rendering/renderer/renderer-implementation.ts b/src/graphics/rendering/renderer/renderer-implementation.ts index d29081c..b8c3d7d 100644 --- a/src/graphics/rendering/renderer/renderer-implementation.ts +++ b/src/graphics/rendering/renderer/renderer-implementation.ts @@ -1,4 +1,5 @@ -import { vec2 } from 'gl-matrix'; +import { ReadonlyVec2, vec2 } from 'gl-matrix'; +import ResizeObserver from 'resize-observer-polyfill'; import { Drawable } from '../../../drawables/drawable'; import { DrawableDescriptor } from '../../../drawables/drawable-descriptor'; import { LightDrawable } from '../../../drawables/lights/light-drawable'; @@ -38,15 +39,16 @@ import { RendererInfo } from './renderer-info'; /** @internal */ export class RendererImplementation implements Renderer { private readonly gl: UniversalRenderingContext; + private readonly uniformsProvider: UniformsProvider; + private readonly distanceFieldFrameBuffer: IntermediateFrameBuffer; + private readonly distancePass: DistanceRenderPass; + private readonly lightingFrameBuffer: DefaultFrameBuffer; + private readonly lightsPass: LightsRenderPass; private stopwatch?: WebGlStopwatch; - private uniformsProvider: UniformsProvider; - private distanceFieldFrameBuffer: IntermediateFrameBuffer; - private distancePass: DistanceRenderPass; - private lightingFrameBuffer: DefaultFrameBuffer; - private lightsPass: LightsRenderPass; - private palette!: PaletteTexture; - private textures: Array = []; + private palette!: PaletteTexture; + private _canvasSize = vec2.create(); + private canvasResizeObserver!: ResizeObserver; private applyRuntimeSettings: { [key in keyof RuntimeSettings]: (value: any) => void; @@ -78,8 +80,8 @@ export class RendererImplementation implements Renderer { } constructor( - private canvas: HTMLCanvasElement, - private descriptors: Array, + private readonly canvas: HTMLCanvasElement, + private readonly descriptors: Array, ignoreWebGL2?: boolean ) { this.gl = getUniversalRenderingContext( @@ -89,17 +91,19 @@ export class RendererImplementation implements Renderer { this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); - this.distanceFieldFrameBuffer = new IntermediateFrameBuffer(this.gl); - this.lightingFrameBuffer = new DefaultFrameBuffer(this.gl); + this.applyCanvasResizeObserver(); + + this.distanceFieldFrameBuffer = new IntermediateFrameBuffer(this.gl, this.canvasSize); + this.lightingFrameBuffer = new DefaultFrameBuffer(this.gl, this.canvasSize); this.distancePass = new DistanceRenderPass(this.gl, this.distanceFieldFrameBuffer); this.lightsPass = new LightsRenderPass(this.gl, this.lightingFrameBuffer); - this.uniformsProvider = new UniformsProvider(this.gl); + this.uniformsProvider = new UniformsProvider(this); this.setViewArea( - vec2.fromValues(0, canvas.clientHeight), - vec2.fromValues(canvas.clientWidth, canvas.clientHeight) + vec2.fromValues(0, this.canvasSize.y), + vec2.fromValues(this.canvasSize.x, this.canvasSize.y) ); const hardwareInfo = getHardwareInfo(this.gl); @@ -107,6 +111,18 @@ export class RendererImplementation implements Renderer { this.gl.insights.vendor = hardwareInfo?.vendor; } + private applyCanvasResizeObserver() { + this.canvasResizeObserver = new ResizeObserver((e) => { + const entry = e[0]; + this._canvasSize = vec2.fromValues( + entry.contentRect.width, + entry.contentRect.height + ); + this.uniformsProvider.calculateScreenToWorldTransformations(); + }); + this.canvasResizeObserver.observe(this.canvas); + } + public async initialize(settingsOverrides: Partial): Promise { const settings = { ...defaultStartupSettings, @@ -208,8 +224,8 @@ export class RendererImplementation implements Renderer { } } - this.distanceFieldFrameBuffer.setSize(); - this.lightingFrameBuffer.setSize(); + this.distanceFieldFrameBuffer.setSize(this.canvasSize); + this.lightingFrameBuffer.setSize(this.canvasSize); const common = { // texture units @@ -238,28 +254,28 @@ export class RendererImplementation implements Renderer { } } - public displayToWorldCoordinates(displayCoordinates: vec2): vec2 { + public displayToWorldCoordinates(displayCoordinates: ReadonlyVec2): vec2 { return this.uniformsProvider.screenToWorldPosition(displayCoordinates); } - public worldToDisplayCoordinates(worldCoordinates: vec2): vec2 { + public worldToDisplayCoordinates(worldCoordinates: ReadonlyVec2): vec2 { return this.uniformsProvider.worldToDisplayCoordinates(worldCoordinates); } - public setViewArea(topLeft: vec2, size: vec2) { + public setViewArea(topLeft: ReadonlyVec2, size: ReadonlyVec2) { this.uniformsProvider.setViewArea(topLeft, size); } - public get viewAreaSize(): vec2 { + public get viewAreaSize(): ReadonlyVec2 { return this.uniformsProvider.getViewArea(); } - public get canvasSize(): vec2 { - const { width, height } = this.canvas.getBoundingClientRect(); - return vec2.fromValues(width, height); + public get canvasSize(): ReadonlyVec2 { + return this._canvasSize; } public destroy(): void { + this.canvasResizeObserver.disconnect(); this.distancePass.destroy(); this.lightsPass.destroy(); this.palette.destroy(); diff --git a/src/graphics/rendering/renderer/renderer.ts b/src/graphics/rendering/renderer/renderer.ts index 3b0d620..af8b180 100644 --- a/src/graphics/rendering/renderer/renderer.ts +++ b/src/graphics/rendering/renderer/renderer.ts @@ -1,4 +1,4 @@ -import { vec2 } from 'gl-matrix'; +import { ReadonlyVec2, vec2 } from 'gl-matrix'; import { Drawable } from '../../../drawables/drawable'; import { RuntimeSettings } from '../settings/runtime-settings'; import { RendererInfo } from './renderer-info'; @@ -10,16 +10,18 @@ import { RendererInfo } from './renderer-info'; */ export interface Renderer { /** - * Get the actual resolution of the canvas. + * Get the actual resolution of the canvas without triggering a reflow. + * + * A ResizeObserver is utilised fot achieving this. */ - readonly canvasSize: vec2; + readonly canvasSize: ReadonlyVec2; /** * Get the viewArea size set by the last `setViewArea`. * * By default, `canvasSize` is used for the view area size. */ - readonly viewAreaSize: vec2; + readonly viewAreaSize: ReadonlyVec2; /** * Set the camera transformation. @@ -28,7 +30,7 @@ export interface Renderer { * @param size need not be equal to the canvas size, though their aspect ratio * should be the same to avoid stretching. */ - setViewArea(topLeft: vec2, size: vec2): void; + setViewArea(topLeft: ReadonlyVec2, size: ReadonlyVec2): void; /** * The inverse of `worldToDisplayCoordinates`, returns the world coordinates @@ -41,7 +43,7 @@ export interface Renderer { * @param displayCoordinates The origin is in the display's top left corner. * Just as in mouse events' clientX and clientY. */ - displayToWorldCoordinates(displayCoordinates: vec2): vec2; + displayToWorldCoordinates(displayCoordinates: ReadonlyVec2): vec2; /** * The inverse of `displayToWorldCoordinates`, returns the screen space position @@ -52,7 +54,7 @@ export interface Renderer { * * @param worldCoordinates Coordinates used when drawing objects. */ - worldToDisplayCoordinates(worldCoordinates: vec2): vec2; + worldToDisplayCoordinates(worldCoordinates: ReadonlyVec2): vec2; /** * Patch the current runtime settings with new values. diff --git a/src/graphics/rendering/uniforms-provider.ts b/src/graphics/rendering/uniforms-provider.ts index 62e4044..f11dc71 100644 --- a/src/graphics/rendering/uniforms-provider.ts +++ b/src/graphics/rendering/uniforms-provider.ts @@ -1,5 +1,5 @@ -import { mat2d, vec2, vec3 } from 'gl-matrix'; -import { UniversalRenderingContext } from '../graphics-library/universal-rendering-context'; +import { mat2d, ReadonlyVec2, vec2, vec3 } from 'gl-matrix'; +import { Renderer } from './renderer/renderer'; /** @internal */ export class UniformsProvider { @@ -9,11 +9,13 @@ export class UniformsProvider { private scaleWorldLengthToNDC = 1; private transformWorldToNDC = mat2d.create(); private viewAreaBottomLeft = vec2.create(); - private worldAreaInView = vec2.create(); + private worldAreaInView: ReadonlyVec2 = vec2.create(); private squareToAspectRatio = vec2.create(); private uvToWorld = mat2d.create(); + private screenToWorldTransformation = mat2d.create(); + private worldToScreenTransformation = mat2d.create(); - public constructor(private readonly gl: UniversalRenderingContext) {} + public constructor(private readonly renderer: Renderer) {} public getUniforms(uniforms: any): any { return { @@ -29,11 +31,11 @@ export class UniformsProvider { }; } - public getViewArea(): vec2 { + public getViewArea(): ReadonlyVec2 { return this.worldAreaInView; } - public setViewArea(topLeft: vec2, size: vec2) { + public setViewArea(topLeft: ReadonlyVec2, size: ReadonlyVec2) { this.worldAreaInView = size; // world coordinates @@ -68,31 +70,43 @@ export class UniformsProvider { this.uvToWorld = mat2d.fromTranslation(mat2d.create(), this.viewAreaBottomLeft); mat2d.scale(this.uvToWorld, this.uvToWorld, this.worldAreaInView); + + this.calculateScreenToWorldTransformations(); } - public screenToWorldPosition(screenPosition: vec2): vec2 { - const transform = this.calculateScreenToWorldTransformation(); - return vec2.transformMat2d(vec2.create(), screenPosition, transform); + public screenToWorldPosition(screenPosition: ReadonlyVec2): vec2 { + return vec2.transformMat2d( + vec2.create(), + screenPosition, + this.screenToWorldTransformation + ); } - public worldToDisplayCoordinates(worldCoordinates: vec2): vec2 { - const transform = this.calculateScreenToWorldTransformation(); - mat2d.invert(transform, transform); - return vec2.transformMat2d(vec2.create(), worldCoordinates, transform); + public worldToDisplayCoordinates(worldCoordinates: ReadonlyVec2): vec2 { + return vec2.transformMat2d( + vec2.create(), + worldCoordinates, + this.worldToScreenTransformation + ); } - private calculateScreenToWorldTransformation(): mat2d { - const { width, height } = (this.gl - .canvas as HTMLCanvasElement).getBoundingClientRect(); + public calculateScreenToWorldTransformations() { + const width = this.renderer.canvasSize.x; + const height = this.renderer.canvasSize.y; const resolution = vec2.fromValues(width, height); - const transform = mat2d.fromTranslation(mat2d.create(), this.viewAreaBottomLeft); + const transform = mat2d.fromTranslation( + this.screenToWorldTransformation, + this.viewAreaBottomLeft + ); mat2d.scale( transform, transform, vec2.divide(vec2.create(), this.worldAreaInView, resolution) ); mat2d.translate(transform, transform, vec2.fromValues(0.5, height - 0.5)); - return mat2d.scale(transform, transform, vec2.fromValues(1, -1)); + mat2d.scale(transform, transform, vec2.fromValues(1, -1)); + + mat2d.invert(this.worldToScreenTransformation, this.screenToWorldTransformation); } } diff --git a/src/main.ts b/src/main.ts index 2112eb2..2dbdd0f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,11 @@ declare global { y: T; } + interface ReadonlyArray { + x: T; + y: T; + } + interface Float32Array { x: number; y: number;