Add shadertoy-server example for WebGL shader rendering (#226)
Introduces a new MCP App example that renders ShaderToy-compatible GLSL fragment shaders in real-time using WebGL 2.0 and ShaderToyLite.js. Features: - Standard ShaderToy uniforms (`iTime`, `iResolution`, `iMouse`, etc.) - Multi-pass rendering with buffers A-D for feedback effects - Shader compilation error overlay Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b273279849
commit
ec3409aa91
17 changed files with 1641 additions and 1901 deletions
|
|
@ -2,3 +2,4 @@ examples/basic-host/**/*.ts
|
|||
examples/basic-host/**/*.tsx
|
||||
examples/basic-server-*/**/*.ts
|
||||
examples/basic-server-*/**/*.tsx
|
||||
**/vendor/**
|
||||
|
|
|
|||
132
examples/shadertoy-server/README.md
Normal file
132
examples/shadertoy-server/README.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Example: ShaderToy Server
|
||||
|
||||
A demo MCP App that renders [ShaderToy](https://www.shadertoy.com/)-compatible GLSL fragment shaders in real-time using WebGL 2.0 and [ShaderToyLite.js](https://github.com/nickoala/ShaderToyLite).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/01-gradient.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/01-gradient.png" alt="Gradient" width="100%"></a></td>
|
||||
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/02-kaleidoscope.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/02-kaleidoscope.png" alt="Kaleidoscope" width="100%"></a></td>
|
||||
<td><a href="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/03-fractal.png"><img src="https://modelcontextprotocol.github.io/ext-apps/screenshots/shadertoy-server/03-fractal.png" alt="Kaleidoscope" width="100%"></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time Rendering**: Renders GLSL shaders using WebGL 2.0
|
||||
- **ShaderToy Compatibility**: Uses the standard `mainImage(out vec4 fragColor, in vec2 fragCoord)` entry point
|
||||
- **Multi-pass Rendering**: Supports buffers A-D for feedback effects, blur chains, and simulations
|
||||
- **Standard Uniforms**: iResolution, iTime, iTimeDelta, iFrame, iMouse, iDate, iChannel0-3
|
||||
|
||||
## Running
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Build and start the server:
|
||||
|
||||
```bash
|
||||
npm run start:http # for Streamable HTTP transport
|
||||
# OR
|
||||
npm run start:stdio # for stdio transport
|
||||
```
|
||||
|
||||
3. View using the [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example or another MCP Apps-compatible host.
|
||||
|
||||
### Tool Input Examples
|
||||
|
||||
**Gradient with Time:**
|
||||
|
||||
```glsl
|
||||
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
_Tool input:_
|
||||
|
||||
```json
|
||||
{
|
||||
"fragmentShader": "void mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = fragCoord / iResolution.xy;\n fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0);\n}"
|
||||
}
|
||||
```
|
||||
|
||||
**Kaleidoscope**:
|
||||
|
||||
```glsl
|
||||
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
||||
float segments = 6.0;
|
||||
float zoom = 1.0 + 0.3 * sin(iTime * 0.2);
|
||||
float angle = atan(uv.y, uv.x) + iTime * 0.3;
|
||||
float r = length(uv) * zoom;
|
||||
angle = mod(angle, 6.28 / segments);
|
||||
angle = abs(angle - 3.14 / segments);
|
||||
vec2 p = vec2(cos(angle), sin(angle)) * r;
|
||||
p += iTime * 0.1;
|
||||
float v = sin(p.x * 10.0) * sin(p.y * 10.0);
|
||||
v += sin(length(p) * 15.0 - iTime * 2.0);
|
||||
v += sin(p.x * 5.0 + p.y * 7.0 + iTime);
|
||||
vec3 col = 0.5 + 0.5 * cos(v * 2.0 + vec3(0.0, 2.0, 4.0) + iTime);
|
||||
fragColor = vec4(col, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
_Tool input:_
|
||||
|
||||
```json
|
||||
{
|
||||
"fragmentShader": "void mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;\n float segments = 6.0;\n float zoom = 1.0 + 0.3 * sin(iTime * 0.2);\n float angle = atan(uv.y, uv.x) + iTime * 0.3;\n float r = length(uv) * zoom;\n angle = mod(angle, 6.28 / segments);\n angle = abs(angle - 3.14 / segments);\n vec2 p = vec2(cos(angle), sin(angle)) * r;\n p += iTime * 0.1;\n float v = sin(p.x * 10.0) * sin(p.y * 10.0);\n v += sin(length(p) * 15.0 - iTime * 2.0);\n v += sin(p.x * 5.0 + p.y * 7.0 + iTime);\n vec3 col = 0.5 + 0.5 * cos(v * 2.0 + vec3(0.0, 2.0, 4.0) + iTime);\n fragColor = vec4(col, 1.0);\n}"
|
||||
}
|
||||
```
|
||||
|
||||
**Interactive Julia Set** (mouse controls the fractal's c parameter):
|
||||
|
||||
```glsl
|
||||
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y * 2.5;
|
||||
vec2 mouse = (iMouse.xy / iResolution.xy - 0.5) * 2.0;
|
||||
vec2 c = mouse;
|
||||
vec2 z = uv;
|
||||
float iter = 0.0;
|
||||
for (int i = 0; i < 100; i++) {
|
||||
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
|
||||
if (dot(z, z) > 4.0) break;
|
||||
iter++;
|
||||
}
|
||||
float t = iter / 100.0;
|
||||
vec3 col = 0.5 + 0.5 * cos(3.0 + t * 6.28 * 2.0 + vec3(0.0, 0.6, 1.0));
|
||||
if (iter == 100.0) col = vec3(0.0);
|
||||
fragColor = vec4(col, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
_Tool input:_
|
||||
|
||||
```json
|
||||
{
|
||||
"fragmentShader": "void mainImage(out vec4 fragColor, in vec2 fragCoord) {\n vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y * 2.5;\n vec2 mouse = (iMouse.xy / iResolution.xy - 0.5) * 2.0;\n vec2 c = mouse;\n vec2 z = uv;\n float iter = 0.0;\n for (int i = 0; i < 100; i++) {\n z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;\n if (dot(z, z) > 4.0) break;\n iter++;\n }\n float t = iter / 100.0;\n vec3 col = 0.5 + 0.5 * cos(3.0 + t * 6.28 * 2.0 + vec3(0.0, 0.6, 1.0));\n if (iter == 100.0) col = vec3(0.0);\n fragColor = vec4(col, 1.0);\n}"
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Server (`server.ts`)
|
||||
|
||||
Exposes a single `render-shadertoy` tool that accepts:
|
||||
|
||||
- `fragmentShader`: Main Image shader code (required)
|
||||
- `common`: Shared code across all shaders (optional)
|
||||
- `bufferA`: Buffer A shader, accessible as iChannel0 (optional)
|
||||
- `bufferB`: Buffer B shader, accessible as iChannel1 (optional)
|
||||
- `bufferC`: Buffer C shader, accessible as iChannel2 (optional)
|
||||
- `bufferD`: Buffer D shader, accessible as iChannel3 (optional)
|
||||
|
||||
### App (`src/mcp-app.ts`)
|
||||
|
||||
- Receives shader code via `ontoolinput` handler
|
||||
- Uses ShaderToyLite.js for WebGL rendering
|
||||
- Displays compilation errors in an overlay
|
||||
15
examples/shadertoy-server/mcp-app.html
Normal file
15
examples/shadertoy-server/mcp-app.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>ShaderToy Renderer</title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="main">
|
||||
<canvas id="canvas"></canvas>
|
||||
</main>
|
||||
<script type="module" src="/src/mcp-app.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
43
examples/shadertoy-server/package.json
Normal file
43
examples/shadertoy-server/package.json
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/server-shadertoy",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/modelcontextprotocol/ext-apps",
|
||||
"directory": "examples/shadertoy-server"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "server.ts",
|
||||
"files": [
|
||||
"server.ts",
|
||||
"server-utils.ts",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
|
||||
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
|
||||
"serve": "bun server.ts",
|
||||
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
|
||||
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/ext-apps": "^0.3.1",
|
||||
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"concurrently": "^9.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^10.1.0",
|
||||
"express": "^5.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-singlefile": "^2.3.0"
|
||||
}
|
||||
}
|
||||
72
examples/shadertoy-server/server-utils.ts
Normal file
72
examples/shadertoy-server/server-utils.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* Shared utilities for running MCP servers with Streamable HTTP transport.
|
||||
*/
|
||||
|
||||
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import cors from "cors";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with Streamable HTTP transport in stateless mode.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance per request.
|
||||
* @param options - Server configuration options.
|
||||
*/
|
||||
export async function startServer(
|
||||
createServer: () => McpServer,
|
||||
options: ServerOptions,
|
||||
): Promise<void> {
|
||||
const { port, name = "MCP Server" } = options;
|
||||
|
||||
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
||||
app.use(cors());
|
||||
|
||||
app.all("/mcp", async (req: Request, res: Response) => {
|
||||
const server = createServer();
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
});
|
||||
|
||||
res.on("close", () => {
|
||||
transport.close().catch(() => {});
|
||||
server.close().catch(() => {});
|
||||
});
|
||||
|
||||
try {
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("MCP error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = app.listen(port, (err) => {
|
||||
if (err) {
|
||||
console.error("Failed to start server:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`${name} listening on http://localhost:${port}/mcp`);
|
||||
});
|
||||
|
||||
const shutdown = () => {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => process.exit(0));
|
||||
};
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
}
|
||||
156
examples/shadertoy-server/server.ts
Normal file
156
examples/shadertoy-server/server.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import {
|
||||
registerAppResource,
|
||||
registerAppTool,
|
||||
RESOURCE_MIME_TYPE,
|
||||
} from "@modelcontextprotocol/ext-apps/server";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { startServer } from "./server-utils.js";
|
||||
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
const TOOL_DESCRIPTION = `Renders a ShaderToy-compatible GLSL fragment shader in real-time using WebGL 2.0.
|
||||
|
||||
SHADER FORMAT (ShaderToy conventions):
|
||||
Use ShaderToy's mainImage entry point - do NOT use generic GLSL conventions.
|
||||
|
||||
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0);
|
||||
}
|
||||
|
||||
Do NOT use: void main(), gl_FragColor, gl_FragCoord - these will not work.
|
||||
|
||||
AVAILABLE UNIFORMS:
|
||||
- iResolution (vec3): viewport resolution in pixels
|
||||
- iTime (float): elapsed time in seconds
|
||||
- iTimeDelta (float): time since last frame
|
||||
- iFrame (int): frame counter
|
||||
- iMouse (vec4): mouse position (xy=current, zw=click)
|
||||
- iDate (vec4): year, month, day, seconds
|
||||
- iChannel0-3 (sampler2D): buffer inputs for multi-pass shaders
|
||||
|
||||
MULTI-PASS RENDERING:
|
||||
- Use bufferA-D parameters for feedback effects, blur chains, simulations
|
||||
- BufferA output -> iChannel0, BufferB -> iChannel1, etc.
|
||||
- Buffers can sample their own previous frame for feedback loops
|
||||
|
||||
LIMITATIONS - Do NOT use:
|
||||
- External textures (generate noise/patterns procedurally)
|
||||
- Keyboard input (iKeyboard not available)
|
||||
- Audio/microphone input
|
||||
- VR features (mainVR not available)
|
||||
|
||||
For procedural noise:
|
||||
float hash(vec2 p) { return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }`;
|
||||
|
||||
const DEFAULT_FRAGMENT_SHADER = `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0);
|
||||
}`;
|
||||
|
||||
/**
|
||||
* Creates a new MCP server instance with tools and resources registered.
|
||||
*/
|
||||
export function createServer(): McpServer {
|
||||
const server = new McpServer({
|
||||
name: "ShaderToy Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
const resourceUri = "ui://shadertoy/mcp-app.html";
|
||||
|
||||
// Register the render-shadertoy tool with UI metadata
|
||||
registerAppTool(
|
||||
server,
|
||||
"render-shadertoy",
|
||||
{
|
||||
title: "ShaderToy Renderer",
|
||||
description: TOOL_DESCRIPTION,
|
||||
inputSchema: z.object({
|
||||
fragmentShader: z
|
||||
.string()
|
||||
.default(DEFAULT_FRAGMENT_SHADER)
|
||||
.describe("Main Image shader - ShaderToy GLSL code"),
|
||||
common: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Common code shared across all shaders (optional)"),
|
||||
bufferA: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Buffer A shader code - accessible as iChannel0 (optional)",
|
||||
),
|
||||
bufferB: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Buffer B shader code - accessible as iChannel1 (optional)",
|
||||
),
|
||||
bufferC: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Buffer C shader code - accessible as iChannel2 (optional)",
|
||||
),
|
||||
bufferD: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Buffer D shader code - accessible as iChannel3 (optional)",
|
||||
),
|
||||
}),
|
||||
_meta: { ui: { resourceUri } },
|
||||
},
|
||||
async (): Promise<CallToolResult> => {
|
||||
// Tool execution returns success - actual rendering happens in the UI
|
||||
return {
|
||||
content: [{ type: "text", text: "Shader rendered successfully" }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// Register the resource which returns the bundled HTML/JavaScript for the UI
|
||||
registerAppResource(
|
||||
server,
|
||||
resourceUri,
|
||||
resourceUri,
|
||||
{ mimeType: RESOURCE_MIME_TYPE },
|
||||
async (): Promise<ReadResourceResult> => {
|
||||
const html = await fs.readFile(
|
||||
path.join(DIST_DIR, "mcp-app.html"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
} else {
|
||||
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||
await startServer(createServer, { port, name: "ShaderToy Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
12
examples/shadertoy-server/src/global.css
Normal file
12
examples/shadertoy-server/src/global.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 1em;
|
||||
}
|
||||
12
examples/shadertoy-server/src/mcp-app.css
Normal file
12
examples/shadertoy-server/src/mcp-app.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
126
examples/shadertoy-server/src/mcp-app.ts
Normal file
126
examples/shadertoy-server/src/mcp-app.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* ShaderToy renderer MCP App using ShaderToyLite.js
|
||||
*/
|
||||
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
|
||||
import "./global.css";
|
||||
import "./mcp-app.css";
|
||||
import ShaderToyLite, {
|
||||
type ShaderToyLiteInstance,
|
||||
} from "./vendor/ShaderToyLite.js";
|
||||
|
||||
interface ShaderInput {
|
||||
fragmentShader: string;
|
||||
common?: string;
|
||||
bufferA?: string;
|
||||
bufferB?: string;
|
||||
bufferC?: string;
|
||||
bufferD?: string;
|
||||
}
|
||||
|
||||
function isShaderInput(value: unknown): value is ShaderInput {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
typeof (value as Record<string, unknown>).fragmentShader === "string"
|
||||
);
|
||||
}
|
||||
|
||||
const log = {
|
||||
info: console.log.bind(console, "[APP]"),
|
||||
warn: console.warn.bind(console, "[APP]"),
|
||||
error: console.error.bind(console, "[APP]"),
|
||||
};
|
||||
|
||||
// Get element references
|
||||
const mainEl = document.querySelector(".main") as HTMLElement;
|
||||
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
||||
// Resize canvas to fill viewport
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
|
||||
// Handle host context changes (safe area insets)
|
||||
function handleHostContextChanged(ctx: McpUiHostContext) {
|
||||
if (ctx.safeAreaInsets) {
|
||||
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
|
||||
mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
|
||||
mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
|
||||
mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
|
||||
}
|
||||
}
|
||||
|
||||
// ShaderToyLite instance
|
||||
let shaderToy: ShaderToyLiteInstance | null = null;
|
||||
|
||||
// Create app instance
|
||||
const app = new App({ name: "ShaderToy Renderer", version: "1.0.0" });
|
||||
|
||||
app.onteardown = async () => {
|
||||
log.info("App is being torn down");
|
||||
if (shaderToy) {
|
||||
shaderToy.pause();
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
app.ontoolinput = (params) => {
|
||||
log.info("Received shader input");
|
||||
|
||||
if (!isShaderInput(params.arguments)) {
|
||||
log.error("Invalid tool input");
|
||||
return;
|
||||
}
|
||||
|
||||
const { fragmentShader, common, bufferA, bufferB, bufferC, bufferD } =
|
||||
params.arguments;
|
||||
|
||||
// Initialize ShaderToyLite if needed
|
||||
if (!shaderToy) {
|
||||
shaderToy = new ShaderToyLite("canvas");
|
||||
}
|
||||
|
||||
// Set common code (shared across all shaders)
|
||||
shaderToy.setCommon(common || "");
|
||||
|
||||
// Set buffer shaders with self-feedback
|
||||
if (bufferA) {
|
||||
shaderToy.setBufferA({ source: bufferA, iChannel0: "A" });
|
||||
}
|
||||
if (bufferB) {
|
||||
shaderToy.setBufferB({ source: bufferB, iChannel1: "B" });
|
||||
}
|
||||
if (bufferC) {
|
||||
shaderToy.setBufferC({ source: bufferC, iChannel2: "C" });
|
||||
}
|
||||
if (bufferD) {
|
||||
shaderToy.setBufferD({ source: bufferD, iChannel3: "D" });
|
||||
}
|
||||
|
||||
// Set main Image shader with buffer inputs
|
||||
shaderToy.setImage({
|
||||
source: fragmentShader,
|
||||
iChannel0: bufferA ? "A" : undefined,
|
||||
iChannel1: bufferB ? "B" : undefined,
|
||||
iChannel2: bufferC ? "C" : undefined,
|
||||
iChannel3: bufferD ? "D" : undefined,
|
||||
});
|
||||
|
||||
shaderToy.play();
|
||||
log.info("Setup complete");
|
||||
};
|
||||
|
||||
app.onerror = log.error;
|
||||
|
||||
app.onhostcontextchanged = handleHostContextChanged;
|
||||
|
||||
// Connect to host
|
||||
app.connect().then(() => {
|
||||
log.info("Connected to host");
|
||||
const ctx = app.getHostContext();
|
||||
if (ctx) {
|
||||
handleHostContextChanged(ctx);
|
||||
}
|
||||
});
|
||||
113
examples/shadertoy-server/src/vendor/ShaderToyLite.d.ts
vendored
Normal file
113
examples/shadertoy-server/src/vendor/ShaderToyLite.d.ts
vendored
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Type declarations for ShaderToyLite.js
|
||||
* https://github.com/chipweinberger/ShaderToyLite.js
|
||||
*/
|
||||
|
||||
/** Valid buffer keys for channel inputs */
|
||||
type BufferKey = "A" | "B" | "C" | "D";
|
||||
|
||||
/** Configuration for shader passes (buffers and image) */
|
||||
interface ShaderConfig {
|
||||
/** GLSL fragment shader source code */
|
||||
source?: string;
|
||||
/** Buffer to use as iChannel0 input */
|
||||
iChannel0?: BufferKey;
|
||||
/** Buffer to use as iChannel1 input */
|
||||
iChannel1?: BufferKey;
|
||||
/** Buffer to use as iChannel2 input */
|
||||
iChannel2?: BufferKey;
|
||||
/** Buffer to use as iChannel3 input */
|
||||
iChannel3?: BufferKey;
|
||||
}
|
||||
|
||||
/** ShaderToyLite instance methods */
|
||||
interface ShaderToyLiteInstance {
|
||||
/**
|
||||
* Set common shader code shared across all passes.
|
||||
* @param source - GLSL source code to prepend to all shaders
|
||||
*/
|
||||
setCommon(source?: string | null): void;
|
||||
|
||||
/**
|
||||
* Set Buffer A shader pass.
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setBufferA(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set Buffer B shader pass.
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setBufferB(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set Buffer C shader pass.
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setBufferC(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set Buffer D shader pass.
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setBufferD(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set the main Image shader pass (renders to screen).
|
||||
* @param config - Shader configuration or falsy to clear
|
||||
*/
|
||||
setImage(config?: ShaderConfig | null): void;
|
||||
|
||||
/**
|
||||
* Set a callback to be called on each draw frame.
|
||||
* @param callback - Function to call each frame
|
||||
*/
|
||||
setOnDraw(callback?: (() => void) | null): void;
|
||||
|
||||
/**
|
||||
* Add a custom texture that can be used as a channel input.
|
||||
* @param texture - WebGL texture object
|
||||
* @param key - Buffer key to reference this texture (only A/B/C/D are recognized by iChannel bindings)
|
||||
*/
|
||||
addTexture(texture: WebGLTexture, key: BufferKey): void;
|
||||
|
||||
/**
|
||||
* Get the current playback time in seconds.
|
||||
* @returns Time elapsed since first draw
|
||||
*/
|
||||
time(): number;
|
||||
|
||||
/**
|
||||
* Check if the shader is currently playing.
|
||||
* @returns True if playing, false if paused
|
||||
*/
|
||||
isPlaying(): boolean;
|
||||
|
||||
/**
|
||||
* Reset playback to the beginning (time = 0, frame = 0).
|
||||
*/
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* Pause shader playback.
|
||||
*/
|
||||
pause(): void;
|
||||
|
||||
/**
|
||||
* Start or resume shader playback.
|
||||
*/
|
||||
play(): void;
|
||||
}
|
||||
|
||||
/** ShaderToyLite constructor interface */
|
||||
interface ShaderToyLiteConstructor {
|
||||
/**
|
||||
* Create a new ShaderToyLite instance.
|
||||
* @param canvasId - ID of the canvas element to render to
|
||||
*/
|
||||
new (canvasId: string): ShaderToyLiteInstance;
|
||||
}
|
||||
|
||||
declare const ShaderToyLite: ShaderToyLiteConstructor;
|
||||
export default ShaderToyLite;
|
||||
export type { ShaderToyLiteInstance, ShaderConfig, BufferKey };
|
||||
437
examples/shadertoy-server/src/vendor/ShaderToyLite.js
vendored
Normal file
437
examples/shadertoy-server/src/vendor/ShaderToyLite.js
vendored
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
/**
|
||||
* ShaderToyLite.js
|
||||
* https://github.com/chipweinberger/ShaderToyLite.js
|
||||
*
|
||||
* Copyright (c) Chip Weinberger
|
||||
* Licensed under the MIT License
|
||||
* https://github.com/chipweinberger/ShaderToyLite.js/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
function ShaderToyLite(canvasId) {
|
||||
|
||||
var hdr =
|
||||
`#version 300 es
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
precision highp int;
|
||||
precision mediump sampler3D;
|
||||
#endif
|
||||
#define texture2D texture
|
||||
uniform vec3 iResolution; // viewport resolution (in pixels)
|
||||
uniform float iTime; // shader playback time (in seconds)
|
||||
uniform float iTimeDelta; // render time (in seconds)
|
||||
uniform float iFrameRate; // shader frame rate
|
||||
uniform int iFrame; // shader playback frame
|
||||
uniform float iChannelTime[4]; // channel playback time (in seconds)
|
||||
uniform vec3 iChannelResolution[4]; // channel resolution (in pixels)
|
||||
uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click
|
||||
uniform sampler2D iChannel0; // input channel 0
|
||||
uniform sampler2D iChannel1; // input channel 1
|
||||
uniform sampler2D iChannel2; // input channel 2
|
||||
uniform sampler2D iChannel3; // input channel 3
|
||||
uniform vec4 iDate; // (year, month, day, unixtime in seconds)
|
||||
uniform float iSampleRate; // sound sample rate (i.e., 44100)
|
||||
out vec4 frag_out_color;
|
||||
void mainImage( out vec4 c, in vec2 f );
|
||||
void main( void )
|
||||
{
|
||||
vec4 color = vec4(0.0,0.0,0.0,0.0);
|
||||
mainImage( color, gl_FragCoord.xy );
|
||||
frag_out_color = vec4(color);
|
||||
}
|
||||
`;
|
||||
|
||||
var basicFragShader =
|
||||
`void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
|
||||
fragColor = texture2D(iChannel0, gl_FragCoord.xy / iResolution.xy);
|
||||
}
|
||||
`;
|
||||
|
||||
const basicVertexShader =
|
||||
`#version 300 es
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
precision highp int;
|
||||
precision mediump sampler3D;
|
||||
#endif
|
||||
in vec2 vertexInPosition;
|
||||
void main() {
|
||||
gl_Position = vec4(vertexInPosition, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const quadVertices = new Float32Array([
|
||||
-1.0, -1.0,
|
||||
1.0, -1.0,
|
||||
-1.0, 1.0,
|
||||
1.0, 1.0,
|
||||
-1.0, 1.0,
|
||||
1.0, -1.0
|
||||
]);
|
||||
|
||||
|
||||
var opts = { alpha: false,
|
||||
depth: false,
|
||||
stencil: false,
|
||||
premultipliedAlpha: false,
|
||||
antialias: true,
|
||||
preserveDrawingBuffer: false,
|
||||
powerPreference: "high-performance" };
|
||||
|
||||
var gl = document.getElementById(canvasId).getContext('webgl2', opts);
|
||||
|
||||
// timing
|
||||
var isPlaying = false;
|
||||
var firstDrawTime = 0;
|
||||
var prevDrawTime = 0;
|
||||
|
||||
// callback
|
||||
var onDrawCallback;
|
||||
|
||||
// uniforms
|
||||
var iFrame = 0;
|
||||
var iMouse = {x: 0, y: 0, clickX: 0, clickY: 0};
|
||||
|
||||
// shader common source
|
||||
var common = "";
|
||||
|
||||
// render passes variables. valid keys:
|
||||
// 'A', 'B', 'C', 'D', 'Image'
|
||||
var sourcecode = {};// fragment shader code
|
||||
var ichannels = {}; // texture inputs
|
||||
var atexture = {}; // front texture (input/output)
|
||||
var btexture = {}; // back texture (input/output)
|
||||
var aframebuf = {}; // front buffer (output)
|
||||
var bframebuf = {}; // back buffer (output)
|
||||
var program = {}; // webgl program
|
||||
var location = {}; // uniform location
|
||||
var flip = {}; // a b flip
|
||||
var quadBuffer; // two full screen triangles
|
||||
|
||||
|
||||
var setup = () => {
|
||||
gl.getExtension( 'OES_texture_float_linear');
|
||||
gl.getExtension( 'OES_texture_half_float_linear');
|
||||
gl.getExtension( 'EXT_color_buffer_float');
|
||||
gl.getExtension( 'WEBGL_debug_shaders');
|
||||
|
||||
['A', 'B', 'C', 'D', 'Image'].forEach((key) => {
|
||||
sourcecode[key] = "";
|
||||
ichannels[key] = {};
|
||||
program[key] = null;
|
||||
location[key] = {};
|
||||
if (key != 'Image') {
|
||||
atexture[key] = createTexture();
|
||||
btexture[key] = createTexture();
|
||||
aframebuf[key] = createFrameBuffer(atexture[key]);
|
||||
bframebuf[key] = createFrameBuffer(btexture[key]);
|
||||
flip[key] = false;
|
||||
}
|
||||
});
|
||||
|
||||
// bind the geometry
|
||||
quadBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, quadVertices, gl.STATIC_DRAW);
|
||||
|
||||
// Set viewport size
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
var canvas = document.getElementById(canvasId);
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
gl.canvas.width = canvas.width;
|
||||
gl.canvas.height = canvas.height;
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
});
|
||||
|
||||
canvas.addEventListener("mousemove", (event) => {
|
||||
iMouse.x = event.offsetX;
|
||||
iMouse.y = canvas.height - event.offsetY;
|
||||
});
|
||||
|
||||
canvas.addEventListener("mousedown", (event) => {
|
||||
iMouse.clickX = event.offsetX;
|
||||
iMouse.clickY = canvas.height - event.offsetY;
|
||||
});
|
||||
|
||||
canvas.addEventListener("mouseup", () => {
|
||||
iMouse.clickX = 0;
|
||||
iMouse.clickY = 0;
|
||||
});
|
||||
}
|
||||
|
||||
var createTexture = () => {
|
||||
var texture = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, gl.canvas.width, gl.canvas.height, 0, gl.RGBA, gl.FLOAT, null);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
return texture;
|
||||
}
|
||||
|
||||
var createFrameBuffer = (texture) => {
|
||||
var framebuffer = gl.createFramebuffer();
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
return framebuffer;
|
||||
};
|
||||
|
||||
var compileProgram = (key) => {
|
||||
var vert = gl.createShader(gl.VERTEX_SHADER);
|
||||
gl.shaderSource(vert, basicVertexShader);
|
||||
gl.compileShader(vert);
|
||||
|
||||
if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) {
|
||||
console.error('Vertex Shader compilation failed: ' + gl.getShaderInfoLog(vert));
|
||||
gl.deleteShader(vert);
|
||||
return null;
|
||||
}
|
||||
|
||||
var source = hdr + common + sourcecode[key];
|
||||
var frag = gl.createShader(gl.FRAGMENT_SHADER);
|
||||
gl.shaderSource(frag, source);
|
||||
gl.compileShader(frag);
|
||||
|
||||
if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) {
|
||||
console.error('Fragment Shader compilation failed: ' + gl.getShaderInfoLog(frag));
|
||||
console.error(source);
|
||||
gl.deleteShader(frag);
|
||||
return null;
|
||||
}
|
||||
|
||||
var program = gl.createProgram();
|
||||
gl.attachShader(program, vert);
|
||||
gl.attachShader(program, frag);
|
||||
gl.linkProgram(program);
|
||||
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||
console.error('Program initialization failed: ' + gl.getProgramInfoLog(program));
|
||||
return null;
|
||||
}
|
||||
|
||||
// uniform locations
|
||||
location[key]["iResolution"] = gl.getUniformLocation(program, "iResolution");
|
||||
location[key]["iTime"] = gl.getUniformLocation(program, "iTime");
|
||||
location[key]["iTimeDelta"] = gl.getUniformLocation(program, "iTimeDelta");
|
||||
location[key]["iFrameRate"] = gl.getUniformLocation(program, "iFrameRate");
|
||||
location[key]["iFrame"] = gl.getUniformLocation(program, "iFrame");
|
||||
location[key]["iChannelTime"] = gl.getUniformLocation(program, "iChannelTime[0]");
|
||||
location[key]["iChannelResolution"] = gl.getUniformLocation(program, "iChannelResolution[0]");
|
||||
location[key]["iChannel0"] = gl.getUniformLocation(program, "iChannel0");
|
||||
location[key]["iChannel1"] = gl.getUniformLocation(program, "iChannel1");
|
||||
location[key]["iChannel2"] = gl.getUniformLocation(program, "iChannel2");
|
||||
location[key]["iChannel3"] = gl.getUniformLocation(program, "iChannel3");
|
||||
location[key]["iMouse"] = gl.getUniformLocation(program, "iMouse");
|
||||
location[key]["iDate"] = gl.getUniformLocation(program, "iDate");
|
||||
location[key]["iSampleRate"] = gl.getUniformLocation(program, "iSampleRate");
|
||||
location[key]["vertexInPosition"] = gl.getAttribLocation(program, "vertexInPosition");
|
||||
|
||||
return program;
|
||||
};
|
||||
|
||||
var repeat = (times, arr) => {
|
||||
let result = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
result = [...result, ...arr];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var setShader = (config, key) => {
|
||||
if (config) {
|
||||
if (config.source) {
|
||||
sourcecode[key] = config.source;
|
||||
program[key] = compileProgram(key);
|
||||
if (program[key] == null) {
|
||||
console.error("Failed to compile " + key);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 4; i++) {
|
||||
var s = config[`iChannel${i}`];
|
||||
if (s == "A" || s == "B" || s == "C" || s == "D") {
|
||||
ichannels[key][i] = s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sourcecode[key] = "";
|
||||
program[key] = null;
|
||||
}
|
||||
};
|
||||
|
||||
var draw = () => {
|
||||
|
||||
// current time
|
||||
var now = isPlaying ? Date.now() : prevDrawTime;
|
||||
var date = new Date(now);
|
||||
|
||||
// first draw?
|
||||
if (firstDrawTime == 0) {
|
||||
firstDrawTime = now;
|
||||
}
|
||||
|
||||
// call callback
|
||||
if (onDrawCallback) {
|
||||
onDrawCallback();
|
||||
}
|
||||
|
||||
// time difference between frames in seconds
|
||||
var iTimeDelta = (now - prevDrawTime) * 0.001;
|
||||
|
||||
// time in seconds
|
||||
var iTime = (now - firstDrawTime) * 0.001;
|
||||
var iDate = [date.getFullYear(), date.getMonth(), date.getDate(), date.getTime() * 0.001];
|
||||
|
||||
// channel uniforms
|
||||
var iChannelTimes = new Float32Array(repeat(4, [iTime]));
|
||||
var iChannelResolutions = new Float32Array(repeat(4, [gl.canvas.width, gl.canvas.height, 0]));
|
||||
|
||||
['A', 'B', 'C', 'D', 'Image'].forEach((key) => {
|
||||
|
||||
if (program[key]) {
|
||||
|
||||
// framebuffer
|
||||
if (key === "Image") {
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
} else {
|
||||
var output = flip[key] ? bframebuf[key] : aframebuf[key];
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, output);
|
||||
}
|
||||
|
||||
// textures
|
||||
for (let i = 0; i < 4; i++) {
|
||||
var chkey = ichannels[key][i];
|
||||
if (chkey) {
|
||||
var input = flip[chkey] ? atexture[chkey] : btexture[chkey];
|
||||
gl.activeTexture(gl[`TEXTURE${i}`]);
|
||||
gl.bindTexture(gl.TEXTURE_2D, input);
|
||||
}
|
||||
}
|
||||
|
||||
// program
|
||||
gl.useProgram(program[key]);
|
||||
|
||||
// uniforms
|
||||
gl.uniform3f( location[key]["iResolution"], gl.canvas.width, gl.canvas.height, 1.0);
|
||||
gl.uniform1f( location[key]["iTime"], iTime);
|
||||
gl.uniform1f( location[key]["iTimeDelta"], iTimeDelta);
|
||||
gl.uniform1f( location[key]["iFrameRate"], 60);
|
||||
gl.uniform1i( location[key]["iFrame"], iFrame);
|
||||
gl.uniform1fv(location[key]["iChannelTime"], iChannelTimes);
|
||||
gl.uniform3fv(location[key]["iChannelResolution"], iChannelResolutions);
|
||||
gl.uniform1i( location[key]["iChannel0"], 0);
|
||||
gl.uniform1i( location[key]["iChannel1"], 1);
|
||||
gl.uniform1i( location[key]["iChannel2"], 2);
|
||||
gl.uniform1i( location[key]["iChannel3"], 3);
|
||||
gl.uniform4f( location[key]["iMouse"], iMouse.x, iMouse.y, iMouse.clickX, iMouse.clickY);
|
||||
gl.uniform4f( location[key]["iDate"], iDate[0], iDate[1], iDate[2], iDate[3]);
|
||||
gl.uniform1f( location[key]["iSampleRate"], 44100);
|
||||
|
||||
// viewport
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// vertexs
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
||||
gl.vertexAttribPointer(location[key]["vertexInPosition"], 2, gl.FLOAT, false, 0, 0);
|
||||
gl.enableVertexAttribArray(location[key]["vertexInPosition"]);
|
||||
|
||||
// draw
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
|
||||
flip[key] = !flip[key];
|
||||
}
|
||||
});
|
||||
|
||||
// time of last draw
|
||||
prevDrawTime = now;
|
||||
|
||||
// frame counter
|
||||
iFrame++;
|
||||
};
|
||||
|
||||
// Animation loop
|
||||
var animate = () => {
|
||||
if (isPlaying) {
|
||||
draw();
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
this.setCommon = (source) => {
|
||||
if (source === undefined) {source = "";}
|
||||
if (source === null) {source = "";}
|
||||
common = source;
|
||||
['A', 'B', 'C', 'D', 'Image'].forEach((key) => {
|
||||
if (program[key]) {
|
||||
program[key] = compileProgram(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.setBufferA = (config) => {
|
||||
setShader(config, 'A');
|
||||
};
|
||||
|
||||
this.setBufferB = (config) => {
|
||||
setShader(config, 'B');
|
||||
};
|
||||
|
||||
this.setBufferC = (config) => {
|
||||
setShader(config, 'C');
|
||||
};
|
||||
|
||||
this.setBufferD = (config) => {
|
||||
setShader(config, 'D');
|
||||
};
|
||||
|
||||
this.setImage = (config) => {
|
||||
setShader(config, 'Image');
|
||||
};
|
||||
|
||||
this.setOnDraw = (callback) => {
|
||||
onDrawCallback = callback;
|
||||
}
|
||||
|
||||
this.addTexture = (texture, key) => {
|
||||
atexture[key] = texture;
|
||||
btexture[key] = texture;
|
||||
flip[key] = false;
|
||||
}
|
||||
|
||||
this.time = () => {
|
||||
return (prevDrawTime - firstDrawTime) * 0.001;
|
||||
}
|
||||
|
||||
this.isPlaying = () => isPlaying;
|
||||
|
||||
this.reset = () => {
|
||||
var now = new Date();
|
||||
firstDrawTime = now;
|
||||
prevDrawTime = now;
|
||||
iFrame = 0;
|
||||
draw();
|
||||
}
|
||||
|
||||
this.pause = () => {
|
||||
isPlaying = false;
|
||||
}
|
||||
|
||||
this.play = () => {
|
||||
if (!isPlaying) {
|
||||
isPlaying = true;
|
||||
var now = Date.now();
|
||||
var elapsed = prevDrawTime - firstDrawTime;
|
||||
firstDrawTime = now - elapsed;
|
||||
prevDrawTime = now;
|
||||
animate();
|
||||
}
|
||||
}
|
||||
|
||||
setup();
|
||||
}
|
||||
|
||||
export default ShaderToyLite;
|
||||
19
examples/shadertoy-server/tsconfig.json
Normal file
19
examples/shadertoy-server/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "server.ts"]
|
||||
}
|
||||
24
examples/shadertoy-server/vite.config.ts
Normal file
24
examples/shadertoy-server/vite.config.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig } from "vite";
|
||||
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||
|
||||
const INPUT = process.env.INPUT;
|
||||
if (!INPUT) {
|
||||
throw new Error("INPUT environment variable is not set");
|
||||
}
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === "development";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [viteSingleFile()],
|
||||
build: {
|
||||
sourcemap: isDevelopment ? "inline" : undefined,
|
||||
cssMinify: !isDevelopment,
|
||||
minify: !isDevelopment,
|
||||
|
||||
rollupOptions: {
|
||||
input: INPUT,
|
||||
},
|
||||
outDir: "dist",
|
||||
emptyOutDir: false,
|
||||
},
|
||||
});
|
||||
1716
package-lock.json
generated
1716
package-lock.json
generated
File diff suppressed because it is too large
Load diff
658
src/generated/schema.json
generated
658
src/generated/schema.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,11 @@
|
|||
import { test, expect, type Page, type ConsoleMessage } from "@playwright/test";
|
||||
|
||||
// Dynamic element selectors to mask for screenshot comparison
|
||||
//
|
||||
// Note: CSS modules generate unique class names, so we use attribute selectors
|
||||
// with partial matches (e.g., [class*="heatmapWrapper"]) for those components
|
||||
//
|
||||
// Note: map-server uses SLOW_SERVERS timeout instead of masking to wait for tiles
|
||||
const DYNAMIC_MASKS: Record<string, string[]> = {
|
||||
integration: ["#server-time"], // Server time display
|
||||
"basic-preact": ["#server-time"], // Server time display
|
||||
|
|
@ -13,7 +16,7 @@ const DYNAMIC_MASKS: Record<string, string[]> = {
|
|||
"basic-vue": ["#server-time"], // Server time display
|
||||
"cohort-heatmap": ['[class*="heatmapWrapper"]'], // Heatmap grid (random data)
|
||||
"customer-segmentation": [".chart-container"], // Scatter plot (random data)
|
||||
// Note: map-server uses SLOW_SERVERS timeout instead of masking to wait for tiles
|
||||
shadertoy: ["#canvas"], // WebGL shader canvas (animated)
|
||||
"system-monitor": [
|
||||
".chart-container", // CPU chart (highly dynamic)
|
||||
"#status-text", // Current timestamp
|
||||
|
|
@ -46,6 +49,7 @@ const SERVERS = [
|
|||
{ key: "customer-segmentation", name: "Customer Segmentation Server" },
|
||||
{ key: "map-server", name: "CesiumJS Map Server" },
|
||||
{ key: "scenario-modeler", name: "SaaS Scenario Modeler" },
|
||||
{ key: "shadertoy", name: "ShaderToy Server" },
|
||||
{ key: "sheet-music", name: "Sheet Music Server" },
|
||||
{ key: "system-monitor", name: "System Monitor Server" },
|
||||
{ key: "threejs", name: "Three.js Server" },
|
||||
|
|
|
|||
BIN
tests/e2e/servers.spec.ts-snapshots/shadertoy.png
Normal file
BIN
tests/e2e/servers.spec.ts-snapshots/shadertoy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Loading…
Reference in a new issue