Compare commits

..
Author SHA1 Message Date
Chris Tate f6d1c5134b react-three-fiber (#209) 2026-03-12 10:14:11 -05:00
Chris Tate 316439fcd7 add solid example (#208) 2026-03-11 22:54:56 -05:00
Chris Tateandctate c180f529a0 feat: add strict mode to jsonSchema() for LLM structured outputs (#193)
* feat: add strict mode to jsonSchema() for LLM structured outputs

This PR adds support for generating strict JSON schemas compatible with LLM structured output APIs (OpenAI, Google Gemini, Anthropic, etc.).

## Problem
`catalog.jsonSchema()` was generating schemas that OpenAI's strict mode rejected due to:
- Use of `propertyNames` keyword (not permitted)
- Missing `additionalProperties: false` on nested objects
- Optional properties not being handled correctly for strict validation

## Changes
- Added `JsonSchemaOptions` interface with `strict` boolean flag
- Updated `catalog.jsonSchema()` to accept optional `JsonSchemaOptions` parameter
- When `strict: true`, the schema generator:
  - Sets `additionalProperties: false` on all object types
  - Removes `propertyNames` constraints
  - Lists all properties (including optional ones) in `required` arrays, using nullable types for optionals
  - Converts record types to fixed-key objects without additional property schemas
- Fixed Zod 4 compatibility issue in type name resolution
- Added comprehensive test suite covering all strict mode requirements
- Exported `JsonSchemaOptions` type from core package

## Usage
```typescript
// Default behavior (unchanged)
const schema = catalog.jsonSchema();

// Strict mode for LLM APIs
const strictSchema = catalog.jsonSchema({ strict: true });
```

The default behavior remains unchanged to maintain backward compatibility.

Fixes #180

* fix: document record type limitation and add anyOf nullable test

- Add clear JSDoc documenting that record/map types become opaque objects
  in strict mode (LLM strict schemas require additionalProperties: false)
- Improve inline comment on the record case in zodToJsonSchema
- Add test exercising the anyOf nullable wrapping for optional properties
  using a non-record schema so the path is directly verified

---------

Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
2026-03-11 22:17:09 -05:00
Chris Tate c3c0a36f20 better visual (#206) 2026-03-11 12:16:51 -05:00
61 changed files with 10684 additions and 153 deletions
+2 -1
View File
@@ -20,7 +20,8 @@
"@json-render/image",
"@json-render/mcp",
"@json-render/svelte",
"@json-render/solid"
"@json-render/solid",
"@json-render/react-three-fiber"
]
],
"linked": [],
+42
View File
@@ -121,6 +121,7 @@ function Dashboard({ spec }) {
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
| `@json-render/solid` | SolidJS renderer with fine-grained reactive contexts |
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (19 built-in components) |
| `@json-render/react-native` | React Native renderer with standard mobile components |
| `@json-render/remotion` | Remotion video renderer, timeline schema |
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
@@ -434,6 +435,47 @@ import { renderToSvg } from "@json-render/image/render";
const svg = await renderToSvg(spec, { fonts });
```
### Three.js (3D)
```tsx
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
} from "@json-render/react-three-fiber";
const catalog = defineCatalog(schema, {
components: {
Box: threeComponentDefinitions.Box,
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
OrbitControls: threeComponentDefinitions.OrbitControls,
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
Box: threeComponents.Box,
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
OrbitControls: threeComponents.OrbitControls,
},
});
<ThreeCanvas
spec={spec}
registry={registry}
shadows
camera={{ position: [5, 5, 5], fov: 50 }}
style={{ width: "100%", height: "100vh" }}
/>;
```
## Features
### Streaming (SpecStream)
@@ -0,0 +1,416 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/react-three-fiber")
# @json-render/react-three-fiber
React Three Fiber renderer for json-render. 19 built-in 3D components for meshes, lights, models, environments, text, cameras, and controls.
## Installation
```bash
npm install @json-render/react-three-fiber @json-render/core @json-render/react @react-three/fiber @react-three/drei three zod
```
## Entry Points
<table>
<thead>
<tr>
<th>Entry Point</th>
<th>Exports</th>
<th>Use For</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@json-render/react-three-fiber</code></td>
<td><code>threeComponents</code>, <code>ThreeRenderer</code>, <code>ThreeCanvas</code>, schemas</td>
<td>React Three Fiber implementations and renderer</td>
</tr>
<tr>
<td><code>@json-render/react-three-fiber/catalog</code></td>
<td><code>threeComponentDefinitions</code></td>
<td>Catalog schemas (no R3F dependency, safe for server)</td>
</tr>
</tbody>
</table>
## Usage
```tsx
import {'{ defineCatalog }'} from "@json-render/core";
import {'{ schema, defineRegistry }'} from "@json-render/react";
import {'{'}
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
{'}'} from "@json-render/react-three-fiber";
const catalog = defineCatalog(schema, {'{'}
components: {'{'}
Box: threeComponentDefinitions.Box,
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
OrbitControls: threeComponentDefinitions.OrbitControls,
{'}'},
actions: {'{}'},
{'}'});
const {'{ registry }'} = defineRegistry(catalog, {'{'}
components: {'{'}
Box: threeComponents.Box,
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
OrbitControls: threeComponents.OrbitControls,
{'}'},
{'}'});
```
### ThreeCanvas (convenience)
```tsx
<ThreeCanvas
spec={'{spec}'}
registry={'{registry}'}
shadows
camera={'{'}{'{ position: [5, 5, 5], fov: 50 }'}{'}'}
style={'{'}{'{ width: "100%", height: "100vh" }'}{'}'}
/>
```
### Manual Canvas Setup
```tsx
import {'{ Canvas }'} from "@react-three/fiber";
import {'{ ThreeRenderer }'} from "@json-render/react-three-fiber";
<Canvas shadows>
<ThreeRenderer spec={'{spec}'} registry={'{registry}'} />
</Canvas>
```
## Components
### Primitives
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>Key Props</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Box</code></td>
<td>Box mesh (default 1x1x1)</td>
<td><code>width</code>, <code>height</code>, <code>depth</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Sphere</code></td>
<td>Sphere mesh</td>
<td><code>radius</code>, <code>widthSegments</code>, <code>heightSegments</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Cylinder</code></td>
<td>Cylinder mesh</td>
<td><code>radiusTop</code>, <code>radiusBottom</code>, <code>height</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Cone</code></td>
<td>Cone mesh</td>
<td><code>radius</code>, <code>height</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Torus</code></td>
<td>Torus (donut) mesh</td>
<td><code>radius</code>, <code>tube</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Plane</code></td>
<td>Flat plane mesh</td>
<td><code>width</code>, <code>height</code>, <code>material</code></td>
</tr>
<tr>
<td><code>Capsule</code></td>
<td>Capsule mesh</td>
<td><code>radius</code>, <code>length</code>, <code>material</code></td>
</tr>
</tbody>
</table>
All primitives share: <code>position</code>, <code>rotation</code>, <code>scale</code>, <code>castShadow</code>, <code>receiveShadow</code>, <code>material</code>.
### Material Schema
<table>
<thead>
<tr>
<th>Property</th>
<th>Type</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>color</code></td>
<td><code>string</code></td>
<td><code>"#ffffff"</code></td>
</tr>
<tr>
<td><code>metalness</code></td>
<td><code>number</code></td>
<td><code>0</code></td>
</tr>
<tr>
<td><code>roughness</code></td>
<td><code>number</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>emissive</code></td>
<td><code>string</code></td>
<td><code>"#000000"</code></td>
</tr>
<tr>
<td><code>emissiveIntensity</code></td>
<td><code>number</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>opacity</code></td>
<td><code>number</code></td>
<td><code>1</code></td>
</tr>
<tr>
<td><code>transparent</code></td>
<td><code>boolean</code></td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>wireframe</code></td>
<td><code>boolean</code></td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
### Lights
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>Key Props</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>AmbientLight</code></td>
<td>Uniform illumination</td>
<td><code>color</code>, <code>intensity</code></td>
</tr>
<tr>
<td><code>DirectionalLight</code></td>
<td>Sunlight-style</td>
<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>castShadow</code></td>
</tr>
<tr>
<td><code>PointLight</code></td>
<td>Radiates from a point</td>
<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>distance</code>, <code>decay</code></td>
</tr>
<tr>
<td><code>SpotLight</code></td>
<td>Cone of light</td>
<td><code>position</code>, <code>color</code>, <code>intensity</code>, <code>angle</code>, <code>penumbra</code></td>
</tr>
</tbody>
</table>
### Other Components
<table>
<thead>
<tr>
<th>Component</th>
<th>Description</th>
<th>Key Props</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Group</code></td>
<td>Container for children</td>
<td><code>position</code>, <code>rotation</code>, <code>scale</code></td>
</tr>
<tr>
<td><code>Model</code></td>
<td>GLTF/GLB model loader</td>
<td><code>url</code>, <code>position</code>, <code>rotation</code>, <code>scale</code></td>
</tr>
<tr>
<td><code>Environment</code></td>
<td>HDRI environment map</td>
<td><code>preset</code>, <code>background</code>, <code>blur</code>, <code>intensity</code></td>
</tr>
<tr>
<td><code>Fog</code></td>
<td>Linear fog effect</td>
<td><code>color</code>, <code>near</code>, <code>far</code></td>
</tr>
<tr>
<td><code>GridHelper</code></td>
<td>Reference grid</td>
<td><code>size</code>, <code>divisions</code>, <code>color</code></td>
</tr>
<tr>
<td><code>Text3D</code></td>
<td>3D text (SDF)</td>
<td><code>text</code>, <code>fontSize</code>, <code>color</code>, <code>anchorX</code>, <code>anchorY</code></td>
</tr>
<tr>
<td><code>PerspectiveCamera</code></td>
<td>Camera</td>
<td><code>position</code>, <code>fov</code>, <code>near</code>, <code>far</code>, <code>makeDefault</code></td>
</tr>
<tr>
<td><code>OrbitControls</code></td>
<td>Camera controls</td>
<td><code>enableDamping</code>, <code>enableZoom</code>, <code>autoRotate</code></td>
</tr>
</tbody>
</table>
## Shared Schemas
Reusable Zod schemas for custom 3D components:
```tsx
import {'{ vector3Schema, materialSchema, transformProps, shadowProps }'} from "@json-render/react-three-fiber";
```
<table>
<thead>
<tr>
<th>Export</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>vector3Schema</code></td>
<td><code>z.tuple([z.number(), z.number(), z.number()])</code></td>
</tr>
<tr>
<td><code>materialSchema</code></td>
<td>Standard material props (color, metalness, roughness, etc.)</td>
</tr>
<tr>
<td><code>transformProps</code></td>
<td><code>{'{ position, rotation, scale }'}</code> schema fields</td>
</tr>
<tr>
<td><code>shadowProps</code></td>
<td><code>{'{ castShadow, receiveShadow }'}</code> schema fields</td>
</tr>
</tbody>
</table>
## ThreeRenderer
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>spec</code></td>
<td><code>Spec | null</code></td>
<td>The spec to render as a 3D scene</td>
</tr>
<tr>
<td><code>registry</code></td>
<td><code>ComponentRegistry</code></td>
<td>Component registry from <code>defineRegistry</code></td>
</tr>
<tr>
<td><code>store</code></td>
<td><code>StateStore</code></td>
<td>External state store (controlled mode)</td>
</tr>
<tr>
<td><code>initialState</code></td>
<td><code>Record&lt;string, unknown&gt;</code></td>
<td>Initial state (uncontrolled mode)</td>
</tr>
<tr>
<td><code>handlers</code></td>
<td><code>Record&lt;string, Function&gt;</code></td>
<td>Action handlers</td>
</tr>
<tr>
<td><code>loading</code></td>
<td><code>boolean</code></td>
<td>Whether the spec is streaming</td>
</tr>
<tr>
<td><code>children</code></td>
<td><code>ReactNode</code></td>
<td>Additional R3F elements alongside the spec</td>
</tr>
</tbody>
</table>
## ThreeCanvas
Extends <code>ThreeRendererProps</code> with Canvas options:
<table>
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>shadows</code></td>
<td><code>boolean</code></td>
<td>Enable shadow maps</td>
</tr>
<tr>
<td><code>camera</code></td>
<td><code>object</code></td>
<td>Default camera config (position, fov, etc.)</td>
</tr>
<tr>
<td><code>className</code></td>
<td><code>string</code></td>
<td>CSS class for the canvas container</td>
</tr>
<tr>
<td><code>style</code></td>
<td><code>CSSProperties</code></td>
<td>Inline styles for the canvas container</td>
</tr>
</tbody>
</table>
## Type Helpers
```tsx
import type {'{ ThreeProps }'} from "@json-render/react-three-fiber";
type BoxProps = ThreeProps<"Box">;
type SphereProps = ThreeProps<"Sphere">;
```
+2 -2
View File
@@ -16,8 +16,8 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
GitHub repository: https://github.com/vercel-labs/json-render
Documentation: https://json-render.dev/docs
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, react-pdf, react-email, react-native, shadcn, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/react-three-fiber, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/codegen, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, react-pdf, react-email, react-native, shadcn, react-three-fiber, image, remotion, vue, svelte, solid, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory.
+218 -61
View File
@@ -1,20 +1,24 @@
"use client";
function Skeleton({ className = "" }: { className?: string }) {
return <div className={`rounded bg-muted-foreground/10 ${className}`} />;
}
/** Simple form wireframe reused in both diagrams */
function FormUI() {
function ScheduleItem({
time,
label,
color,
}: {
time: string;
label: string;
color: string;
}) {
return (
<div className="border border-border rounded-lg p-2.5 space-y-1.5 bg-muted/30">
{/* Title */}
<Skeleton className="h-2.5 w-14 mb-1" />
{/* Input fields */}
<Skeleton className="h-4 w-full rounded-sm" />
<Skeleton className="h-4 w-full rounded-sm" />
{/* Submit button */}
<Skeleton className="h-4 w-16 rounded-sm bg-muted-foreground/20 mt-1" />
<div className="flex items-center gap-2 py-1.5 border-t border-border/50 first:border-t-0">
<span className="text-[10px] text-muted-foreground/60 w-12 shrink-0 tabular-nums">
{time}
</span>
<span
className="w-1.5 h-1.5 rounded-full shrink-0"
style={{ background: color }}
/>
<span className="text-[11px] text-muted-foreground">{label}</span>
</div>
);
}
@@ -22,48 +26,164 @@ function FormUI() {
function InlineModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
<div className="text-sm font-medium text-foreground text-center mb-3">
Inline Mode
</div>
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
{/* Chat area */}
<div className="flex-1 p-3 overflow-hidden flex justify-center">
<div className="w-1/3 min-w-[120px] space-y-3">
{/* User message */}
<div className="flex justify-end">
<div className="bg-muted rounded-xl px-3 py-2">
<Skeleton className="h-2 w-14" />
</div>
<div className="flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-col">
<div className="flex-1 p-4 space-y-3 overflow-hidden">
{/* User message */}
<div className="flex justify-end">
<div className="bg-muted-foreground/20 rounded-2xl rounded-br px-3.5 py-2 max-w-[85%]">
<span className="text-[11px] text-foreground/80">
I have back-to-back meetings today
</span>
</div>
</div>
{/* Assistant text */}
<div className="space-y-2">
<div className="space-y-1.5">
<Skeleton className="h-2 w-full" />
<Skeleton className="h-2 w-3/4" />
</div>
{/* Inline UI */}
<FormUI />
{/* More text after UI */}
<div className="space-y-1.5">
<Skeleton className="h-2 w-4/5" />
{/* AI response */}
<div className="space-y-2">
<p className="text-[11px] text-muted-foreground/70">
Packed day! Here&apos;s what you&apos;ve got:
</p>
<div className="bg-muted/40 border border-border rounded-xl p-3 w-[92%]">
<div className="text-[11px] font-semibold text-foreground/80 mb-2">
Today&apos;s Schedule
</div>
<ScheduleItem
time="10:00 AM"
label="Design Review"
color="#7aa2f7"
/>
<ScheduleItem
time="1:00 PM"
label="Sprint Planning"
color="#bb9af7"
/>
<ScheduleItem
time="3:30 PM"
label="Team Standup"
color="#9ece6a"
/>
<ScheduleItem
time="4:30 PM"
label="Eng All-Hands"
color="#e0af68"
/>
</div>
</div>
</div>
{/* Input bar */}
<div className="p-2 flex justify-center">
<div className="w-1/3 min-w-[120px] flex items-center gap-2">
<Skeleton className="h-7 flex-1 rounded-md" />
<Skeleton className="h-7 w-7 rounded-md" />
{/* Prompt input */}
<div className="p-2.5">
<div className="bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5">
<span className="text-[10px] text-muted-foreground/40 flex-1">
Message...
</span>
<div className="w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center">
<svg
className="w-2.5 h-2.5 text-muted-foreground/50"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
transform="rotate(-90 12 12)"
/>
</svg>
</div>
</div>
</div>
</div>
<div className="text-[10px] text-muted-foreground/60 mt-2 text-center">
Text + UI interleaved in messages
<div className="mt-3 flex flex-col items-center gap-2">
<div className="text-xs font-medium text-muted-foreground">
AI decides when UI beats text
</div>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/50">
<span>AI chatbots</span>
<span className="text-muted-foreground/30">/</span>
<span>Copilots</span>
<span className="text-muted-foreground/30">/</span>
<span>Assistants</span>
</div>
</div>
</div>
);
}
function LandingPage() {
return (
<div className="w-full h-full flex flex-col bg-muted/20">
{/* Nav */}
<div className="flex items-center justify-between px-3 py-2 border-b border-border/50">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
className="shrink-0"
>
<rect
x="2"
y="14"
width="5"
height="8"
rx="1"
className="fill-muted-foreground/60"
/>
<rect
x="9.5"
y="8"
width="5"
height="14"
rx="1"
className="fill-muted-foreground/60"
/>
<rect
x="17"
y="2"
width="5"
height="20"
rx="1"
className="fill-muted-foreground/60"
/>
</svg>
<div className="flex items-center gap-2">
<span className="text-[9px] text-muted-foreground/50">Pricing</span>
<span className="text-[9px] text-muted-foreground/50">Blog</span>
<span className="text-[8px] font-semibold bg-foreground text-background rounded-md px-1.5 py-0.5 whitespace-nowrap">
Get Started
</span>
</div>
</div>
{/* Hero */}
<div className="flex-1 flex flex-col items-center justify-center gap-2.5 text-center px-4">
<span className="text-[8px] font-medium text-green-400 bg-green-400/10 border border-green-400/15 rounded-full px-2 py-0.5">
Trusted by 2,000+ teams
</span>
<div className="text-sm font-bold text-foreground leading-tight">
Analytics that
<br />
move the needle
</div>
<div className="text-[10px] text-muted-foreground/50 leading-relaxed">
Ship what matters. No setup required.
</div>
<div className="flex gap-1.5 mt-1">
<button className="bg-foreground text-background text-[9px] font-semibold rounded-lg px-3 py-1.5 whitespace-nowrap">
Start Free
</button>
<button className="border border-border text-muted-foreground text-[9px] font-medium rounded-lg px-3 py-1.5 whitespace-nowrap">
Book Demo
</button>
</div>
</div>
{/* Footer */}
<div className="flex justify-center gap-3 py-2 border-t border-border/50">
<span className="text-[8px] text-muted-foreground/30">Privacy</span>
<span className="text-[8px] text-muted-foreground/30">Terms</span>
<span className="text-[8px] text-muted-foreground/30">Status</span>
</div>
</div>
);
@@ -72,28 +192,65 @@ function InlineModeDiagram() {
function StandaloneModeDiagram() {
return (
<div className="flex flex-col h-full">
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
<div className="text-sm font-medium text-foreground text-center mb-3">
Standalone Mode
</div>
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
{/* Left panel - prompt */}
<div className="w-[38%] border-r border-border flex flex-col">
<div className="flex-1" />
<div className="p-3 space-y-2">
<Skeleton className="h-7 w-full rounded-md" />
<Skeleton className="h-5 w-16 rounded-md" />
<div className="flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-row">
{/* Left panel - conversation */}
<div className="w-[38%] border-r border-border/50 flex flex-col">
<div className="flex-1 p-3 space-y-2.5">
{/* User message */}
<div className="flex justify-end">
<div className="bg-muted-foreground/20 rounded-2xl rounded-br px-2.5 py-1.5">
<span className="text-[10px] text-foreground/80 block leading-relaxed">
A landing page for my analytics startup
</span>
</div>
</div>
{/* AI text */}
<p className="text-[10px] text-muted-foreground/60 leading-relaxed">
Here&apos;s a clean landing page with a hero, nav, and CTAs.
</p>
</div>
{/* Prompt input */}
<div className="p-2.5">
<div className="bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5">
<span className="text-[10px] text-muted-foreground/40 flex-1">
Message...
</span>
<div className="w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center">
<svg
className="w-2.5 h-2.5 text-muted-foreground/50"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
transform="rotate(-90 12 12)"
/>
</svg>
</div>
</div>
</div>
</div>
{/* Right panel - UI preview */}
<div className="flex-1 p-3 flex items-center justify-center">
<div className="w-3/4">
<FormUI />
</div>
{/* Right panel - landing page */}
<div className="flex-1">
<LandingPage />
</div>
</div>
<div className="text-[10px] text-muted-foreground/60 mt-2 text-center">
Prompt separate from UI preview
<div className="mt-3 flex flex-col items-center gap-2">
<div className="text-xs font-medium text-muted-foreground">
Prompt in, UI out
</div>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/50">
<span>Website builders</span>
<span className="text-muted-foreground/30">/</span>
<span>Text-to-widget</span>
<span className="text-muted-foreground/30">/</span>
<span>Dashboards</span>
</div>
</div>
</div>
);
@@ -103,10 +260,10 @@ export function GenerationModesDiagram() {
return (
<div className="not-prose my-8">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="h-[280px]">
<div className="h-[420px]">
<InlineModeDiagram />
</div>
<div className="h-[280px]">
<div className="h-[420px]">
<StandaloneModeDiagram />
</div>
</div>
+4
View File
@@ -134,6 +134,10 @@ export const docsNavigation: NavSection[] = [
{ title: "@json-render/vue", href: "/docs/api/vue" },
{ title: "@json-render/svelte", href: "/docs/api/svelte" },
{ title: "@json-render/solid", href: "/docs/api/solid" },
{
title: "@json-render/react-three-fiber",
href: "/docs/api/react-three-fiber",
},
{ title: "@json-render/codegen", href: "/docs/api/codegen" },
{ title: "@json-render/mcp", href: "/docs/api/mcp" },
{ title: "@json-render/redux", href: "/docs/api/redux" },
+1
View File
@@ -56,6 +56,7 @@ export const PAGE_TITLES: Record<string, string> = {
"docs/api/redux": "@json-render/redux API",
"docs/api/zustand": "@json-render/zustand API",
"docs/api/jotai": "@json-render/jotai API",
"docs/api/react-three-fiber": "@json-render/react-three-fiber API",
"docs/api/xstate": "@json-render/xstate API",
};
+20
View File
@@ -0,0 +1,20 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "json-render React Three Fiber Example",
description: "3D scenes from JSON specs with @json-render/react-three-fiber",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body style={{ margin: 0, fontFamily: "system-ui, sans-serif" }}>
{children}
</body>
</html>
);
}
+238
View File
@@ -0,0 +1,238 @@
"use client";
import { useState } from "react";
import { defineCatalog } from "@json-render/core";
import type { ComputedFunction } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
} from "@json-render/react-three-fiber";
import { scenes } from "./scenes";
const catalog = defineCatalog(schema, {
components: {
...threeComponentDefinitions,
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
...threeComponents,
},
});
function num(v: unknown, fallback: number): number {
return typeof v === "number" ? v : fallback;
}
const computedFunctions: Record<string, ComputedFunction> = {
halfHeight: (a) => num(a.h, 1) / 2,
helixX: (a) => {
const angle =
(num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +
num(a.strand, 0) * Math.PI;
return num(a.radius, 1.8) * Math.cos(angle);
},
helixY: (a) => num(a.i, 0) * num(a.spacing, 0.3) + num(a.offset, -6),
helixZ: (a) => {
const angle =
(num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +
num(a.strand, 0) * Math.PI;
return num(a.radius, 1.8) * Math.sin(angle);
},
helixHue: (a) => {
const t = num(a.i, 0) / num(a.count, 40);
return `hsl(${Math.round(t * 270 + 180)}, 85%, 60%)`;
},
spiralX: (a) => {
const t = num(a.i, 0) / num(a.count, 60);
const r = 0.5 + t * num(a.maxRadius, 7);
const angle = t * num(a.turns, 4) * 2 * Math.PI;
return r * Math.cos(angle);
},
spiralZ: (a) => {
const t = num(a.i, 0) / num(a.count, 60);
const r = 0.5 + t * num(a.maxRadius, 7);
const angle = t * num(a.turns, 4) * 2 * Math.PI;
return r * Math.sin(angle);
},
spiralY: (a) => {
const i = num(a.i, 0);
return Math.sin(i * 1.7) * 0.4;
},
spiralScale: (a) => {
const t = num(a.i, 0) / num(a.count, 60);
return 0.08 + (1 - t) * 0.25;
},
spiralEmissive: (a) => {
const t = num(a.i, 0) / num(a.count, 60);
return Math.max(0, 1 - t * 1.5);
},
circleX: (a) => {
const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
return num(a.radius, 3) * Math.cos(angle);
},
circleZ: (a) => {
const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
return num(a.radius, 3) * Math.sin(angle);
},
circleY: (a) => {
const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
return Math.sin(angle * num(a.freq, 3)) * num(a.amp, 0.3);
},
circleAngle: (a) => {
return (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
},
circleHue: (a) => {
const t = num(a.i, 0) / num(a.count, 8);
return `hsl(${Math.round(t * 360)}, ${num(a.sat, 70)}%, ${num(a.lit, 80)}%)`;
},
};
function highlightJson(json: string): string {
return json.replace(
/("(?:\\.|[^"\\])*")\s*(:)|("(?:\\.|[^"\\])*")|([-+]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b)|([{}[\]:,])/g,
(_, key, colon, str, num, lit, punct) => {
if (key) return `<span style="color:#FF4D8D">${key}</span>${colon}`;
if (str) return `<span style="color:#00CA50">${str}</span>`;
if (num) return `<span style="color:#47A8FF">${num}</span>`;
if (lit) return `<span style="color:#47A8FF">${lit}</span>`;
if (punct) return `<span style="color:#666">${punct}</span>`;
return _;
},
);
}
const LIST_WIDTH = 220;
const JSON_WIDTH = 380;
const HEADER_HEIGHT = 40;
const headerStyle: React.CSSProperties = {
height: HEADER_HEIGHT,
display: "flex",
alignItems: "center",
padding: "0 16px",
fontSize: 11,
fontWeight: 600,
color: "#555",
letterSpacing: "0.08em",
textTransform: "uppercase",
fontFamily: "ui-monospace, monospace",
borderBottom: "1px solid #1e1e1e",
flexShrink: 0,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
boxSizing: "border-box",
};
export default function Page() {
const [selectedIndex, setSelectedIndex] = useState(0);
const selected = scenes[selectedIndex]!;
return (
<div style={{ height: "100vh", display: "flex", background: "#0a0a0a" }}>
<div
style={{
width: LIST_WIDTH,
flexShrink: 0,
display: "flex",
flexDirection: "column",
borderRight: "1px solid #1e1e1e",
background: "#0f0f0f",
}}
>
<div style={headerStyle}>Scenes</div>
<div style={{ flex: 1, overflowY: "auto", padding: "6px 0" }}>
{scenes.map((scene, i) => (
<button
key={scene.name}
onClick={() => setSelectedIndex(i)}
style={{
display: "block",
width: "100%",
padding: "8px 16px",
fontSize: 13,
border: "none",
textAlign: "left",
background:
i === selectedIndex
? "rgba(255,255,255,0.08)"
: "transparent",
color: i === selectedIndex ? "#fff" : "#888",
fontWeight: i === selectedIndex ? 500 : 400,
cursor: "pointer",
borderLeft:
i === selectedIndex
? "2px solid #fff"
: "2px solid transparent",
fontFamily: "inherit",
}}
>
{scene.name}
</button>
))}
</div>
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
minWidth: 0,
}}
>
<div style={headerStyle}>{selected.description}</div>
<div style={{ flex: 1, background: "#000", position: "relative" }}>
<ThreeCanvas
key={selectedIndex}
spec={selected.spec}
registry={registry}
functions={computedFunctions}
shadows
camera={{ position: [0, 0, 5], fov: 50 }}
style={{ width: "100%", height: "100%" }}
/>
</div>
</div>
<div
style={{
width: JSON_WIDTH,
flexShrink: 0,
display: "flex",
flexDirection: "column",
borderLeft: "1px solid #1e1e1e",
background: "#0d0d0d",
}}
>
<div style={headerStyle}>Spec JSON</div>
<pre
style={{
flex: 1,
margin: 0,
padding: 14,
overflowY: "auto",
overflowX: "auto",
fontSize: 11,
lineHeight: 1.6,
fontFamily:
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace",
color: "#EDEDED",
tabSize: 2,
}}
dangerouslySetInnerHTML={{
__html: highlightJson(JSON.stringify(selected.spec, null, 2)),
}}
/>
</div>
</div>
);
}
@@ -0,0 +1,9 @@
import type { Spec } from "@json-render/core";
export type Scene = { name: string; description: string; spec: Spec };
export const PI = Math.PI;
export function range(n: number) {
return Array.from({ length: n }, (_, i) => ({ i }));
}
@@ -0,0 +1,474 @@
import type { Scene } from "./_helpers";
export const clockworkOrrery: Scene = {
name: "Clockwork Orrery",
description:
"Interlocking brass/gold rings spinning at different tilts and speeds with orbiting metallic planets. Like the inside of a celestial clockwork mechanism.",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"env",
"ambient",
"key",
"warm",
"accent",
"center",
"ring-spin-0",
"ring-spin-1",
"ring-spin-2",
"ring-spin-3",
"planet-orbit-0",
"planet-orbit-1",
"planet-orbit-2",
"sparkles",
"post",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 3, 8],
fov: 50,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "dawn",
background: true,
blur: 1,
intensity: 0.5,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#332200", intensity: 0.4 },
children: [],
},
key: {
type: "DirectionalLight",
props: {
position: [5, 10, 5],
rotation: null,
scale: null,
color: "#ffddaa",
intensity: 2,
castShadow: true,
},
children: [],
},
warm: {
type: "PointLight",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
color: "#ffaa44",
intensity: 30,
distance: 15,
decay: 2,
castShadow: false,
},
children: [],
},
accent: {
type: "PointLight",
props: {
position: [-4, 3, -4],
rotation: null,
scale: null,
color: "#ffcc88",
intensity: 15,
distance: 15,
decay: 2,
castShadow: false,
},
children: [],
},
center: {
type: "Float",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.5,
rotationIntensity: 0.1,
floatIntensity: 0.2,
enabled: true,
},
children: ["center-sphere"],
},
"center-sphere": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.5,
widthSegments: 32,
heightSegments: 16,
color: "#ffddaa",
transmission: 1,
thickness: 0.8,
roughness: 0,
chromaticAberration: 0.1,
ior: 1.8,
distortion: 0.05,
distortionScale: null,
temporalDistortion: null,
samples: 10,
resolution: 256,
},
children: [],
},
"ring-spin-0": {
type: "Spin",
props: {
position: null,
rotation: [0.3, 0, 0.8],
scale: null,
speed: 1.2,
axis: "y",
},
children: ["ring-torus-0"],
},
"ring-torus-0": {
type: "Torus",
props: {
position: null,
rotation: [1.5708, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ffcc44",
metalness: 0.95,
roughness: 0.1,
emissive: "#cc9900",
emissiveIntensity: 0.4,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 2,
tube: 0.04,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"ring-spin-1": {
type: "Spin",
props: {
position: null,
rotation: [0.9, 0, -0.2],
scale: null,
speed: -0.7,
axis: "y",
},
children: ["ring-torus-1"],
},
"ring-torus-1": {
type: "Torus",
props: {
position: null,
rotation: [1.5708, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#cc8833",
metalness: 0.95,
roughness: 0.1,
emissive: "#aa6622",
emissiveIntensity: 0.3,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 3.2,
tube: 0.035,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"ring-spin-2": {
type: "Spin",
props: {
position: null,
rotation: [-0.4, 0, 0.5],
scale: null,
speed: 0.4,
axis: "y",
},
children: ["ring-torus-2"],
},
"ring-torus-2": {
type: "Torus",
props: {
position: null,
rotation: [1.5708, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#bb9944",
metalness: 0.95,
roughness: 0.1,
emissive: "#886622",
emissiveIntensity: 0.25,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 4.5,
tube: 0.03,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"ring-spin-3": {
type: "Spin",
props: {
position: null,
rotation: [0.1, 0, -0.1],
scale: null,
speed: -0.2,
axis: "y",
},
children: ["ring-torus-3"],
},
"ring-torus-3": {
type: "Torus",
props: {
position: null,
rotation: [1.5708, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#997744",
metalness: 0.95,
roughness: 0.1,
emissive: "#664422",
emissiveIntensity: 0.15,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 6,
tube: 0.025,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"planet-orbit-0": {
type: "Orbit",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.6,
radius: 2.5,
tilt: 0.3,
},
children: ["planet-spin-0"],
},
"planet-spin-0": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 3,
axis: "y",
},
children: ["planet-sphere-0"],
},
"planet-sphere-0": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ffffff",
metalness: 1,
roughness: 0.02,
emissive: "#ffffff",
emissiveIntensity: 0.2,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.2,
widthSegments: 32,
heightSegments: 16,
},
children: [],
},
"planet-orbit-1": {
type: "Orbit",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: -0.9,
radius: 3.8,
tilt: 0.8,
},
children: ["planet-spin-1"],
},
"planet-spin-1": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -2,
axis: "x",
},
children: ["planet-sphere-1"],
},
"planet-sphere-1": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ffcc88",
metalness: 0.8,
roughness: 0.15,
emissive: "#ffcc88",
emissiveIntensity: 0.2,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.15,
widthSegments: 32,
heightSegments: 16,
},
children: [],
},
"planet-orbit-2": {
type: "Orbit",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.35,
radius: 5.2,
tilt: -0.5,
},
children: ["planet-spin-2"],
},
"planet-spin-2": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.5,
axis: "z",
},
children: ["planet-sphere-2"],
},
"planet-sphere-2": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ccaa66",
metalness: 0.9,
roughness: 0.05,
emissive: "#ccaa66",
emissiveIntensity: 0.2,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.18,
widthSegments: 32,
heightSegments: 16,
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: null,
rotation: null,
scale: [14, 10, 14],
count: 150,
speed: 0.15,
opacity: 0.3,
color: "#ffcc66",
size: 1,
noise: 2,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 1.2,
luminanceThreshold: 0.2,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.5, darkness: 0.5 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 4,
maxDistance: 18,
minPolarAngle: 0.3,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.3,
},
children: [],
},
},
},
};
@@ -0,0 +1,692 @@
import type { Scene } from "./_helpers";
export const deepSeaAbyss: Scene = {
name: "Deep Sea Abyss",
description:
"Bioluminescent deep ocean. Pitch black with glowing jellyfish-like creatures.",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"env",
"fog",
"ambient",
"jelly-1-pulse",
"jelly-2-pulse",
"jelly-3-pulse",
"light-1",
"light-2",
"light-3",
"tentacles-1",
"tentacles-2",
"tentacles-3",
"sparkles-1",
"sparkles-2",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 1, 6],
fov: 55,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "night",
background: false,
blur: 1,
intensity: 0.05,
},
children: [],
},
fog: {
type: "Fog",
props: { color: "#000510", near: 2, far: 18 },
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#001133", intensity: 0.15 },
children: [],
},
"jelly-1-pulse": {
type: "Pulse",
props: {
position: [0, 1, 0],
rotation: null,
scale: null,
speed: 0.6,
min: 0.85,
max: 1.15,
},
children: ["jelly-1-spin"],
},
"jelly-1-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.2,
axis: "y",
},
children: ["jelly-1-body"],
},
"jelly-1-body": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 1,
widthSegments: 64,
heightSegments: 32,
color: "#00ccff",
speed: 2,
distort: 0.5,
metalness: 0,
roughness: 0.2,
},
children: [],
},
"jelly-2-pulse": {
type: "Pulse",
props: {
position: [-4, -1, -3],
rotation: null,
scale: null,
speed: 0.8,
min: 0.8,
max: 1.2,
},
children: ["jelly-2-spin"],
},
"jelly-2-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.3,
axis: "y",
},
children: ["jelly-2-body"],
},
"jelly-2-body": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.7,
widthSegments: 64,
heightSegments: 32,
color: "#cc44ff",
speed: 2.5,
distort: 0.6,
metalness: 0,
roughness: 0.2,
},
children: [],
},
"jelly-3-pulse": {
type: "Pulse",
props: {
position: [3, 0.5, -5],
rotation: null,
scale: null,
speed: 0.5,
min: 0.9,
max: 1.1,
},
children: ["jelly-3-spin"],
},
"jelly-3-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.15,
axis: "y",
},
children: ["jelly-3-body"],
},
"jelly-3-body": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.5,
widthSegments: 64,
heightSegments: 32,
color: "#ff44aa",
speed: 1.5,
distort: 0.4,
metalness: 0,
roughness: 0.3,
},
children: [],
},
"light-1": {
type: "PointLight",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
color: "#00ccff",
intensity: 20,
distance: 12,
decay: 2,
castShadow: null,
},
children: [],
},
"light-2": {
type: "PointLight",
props: {
position: [-4, -1, -3],
rotation: null,
scale: null,
color: "#cc44ff",
intensity: 15,
distance: 10,
decay: 2,
castShadow: null,
},
children: [],
},
"light-3": {
type: "PointLight",
props: {
position: [3, 0.5, -5],
rotation: null,
scale: null,
color: "#ff44aa",
intensity: 10,
distance: 8,
decay: 2,
castShadow: null,
},
children: [],
},
"tentacles-1": {
type: "Float",
props: {
position: [0, -1, 0],
rotation: null,
scale: null,
speed: 0.3,
rotationIntensity: 0.5,
floatIntensity: 0.8,
enabled: true,
},
children: ["t1-s1", "t1-s2", "t1-s3", "t1-s4", "t1-s5", "t1-s6"],
},
"t1-s1": {
type: "GlassSphere",
props: {
position: [0.3, -0.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.12,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s2": {
type: "GlassSphere",
props: {
position: [-0.2, -1.2, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.1,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s3": {
type: "GlassSphere",
props: {
position: [0.4, -1.9, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.08,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s4": {
type: "GlassSphere",
props: {
position: [-0.3, -2.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.07,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s5": {
type: "GlassSphere",
props: {
position: [0.1, -3.0, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.06,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t1-s6": {
type: "GlassSphere",
props: {
position: [-0.1, -3.4, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.05,
widthSegments: 16,
heightSegments: 8,
color: "#00ccff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"tentacles-2": {
type: "Float",
props: {
position: [-4, -2.5, -3],
rotation: null,
scale: null,
speed: 0.4,
rotationIntensity: 0.5,
floatIntensity: 0.8,
enabled: true,
},
children: ["t2-s1", "t2-s2", "t2-s3", "t2-s4", "t2-s5"],
},
"t2-s1": {
type: "GlassSphere",
props: {
position: [0.3, -0.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.1,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t2-s2": {
type: "GlassSphere",
props: {
position: [-0.2, -1.1, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.08,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t2-s3": {
type: "GlassSphere",
props: {
position: [0.4, -1.7, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.06,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t2-s4": {
type: "GlassSphere",
props: {
position: [-0.3, -2.2, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.05,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t2-s5": {
type: "GlassSphere",
props: {
position: [0.1, -2.6, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.05,
widthSegments: 16,
heightSegments: 8,
color: "#cc44ff",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"tentacles-3": {
type: "Float",
props: {
position: [3, -1, -5],
rotation: null,
scale: null,
speed: 0.35,
rotationIntensity: 0.5,
floatIntensity: 0.8,
enabled: true,
},
children: ["t3-s1", "t3-s2", "t3-s3", "t3-s4"],
},
"t3-s1": {
type: "GlassSphere",
props: {
position: [0.3, -0.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.09,
widthSegments: 16,
heightSegments: 8,
color: "#ff44aa",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t3-s2": {
type: "GlassSphere",
props: {
position: [-0.2, -1.0, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.07,
widthSegments: 16,
heightSegments: 8,
color: "#ff44aa",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t3-s3": {
type: "GlassSphere",
props: {
position: [0.4, -1.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.06,
widthSegments: 16,
heightSegments: 8,
color: "#ff44aa",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"t3-s4": {
type: "GlassSphere",
props: {
position: [-0.1, -1.9, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.05,
widthSegments: 16,
heightSegments: 8,
color: "#ff44aa",
transmission: 0.9,
thickness: 0.3,
roughness: 0.1,
chromaticAberration: 0.1,
ior: 1.5,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"sparkles-1": {
type: "Sparkles",
props: {
position: [0, 0, -3],
rotation: null,
scale: [15, 10, 15],
count: 300,
speed: 0.1,
opacity: 0.4,
color: "#00aaff",
size: 1,
noise: 3,
},
children: [],
},
"sparkles-2": {
type: "Sparkles",
props: {
position: [0, -2, 0],
rotation: null,
scale: [12, 6, 12],
count: 150,
speed: 0.05,
opacity: 0.2,
color: "#8844ff",
size: 0.8,
noise: 2,
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 3,
maxDistance: 18,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: true,
autoRotateSpeed: 0.15,
},
children: [],
},
},
},
};
@@ -0,0 +1,940 @@
import { PI } from "./_helpers";
import type { Scene } from "./_helpers";
export const floatingIslands: Scene = {
name: "Floating Islands",
description:
"Sky, clouds, islands with trees and bridges floating in sunset light",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"sky",
"ambient",
"sun",
"clouds",
"island-main-float",
"island-left-float",
"island-right-float",
"island-far-float",
"island-tiny1-float",
"island-tiny2-float",
"bridge-1",
"bridge-2",
"sparkles",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [8, 6, 14],
rotation: null,
scale: null,
fov: 50,
near: 0.1,
far: 500,
makeDefault: true,
},
children: [],
},
sky: {
type: "Environment",
props: {
preset: "dawn",
background: true,
blur: 0.8,
intensity: 0.5,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#ffd4a0", intensity: 0.5 },
children: [],
},
sun: {
type: "DirectionalLight",
props: {
position: [8, 12, 6],
rotation: null,
scale: null,
color: "#ffcc88",
intensity: 2.5,
castShadow: true,
},
children: [],
},
clouds: {
type: "Cloud",
props: {
position: [0, 4, -8],
rotation: null,
scale: null,
seed: 7,
segments: 40,
bounds: [25, 3, 20],
volume: 10,
speed: 0.1,
fade: 25,
opacity: 0.6,
color: "#ffe8d0",
growth: 8,
},
children: [],
},
"island-main-float": {
type: "Float",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.4,
rotationIntensity: 0.02,
floatIntensity: 0.3,
enabled: true,
},
children: ["island-main"],
},
"island-main": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"main-base",
"main-top",
"main-underhang",
"tree-1-trunk",
"tree-1-canopy",
"tree-2-trunk",
"tree-2-canopy",
"tree-3-trunk",
"tree-3-canopy",
"house-base",
"house-roof",
],
},
"main-base": {
type: "Cylinder",
props: {
position: [0, -0.5, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 3,
radiusBottom: 1.5,
height: 2,
radialSegments: 16,
},
children: [],
},
"main-top": {
type: "Cylinder",
props: {
position: [0, 0.55, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#4a8c3f",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 3.1,
radiusBottom: 3,
height: 0.3,
radialSegments: 16,
},
children: [],
},
"main-underhang": {
type: "Cone",
props: {
position: [0, -2.5, 0],
rotation: [PI, 0, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#6B4513",
metalness: 0.1,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1.5,
height: 2.5,
radialSegments: 12,
},
children: [],
},
"tree-1-trunk": {
type: "Cylinder",
props: {
position: [1, 1, 0.5],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.06,
radiusBottom: 0.1,
height: 1.5,
radialSegments: 8,
},
children: [],
},
"tree-1-canopy": {
type: "Sphere",
props: {
position: [1, 2.2, 0.5],
rotation: null,
scale: [1, 1.2, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#2d7a2d",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.6,
widthSegments: 16,
heightSegments: 12,
},
children: [],
},
"tree-2-trunk": {
type: "Cylinder",
props: {
position: [-1.2, 0.8, -0.8],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.05,
radiusBottom: 0.08,
height: 1.1,
radialSegments: 8,
},
children: [],
},
"tree-2-canopy": {
type: "Sphere",
props: {
position: [-1.2, 1.7, -0.8],
rotation: null,
scale: [1, 1.3, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#3a8c3a",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.45,
widthSegments: 16,
heightSegments: 12,
},
children: [],
},
"tree-3-trunk": {
type: "Cylinder",
props: {
position: [-0.3, 1.2, 1.5],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.07,
radiusBottom: 0.12,
height: 2,
radialSegments: 8,
},
children: [],
},
"tree-3-canopy": {
type: "Sphere",
props: {
position: [-0.3, 2.7, 1.5],
rotation: null,
scale: [1.2, 1.4, 1.2],
castShadow: true,
receiveShadow: false,
material: {
color: "#1a6b1a",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.7,
widthSegments: 16,
heightSegments: 12,
},
children: [],
},
"house-base": {
type: "RoundedBox",
props: {
position: [0, 0.9, -0.5],
rotation: [0, 0.3, 0],
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#e8d8c0",
metalness: 0,
roughness: 0.8,
emissive: "#ffddaa",
emissiveIntensity: 0.05,
opacity: null,
transparent: null,
wireframe: null,
},
width: 0.8,
height: 0.7,
depth: 0.7,
radius: 0.05,
smoothness: 4,
},
children: [],
},
"house-roof": {
type: "Cone",
props: {
position: [0, 1.55, -0.5],
rotation: [0, 1.085, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#a04030",
metalness: 0.1,
roughness: 0.8,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.6,
height: 0.5,
radialSegments: 4,
},
children: [],
},
"island-left-float": {
type: "Float",
props: {
position: [-6, 0.1, 2],
rotation: null,
scale: null,
speed: 0.6,
rotationIntensity: 0.03,
floatIntensity: 0.5,
enabled: true,
},
children: ["island-left"],
},
"island-left": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"left-base",
"left-top",
"left-under",
"left-tree-t",
"left-tree-c",
],
},
"left-base": {
type: "Cylinder",
props: {
position: [0, -0.3, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 1.8,
radiusBottom: 0.8,
height: 1.2,
radialSegments: 12,
},
children: [],
},
"left-top": {
type: "Cylinder",
props: {
position: [0, 0.25, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#4a8c3f",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 1.85,
radiusBottom: 1.8,
height: 0.2,
radialSegments: 12,
},
children: [],
},
"left-under": {
type: "Cone",
props: {
position: [0, -1.5, 0],
rotation: [PI, 0, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#6B4513",
metalness: 0.1,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.8,
height: 1.5,
radialSegments: 10,
},
children: [],
},
"left-tree-t": {
type: "Cylinder",
props: {
position: [0.3, 0.8, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.04,
radiusBottom: 0.07,
height: 1,
radialSegments: 8,
},
children: [],
},
"left-tree-c": {
type: "Sphere",
props: {
position: [0.3, 1.6, 0],
rotation: null,
scale: [1, 1.2, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#2d7a2d",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.4,
widthSegments: 12,
heightSegments: 8,
},
children: [],
},
"island-right-float": {
type: "Float",
props: {
position: [5.5, 0, -3],
rotation: null,
scale: null,
speed: 0.5,
rotationIntensity: 0.02,
floatIntensity: 0.4,
enabled: true,
},
children: ["island-right"],
},
"island-right": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"right-base",
"right-top",
"right-under",
"right-tree-t",
"right-tree-c",
],
},
"right-base": {
type: "Cylinder",
props: {
position: [0, -0.3, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 2,
radiusBottom: 1,
height: 1.5,
radialSegments: 12,
},
children: [],
},
"right-top": {
type: "Cylinder",
props: {
position: [0, 0.4, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#4a8c3f",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 2.05,
radiusBottom: 2,
height: 0.2,
radialSegments: 12,
},
children: [],
},
"right-under": {
type: "Cone",
props: {
position: [0, -2, 0],
rotation: [PI, 0, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#6B4513",
metalness: 0.1,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1,
height: 2,
radialSegments: 10,
},
children: [],
},
"right-tree-t": {
type: "Cylinder",
props: {
position: [-0.5, 0.9, 0.3],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#8B5A2B",
metalness: 0,
roughness: 1,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 0.05,
radiusBottom: 0.09,
height: 1.3,
radialSegments: 8,
},
children: [],
},
"right-tree-c": {
type: "Sphere",
props: {
position: [-0.5, 2, 0.3],
rotation: null,
scale: [1, 1.3, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#3a8c3a",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.5,
widthSegments: 12,
heightSegments: 8,
},
children: [],
},
"island-far-float": {
type: "Float",
props: {
position: [2, 3, -10],
rotation: null,
scale: [0.7, 0.7, 0.7],
speed: 0.3,
rotationIntensity: 0.01,
floatIntensity: 0.3,
enabled: true,
},
children: ["far-base", "far-top", "far-under"],
},
"far-base": {
type: "Cylinder",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 2.5,
radiusBottom: 1.2,
height: 1.5,
radialSegments: 12,
},
children: [],
},
"far-top": {
type: "Cylinder",
props: {
position: [0, 0.65, 0],
rotation: null,
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#3a7a3a",
metalness: 0,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radiusTop: 2.55,
radiusBottom: 2.5,
height: 0.2,
radialSegments: 12,
},
children: [],
},
"far-under": {
type: "Cone",
props: {
position: [0, -1.8, 0],
rotation: [PI, 0, 0],
scale: null,
castShadow: true,
receiveShadow: false,
material: {
color: "#6B4513",
metalness: 0.1,
roughness: 0.95,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1.2,
height: 2,
radialSegments: 10,
},
children: [],
},
"island-tiny1-float": {
type: "Float",
props: {
position: [-3, 2.5, -5],
rotation: null,
scale: [0.4, 0.4, 0.4],
speed: 0.8,
rotationIntensity: 0.05,
floatIntensity: 0.8,
enabled: true,
},
children: ["tiny1-rock"],
},
"tiny1-rock": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: [1, 0.6, 1],
castShadow: true,
receiveShadow: false,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1,
widthSegments: 12,
heightSegments: 8,
},
children: [],
},
"island-tiny2-float": {
type: "Float",
props: {
position: [8, -0.5, 1],
rotation: null,
scale: [0.3, 0.3, 0.3],
speed: 1,
rotationIntensity: 0.08,
floatIntensity: 1,
enabled: true,
},
children: ["tiny2-rock"],
},
"tiny2-rock": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: [1.2, 0.5, 0.8],
castShadow: true,
receiveShadow: false,
material: {
color: "#7a5a10",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 1,
widthSegments: 12,
heightSegments: 8,
},
children: [],
},
"bridge-1": {
type: "RoundedBox",
props: {
position: [-3.57, -0.1, 1.19],
rotation: [0, 0.32, 0],
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
width: 3.5,
height: 0.5,
depth: 0.6,
radius: 0.05,
smoothness: 2,
},
children: [],
},
"bridge-2": {
type: "RoundedBox",
props: {
position: [3.19, -0.1, -1.74],
rotation: [0, 0.5, 0],
scale: null,
castShadow: true,
receiveShadow: true,
material: {
color: "#8B6914",
metalness: 0.1,
roughness: 0.9,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
width: 3.0,
height: 0.5,
depth: 0.6,
radius: 0.05,
smoothness: 2,
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: [0, 2, 0],
rotation: null,
scale: [20, 8, 20],
count: 120,
speed: 0.1,
opacity: 0.5,
color: "#ffeeaa",
size: 1.5,
noise: 1,
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 8,
maxDistance: 30,
minPolarAngle: 0.2,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.3,
target: [0, 1, 0],
},
children: [],
},
},
},
};
@@ -0,0 +1,164 @@
import type { Scene } from "./_helpers";
export const hyperspaceTunnel: Scene = {
name: "Hyperspace Tunnel",
description:
"Neon warp tunnel at breakneck speed -- bloom, camera shake, particles streaming past",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"controls",
"tunnel",
"stars",
"sparkles-core",
"sparkles-wide",
"ambient",
"center-light",
"shake",
"post",
"fog",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 0, 2],
rotation: null,
scale: null,
fov: 80,
near: 0.1,
far: 200,
makeDefault: true,
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableRotate: false,
enableZoom: false,
enablePan: false,
},
children: [],
},
tunnel: {
type: "WarpTunnel",
props: {
position: null,
rotation: null,
scale: null,
ringCount: 100,
radius: 3,
length: 50,
speed: 12,
tubeRadius: 0.025,
color1: "#00ffff",
color2: "#ff00ff",
},
children: [],
},
stars: {
type: "Stars",
props: {
radius: 80,
depth: 60,
count: 6000,
factor: 5,
saturation: 0.4,
fade: true,
speed: 3,
},
children: [],
},
"sparkles-core": {
type: "Sparkles",
props: {
position: [0, 0, -10],
rotation: null,
scale: [4, 4, 30],
count: 300,
speed: 3,
opacity: 1,
color: "#00ffff",
size: 2,
noise: 0.5,
},
children: [],
},
"sparkles-wide": {
type: "Sparkles",
props: {
position: [0, 0, -15],
rotation: null,
scale: [6, 6, 40],
count: 200,
speed: 4,
opacity: 0.7,
color: "#ff44ff",
size: 1.5,
noise: 1,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#110033", intensity: 0.2 },
children: [],
},
"center-light": {
type: "PointLight",
props: {
position: [0, 0, -5],
rotation: null,
scale: null,
color: "#00ccff",
intensity: 30,
distance: 30,
decay: 2,
castShadow: false,
},
children: [],
},
shake: {
type: "CameraShake",
props: {
intensity: 0.3,
maxYaw: 0.03,
maxPitch: 0.03,
maxRoll: 0.02,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 2.5,
luminanceThreshold: 0.1,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.3, darkness: 0.9 },
children: [],
},
fog: {
type: "Fog",
props: { color: "#030008", near: 5, far: 50 },
children: [],
},
},
},
};
@@ -0,0 +1,29 @@
import type { Scene } from "./_helpers";
import { hyperspaceTunnel } from "./hyperspace-tunnel";
import { orbitalChaos } from "./orbital-chaos";
import { clockworkOrrery } from "./clockwork-orrery";
import { deepSeaAbyss } from "./deep-sea-abyss";
import { stormCell } from "./storm-cell";
import { perpetualMotion } from "./perpetual-motion";
import { floatingIslands } from "./floating-islands";
import { productShowroom } from "./product-showroom";
import { portalGallery } from "./portal-gallery";
import { pipes } from "./pipes";
import { mystify } from "./mystify";
import { starfield } from "./starfield";
export type { Scene };
export const scenes: Scene[] = [
hyperspaceTunnel,
orbitalChaos,
clockworkOrrery,
deepSeaAbyss,
stormCell,
perpetualMotion,
floatingIslands,
productShowroom,
portalGallery,
pipes,
mystify,
starfield,
];
@@ -0,0 +1,400 @@
import type { Scene } from "./_helpers";
export const mystify: Scene = {
name: "Mystify",
description:
"Neon wireframe torus knots orbiting in the void -- the Windows Mystify screensaver in 3D",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"ambient",
"shape1",
"shape2",
"shape3",
"shape4",
"shape5",
"shape6",
"shape7",
"post",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 0, 10],
rotation: null,
scale: null,
fov: 55,
near: 0.1,
far: 1000,
makeDefault: true,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#111111", intensity: 0.1 },
children: [],
},
shape1: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.5,
radius: 3,
tilt: 0.3,
},
children: ["s1-spin"],
},
"s1-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.5,
axis: "x",
},
children: ["s1-body"],
},
"s1-body": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.4,
tube: 0.02,
p: 2,
q: 3,
tubularSegments: 128,
radialSegments: 4,
material: {
color: "#00ffff",
emissive: "#00ffff",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape2: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.4,
radius: 4,
tilt: -0.5,
},
children: ["s2-spin"],
},
"s2-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -1,
axis: "z",
},
children: ["s2-body"],
},
"s2-body": {
type: "Torus",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.6,
tube: 0.015,
radialSegments: 4,
tubularSegments: 128,
material: {
color: "#ff00ff",
emissive: "#ff00ff",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape3: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.7,
radius: 2.5,
tilt: 0.8,
},
children: ["s3-spin"],
},
"s3-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 2,
axis: "y",
},
children: ["s3-body"],
},
"s3-body": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.35,
tube: 0.015,
p: 3,
q: 2,
tubularSegments: 128,
radialSegments: 4,
material: {
color: "#ffff00",
emissive: "#ffff00",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape4: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.6,
radius: 3.5,
tilt: -0.2,
},
children: ["s4-spin"],
},
"s4-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.2,
axis: "x",
},
children: ["s4-body"],
},
"s4-body": {
type: "Torus",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.5,
tube: 0.012,
radialSegments: 4,
tubularSegments: 128,
material: {
color: "#00ff44",
emissive: "#00ff44",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape5: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.3,
radius: 5,
tilt: 0.6,
},
children: ["s5-spin"],
},
"s5-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -1.8,
axis: "z",
},
children: ["s5-body"],
},
"s5-body": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.5,
tube: 0.018,
p: 2,
q: 5,
tubularSegments: 128,
radialSegments: 4,
material: {
color: "#ff4400",
emissive: "#ff4400",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape6: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.8,
radius: 2,
tilt: -0.7,
},
children: ["s6-spin"],
},
"s6-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 2.5,
axis: "y",
},
children: ["s6-body"],
},
"s6-body": {
type: "Torus",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.3,
tube: 0.01,
radialSegments: 4,
tubularSegments: 128,
material: {
color: "#4488ff",
emissive: "#4488ff",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
shape7: {
type: "Orbit",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.45,
radius: 4.5,
tilt: 1,
},
children: ["s7-spin"],
},
"s7-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.8,
axis: "x",
},
children: ["s7-body"],
},
"s7-body": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.45,
tube: 0.015,
p: 3,
q: 4,
tubularSegments: 128,
radialSegments: 4,
material: {
color: "#ff88ff",
emissive: "#ff88ff",
emissiveIntensity: 2,
wireframe: true,
},
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 3,
luminanceThreshold: 0.05,
luminanceSmoothing: 0.3,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.2, darkness: 0.8 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: true,
enableRotate: true,
minDistance: 4,
maxDistance: 18,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: true,
autoRotateSpeed: 0.3,
},
children: [],
},
},
},
};
@@ -0,0 +1,554 @@
import { PI } from "./_helpers";
import type { Scene } from "./_helpers";
export const orbitalChaos: Scene = {
name: "Orbital Chaos",
description:
"Everything in motion -- orbiting glass, spinning knots, morphing core, all with bloom",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"env",
"stars",
"ambient",
"core-light",
"accent-1",
"accent-2",
"core-pulse",
"orbit-1",
"orbit-2",
"orbit-3",
"orbit-4",
"orbit-5",
"orbit-6",
"ring-spin-fast",
"ring-spin-slow",
"sparkles",
"post",
"fog",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 2, 8],
fov: 50,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: { preset: "night", background: false, blur: 1, intensity: 0.15 },
children: [],
},
stars: {
type: "Stars",
props: {
radius: 120,
depth: 60,
count: 8000,
factor: 5,
saturation: 0.2,
fade: true,
speed: 0.5,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#110022", intensity: 0.3 },
children: [],
},
"core-light": {
type: "PointLight",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
color: "#ff44aa",
intensity: 80,
distance: 25,
decay: 2,
castShadow: false,
},
children: [],
},
"accent-1": {
type: "PointLight",
props: {
position: [5, 3, 5],
rotation: null,
scale: null,
color: "#00ffff",
intensity: 25,
distance: 20,
decay: 2,
castShadow: false,
},
children: [],
},
"accent-2": {
type: "PointLight",
props: {
position: [-5, -2, -5],
rotation: null,
scale: null,
color: "#ff00ff",
intensity: 25,
distance: 20,
decay: 2,
castShadow: false,
},
children: [],
},
"core-pulse": {
type: "Pulse",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.8,
min: 0.85,
max: 1.15,
},
children: ["core-spin"],
},
"core-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.3,
axis: "y",
},
children: ["core-blob"],
},
"core-blob": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 1.2,
widthSegments: 64,
heightSegments: 32,
color: "#ff2288",
speed: 4,
distort: 0.6,
metalness: 0.1,
roughness: 0.1,
},
children: [],
},
"orbit-1": {
type: "Orbit",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.8,
radius: 4,
tilt: 0.5,
},
children: ["orb-1-spin"],
},
"orb-1-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 2,
axis: "x",
},
children: ["orb-1"],
},
"orb-1": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.4,
widthSegments: 32,
heightSegments: 16,
color: "#00ffff",
transmission: 1,
thickness: 0.5,
roughness: 0,
chromaticAberration: 0.15,
ior: 1.8,
distortion: 0.1,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"orbit-2": {
type: "Orbit",
props: {
position: [0, 0.5, 0],
rotation: null,
scale: null,
speed: -0.6,
radius: 5.5,
tilt: 1,
},
children: ["orb-2-spin"],
},
"orb-2-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 3,
axis: "z",
},
children: ["orb-2"],
},
"orb-2": {
type: "TorusKnot",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ff00ff",
metalness: 0.9,
roughness: 0.1,
emissive: "#ff00ff",
emissiveIntensity: 0.8,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 0.35,
tube: 0.12,
tubularSegments: 64,
radialSegments: 8,
p: 2,
q: 3,
},
children: [],
},
"orbit-3": {
type: "Orbit",
props: {
position: [0, -0.3, 0],
rotation: null,
scale: null,
speed: 1.2,
radius: 3.2,
tilt: 0.3,
},
children: ["orb-3"],
},
"orb-3": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.3,
widthSegments: 32,
heightSegments: 16,
color: "#ff8800",
transmission: 1,
thickness: 0.4,
roughness: 0,
chromaticAberration: 0.1,
ior: 1.6,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"orbit-4": {
type: "Orbit",
props: {
position: [0, 1, 0],
rotation: null,
scale: null,
speed: -1.5,
radius: 6.5,
tilt: -0.8,
},
children: ["orb-4-spin"],
},
"orb-4-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -4,
axis: "y",
},
children: ["orb-4"],
},
"orb-4": {
type: "Sphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#00ff88",
metalness: null,
roughness: null,
emissive: "#00ff88",
emissiveIntensity: 0.8,
opacity: null,
transparent: null,
wireframe: true,
},
radius: 0.35,
widthSegments: 16,
heightSegments: 12,
},
children: [],
},
"orbit-5": {
type: "Orbit",
props: {
position: [0, -0.5, 0],
rotation: null,
scale: null,
speed: 0.4,
radius: 8,
tilt: 0.2,
},
children: ["orb-5-spin"],
},
"orb-5-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.5,
axis: "z",
},
children: ["orb-5"],
},
"orb-5": {
type: "GlassBox",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
width: 0.5,
height: 0.5,
depth: 0.5,
color: "#ff44ff",
transmission: 1,
thickness: 0.4,
roughness: 0,
chromaticAberration: 0.08,
ior: 1.7,
distortion: 0,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
"orbit-6": {
type: "Orbit",
props: {
position: [0, 0.8, 0],
rotation: null,
scale: null,
speed: 2,
radius: 2.2,
tilt: 1.5,
},
children: ["orb-6"],
},
"orb-6": {
type: "DistortSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: false,
receiveShadow: false,
radius: 0.25,
widthSegments: 32,
heightSegments: 16,
color: "#ffcc00",
speed: 5,
distort: 0.7,
metalness: 0.5,
roughness: 0.1,
},
children: [],
},
"ring-spin-fast": {
type: "Spin",
props: {
position: null,
rotation: [0.3, 0, 0],
scale: null,
speed: 0.5,
axis: "y",
},
children: ["ring-inner"],
},
"ring-inner": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#00ffff",
metalness: 0.9,
roughness: 0.1,
emissive: "#00ccff",
emissiveIntensity: 0.5,
opacity: 0.3,
transparent: true,
wireframe: null,
},
radius: 9,
tube: 0.015,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"ring-spin-slow": {
type: "Spin",
props: {
position: null,
rotation: [-0.5, 0, 0.3],
scale: null,
speed: -0.3,
axis: "y",
},
children: ["ring-outer"],
},
"ring-outer": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ff00ff",
metalness: 0.9,
roughness: 0.1,
emissive: "#ff00aa",
emissiveIntensity: 0.3,
opacity: 0.2,
transparent: true,
wireframe: null,
},
radius: 11,
tube: 0.012,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: null,
rotation: null,
scale: [18, 18, 18],
count: 400,
speed: 0.4,
opacity: 0.5,
color: "#ffddaa",
size: 1.2,
noise: 2,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 2,
luminanceThreshold: 0.15,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.4, darkness: 0.7 },
children: [],
},
fog: {
type: "Fog",
props: { color: "#030008", near: 10, far: 40 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 5,
maxDistance: 25,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: true,
autoRotateSpeed: 0.4,
},
children: [],
},
},
},
};
@@ -0,0 +1,307 @@
import { PI } from "./_helpers";
import type { Scene } from "./_helpers";
export const perpetualMotion: Scene = {
name: "Perpetual Motion Machine",
description:
"A gyroscope/gimbal with 3 nested spinning rings on different axes, with a perfectly still glass sphere floating at the center. Clean, minimal, mesmerizing.",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"env",
"ambient",
"key",
"fill",
"outer-spin",
"sparkles",
"floor",
"shadows",
"post",
"controls",
],
},
env: {
type: "Environment",
props: {
preset: "studio",
background: false,
blur: null,
intensity: 1,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#ffffff", intensity: 0.3 },
children: [],
},
key: {
type: "DirectionalLight",
props: {
position: [5, 8, 5],
rotation: null,
scale: null,
color: "#ffffff",
intensity: 2,
castShadow: true,
},
children: [],
},
fill: {
type: "PointLight",
props: {
position: [-3, 4, -3],
rotation: null,
scale: null,
color: "#aaccff",
intensity: 10,
distance: 15,
decay: 2,
castShadow: false,
},
children: [],
},
"outer-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.4,
axis: "y",
},
children: ["outer-ring", "mid-spin"],
},
"outer-ring": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#cccccc",
metalness: 1,
roughness: 0.05,
emissive: "#ffffff",
emissiveIntensity: 0.05,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 4,
tube: 0.06,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"mid-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: -0.8,
axis: "x",
},
children: ["mid-ring", "inner-spin"],
},
"mid-ring": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#ffcc44",
metalness: 0.95,
roughness: 0.08,
emissive: "#cc9900",
emissiveIntensity: 0.1,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 3,
tube: 0.05,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"inner-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 1.5,
axis: "z",
},
children: ["inner-ring", "center-float"],
},
"inner-ring": {
type: "Torus",
props: {
position: null,
rotation: [PI / 2, 0, 0],
scale: null,
castShadow: false,
receiveShadow: false,
material: {
color: "#cc8844",
metalness: 0.9,
roughness: 0.1,
emissive: "#996633",
emissiveIntensity: 0.1,
opacity: null,
transparent: null,
wireframe: null,
},
radius: 2,
tube: 0.04,
radialSegments: null,
tubularSegments: 128,
},
children: [],
},
"center-float": {
type: "Float",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
speed: 0.3,
rotationIntensity: 0,
floatIntensity: 0.1,
enabled: true,
},
children: ["center-orb"],
},
"center-orb": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: true,
receiveShadow: false,
radius: 0.8,
widthSegments: 64,
heightSegments: 32,
color: "#ffffff",
transmission: 1,
thickness: 1,
roughness: 0,
chromaticAberration: 0.12,
ior: 2,
distortion: 0.1,
distortionScale: 0.4,
temporalDistortion: 0.4,
samples: 10,
resolution: 256,
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: [0, 0, 0],
rotation: null,
scale: [10, 10, 10],
count: 100,
speed: 0.1,
opacity: 0.2,
color: "#ccddff",
size: 0.8,
noise: 2,
},
children: [],
},
floor: {
type: "ReflectorPlane",
props: {
position: [0, -4.5, 0],
rotation: [-PI / 2, 0, 0],
scale: null,
width: 25,
height: 25,
color: "#1a1a2a",
resolution: 1024,
blur: 400,
mirror: 0.7,
mixBlur: 10,
mixStrength: 2,
depthScale: 0.2,
metalness: 0.5,
roughness: 1,
},
children: [],
},
shadows: {
type: "ContactShadows",
props: {
position: [0, -4.5, 0],
rotation: null,
scale: null,
opacity: 0.3,
width: 12,
height: 12,
blur: 2.5,
near: null,
far: 10,
smooth: true,
resolution: 512,
frames: null,
color: "#000000",
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 0.8,
luminanceThreshold: 0.25,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.5, darkness: 0.4 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 5,
maxDistance: 16,
minPolarAngle: 0.3,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.3,
},
children: [],
},
},
},
};
@@ -0,0 +1,764 @@
import type { Scene } from "./_helpers";
const goldMaterial = {
color: "#cc8833",
metalness: 1,
roughness: 0.05,
};
const blueMaterial = {
color: "#3388cc",
metalness: 1,
roughness: 0.05,
};
const magentaMaterial = {
color: "#cc3388",
metalness: 1,
roughness: 0.05,
};
export const pipes: Scene = {
name: "Pipes",
description:
"Classic 3D Pipes screensaver -- chrome pipes snaking through space at right angles",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"camera",
"env",
"ambient",
"directional",
"pipe-system",
"post",
"fog",
"controls",
],
},
camera: {
type: "PerspectiveCamera",
props: {
position: [0, 3, 12],
rotation: null,
scale: null,
fov: 50,
near: null,
far: null,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "warehouse",
background: false,
blur: 0.5,
intensity: 1,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#ffffff", intensity: 0.3 },
children: [],
},
directional: {
type: "DirectionalLight",
props: {
position: [5, 10, 5],
rotation: null,
scale: null,
color: "#ffffff",
intensity: 1,
castShadow: null,
},
children: [],
},
"pipe-system": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.1,
axis: "y",
},
children: ["pipe1-group", "pipe2-group", "pipe3-group"],
},
"pipe1-group": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"p1-j0",
"p1-s1",
"p1-j1",
"p1-s2",
"p1-j2",
"p1-s3",
"p1-j3",
"p1-s4",
"p1-j4",
"p1-s5",
"p1-j5",
"p1-s6",
"p1-j6",
],
},
"p1-j0": {
type: "Sphere",
props: {
position: [-3, -3, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j1": {
type: "Sphere",
props: {
position: [-3, 0, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j2": {
type: "Sphere",
props: {
position: [1, 0, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j3": {
type: "Sphere",
props: {
position: [1, 2, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j4": {
type: "Sphere",
props: {
position: [1, 2, -3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j5": {
type: "Sphere",
props: {
position: [1, 0, -3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-j6": {
type: "Sphere",
props: {
position: [-1, 0, -3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p1-s1": {
type: "Cylinder",
props: {
position: [-3, -1.5, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p1-s2": {
type: "Cylinder",
props: {
position: [-1, 0, 0],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 4,
radialSegments: 12,
},
children: [],
},
"p1-s3": {
type: "Cylinder",
props: {
position: [1, 1, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"p1-s4": {
type: "Cylinder",
props: {
position: [1, 2, -1.5],
rotation: [1.5708, 0, 0],
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p1-s5": {
type: "Cylinder",
props: {
position: [1, 1, -3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"p1-s6": {
type: "Cylinder",
props: {
position: [0, 0, -3],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: goldMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"pipe2-group": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"p2-j0",
"p2-s1",
"p2-j1",
"p2-s2",
"p2-j2",
"p2-s3",
"p2-j3",
"p2-s4",
"p2-j4",
"p2-s5",
"p2-j5",
"p2-s6",
"p2-j6",
],
},
"p2-j0": {
type: "Sphere",
props: {
position: [2, -3, 2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j1": {
type: "Sphere",
props: {
position: [2, 1, 2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j2": {
type: "Sphere",
props: {
position: [2, 1, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j3": {
type: "Sphere",
props: {
position: [-1, 1, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j4": {
type: "Sphere",
props: {
position: [-1, 4, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j5": {
type: "Sphere",
props: {
position: [-3, 4, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-j6": {
type: "Sphere",
props: {
position: [-3, 4, 1],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p2-s1": {
type: "Cylinder",
props: {
position: [2, -1, 2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 4,
radialSegments: 12,
},
children: [],
},
"p2-s2": {
type: "Cylinder",
props: {
position: [2, 1, 0],
rotation: [1.5708, 0, 0],
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 4,
radialSegments: 12,
},
children: [],
},
"p2-s3": {
type: "Cylinder",
props: {
position: [0.5, 1, -2],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p2-s4": {
type: "Cylinder",
props: {
position: [-1, 2.5, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p2-s5": {
type: "Cylinder",
props: {
position: [-2, 4, -2],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"p2-s6": {
type: "Cylinder",
props: {
position: [-3, 4, -0.5],
rotation: [1.5708, 0, 0],
scale: null,
castShadow: null,
receiveShadow: null,
material: blueMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"pipe3-group": {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"p3-j0",
"p3-s1",
"p3-j1",
"p3-s2",
"p3-j2",
"p3-s3",
"p3-j3",
"p3-s4",
"p3-j4",
"p3-s5",
"p3-j5",
],
},
"p3-j0": {
type: "Sphere",
props: {
position: [-2, 3, 3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j1": {
type: "Sphere",
props: {
position: [-2, -2, 3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j2": {
type: "Sphere",
props: {
position: [1, -2, 3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j3": {
type: "Sphere",
props: {
position: [1, -2, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j4": {
type: "Sphere",
props: {
position: [1, 0, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-j5": {
type: "Sphere",
props: {
position: [-3, 0, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radius: 0.12,
widthSegments: null,
heightSegments: null,
},
children: [],
},
"p3-s1": {
type: "Cylinder",
props: {
position: [-2, 0.5, 3],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 5,
radialSegments: 12,
},
children: [],
},
"p3-s2": {
type: "Cylinder",
props: {
position: [-0.5, -2, 3],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 3,
radialSegments: 12,
},
children: [],
},
"p3-s3": {
type: "Cylinder",
props: {
position: [1, -2, 0.5],
rotation: [1.5708, 0, 0],
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 5,
radialSegments: 12,
},
children: [],
},
"p3-s4": {
type: "Cylinder",
props: {
position: [1, -1, -2],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 2,
radialSegments: 12,
},
children: [],
},
"p3-s5": {
type: "Cylinder",
props: {
position: [-1, 0, -2],
rotation: [0, 0, 1.5708],
scale: null,
castShadow: null,
receiveShadow: null,
material: magentaMaterial,
radiusTop: 0.08,
radiusBottom: 0.08,
height: 4,
radialSegments: 12,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 0.4,
luminanceThreshold: 0.4,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.3, darkness: 0.4 },
children: [],
},
fog: {
type: "Fog",
props: { color: "#111111", near: 10, far: 30 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: null,
enablePan: null,
enableRotate: null,
minDistance: 5,
maxDistance: 20,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: true,
autoRotateSpeed: 0.6,
},
children: [],
},
},
},
};
@@ -0,0 +1,809 @@
import type { Scene } from "./_helpers";
export const portalGallery: Scene = {
name: "Portal Gallery",
description:
"Three framed portals into different worlds -- sunset clouds, starfield, and enchanted forest",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"env",
"ambient",
"directional",
"floor",
"shadows",
"frame1",
"frame2",
"frame3",
"gallery-sparkles",
"post",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 2, 7],
rotation: null,
scale: null,
fov: 50,
near: 0.1,
far: 200,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "lobby",
background: false,
blur: 0,
intensity: 0.4,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#e8e0d8", intensity: 0.5 },
children: [],
},
directional: {
type: "DirectionalLight",
props: {
position: [5, 8, 3],
rotation: null,
scale: null,
color: "#ffffff",
intensity: 1,
castShadow: true,
},
children: [],
},
floor: {
type: "ReflectorPlane",
props: {
position: [0, 0, 0],
rotation: [-1.5708, 0, 0],
scale: null,
width: 30,
height: 30,
color: "#2a2a2a",
resolution: 1024,
blur: 400,
mirror: 0.4,
mixBlur: 10,
mixStrength: 1.5,
metalness: 0.5,
roughness: 0.8,
},
children: [],
},
shadows: {
type: "ContactShadows",
props: {
position: [0, 0.01, 0],
rotation: null,
scale: null,
opacity: 0.4,
width: 20,
height: 20,
blur: 2.5,
far: 8,
color: "#000000",
},
children: [],
},
// --- PORTAL 1 (left, sunset world) ---
frame1: {
type: "Group",
props: {
position: [-3.5, 2, -2],
rotation: [0, 0.3, 0],
scale: null,
},
children: [
"frame1-top",
"frame1-bottom",
"frame1-left",
"frame1-right",
"portal1-mesh",
],
},
"frame1-top": {
type: "Box",
props: {
position: [0, 1.55, 0],
rotation: null,
scale: null,
width: 2.3,
height: 0.15,
depth: 0.15,
material: { color: "#8B7355", metalness: 0.3, roughness: 0.6 },
},
children: [],
},
"frame1-bottom": {
type: "Box",
props: {
position: [0, -1.55, 0],
rotation: null,
scale: null,
width: 2.3,
height: 0.15,
depth: 0.15,
material: { color: "#8B7355", metalness: 0.3, roughness: 0.6 },
},
children: [],
},
"frame1-left": {
type: "Box",
props: {
position: [-1.075, 0, 0],
rotation: null,
scale: null,
width: 0.15,
height: 3.25,
depth: 0.15,
material: { color: "#8B7355", metalness: 0.3, roughness: 0.6 },
},
children: [],
},
"frame1-right": {
type: "Box",
props: {
position: [1.075, 0, 0],
rotation: null,
scale: null,
width: 0.15,
height: 3.25,
depth: 0.15,
material: { color: "#8B7355", metalness: 0.3, roughness: 0.6 },
},
children: [],
},
"portal1-mesh": {
type: "Plane",
props: {
position: null,
rotation: null,
scale: null,
width: 2,
height: 3,
},
children: ["portal1-mat"],
},
"portal1-mat": {
type: "MeshPortalMaterial",
props: { blend: 0, blur: 0.5, resolution: 512 },
children: [
"p1-sky",
"p1-sun-light",
"p1-cloud1",
"p1-cloud2",
"p1-cloud3",
"p1-ground",
],
},
"p1-sky": {
type: "Sky",
props: {
distance: null,
sunPosition: [100, 10, 50],
inclination: null,
azimuth: null,
rayleigh: 2,
turbidity: 10,
mieCoefficient: 0.005,
mieDirectionalG: null,
},
children: [],
},
"p1-sun-light": {
type: "DirectionalLight",
props: {
position: [5, 5, 3],
rotation: null,
scale: null,
color: "#ffaa55",
intensity: 2,
castShadow: false,
},
children: [],
},
"p1-cloud1": {
type: "Cloud",
props: {
position: [-3, 4, -5],
rotation: null,
scale: null,
seed: 1,
segments: 20,
bounds: [6, 2, 3],
opacity: 0.6,
color: "#ffddaa",
speed: 0.1,
volume: 5,
fade: null,
growth: null,
},
children: [],
},
"p1-cloud2": {
type: "Cloud",
props: {
position: [4, 5, -8],
rotation: null,
scale: null,
seed: 7,
segments: 25,
bounds: [8, 2, 4],
opacity: 0.5,
color: "#ffccaa",
speed: 0.08,
volume: 6,
fade: null,
growth: null,
},
children: [],
},
"p1-cloud3": {
type: "Cloud",
props: {
position: [0, 3.5, -4],
rotation: null,
scale: null,
seed: 14,
segments: 15,
bounds: [5, 1.5, 2.5],
opacity: 0.7,
color: "#ffeedd",
speed: 0.12,
volume: 4,
fade: null,
growth: null,
},
children: [],
},
"p1-ground": {
type: "Plane",
props: {
position: [0, -2, 0],
rotation: [-1.5708, 0, 0],
scale: null,
width: 30,
height: 30,
material: { color: "#886644", roughness: 1 },
},
children: [],
},
// --- PORTAL 2 (center, night/stars world) ---
frame2: {
type: "Group",
props: {
position: [0, 2, -3],
rotation: null,
scale: null,
},
children: [
"frame2-top",
"frame2-bottom",
"frame2-left",
"frame2-right",
"portal2-mesh",
],
},
"frame2-top": {
type: "Box",
props: {
position: [0, 1.55, 0],
rotation: null,
scale: null,
width: 2.3,
height: 0.15,
depth: 0.15,
material: { color: "#555566", metalness: 0.8, roughness: 0.2 },
},
children: [],
},
"frame2-bottom": {
type: "Box",
props: {
position: [0, -1.55, 0],
rotation: null,
scale: null,
width: 2.3,
height: 0.15,
depth: 0.15,
material: { color: "#555566", metalness: 0.8, roughness: 0.2 },
},
children: [],
},
"frame2-left": {
type: "Box",
props: {
position: [-1.075, 0, 0],
rotation: null,
scale: null,
width: 0.15,
height: 3.25,
depth: 0.15,
material: { color: "#555566", metalness: 0.8, roughness: 0.2 },
},
children: [],
},
"frame2-right": {
type: "Box",
props: {
position: [1.075, 0, 0],
rotation: null,
scale: null,
width: 0.15,
height: 3.25,
depth: 0.15,
material: { color: "#555566", metalness: 0.8, roughness: 0.2 },
},
children: [],
},
"portal2-mesh": {
type: "Plane",
props: {
position: null,
rotation: null,
scale: null,
width: 2,
height: 3,
},
children: ["portal2-mat"],
},
"portal2-mat": {
type: "MeshPortalMaterial",
props: { blend: 0, blur: 0.5, resolution: 512 },
children: [
"p2-stars",
"p2-ambient",
"p2-nebula1",
"p2-nebula2",
"p2-sparkles",
],
},
"p2-stars": {
type: "Stars",
props: {
radius: 80,
depth: 40,
count: 6000,
factor: 5,
saturation: 0.5,
fade: true,
speed: 0.3,
},
children: [],
},
"p2-ambient": {
type: "AmbientLight",
props: { color: "#111133", intensity: 0.3 },
children: [],
},
"p2-nebula1": {
type: "Sphere",
props: {
position: [3, 2, -8],
rotation: null,
scale: null,
radius: 2,
widthSegments: null,
heightSegments: null,
material: {
color: "#4400aa",
emissive: "#4400aa",
emissiveIntensity: 0.5,
transparent: true,
opacity: 0.3,
},
},
children: [],
},
"p2-nebula2": {
type: "Sphere",
props: {
position: [-4, -1, -10],
rotation: null,
scale: null,
radius: 3,
widthSegments: null,
heightSegments: null,
material: {
color: "#aa0044",
emissive: "#aa0044",
emissiveIntensity: 0.4,
transparent: true,
opacity: 0.25,
},
},
children: [],
},
"p2-sparkles": {
type: "Sparkles",
props: {
position: [0, 0, -5],
rotation: null,
scale: [10, 10, 10],
count: 200,
speed: 0.2,
opacity: 0.8,
color: "#aabbff",
size: 2,
noise: 3,
},
children: [],
},
// --- PORTAL 3 (right, forest/nature world) ---
frame3: {
type: "Group",
props: {
position: [3.5, 2, -2],
rotation: [0, -0.3, 0],
scale: null,
},
children: [
"frame3-top",
"frame3-bottom",
"frame3-left",
"frame3-right",
"portal3-mesh",
],
},
"frame3-top": {
type: "Box",
props: {
position: [0, 1.55, 0],
rotation: null,
scale: null,
width: 2.3,
height: 0.15,
depth: 0.15,
material: { color: "#6B4423", metalness: 0.1, roughness: 0.8 },
},
children: [],
},
"frame3-bottom": {
type: "Box",
props: {
position: [0, -1.55, 0],
rotation: null,
scale: null,
width: 2.3,
height: 0.15,
depth: 0.15,
material: { color: "#6B4423", metalness: 0.1, roughness: 0.8 },
},
children: [],
},
"frame3-left": {
type: "Box",
props: {
position: [-1.075, 0, 0],
rotation: null,
scale: null,
width: 0.15,
height: 3.25,
depth: 0.15,
material: { color: "#6B4423", metalness: 0.1, roughness: 0.8 },
},
children: [],
},
"frame3-right": {
type: "Box",
props: {
position: [1.075, 0, 0],
rotation: null,
scale: null,
width: 0.15,
height: 3.25,
depth: 0.15,
material: { color: "#6B4423", metalness: 0.1, roughness: 0.8 },
},
children: [],
},
"portal3-mesh": {
type: "Plane",
props: {
position: null,
rotation: null,
scale: null,
width: 2,
height: 3,
},
children: ["portal3-mat"],
},
"portal3-mat": {
type: "MeshPortalMaterial",
props: { blend: 0, blur: 0.5, resolution: 512 },
children: [
"p3-env",
"p3-ambient",
"p3-sun",
"p3-tree1",
"p3-tree2",
"p3-tree3",
"p3-tree4",
"p3-tree5",
"p3-ground",
"p3-fireflies",
],
},
"p3-env": {
type: "Environment",
props: { preset: "forest", background: false, blur: 0, intensity: 0.8 },
children: [],
},
"p3-ambient": {
type: "AmbientLight",
props: { color: "#334422", intensity: 0.4 },
children: [],
},
"p3-sun": {
type: "DirectionalLight",
props: {
position: [-3, 8, 2],
rotation: null,
scale: null,
color: "#aaffaa",
intensity: 1.5,
castShadow: true,
},
children: [],
},
"p3-tree1": {
type: "Group",
props: { position: [-2, -2, -4], rotation: null, scale: null },
children: ["p3-t1-trunk", "p3-t1-leaves"],
},
"p3-t1-trunk": {
type: "Cylinder",
props: {
position: [0, 0.6, 0],
rotation: null,
scale: null,
radiusTop: 0.08,
radiusBottom: 0.12,
height: 1.2,
radialSegments: 8,
material: { color: "#4a3520", roughness: 0.9 },
},
children: [],
},
"p3-t1-leaves": {
type: "Cone",
props: {
position: [0, 1.8, 0],
rotation: null,
scale: null,
radius: 0.6,
height: 1.5,
radialSegments: 8,
material: { color: "#2d5a1e", roughness: 0.8 },
},
children: [],
},
"p3-tree2": {
type: "Group",
props: { position: [1.5, -2, -6], rotation: null, scale: null },
children: ["p3-t2-trunk", "p3-t2-leaves"],
},
"p3-t2-trunk": {
type: "Cylinder",
props: {
position: [0, 0.8, 0],
rotation: null,
scale: null,
radiusTop: 0.1,
radiusBottom: 0.15,
height: 1.6,
radialSegments: 8,
material: { color: "#3d2b15", roughness: 0.9 },
},
children: [],
},
"p3-t2-leaves": {
type: "Cone",
props: {
position: [0, 2.3, 0],
rotation: null,
scale: null,
radius: 0.8,
height: 2,
radialSegments: 8,
material: { color: "#1e4a10", roughness: 0.8 },
},
children: [],
},
"p3-tree3": {
type: "Group",
props: { position: [-0.5, -2, -3], rotation: null, scale: null },
children: ["p3-t3-trunk", "p3-t3-leaves"],
},
"p3-t3-trunk": {
type: "Cylinder",
props: {
position: [0, 0.5, 0],
rotation: null,
scale: null,
radiusTop: 0.06,
radiusBottom: 0.1,
height: 1,
radialSegments: 8,
material: { color: "#4a3520", roughness: 0.9 },
},
children: [],
},
"p3-t3-leaves": {
type: "Cone",
props: {
position: [0, 1.5, 0],
rotation: null,
scale: null,
radius: 0.5,
height: 1.2,
radialSegments: 8,
material: { color: "#3a6b2a", roughness: 0.8 },
},
children: [],
},
"p3-tree4": {
type: "Group",
props: { position: [3, -2, -8], rotation: null, scale: null },
children: ["p3-t4-trunk", "p3-t4-leaves"],
},
"p3-t4-trunk": {
type: "Cylinder",
props: {
position: [0, 1, 0],
rotation: null,
scale: null,
radiusTop: 0.12,
radiusBottom: 0.18,
height: 2,
radialSegments: 8,
material: { color: "#3d2b15", roughness: 0.9 },
},
children: [],
},
"p3-t4-leaves": {
type: "Cone",
props: {
position: [0, 2.8, 0],
rotation: null,
scale: null,
radius: 1,
height: 2.5,
radialSegments: 8,
material: { color: "#2d5a1e", roughness: 0.8 },
},
children: [],
},
"p3-tree5": {
type: "Group",
props: { position: [-3.5, -2, -7], rotation: null, scale: null },
children: ["p3-t5-trunk", "p3-t5-leaves"],
},
"p3-t5-trunk": {
type: "Cylinder",
props: {
position: [0, 0.7, 0],
rotation: null,
scale: null,
radiusTop: 0.09,
radiusBottom: 0.13,
height: 1.4,
radialSegments: 8,
material: { color: "#4a3520", roughness: 0.9 },
},
children: [],
},
"p3-t5-leaves": {
type: "Cone",
props: {
position: [0, 2, 0],
rotation: null,
scale: null,
radius: 0.7,
height: 1.8,
radialSegments: 8,
material: { color: "#1e4a10", roughness: 0.8 },
},
children: [],
},
"p3-ground": {
type: "Plane",
props: {
position: [0, -2, 0],
rotation: [-1.5708, 0, 0],
scale: null,
width: 30,
height: 30,
material: { color: "#3a5a2a", roughness: 1 },
},
children: [],
},
"p3-fireflies": {
type: "Sparkles",
props: {
position: [0, 0, -5],
rotation: null,
scale: [6, 4, 6],
count: 80,
speed: 0.3,
opacity: 0.7,
color: "#aaff66",
size: 1.5,
noise: 2,
},
children: [],
},
"gallery-sparkles": {
type: "Sparkles",
props: {
position: [0, 3, 0],
rotation: null,
scale: [12, 5, 10],
count: 40,
speed: 0.05,
opacity: 0.2,
color: "#ffffff",
size: 0.3,
noise: null,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 0.6,
luminanceThreshold: 0.3,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.4, darkness: 0.4 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 4,
maxDistance: 16,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: true,
autoRotateSpeed: 0.3,
target: [0, 1.8, -2],
},
children: [],
},
},
},
};
@@ -0,0 +1,455 @@
import type { Scene } from "./_helpers";
export const productShowroom: Scene = {
name: "Product Showroom",
description:
"Sleek glass torus knot on a pedestal with studio lighting, reflections, and contact shadows",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"camera",
"env",
"backdrop",
"ambient",
"key-light",
"fill-light",
"rim-light",
"accent-light",
"pedestal",
"hero-float",
"accent1",
"accent2",
"accent3",
"floor",
"shadows",
"sparkles",
"fx",
"controls",
],
},
camera: {
type: "PerspectiveCamera",
props: {
position: [0, 2, 6],
rotation: null,
scale: null,
fov: 40,
near: null,
far: null,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "studio",
background: false,
blur: 0.5,
intensity: 0.8,
},
children: [],
},
backdrop: {
type: "Backdrop",
props: {
position: [0, -0.5, -3],
rotation: null,
scale: [25, 10, 8],
floor: 0.3,
segments: 30,
receiveShadow: true,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#e8e0f0", intensity: 0.25 },
children: [],
},
"key-light": {
type: "SpotLight",
props: {
position: [5, 8, 3],
rotation: null,
scale: null,
color: "#ffffff",
intensity: 80,
distance: 25,
decay: null,
angle: 0.5,
penumbra: 0.8,
castShadow: true,
},
children: [],
},
"fill-light": {
type: "SpotLight",
props: {
position: [-4, 5, 2],
rotation: null,
scale: null,
color: "#aaccff",
intensity: 30,
distance: 20,
decay: null,
angle: 0.6,
penumbra: 1,
castShadow: false,
},
children: [],
},
"rim-light": {
type: "SpotLight",
props: {
position: [0, 3, -5],
rotation: null,
scale: null,
color: "#ffddcc",
intensity: 40,
distance: 15,
decay: null,
angle: 0.4,
penumbra: 0.5,
castShadow: false,
},
children: [],
},
"accent-light": {
type: "PointLight",
props: {
position: [0, 0.5, 0],
rotation: null,
scale: null,
color: "#ffffff",
intensity: 5,
distance: 4,
decay: null,
castShadow: null,
},
children: [],
},
pedestal: {
type: "Group",
props: { position: [0, 0, 0], rotation: null, scale: null },
children: ["ped-base", "ped-column", "ped-top"],
},
"ped-base": {
type: "Cylinder",
props: {
position: [0, 0.05, 0],
rotation: null,
scale: null,
radiusTop: 1.2,
radiusBottom: 1.3,
height: 0.1,
radialSegments: 48,
castShadow: null,
receiveShadow: null,
material: {
color: "#1a1a1a",
metalness: 0.9,
roughness: 0.1,
},
},
children: [],
},
"ped-column": {
type: "Cylinder",
props: {
position: [0, 0.45, 0],
rotation: null,
scale: null,
radiusTop: 0.6,
radiusBottom: 0.8,
height: 0.7,
radialSegments: 48,
castShadow: null,
receiveShadow: null,
material: {
color: "#111111",
metalness: 0.95,
roughness: 0.05,
},
},
children: [],
},
"ped-top": {
type: "Cylinder",
props: {
position: [0, 0.85, 0],
rotation: null,
scale: null,
radiusTop: 0.9,
radiusBottom: 0.7,
height: 0.1,
radialSegments: 48,
castShadow: null,
receiveShadow: null,
material: {
color: "#1a1a1a",
metalness: 0.9,
roughness: 0.1,
},
},
children: [],
},
"hero-float": {
type: "Float",
props: {
position: [0, 2.2, 0],
rotation: null,
scale: null,
speed: 1.5,
rotationIntensity: 0.3,
floatIntensity: 0.4,
enabled: true,
},
children: ["hero-spin"],
},
"hero-spin": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.3,
axis: "y",
},
children: ["hero-object"],
},
"hero-object": {
type: "TorusKnot",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.6,
tube: 0.22,
p: 2,
q: 3,
tubularSegments: 128,
radialSegments: 24,
material: {
color: "#ffffff",
metalness: 1,
roughness: 0,
},
},
children: [],
},
accent1: {
type: "Float",
props: {
position: [-1.5, 1.8, 0.5],
rotation: null,
scale: null,
speed: 2,
rotationIntensity: 0.5,
floatIntensity: 1.2,
enabled: true,
},
children: ["accent1-body"],
},
"accent1-body": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.15,
widthSegments: null,
heightSegments: null,
color: "#eeeeff",
transmission: 1,
thickness: 0.3,
roughness: 0,
chromaticAberration: 0.06,
ior: 1.5,
distortion: null,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
accent2: {
type: "Float",
props: {
position: [1.2, 2.5, -0.8],
rotation: null,
scale: null,
speed: 1.8,
rotationIntensity: 0.4,
floatIntensity: 1.5,
enabled: true,
},
children: ["accent2-body"],
},
"accent2-body": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.1,
widthSegments: null,
heightSegments: null,
color: "#ffeeff",
transmission: 1,
thickness: 0.2,
roughness: 0,
chromaticAberration: 0.08,
ior: 1.6,
distortion: null,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
accent3: {
type: "Float",
props: {
position: [0.8, 1.5, 1.2],
rotation: null,
scale: null,
speed: 2.2,
rotationIntensity: 0.6,
floatIntensity: 0.8,
enabled: true,
},
children: ["accent3-body"],
},
"accent3-body": {
type: "GlassSphere",
props: {
position: null,
rotation: null,
scale: null,
castShadow: null,
receiveShadow: null,
radius: 0.12,
widthSegments: null,
heightSegments: null,
color: "#eeffee",
transmission: 1,
thickness: 0.25,
roughness: 0,
chromaticAberration: 0.05,
ior: 1.4,
distortion: null,
distortionScale: null,
temporalDistortion: null,
samples: 6,
resolution: 128,
},
children: [],
},
floor: {
type: "ReflectorPlane",
props: {
position: [0, -0.5, 0],
rotation: [-1.5708, 0, 0],
scale: null,
width: 25,
height: 25,
color: "#0a0a0a",
resolution: 1024,
blur: 300,
mirror: 0.6,
mixBlur: 8,
mixStrength: 2,
depthScale: 0.2,
metalness: 0.8,
roughness: 0.3,
},
children: [],
},
shadows: {
type: "ContactShadows",
props: {
position: [0, -0.49, 0],
rotation: null,
scale: null,
opacity: 0.5,
width: 10,
height: 10,
blur: 2,
near: null,
far: 5,
smooth: true,
resolution: 512,
frames: null,
color: "#000000",
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: [0, 2, 0],
rotation: null,
scale: [6, 4, 6],
count: 30,
speed: 0.05,
opacity: 0.15,
color: "#ffffff",
size: 0.3,
noise: 0.5,
},
children: [],
},
fx: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 0.5,
luminanceThreshold: 0.3,
luminanceSmoothing: 0.4,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.3, darkness: 0.5 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: null,
enablePan: null,
enableRotate: null,
minDistance: 3,
maxDistance: 12,
minPolarAngle: 0.3,
maxPolarAngle: 1.4,
autoRotate: true,
autoRotateSpeed: 1,
target: [0, 1.5, 0],
},
children: [],
},
},
},
};
@@ -0,0 +1,149 @@
import type { Scene } from "./_helpers";
export const starfield: Scene = {
name: "Starfield",
description:
"Classic Windows starfield screensaver -- flying through stars at warp speed",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"cam",
"ambient",
"tunnel",
"stars1",
"stars2",
"sparkle-dust",
"shake",
"post",
"controls",
],
},
cam: {
type: "PerspectiveCamera",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
fov: 75,
near: 0.1,
far: 1000,
makeDefault: true,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#111122", intensity: 0.15 },
children: [],
},
tunnel: {
type: "WarpTunnel",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
ringCount: 120,
radius: 4,
length: 40,
speed: 12,
tubeRadius: 0.008,
color1: "#ffffff",
color2: "#aabbff",
},
children: [],
},
stars1: {
type: "Stars",
props: {
radius: 50,
depth: 80,
count: 10000,
factor: 6,
saturation: 0,
fade: true,
speed: 3,
},
children: [],
},
stars2: {
type: "Stars",
props: {
radius: 30,
depth: 50,
count: 5000,
factor: 3,
saturation: 0.1,
fade: true,
speed: 5,
},
children: [],
},
"sparkle-dust": {
type: "Sparkles",
props: {
position: [0, 0, -5],
rotation: null,
scale: [8, 8, 30],
count: 300,
speed: 2,
opacity: 0.6,
color: "#ffffff",
size: 1,
noise: 4,
},
children: [],
},
shake: {
type: "CameraShake",
props: {
intensity: 0.15,
maxYaw: 0.02,
maxPitch: 0.02,
maxRoll: 0.01,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 1.5,
luminanceThreshold: 0.15,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.1, darkness: 0.9 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: null,
dampingFactor: null,
enableZoom: false,
enablePan: false,
enableRotate: false,
minDistance: null,
maxDistance: null,
minPolarAngle: null,
maxPolarAngle: null,
autoRotate: false,
autoRotateSpeed: null,
},
children: [],
},
},
},
};
@@ -0,0 +1,295 @@
import type { Scene } from "./_helpers";
export const stormCell: Scene = {
name: "Storm Cell",
description:
"Dark violent thunderstorm -- spinning cloud masses, pulsing lightning, rain particles, camera shake",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"sky",
"ambient",
"sun",
"cloud-spin-1",
"cloud-spin-2",
"cloud-spin-3",
"lightning-1-pulse",
"lightning-2-pulse",
"lightning-3-pulse",
"rain",
"shake",
"post",
"fog",
"controls",
],
},
sky: {
type: "Sky",
props: {
distance: null,
sunPosition: [10, 2, 0],
inclination: null,
azimuth: null,
turbidity: 20,
rayleigh: 0.5,
mieCoefficient: 0.01,
mieDirectionalG: 0.9,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#111122", intensity: 0.15 },
children: [],
},
sun: {
type: "DirectionalLight",
props: {
position: [10, 3, 0],
rotation: null,
scale: null,
color: "#334466",
intensity: 0.5,
castShadow: true,
},
children: [],
},
"cloud-spin-1": {
type: "Spin",
props: {
position: [0, 4, 0],
rotation: null,
scale: null,
speed: 0.08,
axis: "y",
},
children: ["cloud-1"],
},
"cloud-1": {
type: "Cloud",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
seed: 1,
segments: 40,
bounds: [25, 3, 25],
volume: 15,
speed: 0.3,
fade: 20,
opacity: 0.9,
color: "#222233",
growth: 10,
},
children: [],
},
"cloud-spin-2": {
type: "Spin",
props: {
position: [0, 6, -3],
rotation: null,
scale: null,
speed: -0.05,
axis: "y",
},
children: ["cloud-2"],
},
"cloud-2": {
type: "Cloud",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
seed: 42,
segments: 30,
bounds: [20, 2, 20],
volume: 12,
speed: 0.2,
fade: 15,
opacity: 0.7,
color: "#1a1a2a",
growth: 8,
},
children: [],
},
"cloud-spin-3": {
type: "Spin",
props: {
position: [0, 8, 2],
rotation: null,
scale: null,
speed: 0.03,
axis: "y",
},
children: ["cloud-3"],
},
"cloud-3": {
type: "Cloud",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
seed: 99,
segments: 25,
bounds: [30, 2, 30],
volume: 10,
speed: 0.15,
fade: 25,
opacity: 0.5,
color: "#2a2a3a",
growth: 6,
},
children: [],
},
"lightning-1-pulse": {
type: "Pulse",
props: {
position: [2, 5, -1],
rotation: null,
scale: null,
speed: 3,
min: 0,
max: 1,
},
children: ["lightning-1"],
},
"lightning-1": {
type: "PointLight",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
color: "#aabbff",
intensity: 80,
distance: 30,
decay: 2,
castShadow: false,
},
children: [],
},
"lightning-2-pulse": {
type: "Pulse",
props: {
position: [-3, 6, 3],
rotation: null,
scale: null,
speed: 4.5,
min: 0,
max: 1,
},
children: ["lightning-2"],
},
"lightning-2": {
type: "PointLight",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
color: "#aaccff",
intensity: 60,
distance: 25,
decay: 2,
castShadow: false,
},
children: [],
},
"lightning-3-pulse": {
type: "Pulse",
props: {
position: [0, 4, -4],
rotation: null,
scale: null,
speed: 2.5,
min: 0,
max: 1,
},
children: ["lightning-3"],
},
"lightning-3": {
type: "PointLight",
props: {
position: [0, 0, 0],
rotation: null,
scale: null,
color: "#99bbff",
intensity: 40,
distance: 20,
decay: 2,
castShadow: false,
},
children: [],
},
rain: {
type: "Sparkles",
props: {
position: [0, 5, 0],
rotation: null,
scale: [20, 15, 20],
count: 500,
speed: 5,
opacity: 0.4,
color: "#6688aa",
size: 0.5,
noise: 0.5,
},
children: [],
},
shake: {
type: "CameraShake",
props: {
intensity: 0.2,
maxYaw: 0.02,
maxPitch: 0.03,
maxRoll: 0.01,
},
children: [],
},
post: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 0.8,
luminanceThreshold: 0.3,
luminanceSmoothing: null,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.3, darkness: 0.8 },
children: [],
},
fog: {
type: "Fog",
props: { color: "#0a0a15", near: 5, far: 30 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: null,
dampingFactor: null,
enableZoom: true,
enablePan: null,
enableRotate: true,
minDistance: 3,
maxDistance: 20,
minPolarAngle: 0.2,
maxPolarAngle: 1.3,
autoRotate: true,
autoRotateSpeed: 0.1,
},
children: [],
},
},
},
};
@@ -0,0 +1,12 @@
import { nextJsConfig } from "@internal/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */
export default [
...nextJsConfig,
{
rules: {
"react/prop-types": "off",
"react/no-unknown-property": "off",
},
},
];
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+12
View File
@@ -0,0 +1,12 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: [
"@json-render/core",
"@json-render/react",
"@json-render/react-three-fiber",
"three",
],
};
export default nextConfig;
+35
View File
@@ -0,0 +1,35 @@
{
"name": "example-react-three-fiber",
"version": "0.1.6",
"type": "module",
"private": true,
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless react-three-fiber-demo.json-render next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint --max-warnings 0",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/react": "workspace:*",
"@json-render/react-three-fiber": "workspace:*",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"next": "16.1.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"three": "^0.183.2",
"zod": "4.3.6"
},
"devDependencies": {
"@internal/eslint-config": "workspace:*",
"@types/node": "^22.10.0",
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"@types/three": "^0.183.1",
"eslint": "^9.39.1",
"typescript": "^5.7.2"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../packages/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"declaration": false,
"declarationMap": false,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>json-render solid example</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
{
"name": "example-solid",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "portless solid-demo.json-render vite",
"build": "vite build",
"preview": "vite preview",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/solid": "workspace:*",
"solid-js": "^1.9.11",
"zod": "^4.3.6"
},
"devDependencies": {
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-solid": "^2.11.10"
}
}
+13
View File
@@ -0,0 +1,13 @@
import { StateProvider } from "@json-render/solid";
import { demoSpec } from "./lib/spec";
import { DemoRenderer } from "./DemoRenderer";
export function App() {
const initialState = demoSpec.state ?? {};
return (
<StateProvider initialState={initialState}>
<DemoRenderer />
</StateProvider>
);
}
+51
View File
@@ -0,0 +1,51 @@
import {
ActionProvider,
ValidationProvider,
VisibilityProvider,
Renderer,
useStateStore,
} from "@json-render/solid";
import { demoSpec } from "./lib/spec";
import { registry } from "./lib/registry";
export function DemoRenderer() {
const stateStore = useStateStore();
const handlers = {
increment: async () => {
stateStore.set("/count", Number(stateStore.get("/count") || 0) + 1);
},
decrement: async () => {
stateStore.set(
"/count",
Math.max(0, Number(stateStore.get("/count") || 0) - 1),
);
},
reset: async () => {
stateStore.set("/count", 0);
},
toggleItem: async (params: Record<string, unknown>) => {
const index = params.index as number;
const todos = (
stateStore.get("/todos") as Array<{
id: number;
title: string;
completed: boolean;
}>
).map((item, i) =>
i === index ? { ...item, completed: !item.completed } : item,
);
stateStore.set("/todos", todos);
},
};
return (
<ActionProvider handlers={handlers}>
<VisibilityProvider>
<ValidationProvider>
<Renderer spec={demoSpec} registry={registry} />
</ValidationProvider>
</VisibilityProvider>
</ActionProvider>
);
}
+13
View File
@@ -0,0 +1,13 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f9fafb;
color: #111827;
min-height: 100vh;
}
+5
View File
@@ -0,0 +1,5 @@
import { render } from "solid-js/web";
import { App } from "./App";
import "./app.css";
render(() => <App />, document.getElementById("app")!);
+90
View File
@@ -0,0 +1,90 @@
import { schema } from "@json-render/solid/schema";
import { z } from "zod";
export const catalog = schema.createCatalog({
components: {
Stack: {
props: z.object({
gap: z.number().optional(),
padding: z.number().optional(),
direction: z.enum(["vertical", "horizontal"]).optional(),
align: z.enum(["start", "center", "end"]).optional(),
}),
slots: ["default"],
description:
"Layout container that stacks children vertically or horizontally",
},
Card: {
props: z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
}),
slots: ["default"],
description: "A card container with optional title and subtitle",
},
Text: {
props: z.object({
content: z.string(),
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
weight: z.enum(["normal", "medium", "bold"]).optional(),
color: z.string().optional(),
}),
slots: [],
description: "Displays a text string",
},
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "danger"]).optional(),
disabled: z.boolean().optional(),
}),
slots: [],
description: "A clickable button that emits a 'press' event",
},
Badge: {
props: z.object({
label: z.string(),
color: z.string().optional(),
}),
slots: [],
description: "A small badge/tag label",
},
ListItem: {
props: z.object({
title: z.string(),
description: z.string().optional(),
completed: z.boolean().optional(),
}),
slots: [],
description: "A single item in a list",
},
Input: {
props: z.object({
value: z.string().optional(),
placeholder: z.string().optional(),
}),
slots: [],
description: "A text input field that supports two-way state binding",
},
},
actions: {
increment: {
params: z.object({}),
description: "Increment the counter by 1",
},
decrement: {
params: z.object({}),
description: "Decrement the counter by 1",
},
reset: {
params: z.object({}),
description: "Reset the counter to 0",
},
toggleItem: {
params: z.object({
index: z.number(),
}),
description: "Toggle the completed state of a todo item",
},
},
});
@@ -0,0 +1,27 @@
import type { BaseComponentProps } from "@json-render/solid";
interface BadgeProps {
label: string;
color?: string;
}
export function Badge(renderProps: BaseComponentProps<BadgeProps>) {
const color = () => renderProps.props.color ?? "#6b7280";
return (
<span
style={{
display: "inline-block",
padding: "4px 12px",
"border-radius": "9999px",
"font-size": "13px",
"font-weight": "500",
"background-color": `${color()}20`,
color: color(),
border: `1px solid ${color()}40`,
}}
>
{renderProps.props.label}
</span>
);
}
@@ -0,0 +1,47 @@
import type { BaseComponentProps } from "@json-render/solid";
interface ButtonProps {
label: string;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
}
const variantStyles: Record<string, Record<string, string>> = {
primary: {
background: "#2563eb",
color: "#ffffff",
border: "1px solid #2563eb",
},
secondary: {
background: "#f3f4f6",
color: "#374151",
border: "1px solid #d1d5db",
},
danger: {
background: "#fee2e2",
color: "#dc2626",
border: "1px solid #fca5a5",
},
};
export function Button(renderProps: BaseComponentProps<ButtonProps>) {
const variant = () => renderProps.props.variant ?? "primary";
return (
<button
disabled={renderProps.props.disabled}
onClick={() => renderProps.emit("press")}
style={{
padding: "8px 16px",
"border-radius": "8px",
"font-size": "14px",
"font-weight": "500",
cursor: renderProps.props.disabled ? "not-allowed" : "pointer",
opacity: renderProps.props.disabled ? "0.5" : "1",
...variantStyles[variant()],
}}
>
{renderProps.props.label}
</button>
);
}
@@ -0,0 +1,44 @@
import type { BaseComponentProps } from "@json-render/solid";
interface CardProps {
title?: string;
subtitle?: string;
}
export function Card(renderProps: BaseComponentProps<CardProps>) {
return (
<div
style={{
background: "#ffffff",
"border-radius": "12px",
border: "1px solid #e5e7eb",
padding: "20px",
"box-shadow": "0 1px 3px rgba(0,0,0,0.06)",
}}
>
{renderProps.props.title && (
<h2
style={{
"font-size": "18px",
"font-weight": "600",
"margin-bottom": renderProps.props.subtitle ? "4px" : "16px",
}}
>
{renderProps.props.title}
</h2>
)}
{renderProps.props.subtitle && (
<p
style={{
"font-size": "14px",
color: "#6b7280",
"margin-bottom": "16px",
}}
>
{renderProps.props.subtitle}
</p>
)}
{renderProps.children}
</div>
);
}
@@ -0,0 +1,30 @@
import type { BaseComponentProps } from "@json-render/solid";
import { useBoundProp } from "@json-render/solid";
interface InputProps {
value?: string;
placeholder?: string;
}
export function Input(renderProps: BaseComponentProps<InputProps>) {
const [value, setValue] = useBoundProp<string>(
renderProps.props.value,
renderProps.bindings?.value,
);
return (
<input
value={String(value ?? "")}
onInput={(e) => setValue(e.currentTarget.value)}
placeholder={renderProps.props.placeholder ?? ""}
style={{
padding: "8px 12px",
"border-radius": "8px",
border: "1px solid #d1d5db",
"font-size": "14px",
outline: "none",
width: "100%",
}}
/>
);
}
@@ -0,0 +1,55 @@
import type { BaseComponentProps } from "@json-render/solid";
interface ListItemProps {
title: string;
description?: string;
completed?: boolean;
}
export function ListItem(renderProps: BaseComponentProps<ListItemProps>) {
return (
<div
onClick={() => renderProps.emit("press")}
style={{
display: "flex",
"align-items": "center",
gap: "10px",
padding: "10px 12px",
"border-radius": "8px",
border: "1px solid #e5e7eb",
cursor: "pointer",
"user-select": "none",
background: renderProps.props.completed ? "#f0fdf4" : "#ffffff",
}}
>
<span
style={{
width: "22px",
height: "22px",
"border-radius": "6px",
border: renderProps.props.completed
? "2px solid #22c55e"
: "2px solid #d1d5db",
display: "flex",
"align-items": "center",
"justify-content": "center",
"font-size": "13px",
color: "#22c55e",
"flex-shrink": "0",
}}
>
{renderProps.props.completed ? "\u2713" : ""}
</span>
<span
style={{
"text-decoration": renderProps.props.completed
? "line-through"
: "none",
color: renderProps.props.completed ? "#9ca3af" : "#111827",
}}
>
{renderProps.props.title}
</span>
</div>
);
}
@@ -0,0 +1,27 @@
import type { BaseComponentProps } from "@json-render/solid";
interface StackProps {
gap?: number;
padding?: number;
direction?: "vertical" | "horizontal";
align?: "start" | "center" | "end";
}
export function Stack(renderProps: BaseComponentProps<StackProps>) {
return (
<div
style={{
display: "flex",
"flex-direction":
renderProps.props.direction === "horizontal" ? "row" : "column",
gap: renderProps.props.gap ? `${renderProps.props.gap}px` : undefined,
padding: renderProps.props.padding
? `${renderProps.props.padding}px`
: undefined,
"align-items": renderProps.props.align,
}}
>
{renderProps.children}
</div>
);
}
@@ -0,0 +1,35 @@
import type { BaseComponentProps } from "@json-render/solid";
interface TextProps {
content: string;
size?: "sm" | "md" | "lg" | "xl";
weight?: "normal" | "medium" | "bold";
color?: string;
}
const sizeMap: Record<string, string> = {
sm: "12px",
md: "14px",
lg: "18px",
xl: "24px",
};
const weightMap: Record<string, string> = {
normal: "400",
medium: "500",
bold: "700",
};
export function Text(renderProps: BaseComponentProps<TextProps>) {
return (
<span
style={{
"font-size": sizeMap[renderProps.props.size ?? "md"],
"font-weight": weightMap[renderProps.props.weight ?? "normal"],
color: renderProps.props.color,
}}
>
{String(renderProps.props.content ?? "")}
</span>
);
}
+29
View File
@@ -0,0 +1,29 @@
import { type Components, defineRegistry } from "@json-render/solid";
import { catalog } from "./catalog";
import { Stack } from "./components/Stack";
import { Card } from "./components/Card";
import { Text } from "./components/Text";
import { Button } from "./components/Button";
import { Badge } from "./components/Badge";
import { ListItem } from "./components/ListItem";
import { Input } from "./components/Input";
const components: Components<typeof catalog> = {
Stack,
Card,
Text,
Button,
Badge,
ListItem,
Input,
};
export const { registry, handlers: makeHandlers } = defineRegistry(catalog, {
components,
actions: {
increment: async () => {},
decrement: async () => {},
reset: async () => {},
toggleItem: async () => {},
},
});
+130
View File
@@ -0,0 +1,130 @@
import type { Spec } from "@json-render/core";
export const demoSpec: Spec = {
root: "root",
state: {
count: 0,
name: "",
todos: [
{ id: 1, title: "Learn SolidJS", completed: true },
{ id: 2, title: "Try @json-render/solid", completed: false },
{ id: 3, title: "Build something awesome", completed: false },
],
},
elements: {
root: {
type: "Stack",
props: { gap: 24, padding: 24, direction: "vertical" },
children: [
"header",
"counter-card",
"milestone-badge",
"todos-card",
"input-card",
],
},
header: {
type: "Text",
props: {
content: "@json-render/solid demo",
size: "xl",
weight: "bold",
},
},
"counter-card": {
type: "Card",
props: {
title: "Counter",
subtitle: "Click the buttons to change the count",
},
children: ["counter-body"],
},
"counter-body": {
type: "Stack",
props: { gap: 12, direction: "horizontal", align: "center" },
children: [
"decrement-btn",
"counter-value",
"increment-btn",
"reset-btn",
],
},
"decrement-btn": {
type: "Button",
props: { label: "\u2212", variant: "secondary" },
on: { press: { action: "decrement" } },
},
"counter-value": {
type: "Text",
props: {
content: { $state: "/count" },
size: "xl",
weight: "bold",
},
},
"increment-btn": {
type: "Button",
props: { label: "+", variant: "primary" },
on: { press: { action: "increment" } },
},
"reset-btn": {
type: "Button",
props: { label: "Reset", variant: "danger" },
on: { press: { action: "reset" } },
},
"milestone-badge": {
type: "Badge",
props: { label: "Milestone reached: 10!", color: "#10b981" },
visible: { $state: "/count", gte: 10 },
},
"todos-card": {
type: "Card",
props: { title: "Todo List", subtitle: "Your tasks" },
children: ["todos-list"],
},
"todos-list": {
type: "Stack",
props: { gap: 8, direction: "vertical" },
repeat: { statePath: "/todos", key: "id" },
children: ["todo-item"],
},
"todo-item": {
type: "ListItem",
props: {
title: { $item: "title" },
completed: { $item: "completed" },
},
on: {
press: { action: "toggleItem", params: { index: { $index: true } } },
},
},
"input-card": {
type: "Card",
props: {
title: "Bound Input",
subtitle: "Type to update state and see reactive text",
},
children: ["input-body"],
},
"input-body": {
type: "Stack",
props: { gap: 12, direction: "vertical" },
children: ["name-input", "name-display"],
},
"name-input": {
type: "Input",
props: {
value: { $bindState: "/name" },
placeholder: "Enter your name...",
},
},
"name-display": {
type: "Text",
props: {
content: { $state: "/name" },
size: "md",
color: "#6b7280",
},
},
},
};
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"jsx": "preserve",
"jsxImportSource": "solid-js"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
export default defineConfig({
plugins: [solid()],
});
+1
View File
@@ -160,6 +160,7 @@ export { defineSchema } from "./schema";
// Catalog — defines the vocabulary (what components and actions are available)
export type {
Catalog,
JsonSchemaOptions,
PromptOptions,
PromptContext,
SpecValidationResult,
+188
View File
@@ -592,6 +592,194 @@ describe("catalog.jsonSchema", () => {
expect(jsonSchema).not.toBeNull();
expect(typeof jsonSchema).toBe("object");
});
describe("strict mode (LLM structured output compatible)", () => {
function hasNoPropertyNames(obj: unknown): boolean {
if (typeof obj !== "object" || obj === null) return true;
if ("propertyNames" in obj) return false;
return Object.values(obj).every(hasNoPropertyNames);
}
function allObjectsHaveAdditionalPropertiesFalse(obj: unknown): boolean {
if (typeof obj !== "object" || obj === null) return true;
const record = obj as Record<string, unknown>;
if (record.type === "object") {
if (record.additionalProperties !== false) return false;
}
return Object.values(record).every(
allObjectsHaveAdditionalPropertiesFalse,
);
}
function allObjectPropertiesRequired(obj: unknown): boolean {
if (typeof obj !== "object" || obj === null) return true;
const record = obj as Record<string, unknown>;
if (
record.type === "object" &&
record.properties &&
typeof record.properties === "object"
) {
const propKeys = Object.keys(record.properties);
const required = (record.required as string[]) ?? [];
if (!propKeys.every((k) => required.includes(k))) return false;
}
return Object.values(record).every(allObjectPropertiesRequired);
}
it("sets additionalProperties: false on all nested objects", () => {
const catalog = defineCatalog(testSchema, {
components: {
Card: {
props: z.object({
title: z.string(),
subtitle: z.string().optional(),
}),
description: "",
slots: [],
},
},
actions: {},
});
const schema = catalog.jsonSchema({ strict: true });
expect(allObjectsHaveAdditionalPropertiesFalse(schema)).toBe(true);
});
it("does not emit propertyNames", () => {
const catalog = defineCatalog(testSchema, {
components: {
Text: {
props: z.object({ content: z.string() }),
description: "",
slots: [],
},
},
actions: {},
});
const schema = catalog.jsonSchema({ strict: true });
expect(hasNoPropertyNames(schema)).toBe(true);
});
it("lists all properties in required (optional uses nullable)", () => {
const catalog = defineCatalog(testSchema, {
components: {
Card: {
props: z.object({
title: z.string(),
subtitle: z.string().optional(),
}),
description: "",
slots: [],
},
},
actions: {},
});
const schema = catalog.jsonSchema({ strict: true });
expect(allObjectPropertiesRequired(schema)).toBe(true);
});
it("converts record types without additionalProperties schema value", () => {
const catalog = defineCatalog(testSchema, {
components: {
Text: {
props: z.object({ content: z.string() }),
description: "",
slots: [],
},
},
actions: {},
});
const schema = catalog.jsonSchema({
strict: true,
}) as Record<string, unknown>;
// Walk the schema and ensure no additionalProperties is set to a non-false value
function noAdditionalPropertiesSchema(obj: unknown): boolean {
if (typeof obj !== "object" || obj === null) return true;
const rec = obj as Record<string, unknown>;
if (
"additionalProperties" in rec &&
rec.additionalProperties !== false
) {
return false;
}
return Object.values(rec).every(noAdditionalPropertiesSchema);
}
expect(noAdditionalPropertiesSchema(schema)).toBe(true);
});
it("wraps optional properties with anyOf nullable", () => {
// Use a schema where propsOf resolves to a single component's props
// (no record wrapper around the props) so the optional anyOf handling
// is directly visible in the JSON Schema output.
const flatSchema = defineSchema((s) => ({
spec: s.object({
component: s.object({
type: s.ref("catalog.components"),
props: s.propsOf("catalog.components"),
}),
}),
catalog: s.object({
components: s.map({
props: s.zod(),
description: s.string(),
}),
}),
}));
const catalog = defineCatalog(flatSchema, {
components: {
Card: {
props: z.object({
heading: z.string(),
caption: z.string().optional(),
}),
description: "",
},
},
});
const schema = catalog.jsonSchema({ strict: true }) as {
properties: {
component: {
properties: { props: Record<string, unknown> };
};
};
};
const propsSchema = schema.properties.component.properties.props;
// caption is optional – in strict mode it must be in `required`
// and wrapped in anyOf with null
const captionSchema = (
propsSchema as {
properties: { caption: Record<string, unknown> };
}
).properties.caption;
expect(captionSchema).toEqual({
anyOf: [{ type: "string" }, { type: "null" }],
});
const propsRequired = (propsSchema as { required: string[] }).required;
expect(propsRequired).toContain("heading");
expect(propsRequired).toContain("caption");
});
it("does not affect default (non-strict) output", () => {
const catalog = defineCatalog(testSchema, {
components: {
Text: {
props: z.object({ content: z.string() }),
description: "",
slots: [],
},
},
actions: {},
});
const defaultSchema = catalog.jsonSchema();
const defaultSchema2 = catalog.jsonSchema({ strict: false });
expect(defaultSchema).toEqual(defaultSchema2);
});
});
});
// =============================================================================
+147 -34
View File
@@ -92,7 +92,7 @@ export interface Catalog<
/** Generate system prompt for AI */
prompt(options?: PromptOptions): string;
/** Export as JSON Schema for structured outputs */
jsonSchema(): object;
jsonSchema(options?: JsonSchemaOptions): object;
/** Validate a spec against this catalog */
validate(spec: unknown): SpecValidationResult<InferSpec<TDef, TCatalog>>;
/** Get the Zod schema for the spec */
@@ -101,6 +101,28 @@ export interface Catalog<
readonly _specType: InferSpec<TDef, TCatalog>;
}
/**
* Options for JSON Schema export
*/
export interface JsonSchemaOptions {
/**
* When true, produces a strict JSON Schema subset compatible with
* LLM structured output APIs (OpenAI, Google Gemini, Anthropic, etc.).
* This ensures:
* - `additionalProperties: false` on every object
* - All object properties listed in `required` (optionals use nullable types)
* - Record/map types converted to fixed-key objects
*
* **Limitation:** Record types (dynamic-key maps) cannot be represented in
* strict JSON Schema because `additionalProperties` must be `false`. They
* are emitted as `{ type: "object", properties: {}, additionalProperties: false }`.
* The LLM prompt (via `catalog.prompt()`) still describes the full structure,
* so the model can produce valid output even though the JSON Schema for
* record entries is opaque.
*/
strict?: boolean;
}
/**
* Prompt generation options
*/
@@ -414,8 +436,8 @@ function createCatalogFromSchema<TDef extends SchemaDefinition, TCatalog>(
return generatePrompt(this, options);
},
jsonSchema(): object {
return zodToJsonSchema(zodSchema);
jsonSchema(options: JsonSchemaOptions = {}): object {
return zodToJsonSchema(zodSchema, options.strict ?? false);
},
validate(spec: unknown): SpecValidationResult<InferSpec<TDef, TCatalog>> {
@@ -1319,44 +1341,111 @@ function formatZodType(schema: z.ZodType): string {
}
/**
* Convert Zod schema to JSON Schema
* Resolve the Zod type name from a schema's internal definition.
* Supports both Zod 3 (`_def.typeName`) and Zod 4 (`_def.type`).
*/
function zodToJsonSchema(schema: z.ZodType): object {
// Simplified JSON Schema conversion
const def = schema._def as unknown as Record<string, unknown>;
const typeName = (def.typeName as string) ?? "";
function zodTypeName(def: Record<string, unknown>): string {
// Zod 4 uses _def.type as a plain string (e.g. "string", "object")
if (typeof def.type === "string") return def.type;
// Zod 3 uses _def.typeName (e.g. "ZodString", "ZodObject")
if (typeof def.typeName === "string") return def.typeName;
return "";
}
switch (typeName) {
case "ZodString":
/**
* Normalise a Zod type name to a canonical lowercase form.
* Handles both Zod 3 ("ZodString") and Zod 4 ("string") conventions.
*/
function normalizeTypeName(raw: string): string {
// Zod 3 names start with "Zod", e.g. "ZodString" → "string"
if (raw.startsWith("Zod")) {
return raw.slice(3).toLowerCase();
}
return raw.toLowerCase();
}
/**
* Convert Zod schema to JSON Schema.
*
* When `strict` is true the output conforms to the JSON Schema subset required
* by LLM structured output APIs (no `propertyNames`, `additionalProperties: false`
* everywhere, all properties listed in `required`).
*/
function zodToJsonSchema(schema: z.ZodType, strict = false): object {
const def = schema._def as unknown as Record<string, unknown>;
const kind = normalizeTypeName(zodTypeName(def));
switch (kind) {
case "string":
return { type: "string" };
case "ZodNumber":
case "number":
return { type: "number" };
case "ZodBoolean":
case "boolean":
return { type: "boolean" };
case "ZodLiteral":
return { const: def.value };
case "ZodEnum":
return { enum: def.values };
case "ZodArray": {
const inner = def.type as z.ZodType | undefined;
case "literal": {
// Zod 4: _def.values (array), Zod 3: _def.value (single)
const values = def.values as unknown[] | undefined;
const value = values ? values[0] : def.value;
return { const: value };
}
case "enum": {
// Zod 4: _def.entries (object { a:"a", b:"b" }), Zod 3: _def.values (string[])
const entries = def.entries as Record<string, string> | undefined;
const values = entries
? Object.values(entries)
: (def.values as string[] | undefined);
return { enum: values ?? [] };
}
case "array": {
// Zod 4: _def.element, Zod 3: _def.type
const inner = (def.element ?? def.type) as z.ZodType | undefined;
return {
type: "array",
items: inner ? zodToJsonSchema(inner) : {},
items: inner ? zodToJsonSchema(inner, strict) : {},
};
}
case "ZodObject": {
const shape = (def.shape as () => Record<string, z.ZodType>)?.();
if (!shape) return { type: "object" };
case "object": {
// Zod 4: _def.shape is an object, Zod 3: _def.shape is a function
const rawShape = def.shape;
const shape: Record<string, z.ZodType> | undefined =
typeof rawShape === "function"
? (rawShape as () => Record<string, z.ZodType>)()
: (rawShape as Record<string, z.ZodType> | undefined);
if (!shape) {
if (strict) {
return {
type: "object",
properties: {},
required: [],
additionalProperties: false,
};
}
return { type: "object" };
}
const properties: Record<string, object> = {};
const required: string[] = [];
for (const [key, value] of Object.entries(shape)) {
properties[key] = zodToJsonSchema(value);
const innerDef = value._def as unknown as Record<string, unknown>;
if (
innerDef.typeName !== "ZodOptional" &&
innerDef.typeName !== "ZodNullable"
) {
const innerKind = normalizeTypeName(zodTypeName(innerDef));
const isOptional = innerKind === "optional" || innerKind === "nullable";
if (strict) {
// In strict mode, all properties must be in required.
// Optional properties are represented as nullable types.
required.push(key);
if (isOptional) {
const unwrapped = zodToJsonSchema(value, strict);
properties[key] = { anyOf: [unwrapped, { type: "null" }] };
} else {
properties[key] = zodToJsonSchema(value, strict);
}
} else {
properties[key] = zodToJsonSchema(value);
if (!isOptional) {
required.push(key);
}
}
}
return {
@@ -1366,23 +1455,47 @@ function zodToJsonSchema(schema: z.ZodType): object {
additionalProperties: false,
};
}
case "ZodRecord": {
case "record": {
const valueType = def.valueType as z.ZodType | undefined;
if (strict) {
// LLM strict schemas require `additionalProperties: false` and do not
// permit a schema value for `additionalProperties`. Since record types
// have dynamic keys that cannot be enumerated at schema-generation time,
// we emit an opaque object. The LLM prompt still describes the expected
// structure so the model can produce valid output.
return {
type: "object",
properties: {},
required: [],
additionalProperties: false,
};
}
return {
type: "object",
additionalProperties: valueType ? zodToJsonSchema(valueType) : true,
};
}
case "ZodOptional":
case "ZodNullable": {
case "optional":
case "nullable": {
const inner = def.innerType as z.ZodType | undefined;
return inner ? zodToJsonSchema(inner) : {};
return inner ? zodToJsonSchema(inner, strict) : {};
}
case "ZodUnion": {
case "union": {
const options = def.options as z.ZodType[] | undefined;
return options ? { anyOf: options.map(zodToJsonSchema) } : {};
return options
? { anyOf: options.map((o) => zodToJsonSchema(o, strict)) }
: {};
}
case "ZodAny":
case "any":
case "unknown":
if (strict) {
return {
type: "object",
properties: {},
required: [],
additionalProperties: false,
};
}
return {};
default:
return {};
+75
View File
@@ -0,0 +1,75 @@
# @json-render/react-three-fiber
React Three Fiber renderer for [`@json-render/core`](https://json-render.dev). JSON becomes 3D scenes.
19 built-in components for meshes, lights, models, environments, text, cameras, and controls.
## Installation
```bash
npm install @json-render/react-three-fiber @json-render/core @json-render/react @react-three/fiber @react-three/drei three zod
```
## Usage
```tsx
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
} from "@json-render/react-three-fiber";
// Build catalog with 3D components
const catalog = defineCatalog(schema, {
components: {
Box: threeComponentDefinitions.Box,
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
OrbitControls: threeComponentDefinitions.OrbitControls,
},
actions: {},
});
// Register implementations
const { registry } = defineRegistry(catalog, {
components: {
Box: threeComponents.Box,
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
OrbitControls: threeComponents.OrbitControls,
},
});
// Render
<ThreeCanvas
spec={spec}
registry={registry}
shadows
camera={{ position: [5, 5, 5], fov: 50 }}
style={{ width: "100%", height: "100vh" }}
/>;
```
## Components
**Primitives:** Box, Sphere, Cylinder, Cone, Torus, Plane, Capsule
**Lights:** AmbientLight, DirectionalLight, PointLight, SpotLight
**Container:** Group
**Model:** Model (GLTF/GLB)
**Environment:** Environment, Fog, GridHelper
**Text:** Text3D
**Camera/Controls:** PerspectiveCamera, OrbitControls
## Docs
Full API reference: [json-render.dev/docs/api/three](https://json-render.dev/docs/api/three)
+80
View File
@@ -0,0 +1,80 @@
{
"name": "@json-render/react-three-fiber",
"version": "0.12.1",
"license": "Apache-2.0",
"description": "React Three Fiber renderer for @json-render/core. JSON becomes 3D scenes.",
"keywords": [
"json",
"3d",
"three",
"threejs",
"react-three-fiber",
"r3f",
"webgl",
"ai",
"generative-ui",
"llm",
"renderer",
"streaming",
"components"
],
"repository": {
"type": "git",
"url": "git+https://github.com/vercel-labs/json-render.git",
"directory": "packages/react-three-fiber"
},
"homepage": "https://json-render.dev",
"bugs": {
"url": "https://github.com/vercel-labs/json-render/issues"
},
"publishConfig": {
"access": "public"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./catalog": {
"types": "./dist/catalog.d.ts",
"import": "./dist/catalog.mjs",
"require": "./dist/catalog.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"check-types": "tsc --noEmit",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/react": "workspace:*",
"@react-three/postprocessing": "^3.0.4"
},
"devDependencies": {
"@internal/typescript-config": "workspace:*",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@types/react": "19.2.3",
"@types/three": "^0.183.1",
"three": "^0.183.2",
"tsup": "^8.0.2",
"typescript": "^5.4.5",
"zod": "^4.3.6"
},
"peerDependencies": {
"@react-three/drei": ">=9.0.0",
"@react-three/fiber": ">=8.0.0",
"react": "^19.0.0",
"three": ">=0.160.0",
"zod": "^4.0.0"
}
}
+841
View File
@@ -0,0 +1,841 @@
import { z } from "zod";
import {
vector3Schema,
materialSchema,
transformProps,
shadowProps,
} from "./schemas";
/**
* React Three Fiber component definitions for json-render catalogs.
*
* These can be used directly or extended with custom 3D components.
* All components render as React Three Fiber elements.
*/
export const threeComponentDefinitions = {
// ===========================================================================
// Primitives
// ===========================================================================
Box: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
width: z.number().nullable(),
height: z.number().nullable(),
depth: z.number().nullable(),
}),
description:
"Box mesh. Defaults to 1x1x1. Use material for appearance, transform props for placement.",
example: {
position: [0, 0.5, 0],
material: { color: "#4488ff" },
},
},
Sphere: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
radius: z.number().nullable(),
widthSegments: z.number().nullable(),
heightSegments: z.number().nullable(),
}),
description: "Sphere mesh. Defaults to radius 1.",
example: {
position: [0, 1, 0],
radius: 0.5,
material: { color: "#ff4444" },
},
},
Cylinder: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
radiusTop: z.number().nullable(),
radiusBottom: z.number().nullable(),
height: z.number().nullable(),
radialSegments: z.number().nullable(),
}),
description: "Cylinder mesh. Use radiusTop/radiusBottom for tapering.",
example: {
position: [0, 0.5, 0],
height: 1,
material: { color: "#44ff44" },
},
},
Cone: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
radius: z.number().nullable(),
height: z.number().nullable(),
radialSegments: z.number().nullable(),
}),
description: "Cone mesh.",
example: {
position: [0, 0.5, 0],
material: { color: "#ffaa00" },
},
},
Torus: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
radius: z.number().nullable(),
tube: z.number().nullable(),
radialSegments: z.number().nullable(),
tubularSegments: z.number().nullable(),
}),
description: "Torus (donut) mesh.",
example: {
position: [0, 1, 0],
material: { color: "#ff44ff" },
},
},
Plane: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
width: z.number().nullable(),
height: z.number().nullable(),
}),
slots: ["default"],
description:
"Flat plane mesh. Useful for floors/walls. Defaults to XY plane; rotate [-Math.PI/2, 0, 0] for ground. Pass MeshPortalMaterial as child for portal surfaces.",
example: {
position: [0, 0, 0],
rotation: [-1.5708, 0, 0],
scale: [10, 10, 1],
material: { color: "#888888" },
receiveShadow: true,
},
},
Capsule: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
radius: z.number().nullable(),
length: z.number().nullable(),
capSegments: z.number().nullable(),
radialSegments: z.number().nullable(),
}),
description: "Capsule mesh (cylinder with hemispherical caps).",
example: {
position: [0, 1, 0],
material: { color: "#00cccc" },
},
},
// ===========================================================================
// Lights
// ===========================================================================
AmbientLight: {
props: z.object({
color: z.string().nullable(),
intensity: z.number().nullable(),
}),
description:
"Ambient light that illuminates all objects equally. No position needed.",
example: { color: "#ffffff", intensity: 0.5 },
},
DirectionalLight: {
props: z.object({
...transformProps,
color: z.string().nullable(),
intensity: z.number().nullable(),
castShadow: z.boolean().nullable(),
}),
description:
"Directional light (like sunlight). Position determines the direction it shines from.",
example: {
position: [5, 10, 5],
intensity: 1,
castShadow: true,
},
},
PointLight: {
props: z.object({
...transformProps,
color: z.string().nullable(),
intensity: z.number().nullable(),
distance: z.number().nullable(),
decay: z.number().nullable(),
castShadow: z.boolean().nullable(),
}),
description: "Point light that radiates in all directions from a position.",
example: {
position: [0, 3, 0],
intensity: 1,
distance: 10,
},
},
SpotLight: {
props: z.object({
...transformProps,
color: z.string().nullable(),
intensity: z.number().nullable(),
distance: z.number().nullable(),
decay: z.number().nullable(),
angle: z.number().nullable(),
penumbra: z.number().nullable(),
castShadow: z.boolean().nullable(),
}),
description:
"Spot light emitting a cone of light. Angle in radians (default ~Math.PI/3).",
example: {
position: [0, 5, 0],
intensity: 1,
angle: 0.5,
penumbra: 0.5,
castShadow: true,
},
},
// ===========================================================================
// Container
// ===========================================================================
Group: {
props: z.object({
...transformProps,
}),
slots: ["default"],
description:
"Container for grouping child objects. Transforms apply to all children.",
example: { position: [0, 0, 0] },
},
// ===========================================================================
// Model
// ===========================================================================
Model: {
props: z.object({
...transformProps,
...shadowProps,
url: z.string(),
}),
description:
"GLTF/GLB 3D model loader. Provide a URL to a .glb or .gltf file.",
example: {
url: "/models/robot.glb",
position: [0, 0, 0],
scale: [1, 1, 1],
},
},
// ===========================================================================
// Environment
// ===========================================================================
Environment: {
props: z.object({
preset: z
.enum([
"apartment",
"city",
"dawn",
"forest",
"lobby",
"night",
"park",
"studio",
"sunset",
"warehouse",
])
.nullable(),
background: z.boolean().nullable(),
blur: z.number().nullable(),
intensity: z.number().nullable(),
}),
description:
"HDRI environment map for lighting and background. Choose a preset or provide a custom HDRI.",
example: { preset: "sunset", background: true },
},
Fog: {
props: z.object({
color: z.string().nullable(),
near: z.number().nullable(),
far: z.number().nullable(),
}),
description:
"Linear fog effect. Objects fade between near and far distances.",
example: { color: "#cccccc", near: 10, far: 50 },
},
GridHelper: {
props: z.object({
...transformProps,
size: z.number().nullable(),
divisions: z.number().nullable(),
color: z.string().nullable(),
secondaryColor: z.string().nullable(),
}),
description: "Visual grid for reference. Renders on the XZ plane.",
example: { size: 10, divisions: 10 },
},
// ===========================================================================
// Text
// ===========================================================================
Text3D: {
props: z.object({
...transformProps,
text: z.string(),
fontSize: z.number().nullable(),
color: z.string().nullable(),
anchorX: z.enum(["left", "center", "right"]).nullable(),
anchorY: z
.enum(["top", "top-baseline", "middle", "bottom-baseline", "bottom"])
.nullable(),
maxWidth: z.number().nullable(),
}),
description:
"3D text rendered in the scene. Uses SDF text for crisp rendering at any size.",
example: {
text: "Hello World",
fontSize: 1,
color: "#ffffff",
position: [0, 2, 0],
},
},
// ===========================================================================
// Effects / Atmosphere
// ===========================================================================
Sparkles: {
props: z.object({
...transformProps,
count: z.number().nullable(),
speed: z.number().nullable(),
opacity: z.number().nullable(),
color: z.string().nullable(),
size: z.number().nullable(),
noise: z.number().nullable(),
}),
description:
"Floating particle sparkles. Great for magic, snow, or ambient effects.",
example: {
count: 100,
speed: 0.5,
size: 2,
color: "#ffffff",
scale: [5, 5, 5],
},
},
Stars: {
props: z.object({
radius: z.number().nullable(),
depth: z.number().nullable(),
count: z.number().nullable(),
factor: z.number().nullable(),
saturation: z.number().nullable(),
fade: z.boolean().nullable(),
speed: z.number().nullable(),
}),
description:
"Starfield background. Renders thousands of stars in a sphere around the scene.",
example: { radius: 100, depth: 50, count: 5000, factor: 4, fade: true },
},
Sky: {
props: z.object({
distance: z.number().nullable(),
sunPosition: vector3Schema.nullable(),
inclination: z.number().nullable(),
azimuth: z.number().nullable(),
mieCoefficient: z.number().nullable(),
mieDirectionalG: z.number().nullable(),
rayleigh: z.number().nullable(),
turbidity: z.number().nullable(),
}),
description:
"Procedural sky with sun. Control sun position, haze, and scattering.",
example: { sunPosition: [100, 20, 100], turbidity: 8, rayleigh: 2 },
},
Cloud: {
props: z.object({
...transformProps,
seed: z.number().nullable(),
segments: z.number().nullable(),
bounds: vector3Schema.nullable(),
volume: z.number().nullable(),
speed: z.number().nullable(),
fade: z.number().nullable(),
opacity: z.number().nullable(),
color: z.string().nullable(),
growth: z.number().nullable(),
}),
description:
"Volumetric cloud. Use multiple for a cloudscape. Wrap in a Clouds parent for batching.",
example: {
position: [0, 5, 0],
speed: 0.2,
opacity: 0.6,
color: "#ffffff",
},
},
// ===========================================================================
// Special Materials (geometry + material combos)
// ===========================================================================
GlassSphere: {
props: z.object({
...transformProps,
...shadowProps,
radius: z.number().nullable(),
widthSegments: z.number().nullable(),
heightSegments: z.number().nullable(),
color: z.string().nullable(),
transmission: z.number().nullable(),
thickness: z.number().nullable(),
roughness: z.number().nullable(),
chromaticAberration: z.number().nullable(),
ior: z.number().nullable(),
distortion: z.number().nullable(),
distortionScale: z.number().nullable(),
temporalDistortion: z.number().nullable(),
samples: z.number().nullable(),
resolution: z.number().nullable(),
}),
description:
"Glass sphere with transmission/refraction. Creates photorealistic glass effect.",
example: {
position: [0, 1, 0],
radius: 1,
transmission: 1,
thickness: 0.5,
roughness: 0,
chromaticAberration: 0.06,
},
},
GlassBox: {
props: z.object({
...transformProps,
...shadowProps,
width: z.number().nullable(),
height: z.number().nullable(),
depth: z.number().nullable(),
color: z.string().nullable(),
transmission: z.number().nullable(),
thickness: z.number().nullable(),
roughness: z.number().nullable(),
chromaticAberration: z.number().nullable(),
ior: z.number().nullable(),
distortion: z.number().nullable(),
distortionScale: z.number().nullable(),
temporalDistortion: z.number().nullable(),
samples: z.number().nullable(),
resolution: z.number().nullable(),
}),
description:
"Glass box with transmission/refraction. Photorealistic glass cuboid.",
example: {
position: [0, 0.5, 0],
width: 1,
height: 1,
depth: 1,
transmission: 1,
thickness: 0.5,
},
},
DistortSphere: {
props: z.object({
...transformProps,
...shadowProps,
radius: z.number().nullable(),
widthSegments: z.number().nullable(),
heightSegments: z.number().nullable(),
color: z.string().nullable(),
speed: z.number().nullable(),
distort: z.number().nullable(),
metalness: z.number().nullable(),
roughness: z.number().nullable(),
}),
description:
"Animated distorting sphere. Organic, blobby look -- like liquid metal.",
example: {
position: [0, 1, 0],
radius: 1,
color: "#ff6600",
speed: 2,
distort: 0.5,
},
},
// ===========================================================================
// Extended Geometry
// ===========================================================================
TorusKnot: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
radius: z.number().nullable(),
tube: z.number().nullable(),
tubularSegments: z.number().nullable(),
radialSegments: z.number().nullable(),
p: z.number().nullable(),
q: z.number().nullable(),
}),
description:
"Torus knot mesh. A continuous 3D curve that creates intricate knot shapes via p and q parameters.",
example: {
position: [0, 1, 0],
radius: 1,
tube: 0.3,
p: 2,
q: 3,
material: { color: "#ff44ff", metalness: 0.8, roughness: 0.2 },
},
},
RoundedBox: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
width: z.number().nullable(),
height: z.number().nullable(),
depth: z.number().nullable(),
radius: z.number().nullable(),
smoothness: z.number().nullable(),
}),
description:
"Box with rounded edges. More polished look than a plain box. Great for product-style scenes.",
example: {
position: [0, 0.5, 0],
width: 1,
height: 1,
depth: 1,
radius: 0.1,
material: { color: "#4488ff" },
},
},
// ===========================================================================
// Shadows / Staging
// ===========================================================================
ContactShadows: {
props: z.object({
...transformProps,
opacity: z.number().nullable(),
width: z.number().nullable(),
height: z.number().nullable(),
blur: z.number().nullable(),
near: z.number().nullable(),
far: z.number().nullable(),
smooth: z.boolean().nullable(),
resolution: z.number().nullable(),
frames: z.number().nullable(),
color: z.string().nullable(),
}),
description:
"Soft contact shadows projected onto the ground plane. Place at y=0 under objects.",
example: {
position: [0, 0, 0],
opacity: 0.5,
blur: 2,
width: 10,
height: 10,
},
},
Float: {
props: z.object({
...transformProps,
speed: z.number().nullable(),
rotationIntensity: z.number().nullable(),
floatIntensity: z.number().nullable(),
enabled: z.boolean().nullable(),
}),
slots: ["default"],
description:
"Wrapper that makes children gently float and bob. Adds organic motion to any object.",
example: { speed: 1.5, rotationIntensity: 1, floatIntensity: 1 },
},
ReflectorPlane: {
props: z.object({
...transformProps,
width: z.number().nullable(),
height: z.number().nullable(),
color: z.string().nullable(),
resolution: z.number().nullable(),
blur: z.number().nullable(),
mirror: z.number().nullable(),
mixBlur: z.number().nullable(),
mixStrength: z.number().nullable(),
depthScale: z.number().nullable(),
metalness: z.number().nullable(),
roughness: z.number().nullable(),
}),
description:
"Reflective floor/surface. Real-time mirror reflections on a plane. Rotate [-PI/2,0,0] for ground.",
example: {
position: [0, 0, 0],
rotation: [-1.5708, 0, 0],
width: 20,
height: 20,
mirror: 0.5,
blur: [300, 100],
resolution: 1024,
},
},
Backdrop: {
props: z.object({
...transformProps,
floor: z.number().nullable(),
segments: z.number().nullable(),
receiveShadow: z.boolean().nullable(),
}),
slots: ["default"],
description:
"Curved studio backdrop for product-style scenes. Set floor for how much bends.",
example: { floor: 0.25, segments: 20, receiveShadow: true },
},
// ===========================================================================
// Crazy / Magic
// ===========================================================================
MeshPortalMaterial: {
props: z.object({
blend: z.number().nullable(),
blur: z.number().nullable(),
resolution: z.number().nullable(),
}),
slots: ["default"],
description:
"Renders children into a portal surface (like a window to another world). Use as a child of a Mesh.",
example: { blend: 0 },
},
HtmlLabel: {
props: z.object({
...transformProps,
text: z.string(),
transform: z.boolean().nullable(),
distanceFactor: z.number().nullable(),
color: z.string().nullable(),
fontSize: z.number().nullable(),
center: z.boolean().nullable(),
}),
description: "Renders actual HTML/DOM text floating in the 3D scene.",
example: { text: "Hello World", transform: true, distanceFactor: 10 },
},
// ===========================================================================
// Post-Processing
// ===========================================================================
EffectComposer: {
props: z.object({
enabled: z.boolean().nullable(),
multisampling: z.number().nullable(),
}),
slots: ["default"],
description:
"Wrapper for post-processing effects. Add effects as children.",
example: { multisampling: 8 },
},
Bloom: {
props: z.object({
intensity: z.number().nullable(),
luminanceThreshold: z.number().nullable(),
luminanceSmoothing: z.number().nullable(),
mipmapBlur: z.boolean().nullable(),
}),
description: "Bloom post-processing effect. Makes emissive materials glow.",
example: { intensity: 1.5, luminanceThreshold: 0.1, mipmapBlur: true },
},
Glitch: {
props: z.object({
delay: z.tuple([z.number(), z.number()]).nullable(),
duration: z.tuple([z.number(), z.number()]).nullable(),
strength: z.tuple([z.number(), z.number()]).nullable(),
active: z.boolean().nullable(),
ratio: z.number().nullable(),
}),
description: "Cyberpunk glitch post-processing effect.",
example: { active: true },
},
Vignette: {
props: z.object({
offset: z.number().nullable(),
darkness: z.number().nullable(),
}),
description: "Vignette post-processing effect (darkened corners).",
example: { offset: 0.5, darkness: 0.5 },
},
// ===========================================================================
// Animation Wrappers
// ===========================================================================
WarpTunnel: {
props: z.object({
...transformProps,
ringCount: z.number().nullable(),
radius: z.number().nullable(),
length: z.number().nullable(),
speed: z.number().nullable(),
tubeRadius: z.number().nullable(),
color1: z.string().nullable(),
color2: z.string().nullable(),
}),
description:
"Animated neon tunnel flythrough. Rings rush toward the camera creating a hyperspace warp effect.",
example: {
speed: 8,
ringCount: 80,
radius: 3,
color1: "#00ffff",
color2: "#ff00ff",
},
},
Spin: {
props: z.object({
...transformProps,
speed: z.number().nullable(),
axis: z.enum(["x", "y", "z"]).nullable(),
}),
slots: ["default"],
description:
"Wrapper that continuously spins children around an axis. Speed in radians/second.",
example: { speed: 1, axis: "y" },
},
Orbit: {
props: z.object({
...transformProps,
speed: z.number().nullable(),
radius: z.number().nullable(),
tilt: z.number().nullable(),
}),
slots: ["default"],
description:
"Wrapper that orbits children around the Y axis at a given radius and speed.",
example: { speed: 1, radius: 3, tilt: 0.5 },
},
Pulse: {
props: z.object({
...transformProps,
speed: z.number().nullable(),
min: z.number().nullable(),
max: z.number().nullable(),
}),
slots: ["default"],
description:
"Wrapper that pulses children's scale between min and max values.",
example: { speed: 1, min: 0.8, max: 1.2 },
},
CameraShake: {
props: z.object({
intensity: z.number().nullable(),
maxYaw: z.number().nullable(),
maxPitch: z.number().nullable(),
maxRoll: z.number().nullable(),
}),
description:
"Adds camera shake/vibration. Great for speed, impact, or unease effects.",
example: { intensity: 0.5, maxYaw: 0.1, maxPitch: 0.1, maxRoll: 0.1 },
},
// ===========================================================================
// Camera / Controls
// ===========================================================================
PerspectiveCamera: {
props: z.object({
...transformProps,
fov: z.number().nullable(),
near: z.number().nullable(),
far: z.number().nullable(),
makeDefault: z.boolean().nullable(),
}),
description:
"Perspective camera. Set makeDefault to use as the scene's main camera.",
example: {
position: [5, 5, 5],
fov: 50,
makeDefault: true,
},
},
OrbitControls: {
props: z.object({
enableDamping: z.boolean().nullable(),
dampingFactor: z.number().nullable(),
enableZoom: z.boolean().nullable(),
enablePan: z.boolean().nullable(),
enableRotate: z.boolean().nullable(),
minDistance: z.number().nullable(),
maxDistance: z.number().nullable(),
minPolarAngle: z.number().nullable(),
maxPolarAngle: z.number().nullable(),
autoRotate: z.boolean().nullable(),
autoRotateSpeed: z.number().nullable(),
target: z.tuple([z.number(), z.number(), z.number()]).nullable(),
}),
description:
"Orbit camera controls. Allows the user to rotate, zoom, and pan around the scene.",
example: { enableDamping: true, autoRotate: false },
},
};
// =============================================================================
// Types
// =============================================================================
export type ComponentDefinition = {
props: z.ZodType;
slots?: string[];
description: string;
example?: Record<string, unknown>;
};
/**
* Infer the props type for a Three component by name.
*
* @example
* ```ts
* type BoxProps = ThreeProps<"Box">;
* ```
*/
export type ThreeProps<K extends keyof typeof threeComponentDefinitions> =
z.output<(typeof threeComponentDefinitions)[K]["props"]>;
@@ -0,0 +1,909 @@
// @ts-nocheck
import React, { type ReactNode, Suspense, useRef, useMemo } from "react";
import { useFrame } from "@react-three/fiber";
import {
Environment as DreiEnvironment,
OrbitControls as DreiOrbitControls,
PerspectiveCamera as DreiPerspectiveCamera,
Text as DreiText,
Gltf,
Sparkles as DreiSparkles,
Stars as DreiStars,
Sky as DreiSky,
Cloud as DreiCloud,
Clouds as DreiClouds,
Float as DreiFloat,
ContactShadows as DreiContactShadows,
MeshTransmissionMaterial,
MeshDistortMaterial,
MeshReflectorMaterial,
MeshPortalMaterial as DreiMeshPortalMaterial,
Html as DreiHtml,
RoundedBox as DreiRoundedBox,
Backdrop as DreiBackdrop,
CameraShake as DreiCameraShake,
} from "@react-three/drei";
// @ts-ignore
import * as postprocessing from "@react-three/postprocessing";
import * as THREE from "three";
import type { BaseComponentProps } from "@json-render/react";
import type { ThreeProps } from "./catalog";
// =============================================================================
// Helpers
// =============================================================================
type Vec3 = [number, number, number];
const DEFAULT_POS: Vec3 = [0, 0, 0];
const DEFAULT_ROT: Vec3 = [0, 0, 0];
const DEFAULT_SCALE: Vec3 = [1, 1, 1];
function pos(v: Vec3 | null | undefined): Vec3 {
return v ?? DEFAULT_POS;
}
function rot(v: Vec3 | null | undefined): Vec3 {
return v ?? DEFAULT_ROT;
}
function scl(v: Vec3 | null | undefined): Vec3 {
return v ?? DEFAULT_SCALE;
}
function MaterialComponent({
material,
}: {
material: ThreeProps<"Box">["material"] | null | undefined;
}) {
if (!material) return <meshStandardMaterial />;
return (
<meshStandardMaterial
color={material.color ?? "#ffffff"}
metalness={material.metalness ?? 0}
roughness={material.roughness ?? 1}
emissive={material.emissive ?? "#000000"}
emissiveIntensity={material.emissiveIntensity ?? 1}
opacity={material.opacity ?? 1}
transparent={material.transparent ?? false}
wireframe={material.wireframe ?? false}
/>
);
}
// =============================================================================
// Component Implementations
// =============================================================================
/**
* React Three Fiber component implementations for json-render.
*
* Pass to `defineRegistry()` from `@json-render/react` to create a
* component registry for rendering JSON specs as 3D scenes.
*
* @example
* ```ts
* import { defineRegistry } from "@json-render/react";
* import { threeComponents } from "@json-render/react-three-fiber";
*
* const { registry } = defineRegistry(catalog, {
* components: { ...threeComponents },
* });
* ```
*/
export const threeComponents = {
// ── Primitives ─────────────────────────────────────────────────────────
Box: ({ props }: BaseComponentProps<ThreeProps<"Box">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<boxGeometry
args={[props.width ?? 1, props.height ?? 1, props.depth ?? 1]}
/>
<MaterialComponent material={props.material} />
</mesh>
),
Sphere: ({ props }: BaseComponentProps<ThreeProps<"Sphere">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<sphereGeometry
args={[
props.radius ?? 1,
props.widthSegments ?? 32,
props.heightSegments ?? 16,
]}
/>
<MaterialComponent material={props.material} />
</mesh>
),
Cylinder: ({ props }: BaseComponentProps<ThreeProps<"Cylinder">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<cylinderGeometry
args={[
props.radiusTop ?? 1,
props.radiusBottom ?? 1,
props.height ?? 1,
props.radialSegments ?? 32,
]}
/>
<MaterialComponent material={props.material} />
</mesh>
),
Cone: ({ props }: BaseComponentProps<ThreeProps<"Cone">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<coneGeometry
args={[
props.radius ?? 1,
props.height ?? 1,
props.radialSegments ?? 32,
]}
/>
<MaterialComponent material={props.material} />
</mesh>
),
Torus: ({ props }: BaseComponentProps<ThreeProps<"Torus">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<torusGeometry
args={[
props.radius ?? 1,
props.tube ?? 0.4,
props.radialSegments ?? 16,
props.tubularSegments ?? 48,
]}
/>
<MaterialComponent material={props.material} />
</mesh>
),
Plane: ({
props,
children,
}: BaseComponentProps<ThreeProps<"Plane">> & { children?: ReactNode }) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<planeGeometry args={[props.width ?? 1, props.height ?? 1]} />
{children ?? <MaterialComponent material={props.material} />}
</mesh>
),
Capsule: ({ props }: BaseComponentProps<ThreeProps<"Capsule">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<capsuleGeometry
args={[
props.radius ?? 0.5,
props.length ?? 1,
props.capSegments ?? 4,
props.radialSegments ?? 16,
]}
/>
<MaterialComponent material={props.material} />
</mesh>
),
// ── Lights ─────────────────────────────────────────────────────────────
AmbientLight: ({ props }: BaseComponentProps<ThreeProps<"AmbientLight">>) => (
<ambientLight
color={props.color ?? "#ffffff"}
intensity={props.intensity ?? 1}
/>
),
DirectionalLight: ({
props,
}: BaseComponentProps<ThreeProps<"DirectionalLight">>) => (
<directionalLight
position={pos(props.position)}
rotation={rot(props.rotation)}
color={props.color ?? "#ffffff"}
intensity={props.intensity ?? 1}
castShadow={props.castShadow ?? false}
/>
),
PointLight: ({ props }: BaseComponentProps<ThreeProps<"PointLight">>) => (
<pointLight
position={pos(props.position)}
color={props.color ?? "#ffffff"}
intensity={props.intensity ?? 1}
distance={props.distance ?? 0}
decay={props.decay ?? 2}
castShadow={props.castShadow ?? false}
/>
),
SpotLight: ({ props }: BaseComponentProps<ThreeProps<"SpotLight">>) => (
<spotLight
position={pos(props.position)}
color={props.color ?? "#ffffff"}
intensity={props.intensity ?? 1}
distance={props.distance ?? 0}
decay={props.decay ?? 2}
angle={props.angle ?? Math.PI / 3}
penumbra={props.penumbra ?? 0}
castShadow={props.castShadow ?? false}
/>
),
// ── Container ──────────────────────────────────────────────────────────
Group: ({
props,
children,
}: BaseComponentProps<ThreeProps<"Group">> & { children?: ReactNode }) => (
<group
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
>
{children}
</group>
),
// ── Model ──────────────────────────────────────────────────────────────
Model: ({ props }: BaseComponentProps<ThreeProps<"Model">>) => (
<Suspense fallback={null}>
<Gltf
src={props.url}
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
/>
</Suspense>
),
// ── Environment ────────────────────────────────────────────────────────
Environment: ({ props }: BaseComponentProps<ThreeProps<"Environment">>) => (
<DreiEnvironment
preset={props.preset ?? "sunset"}
background={props.background ?? false}
blur={props.blur ?? 0}
environmentIntensity={props.intensity ?? 1}
/>
),
Fog: ({ props }: BaseComponentProps<ThreeProps<"Fog">>) => (
<fog
attach="fog"
args={[props.color ?? "#cccccc", props.near ?? 10, props.far ?? 50]}
/>
),
GridHelper: ({ props }: BaseComponentProps<ThreeProps<"GridHelper">>) => (
<group
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
>
<gridHelper
args={[
props.size ?? 10,
props.divisions ?? 10,
props.color ?? "#888888",
props.secondaryColor ?? "#444444",
]}
/>
</group>
),
// ── Text ───────────────────────────────────────────────────────────────
Text3D: ({ props }: BaseComponentProps<ThreeProps<"Text3D">>) => (
<DreiText
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
fontSize={props.fontSize ?? 1}
color={props.color ?? "#ffffff"}
anchorX={props.anchorX ?? "center"}
anchorY={props.anchorY ?? "middle"}
maxWidth={props.maxWidth ?? undefined}
>
{props.text}
</DreiText>
),
// ── Effects / Atmosphere ──────────────────────────────────────────────
Sparkles: ({ props }: BaseComponentProps<ThreeProps<"Sparkles">>) => (
<DreiSparkles
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
count={props.count ?? 50}
speed={props.speed ?? 0.4}
opacity={props.opacity ?? 1}
color={props.color ?? "#ffffff"}
size={props.size ?? 1}
noise={props.noise ?? 1}
/>
),
Stars: ({ props }: BaseComponentProps<ThreeProps<"Stars">>) => (
<DreiStars
radius={props.radius ?? 100}
depth={props.depth ?? 50}
count={props.count ?? 5000}
factor={props.factor ?? 4}
saturation={props.saturation ?? 0}
fade={props.fade ?? true}
speed={props.speed ?? 1}
/>
),
Sky: ({ props }: BaseComponentProps<ThreeProps<"Sky">>) => (
<DreiSky
distance={props.distance ?? 450000}
sunPosition={props.sunPosition ?? [0, 1, 0]}
inclination={props.inclination ?? undefined}
azimuth={props.azimuth ?? undefined}
mieCoefficient={props.mieCoefficient ?? 0.005}
mieDirectionalG={props.mieDirectionalG ?? 0.8}
rayleigh={props.rayleigh ?? 0.5}
turbidity={props.turbidity ?? 10}
/>
),
Cloud: ({ props }: BaseComponentProps<ThreeProps<"Cloud">>) => (
<DreiClouds limit={200}>
<DreiCloud
position={pos(props.position)}
seed={props.seed ?? undefined}
segments={props.segments ?? 20}
bounds={props.bounds ?? [10, 2, 10]}
volume={props.volume ?? 6}
speed={props.speed ?? 0.2}
fade={props.fade ?? 30}
opacity={props.opacity ?? 0.6}
color={props.color ?? "#ffffff"}
growth={props.growth ?? 4}
/>
</DreiClouds>
),
// ── Special Materials ───────────────────────────────────────────────
GlassSphere: ({ props }: BaseComponentProps<ThreeProps<"GlassSphere">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<sphereGeometry
args={[
props.radius ?? 1,
props.widthSegments ?? 64,
props.heightSegments ?? 32,
]}
/>
<MeshTransmissionMaterial
color={props.color ?? "#ffffff"}
transmission={props.transmission ?? 1}
thickness={props.thickness ?? 0.5}
roughness={props.roughness ?? 0}
chromaticAberration={props.chromaticAberration ?? 0.06}
ior={props.ior ?? 1.5}
distortion={props.distortion ?? 0}
distortionScale={props.distortionScale ?? 0.3}
temporalDistortion={props.temporalDistortion ?? 0.5}
samples={props.samples ?? 10}
resolution={props.resolution ?? 256}
backside
/>
</mesh>
),
GlassBox: ({ props }: BaseComponentProps<ThreeProps<"GlassBox">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<boxGeometry
args={[props.width ?? 1, props.height ?? 1, props.depth ?? 1]}
/>
<MeshTransmissionMaterial
color={props.color ?? "#ffffff"}
transmission={props.transmission ?? 1}
thickness={props.thickness ?? 0.5}
roughness={props.roughness ?? 0}
chromaticAberration={props.chromaticAberration ?? 0.06}
ior={props.ior ?? 1.5}
distortion={props.distortion ?? 0}
distortionScale={props.distortionScale ?? 0.3}
temporalDistortion={props.temporalDistortion ?? 0.5}
samples={props.samples ?? 10}
resolution={props.resolution ?? 256}
backside
/>
</mesh>
),
DistortSphere: ({
props,
}: BaseComponentProps<ThreeProps<"DistortSphere">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<sphereGeometry
args={[
props.radius ?? 1,
props.widthSegments ?? 64,
props.heightSegments ?? 32,
]}
/>
<MeshDistortMaterial
color={props.color ?? "#ff6600"}
speed={props.speed ?? 2}
distort={props.distort ?? 0.5}
metalness={props.metalness ?? 0}
roughness={props.roughness ?? 0.2}
/>
</mesh>
),
// ── Extended Geometry ───────────────────────────────────────────────
TorusKnot: ({ props }: BaseComponentProps<ThreeProps<"TorusKnot">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
<torusKnotGeometry
args={[
props.radius ?? 1,
props.tube ?? 0.4,
props.tubularSegments ?? 64,
props.radialSegments ?? 8,
props.p ?? 2,
props.q ?? 3,
]}
/>
<MaterialComponent material={props.material} />
</mesh>
),
RoundedBox: ({ props }: BaseComponentProps<ThreeProps<"RoundedBox">>) => (
<DreiRoundedBox
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
args={[props.width ?? 1, props.height ?? 1, props.depth ?? 1]}
radius={props.radius ?? 0.05}
smoothness={props.smoothness ?? 4}
>
<MaterialComponent material={props.material} />
</DreiRoundedBox>
),
// ── Shadows / Staging ───────────────────────────────────────────────
ContactShadows: ({
props,
}: BaseComponentProps<ThreeProps<"ContactShadows">>) => (
<DreiContactShadows
position={pos(props.position)}
rotation={rot(props.rotation)}
opacity={props.opacity ?? 0.5}
width={props.width ?? 10}
height={props.height ?? 10}
blur={props.blur ?? 2}
near={props.near ?? undefined}
far={props.far ?? 10}
smooth={props.smooth ?? true}
resolution={props.resolution ?? 256}
frames={props.frames ?? undefined}
color={props.color ?? "#000000"}
/>
),
Float: ({
props,
children,
}: BaseComponentProps<ThreeProps<"Float">> & { children?: ReactNode }) => (
<DreiFloat
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
speed={props.speed ?? 1}
rotationIntensity={props.rotationIntensity ?? 1}
floatIntensity={props.floatIntensity ?? 1}
enabled={props.enabled ?? true}
>
{children}
</DreiFloat>
),
ReflectorPlane: ({
props,
}: BaseComponentProps<ThreeProps<"ReflectorPlane">>) => (
<mesh
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
>
<planeGeometry args={[props.width ?? 10, props.height ?? 10]} />
<MeshReflectorMaterial
color={props.color ?? "#888888"}
resolution={props.resolution ?? 1024}
blur={props.blur ? [props.blur, props.blur] : [300, 100]}
mirror={props.mirror ?? 0.5}
mixBlur={props.mixBlur ?? 10}
mixStrength={props.mixStrength ?? 2}
depthScale={props.depthScale ?? 0.1}
metalness={props.metalness ?? 0.5}
roughness={props.roughness ?? 1}
/>
</mesh>
),
Backdrop: ({
props,
children,
}: BaseComponentProps<ThreeProps<"Backdrop">> & { children?: ReactNode }) => (
<DreiBackdrop
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
floor={props.floor ?? 0.25}
segments={props.segments ?? 20}
receiveShadow={props.receiveShadow ?? true}
>
{children}
</DreiBackdrop>
),
// ── Crazy / Magic ─────────────────────────────────────────────────────
MeshPortalMaterial: ({
props,
children,
}: BaseComponentProps<ThreeProps<"MeshPortalMaterial">> & {
children?: ReactNode;
}) => (
<DreiMeshPortalMaterial
blend={props.blend ?? 0}
blur={props.blur ?? 0}
resolution={props.resolution ?? 512}
>
{children}
</DreiMeshPortalMaterial>
),
HtmlLabel: ({ props }: BaseComponentProps<ThreeProps<"HtmlLabel">>) => (
<DreiHtml
position={pos(props.position)}
transform={props.transform ?? true}
distanceFactor={props.distanceFactor ?? 10}
center={props.center ?? true}
style={{
color: props.color ?? "#ffffff",
fontSize: props.fontSize ? `${props.fontSize}px` : "16px",
fontFamily: "system-ui, sans-serif",
whiteSpace: "nowrap",
pointerEvents: "none",
}}
>
{props.text}
</DreiHtml>
),
// ── Post-Processing ────────────────────────────────────────────────────
EffectComposer: ({
props,
children,
}: BaseComponentProps<ThreeProps<"EffectComposer">> & {
children?: ReactNode;
}) => {
if (props.enabled === false) return null;
return (
<postprocessing.EffectComposer multisampling={props.multisampling ?? 8}>
{children}
</postprocessing.EffectComposer>
);
},
Bloom: ({ props }: BaseComponentProps<ThreeProps<"Bloom">>) => (
<postprocessing.Bloom
intensity={props.intensity ?? 1.5}
luminanceThreshold={props.luminanceThreshold ?? 0.1}
luminanceSmoothing={props.luminanceSmoothing ?? 0.025}
mipmapBlur={props.mipmapBlur ?? true}
/>
),
Glitch: ({ props }: BaseComponentProps<ThreeProps<"Glitch">>) => {
const delay = props.delay ? new THREE.Vector2(...props.delay) : undefined;
const duration = props.duration
? new THREE.Vector2(...props.duration)
: undefined;
const strength = props.strength
? new THREE.Vector2(...props.strength)
: undefined;
return (
<postprocessing.Glitch
delay={delay}
duration={duration}
strength={strength}
active={props.active ?? true}
ratio={props.ratio ?? 0.85}
/>
);
},
Vignette: ({ props }: BaseComponentProps<ThreeProps<"Vignette">>) => (
<postprocessing.Vignette
offset={props.offset ?? 0.5}
darkness={props.darkness ?? 0.5}
/>
),
// ── Animation Wrappers ──────────────────────────────────────────────
WarpTunnel: (() => {
function TunnelRing({
index,
ringCount,
radius,
length,
speed,
tubeRadius,
color,
}: {
index: number;
ringCount: number;
radius: number;
length: number;
speed: number;
tubeRadius: number;
color: THREE.Color;
}) {
const meshRef = useRef<THREE.Mesh>(null);
const spacing = length / ringCount;
useFrame((state) => {
if (!meshRef.current) return;
const t = state.clock.elapsedTime;
const z =
((((-index * spacing + t * speed) % length) + length) % length) -
length;
meshRef.current.position.z = z;
const distFactor = 1 - Math.abs(z) / length;
const s = 0.3 + distFactor * 0.7;
meshRef.current.scale.set(s, s, s);
});
return (
<mesh ref={meshRef} rotation={[0, 0, (index * Math.PI) / 12]}>
<torusGeometry args={[radius, tubeRadius, 16, 64]} />
<meshStandardMaterial
color={color}
emissive={color}
emissiveIntensity={3}
transparent
opacity={0.7}
side={THREE.DoubleSide}
/>
</mesh>
);
}
return ({ props }: BaseComponentProps<ThreeProps<"WarpTunnel">>) => {
const ringCount = props.ringCount ?? 80;
const radius = props.radius ?? 3;
const length = props.length ?? 40;
const speed = props.speed ?? 8;
const tubeRadius = props.tubeRadius ?? 0.02;
const color1 = props.color1 ?? "#00ffff";
const color2 = props.color2 ?? "#ff00ff";
const colors = useMemo(() => {
const c1 = new THREE.Color(color1);
const c2 = new THREE.Color(color2);
return Array.from({ length: ringCount }, (_, i) =>
new THREE.Color().lerpColors(c1, c2, i / ringCount),
);
}, [ringCount, color1, color2]);
return (
<group
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
>
{colors.map((color, i) => (
<TunnelRing
key={i}
index={i}
ringCount={ringCount}
radius={radius}
length={length}
speed={speed}
tubeRadius={tubeRadius}
color={color}
/>
))}
</group>
);
};
})(),
Spin: ({
props,
children,
}: BaseComponentProps<ThreeProps<"Spin">> & { children?: ReactNode }) => {
const ref = useRef<THREE.Group>(null);
const speed = props.speed ?? 1;
const axis = props.axis ?? "y";
useFrame((_, delta) => {
if (!ref.current) return;
if (axis === "x") ref.current.rotation.x += delta * speed;
else if (axis === "z") ref.current.rotation.z += delta * speed;
else ref.current.rotation.y += delta * speed;
});
return (
<group
ref={ref}
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
>
{children}
</group>
);
},
Orbit: ({
props,
children,
}: BaseComponentProps<ThreeProps<"Orbit">> & { children?: ReactNode }) => {
const ref = useRef<THREE.Group>(null);
const speed = props.speed ?? 1;
const radius = props.radius ?? 3;
const tilt = props.tilt ?? 0;
const baseY = props.position?.[1] ?? 0;
useFrame((state) => {
if (!ref.current) return;
const t = state.clock.elapsedTime * speed;
ref.current.position.x = Math.cos(t) * radius;
ref.current.position.z = Math.sin(t) * radius;
ref.current.position.y = baseY + Math.sin(t * 0.7) * tilt;
});
return <group ref={ref}>{children}</group>;
},
Pulse: ({
props,
children,
}: BaseComponentProps<ThreeProps<"Pulse">> & { children?: ReactNode }) => {
const ref = useRef<THREE.Group>(null);
const speed = props.speed ?? 1;
const min = props.min ?? 0.8;
const max = props.max ?? 1.2;
useFrame((state) => {
if (!ref.current) return;
const t = (Math.sin(state.clock.elapsedTime * speed) + 1) / 2;
const s = min + t * (max - min);
ref.current.scale.setScalar(s);
});
return (
<group
ref={ref}
position={pos(props.position)}
rotation={rot(props.rotation)}
>
{children}
</group>
);
},
CameraShake: ({ props }: BaseComponentProps<ThreeProps<"CameraShake">>) => (
<DreiCameraShake
intensity={props.intensity ?? 0.5}
maxYaw={props.maxYaw ?? 0.1}
maxPitch={props.maxPitch ?? 0.1}
maxRoll={props.maxRoll ?? 0.1}
/>
),
// ── Camera / Controls ──────────────────────────────────────────────
PerspectiveCamera: ({
props,
}: BaseComponentProps<ThreeProps<"PerspectiveCamera">>) => (
<DreiPerspectiveCamera
position={pos(props.position)}
rotation={rot(props.rotation)}
fov={props.fov ?? 50}
near={props.near ?? 0.1}
far={props.far ?? 1000}
makeDefault={props.makeDefault ?? false}
/>
),
OrbitControls: ({
props,
}: BaseComponentProps<ThreeProps<"OrbitControls">>) => (
<DreiOrbitControls
enableDamping={props.enableDamping ?? true}
dampingFactor={props.dampingFactor ?? 0.05}
enableZoom={props.enableZoom ?? true}
enablePan={props.enablePan ?? true}
enableRotate={props.enableRotate ?? true}
minDistance={props.minDistance ?? undefined}
maxDistance={props.maxDistance ?? undefined}
minPolarAngle={props.minPolarAngle ?? undefined}
maxPolarAngle={props.maxPolarAngle ?? undefined}
autoRotate={props.autoRotate ?? false}
autoRotateSpeed={props.autoRotateSpeed ?? 2}
target={props.target ?? undefined}
/>
),
};
+25
View File
@@ -0,0 +1,25 @@
// Component implementations
export { threeComponents } from "./components";
// Catalog definitions (also available via @json-render/react-three-fiber/catalog)
export {
threeComponentDefinitions,
type ComponentDefinition,
type ThreeProps,
} from "./catalog";
// Shared schemas
export {
vector3Schema,
materialSchema,
transformProps,
shadowProps,
} from "./schemas";
// Renderer
export {
ThreeRenderer,
ThreeCanvas,
type ThreeRendererProps,
type ThreeCanvasProps,
} from "./renderer";
+19
View File
@@ -0,0 +1,19 @@
import type { ThreeElements } from "@react-three/fiber";
declare module "react/jsx-runtime" {
namespace JSX {
interface IntrinsicElements extends ThreeElements {}
}
}
declare module "react/jsx-dev-runtime" {
namespace JSX {
interface IntrinsicElements extends ThreeElements {}
}
}
declare module "react" {
namespace JSX {
interface IntrinsicElements extends ThreeElements {}
}
}
+144
View File
@@ -0,0 +1,144 @@
import { type ReactNode } from "react";
import { Canvas, type CanvasProps } from "@react-three/fiber";
import {
Renderer,
StateProvider,
VisibilityProvider,
ActionProvider,
ValidationProvider,
type ComponentRegistry,
type ComponentRenderer,
} from "@json-render/react";
import type { Spec, StateStore, ComputedFunction } from "@json-render/core";
// =============================================================================
// ThreeRenderer
// =============================================================================
export interface ThreeRendererProps {
/** The spec to render as a 3D scene */
spec: Spec | null;
/** Component registry */
registry: ComponentRegistry;
/**
* External store (controlled mode). When provided, `initialState` and
* `onStateChange` are ignored.
*/
store?: StateStore;
/** Initial state model (uncontrolled mode) */
initialState?: Record<string, unknown>;
/** Action handlers */
handlers?: Record<
string,
(params: Record<string, unknown>) => Promise<unknown> | unknown
>;
/** Named functions for `$computed` expressions in props */
functions?: Record<string, ComputedFunction>;
/** Callback when state changes (uncontrolled mode) */
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
/** Whether the spec is currently loading/streaming */
loading?: boolean;
/** Fallback component for unknown types */
fallback?: ComponentRenderer;
/** Additional R3F elements to render alongside the spec (lights, helpers, etc.) */
children?: ReactNode;
}
/**
* Renders a json-render spec as a React Three Fiber scene graph.
*
* Place inside a `<Canvas>` or use `<ThreeCanvas>` for a convenience wrapper.
*
* @example
* ```tsx
* <Canvas shadows>
* <ThreeRenderer spec={spec} registry={registry} />
* </Canvas>
* ```
*/
export function ThreeRenderer({
spec,
registry,
store,
initialState,
handlers,
functions,
onStateChange,
loading,
fallback,
children,
}: ThreeRendererProps) {
return (
<StateProvider
store={store}
initialState={initialState ?? spec?.state}
onStateChange={onStateChange}
>
<VisibilityProvider>
<ValidationProvider>
<ActionProvider handlers={handlers}>
<Renderer
spec={spec}
registry={registry}
loading={loading}
fallback={fallback}
/>
{children}
</ActionProvider>
</ValidationProvider>
</VisibilityProvider>
</StateProvider>
);
}
// =============================================================================
// ThreeCanvas
// =============================================================================
export interface ThreeCanvasProps extends ThreeRendererProps {
/** Enable shadows on the Canvas */
shadows?: CanvasProps["shadows"];
/** Canvas camera config */
camera?: CanvasProps["camera"];
/** Canvas className */
className?: string;
/** Canvas style */
style?: React.CSSProperties;
/** Additional Canvas props */
canvasProps?: Omit<CanvasProps, "shadows" | "camera" | "className" | "style">;
}
/**
* Convenience wrapper that sets up a `<Canvas>` with a `<ThreeRenderer>` inside.
*
* @example
* ```tsx
* <ThreeCanvas
* spec={spec}
* registry={registry}
* shadows
* camera={{ position: [5, 5, 5], fov: 50 }}
* style={{ width: "100%", height: "100vh" }}
* />
* ```
*/
export function ThreeCanvas({
shadows,
camera,
className,
style,
canvasProps,
...rendererProps
}: ThreeCanvasProps) {
return (
<Canvas
shadows={shadows}
camera={camera}
className={className}
style={style}
{...canvasProps}
>
<ThreeRenderer {...rendererProps} />
</Canvas>
);
}
+25
View File
@@ -0,0 +1,25 @@
import { z } from "zod";
export const vector3Schema = z.tuple([z.number(), z.number(), z.number()]);
export const materialSchema = z.object({
color: z.string().nullable(),
metalness: z.number().nullable(),
roughness: z.number().nullable(),
emissive: z.string().nullable(),
emissiveIntensity: z.number().nullable(),
opacity: z.number().nullable(),
transparent: z.boolean().nullable(),
wireframe: z.boolean().nullable(),
});
export const transformProps = {
position: vector3Schema.nullable(),
rotation: vector3Schema.nullable(),
scale: vector3Schema.nullable(),
} as const;
export const shadowProps = {
castShadow: z.boolean().nullable(),
receiveShadow: z.boolean().nullable(),
} as const;
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@internal/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
+19
View File
@@ -0,0 +1,19 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts", "src/catalog.ts"],
format: ["cjs", "esm"],
dts: true,
sourcemap: true,
clean: true,
external: [
"react",
"react-dom",
"@json-render/core",
"@json-render/react",
"@react-three/fiber",
"@react-three/drei",
"three",
"zod",
],
});
+305 -55
View File
@@ -867,6 +867,61 @@ importers:
specifier: ^5.7.2
version: 5.9.3
examples/react-three-fiber:
dependencies:
'@json-render/core':
specifier: workspace:*
version: link:../../packages/core
'@json-render/react':
specifier: workspace:*
version: link:../../packages/react
'@json-render/react-three-fiber':
specifier: workspace:*
version: link:../../packages/react-three-fiber
'@react-three/drei':
specifier: ^10.7.7
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.3)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
'@react-three/fiber':
specifier: ^9.5.0
version: 9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2)
next:
specifier: 16.1.6
version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
specifier: 19.2.4
version: 19.2.4
react-dom:
specifier: 19.2.4
version: 19.2.4(react@19.2.4)
three:
specifier: ^0.183.2
version: 0.183.2
zod:
specifier: 4.3.6
version: 4.3.6
devDependencies:
'@internal/eslint-config':
specifier: workspace:*
version: link:../../packages/eslint-config
'@types/node':
specifier: ^22.10.0
version: 22.19.6
'@types/react':
specifier: 19.2.3
version: 19.2.3
'@types/react-dom':
specifier: 19.2.3
version: 19.2.3(@types/react@19.2.3)
'@types/three':
specifier: ^0.183.1
version: 0.183.1
eslint:
specifier: ^9.39.1
version: 9.39.2(jiti@2.6.1)
typescript:
specifier: ^5.7.2
version: 5.9.3
examples/remotion:
dependencies:
'@ai-sdk/gateway':
@@ -946,6 +1001,31 @@ importers:
specifier: ^5.7.2
version: 5.9.3
examples/solid:
dependencies:
'@json-render/core':
specifier: workspace:*
version: link:../../packages/core
'@json-render/solid':
specifier: workspace:*
version: link:../../packages/solid
solid-js:
specifier: ^1.9.11
version: 1.9.11
zod:
specifier: ^4.3.6
version: 4.3.6
devDependencies:
typescript:
specifier: ^5.9.3
version: 5.9.3
vite:
specifier: ^7.3.1
version: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
vite-plugin-solid:
specifier: ^2.11.10
version: 2.11.10(solid-js@1.9.11)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
examples/stripe-app/api:
dependencies:
'@ai-sdk/gateway':
@@ -1512,6 +1592,49 @@ importers:
specifier: ^5.4.5
version: 5.9.3
packages/react-three-fiber:
dependencies:
'@json-render/core':
specifier: workspace:*
version: link:../core
'@json-render/react':
specifier: workspace:*
version: link:../react
'@react-three/postprocessing':
specifier: ^3.0.4
version: 3.0.4(@react-three/fiber@9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/three@0.183.1)(react@19.2.4)(three@0.183.2)
react:
specifier: ^19.0.0
version: 19.2.4
devDependencies:
'@internal/typescript-config':
specifier: workspace:*
version: link:../typescript-config
'@react-three/drei':
specifier: ^10.7.7
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.3)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
'@react-three/fiber':
specifier: ^9.5.0
version: 9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2)
'@types/react':
specifier: 19.2.3
version: 19.2.3
'@types/three':
specifier: ^0.183.1
version: 0.183.1
three:
specifier: ^0.183.2
version: 0.183.2
tsup:
specifier: ^8.0.2
version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
typescript:
specifier: ^5.4.5
version: 5.9.3
zod:
specifier: ^4.3.6
version: 4.3.6
packages/redux:
dependencies:
'@json-render/core':
@@ -5108,6 +5231,13 @@ packages:
react-native:
optional: true
'@react-three/postprocessing@3.0.4':
resolution: {integrity: sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==}
peerDependencies:
'@react-three/fiber': ^9.0.0
react: ^19.0
three: '>= 0.156.0'
'@reduxjs/toolkit@2.11.2':
resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
peerDependencies:
@@ -6005,6 +6135,9 @@ packages:
'@types/three@0.182.0':
resolution: {integrity: sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==}
'@types/three@0.183.1':
resolution: {integrity: sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -9684,6 +9817,12 @@ packages:
'@types/three': '>=0.134.0'
three: '>=0.134.0'
maath@0.6.0:
resolution: {integrity: sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==}
peerDependencies:
'@types/three': '>=0.144.0'
three: '>=0.144.0'
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -9812,6 +9951,9 @@ packages:
meshoptimizer@0.22.0:
resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==}
meshoptimizer@1.0.1:
resolution: {integrity: sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==}
metro-babel-transformer@0.83.3:
resolution: {integrity: sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==}
engines: {node: '>=20.19.4'}
@@ -10091,6 +10233,12 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
n8ao@1.10.1:
resolution: {integrity: sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==}
peerDependencies:
postprocessing: '>=6.30.0'
three: '>=0.137'
nano-spawn@2.0.0:
resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==}
engines: {node: '>=20.17'}
@@ -10599,6 +10747,11 @@ packages:
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
engines: {node: '>=12'}
postprocessing@6.38.3:
resolution: {integrity: sha512-5qCFp8j62nWL6sSVv/RKuHscQUIV+VMMgWeHLYZQEBpAk7G+r3jA3bSKON7gZjiuxdZ/F4PXj2Jc1oPh/7Eg+g==}
peerDependencies:
three: '>= 0.157.0 < 0.184.0'
potpack@1.0.2:
resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
@@ -11843,6 +11996,9 @@ packages:
three@0.182.0:
resolution: {integrity: sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==}
three@0.183.2:
resolution: {integrity: sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==}
throat@5.0.0:
resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==}
@@ -14354,7 +14510,6 @@ snapshots:
- graphql
- supports-color
- utf-8-validate
optional: true
'@expo/code-signing-certificates@0.0.6':
dependencies:
@@ -14419,7 +14574,6 @@ snapshots:
optionalDependencies:
react: 19.2.4
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
optional: true
'@expo/env@2.0.8':
dependencies:
@@ -14489,7 +14643,7 @@ snapshots:
postcss: 8.4.49
resolve-from: 5.0.0
optionalDependencies:
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -14570,7 +14724,7 @@ snapshots:
'@expo/json-file': 10.0.8
'@react-native/normalize-colors': 0.81.5
debug: 4.4.3
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
resolve-from: 5.0.0
semver: 7.7.3
xml2js: 0.6.0
@@ -14598,7 +14752,6 @@ snapshots:
expo-font: 14.0.11(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
optional: true
'@expo/ws-tunnel@1.0.6': {}
@@ -15098,7 +15251,7 @@ snapshots:
'@mdx-js/mdx': 3.1.1
source-map: 0.7.6
optionalDependencies:
webpack: 5.96.1
webpack: 5.96.1(esbuild@0.25.0)
transitivePeerDependencies:
- supports-color
@@ -15205,6 +15358,11 @@ snapshots:
promise-worker-transferable: 1.0.4
three: 0.182.0
'@monogrid/gainmap-js@3.4.0(three@0.183.2)':
dependencies:
promise-worker-transferable: 1.0.4
three: 0.183.2
'@mswjs/interceptors@0.41.3':
dependencies:
'@open-draft/deferred-promise': 2.2.0
@@ -17620,6 +17778,39 @@ snapshots:
- '@types/three'
- immer
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.3)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)':
dependencies:
'@babel/runtime': 7.28.6
'@mediapipe/tasks-vision': 0.10.17
'@monogrid/gainmap-js': 3.4.0(three@0.183.2)
'@react-three/fiber': 9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2)
'@use-gesture/react': 10.3.1(react@19.2.4)
camera-controls: 3.1.2(three@0.183.2)
cross-env: 7.0.3
detect-gpu: 5.0.70
glsl-noise: 0.0.0
hls.js: 1.6.15
maath: 0.10.8(@types/three@0.183.1)(three@0.183.2)
meshline: 3.3.1(three@0.183.2)
react: 19.2.4
stats-gl: 2.4.2(@types/three@0.183.1)(three@0.183.2)
stats.js: 0.17.0
suspend-react: 0.1.3(react@19.2.4)
three: 0.183.2
three-mesh-bvh: 0.8.3(three@0.183.2)
three-stdlib: 2.36.1(three@0.183.2)
troika-three-text: 0.52.4(three@0.183.2)
tunnel-rat: 0.1.2(@types/react@19.2.3)(immer@11.1.4)(react@19.2.4)
use-sync-external-store: 1.6.0(react@19.2.4)
utility-types: 3.11.0
zustand: 5.0.11(@types/react@19.2.3)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
transitivePeerDependencies:
- '@types/react'
- '@types/three'
- immer
'@react-three/fiber@9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.182.0)':
dependencies:
'@babel/runtime': 7.28.6
@@ -17644,6 +17835,41 @@ snapshots:
- '@types/react'
- immer
'@react-three/fiber@9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2)':
dependencies:
'@babel/runtime': 7.28.6
'@types/webxr': 0.5.24
base64-js: 1.5.1
buffer: 6.0.3
its-fine: 2.0.0(@types/react@19.2.3)(react@19.2.4)
react: 19.2.4
react-use-measure: 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
scheduler: 0.27.0
suspend-react: 0.1.3(react@19.2.4)
three: 0.183.2
use-sync-external-store: 1.6.0(react@19.2.4)
zustand: 5.0.11(@types/react@19.2.3)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
optionalDependencies:
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
expo-asset: 12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
expo-file-system: 19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))
react-dom: 19.2.4(react@19.2.4)
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
transitivePeerDependencies:
- '@types/react'
- immer
'@react-three/postprocessing@3.0.4(@react-three/fiber@9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/three@0.183.1)(react@19.2.4)(three@0.183.2)':
dependencies:
'@react-three/fiber': 9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2)
maath: 0.6.0(@types/three@0.183.1)(three@0.183.2)
n8ao: 1.10.1(postprocessing@6.38.3(three@0.183.2))(three@0.183.2)
postprocessing: 6.38.3(three@0.183.2)
react: 19.2.4
three: 0.183.2
transitivePeerDependencies:
- '@types/three'
'@reduxjs/toolkit@2.11.2(react@19.2.4)':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -18558,6 +18784,16 @@ snapshots:
fflate: 0.8.2
meshoptimizer: 0.22.0
'@types/three@0.183.1':
dependencies:
'@dimforge/rapier3d-compat': 0.12.0
'@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.4
'@types/webxr': 0.5.24
'@webgpu/types': 0.1.69
fflate: 0.8.2
meshoptimizer: 1.0.1
'@types/trusted-types@2.0.7': {}
'@types/unist@2.0.11': {}
@@ -19483,7 +19719,7 @@ snapshots:
resolve-from: 5.0.0
optionalDependencies:
'@babel/runtime': 7.28.6
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- '@babel/core'
- supports-color
@@ -19683,6 +19919,10 @@ snapshots:
dependencies:
three: 0.182.0
camera-controls@3.1.2(three@0.183.2):
dependencies:
three: 0.183.2
caniuse-lite@1.0.30001764: {}
ccount@2.0.1: {}
@@ -20916,7 +21156,6 @@ snapshots:
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
transitivePeerDependencies:
- supports-color
optional: true
expo-clipboard@8.0.8(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -20941,7 +21180,6 @@ snapshots:
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
transitivePeerDependencies:
- supports-color
optional: true
expo-file-system@19.0.21(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)):
dependencies:
@@ -20952,7 +21190,6 @@ snapshots:
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
optional: true
expo-font@14.0.11(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -20967,7 +21204,6 @@ snapshots:
fontfaceobserver: 2.3.0
react: 19.2.4
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
optional: true
expo-keep-awake@15.0.8(expo@54.0.33)(react@19.1.0):
dependencies:
@@ -20978,7 +21214,6 @@ snapshots:
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
react: 19.2.4
optional: true
expo-linking@8.0.11(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -21020,7 +21255,6 @@ snapshots:
invariant: 2.2.4
react: 19.2.4
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
optional: true
expo-router@6.0.23(@expo/metro-runtime@6.1.2)(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.11)(expo@54.0.33)(react-dom@19.2.4(react@19.1.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.11.1(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -21183,7 +21417,6 @@ snapshots:
- graphql
- supports-color
- utf-8-validate
optional: true
exponential-backoff@3.1.3: {}
@@ -23087,6 +23320,16 @@ snapshots:
'@types/three': 0.182.0
three: 0.182.0
maath@0.10.8(@types/three@0.183.1)(three@0.183.2):
dependencies:
'@types/three': 0.183.1
three: 0.183.2
maath@0.6.0(@types/three@0.183.1)(three@0.183.2):
dependencies:
'@types/three': 0.183.1
three: 0.183.2
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -23307,8 +23550,14 @@ snapshots:
dependencies:
three: 0.182.0
meshline@3.3.1(three@0.183.2):
dependencies:
three: 0.183.2
meshoptimizer@0.22.0: {}
meshoptimizer@1.0.1: {}
metro-babel-transformer@0.83.3:
dependencies:
'@babel/core': 7.29.0
@@ -23883,6 +24132,11 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
n8ao@1.10.1(postprocessing@6.38.3(three@0.183.2))(three@0.183.2):
dependencies:
postprocessing: 6.38.3(three@0.183.2)
three: 0.183.2
nano-spawn@2.0.0: {}
nanoid@3.3.11: {}
@@ -24381,6 +24635,10 @@ snapshots:
postgres@3.4.8: {}
postprocessing@6.38.3(three@0.183.2):
dependencies:
three: 0.183.2
potpack@1.0.2: {}
powershell-utils@0.1.0: {}
@@ -25845,6 +26103,11 @@ snapshots:
'@types/three': 0.182.0
three: 0.182.0
stats-gl@2.4.2(@types/three@0.183.1)(three@0.183.2):
dependencies:
'@types/three': 0.183.1
three: 0.183.2
stats.js@0.17.0: {}
statuses@1.5.0: {}
@@ -26222,16 +26485,6 @@ snapshots:
optionalDependencies:
esbuild: 0.25.0
terser-webpack-plugin@5.3.16(webpack@5.96.1):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
serialize-javascript: 6.0.2
terser: 5.46.0
webpack: 5.96.1
optional: true
terser@5.46.0:
dependencies:
'@jridgewell/source-map': 0.3.11
@@ -26259,6 +26512,10 @@ snapshots:
dependencies:
three: 0.182.0
three-mesh-bvh@0.8.3(three@0.183.2):
dependencies:
three: 0.183.2
three-stdlib@2.36.1(three@0.182.0):
dependencies:
'@types/draco3d': 1.4.10
@@ -26269,8 +26526,20 @@ snapshots:
potpack: 1.0.2
three: 0.182.0
three-stdlib@2.36.1(three@0.183.2):
dependencies:
'@types/draco3d': 1.4.10
'@types/offscreencanvas': 2019.7.3
'@types/webxr': 0.5.24
draco3d: 1.5.7
fflate: 0.6.10
potpack: 1.0.2
three: 0.183.2
three@0.182.0: {}
three@0.183.2: {}
throat@5.0.0: {}
throat@6.0.2: {}
@@ -26351,10 +26620,22 @@ snapshots:
troika-worker-utils: 0.52.0
webgl-sdf-generator: 1.1.1
troika-three-text@0.52.4(three@0.183.2):
dependencies:
bidi-js: 1.0.3
three: 0.183.2
troika-three-utils: 0.52.4(three@0.183.2)
troika-worker-utils: 0.52.0
webgl-sdf-generator: 1.1.1
troika-three-utils@0.52.4(three@0.182.0):
dependencies:
three: 0.182.0
troika-three-utils@0.52.4(three@0.183.2):
dependencies:
three: 0.183.2
troika-worker-utils@0.52.0: {}
trough@2.2.0: {}
@@ -27031,37 +27312,6 @@ snapshots:
webpack-sources@3.3.3: {}
webpack@5.96.1:
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.8
'@webassemblyjs/ast': 1.14.1
'@webassemblyjs/wasm-edit': 1.14.1
'@webassemblyjs/wasm-parser': 1.14.1
acorn: 8.15.0
browserslist: 4.28.1
chrome-trace-event: 1.0.4
enhanced-resolve: 5.19.0
es-module-lexer: 1.7.0
eslint-scope: 5.1.1
events: 3.3.0
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
json-parse-even-better-errors: 2.3.1
loader-runner: 4.3.1
mime-types: 2.1.35
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.3.0
terser-webpack-plugin: 5.3.16(webpack@5.96.1)
watchpack: 2.5.1
webpack-sources: 3.3.3
transitivePeerDependencies:
- '@swc/core'
- esbuild
- uglify-js
optional: true
webpack@5.96.1(esbuild@0.25.0):
dependencies:
'@types/eslint-scope': 3.7.7
+180
View File
@@ -0,0 +1,180 @@
---
name: react-three-fiber
description: React Three Fiber 3D renderer for json-render. Use when working with @json-render/react-three-fiber, building 3D scenes from JSON specs, rendering meshes/lights/models/environments, or integrating Three.js with json-render catalogs.
---
# @json-render/react-three-fiber
React Three Fiber renderer for json-render. 19 built-in 3D components.
## Two Entry Points
| Entry Point | Exports | Use For |
|-------------|---------|---------|
| `@json-render/react-three-fiber/catalog` | `threeComponentDefinitions` | Catalog schemas (no R3F dependency, safe for server) |
| `@json-render/react-three-fiber` | `threeComponents`, `ThreeRenderer`, `ThreeCanvas`, schemas | R3F implementations and renderer |
## Usage Pattern
Pick the 3D components you need from the standard definitions:
```typescript
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { threeComponentDefinitions } from "@json-render/react-three-fiber/catalog";
import { defineRegistry } from "@json-render/react";
import { threeComponents, ThreeCanvas } from "@json-render/react-three-fiber";
// Catalog: pick definitions
const catalog = defineCatalog(schema, {
components: {
Box: threeComponentDefinitions.Box,
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
OrbitControls: threeComponentDefinitions.OrbitControls,
},
actions: {},
});
// Registry: pick matching implementations
const { registry } = defineRegistry(catalog, {
components: {
Box: threeComponents.Box,
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
OrbitControls: threeComponents.OrbitControls,
},
});
```
## Rendering
### ThreeCanvas (convenience wrapper)
```tsx
<ThreeCanvas
spec={spec}
registry={registry}
shadows
camera={{ position: [5, 5, 5], fov: 50 }}
style={{ width: "100%", height: "100vh" }}
/>
```
### Manual Canvas setup
```tsx
import { Canvas } from "@react-three/fiber";
import { ThreeRenderer } from "@json-render/react-three-fiber";
<Canvas shadows>
<ThreeRenderer spec={spec} registry={registry}>
{/* Additional R3F elements */}
</ThreeRenderer>
</Canvas>
```
## Available Components (19)
### Primitives (7)
- `Box` -- width, height, depth, material
- `Sphere` -- radius, widthSegments, heightSegments, material
- `Cylinder` -- radiusTop, radiusBottom, height, material
- `Cone` -- radius, height, material
- `Torus` -- radius, tube, material
- `Plane` -- width, height, material
- `Capsule` -- radius, length, material
All primitives share: `position`, `rotation`, `scale`, `castShadow`, `receiveShadow`, `material`.
### Lights (4)
- `AmbientLight` -- color, intensity
- `DirectionalLight` -- position, color, intensity, castShadow
- `PointLight` -- position, color, intensity, distance, decay
- `SpotLight` -- position, color, intensity, angle, penumbra
### Other (8)
- `Group` -- container with position/rotation/scale, supports children
- `Model` -- GLTF/GLB loader via url prop
- `Environment` -- HDRI environment map (preset, background, blur, intensity)
- `Fog` -- linear fog (color, near, far)
- `GridHelper` -- reference grid (size, divisions, color)
- `Text3D` -- SDF text (text, fontSize, color, anchorX, anchorY)
- `PerspectiveCamera` -- camera (position, fov, near, far, makeDefault)
- `OrbitControls` -- orbit controls (enableDamping, enableZoom, autoRotate)
## Shared Schemas
Reusable Zod schemas for custom 3D catalog definitions:
```typescript
import { vector3Schema, materialSchema, transformProps, shadowProps } from "@json-render/react-three-fiber";
import { z } from "zod";
// Custom 3D component
const myComponentDef = {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
myCustomProp: z.string(),
}),
description: "My custom 3D component",
};
```
## Material Schema
```typescript
materialSchema = z.object({
color: z.string().nullable(), // default "#ffffff"
metalness: z.number().nullable(), // default 0
roughness: z.number().nullable(), // default 1
emissive: z.string().nullable(), // default "#000000"
emissiveIntensity: z.number().nullable(), // default 1
opacity: z.number().nullable(), // default 1
transparent: z.boolean().nullable(), // default false
wireframe: z.boolean().nullable(), // default false
});
```
## Spec Format
3D specs use the standard json-render flat element format:
```json
{
"root": "scene",
"elements": {
"scene": {
"type": "Group",
"props": { "position": [0, 0, 0] },
"children": ["light", "box"]
},
"light": {
"type": "AmbientLight",
"props": { "intensity": 0.5 },
"children": []
},
"box": {
"type": "Box",
"props": {
"position": [0, 0.5, 0],
"material": { "color": "#4488ff", "metalness": 0.3, "roughness": 0.7 }
},
"children": []
}
}
}
```
## Dependencies
Peer dependencies required:
- `@react-three/fiber` >= 8.0.0
- `@react-three/drei` >= 9.0.0
- `three` >= 0.160.0
- `react` ^19.0.0
- `zod` ^4.0.0