feat(shadertoy): add fullscreen support + ditch insets for max visual effect (#278)
* feat(shadertoy): add fullscreen support Add fullscreen toggle button that appears when the host supports fullscreen display mode. Uses the same pattern as pdf-server: - Check availableDisplayModes in host context - Show/hide button based on capability - Toggle between inline and fullscreen via requestDisplayMode() - Update UI state on host context changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(shadertoy): button visibility after exiting fullscreen + Escape key - Only update button visibility when availableDisplayModes is present in context (partial updates don't include all fields) - Add Escape key handler to exit fullscreen mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
89a684546b
commit
b78cd74c1b
5 changed files with 115 additions and 6 deletions
|
|
@ -9,6 +9,14 @@
|
||||||
<body>
|
<body>
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
|
<button id="fullscreen-btn" class="fullscreen-btn" title="Toggle fullscreen">
|
||||||
|
<svg class="expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||||
|
</svg>
|
||||||
|
<svg class="collapse-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</main>
|
</main>
|
||||||
<script type="module" src="/src/mcp-app.ts"></script>
|
<script type="module" src="/src/mcp-app.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
|
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
|
||||||
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
|
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
|
||||||
"serve": "bun --watch main.ts",
|
"serve": "bun --watch main.ts",
|
||||||
|
"serve:stdio": "bun main.ts --stdio",
|
||||||
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
|
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
|
||||||
|
"start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio",
|
||||||
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
|
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 116 KiB |
|
|
@ -5,8 +5,61 @@ html, body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#canvas {
|
#canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fullscreen button */
|
||||||
|
.fullscreen-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: none; /* Hidden by default, shown when fullscreen available */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s, opacity 0.2s;
|
||||||
|
opacity: 0.7;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-btn svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-btn .collapse-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-btn.available {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fullscreen mode */
|
||||||
|
.main.fullscreen .fullscreen-btn .expand-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main.fullscreen .fullscreen-btn .collapse-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,13 @@ const log = {
|
||||||
// Get element references
|
// Get element references
|
||||||
const mainEl = document.querySelector(".main") as HTMLElement;
|
const mainEl = document.querySelector(".main") as HTMLElement;
|
||||||
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
||||||
|
const fullscreenBtn = document.getElementById(
|
||||||
|
"fullscreen-btn",
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
|
// Display mode state
|
||||||
|
let currentDisplayMode: "inline" | "fullscreen" = "inline";
|
||||||
|
|
||||||
// Resize canvas to fill viewport
|
// Resize canvas to fill viewport
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
|
|
@ -42,16 +49,55 @@ function resizeCanvas() {
|
||||||
resizeCanvas();
|
resizeCanvas();
|
||||||
window.addEventListener("resize", resizeCanvas);
|
window.addEventListener("resize", resizeCanvas);
|
||||||
|
|
||||||
// Handle host context changes (safe area insets)
|
// Handle host context changes (display mode)
|
||||||
function handleHostContextChanged(ctx: McpUiHostContext) {
|
function handleHostContextChanged(ctx: McpUiHostContext) {
|
||||||
if (ctx.safeAreaInsets) {
|
// Note: We ignore safeAreaInsets to maximize shader display area
|
||||||
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
|
|
||||||
mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
|
// Show fullscreen button if available (only update if field is present)
|
||||||
mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
|
if (ctx.availableDisplayModes !== undefined) {
|
||||||
mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
|
if (ctx.availableDisplayModes.includes("fullscreen")) {
|
||||||
|
fullscreenBtn.classList.add("available");
|
||||||
|
} else {
|
||||||
|
fullscreenBtn.classList.remove("available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update display mode state and UI
|
||||||
|
if (ctx.displayMode) {
|
||||||
|
currentDisplayMode = ctx.displayMode as "inline" | "fullscreen";
|
||||||
|
if (currentDisplayMode === "fullscreen") {
|
||||||
|
mainEl.classList.add("fullscreen");
|
||||||
|
} else {
|
||||||
|
mainEl.classList.remove("fullscreen");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Escape key to exit fullscreen
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && currentDisplayMode === "fullscreen") {
|
||||||
|
toggleFullscreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle fullscreen mode
|
||||||
|
async function toggleFullscreen() {
|
||||||
|
const newMode = currentDisplayMode === "fullscreen" ? "inline" : "fullscreen";
|
||||||
|
try {
|
||||||
|
const result = await app.requestDisplayMode({ mode: newMode });
|
||||||
|
currentDisplayMode = result.mode as "inline" | "fullscreen";
|
||||||
|
if (currentDisplayMode === "fullscreen") {
|
||||||
|
mainEl.classList.add("fullscreen");
|
||||||
|
} else {
|
||||||
|
mainEl.classList.remove("fullscreen");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error("Failed to change display mode:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fullscreenBtn.addEventListener("click", toggleFullscreen);
|
||||||
|
|
||||||
// ShaderToyLite instance
|
// ShaderToyLite instance
|
||||||
let shaderToy: ShaderToyLiteInstance | null = null;
|
let shaderToy: ShaderToyLiteInstance | null = null;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue