Compare commits

..
Author SHA1 Message Date
Chris Tate e61a17659f fix lint 2026-03-19 01:51:57 -05:00
Chris Tate ad6237dce2 rate limits 2026-03-19 01:46:39 -05:00
Chris Tate f52f1ff070 r3f mobile 2026-03-19 01:32:08 -05:00
Chris Tate 7b096346cc og's 2026-03-19 01:19:46 -05:00
Chris Tate bc3b7d7e92 live demos 2026-03-19 01:07:14 -05:00
Chris Tate a2682c4c09 add examples 2026-03-17 10:22:26 -05:00
github-actions[bot] 22545b49cc chore: version packages (#221)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-13 17:05:54 -05:00
Chris Tate a8afd8bfe2 prepare v0.14 (#220) 2026-03-13 13:50:56 -05:00
Chris Tate dc489601be add yaml wire format and universal edit modes (#219)
* add yaml wire format and universal edit modes

**Summary**

- Add `@json-render/yaml` package with streaming parser, YAML prompt generation, and AI SDK transform
- Add universal edit modes (patch, merge, diff) to `@json-render/core`, usable across both JSONL and YAML formats
- Integrate YAML support and edit mode selection into the web playground with format toggle, token usage display, and prompt caching

* improvements

* fixes

* fixes ci

* docs
2026-03-13 13:37:21 -05:00
Chris Tate 7758d4bbd5 faster ci (#212)
* faster ci

* fix workflow
2026-03-12 22:47:02 -05:00
github-actions[bot] 6225fc41eb chore: version packages (#211)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-12 10:20:48 -05:00
Chris Tate 5b32de8720 prepare v0.13 (#210) 2026-03-12 10:18:18 -05:00
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
197 changed files with 16607 additions and 355 deletions
+3 -1
View File
@@ -20,7 +20,9 @@
"@json-render/image",
"@json-render/mcp",
"@json-render/svelte",
"@json-render/solid"
"@json-render/solid",
"@json-render/react-three-fiber",
"@json-render/yaml"
]
],
"linked": [],
+34 -25
View File
@@ -13,34 +13,43 @@ concurrency:
cancel-in-progress: true
jobs:
ci:
name: Lint, Type Check & Build
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- name: Install dependencies
run: pnpm install --frozen-lockfile
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm turbo run build --filter='./packages/*'
- run: pnpm test
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm type-check
- name: Test
run: pnpm test
- name: Build
run: pnpm build
typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm type-check
+45
View File
@@ -23,6 +23,8 @@ npm install @json-render/core @json-render/vue
npm install @json-render/core @json-render/svelte
# or for SolidJS
npm install @json-render/core @json-render/solid
# or for 3D scenes
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
```
## Why json-render?
@@ -121,6 +123,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 |
@@ -132,6 +135,7 @@ function Dashboard({ spec }) {
| `@json-render/jotai` | Jotai adapter for `StateStore` |
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code |
| `@json-render/yaml` | YAML wire format with streaming parser, edit modes, AI SDK transform |
## Renderers
@@ -434,6 +438,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)
+19
View File
@@ -1,5 +1,24 @@
# web
## 0.1.8
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/yaml@0.14.0
- @json-render/codegen@0.14.0
- @json-render/react@0.14.0
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/codegen@0.13.0
- @json-render/react@0.13.0
## 0.1.6
### Patch Changes
+93 -3
View File
@@ -76,6 +76,7 @@ interface PromptOptions {
system?: string; // Custom system message intro
customRules?: string[]; // Additional rules to append
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
editModes?: EditMode[]; // Edit modes to document in prompt (default: ["patch"])
}
interface SpecValidationResult<T> {
@@ -480,9 +481,10 @@ function buildUserPrompt(options: UserPromptOptions): string
interface UserPromptOptions {
prompt: string; // The user's text prompt
currentSpec?: Spec | null; // Existing spec to refine (triggers patch-only mode)
currentSpec?: Spec | null; // Existing spec to refine (triggers edit mode)
state?: Record<string, unknown> | null; // Runtime state context to include
maxPromptLength?: number; // Max length for user text (truncates before wrapping)
editModes?: EditMode[]; // Edit modes for refinement (default: ["patch"])
}
```
@@ -492,14 +494,15 @@ interface UserPromptOptions {
const userPrompt = buildUserPrompt({ prompt: "create a todo app" });
```
### Refinement (patch-only mode)
### Refinement (edit modes)
When `currentSpec` is provided, the prompt instructs the AI to output only the patches needed for the change, not recreate the entire spec:
When `currentSpec` is provided, the prompt instructs the AI to use the specified edit modes instead of recreating the entire spec. Available modes: `"patch"` (RFC 6902), `"merge"` (RFC 7396), and `"diff"` (unified diff).
```typescript
const userPrompt = buildUserPrompt({
prompt: "add a dark mode toggle",
currentSpec: existingSpec,
editModes: ["patch", "merge"],
});
```
@@ -514,6 +517,93 @@ const userPrompt = buildUserPrompt({
});
```
## Edit Modes
Universal edit mode utilities for modifying existing specs. Used by `buildUserPrompt` internally and available for direct use.
```typescript
import {
buildEditInstructions,
buildEditUserPrompt,
isNonEmptySpec,
type EditMode,
type EditConfig,
} from '@json-render/core';
type EditMode = "patch" | "merge" | "diff";
```
### buildEditInstructions
Generate the prompt section describing available edit modes. Supports both JSON and YAML formats.
```typescript
function buildEditInstructions(config: EditConfig, format: "json" | "yaml"): string
const instructions = buildEditInstructions({ modes: ["patch", "merge"] }, "json");
```
### buildEditUserPrompt
Build a user prompt for editing an existing spec. Includes the current spec (with line numbers when diff mode is enabled) and mode-specific instructions.
```typescript
function buildEditUserPrompt(options: BuildEditUserPromptOptions): string
interface BuildEditUserPromptOptions {
prompt: string;
currentSpec?: Spec | null;
config?: EditConfig;
format: "json" | "yaml";
maxPromptLength?: number;
serializer?: (spec: Spec) => string;
}
```
### isNonEmptySpec
Check whether a value is a non-empty spec (has a root string and at least one element).
```typescript
function isNonEmptySpec(spec: unknown): spec is Spec
```
## Deep Merge and Diff
Format-agnostic utilities for merging and diffing spec objects.
### deepMergeSpec
Deep-merge with RFC 7396 semantics: `null` deletes, arrays replace, objects recurse. Neither input is mutated.
```typescript
import { deepMergeSpec } from '@json-render/core';
function deepMergeSpec(
base: Record<string, unknown>,
patch: Record<string, unknown>
): Record<string, unknown>
const merged = deepMergeSpec(currentSpec, { elements: { main: { props: { title: "New" } } } });
```
### diffToPatches
Generate RFC 6902 JSON Patch operations that transform one object into another. Arrays are compared shallowly and replaced atomically; plain objects recurse.
```typescript
import { diffToPatches } from '@json-render/core';
function diffToPatches(
oldObj: Record<string, unknown>,
newObj: Record<string, unknown>,
basePath?: string
): JsonPatch[]
const patches = diffToPatches(oldSpec, newSpec);
// [{ op: "replace", path: "/elements/main/props/title", value: "New Title" }]
```
## evaluateVisibility
Evaluates a visibility condition against the state model.
@@ -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">;
```
+232
View File
@@ -0,0 +1,232 @@
import { pageMetadata } from "@/lib/page-metadata"
export const metadata = pageMetadata("docs/api/yaml")
# @json-render/yaml
YAML wire format for json-render. Progressive rendering and surgical edits via streaming YAML.
## Prompt Generation
### yamlPrompt
Generate a YAML-format system prompt from any json-render catalog. Works with catalogs from any renderer.
```typescript
function yamlPrompt(
catalog: Catalog,
options?: YamlPromptOptions
): string
```
```typescript
import { yamlPrompt } from "@json-render/yaml";
const systemPrompt = yamlPrompt(catalog, {
mode: "standalone",
customRules: ["Always use dark theme"],
editModes: ["merge"],
});
```
### YamlPromptOptions
<table>
<thead>
<tr>
<th>Option</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>system</code></td>
<td><code>string</code></td>
<td><code>{'\"You are a UI generator that outputs YAML.\"'}</code></td>
<td>Custom system message intro</td>
</tr>
<tr>
<td><code>mode</code></td>
<td><code>{'\"standalone\" | \"inline\"'}</code></td>
<td><code>{'\"standalone\"'}</code></td>
<td>Standalone outputs only YAML; inline allows conversational responses with embedded YAML fences</td>
</tr>
<tr>
<td><code>customRules</code></td>
<td><code>{'string[]'}</code></td>
<td><code>{'[]'}</code></td>
<td>Additional rules appended to the prompt</td>
</tr>
<tr>
<td><code>editModes</code></td>
<td><code>{'EditMode[]'}</code></td>
<td><code>{'[\"merge\"]'}</code></td>
<td>Edit modes to document in the prompt (patch, merge, diff)</td>
</tr>
</tbody>
</table>
## AI SDK Transform
### createYamlTransform
Creates a `TransformStream` that intercepts AI SDK stream chunks and converts YAML spec/edit blocks into json-render patch data parts.
```typescript
function createYamlTransform(
options?: YamlTransformOptions
): TransformStream<StreamChunk, StreamChunk>
```
Recognized fence types:
- <code>{'```yaml-spec'}</code> -- Full YAML spec, parsed progressively
- <code>{'```yaml-edit'}</code> -- Partial YAML, deep-merged with current spec
- <code>{'```yaml-patch'}</code> -- RFC 6902 JSON Patch lines
- <code>{'```diff'}</code> -- Unified diff against serialized spec
### pipeYamlRender
Convenience wrapper that pipes an AI SDK stream through the YAML transform. Drop-in replacement for `pipeJsonRender` from `@json-render/core`.
```typescript
function pipeYamlRender<T>(
stream: ReadableStream<T>,
options?: YamlTransformOptions
): ReadableStream<T>
```
```typescript
import { pipeYamlRender } from "@json-render/yaml";
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(pipeYamlRender(result.toUIMessageStream()));
},
});
return createUIMessageStreamResponse({ stream });
```
## Streaming Parser
### createYamlStreamCompiler
Create a streaming YAML compiler that incrementally parses YAML text and emits JSON Patch operations by diffing each successful parse against the previous snapshot.
```typescript
function createYamlStreamCompiler<T>(
initial?: Partial<T>
): YamlStreamCompiler<T>
```
```typescript
import { createYamlStreamCompiler } from "@json-render/yaml";
const compiler = createYamlStreamCompiler<Spec>();
compiler.push("root: main\n");
compiler.push("elements:\n main:\n type: Card\n");
const { result, newPatches } = compiler.flush();
```
### YamlStreamCompiler
<table>
<thead>
<tr>
<th>Method</th>
<th>Returns</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>push(chunk)</code></td>
<td><code>{'{ result: T; newPatches: JsonPatch[] }'}</code></td>
<td>Push a chunk of text, returns current result and new patches</td>
</tr>
<tr>
<td><code>flush()</code></td>
<td><code>{'{ result: T; newPatches: JsonPatch[] }'}</code></td>
<td>Flush remaining buffer, return final result</td>
</tr>
<tr>
<td><code>getResult()</code></td>
<td><code>T</code></td>
<td>Get the current compiled result</td>
</tr>
<tr>
<td><code>getPatches()</code></td>
<td><code>{'JsonPatch[]'}</code></td>
<td>Get all patches applied so far</td>
</tr>
<tr>
<td><code>reset(initial?)</code></td>
<td><code>void</code></td>
<td>Reset to initial state</td>
</tr>
</tbody>
</table>
## Fence Constants
Exported string constants for fence detection in custom parsers:
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>YAML_SPEC_FENCE</code></td>
<td><code>{'\"```yaml-spec\"'}</code></td>
</tr>
<tr>
<td><code>YAML_EDIT_FENCE</code></td>
<td><code>{'\"```yaml-edit\"'}</code></td>
</tr>
<tr>
<td><code>YAML_PATCH_FENCE</code></td>
<td><code>{'\"```yaml-patch\"'}</code></td>
</tr>
<tr>
<td><code>DIFF_FENCE</code></td>
<td><code>{'\"```diff\"'}</code></td>
</tr>
<tr>
<td><code>FENCE_CLOSE</code></td>
<td><code>{'\"```\"'}</code></td>
</tr>
</tbody>
</table>
## Re-exports from @json-render/core
### diffToPatches
Generate RFC 6902 JSON Patch operations that transform one object into another.
```typescript
function diffToPatches(
oldObj: Record<string, unknown>,
newObj: Record<string, unknown>,
basePath?: string
): JsonPatch[]
```
### deepMergeSpec
Deep-merge with RFC 7396 semantics: `null` deletes, arrays replace, objects recurse.
```typescript
function deepMergeSpec(
base: Record<string, unknown>,
patch: Record<string, unknown>
): Record<string, unknown>
```
+7 -1
View File
@@ -1,6 +1,7 @@
import { DocsMobileNav } from "@/components/docs-mobile-nav";
import { DocsSidebar } from "@/components/docs-sidebar";
import { CopyPageButton } from "@/components/copy-page-button";
import { TableOfContents } from "@/components/table-of-contents";
export default function DocsLayout({
children,
@@ -10,7 +11,7 @@ export default function DocsLayout({
return (
<>
<DocsMobileNav />
<div className="max-w-5xl mx-auto px-6 py-8 lg:py-12 flex gap-16">
<div className="max-w-7xl mx-auto px-6 py-8 lg:py-12 flex gap-12">
{/* Sidebar */}
<aside className="w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
<DocsSidebar />
@@ -23,6 +24,11 @@ export default function DocsLayout({
</div>
<article>{children}</article>
</div>
{/* On this page */}
<aside className="w-44 shrink-0 hidden xl:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
<TableOfContents />
</aside>
</div>
</>
);
+133
View File
@@ -0,0 +1,133 @@
"use client";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { examples, allTags, getGitHubUrl, type Example } from "@/lib/examples";
import { cn } from "@/lib/utils";
function ExampleCard({ example }: { example: Example }) {
return (
<div className="group flex flex-col rounded-xl border border-border bg-card text-card-foreground overflow-hidden transition-colors hover:border-foreground/25">
<div className="flex flex-1 flex-col gap-3 p-5">
<h3 className="font-semibold leading-none">{example.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{example.description}
</p>
<div className="flex flex-wrap gap-1.5">
{example.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-[11px]">
{tag}
</Badge>
))}
</div>
<div className="mt-auto flex items-center gap-3 pt-2">
{example.demoUrl && (
<a
href={example.demoUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-sm text-foreground hover:text-primary transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 3h6v6" />
<path d="M10 14 21 3" />
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
</svg>
Live Demo
</a>
)}
<a
href={getGitHubUrl(example)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<svg
viewBox="0 0 16 16"
className="h-3.5 w-3.5"
fill="currentColor"
aria-hidden="true"
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
Source
</a>
</div>
</div>
</div>
);
}
export default function ExamplesPage() {
const [activeTag, setActiveTag] = useState<string | null>(null);
const filtered = activeTag
? examples.filter((e) => e.tags.includes(activeTag))
: examples;
return (
<section className="mx-auto max-w-6xl px-6 py-16">
<div className="mb-10">
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
Examples
</h1>
<p className="mt-3 text-lg text-muted-foreground">
Explore json-render across frameworks, renderers, and use cases.
</p>
</div>
<div className="mb-8 flex flex-wrap gap-2">
<button
onClick={() => setActiveTag(null)}
className={cn(
"rounded-full border px-3 py-1 text-xs font-medium transition-colors",
activeTag === null
? "border-foreground bg-foreground text-background"
: "border-border text-muted-foreground hover:text-foreground hover:border-foreground/50",
)}
>
All
</button>
{allTags.map((tag) => (
<button
key={tag}
onClick={() => setActiveTag(activeTag === tag ? null : tag)}
className={cn(
"rounded-full border px-3 py-1 text-xs font-medium transition-colors",
activeTag === tag
? "border-foreground bg-foreground text-background"
: "border-border text-muted-foreground hover:text-foreground hover:border-foreground/50",
)}
>
{tag}
</button>
))}
</div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{filtered.map((example) => (
<ExampleCard key={example.slug} example={example} />
))}
</div>
{filtered.length === 0 && (
<p className="py-12 text-center text-muted-foreground">
No examples match the selected filter.
</p>
)}
</section>
);
}
+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, @json-render/yaml
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, yaml. 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.
+78 -24
View File
@@ -1,32 +1,73 @@
import { streamText } from "ai";
import { headers } from "next/headers";
import { buildUserPrompt } from "@json-render/core";
import type { Spec, EditMode } from "@json-render/core";
import {
buildUserPrompt,
buildEditUserPrompt,
isNonEmptySpec,
} from "@json-render/core";
import { yamlPrompt } from "@json-render/yaml";
import { stringify as yamlStringify } from "yaml";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
import { playgroundCatalog } from "@/lib/render/catalog";
export const maxDuration = 30;
const SYSTEM_PROMPT = playgroundCatalog.prompt({
customRules: [
"NEVER use viewport height classes (min-h-screen, h-screen) - the UI renders inside a fixed-size container.",
"NEVER use page background colors (bg-gray-50) - the container has its own background.",
"For forms or small UIs: use Card as root with maxWidth:'sm' or 'md' and centered:true.",
"For content-heavy UIs (blogs, dashboards, product listings): use Stack or Grid as root. Use Grid with 2-3 columns for card layouts.",
"Wrap each repeated item in a Card for visual separation and structure.",
"Use realistic, professional sample data. Include 3-5 items with varied content. Never leave state arrays empty.",
'For form inputs (Input, Textarea, Select), always include checks for validation (e.g. required, email, minLength). Always pair checks with a $bindState expression on the value prop (e.g. { "$bindState": "/path" }).',
],
});
const PLAYGROUND_RULES = [
"NEVER use viewport height classes (min-h-screen, h-screen) - the UI renders inside a fixed-size container.",
"NEVER use page background colors (bg-gray-50) - the container has its own background.",
"For forms or small UIs: use Card as root with maxWidth:'sm' or 'md' and centered:true.",
"For content-heavy UIs (blogs, dashboards, product listings): use Stack or Grid as root. Use Grid with 2-3 columns for card layouts.",
"Wrap each repeated item in a Card for visual separation and structure.",
"Use realistic, professional sample data. Include 3-5 items with varied content. Never leave state arrays empty.",
'For form inputs (Input, Textarea, Select), always include checks for validation (e.g. required, email, minLength). Always pair checks with a $bindState expression on the value prop (e.g. { "$bindState": "/path" }).',
];
const MAX_PROMPT_LENGTH = 500;
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
function getSystemPrompt(isYaml: boolean, editModes?: EditMode[]): string {
if (isYaml) {
return yamlPrompt(playgroundCatalog, {
mode: "standalone",
customRules: PLAYGROUND_RULES,
editModes: editModes ?? ["merge"],
});
}
return playgroundCatalog.prompt({
customRules: PLAYGROUND_RULES,
editModes,
});
}
function buildYamlUserPrompt(
prompt: string,
previousSpec?: Spec | null,
editModes?: EditMode[],
): string {
if (isNonEmptySpec(previousSpec)) {
return buildEditUserPrompt({
prompt,
currentSpec: previousSpec,
config: { modes: editModes ?? ["merge"] },
format: "yaml",
maxPromptLength: MAX_PROMPT_LENGTH,
serializer: (s) => yamlStringify(s, { indent: 2 }).trimEnd(),
});
}
const userText = prompt.slice(0, MAX_PROMPT_LENGTH);
return [
userText,
"",
"Output the full spec in a ```yaml-spec fence. Stream progressively — output elements one at a time.",
].join("\n");
}
export async function POST(req: Request) {
// Get client IP for rate limiting
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
// Check rate limits (minute and daily)
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
@@ -48,22 +89,34 @@ export async function POST(req: Request) {
);
}
const { prompt, context } = await req.json();
const { prompt, context, format, editModes } = await req.json();
const isYaml = format === "yaml";
const userPrompt = buildUserPrompt({
prompt,
currentSpec: context?.previousSpec,
maxPromptLength: MAX_PROMPT_LENGTH,
});
const systemPrompt = getSystemPrompt(isYaml, editModes);
const userPrompt = isYaml
? buildYamlUserPrompt(prompt, context?.previousSpec, editModes)
: buildUserPrompt({
prompt,
currentSpec: context?.previousSpec,
maxPromptLength: MAX_PROMPT_LENGTH,
editModes,
});
const result = streamText({
model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
system: SYSTEM_PROMPT,
system: [
{
role: "system",
content: systemPrompt,
providerOptions: {
anthropic: { cacheControl: { type: "ephemeral" } },
},
},
],
prompt: userPrompt,
temperature: 0.7,
});
// Stream the text, then append token usage metadata at the end
const encoder = new TextEncoder();
const textStream = result.textStream;
@@ -72,7 +125,6 @@ export async function POST(req: Request) {
for await (const chunk of textStream) {
controller.enqueue(encoder.encode(chunk));
}
// Append usage metadata after stream completes
try {
const usage = await result.usage;
const meta = JSON.stringify({
@@ -80,10 +132,12 @@ export async function POST(req: Request) {
promptTokens: usage.inputTokens,
completionTokens: usage.outputTokens,
totalTokens: usage.totalTokens,
cachedTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
cacheWriteTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
});
controller.enqueue(encoder.encode(`\n${meta}\n`));
} catch {
// Usage not available — skip silently
// Usage not available
}
controller.close();
},
+74
View File
@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from "next/server";
import { getSearchIndex } from "@/lib/search-index";
export async function GET(req: NextRequest) {
const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
if (!q) {
return NextResponse.json({ results: [] });
}
const index = await getSearchIndex();
const terms = q.split(/\s+/).filter(Boolean);
const results = index
.map((entry) => {
const titleLower = entry.title.toLowerCase();
const contentLower = entry.content.toLowerCase();
const titleMatch = terms.every((t) => titleLower.includes(t));
const contentMatch = terms.every((t) => contentLower.includes(t));
if (!titleMatch && !contentMatch) return null;
let snippet = "";
if (contentMatch) {
const firstTermIdx = Math.min(
...terms.map((t) => {
const idx = contentLower.indexOf(t);
return idx === -1 ? Infinity : idx;
}),
);
if (firstTermIdx !== Infinity) {
const start = Math.max(0, firstTermIdx - 40);
const end = Math.min(entry.content.length, firstTermIdx + 120);
snippet =
(start > 0 ? "..." : "") +
entry.content.slice(start, end).replace(/\n/g, " ") +
(end < entry.content.length ? "..." : "");
}
}
return {
title: entry.title,
href: entry.href,
section: entry.section,
snippet,
score: titleMatch ? 2 : 1,
};
})
.filter(
(
r,
): r is {
title: string;
href: string;
section: string;
snippet: string;
score: number;
} => r !== null,
)
.sort((a, b) => b.score - a.score)
.slice(0, 20)
.map(({ title, href, section, snippet }) => ({
title,
href,
section,
snippet,
}));
return NextResponse.json(
{ results },
{ headers: { "Cache-Control": "public, max-age=60" } },
);
}
+2 -2
View File
@@ -145,7 +145,7 @@ function getHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: [vercelLightTheme, vercelDarkTheme],
langs: ["json", "tsx", "typescript"],
langs: ["json", "tsx", "typescript", "yaml"],
});
}
return highlighterPromise;
@@ -158,7 +158,7 @@ if (typeof window !== "undefined") {
interface CodeBlockProps {
code: string;
lang: "json" | "tsx" | "typescript";
lang: "json" | "tsx" | "typescript" | "yaml";
fillHeight?: boolean;
hideCopyButton?: boolean;
}
+4 -4
View File
@@ -261,10 +261,10 @@ export function DocsChat({
}
}, [messages, isLoading]);
// Cmd+K to open sidebar and focus prompt, Escape to close
// Cmd+I to open sidebar and focus prompt, Escape to close
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
if (e.key === "i" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
setOpen((prev) => {
if (!prev) {
@@ -497,12 +497,12 @@ export function DocsChat({
{!open && (
<button
onClick={() => setOpen(true)}
className="fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg border bg-background text-primary shadow-lg hover:bg-primary hover:text-primary-foreground transition-colors text-sm font-medium"
className="fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg border border-primary bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors text-sm font-medium"
aria-label="Ask AI"
>
Ask AI
<kbd className="hidden sm:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono">
<span>&#8984;</span>K
<span>&#8984;</span>I
</kbd>
</button>
)}
+114 -39
View File
@@ -1,17 +1,59 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ThemeToggle } from "./theme-toggle";
import { Search } from "./search";
import {
Sheet,
SheetTrigger,
SheetContent,
SheetTitle,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
const navLinks = [
{ href: "/playground", label: "Playground" },
{ href: "/examples", label: "Examples" },
{ href: "/docs", label: "Docs" },
];
function GitHubLink({ className }: { className?: string }) {
return (
<a
href="https://github.com/vercel-labs/json-render"
target="_blank"
rel="noopener noreferrer"
className={cn(
"flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors",
className,
)}
>
<svg
viewBox="0 0 16 16"
className="h-4 w-4"
fill="currentColor"
aria-hidden="true"
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
<span>12k</span>
</a>
);
}
export function Header() {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const isActive = (href: string) => {
if (href === "/playground") {
return pathname === "/playground";
}
if (href === "/examples") {
return pathname.startsWith("/examples");
}
if (href === "/docs") {
return pathname.startsWith("/docs");
}
@@ -62,48 +104,81 @@ export function Header() {
</span>
</Link>
</div>
<nav className="flex items-center gap-4">
<Link
href="/playground"
className={cn(
"text-sm transition-colors",
isActive("/playground")
? "text-primary font-medium"
: "text-muted-foreground hover:text-foreground",
)}
>
<span className="sm:hidden">Play</span>
<span className="hidden sm:inline">Playground</span>
</Link>
<Link
href="/docs"
className={cn(
"text-sm transition-colors",
isActive("/docs")
? "text-primary font-medium"
: "text-muted-foreground hover:text-foreground",
)}
>
Docs
</Link>
<a
href="https://github.com/vercel-labs/json-render"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<svg
viewBox="0 0 16 16"
className="h-4 w-4"
fill="currentColor"
aria-hidden="true"
{/* Desktop nav */}
<nav className="hidden sm:flex items-center gap-4">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className={cn(
"text-sm transition-colors",
isActive(link.href)
? "text-primary"
: "text-muted-foreground hover:text-foreground",
)}
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
<span>11k</span>
</a>
{link.label}
</Link>
))}
<Search />
<GitHubLink />
<ThemeToggle />
</nav>
{/* Mobile nav */}
<div className="flex sm:hidden items-center gap-3">
<Search />
<GitHubLink />
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetTrigger
className="flex items-center justify-center"
aria-label="Open menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-muted-foreground"
>
<line x1="4" x2="20" y1="12" y2="12" />
<line x1="4" x2="20" y1="6" y2="6" />
<line x1="4" x2="20" y1="18" y2="18" />
</svg>
</SheetTrigger>
<SheetContent side="right" className="overflow-y-auto p-6">
<SheetTitle className="mb-6">Menu</SheetTitle>
<nav className="flex flex-col gap-1">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setMobileOpen(false)}
className={cn(
"block py-2.5 text-sm transition-colors",
isActive(link.href)
? "text-primary"
: "text-muted-foreground hover:text-foreground",
)}
>
{link.label}
</Link>
))}
<div className="my-3 border-t border-border" />
<div className="flex items-center justify-between py-2.5">
<span className="text-sm text-muted-foreground">Theme</span>
<ThemeToggle />
</div>
</nav>
</SheetContent>
</Sheet>
</div>
</div>
</header>
);
+174 -56
View File
@@ -2,10 +2,16 @@
import { useEffect, useState, useCallback, useRef, useMemo } from "react";
import { flushSync } from "react-dom";
import { useUIStream, type TokenUsage } from "@json-render/react";
import type { Spec } from "@json-render/core";
import { collectUsedComponents, serializeProps } from "@json-render/codegen";
import { toast } from "sonner";
import { stringify as yamlStringify } from "yaml";
import type { EditMode } from "@json-render/core";
import {
usePlaygroundStream,
type StreamFormat,
type TokenUsage,
} from "@/lib/use-playground-stream";
import {
ResizablePanelGroup,
ResizablePanel,
@@ -22,10 +28,10 @@ import { PlaygroundRenderer } from "@/lib/render/renderer";
import { playgroundCatalog } from "@/lib/render/catalog";
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
type Tab = "json" | "nested" | "stream" | "catalog" | "visual";
type Tab = "spec" | "nested" | "stream" | "catalog" | "visual";
type RenderView = "preview" | "code";
type MobileView =
| "json"
| "spec"
| "nested"
| "stream"
| "catalog"
@@ -40,6 +46,79 @@ interface Version {
status: "generating" | "complete" | "error";
usage: TokenUsage | null;
rawLines: string[];
format: StreamFormat;
}
function formatTokens(n: number): string {
if (n >= 1000) return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
return String(n);
}
function PlaygroundControls({
format,
setFormat,
editModes,
setEditModes,
showClear,
onClear,
}: {
format: StreamFormat;
setFormat: (f: StreamFormat) => void;
editModes: EditMode[];
setEditModes: React.Dispatch<React.SetStateAction<EditMode[]>>;
showClear: boolean;
onClear: () => void;
}) {
return (
<div className="flex items-center gap-2">
<div className="flex items-center rounded border border-border text-[10px] font-mono overflow-hidden">
{(["jsonl", "yaml"] as const).map((f) => (
<button
key={f}
onClick={() => setFormat(f)}
className={`px-1.5 py-0.5 transition-colors ${
format === f
? "bg-muted text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{f}
</button>
))}
</div>
<div className="flex items-center rounded border border-border text-[10px] font-mono overflow-hidden">
{(["patch", "merge", "diff"] as const).map((m) => (
<button
key={m}
onClick={() => {
setEditModes((prev) =>
prev.includes(m)
? prev.length > 1
? prev.filter((x) => x !== m)
: prev
: [...prev, m],
);
}}
className={`px-1.5 py-0.5 transition-colors ${
editModes.includes(m)
? "bg-muted text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{m}
</button>
))}
</div>
{showClear && (
<button
onClick={onClear}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Clear
</button>
)}
</div>
);
}
/**
@@ -100,13 +179,15 @@ export function Playground() {
null,
);
const [inputValue, setInputValue] = useState("");
const [activeTab, setActiveTab] = useState<Tab>("json");
const [activeTab, setActiveTab] = useState<Tab>("spec");
const [catalogSection, setCatalogSection] = useState<
"components" | "actions"
>("components");
const [renderView, setRenderView] = useState<RenderView>("preview");
const [mobileView, setMobileView] = useState<MobileView>("preview");
const [versionsSheetOpen, setVersionsSheetOpen] = useState(false);
const [format, setFormat] = useState<StreamFormat>("jsonl");
const [editModes, setEditModes] = useState<EditMode[]>(["patch"]);
const inputRef = useRef<HTMLTextAreaElement>(null);
const mobileInputRef = useRef<HTMLTextAreaElement>(null);
const versionsEndRef = useRef<HTMLDivElement>(null);
@@ -124,12 +205,13 @@ export function Playground() {
rawLines: streamRawLines,
send,
clear,
} = useUIStream({
} = usePlaygroundStream({
api: "/api/generate",
format,
editModes,
onError: (err: Error) => {
console.error("Generation error:", err);
toast.error(err.message || "Generation failed. Please try again.");
// Mark the version as errored
if (generatingVersionIdRef.current) {
const erroredVersionId = generatingVersionIdRef.current;
setVersions((prev) =>
@@ -140,7 +222,7 @@ export function Playground() {
generatingVersionIdRef.current = null;
}
},
} as Parameters<typeof useUIStream>[0]);
});
// Get the selected version
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
@@ -215,6 +297,7 @@ export function Playground() {
status: "generating",
usage: null,
rawLines: [],
format,
};
generatingVersionIdRef.current = newVersionId;
@@ -224,7 +307,7 @@ export function Playground() {
// Pass the current tree as context so the API can iterate on it
await send(inputValue.trim(), { previousSpec: currentTreeRef.current });
}, [inputValue, isStreaming, send]);
}, [inputValue, isStreaming, send, format]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -250,9 +333,16 @@ export function Playground() {
[selectedVersionId, isStreaming],
);
const jsonCode = currentTree
? JSON.stringify(currentTree, null, 2)
: "// waiting...";
const specCode = useMemo(() => {
if (!currentTree)
return format === "yaml" ? "# waiting..." : "// waiting...";
if (format === "yaml") {
return yamlStringify(currentTree, { indent: 2 }).trimEnd();
}
return JSON.stringify(currentTree, null, 2);
}, [currentTree, format]);
const specLang = format === "yaml" ? "yaml" : "json";
const nestedCode = useMemo(() => {
if (!currentTree || !currentTree.root) return "// waiting...";
@@ -275,7 +365,7 @@ export function Playground() {
const componentName = element.type;
const propsObj: Record<string, unknown> = {};
for (const [k, v] of Object.entries(element.props)) {
for (const [k, v] of Object.entries(element.props ?? {})) {
if (v !== null && v !== undefined) {
propsObj[k] = v;
}
@@ -321,6 +411,15 @@ ${jsx}
}`;
}, [currentTree]);
// Determine syntax lang for raw stream based on selected version's format
const streamLang = isSelectedVersionGenerating
? format === "yaml"
? "yaml"
: "json"
: selectedVersion?.format === "yaml"
? "yaml"
: "json";
// Chat pane content
const chatPane = (
<div className="h-full flex flex-col border-t border-border">
@@ -392,9 +491,15 @@ ${jsx}
)}
</div>
{version.usage && (
<div className="flex items-center gap-2 mt-1 ml-6">
<div className="mt-1 ml-6">
<span className="text-[10px] font-mono text-muted-foreground/60">
{version.usage.totalTokens.toLocaleString()} tokens
{formatTokens(
version.usage.promptTokens - version.usage.cachedTokens,
)}{" "}
in · {formatTokens(version.usage.completionTokens)} out
{version.usage.cachedTokens > 0
? ` · ${formatTokens(version.usage.cachedTokens)} cached`
: ""}
</span>
</div>
)}
@@ -425,20 +530,18 @@ ${jsx}
autoFocus
/>
<div className="flex justify-between items-center mt-2">
{versions.length > 0 ? (
<button
onClick={() => {
setVersions([]);
setSelectedVersionId(null);
clear();
}}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Clear
</button>
) : (
<div />
)}
<PlaygroundControls
format={format}
setFormat={setFormat}
editModes={editModes}
setEditModes={setEditModes}
showClear={versions.length > 0}
onClear={() => {
setVersions([]);
setSelectedVersionId(null);
clear();
}}
/>
{isStreaming ? (
<button
onClick={() => clear()}
@@ -492,18 +595,18 @@ ${jsx}
const copyText =
activeTab === "stream"
? currentRawLines.join("\n")
: activeTab === "json"
? jsonCode
: activeTab === "spec"
? specCode
: activeTab === "nested"
? nestedCode
: activeTab === "visual"
? jsonCode
? specCode
: "";
const codePane = (
<div className="h-full flex flex-col border-t border-border">
<div className="border-b border-border px-3 h-9 flex items-center gap-3">
{(["json", "visual", "nested", "stream", "catalog"] as const).map(
{(["spec", "visual", "nested", "stream", "catalog"] as const).map(
(tab) => (
<button
key={tab}
@@ -514,7 +617,7 @@ ${jsx}
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab}
{tab === "spec" ? (format === "yaml" ? "yaml" : "json") : tab}
</button>
),
)}
@@ -679,7 +782,7 @@ ${jsx}
currentRawLines.length > 0 ? (
<CodeBlock
code={currentRawLines.join("\n")}
lang="json"
lang={streamLang}
fillHeight
hideCopyButton
/>
@@ -691,7 +794,12 @@ ${jsx}
) : activeTab === "nested" ? (
<CodeBlock code={nestedCode} lang="json" fillHeight hideCopyButton />
) : (
<CodeBlock code={jsonCode} lang="json" fillHeight hideCopyButton />
<CodeBlock
code={specCode}
lang={specLang}
fillHeight
hideCopyButton
/>
)}
</div>
</div>
@@ -790,7 +898,7 @@ ${jsx}
: 0}
</button>
{/* Code tabs */}
{(["json", "visual", "nested", "stream", "catalog"] as const).map(
{(["spec", "visual", "nested", "stream", "catalog"] as const).map(
(tab) => (
<button
key={tab}
@@ -801,7 +909,7 @@ ${jsx}
: "text-muted-foreground hover:text-foreground"
}`}
>
{tab}
{tab === "spec" ? (format === "yaml" ? "yaml" : "json") : tab}
</button>
),
)}
@@ -982,7 +1090,7 @@ ${jsx}
currentRawLines.length > 0 ? (
<CodeBlock
code={currentRawLines.join("\n")}
lang="json"
lang={streamLang}
fillHeight
hideCopyButton
/>
@@ -998,8 +1106,13 @@ ${jsx}
fillHeight
hideCopyButton
/>
) : mobileView === "json" ? (
<CodeBlock code={jsonCode} lang="json" fillHeight hideCopyButton />
) : mobileView === "spec" ? (
<CodeBlock
code={specCode}
lang={specLang}
fillHeight
hideCopyButton
/>
) : mobileView === "preview" ? (
currentTree && currentTree.root ? (
<div className="w-full min-h-full flex items-center justify-center p-6">
@@ -1075,20 +1188,18 @@ ${jsx}
rows={2}
/>
<div className="flex justify-between items-center mt-2">
{versions.length > 0 ? (
<button
onClick={() => {
setVersions([]);
setSelectedVersionId(null);
clear();
}}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Clear
</button>
) : (
<div />
)}
<PlaygroundControls
format={format}
setFormat={setFormat}
editModes={editModes}
setEditModes={setEditModes}
showClear={versions.length > 0}
onClear={() => {
setVersions([]);
setSelectedVersionId(null);
clear();
}}
/>
{isStreaming ? (
<button
onClick={() => clear()}
@@ -1165,9 +1276,16 @@ ${jsx}
)}
</div>
{version.usage && (
<div className="flex items-center gap-2 mt-1 ml-6">
<div className="mt-1 ml-6">
<span className="text-[10px] font-mono text-muted-foreground/60">
{version.usage.totalTokens.toLocaleString()} tokens
{formatTokens(
version.usage.promptTokens -
version.usage.cachedTokens,
)}{" "}
in · {formatTokens(version.usage.completionTokens)} out
{version.usage.cachedTokens > 0
? ` · ${formatTokens(version.usage.cachedTokens)} cached`
: ""}
</span>
</div>
)}
+266
View File
@@ -0,0 +1,266 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
type SearchResult = {
title: string;
href: string;
section: string;
snippet: string;
};
export function Search() {
const router = useRouter();
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const abortRef = useRef<AbortController | null>(null);
const navigate = useCallback(
(href: string) => {
setOpen(false);
setQuery("");
setResults([]);
router.push(href);
},
[router],
);
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setOpen((prev) => !prev);
}
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, []);
useEffect(() => {
if (open) {
setTimeout(() => inputRef.current?.focus(), 0);
} else {
setQuery("");
setResults([]);
}
}, [open]);
useEffect(() => {
const q = query.trim();
if (!q) {
setResults([]);
setLoading(false);
return;
}
setLoading(true);
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
const timeout = setTimeout(async () => {
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: controller.signal,
});
if (res.ok) {
const data = await res.json();
setResults(data.results);
}
} catch {
// aborted or network error
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}, 150);
return () => {
clearTimeout(timeout);
controller.abort();
};
}, [query]);
useEffect(() => {
setActiveIndex(0);
}, [results]);
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === "ArrowDown") {
e.preventDefault();
setActiveIndex((i) => Math.min(i + 1, results.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActiveIndex((i) => Math.max(i - 1, 0));
} else if (e.key === "Enter" && results[activeIndex]) {
e.preventDefault();
navigate(results[activeIndex].href);
}
}
useEffect(() => {
const active = listRef.current?.querySelector("[data-active='true']");
active?.scrollIntoView({ block: "nearest" });
}, [activeIndex]);
const hasQuery = query.trim().length > 0;
return (
<>
<button
onClick={() => setOpen(true)}
className="hidden sm:flex items-center gap-2 rounded-md border border-border bg-muted/50 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:border-foreground/25 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
Search docs
<kbd className="pointer-events-none ml-1 inline-flex items-center gap-0.5 rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
<span>&#8984;</span>K
</kbd>
</button>
<button
onClick={() => setOpen(true)}
className="sm:hidden flex items-center text-muted-foreground hover:text-foreground transition-colors"
aria-label="Search docs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
showCloseButton={false}
className="gap-0 p-0 sm:max-w-lg"
>
<DialogTitle className="sr-only">Search documentation</DialogTitle>
<div className="flex items-center gap-2 border-b px-3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0 text-muted-foreground"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search docs..."
className="flex-1 bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
/>
{query && (
<button
onClick={() => setQuery("")}
className="text-muted-foreground hover:text-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
)}
</div>
<div
ref={listRef}
className="max-h-[min(60vh,400px)] overflow-y-auto p-2"
>
{loading && hasQuery ? (
<div className="flex items-center justify-center py-6">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
</div>
) : hasQuery && results.length === 0 ? (
<p className="py-6 text-center text-sm text-muted-foreground">
No results found.
</p>
) : !hasQuery ? (
<p className="py-6 text-center text-sm text-muted-foreground">
Type to search documentation...
</p>
) : (
results.map((item, i) => (
<button
key={item.href}
data-active={i === activeIndex}
onClick={() => navigate(item.href)}
onMouseEnter={() => setActiveIndex(i)}
className={cn(
"flex w-full flex-col gap-1 rounded-md px-3 py-2 text-left transition-colors",
i === activeIndex
? "bg-accent text-accent-foreground"
: "text-foreground",
)}
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium">{item.title}</span>
<span className="shrink-0 text-xs text-muted-foreground">
{item.section}
</span>
</div>
{item.snippet && (
<span className="line-clamp-2 text-xs text-muted-foreground leading-relaxed">
{item.snippet}
</span>
)}
</button>
))
)}
</div>
</DialogContent>
</Dialog>
</>
);
}
+83
View File
@@ -0,0 +1,83 @@
"use client";
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
type Heading = {
id: string;
text: string;
level: number;
};
function getHeadings(): Heading[] {
const article = document.querySelector("article");
if (!article) return [];
const elements = article.querySelectorAll("h2[id], h3[id]");
return Array.from(elements).map((el) => ({
id: el.id,
text: el.textContent?.replace(/#$/, "").trim() ?? "",
level: el.tagName === "H3" ? 3 : 2,
}));
}
export function TableOfContents() {
const pathname = usePathname();
const [headings, setHeadings] = useState<Heading[]>([]);
const [activeId, setActiveId] = useState<string>("");
useEffect(() => {
const timer = setTimeout(() => setHeadings(getHeadings()), 100);
return () => clearTimeout(timer);
}, [pathname]);
useEffect(() => {
if (headings.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
}
},
{ rootMargin: "0px 0px -75% 0px", threshold: 0.1 },
);
for (const h of headings) {
const el = document.getElementById(h.id);
if (el) observer.observe(el);
}
return () => observer.disconnect();
}, [headings]);
if (headings.length === 0) return null;
return (
<nav aria-label="On this page">
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
On this page
</h4>
<ul className="space-y-1">
{headings.map((h) => (
<li key={h.id}>
<a
href={`#${h.id}`}
className={cn(
"block text-xs leading-relaxed py-0.5 transition-colors",
h.level === 3 && "pl-3",
activeId === h.id
? "text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
{h.text}
</a>
</li>
))}
</ul>
</nav>
);
}
+6 -57
View File
@@ -45,63 +45,7 @@ export const docsNavigation: NavSection[] = [
},
{
title: "Examples",
items: [
{
title: "Chat",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/chat",
external: true,
},
{
title: "Dashboard",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/dashboard",
external: true,
},
{
title: "React Native",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-native",
external: true,
},
{
title: "React PDF",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-pdf",
external: true,
},
{
title: "React Email",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/react-email",
external: true,
},
{
title: "Remotion",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/remotion",
external: true,
},
{
title: "Image",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/image",
external: true,
},
{
title: "Svelte",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/svelte",
external: true,
},
{
title: "Vue",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue",
external: true,
},
{
title: "Renders with Vite (Vue / React / Svelte / Solid)",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
external: true,
},
{
title: "MCP App",
href: "https://github.com/vercel-labs/json-render/tree/main/examples/mcp",
external: true,
},
],
items: [{ title: "Browse All Examples", href: "/examples" }],
},
{
title: "Guides",
@@ -134,12 +78,17 @@ 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" },
{ title: "@json-render/zustand", href: "/docs/api/zustand" },
{ title: "@json-render/jotai", href: "/docs/api/jotai" },
{ title: "@json-render/xstate", href: "/docs/api/xstate" },
{ title: "@json-render/yaml", href: "/docs/api/yaml" },
],
},
];
+152
View File
@@ -0,0 +1,152 @@
export type Example = {
slug: string;
title: string;
description: string;
tags: string[];
githubPath: string;
demoUrl?: string;
};
const GITHUB_BASE =
"https://github.com/vercel-labs/json-render/tree/main/examples";
export const examples: Example[] = [
{
slug: "chat",
title: "Chat",
description:
"AI chat app with tool calling, streaming UI, and rich components powered by the AI SDK.",
tags: ["React", "Next.js", "AI"],
githubPath: "examples/chat",
demoUrl: "https://chat-demo.json-render.dev",
},
{
slug: "dashboard",
title: "Dashboard",
description:
"AI-generated dashboard with drag-and-drop, charts, and real-time data binding.",
tags: ["React", "Next.js", "AI"],
githubPath: "examples/dashboard",
demoUrl: "https://dashboard-demo.json-render.dev",
},
{
slug: "no-ai",
title: "No AI",
description:
"Static specs rendered without any AI — forms, cards, tables, and more from hardcoded JSON.",
tags: ["React", "Next.js"],
githubPath: "examples/no-ai",
demoUrl: "https://no-ai-demo.json-render.dev",
},
{
slug: "svelte",
title: "Svelte",
description:
"Svelte renderer demo with counter, todo list, and two-way data binding.",
tags: ["Svelte", "Vite"],
githubPath: "examples/svelte",
demoUrl: "https://svelte-demo.json-render.dev",
},
{
slug: "svelte-chat",
title: "Svelte Chat",
description: "AI chat app built with SvelteKit and the Svelte renderer.",
tags: ["Svelte", "SvelteKit", "AI"],
githubPath: "examples/svelte-chat",
demoUrl: "https://json-render-svelte-chat-demo.labs.vercel.dev",
},
{
slug: "vue",
title: "Vue",
description:
"Vue renderer demo with counter, todo list, and two-way data binding.",
tags: ["Vue", "Vite"],
githubPath: "examples/vue",
demoUrl: "https://vue-demo.json-render.dev",
},
{
slug: "solid",
title: "Solid",
description:
"Solid renderer demo with counter, todo list, and two-way data binding.",
tags: ["Solid", "Vite"],
githubPath: "examples/solid",
demoUrl: "https://solid-demo.json-render.dev",
},
{
slug: "vite-renderers",
title: "Multi-Framework Renderers",
description:
"Same spec rendered with React, Vue, Svelte, and Solid side by side — hot-swappable at runtime.",
tags: ["React", "Vue", "Svelte", "Solid", "Vite"],
githubPath: "examples/vite-renderers",
},
{
slug: "react-email",
title: "React Email",
description:
"Generate HTML and plain-text emails from json-render specs using React Email.",
tags: ["React", "Email"],
githubPath: "examples/react-email",
demoUrl: "https://react-email-demo.json-render.dev",
},
{
slug: "react-pdf",
title: "React PDF",
description:
"Generate PDF documents from json-render specs with @react-pdf/renderer.",
tags: ["React", "PDF"],
githubPath: "examples/react-pdf",
demoUrl: "https://react-pdf-demo.json-render.dev",
},
{
slug: "react-three-fiber",
title: "React Three Fiber",
description:
"3D scenes generated from json-render specs using Three.js and React Three Fiber.",
tags: ["React", "3D"],
githubPath: "examples/react-three-fiber",
demoUrl: "https://react-three-fiber-demo.json-render.dev",
},
{
slug: "react-native",
title: "React Native",
description:
"Mobile app rendering json-render specs with Expo and React Native.",
tags: ["React Native", "Expo"],
githubPath: "examples/react-native",
},
{
slug: "remotion",
title: "Remotion",
description: "Generate videos from json-render specs using Remotion.",
tags: ["React", "Video"],
githubPath: "examples/remotion",
demoUrl: "https://remotion-demo.json-render.dev",
},
{
slug: "image",
title: "Image",
description:
"Generate OG images and social cards from json-render specs using Satori.",
tags: ["React", "Image"],
githubPath: "examples/image",
demoUrl: "https://image-demo.json-render.dev",
},
{
slug: "mcp",
title: "MCP App",
description:
"MCP server that serves shadcn UIs to Claude, ChatGPT, Cursor, and VS Code.",
tags: ["React", "MCP", "Vite"],
githubPath: "examples/mcp",
},
];
export const allTags = Array.from(
new Set(examples.flatMap((e) => e.tags)),
).sort();
export function getGitHubUrl(example: Example): string {
return `${GITHUB_BASE}/${example.slug}`;
}
+3
View File
@@ -11,6 +11,7 @@ export const PAGE_TITLES: Record<string, string> = {
// Top-level
playground: "Playground",
examples: "Examples",
// Docs
docs: "Introduction",
@@ -56,7 +57,9 @@ 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",
"docs/api/yaml": "@json-render/yaml API",
};
/**
+76
View File
@@ -0,0 +1,76 @@
import { readFile } from "fs/promises";
import { join } from "path";
import { docsNavigation } from "./docs-navigation";
import { mdxToCleanMarkdown } from "./mdx-to-markdown";
export type IndexEntry = {
title: string;
href: string;
section: string;
content: string;
};
let cached: IndexEntry[] | null = null;
function stripMarkdown(md: string): string {
return (
md
// Remove fenced code blocks entirely
.replace(/```[\s\S]*?```/g, "")
// Remove inline code
.replace(/`[^`]+`/g, "")
// Remove markdown links, keep text
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
// Remove heading markers
.replace(/^#{1,6}\s+/gm, "")
// Remove bold/italic markers
.replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1")
// Remove HTML tags
.replace(/<[^>]+>/g, "")
// Collapse whitespace
.replace(/\n{3,}/g, "\n\n")
.trim()
);
}
function mdxFileForSlug(slug: string): string {
const docsRoot = join(process.cwd(), "app", "(main)", "docs");
if (slug === "/docs") {
return join(docsRoot, "page.mdx");
}
const rest = slug.replace(/^\/docs\/?/, "");
return join(docsRoot, ...rest.split("/"), "page.mdx");
}
export async function getSearchIndex(): Promise<IndexEntry[]> {
if (cached) return cached;
const entries: IndexEntry[] = [];
for (const section of docsNavigation) {
for (const item of section.items) {
if (item.external) continue;
try {
const raw = await readFile(mdxFileForSlug(item.href), "utf-8");
const md = mdxToCleanMarkdown(raw);
const content = stripMarkdown(md);
entries.push({
title: item.title,
href: item.href,
section: section.title,
content,
});
} catch {
entries.push({
title: item.title,
href: item.href,
section: section.title,
content: "",
});
}
}
}
cached = entries;
return entries;
}
+155
View File
@@ -0,0 +1,155 @@
import type { Spec, JsonPatch } from "@json-render/core";
import { setByPath, getByPath, removeByPath } from "@json-render/core";
export function setSpecValue(
newSpec: Spec,
path: string,
value: unknown,
): void {
if (path === "/root") {
newSpec.root = value as string;
return;
}
if (path === "/state") {
newSpec.state = value as Record<string, unknown>;
return;
}
if (path.startsWith("/state/")) {
if (!newSpec.state) newSpec.state = {};
setByPath(
newSpec.state as Record<string, unknown>,
path.slice("/state".length),
value,
);
return;
}
if (path.startsWith("/elements/")) {
const pathParts = path.slice("/elements/".length).split("/");
const elementKey = pathParts[0];
if (!elementKey) return;
if (pathParts.length === 1) {
if (value == null || typeof value !== "object") return;
const el = value as Record<string, unknown>;
newSpec.elements[elementKey] = {
...el,
type: typeof el.type === "string" ? el.type : "",
props: el.props != null && typeof el.props === "object" ? el.props : {},
children: Array.isArray(el.children) ? el.children : [],
} as Spec["elements"][string];
} else {
const element = newSpec.elements[elementKey];
if (element) {
const newElement = { ...element };
setByPath(
newElement as unknown as Record<string, unknown>,
"/" + pathParts.slice(1).join("/"),
value,
);
newSpec.elements[elementKey] = newElement;
}
}
}
}
export function removeSpecValue(newSpec: Spec, path: string): void {
if (path === "/state") {
delete newSpec.state;
return;
}
if (path.startsWith("/state/") && newSpec.state) {
removeByPath(
newSpec.state as Record<string, unknown>,
path.slice("/state".length),
);
return;
}
if (path.startsWith("/elements/")) {
const pathParts = path.slice("/elements/".length).split("/");
const elementKey = pathParts[0];
if (!elementKey) return;
if (pathParts.length === 1) {
delete newSpec.elements[elementKey];
} else {
const element = newSpec.elements[elementKey];
if (element) {
const newElement = { ...element };
removeByPath(
newElement as unknown as Record<string, unknown>,
"/" + pathParts.slice(1).join("/"),
);
newSpec.elements[elementKey] = newElement;
}
}
}
}
export function getSpecValue(spec: Spec, path: string): unknown {
if (path === "/root") return spec.root;
if (path === "/state") return spec.state;
if (path.startsWith("/state/") && spec.state) {
return getByPath(
spec.state as Record<string, unknown>,
path.slice("/state".length),
);
}
return getByPath(spec as unknown as Record<string, unknown>, path);
}
export function normalizeSpec(spec: Spec): void {
if (
spec.state === null ||
(spec.state !== undefined && typeof spec.state !== "object")
) {
spec.state = undefined;
}
for (const key of Object.keys(spec.elements)) {
const el = spec.elements[key];
if (!el || typeof el !== "object") {
delete spec.elements[key];
continue;
}
if (el.props == null || typeof el.props !== "object") {
spec.elements[key] = { ...el, props: {} } as Spec["elements"][string];
}
if (!Array.isArray(spec.elements[key]!.children)) {
spec.elements[key] = {
...spec.elements[key]!,
children: [],
} as Spec["elements"][string];
}
}
}
export function applySpecPatch(spec: Spec, patch: JsonPatch): Spec {
const newSpec = {
...spec,
elements: { ...spec.elements },
...(spec.state ? { state: { ...spec.state } } : {}),
};
switch (patch.op) {
case "add":
case "replace":
setSpecValue(newSpec, patch.path, patch.value);
break;
case "remove":
removeSpecValue(newSpec, patch.path);
break;
case "move":
if (patch.from) {
const moveValue = getSpecValue(newSpec, patch.from);
removeSpecValue(newSpec, patch.from);
setSpecValue(newSpec, patch.path, moveValue);
}
break;
case "copy":
if (patch.from) {
setSpecValue(newSpec, patch.path, getSpecValue(newSpec, patch.from));
}
break;
case "test":
break;
}
normalizeSpec(newSpec);
return newSpec;
}
+482
View File
@@ -0,0 +1,482 @@
"use client";
import { useState, useCallback, useRef, useEffect } from "react";
import type { Spec, JsonPatch, EditMode } from "@json-render/core";
import { deepMergeSpec, diffToPatches } from "@json-render/core";
import { parse as yamlParse, stringify as yamlStringify } from "yaml";
import { applyPatch as applyUnifiedDiff } from "diff";
import {
createYamlStreamCompiler,
YAML_SPEC_FENCE,
YAML_EDIT_FENCE,
YAML_PATCH_FENCE,
DIFF_FENCE,
FENCE_CLOSE,
} from "@json-render/yaml";
import { applySpecPatch } from "./spec-patch";
export type StreamFormat = "jsonl" | "yaml";
export interface TokenUsage {
promptTokens: number;
completionTokens: number;
totalTokens: number;
cachedTokens: number;
cacheWriteTokens: number;
}
export interface UsePlaygroundStreamOptions {
api: string;
format: StreamFormat;
editModes?: EditMode[];
onError?: (error: Error) => void;
onComplete?: (spec: Spec) => void;
}
export interface UsePlaygroundStreamReturn {
spec: Spec | null;
isStreaming: boolean;
error: Error | null;
usage: TokenUsage | null;
rawLines: string[];
send: (prompt: string, context?: Record<string, unknown>) => Promise<void>;
clear: () => void;
}
// ── JSONL helpers ──
type ParsedLine =
| { type: "patch"; patch: JsonPatch }
| { type: "usage"; usage: TokenUsage }
| { type: "json-edit"; mergeObj: Record<string, unknown> }
| null;
function parseLine(line: string): ParsedLine {
try {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("//")) return null;
const parsed = JSON.parse(trimmed);
if (parsed.__meta === "usage") {
return {
type: "usage",
usage: {
promptTokens: parsed.promptTokens ?? 0,
completionTokens: parsed.completionTokens ?? 0,
totalTokens: parsed.totalTokens ?? 0,
cachedTokens: parsed.cachedTokens ?? 0,
cacheWriteTokens: parsed.cacheWriteTokens ?? 0,
},
};
}
if (parsed.__json_edit === true) {
const mergeObj = { ...parsed };
delete mergeObj.__json_edit;
return { type: "json-edit", mergeObj };
}
return { type: "patch", patch: parsed as JsonPatch };
} catch {
return null;
}
}
type FenceState = "outside" | "yaml-spec" | "yaml-edit" | "yaml-patch" | "diff";
// ── Hook ──
export function usePlaygroundStream({
api,
format,
editModes,
onError,
onComplete,
}: UsePlaygroundStreamOptions): UsePlaygroundStreamReturn {
const [spec, setSpec] = useState<Spec | null>(null);
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [usage, setUsage] = useState<TokenUsage | null>(null);
const [rawLines, setRawLines] = useState<string[]>([]);
const rawLinesRef = useRef<string[]>([]);
const abortControllerRef = useRef<AbortController | null>(null);
const onCompleteRef = useRef(onComplete);
onCompleteRef.current = onComplete;
const onErrorRef = useRef(onError);
onErrorRef.current = onError;
const formatRef = useRef(format);
formatRef.current = format;
const editModesRef = useRef(editModes);
editModesRef.current = editModes;
const clear = useCallback(() => {
setSpec(null);
setError(null);
}, []);
const send = useCallback(
async (prompt: string, context?: Record<string, unknown>) => {
abortControllerRef.current = new AbortController();
setIsStreaming(true);
setError(null);
setUsage(null);
rawLinesRef.current = [];
setRawLines([]);
const previousSpec = context?.previousSpec as Spec | undefined;
let currentSpec: Spec =
previousSpec && previousSpec.root
? { ...previousSpec, elements: { ...previousSpec.elements } }
: { root: "", elements: {} };
setSpec(currentSpec);
try {
const response = await fetch(api, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt,
context,
format: formatRef.current,
editModes: editModesRef.current,
}),
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
let errorMessage = `HTTP error: ${response.status}`;
try {
const errorData = await response.json();
if (errorData.message) errorMessage = errorData.message;
else if (errorData.error) errorMessage = errorData.error;
} catch {
// use default
}
throw new Error(errorMessage);
}
const reader = response.body?.getReader();
if (!reader) throw new Error("No response body");
const decoder = new TextDecoder();
let buffer = "";
if (formatRef.current === "yaml") {
// ── YAML streaming ──
let fenceState: FenceState = "outside";
const compiler = createYamlStreamCompiler<Record<string, unknown>>();
let yamlEditAccumulated = "";
let yamlPatchAccumulated = "";
let diffAccumulated = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const trimmed = line.trim();
// Check for usage metadata (appended after stream)
if (trimmed.startsWith("{") && trimmed.includes('"__meta"')) {
try {
const parsed = JSON.parse(trimmed);
if (parsed.__meta === "usage") {
setUsage({
promptTokens: parsed.promptTokens ?? 0,
completionTokens: parsed.completionTokens ?? 0,
totalTokens: parsed.totalTokens ?? 0,
cachedTokens: parsed.cachedTokens ?? 0,
cacheWriteTokens: parsed.cacheWriteTokens ?? 0,
});
continue;
}
} catch {
// not JSON
}
}
rawLinesRef.current.push(line);
if (fenceState === "outside") {
if (
trimmed === YAML_SPEC_FENCE ||
trimmed.startsWith(YAML_SPEC_FENCE + " ")
) {
fenceState = "yaml-spec";
compiler.reset();
} else if (
trimmed === YAML_EDIT_FENCE ||
trimmed.startsWith(YAML_EDIT_FENCE + " ")
) {
fenceState = "yaml-edit";
yamlEditAccumulated = "";
} else if (
trimmed === YAML_PATCH_FENCE ||
trimmed.startsWith(YAML_PATCH_FENCE + " ")
) {
fenceState = "yaml-patch";
yamlPatchAccumulated = "";
} else if (
trimmed === DIFF_FENCE ||
trimmed.startsWith(DIFF_FENCE + " ")
) {
fenceState = "diff";
diffAccumulated = "";
}
} else if (trimmed === FENCE_CLOSE || trimmed === "````") {
if (fenceState === "yaml-spec") {
const { result, newPatches } = compiler.flush();
if (result && typeof result === "object" && result.root) {
for (const patch of newPatches) {
currentSpec = applySpecPatch(currentSpec, patch);
}
setSpec({ ...currentSpec });
}
} else if (fenceState === "yaml-edit") {
try {
const editObj = yamlParse(yamlEditAccumulated);
if (editObj && typeof editObj === "object") {
const merged = deepMergeSpec(
currentSpec as unknown as Record<string, unknown>,
editObj as Record<string, unknown>,
);
const patches = diffToPatches(
currentSpec as unknown as Record<string, unknown>,
merged,
);
for (const patch of patches) {
currentSpec = applySpecPatch(currentSpec, patch);
}
setSpec({ ...currentSpec });
}
} catch {
// Invalid YAML edit
}
} else if (fenceState === "yaml-patch") {
for (const patchLine of yamlPatchAccumulated.split("\n")) {
const t = patchLine.trim();
if (!t) continue;
try {
const patch = JSON.parse(t) as JsonPatch;
if (patch.op) {
currentSpec = applySpecPatch(currentSpec, patch);
}
} catch {
// Skip invalid JSON lines
}
}
setSpec({ ...currentSpec });
} else if (fenceState === "diff") {
try {
const specYaml = yamlStringify(currentSpec, { indent: 2 });
const patched = applyUnifiedDiff(specYaml, diffAccumulated);
if (typeof patched === "string") {
const parsed = yamlParse(patched);
if (parsed && typeof parsed === "object") {
const patches = diffToPatches(
currentSpec as unknown as Record<string, unknown>,
parsed as Record<string, unknown>,
);
for (const patch of patches) {
currentSpec = applySpecPatch(currentSpec, patch);
}
setSpec({ ...currentSpec });
}
}
} catch {
// Diff apply or reparse failed
}
}
fenceState = "outside";
} else if (fenceState === "yaml-spec") {
const { newPatches } = compiler.push(line + "\n");
if (newPatches.length > 0) {
for (const patch of newPatches) {
currentSpec = applySpecPatch(currentSpec, patch);
}
setSpec({ ...currentSpec });
}
} else if (fenceState === "yaml-edit") {
yamlEditAccumulated += line + "\n";
} else if (fenceState === "yaml-patch") {
yamlPatchAccumulated += line + "\n";
} else if (fenceState === "diff") {
diffAccumulated += line + "\n";
}
}
setRawLines([...rawLinesRef.current]);
}
// Process remaining buffer
if (buffer.trim()) {
const trimmed = buffer.trim();
if (trimmed.startsWith("{") && trimmed.includes('"__meta"')) {
try {
const parsed = JSON.parse(trimmed);
if (parsed.__meta === "usage") {
setUsage({
promptTokens: parsed.promptTokens ?? 0,
completionTokens: parsed.completionTokens ?? 0,
totalTokens: parsed.totalTokens ?? 0,
cachedTokens: parsed.cachedTokens ?? 0,
cacheWriteTokens: parsed.cacheWriteTokens ?? 0,
});
}
} catch {
// not JSON
}
} else if (fenceState === "yaml-spec") {
compiler.push(buffer);
const { result, newPatches } = compiler.flush();
if (result && typeof result === "object" && result.root) {
for (const patch of newPatches) {
currentSpec = applySpecPatch(currentSpec, patch);
}
setSpec({ ...currentSpec });
}
}
}
} else {
// ── JSONL streaming ──
let jsonlDiffState: "outside" | "diff" = "outside";
let jsonlDiffAccumulated = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
// Diff fence detection within JSONL mode
if (jsonlDiffState === "outside") {
if (
trimmed === DIFF_FENCE ||
trimmed.startsWith(DIFF_FENCE + " ")
) {
jsonlDiffState = "diff";
jsonlDiffAccumulated = "";
continue;
}
} else if (
jsonlDiffState === "diff" &&
(trimmed === FENCE_CLOSE || trimmed === "````")
) {
try {
const specJson = JSON.stringify(currentSpec, null, 2);
const patched = applyUnifiedDiff(
specJson,
jsonlDiffAccumulated,
);
if (typeof patched === "string") {
const parsed = JSON.parse(patched);
if (parsed && typeof parsed === "object") {
const patches = diffToPatches(
currentSpec as unknown as Record<string, unknown>,
parsed as Record<string, unknown>,
);
for (const patch of patches) {
currentSpec = applySpecPatch(currentSpec, patch);
}
setSpec({ ...currentSpec });
}
}
} catch {
// Diff apply failed
}
jsonlDiffState = "outside";
continue;
}
if (jsonlDiffState === "diff") {
jsonlDiffAccumulated += line + "\n";
rawLinesRef.current.push(line);
continue;
}
// Standard JSONL line parsing
const result = parseLine(trimmed);
if (!result) continue;
if (result.type === "usage") {
setUsage(result.usage);
} else if (result.type === "json-edit") {
const merged = deepMergeSpec(
currentSpec as unknown as Record<string, unknown>,
result.mergeObj,
);
const patches = diffToPatches(
currentSpec as unknown as Record<string, unknown>,
merged,
);
for (const patch of patches) {
currentSpec = applySpecPatch(currentSpec, patch);
}
rawLinesRef.current.push(trimmed);
setSpec({ ...currentSpec });
} else {
rawLinesRef.current.push(trimmed);
currentSpec = applySpecPatch(currentSpec, result.patch);
setSpec({ ...currentSpec });
}
}
setRawLines([...rawLinesRef.current]);
}
if (buffer.trim()) {
const trimmed = buffer.trim();
const result = parseLine(trimmed);
if (result) {
if (result.type === "usage") {
setUsage(result.usage);
} else if (result.type === "json-edit") {
const merged = deepMergeSpec(
currentSpec as unknown as Record<string, unknown>,
result.mergeObj,
);
const patches = diffToPatches(
currentSpec as unknown as Record<string, unknown>,
merged,
);
for (const patch of patches) {
currentSpec = applySpecPatch(currentSpec, patch);
}
rawLinesRef.current.push(trimmed);
setSpec({ ...currentSpec });
} else {
rawLinesRef.current.push(trimmed);
currentSpec = applySpecPatch(currentSpec, result.patch);
setSpec({ ...currentSpec });
}
}
}
}
onCompleteRef.current?.(currentSpec);
} catch (err) {
if ((err as Error).name === "AbortError") return;
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
onErrorRef.current?.(error);
} finally {
setIsStreaming(false);
}
},
[api],
);
useEffect(() => {
return () => {
abortControllerRef.current?.abort();
};
}, []);
return { spec, isStreaming, error, usage, rawLines, send, clear };
}
+14 -4
View File
@@ -35,16 +35,26 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
h2: ({ children }: { children?: React.ReactNode }) => {
const id = slugify(extractText(children));
return (
<h2 id={id} className="text-xl font-semibold mt-12 mb-4">
{children}
<h2 id={id} className="group text-xl font-semibold mt-12 mb-4">
<a href={`#${id}`} className="no-underline hover:no-underline">
{children}
<span className="ml-2 text-muted-foreground/0 group-hover:text-muted-foreground transition-colors select-none">
#
</span>
</a>
</h2>
);
},
h3: ({ children }: { children?: React.ReactNode }) => {
const id = slugify(extractText(children));
return (
<h3 id={id} className="text-lg font-medium mt-8 mb-3">
{children}
<h3 id={id} className="group text-lg font-medium mt-8 mb-3">
<a href={`#${id}`} className="no-underline hover:no-underline">
{children}
<span className="ml-2 text-muted-foreground/0 group-hover:text-muted-foreground transition-colors select-none">
#
</span>
</a>
</h3>
);
},
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.1.6",
"version": "0.1.8",
"type": "module",
"private": true,
"license": "Apache-2.0",
@@ -18,6 +18,7 @@
"@json-render/codegen": "workspace:*",
"@json-render/core": "workspace:*",
"@json-render/react": "workspace:*",
"@json-render/yaml": "workspace:*",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
@@ -34,9 +35,9 @@
"bash-tool": "1.3.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"diff": "^8.0.3",
"embla-carousel-react": "^8.6.0",
"geist": "1.7.0",
"just-bash": "2.9.6",
"lucide-react": "^0.562.0",
"next": "16.1.1",
"next-themes": "^0.4.6",
@@ -51,6 +52,7 @@
"tailwind-merge": "^3.4.0",
"unist-util-visit": "5.1.0",
"vaul": "^1.1.2",
"yaml": "^2.8.2",
"zod": "^4.3.6"
},
"devDependencies": {
+15
View File
@@ -0,0 +1,15 @@
# Vercel AI Gateway
# Automatically authenticated when deployed on Vercel
# For local development, get your key from https://vercel.com/ai-gateway
AI_GATEWAY_API_KEY=
# AI Model Configuration
# Default: anthropic/claude-haiku-4.5
AI_GATEWAY_MODEL=anthropic/claude-haiku-4.5
# Rate Limiting (Upstash Redis)
# Optional - rate limiting is disabled when these are not set
KV_REST_API_URL=
KV_REST_API_TOKEN=
RATE_LIMIT_PER_MINUTE=10
RATE_LIMIT_PER_DAY=100
+18
View File
@@ -1,5 +1,23 @@
# example-chat
## 0.1.8
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/react@0.14.0
- @json-render/shadcn@0.14.0
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react@0.13.0
- @json-render/shadcn@0.13.0
## 0.1.6
### Patch Changes
+26
View File
@@ -1,4 +1,5 @@
import { agent } from "@/lib/agent";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
import {
convertToModelMessages,
createUIMessageStream,
@@ -6,10 +7,35 @@ import {
type UIMessage,
} from "ai";
import { pipeJsonRender } from "@json-render/core";
import { headers } from "next/headers";
export const maxDuration = 60;
export async function POST(req: Request) {
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
]);
if (!minuteResult.success || !dailyResult.success) {
const isMinuteLimit = !minuteResult.success;
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: isMinuteLimit
? "Too many requests. Please wait a moment before trying again."
: "Daily limit reached. Please try again tomorrow.",
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
},
);
}
const body = await req.json();
const uiMessages: UIMessage[] = body.messages;
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+57
View File
@@ -0,0 +1,57 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// Lazy initialization to avoid errors when Redis env vars are not configured
let _minuteRateLimit: Ratelimit | null = null;
let _dailyRateLimit: Ratelimit | null = null;
function getRedis(): Redis | null {
const url = process.env.KV_REST_API_URL;
const token = process.env.KV_REST_API_TOKEN;
if (!url || !token) {
return null;
}
return new Redis({ url, token });
}
// No-op rate limiter for when Redis is not configured
const noopRateLimiter = {
limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),
};
const MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;
const DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;
// Requests per minute (sliding window)
export const minuteRateLimit = {
limit: async (identifier: string) => {
if (!_minuteRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_minuteRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
prefix: "ratelimit:chat:minute",
});
}
return _minuteRateLimit.limit(identifier);
},
};
// Requests per day (fixed window)
export const dailyRateLimit = {
limit: async (identifier: string) => {
if (!_dailyRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_dailyRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
prefix: "ratelimit:chat:daily",
});
}
return _dailyRateLimit.limit(identifier);
},
};
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-chat",
"version": "0.1.6",
"version": "0.1.8",
"type": "module",
"private": true,
"scripts": {
@@ -34,6 +34,8 @@
"streamdown": "^2.2.0",
"tailwind-merge": "^3.4.0",
"three": "^0.182.0",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.37.0",
"zod": "4.3.6"
},
"devDependencies": {
+7
View File
@@ -4,3 +4,10 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/json_render_dashboard
# AI (optional - for UI generation)
AI_GATEWAY_API_KEY=
AI_GATEWAY_MODEL=anthropic/claude-haiku-4.5
# Rate Limiting (Upstash Redis)
# Optional - rate limiting is disabled when these are not set
KV_REST_API_URL=
KV_REST_API_TOKEN=
RATE_LIMIT_PER_MINUTE=10
RATE_LIMIT_PER_DAY=100
+18
View File
@@ -1,5 +1,23 @@
# example-dashboard
## 0.1.8
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/codegen@0.14.0
- @json-render/react@0.14.0
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/codegen@0.13.0
- @json-render/react@0.13.0
## 0.1.6
### Patch Changes
@@ -1,6 +1,8 @@
import { streamText } from "ai";
import { buildUserPrompt } from "@json-render/core";
import { dashboardCatalog } from "@/lib/render/catalog";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
import { headers } from "next/headers";
export const maxDuration = 30;
@@ -9,6 +11,30 @@ const SYSTEM_PROMPT = dashboardCatalog.prompt();
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
export async function POST(req: Request) {
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
]);
if (!minuteResult.success || !dailyResult.success) {
const isMinuteLimit = !minuteResult.success;
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: isMinuteLimit
? "Too many requests. Please wait a moment before trying again."
: "Daily limit reached. Please try again tomorrow.",
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
},
);
}
const { prompt, context } = await req.json();
const userPrompt = buildUserPrompt({
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+57
View File
@@ -0,0 +1,57 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// Lazy initialization to avoid errors when Redis env vars are not configured
let _minuteRateLimit: Ratelimit | null = null;
let _dailyRateLimit: Ratelimit | null = null;
function getRedis(): Redis | null {
const url = process.env.KV_REST_API_URL;
const token = process.env.KV_REST_API_TOKEN;
if (!url || !token) {
return null;
}
return new Redis({ url, token });
}
// No-op rate limiter for when Redis is not configured
const noopRateLimiter = {
limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),
};
const MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;
const DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;
// Requests per minute (sliding window)
export const minuteRateLimit = {
limit: async (identifier: string) => {
if (!_minuteRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_minuteRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
prefix: "ratelimit:dashboard:minute",
});
}
return _minuteRateLimit.limit(identifier);
},
};
// Requests per day (fixed window)
export const dailyRateLimit = {
limit: async (identifier: string) => {
if (!_dailyRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_dailyRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
prefix: "ratelimit:dashboard:daily",
});
}
return _dailyRateLimit.limit(identifier);
},
};
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-dashboard",
"version": "0.1.6",
"version": "0.1.8",
"type": "module",
"private": true,
"scripts": {
@@ -40,6 +40,8 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"vaul": "^1.1.2",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.37.0",
"zod": "^4.3.6"
},
"devDependencies": {
+15
View File
@@ -0,0 +1,15 @@
# Vercel AI Gateway
# Automatically authenticated when deployed on Vercel
# For local development, get your key from https://vercel.com/ai-gateway
AI_GATEWAY_API_KEY=
# AI Model Configuration
# Default: anthropic/claude-haiku-4.5
AI_GATEWAY_MODEL=anthropic/claude-haiku-4.5
# Rate Limiting (Upstash Redis)
# Optional - rate limiting is disabled when these are not set
KV_REST_API_URL=
KV_REST_API_TOKEN=
RATE_LIMIT_PER_MINUTE=10
RATE_LIMIT_PER_DAY=100
+16
View File
@@ -1,5 +1,21 @@
# example-image
## 0.1.5
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/image@0.14.0
## 0.1.4
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/image@0.13.0
## 0.1.3
### Patch Changes
+26
View File
@@ -1,6 +1,8 @@
import { streamText } from "ai";
import { buildUserPrompt, type Spec } from "@json-render/core";
import { imageCatalog } from "@/lib/catalog";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
import { headers } from "next/headers";
export const maxDuration = 60;
@@ -9,6 +11,30 @@ const SYSTEM_PROMPT = imageCatalog.prompt();
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
export async function POST(req: Request) {
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
]);
if (!minuteResult.success || !dailyResult.success) {
const isMinuteLimit = !minuteResult.success;
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: isMinuteLimit
? "Too many requests. Please wait a moment before trying again."
: "Daily limit reached. Please try again tomorrow.",
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
},
);
}
const { prompt, startingSpec } = (await req.json()) as {
prompt: string;
startingSpec?: Spec | null;
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+57
View File
@@ -0,0 +1,57 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// Lazy initialization to avoid errors when Redis env vars are not configured
let _minuteRateLimit: Ratelimit | null = null;
let _dailyRateLimit: Ratelimit | null = null;
function getRedis(): Redis | null {
const url = process.env.KV_REST_API_URL;
const token = process.env.KV_REST_API_TOKEN;
if (!url || !token) {
return null;
}
return new Redis({ url, token });
}
// No-op rate limiter for when Redis is not configured
const noopRateLimiter = {
limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),
};
const MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;
const DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;
// Requests per minute (sliding window)
export const minuteRateLimit = {
limit: async (identifier: string) => {
if (!_minuteRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_minuteRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
prefix: "ratelimit:image:minute",
});
}
return _minuteRateLimit.limit(identifier);
},
};
// Requests per day (fixed window)
export const dailyRateLimit = {
limit: async (identifier: string) => {
if (!_dailyRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_dailyRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
prefix: "ratelimit:image:daily",
});
}
return _dailyRateLimit.limit(identifier);
},
};
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-image",
"version": "0.1.3",
"version": "0.1.5",
"type": "module",
"private": true,
"scripts": {
@@ -28,6 +28,8 @@
"react-resizable-panels": "^4.4.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.37.0",
"zod": "4.3.6"
},
"devDependencies": {
+20
View File
@@ -1,5 +1,25 @@
# example-mcp
## 0.1.4
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/mcp@0.14.0
- @json-render/react@0.14.0
- @json-render/shadcn@0.14.0
## 0.1.3
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/mcp@0.13.0
- @json-render/react@0.13.0
- @json-render/shadcn@0.13.0
## 0.1.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-mcp",
"version": "0.1.2",
"version": "0.1.4",
"type": "module",
"private": true,
"scripts": {
+18
View File
@@ -1,5 +1,23 @@
# example-no-ai
## 0.1.8
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/react@0.14.0
- @json-render/shadcn@0.14.0
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react@0.13.0
- @json-render/shadcn@0.13.0
## 0.1.6
### Patch Changes
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-no-ai",
"version": "0.1.6",
"version": "0.1.8",
"type": "module",
"private": true,
"scripts": {
+7
View File
@@ -7,3 +7,10 @@ AI_GATEWAY_API_KEY=
# Override the default model used for email generation
# Default: anthropic/claude-haiku-4.5
AI_GATEWAY_MODEL=anthropic/claude-haiku-4.5
# Rate Limiting (Upstash Redis)
# Optional - rate limiting is disabled when these are not set
KV_REST_API_URL=
KV_REST_API_TOKEN=
RATE_LIMIT_PER_MINUTE=10
RATE_LIMIT_PER_DAY=100
+16
View File
@@ -1,5 +1,21 @@
# example-react-email
## 0.1.4
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/react-email@0.14.0
## 0.1.3
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react-email@0.13.0
## 0.1.2
### Patch Changes
@@ -1,6 +1,8 @@
import { streamText } from "ai";
import { buildUserPrompt, type Spec } from "@json-render/core";
import { emailCatalog } from "@/lib/catalog";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
import { headers } from "next/headers";
export const maxDuration = 60;
@@ -9,6 +11,30 @@ const SYSTEM_PROMPT = emailCatalog.prompt();
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
export async function POST(req: Request) {
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
]);
if (!minuteResult.success || !dailyResult.success) {
const isMinuteLimit = !minuteResult.success;
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: isMinuteLimit
? "Too many requests. Please wait a moment before trying again."
: "Daily limit reached. Please try again tomorrow.",
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
},
);
}
const { prompt, startingSpec } = (await req.json()) as {
prompt: string;
startingSpec?: Spec | null;
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+57
View File
@@ -0,0 +1,57 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// Lazy initialization to avoid errors when Redis env vars are not configured
let _minuteRateLimit: Ratelimit | null = null;
let _dailyRateLimit: Ratelimit | null = null;
function getRedis(): Redis | null {
const url = process.env.KV_REST_API_URL;
const token = process.env.KV_REST_API_TOKEN;
if (!url || !token) {
return null;
}
return new Redis({ url, token });
}
// No-op rate limiter for when Redis is not configured
const noopRateLimiter = {
limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),
};
const MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;
const DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;
// Requests per minute (sliding window)
export const minuteRateLimit = {
limit: async (identifier: string) => {
if (!_minuteRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_minuteRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
prefix: "ratelimit:react-email:minute",
});
}
return _minuteRateLimit.limit(identifier);
},
};
// Requests per day (fixed window)
export const dailyRateLimit = {
limit: async (identifier: string) => {
if (!_dailyRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_dailyRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
prefix: "ratelimit:react-email:daily",
});
}
return _dailyRateLimit.limit(identifier);
},
};
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-email",
"version": "0.1.2",
"version": "0.1.4",
"type": "module",
"private": true,
"scripts": {
@@ -25,6 +25,8 @@
"react-resizable-panels": "^4.4.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.37.0",
"zod": "4.3.6"
},
"devDependencies": {
+16
View File
@@ -1,5 +1,21 @@
# example-react-native
## 0.1.8
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/react-native@0.14.0
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react-native@0.13.0
## 0.1.6
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-native",
"version": "0.1.6",
"version": "0.1.8",
"private": true,
"main": "expo-router/entry",
"scripts": {
+7
View File
@@ -3,3 +3,10 @@ AI_GATEWAY_API_KEY=
# Optional: override the default model (default: anthropic/claude-haiku-4.5)
# AI_GATEWAY_MODEL=openai/gpt-4o-mini
# Rate Limiting (Upstash Redis)
# Optional - rate limiting is disabled when these are not set
KV_REST_API_URL=
KV_REST_API_TOKEN=
RATE_LIMIT_PER_MINUTE=10
RATE_LIMIT_PER_DAY=100
+16
View File
@@ -1,5 +1,21 @@
# example-react-pdf
## 0.1.8
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/react-pdf@0.14.0
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react-pdf@0.13.0
## 0.1.6
### Patch Changes
@@ -2,6 +2,8 @@ import { streamText } from "ai";
import { gateway } from "@ai-sdk/gateway";
import { buildUserPrompt, type Spec } from "@json-render/core";
import { pdfCatalog } from "@/lib/catalog";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
import { headers } from "next/headers";
export const maxDuration = 60;
@@ -10,6 +12,30 @@ const SYSTEM_PROMPT = pdfCatalog.prompt();
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
export async function POST(req: Request) {
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
]);
if (!minuteResult.success || !dailyResult.success) {
const isMinuteLimit = !minuteResult.success;
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: isMinuteLimit
? "Too many requests. Please wait a moment before trying again."
: "Daily limit reached. Please try again tomorrow.",
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
},
);
}
const { prompt, startingSpec } = (await req.json()) as {
prompt: string;
startingSpec?: Spec | null;
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+57
View File
@@ -0,0 +1,57 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// Lazy initialization to avoid errors when Redis env vars are not configured
let _minuteRateLimit: Ratelimit | null = null;
let _dailyRateLimit: Ratelimit | null = null;
function getRedis(): Redis | null {
const url = process.env.KV_REST_API_URL;
const token = process.env.KV_REST_API_TOKEN;
if (!url || !token) {
return null;
}
return new Redis({ url, token });
}
// No-op rate limiter for when Redis is not configured
const noopRateLimiter = {
limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),
};
const MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;
const DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;
// Requests per minute (sliding window)
export const minuteRateLimit = {
limit: async (identifier: string) => {
if (!_minuteRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_minuteRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
prefix: "ratelimit:react-pdf:minute",
});
}
return _minuteRateLimit.limit(identifier);
},
};
// Requests per day (fixed window)
export const dailyRateLimit = {
limit: async (identifier: string) => {
if (!_dailyRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_dailyRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
prefix: "ratelimit:react-pdf:daily",
});
}
return _dailyRateLimit.limit(identifier);
},
};
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-react-pdf",
"version": "0.1.6",
"version": "0.1.8",
"type": "module",
"private": true,
"scripts": {
@@ -26,6 +26,8 @@
"react-resizable-panels": "^4.4.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.1.18",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.37.0",
"zod": "4.3.6"
},
"devDependencies": {
+19
View File
@@ -0,0 +1,19 @@
# example-react-three-fiber
## 0.1.8
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/react@0.14.0
- @json-render/react-three-fiber@0.14.0
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react-three-fiber@0.13.0
- @json-render/react@0.13.0
+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>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+490
View File
@@ -0,0 +1,490 @@
"use client";
import { useState, useSyncExternalStore, useCallback } 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 MOBILE_BREAKPOINT = 768;
function subscribeToResize(cb: () => void) {
window.addEventListener("resize", cb);
return () => window.removeEventListener("resize", cb);
}
function getIsMobile() {
return typeof window !== "undefined"
? window.innerWidth < MOBILE_BREAKPOINT
: false;
}
function useIsMobile() {
return useSyncExternalStore(subscribeToResize, getIsMobile, () => false);
}
const LIST_WIDTH = 220;
const JSON_WIDTH = 380;
const HEADER_HEIGHT = 40;
const MOBILE_HEADER_HEIGHT = 48;
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",
};
function MobileLayout() {
const [selectedIndex, setSelectedIndex] = useState(0);
const [showJson, setShowJson] = useState(false);
const [showScenes, setShowScenes] = useState(false);
const selected = scenes[selectedIndex]!;
const closePanels = useCallback(() => {
setShowJson(false);
setShowScenes(false);
}, []);
return (
<div
style={{
height: "100dvh",
display: "flex",
flexDirection: "column",
background: "#0a0a0a",
overflow: "hidden",
}}
>
<div
style={{
height: MOBILE_HEADER_HEIGHT,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0 12px",
borderBottom: "1px solid #1e1e1e",
background: "#0f0f0f",
flexShrink: 0,
gap: 8,
}}
>
<button
onClick={() => {
setShowScenes((v) => !v);
setShowJson(false);
}}
style={{
background: showScenes ? "rgba(255,255,255,0.1)" : "transparent",
border: "1px solid #333",
borderRadius: 6,
color: "#ccc",
fontSize: 12,
fontWeight: 500,
padding: "6px 10px",
cursor: "pointer",
fontFamily: "inherit",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "50%",
}}
>
{selected.name}
</button>
<span
style={{
flex: 1,
fontSize: 10,
color: "#555",
textAlign: "center",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
fontFamily: "ui-monospace, monospace",
textTransform: "uppercase",
letterSpacing: "0.06em",
}}
>
{selected.description}
</span>
<button
onClick={() => {
setShowJson((v) => !v);
setShowScenes(false);
}}
style={{
background: showJson ? "rgba(255,255,255,0.1)" : "transparent",
border: "1px solid #333",
borderRadius: 6,
color: "#ccc",
fontSize: 11,
fontWeight: 500,
padding: "6px 10px",
cursor: "pointer",
fontFamily: "ui-monospace, monospace",
whiteSpace: "nowrap",
letterSpacing: "0.04em",
}}
>
JSON
</button>
</div>
<div style={{ flex: 1, position: "relative", minHeight: 0 }}>
<ThreeCanvas
key={selectedIndex}
spec={selected.spec}
registry={registry}
functions={computedFunctions}
shadows
camera={{ position: [0, 0, 5], fov: 50 }}
style={{ width: "100%", height: "100%" }}
/>
{showScenes && (
<>
<div
onClick={closePanels}
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.5)",
zIndex: 10,
}}
/>
<div
style={{
position: "absolute",
top: 0,
left: 0,
bottom: 0,
width: "75%",
maxWidth: 280,
background: "#0f0f0f",
borderRight: "1px solid #1e1e1e",
zIndex: 11,
display: "flex",
flexDirection: "column",
overflowY: "auto",
WebkitOverflowScrolling: "touch",
}}
>
<div style={{ ...headerStyle, height: 36 }}>Scenes</div>
<div style={{ padding: "6px 0" }}>
{scenes.map((scene, i) => (
<button
key={scene.name}
onClick={() => {
setSelectedIndex(i);
setShowScenes(false);
}}
style={{
display: "block",
width: "100%",
padding: "10px 16px",
fontSize: 14,
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>
</>
)}
{showJson && (
<>
<div
onClick={closePanels}
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.5)",
zIndex: 10,
}}
/>
<div
style={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: "85%",
maxWidth: 400,
background: "#0d0d0d",
borderLeft: "1px solid #1e1e1e",
zIndex: 11,
display: "flex",
flexDirection: "column",
}}
>
<div style={{ ...headerStyle, height: 36 }}>Spec JSON</div>
<pre
style={{
flex: 1,
margin: 0,
padding: 14,
overflowY: "auto",
overflowX: "auto",
WebkitOverflowScrolling: "touch",
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>
</div>
);
}
function DesktopLayout() {
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>
);
}
export default function Page() {
const isMobile = useIsMobile();
return isMobile ? <MobileLayout /> : <DesktopLayout />;
}
@@ -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.8",
"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"]
}
+7
View File
@@ -7,3 +7,10 @@ AI_GATEWAY_API_KEY=
# Override the default model used for video generation
# Default: anthropic/claude-haiku-4.5
AI_GATEWAY_MODEL=anthropic/claude-haiku-4.5
# Rate Limiting (Upstash Redis)
# Optional - rate limiting is disabled when these are not set
KV_REST_API_URL=
KV_REST_API_TOKEN=
RATE_LIMIT_PER_MINUTE=10
RATE_LIMIT_PER_DAY=100
+16
View File
@@ -1,5 +1,21 @@
# example-remotion
## 0.1.8
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/remotion@0.14.0
## 0.1.7
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/remotion@0.13.0
## 0.1.6
### Patch Changes
@@ -1,5 +1,7 @@
import { streamText } from "ai";
import { getVideoPrompt } from "@/lib/catalog";
import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
import { headers } from "next/headers";
export const maxDuration = 30;
@@ -10,6 +12,30 @@ const MAX_PROMPT_LENGTH = 500;
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
export async function POST(req: Request) {
const headersList = await headers();
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
const [minuteResult, dailyResult] = await Promise.all([
minuteRateLimit.limit(ip),
dailyRateLimit.limit(ip),
]);
if (!minuteResult.success || !dailyResult.success) {
const isMinuteLimit = !minuteResult.success;
return new Response(
JSON.stringify({
error: "Rate limit exceeded",
message: isMinuteLimit
? "Too many requests. Please wait a moment before trying again."
: "Daily limit reached. Please try again tomorrow.",
}),
{
status: 429,
headers: { "Content-Type": "application/json" },
},
);
}
const { prompt } = await req.json();
const sanitizedPrompt = String(prompt || "").slice(0, MAX_PROMPT_LENGTH);
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+57
View File
@@ -0,0 +1,57 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
// Lazy initialization to avoid errors when Redis env vars are not configured
let _minuteRateLimit: Ratelimit | null = null;
let _dailyRateLimit: Ratelimit | null = null;
function getRedis(): Redis | null {
const url = process.env.KV_REST_API_URL;
const token = process.env.KV_REST_API_TOKEN;
if (!url || !token) {
return null;
}
return new Redis({ url, token });
}
// No-op rate limiter for when Redis is not configured
const noopRateLimiter = {
limit: async () => ({ success: true, limit: 0, remaining: 0, reset: 0 }),
};
const MINUTE_LIMIT = Number(process.env.RATE_LIMIT_PER_MINUTE) || 10;
const DAILY_LIMIT = Number(process.env.RATE_LIMIT_PER_DAY) || 100;
// Requests per minute (sliding window)
export const minuteRateLimit = {
limit: async (identifier: string) => {
if (!_minuteRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_minuteRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
prefix: "ratelimit:remotion:minute",
});
}
return _minuteRateLimit.limit(identifier);
},
};
// Requests per day (fixed window)
export const dailyRateLimit = {
limit: async (identifier: string) => {
if (!_dailyRateLimit) {
const redis = getRedis();
if (!redis) return noopRateLimiter.limit();
_dailyRateLimit = new Ratelimit({
redis,
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
prefix: "ratelimit:remotion:daily",
});
}
return _dailyRateLimit.limit(identifier);
},
};
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-remotion",
"version": "0.1.6",
"version": "0.1.8",
"type": "module",
"private": true,
"scripts": {
@@ -27,6 +27,8 @@
"remotion": "4.0.418",
"shiki": "^3.21.0",
"tailwind-merge": "^3.4.0",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.37.0",
"zod": "^4.3.6"
},
"devDependencies": {
+17
View File
@@ -0,0 +1,17 @@
# example-solid
## 0.1.2
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/solid@0.14.0
## 0.1.1
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/solid@0.13.0
+7
View File
@@ -4,6 +4,13 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>json-render solid example</title>
<meta property="og:title" content="Solid Example | json-render" />
<meta property="og:image" content="/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Solid Example | json-render" />
<meta name="twitter:image" content="/og-image.png" />
</head>
<body>
<div id="app"></div>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "example-solid",
"version": "0.1.0",
"version": "0.1.2",
"private": true,
"type": "module",
"scripts": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -1,5 +1,21 @@
# com.example.json-render-demo
## 0.0.9
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/react@0.14.0
## 0.0.8
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react@0.13.0
## 0.0.7
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-demo",
"version": "0.0.7",
"version": "0.0.9",
"description": "Test",
"private": true,
"license": "~~proprietary~~",
@@ -1,5 +1,21 @@
# com.example.json-render-fullpage-demo
## 0.0.9
### Patch Changes
- Updated dependencies [a8afd8b]
- @json-render/core@0.14.0
- @json-render/react@0.14.0
## 0.0.8
### Patch Changes
- Updated dependencies [5b32de8]
- @json-render/core@0.13.0
- @json-render/react@0.13.0
## 0.0.7
### Patch Changes
@@ -1,6 +1,6 @@
{
"name": "com.example.json-render-fullpage-demo",
"version": "0.0.7",
"version": "0.0.9",
"description": "Full-page Stripe App example (alpha)",
"private": true,
"license": "~~proprietary~~",

Some files were not shown because too many files have changed in this diff Show More