Avoid triggering reflow on canvasSize querying

This commit is contained in:
schmelczerandras 2020-10-25 15:56:20 +01:00
parent 582a0cea08
commit 3fb6cc0411
9 changed files with 116 additions and 76 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "sdf-2d", "name": "sdf-2d",
"version": "0.6.0", "version": "0.6.1",
"description": "Graphics framework for efficiently rendering 2D signed distance fields.", "description": "Graphics framework for efficiently rendering 2D signed distance fields.",
"keywords": [ "keywords": [
"webgl", "webgl",
@ -27,6 +27,11 @@
"url": "git://github.com/schmelczerandras/sdf-2d.git" "url": "git://github.com/schmelczerandras/sdf-2d.git"
}, },
"license": "ISC", "license": "ISC",
"exports": {
".": {
"import": "./dist/main.js"
}
},
"scripts": { "scripts": {
"start": "rm -rf lib/* && webpack --mode development -w", "start": "rm -rf lib/* && webpack --mode development -w",
"build": "rm -rf lib/* docs/* && typedoc && webpack --mode production" "build": "rm -rf lib/* docs/* && typedoc && webpack --mode production"
@ -40,7 +45,8 @@
"src/main.ts" "src/main.ts"
], ],
"dependencies": { "dependencies": {
"gl-matrix": "^3.3.0" "gl-matrix": "^3.3.0",
"resize-observer-polyfill": "^1.5.1"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^3.10.1", "@typescript-eslint/eslint-plugin": "^3.10.1",

View file

@ -1,16 +1,17 @@
import { ReadonlyVec2 } from 'gl-matrix';
import { UniversalRenderingContext } from '../universal-rendering-context'; import { UniversalRenderingContext } from '../universal-rendering-context';
import { FrameBuffer } from './frame-buffer'; import { FrameBuffer } from './frame-buffer';
/** @internal */ /** @internal */
export class DefaultFrameBuffer extends FrameBuffer { export class DefaultFrameBuffer extends FrameBuffer {
constructor(gl: UniversalRenderingContext) { constructor(gl: UniversalRenderingContext, canvasSize: ReadonlyVec2) {
super(gl); super(gl);
this.frameBuffer = null; this.frameBuffer = null;
this.setSize(); this.setSize(canvasSize);
} }
public setSize(): boolean { public setSize(canvasSize: ReadonlyVec2): boolean {
const hasChanged = super.setSize(); const hasChanged = super.setSize(canvasSize);
if (hasChanged) { if (hasChanged) {
this.gl.canvas.width = this.size.x; this.gl.canvas.width = this.size.x;

View file

@ -1,4 +1,4 @@
import { vec2 } from 'gl-matrix'; import { ReadonlyVec2, vec2 } from 'gl-matrix';
import { Texture } from '../texture/texture'; import { Texture } from '../texture/texture';
import { UniversalRenderingContext } from '../universal-rendering-context'; import { UniversalRenderingContext } from '../universal-rendering-context';
@ -26,15 +26,12 @@ export abstract class FrameBuffer {
this.gl.deleteFramebuffer(this.frameBuffer); this.gl.deleteFramebuffer(this.frameBuffer);
} }
public setSize(): boolean { public setSize(canvasSize: ReadonlyVec2): boolean {
const realToCssPixels = const realToCssPixels =
(this.enableHighDpiRendering ? devicePixelRatio : 1) * this.renderScale; (this.enableHighDpiRendering ? devicePixelRatio : 1) * this.renderScale;
const canvasWidth = (this.gl.canvas as HTMLCanvasElement).clientWidth; const displayWidth = Math.floor(canvasSize.x * realToCssPixels);
const canvasHeight = (this.gl.canvas as HTMLCanvasElement).clientHeight; const displayHeight = Math.floor(canvasSize.y * realToCssPixels);
const displayWidth = Math.floor(canvasWidth * realToCssPixels);
const displayHeight = Math.floor(canvasHeight * realToCssPixels);
const oldSize = vec2.clone(this.getSize()); const oldSize = vec2.clone(this.getSize());
this.size = vec2.fromValues(displayWidth, displayHeight); this.size = vec2.fromValues(displayWidth, displayHeight);

View file

@ -1,3 +1,4 @@
import { ReadonlyVec2 } from 'gl-matrix';
import { ColorTexture } from '../texture/color-texture'; import { ColorTexture } from '../texture/color-texture';
import { DistanceTexture } from '../texture/distance-texture'; import { DistanceTexture } from '../texture/distance-texture';
import { Texture } from '../texture/texture'; import { Texture } from '../texture/texture';
@ -9,7 +10,7 @@ export class IntermediateFrameBuffer extends FrameBuffer {
private distanceTexture?: DistanceTexture; private distanceTexture?: DistanceTexture;
private colorTexture: ColorTexture; private colorTexture: ColorTexture;
constructor(gl: UniversalRenderingContext) { constructor(gl: UniversalRenderingContext, canvasSize: ReadonlyVec2) {
super(gl); super(gl);
this.colorTexture = new ColorTexture(gl); this.colorTexture = new ColorTexture(gl);
@ -21,7 +22,7 @@ export class IntermediateFrameBuffer extends FrameBuffer {
this.frameBuffer = this.gl.createFramebuffer()!; this.frameBuffer = this.gl.createFramebuffer()!;
this.configureFrameBuffer(); this.configureFrameBuffer();
this.setSize(); this.setSize(canvasSize);
} }
public destroy(): void { public destroy(): void {
@ -38,8 +39,8 @@ export class IntermediateFrameBuffer extends FrameBuffer {
: [this.colorTexture]; : [this.colorTexture];
} }
public setSize(): boolean { public setSize(canvasSize: ReadonlyVec2): boolean {
const hasChanged = super.setSize(); const hasChanged = super.setSize(canvasSize);
if (hasChanged) { if (hasChanged) {
this.colorTexture.setSize(this.size); this.colorTexture.setSize(this.size);

View file

@ -1,4 +1,4 @@
import { vec2 } from 'gl-matrix'; import { ReadonlyVec2, vec2 } from 'gl-matrix';
import { Drawable } from '../../../drawables/drawable'; import { Drawable } from '../../../drawables/drawable';
import { DrawableDescriptor } from '../../../drawables/drawable-descriptor'; import { DrawableDescriptor } from '../../../drawables/drawable-descriptor';
import { RuntimeSettings } from '../settings/runtime-settings'; import { RuntimeSettings } from '../settings/runtime-settings';
@ -14,8 +14,8 @@ export class ContextAwareRenderer implements Renderer {
private readyPromise!: Promise<void>; private readyPromise!: Promise<void>;
private runtimeOverrides: Partial<RuntimeSettings> = {}; private runtimeOverrides: Partial<RuntimeSettings> = {};
private ignoreWebGL2?: boolean; private ignoreWebGL2?: boolean;
private previousViewAreaTopLeft?: vec2; private previousViewAreaTopLeft?: ReadonlyVec2;
private previousViewAreaSize?: vec2; private previousViewAreaSize?: ReadonlyVec2;
private contextRestoredHandler = this.handleContextRestored.bind(this); private contextRestoredHandler = this.handleContextRestored.bind(this);
private contextLostHandler = this.handleContextLost.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()); return this.handle(() => this.renderer.canvasSize, vec2.create());
} }
public get viewAreaSize(): vec2 { public get viewAreaSize(): ReadonlyVec2 {
return this.handle(() => this.renderer.viewAreaSize, vec2.create()); return this.handle(() => this.renderer.viewAreaSize, vec2.create());
} }
@ -114,7 +114,7 @@ export class ContextAwareRenderer implements Renderer {
return this.handle(() => this.renderer.insights, null); 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.previousViewAreaTopLeft = topLeft;
this.previousViewAreaSize = size; this.previousViewAreaSize = size;
return this.handle(() => this.renderer.setViewArea(topLeft, size), undefined); 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); return this.handle(() => this.renderer.addDrawable(drawable), undefined);
} }
public displayToWorldCoordinates(displayCoordinates: vec2): vec2 { public displayToWorldCoordinates(displayCoordinates: ReadonlyVec2): vec2 {
return this.handle( return this.handle(
() => this.renderer.displayToWorldCoordinates(displayCoordinates), () => this.renderer.displayToWorldCoordinates(displayCoordinates),
vec2.create() vec2.create()
); );
} }
public worldToDisplayCoordinates(worldCoordinates: vec2): vec2 { public worldToDisplayCoordinates(worldCoordinates: ReadonlyVec2): vec2 {
return this.handle( return this.handle(
() => this.renderer.worldToDisplayCoordinates(worldCoordinates), () => this.renderer.worldToDisplayCoordinates(worldCoordinates),
vec2.create() vec2.create()
@ -159,9 +159,7 @@ export class ContextAwareRenderer implements Renderer {
); );
this.canvas.removeEventListener('webglcontextlost', this.contextLostHandler, false); this.canvas.removeEventListener('webglcontextlost', this.contextLostHandler, false);
this.isRendererReady = false; this.isRendererReady = false;
this.handle(() => this.renderer.destroy(), undefined); this.handle(() => this.renderer.destroy(), undefined);
} }
} }

View file

@ -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 { Drawable } from '../../../drawables/drawable';
import { DrawableDescriptor } from '../../../drawables/drawable-descriptor'; import { DrawableDescriptor } from '../../../drawables/drawable-descriptor';
import { LightDrawable } from '../../../drawables/lights/light-drawable'; import { LightDrawable } from '../../../drawables/lights/light-drawable';
@ -38,15 +39,16 @@ import { RendererInfo } from './renderer-info';
/** @internal */ /** @internal */
export class RendererImplementation implements Renderer { export class RendererImplementation implements Renderer {
private readonly gl: UniversalRenderingContext; 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 stopwatch?: WebGlStopwatch;
private uniformsProvider: UniformsProvider;
private distanceFieldFrameBuffer: IntermediateFrameBuffer;
private distancePass: DistanceRenderPass;
private lightingFrameBuffer: DefaultFrameBuffer;
private lightsPass: LightsRenderPass;
private palette!: PaletteTexture;
private textures: Array<Texture> = []; private textures: Array<Texture> = [];
private palette!: PaletteTexture;
private _canvasSize = vec2.create();
private canvasResizeObserver!: ResizeObserver;
private applyRuntimeSettings: { private applyRuntimeSettings: {
[key in keyof RuntimeSettings]: (value: any) => void; [key in keyof RuntimeSettings]: (value: any) => void;
@ -78,8 +80,8 @@ export class RendererImplementation implements Renderer {
} }
constructor( constructor(
private canvas: HTMLCanvasElement, private readonly canvas: HTMLCanvasElement,
private descriptors: Array<DrawableDescriptor>, private readonly descriptors: Array<DrawableDescriptor>,
ignoreWebGL2?: boolean ignoreWebGL2?: boolean
) { ) {
this.gl = getUniversalRenderingContext( this.gl = getUniversalRenderingContext(
@ -89,17 +91,19 @@ export class RendererImplementation implements Renderer {
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
this.distanceFieldFrameBuffer = new IntermediateFrameBuffer(this.gl); this.applyCanvasResizeObserver();
this.lightingFrameBuffer = new DefaultFrameBuffer(this.gl);
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.distancePass = new DistanceRenderPass(this.gl, this.distanceFieldFrameBuffer);
this.lightsPass = new LightsRenderPass(this.gl, this.lightingFrameBuffer); this.lightsPass = new LightsRenderPass(this.gl, this.lightingFrameBuffer);
this.uniformsProvider = new UniformsProvider(this.gl); this.uniformsProvider = new UniformsProvider(this);
this.setViewArea( this.setViewArea(
vec2.fromValues(0, canvas.clientHeight), vec2.fromValues(0, this.canvasSize.y),
vec2.fromValues(canvas.clientWidth, canvas.clientHeight) vec2.fromValues(this.canvasSize.x, this.canvasSize.y)
); );
const hardwareInfo = getHardwareInfo(this.gl); const hardwareInfo = getHardwareInfo(this.gl);
@ -107,6 +111,18 @@ export class RendererImplementation implements Renderer {
this.gl.insights.vendor = hardwareInfo?.vendor; 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<StartupSettings>): Promise<void> { public async initialize(settingsOverrides: Partial<StartupSettings>): Promise<void> {
const settings = { const settings = {
...defaultStartupSettings, ...defaultStartupSettings,
@ -208,8 +224,8 @@ export class RendererImplementation implements Renderer {
} }
} }
this.distanceFieldFrameBuffer.setSize(); this.distanceFieldFrameBuffer.setSize(this.canvasSize);
this.lightingFrameBuffer.setSize(); this.lightingFrameBuffer.setSize(this.canvasSize);
const common = { const common = {
// texture units // 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); return this.uniformsProvider.screenToWorldPosition(displayCoordinates);
} }
public worldToDisplayCoordinates(worldCoordinates: vec2): vec2 { public worldToDisplayCoordinates(worldCoordinates: ReadonlyVec2): vec2 {
return this.uniformsProvider.worldToDisplayCoordinates(worldCoordinates); return this.uniformsProvider.worldToDisplayCoordinates(worldCoordinates);
} }
public setViewArea(topLeft: vec2, size: vec2) { public setViewArea(topLeft: ReadonlyVec2, size: ReadonlyVec2) {
this.uniformsProvider.setViewArea(topLeft, size); this.uniformsProvider.setViewArea(topLeft, size);
} }
public get viewAreaSize(): vec2 { public get viewAreaSize(): ReadonlyVec2 {
return this.uniformsProvider.getViewArea(); return this.uniformsProvider.getViewArea();
} }
public get canvasSize(): vec2 { public get canvasSize(): ReadonlyVec2 {
const { width, height } = this.canvas.getBoundingClientRect(); return this._canvasSize;
return vec2.fromValues(width, height);
} }
public destroy(): void { public destroy(): void {
this.canvasResizeObserver.disconnect();
this.distancePass.destroy(); this.distancePass.destroy();
this.lightsPass.destroy(); this.lightsPass.destroy();
this.palette.destroy(); this.palette.destroy();

View file

@ -1,4 +1,4 @@
import { vec2 } from 'gl-matrix'; import { ReadonlyVec2, vec2 } from 'gl-matrix';
import { Drawable } from '../../../drawables/drawable'; import { Drawable } from '../../../drawables/drawable';
import { RuntimeSettings } from '../settings/runtime-settings'; import { RuntimeSettings } from '../settings/runtime-settings';
import { RendererInfo } from './renderer-info'; import { RendererInfo } from './renderer-info';
@ -10,16 +10,18 @@ import { RendererInfo } from './renderer-info';
*/ */
export interface Renderer { 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`. * Get the viewArea size set by the last `setViewArea`.
* *
* By default, `canvasSize` is used for the view area size. * By default, `canvasSize` is used for the view area size.
*/ */
readonly viewAreaSize: vec2; readonly viewAreaSize: ReadonlyVec2;
/** /**
* Set the camera transformation. * 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 * @param size need not be equal to the canvas size, though their aspect ratio
* should be the same to avoid stretching. * 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 * 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. * @param displayCoordinates The origin is in the display's top left corner.
* Just as in mouse events' clientX and clientY. * 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 * The inverse of `displayToWorldCoordinates`, returns the screen space position
@ -52,7 +54,7 @@ export interface Renderer {
* *
* @param worldCoordinates Coordinates used when drawing objects. * @param worldCoordinates Coordinates used when drawing objects.
*/ */
worldToDisplayCoordinates(worldCoordinates: vec2): vec2; worldToDisplayCoordinates(worldCoordinates: ReadonlyVec2): vec2;
/** /**
* Patch the current runtime settings with new values. * Patch the current runtime settings with new values.

View file

@ -1,5 +1,5 @@
import { mat2d, vec2, vec3 } from 'gl-matrix'; import { mat2d, ReadonlyVec2, vec2, vec3 } from 'gl-matrix';
import { UniversalRenderingContext } from '../graphics-library/universal-rendering-context'; import { Renderer } from './renderer/renderer';
/** @internal */ /** @internal */
export class UniformsProvider { export class UniformsProvider {
@ -9,11 +9,13 @@ export class UniformsProvider {
private scaleWorldLengthToNDC = 1; private scaleWorldLengthToNDC = 1;
private transformWorldToNDC = mat2d.create(); private transformWorldToNDC = mat2d.create();
private viewAreaBottomLeft = vec2.create(); private viewAreaBottomLeft = vec2.create();
private worldAreaInView = vec2.create(); private worldAreaInView: ReadonlyVec2 = vec2.create();
private squareToAspectRatio = vec2.create(); private squareToAspectRatio = vec2.create();
private uvToWorld = mat2d.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 { public getUniforms(uniforms: any): any {
return { return {
@ -29,11 +31,11 @@ export class UniformsProvider {
}; };
} }
public getViewArea(): vec2 { public getViewArea(): ReadonlyVec2 {
return this.worldAreaInView; return this.worldAreaInView;
} }
public setViewArea(topLeft: vec2, size: vec2) { public setViewArea(topLeft: ReadonlyVec2, size: ReadonlyVec2) {
this.worldAreaInView = size; this.worldAreaInView = size;
// world coordinates // world coordinates
@ -68,31 +70,43 @@ export class UniformsProvider {
this.uvToWorld = mat2d.fromTranslation(mat2d.create(), this.viewAreaBottomLeft); this.uvToWorld = mat2d.fromTranslation(mat2d.create(), this.viewAreaBottomLeft);
mat2d.scale(this.uvToWorld, this.uvToWorld, this.worldAreaInView); mat2d.scale(this.uvToWorld, this.uvToWorld, this.worldAreaInView);
this.calculateScreenToWorldTransformations();
} }
public screenToWorldPosition(screenPosition: vec2): vec2 { public screenToWorldPosition(screenPosition: ReadonlyVec2): vec2 {
const transform = this.calculateScreenToWorldTransformation(); return vec2.transformMat2d(
return vec2.transformMat2d(vec2.create(), screenPosition, transform); vec2.create(),
screenPosition,
this.screenToWorldTransformation
);
} }
public worldToDisplayCoordinates(worldCoordinates: vec2): vec2 { public worldToDisplayCoordinates(worldCoordinates: ReadonlyVec2): vec2 {
const transform = this.calculateScreenToWorldTransformation(); return vec2.transformMat2d(
mat2d.invert(transform, transform); vec2.create(),
return vec2.transformMat2d(vec2.create(), worldCoordinates, transform); worldCoordinates,
this.worldToScreenTransformation
);
} }
private calculateScreenToWorldTransformation(): mat2d { public calculateScreenToWorldTransformations() {
const { width, height } = (this.gl const width = this.renderer.canvasSize.x;
.canvas as HTMLCanvasElement).getBoundingClientRect(); const height = this.renderer.canvasSize.y;
const resolution = vec2.fromValues(width, height); const resolution = vec2.fromValues(width, height);
const transform = mat2d.fromTranslation(mat2d.create(), this.viewAreaBottomLeft); const transform = mat2d.fromTranslation(
this.screenToWorldTransformation,
this.viewAreaBottomLeft
);
mat2d.scale( mat2d.scale(
transform, transform,
transform, transform,
vec2.divide(vec2.create(), this.worldAreaInView, resolution) vec2.divide(vec2.create(), this.worldAreaInView, resolution)
); );
mat2d.translate(transform, transform, vec2.fromValues(0.5, height - 0.5)); 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);
} }
} }

View file

@ -14,6 +14,11 @@ declare global {
y: T; y: T;
} }
interface ReadonlyArray<T> {
x: T;
y: T;
}
interface Float32Array { interface Float32Array {
x: number; x: number;
y: number; y: number;