Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions app/src/room/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import { GLContext } from "./gl/context";

export class Canvas {
#canvas: HTMLCanvasElement;
#glContext: GLContext;
#camera: Camera;
#backgroundRenderer: BackgroundRenderer;
#glContext?: GLContext;
#camera?: Camera;
#backgroundRenderer?: BackgroundRenderer;

// True when a WebGL2 context was successfully created.
// Some browsers (or privacy configurations) disable WebGL entirely, in which
// case we degrade gracefully instead of crashing the whole app.
readonly supported: boolean;

// Use a callback to render after the background.
onRender?: (now: DOMHighResTimeStamp) => void;
Expand All @@ -24,14 +29,17 @@ export class Canvas {
}

get gl(): WebGL2RenderingContext {
if (!this.#glContext) throw new Error("WebGL2 not supported");
return this.#glContext.gl;
}

get glContext(): GLContext {
if (!this.#glContext) throw new Error("WebGL2 not supported");
return this.#glContext;
}

get camera() {
get camera(): Camera {
if (!this.#camera) throw new Error("WebGL2 not supported");
return this.#camera;
}

Expand All @@ -41,10 +49,17 @@ export class Canvas {
this.visible = new Signal(false);
this.viewport = new Signal(Vector.create(0, 0));

// Initialize WebGL2 context
this.#glContext = new GLContext(this.#canvas, this.viewport);
this.#camera = new Camera();
this.#backgroundRenderer = new BackgroundRenderer(this.#glContext);
// Initialize WebGL2 context. This can fail when WebGL is disabled by the
// browser or a privacy extension; in that case we keep the app running
// without the GL-rendered background instead of throwing.
try {
this.#glContext = new GLContext(this.#canvas, this.viewport);
this.#camera = new Camera();
this.#backgroundRenderer = new BackgroundRenderer(this.#glContext);
} catch (err) {
console.warn("WebGL2 unavailable, disabling canvas rendering", err);
}
this.supported = this.#glContext !== undefined;

const resize = (entries: ResizeObserverEntry[]) => {
for (const entry of entries) {
Expand All @@ -68,14 +83,14 @@ export class Canvas {
this.#canvas.height = newHeight;

// Update WebGL viewport
this.#glContext.resize(newWidth, newHeight);
this.#glContext?.resize(newWidth, newHeight);

// The internal logic ignores devicePixelRatio because we automatically scale when rendering.
const viewport = Vector.create(newWidth / dpr, newHeight / dpr);
this.viewport.set(viewport);

// Update camera projection
this.#camera.updateOrtho(viewport);
this.#camera?.updateOrtho(viewport);

// Render immediately to avoid black flicker during resize
if (this.visible.peek()) {
Expand Down Expand Up @@ -108,8 +123,10 @@ export class Canvas {
resizeObserver.disconnect();
});

// Only render the canvas when it's visible.
// Only render the canvas when it's visible (and WebGL is available).
this.#signals.effect((effect) => {
if (!this.#glContext) return;

const visible = effect.get(this.visible);
if (!visible) return;

Expand All @@ -130,6 +147,8 @@ export class Canvas {
}

#render(now: DOMHighResTimeStamp) {
if (!this.#glContext || !this.#backgroundRenderer) return;

// Clear the screen
this.#glContext.clear();

Expand Down Expand Up @@ -169,6 +188,6 @@ export class Canvas {

close() {
this.#signals.close();
this.#backgroundRenderer.cleanup();
this.#backgroundRenderer?.cleanup();
}
}
17 changes: 16 additions & 1 deletion app/src/sup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,26 @@ export function Sup(props: { canvas: Canvas; room: string }): JSX.Element {

return (
<Show when={publish()} fallback={<Preview room={props.room} local={local} connection={connection} />}>
<App connection={connection} canvas={props.canvas} room={props.room} sound={sound} local={local} />
<Show when={props.canvas.supported} fallback={<WebGLUnsupported />}>
<App connection={connection} canvas={props.canvas} room={props.room} sound={sound} local={local} />
</Show>
</Show>
);
}

function WebGLUnsupported(): JSX.Element {
return (
<WebLayout>
<h1 class="text-2xl font-bold mb-4">WebGL is required</h1>
<p class="mb-4">
This room needs WebGL to render video, but your browser has it disabled. Some browsers and privacy
extensions block WebGL by default to prevent fingerprinting.
</p>
<p>Please enable WebGL (for this site) and reload the page.</p>
</WebLayout>
);
}

function App(props: {
connection: Moq.Connection.Reload;
canvas: Canvas;
Expand Down
Loading