Skip to content

Commit

Permalink
Display settings dialog (#301)
Browse files Browse the repository at this point in the history
The dialog allows changing the canvas pixel size and hiding the canvas.

The intent is to give people on low-end GPUs a way to improve frame
rates if the current Hydra shader renders too slowly, or a way to hide
it entirely if it's distracting.

(The thread that inspried this:
https://post.lurk.org/@yaxu/113554740489867054)
  • Loading branch information
munshkr authored Nov 29, 2024
2 parents 3ece419 + 4535ae7 commit 6a10b2e
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 8 deletions.
86 changes: 86 additions & 0 deletions packages/web/src/components/display-settings-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogFooter,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { DialogProps } from "@radix-ui/react-dialog";
import { useState, FormEvent } from "react";
import {
DisplaySettings,
sanitizeDisplaySettings,
} from "@/lib/display-settings";

interface DisplaySettingsDialogProps extends DialogProps {
settings: DisplaySettings,
onAccept: (settings: DisplaySettings) => void;
}

export default function DisplaySettingsDialog({
settings,
onAccept,
...props
}: DisplaySettingsDialogProps) {

const [unsavedSettings, setUnsavedSettings] = useState({...settings});
const sanitizeAndSetUnsavedSettings = (settings: DisplaySettings) =>
setUnsavedSettings(sanitizeDisplaySettings(settings));

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onAccept(unsavedSettings);
props.onOpenChange && props.onOpenChange(false);
};

return (
<Dialog {...props}>
<DialogContent>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Change display settings</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Canvas pixel size
</Label>
<Input
id="canvasPixelSize"
type="number"
value={unsavedSettings.canvasPixelSize}
className="col-span-3"
onChange={(e) => sanitizeAndSetUnsavedSettings({
...unsavedSettings,
canvasPixelSize: parseInt(e.target.value, 10),
})}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">
Show canvas
</Label>
<input
id="showCanvas"
type="checkbox"
checked={unsavedSettings.showCanvas}
className="w-5"
onChange={(e) => sanitizeAndSetUnsavedSettings({
...unsavedSettings,
showCanvas: e.target.checked,
})}
/>
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
10 changes: 6 additions & 4 deletions packages/web/src/components/hydra-canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from "react";
import { cn } from "@/lib/utils";
import { DisplaySettings } from "@/lib/display-settings";

interface HydraCanvasProps {
fullscreen?: boolean;
displaySettings: DisplaySettings;
}

const HydraCanvas = React.forwardRef(
(
{ fullscreen }: HydraCanvasProps,
{ fullscreen, displaySettings }: HydraCanvasProps,
ref: React.ForwardedRef<HTMLCanvasElement>
) => (
<canvas
Expand All @@ -16,9 +18,9 @@ const HydraCanvas = React.forwardRef(
"absolute top-0 left-0",
fullscreen && "h-full w-full block overflow-hidden"
)}
style={{ imageRendering: "pixelated" }}
width={window.innerWidth}
height={window.innerHeight}
style={{ imageRendering: "pixelated", display: displaySettings.showCanvas ? "" : "none" }}
width={window.innerWidth / displaySettings.canvasPixelSize}
height={window.innerHeight / displaySettings.canvasPixelSize}
/>
)
);
Expand Down
16 changes: 16 additions & 0 deletions packages/web/src/components/session-command-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,30 @@ import {
Palette,
Settings,
Share,
Monitor,
} from "lucide-react";
import { Link } from "react-router-dom";
import { useState } from "react";
import { EditorSettings } from "./editor";
import { DisplaySettings } from "@/lib/display-settings";

interface SessionCommandDialogProps extends CommandDialogProps {
editorSettings: EditorSettings;
onEditorSettingsChange: (settings: EditorSettings) => void;
displaySettings: DisplaySettings;
onDisplaySettingsChange: (settings: DisplaySettings) => void;
onSessionChangeUsername: () => void;
onSessionNew: () => void;
onSessionShareUrl: () => void;
onLayoutAdd: () => void;
onLayoutRemove: () => void;
onLayoutConfigure: () => void;
onEditorChangeDisplaySettings: () => void;
}

export default function SessionCommandDialog({
editorSettings,
displaySettings,
onEditorSettingsChange,
...props
}: SessionCommandDialogProps) {
Expand Down Expand Up @@ -144,6 +150,7 @@ export default function SessionCommandDialog({
<span>Remove Pane</span>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Editor">
<CommandList className="ml-2">
<CommandItem onSelect={() => setPages([...pages, "fonts"])}>
Expand Down Expand Up @@ -194,6 +201,15 @@ export default function SessionCommandDialog({
</CommandList>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Display">
<CommandList>
<CommandItem onSelect={wrapHandler(props.onEditorChangeDisplaySettings)}>
<Monitor className="mr-2 h-4 w-4" />
<span>Change display settings</span>
</CommandItem>
</CommandList>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Help">
{/* <CommandItem>
<HelpCircle className="mr-2 h-4 w-4" />
Expand Down
23 changes: 21 additions & 2 deletions packages/web/src/components/web-target-iframe.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { useQuery } from "@/hooks/use-query";
import { EvalMessage, Session } from "@flok-editor/session";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { DisplaySettings } from "@/lib/display-settings";

export interface WebTargetIframeProps {
target: string;
session: Session | null;
displaySettings: DisplaySettings;
}

export const WebTargetIframe = ({ target, session }: WebTargetIframeProps) => {
export const WebTargetIframe = ({ target, session, displaySettings }: WebTargetIframeProps) => {
const ref = useRef<HTMLIFrameElement | null>(null);

const query = useQuery();
const noWebEval = query.get("noWebEval")?.split(",") || [];
const [firstEval, setFirstEval] = useState(true);

// Check if we should load the target
if (noWebEval.includes(target) || noWebEval.includes("*")) {
Expand All @@ -28,6 +31,7 @@ export const WebTargetIframe = ({ target, session }: WebTargetIframeProps) => {
body: msg,
};
ref.current?.contentWindow?.postMessage(payload, "*");
setFirstEval(false);
};

session.on(`eval:${target}`, handler);
Expand All @@ -43,6 +47,7 @@ export const WebTargetIframe = ({ target, session }: WebTargetIframeProps) => {
if (!ref.current) return;
const interactionMessage = { type: "user-interaction" };
ref.current.contentWindow?.postMessage(interactionMessage, "*");
setFirstEval(false);
};

window.addEventListener("click", handleUserInteraction);
Expand All @@ -54,6 +59,20 @@ export const WebTargetIframe = ({ target, session }: WebTargetIframeProps) => {
};
}, [ref]);

// Post display settings to iframe when the settings change, or when the
// first eval occurs. The latter is a bit of a hack that prevents us from
// having to detect when the iframe is ready to receive messages (adding
// `ref` to the useEffect() dependencies doesn't do the trick), and it may
// break if flok begins eval'ing on load (currently nothing is eval'd on
// load).
useEffect(() => {
const message = {
type: "settings",
body: { displaySettings },
};
ref.current?.contentWindow?.postMessage(message, "*");
}, [displaySettings, firstEval]);

return (
<iframe
ref={ref}
Expand Down
19 changes: 19 additions & 0 deletions packages/web/src/hooks/use-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SettingsMessage } from "@/lib/settings";
import { useEffect } from "react";

export function useSettings(callback: (message: SettingsMessage) => void) {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data.type !== "settings") return;
callback(event.data.body as SettingsMessage);
};

window.addEventListener("message", handleMessage);

return () => {
window.removeEventListener("message", handleMessage);
};
}, [callback]);

return;
}
27 changes: 27 additions & 0 deletions packages/web/src/lib/display-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface DisplaySettings {
canvasPixelSize: number;
showCanvas: boolean;
}

export const defaultDisplaySettings: DisplaySettings = {
canvasPixelSize: 1,
showCanvas: true,
}

export function sanitizeDisplaySettings(settings: DisplaySettings): DisplaySettings {
// Pixel size should be at least 1 to prevent division-by-zero errors
const minPixelSize = 1;

// A maximum pixel size of 50 gives you 2-digit width/heights for a 4k
// canvas; should be low enough
const maxPixelSize = 50;

return {
...settings,
canvasPixelSize: Math.max(
minPixelSize,
Math.min(
maxPixelSize,
Math.round(settings.canvasPixelSize))),
};
}
5 changes: 5 additions & 0 deletions packages/web/src/lib/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DisplaySettings } from "@/lib/display-settings";

export interface SettingsMessage {
displaySettings?: DisplaySettings
}
17 changes: 16 additions & 1 deletion packages/web/src/routes/frames/hydra.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import HydraCanvas from "@/components/hydra-canvas";
import { useAnimationFrame } from "@/hooks/use-animation-frame";
import { useEvalHandler } from "@/hooks/use-eval-handler";
import { useSettings } from "@/hooks/use-settings";
import { HydraWrapper } from "@/lib/hydra-wrapper";
import { sendToast } from "@/lib/utils";
import { isWebglSupported } from "@/lib/webgl-detector";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { defaultDisplaySettings } from "@/lib/display-settings";

declare global {
interface Window {
Expand All @@ -16,6 +18,7 @@ export function Component() {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const hasWebGl = useMemo(() => isWebglSupported(), []);
const [instance, setInstance] = useState<HydraWrapper | null>(null);
const [displaySettings, setDisplaySettings] = useState(defaultDisplaySettings);

useEffect(() => {
if (hasWebGl) return;
Expand Down Expand Up @@ -66,5 +69,17 @@ export function Component() {
)
);

return hasWebGl && canvasRef && <HydraCanvas ref={canvasRef} fullscreen />;
useSettings(
useCallback(
(msg) => {
if (!instance) return;
if (msg.displaySettings) {
setDisplaySettings(msg.displaySettings);
}
},
[instance]
)
);

return hasWebGl && canvasRef && <HydraCanvas ref={canvasRef} fullscreen displaySettings={displaySettings} />;
}
Loading

0 comments on commit 6a10b2e

Please sign in to comment.