Skip to content

Commit d60224f

Browse files
committed
feat(threejs): add visibility optimization, host styling, transparent background
- Add visibility-based animation pause (IntersectionObserver + wrapped rAF) - Zero CPU usage when widget scrolls out of view - Apply host style variables via useHostStyles - Support transparent WebGL background (alpha: true) - Use light-dark() for theme-aware fallback colors - Tool no longer echoes code back in result - Document performance features in README
1 parent 8c3b1da commit d60224f

5 files changed

Lines changed: 142 additions & 56 deletions

File tree

examples/threejs-server/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,27 @@ React component that:
108108
- Displays streaming preview from `toolInputsPartial.code` as code arrives
109109
- Executes final code from `toolInputs.code` when complete
110110
- Renders to a pre-created canvas with configurable height
111+
112+
## Performance
113+
114+
### Visibility-Based Pause
115+
116+
Animation automatically pauses when the widget scrolls out of view:
117+
118+
- Uses `IntersectionObserver` to track visibility (browser-native, no polling)
119+
- Wraps `requestAnimationFrame` to skip frames when not visible
120+
- Animation loop stops completely → zero CPU usage while hidden
121+
- Queued callbacks resume instantly when scrolled back into view
122+
123+
This is transparent to user code - just use `requestAnimationFrame` normally.
124+
125+
### Transparent Background
126+
127+
Supports alpha transparency for seamless host UI integration:
128+
129+
```javascript
130+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
131+
renderer.setClearColor(0x000000, 0); // Fully transparent
132+
```
133+
134+
This is the default - 3D objects composite directly over the host background.

examples/threejs-server/server.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ const DIST_DIR = import.meta.filename.endsWith(".ts")
2222
// Default code example for the Three.js widget
2323
const DEFAULT_THREEJS_CODE = `const scene = new THREE.Scene();
2424
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
25-
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
25+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
2626
renderer.setSize(width, height);
27-
renderer.setClearColor(0x1a1a2e);
27+
renderer.setClearColor(0x000000, 0); // Transparent background
2828
2929
const cube = new THREE.Mesh(
3030
new THREE.BoxGeometry(1, 1, 1),
@@ -54,35 +54,38 @@ const THREEJS_DOCUMENTATION = `# Three.js Widget Documentation
5454
- \`OrbitControls\` - Interactive camera controls
5555
- \`EffectComposer\`, \`RenderPass\`, \`UnrealBloomPass\` - Post-processing effects
5656
57-
## Basic Template
57+
## Basic Template (Transparent Background)
5858
\`\`\`javascript
5959
const scene = new THREE.Scene();
6060
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
61-
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
61+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
6262
renderer.setSize(width, height);
63-
renderer.setClearColor(0x1a1a2e); // Dark background
63+
renderer.setClearColor(0x000000, 0); // Transparent - blends with host UI
6464
6565
// Add objects here...
6666
6767
camera.position.z = 5;
68-
renderer.render(scene, camera); // Static render
68+
renderer.render(scene, camera);
6969
\`\`\`
7070
71-
## Example: Rotating Cube with Lighting
71+
## Transparent vs Solid Background
72+
- **Transparent (default)**: Use \`alpha: true\` and \`setClearColor(0x000000, 0)\`
73+
- **Solid color**: Use \`setClearColor(0x1a1a2e)\` (omit alpha param)
74+
75+
## Example: Rotating Cube
7276
\`\`\`javascript
7377
const scene = new THREE.Scene();
7478
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
75-
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
79+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
7680
renderer.setSize(width, height);
77-
renderer.setClearColor(0x1a1a2e);
81+
renderer.setClearColor(0x000000, 0);
7882
7983
const cube = new THREE.Mesh(
8084
new THREE.BoxGeometry(1, 1, 1),
8185
new THREE.MeshStandardMaterial({ color: 0x00ff88 })
8286
);
8387
scene.add(cube);
8488
85-
// Lighting - keep intensity at 1 or below
8689
scene.add(new THREE.DirectionalLight(0xffffff, 1));
8790
scene.add(new THREE.AmbientLight(0x404040));
8891
@@ -101,9 +104,9 @@ animate();
101104
\`\`\`javascript
102105
const scene = new THREE.Scene();
103106
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
104-
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
107+
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
105108
renderer.setSize(width, height);
106-
renderer.setClearColor(0x2d2d44);
109+
renderer.setClearColor(0x000000, 0);
107110
108111
const controls = new OrbitControls(camera, renderer.domElement);
109112
controls.enableDamping = true;
@@ -128,7 +131,7 @@ animate();
128131
\`\`\`
129132
130133
## Tips
131-
- Always set \`renderer.setClearColor()\` to a dark color
134+
- Use \`alpha: true\` for transparent backgrounds that blend with host UI
132135
- Keep light intensity ≤ 1 to avoid washed-out scenes
133136
- Use \`MeshStandardMaterial\` for realistic lighting
134137
- For animations, use \`requestAnimationFrame\`
@@ -157,7 +160,7 @@ export function createServer(): McpServer {
157160
{
158161
title: "Show Three.js Scene",
159162
description:
160-
"Render an interactive 3D scene with custom Three.js code. Available globals: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, height.",
163+
"Render an interactive 3D scene with custom Three.js code. Supports transparent backgrounds (alpha: true) for seamless host UI integration. Available globals: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, height.",
161164
inputSchema: {
162165
code: z
163166
.string()
@@ -171,16 +174,14 @@ export function createServer(): McpServer {
171174
.describe("Height in pixels"),
172175
},
173176
outputSchema: z.object({
174-
code: z.string(),
175-
height: z.number(),
177+
success: z.boolean(),
176178
}),
177179
_meta: { ui: { resourceUri } },
178180
},
179-
async ({ code, height }) => {
180-
const data = { code, height };
181+
async () => {
181182
return {
182-
content: [{ type: "text", text: JSON.stringify(data) }],
183-
structuredContent: data,
183+
content: [{ type: "text", text: "Three.js scene rendered" }],
184+
structuredContent: { success: true },
184185
};
185186
},
186187
);
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
1+
:root {
2+
color-scheme: light dark;
3+
}
4+
15
* {
26
box-sizing: border-box;
37
}
48

59
html, body {
6-
font-family: system-ui, -apple-system, sans-serif;
10+
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
711
font-size: 1rem;
812
margin: 0;
913
padding: 0;
10-
background: #1a1a2e;
14+
background: transparent;
1115
}
1216

1317
.loading {
1418
display: flex;
1519
align-items: center;
1620
justify-content: center;
1721
height: 400px;
18-
color: #888;
22+
color: var(--color-text-secondary, #888);
1923
font-size: 14px;
2024
}
2125

2226
.error {
2327
padding: 20px;
24-
color: #ff6b6b;
28+
color: var(--color-text-danger, #ff6b6b);
2529
}
2630

2731
.threejs-container {
@@ -35,9 +39,9 @@ html, body {
3539
display: flex;
3640
align-items: center;
3741
justify-content: center;
38-
color: #ff6b6b;
39-
background: rgba(26, 26, 46, 0.9);
40-
border-radius: 8px;
41-
font-family: system-ui;
42+
color: var(--color-text-danger, #ff6b6b);
43+
background: var(--color-background-secondary, rgba(26, 26, 46, 0.9));
44+
border-radius: var(--border-radius-md, 8px);
45+
font-family: var(--font-sans, system-ui);
4246
padding: 20px;
4347
}

examples/threejs-server/src/mcp-app-wrapper.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* props to the actual widget component.
66
*/
77
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
8-
import { useApp } from "@modelcontextprotocol/ext-apps/react";
8+
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
99
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
1010
import { StrictMode, useState, useCallback, useEffect } from "react";
1111
import { createRoot } from "react-dom/client";
@@ -78,6 +78,9 @@ function McpAppWrapper() {
7878
},
7979
});
8080

81+
// Apply host styling (theme, CSS variables, fonts)
82+
useHostStyles(app);
83+
8184
// Get initial host context after connection
8285
useEffect(() => {
8386
if (app) {

0 commit comments

Comments
 (0)