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",
"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",

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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<void>;
private runtimeOverrides: Partial<RuntimeSettings> = {};
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);
}
}

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 { 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<Texture> = [];
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<DrawableDescriptor>,
private readonly canvas: HTMLCanvasElement,
private readonly descriptors: Array<DrawableDescriptor>,
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<StartupSettings>): Promise<void> {
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();

View file

@ -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.

View file

@ -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);
}
}

View file

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