Avoid triggering reflow on canvasSize querying
This commit is contained in:
parent
582a0cea08
commit
3fb6cc0411
9 changed files with 116 additions and 76 deletions
10
package.json
10
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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ declare global {
|
|||
y: T;
|
||||
}
|
||||
|
||||
interface ReadonlyArray<T> {
|
||||
x: T;
|
||||
y: T;
|
||||
}
|
||||
|
||||
interface Float32Array {
|
||||
x: number;
|
||||
y: number;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue