mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 04:18:15 +08:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22545b49cc | ||
|
|
a8afd8bfe2 | ||
|
|
dc489601be | ||
|
|
7758d4bbd5 | ||
|
|
6225fc41eb | ||
|
|
5b32de8720 | ||
|
|
f6d1c5134b | ||
|
|
316439fcd7 | ||
|
|
c180f529a0 | ||
|
|
c3c0a36f20 | ||
|
|
9bb82604f0 |
@@ -19,7 +19,10 @@
|
|||||||
"@json-render/xstate",
|
"@json-render/xstate",
|
||||||
"@json-render/image",
|
"@json-render/image",
|
||||||
"@json-render/mcp",
|
"@json-render/mcp",
|
||||||
"@json-render/svelte"
|
"@json-render/svelte",
|
||||||
|
"@json-render/solid",
|
||||||
|
"@json-render/react-three-fiber",
|
||||||
|
"@json-render/yaml"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
|
|||||||
+34
-25
@@ -13,34 +13,43 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
lint:
|
||||||
name: Lint, Type Check & Build
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
- uses: pnpm/action-setup@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: "pnpm"
|
cache: pnpm
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm lint
|
||||||
|
|
||||||
- name: Install dependencies
|
test:
|
||||||
run: pnpm install --frozen-lockfile
|
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
|
typecheck:
|
||||||
run: pnpm lint
|
name: Type Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
- name: Type check
|
steps:
|
||||||
run: pnpm type-check
|
- uses: actions/checkout@v4
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
- name: Test
|
- uses: actions/setup-node@v4
|
||||||
run: pnpm test
|
with:
|
||||||
|
node-version: 20
|
||||||
- name: Build
|
cache: pnpm
|
||||||
run: pnpm build
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm type-check
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ npm install @json-render/core @json-render/react-email @react-email/components @
|
|||||||
npm install @json-render/core @json-render/vue
|
npm install @json-render/core @json-render/vue
|
||||||
# or for Svelte
|
# or for Svelte
|
||||||
npm install @json-render/core @json-render/svelte
|
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?
|
## Why json-render?
|
||||||
@@ -30,7 +34,7 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur
|
|||||||
- **Guardrailed** - AI can only use components in your catalog
|
- **Guardrailed** - AI can only use components in your catalog
|
||||||
- **Predictable** - JSON output matches your schema, every time
|
- **Predictable** - JSON output matches your schema, every time
|
||||||
- **Fast** - Stream and render progressively as the model responds
|
- **Fast** - Stream and render progressively as the model responds
|
||||||
- **Cross-Platform** - React, Vue, Svelte (web), React Native (mobile) from the same catalog
|
- **Cross-Platform** - React, Vue, Svelte, Solid (web), React Native (mobile) from the same catalog
|
||||||
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
|
- **Batteries Included** - 36 pre-built shadcn/ui components ready to use
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -91,9 +95,7 @@ const { registry } = defineRegistry(catalog, {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
Button: ({ props, emit }) => (
|
Button: ({ props, emit }) => (
|
||||||
<button onClick={() => emit("press")}>
|
<button onClick={() => emit("press")}>{props.label}</button>
|
||||||
{props.label}
|
|
||||||
</button>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -113,24 +115,27 @@ function Dashboard({ spec }) {
|
|||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
| Package | Description |
|
| Package | Description |
|
||||||
|---------|-------------|
|
| --------------------------- | ---------------------------------------------------------------------- |
|
||||||
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities |
|
||||||
| `@json-render/react` | React renderer, contexts, hooks |
|
| `@json-render/react` | React renderer, contexts, hooks |
|
||||||
| `@json-render/vue` | Vue 3 renderer, composables, providers |
|
| `@json-render/vue` | Vue 3 renderer, composables, providers |
|
||||||
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
|
| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity |
|
||||||
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
|
| `@json-render/solid` | SolidJS renderer with fine-grained reactive contexts |
|
||||||
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
|
||||||
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (19 built-in components) |
|
||||||
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
|
| `@json-render/react-native` | React Native renderer with standard mobile components |
|
||||||
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
|
| `@json-render/remotion` | Remotion video renderer, timeline schema |
|
||||||
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
|
| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs |
|
||||||
| `@json-render/codegen` | Utilities for generating code from json-render UI trees |
|
| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs |
|
||||||
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
|
| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori |
|
||||||
| `@json-render/zustand` | Zustand adapter for `StateStore` |
|
| `@json-render/codegen` | Utilities for generating code from json-render UI trees |
|
||||||
| `@json-render/jotai` | Jotai adapter for `StateStore` |
|
| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` |
|
||||||
| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` |
|
| `@json-render/zustand` | Zustand adapter for `StateStore` |
|
||||||
| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code |
|
| `@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
|
## Renderers
|
||||||
|
|
||||||
@@ -159,7 +164,7 @@ const spec = {
|
|||||||
|
|
||||||
// defineRegistry creates a type-safe component registry
|
// defineRegistry creates a type-safe component registry
|
||||||
const { registry } = defineRegistry(catalog, { components });
|
const { registry } = defineRegistry(catalog, { components });
|
||||||
<Renderer spec={spec} registry={registry} />
|
<Renderer spec={spec} registry={registry} />;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Vue (UI)
|
### Vue (UI)
|
||||||
@@ -199,6 +204,26 @@ const { registry } = defineRegistry(catalog, {
|
|||||||
// <Renderer spec={spec} registry={registry} />
|
// <Renderer spec={spec} registry={registry} />
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Solid (UI)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/solid";
|
||||||
|
import { schema } from "@json-render/solid/schema";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: (renderProps) => <div>{renderProps.children}</div>,
|
||||||
|
Button: (renderProps) => (
|
||||||
|
<button onClick={() => renderProps.emit("press")}>
|
||||||
|
{renderProps.element.props.label as string}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
<Renderer spec={spec} registry={registry} />;
|
||||||
|
```
|
||||||
|
|
||||||
### shadcn/ui (Web)
|
### shadcn/ui (Web)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
@@ -229,7 +254,7 @@ const { registry } = defineRegistry(catalog, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
<Renderer spec={spec} registry={registry} />
|
<Renderer spec={spec} registry={registry} />;
|
||||||
```
|
```
|
||||||
|
|
||||||
### React Native (Mobile)
|
### React Native (Mobile)
|
||||||
@@ -250,23 +275,40 @@ const catalog = defineCatalog(schema, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { registry } = defineRegistry(catalog, { components: {} });
|
const { registry } = defineRegistry(catalog, { components: {} });
|
||||||
<Renderer spec={spec} registry={registry} />
|
<Renderer spec={spec} registry={registry} />;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Remotion (Video)
|
### Remotion (Video)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Player } from "@remotion/player";
|
import { Player } from "@remotion/player";
|
||||||
import { Renderer, schema, standardComponentDefinitions } from "@json-render/remotion";
|
import {
|
||||||
|
Renderer,
|
||||||
|
schema,
|
||||||
|
standardComponentDefinitions,
|
||||||
|
} from "@json-render/remotion";
|
||||||
|
|
||||||
// Timeline spec format
|
// Timeline spec format
|
||||||
const spec = {
|
const spec = {
|
||||||
composition: { id: "video", fps: 30, width: 1920, height: 1080, durationInFrames: 300 },
|
composition: {
|
||||||
|
id: "video",
|
||||||
|
fps: 30,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
durationInFrames: 300,
|
||||||
|
},
|
||||||
tracks: [{ id: "main", name: "Main", type: "video", enabled: true }],
|
tracks: [{ id: "main", name: "Main", type: "video", enabled: true }],
|
||||||
clips: [
|
clips: [
|
||||||
{ id: "clip-1", trackId: "main", component: "TitleCard", props: { title: "Hello" }, from: 0, durationInFrames: 90 }
|
{
|
||||||
|
id: "clip-1",
|
||||||
|
trackId: "main",
|
||||||
|
component: "TitleCard",
|
||||||
|
props: { title: "Hello" },
|
||||||
|
from: 0,
|
||||||
|
durationInFrames: 90,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
audio: { tracks: [] }
|
audio: { tracks: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
<Player
|
<Player
|
||||||
@@ -276,7 +318,7 @@ const spec = {
|
|||||||
fps={spec.composition.fps}
|
fps={spec.composition.fps}
|
||||||
compositionWidth={spec.composition.width}
|
compositionWidth={spec.composition.width}
|
||||||
compositionHeight={spec.composition.height}
|
compositionHeight={spec.composition.height}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
### React PDF (Documents)
|
### React PDF (Documents)
|
||||||
@@ -287,7 +329,11 @@ import { renderToBuffer } from "@json-render/react-pdf";
|
|||||||
const spec = {
|
const spec = {
|
||||||
root: "doc",
|
root: "doc",
|
||||||
elements: {
|
elements: {
|
||||||
doc: { type: "Document", props: { title: "Invoice" }, children: ["page-1"] },
|
doc: {
|
||||||
|
type: "Document",
|
||||||
|
props: { title: "Invoice" },
|
||||||
|
children: ["page-1"],
|
||||||
|
},
|
||||||
"page-1": {
|
"page-1": {
|
||||||
type: "Page",
|
type: "Page",
|
||||||
props: { size: "A4" },
|
props: { size: "A4" },
|
||||||
@@ -301,8 +347,14 @@ const spec = {
|
|||||||
"table-1": {
|
"table-1": {
|
||||||
type: "Table",
|
type: "Table",
|
||||||
props: {
|
props: {
|
||||||
columns: [{ header: "Item", width: "60%" }, { header: "Price", width: "40%", align: "right" }],
|
columns: [
|
||||||
rows: [["Widget A", "$10.00"], ["Widget B", "$25.00"]],
|
{ header: "Item", width: "60%" },
|
||||||
|
{ header: "Price", width: "40%", align: "right" },
|
||||||
|
],
|
||||||
|
rows: [
|
||||||
|
["Widget A", "$10.00"],
|
||||||
|
["Widget B", "$25.00"],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
@@ -327,7 +379,11 @@ const catalog = defineCatalog(schema, {
|
|||||||
const spec = {
|
const spec = {
|
||||||
root: "html-1",
|
root: "html-1",
|
||||||
elements: {
|
elements: {
|
||||||
"html-1": { type: "Html", props: { lang: "en", dir: "ltr" }, children: ["head-1", "body-1"] },
|
"html-1": {
|
||||||
|
type: "Html",
|
||||||
|
props: { lang: "en", dir: "ltr" },
|
||||||
|
children: ["head-1", "body-1"],
|
||||||
|
},
|
||||||
"head-1": { type: "Head", props: {}, children: [] },
|
"head-1": { type: "Head", props: {}, children: [] },
|
||||||
"body-1": {
|
"body-1": {
|
||||||
type: "Body",
|
type: "Body",
|
||||||
@@ -336,11 +392,17 @@ const spec = {
|
|||||||
},
|
},
|
||||||
"container-1": {
|
"container-1": {
|
||||||
type: "Container",
|
type: "Container",
|
||||||
props: { style: { maxWidth: "600px", margin: "0 auto", padding: "20px" } },
|
props: {
|
||||||
|
style: { maxWidth: "600px", margin: "0 auto", padding: "20px" },
|
||||||
|
},
|
||||||
children: ["heading-1", "text-1"],
|
children: ["heading-1", "text-1"],
|
||||||
},
|
},
|
||||||
"heading-1": { type: "Heading", props: { text: "Welcome" }, children: [] },
|
"heading-1": { type: "Heading", props: { text: "Welcome" }, children: [] },
|
||||||
"text-1": { type: "Text", props: { text: "Thanks for signing up." }, children: [] },
|
"text-1": {
|
||||||
|
type: "Text",
|
||||||
|
props: { text: "Thanks for signing up." },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -376,6 +438,47 @@ import { renderToSvg } from "@json-render/image/render";
|
|||||||
const svg = await renderToSvg(spec, { fonts });
|
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
|
## Features
|
||||||
|
|
||||||
### Streaming (SpecStream)
|
### Streaming (SpecStream)
|
||||||
@@ -425,8 +528,16 @@ Any prop value can be data-driven using expressions:
|
|||||||
{
|
{
|
||||||
"type": "Icon",
|
"type": "Icon",
|
||||||
"props": {
|
"props": {
|
||||||
"name": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "home", "$else": "home-outline" },
|
"name": {
|
||||||
"color": { "$cond": { "$state": "/activeTab", "eq": "home" }, "$then": "#007AFF", "$else": "#8E8E93" }
|
"$cond": { "$state": "/activeTab", "eq": "home" },
|
||||||
|
"$then": "home",
|
||||||
|
"$else": "home-outline"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"$cond": { "$state": "/activeTab", "eq": "home" },
|
||||||
|
"$then": "#007AFF",
|
||||||
|
"$else": "#8E8E93"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -445,7 +556,10 @@ Components can trigger actions, including the built-in `setState` action:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "Pressable",
|
"type": "Pressable",
|
||||||
"props": { "action": "setState", "actionParams": { "statePath": "/activeTab", "value": "home" } },
|
"props": {
|
||||||
|
"action": "setState",
|
||||||
|
"actionParams": { "statePath": "/activeTab", "value": "home" }
|
||||||
|
},
|
||||||
"children": ["home-icon"]
|
"children": ["home-icon"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -459,9 +573,15 @@ React to state changes by triggering actions:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "Select",
|
"type": "Select",
|
||||||
"props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada", "UK"] },
|
"props": {
|
||||||
|
"value": { "$bindState": "/form/country" },
|
||||||
|
"options": ["US", "Canada", "UK"]
|
||||||
|
},
|
||||||
"watch": {
|
"watch": {
|
||||||
"/form/country": { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } }
|
"/form/country": {
|
||||||
|
"action": "loadCities",
|
||||||
|
"params": { "country": { "$state": "/form/country" } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -486,7 +606,7 @@ pnpm dev
|
|||||||
- Chat Example: run `pnpm dev` in `examples/chat`
|
- Chat Example: run `pnpm dev` in `examples/chat`
|
||||||
- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
|
- Svelte Example: run `pnpm dev` in `examples/svelte` or `examples/svelte-chat`
|
||||||
- Vue Example: run `pnpm dev` in `examples/vue`
|
- Vue Example: run `pnpm dev` in `examples/vue`
|
||||||
- Vite Renderers (React + Vue + Svelte): run `pnpm dev` in `examples/vite-renderers`
|
- Vite Renderers (React + Vue + Svelte + Solid): run `pnpm dev` in `examples/vite-renderers`
|
||||||
- React Native example: run `npx expo start` in `examples/react-native`
|
- React Native example: run `npx expo start` in `examples/react-native`
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
@@ -496,7 +616,7 @@ flowchart LR
|
|||||||
A[User Prompt] --> B[AI + Catalog]
|
A[User Prompt] --> B[AI + Catalog]
|
||||||
B --> C[JSON Spec]
|
B --> C[JSON Spec]
|
||||||
C --> D[Renderer]
|
C --> D[Renderer]
|
||||||
|
|
||||||
B -.- E([guardrailed])
|
B -.- E([guardrailed])
|
||||||
C -.- F([predictable])
|
C -.- F([predictable])
|
||||||
D -.- G([streamed])
|
D -.- G([streamed])
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
# web
|
# 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
|
## 0.1.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ interface PromptOptions {
|
|||||||
system?: string; // Custom system message intro
|
system?: string; // Custom system message intro
|
||||||
customRules?: string[]; // Additional rules to append
|
customRules?: string[]; // Additional rules to append
|
||||||
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
|
mode?: "standalone" | "inline" | "generate" | "chat"; // Output mode (default: "standalone")
|
||||||
|
editModes?: EditMode[]; // Edit modes to document in prompt (default: ["patch"])
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpecValidationResult<T> {
|
interface SpecValidationResult<T> {
|
||||||
@@ -480,9 +481,10 @@ function buildUserPrompt(options: UserPromptOptions): string
|
|||||||
|
|
||||||
interface UserPromptOptions {
|
interface UserPromptOptions {
|
||||||
prompt: string; // The user's text prompt
|
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
|
state?: Record<string, unknown> | null; // Runtime state context to include
|
||||||
maxPromptLength?: number; // Max length for user text (truncates before wrapping)
|
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" });
|
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
|
```typescript
|
||||||
const userPrompt = buildUserPrompt({
|
const userPrompt = buildUserPrompt({
|
||||||
prompt: "add a dark mode toggle",
|
prompt: "add a dark mode toggle",
|
||||||
currentSpec: existingSpec,
|
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
|
## evaluateVisibility
|
||||||
|
|
||||||
Evaluates a visibility condition against the state model.
|
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<string, unknown></code></td>
|
||||||
|
<td>Initial state (uncontrolled mode)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>handlers</code></td>
|
||||||
|
<td><code>Record<string, Function></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">;
|
||||||
|
```
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import { pageMetadata } from "@/lib/page-metadata";
|
||||||
|
export const metadata = pageMetadata("docs/api/solid");
|
||||||
|
|
||||||
|
# @json-render/solid
|
||||||
|
|
||||||
|
SolidJS components, providers, and hooks for rendering json-render specs.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<PackageInstall packages="@json-render/core @json-render/solid" />
|
||||||
|
|
||||||
|
Peer dependencies: `solid-js ^1.9.0` and `zod ^4.0.0`.
|
||||||
|
|
||||||
|
<PackageInstall packages="solid-js zod" />
|
||||||
|
|
||||||
|
## Providers
|
||||||
|
|
||||||
|
### StateProvider
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StateProvider
|
||||||
|
initialState={{}}
|
||||||
|
onStateChange={(changes) => console.log(changes)}
|
||||||
|
>
|
||||||
|
{/* children */}
|
||||||
|
</StateProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Prop</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>store</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>StateStore</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
External store (controlled mode). When provided,{" "}
|
||||||
|
<code>initialState</code> and <code>onStateChange</code> are ignored.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>initialState</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>Record<string, unknown></code>
|
||||||
|
</td>
|
||||||
|
<td>Initial state model for uncontrolled mode.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>onStateChange</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>
|
||||||
|
{"(changes: Array<{ path: string; value: unknown }>) => void"}
|
||||||
|
</code>
|
||||||
|
</td>
|
||||||
|
<td>Called for uncontrolled state updates.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### ActionProvider
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ActionProvider
|
||||||
|
handlers={{ submit: async (params) => {} }}
|
||||||
|
navigate={(path) => {}}
|
||||||
|
>
|
||||||
|
{/* children */}
|
||||||
|
</ActionProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### VisibilityProvider
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<VisibilityProvider>{/* children */}</VisibilityProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ValidationProvider
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ValidationProvider customFunctions={{ custom: (value) => Boolean(value) }}>
|
||||||
|
{/* children */}
|
||||||
|
</ValidationProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### JSONUIProvider
|
||||||
|
|
||||||
|
Combined provider wrapper for state, visibility, validation, and actions.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<JSONUIProvider
|
||||||
|
registry={registry}
|
||||||
|
initialState={{}}
|
||||||
|
handlers={handlers}
|
||||||
|
validationFunctions={validationFunctions}
|
||||||
|
>
|
||||||
|
<Renderer spec={spec} registry={registry} />
|
||||||
|
</JSONUIProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## defineRegistry
|
||||||
|
|
||||||
|
Create a typed component registry and action helpers from a catalog.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const { registry, handlers, executeAction } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: (renderProps) => <div>{renderProps.children}</div>,
|
||||||
|
Button: (renderProps) => (
|
||||||
|
<button onClick={() => renderProps.emit("press")}>
|
||||||
|
{renderProps.element.props.label as string}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
submit: async (params, setState, state) => {
|
||||||
|
// custom action logic
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Renderer
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Renderer spec={spec} registry={registry} loading={false} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Renders a `Spec` tree using your registry.
|
||||||
|
|
||||||
|
### createRenderer
|
||||||
|
|
||||||
|
Build an app-level renderer from catalog + components:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const AppRenderer = createRenderer(catalog, {
|
||||||
|
Card: (renderProps) => <div>{renderProps.children}</div>,
|
||||||
|
});
|
||||||
|
|
||||||
|
<AppRenderer spec={spec} state={{}} onAction={(name, params) => {}} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
- `useStateStore()`
|
||||||
|
- `useStateValue(path)` - returns an accessor
|
||||||
|
- `useStateBinding(path)` - returns `[Accessor<T | undefined>, setValue]`
|
||||||
|
- `useVisibility()` / `useIsVisible(condition)`
|
||||||
|
- `useActions()` / `useAction(binding)`
|
||||||
|
- `useValidation()` / `useOptionalValidation()`
|
||||||
|
- `useFieldValidation(path, config)` - returns accessor-backed `state`, `errors`, and `isValid`
|
||||||
|
- `useBoundProp(value, bindingPath)`
|
||||||
|
- `useUIStream(options)`
|
||||||
|
- `useChatUI(options)`
|
||||||
|
|
||||||
|
## Built-in Actions
|
||||||
|
|
||||||
|
`ActionProvider` handles these built-in actions:
|
||||||
|
|
||||||
|
- `setState`
|
||||||
|
- `pushState`
|
||||||
|
- `removeState`
|
||||||
|
- `validateForm`
|
||||||
|
|
||||||
|
## Component Props
|
||||||
|
|
||||||
|
Registry components receive:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ComponentRenderProps<P = Record<string, unknown>> {
|
||||||
|
element: UIElement<string, P>;
|
||||||
|
children?: JSX.Element;
|
||||||
|
emit: (event: string) => void;
|
||||||
|
on: (event: string) => EventHandle;
|
||||||
|
bindings?: Record<string, string>;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `emit("event")` to dispatch event bindings. Use `on("event")` to access `EventHandle` metadata (`bound`, `shouldPreventDefault`, `emit`).
|
||||||
|
|
||||||
|
## Reactivity Notes
|
||||||
|
|
||||||
|
- Keep changing reads in JSX expressions, `createMemo`, or `createEffect`.
|
||||||
|
- Avoid props destructuring in component signatures when you need live updates.
|
||||||
|
- `StateProvider` and other contexts expose getter-backed values so consumers read live signals.
|
||||||
|
- `useStateValue`, `useStateBinding`, and `useFieldValidation` expose reactive accessors; call them as functions.
|
||||||
@@ -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>
|
||||||
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { pageMetadata } from "@/lib/page-metadata"
|
import { pageMetadata } from "@/lib/page-metadata";
|
||||||
export const metadata = pageMetadata("docs/renderers")
|
export const metadata = pageMetadata("docs/renderers");
|
||||||
|
|
||||||
# Renderers
|
# Renderers
|
||||||
|
|
||||||
@@ -22,37 +22,65 @@ All renderers share the same workflow:
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>React</td>
|
<td>React</td>
|
||||||
<td><code>@json-render/react</code></td>
|
<td>
|
||||||
|
<code>@json-render/react</code>
|
||||||
|
</td>
|
||||||
<td>React component tree</td>
|
<td>React component tree</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Vue</td>
|
<td>Vue</td>
|
||||||
<td><code>@json-render/vue</code></td>
|
<td>
|
||||||
|
<code>@json-render/vue</code>
|
||||||
|
</td>
|
||||||
<td>Vue 3 component tree</td>
|
<td>Vue 3 component tree</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Svelte</td>
|
||||||
|
<td>
|
||||||
|
<code>@json-render/svelte</code>
|
||||||
|
</td>
|
||||||
|
<td>Svelte 5 component tree</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Solid</td>
|
||||||
|
<td>
|
||||||
|
<code>@json-render/solid</code>
|
||||||
|
</td>
|
||||||
|
<td>SolidJS component tree</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>shadcn/ui</td>
|
<td>shadcn/ui</td>
|
||||||
<td><code>@json-render/shadcn</code></td>
|
<td>
|
||||||
|
<code>@json-render/shadcn</code>
|
||||||
|
</td>
|
||||||
<td>Pre-built Radix UI + Tailwind components (uses React renderer)</td>
|
<td>Pre-built Radix UI + Tailwind components (uses React renderer)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>React Native</td>
|
<td>React Native</td>
|
||||||
<td><code>@json-render/react-native</code></td>
|
<td>
|
||||||
|
<code>@json-render/react-native</code>
|
||||||
|
</td>
|
||||||
<td>Native mobile views</td>
|
<td>Native mobile views</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Image</td>
|
<td>Image</td>
|
||||||
<td><code>@json-render/image</code></td>
|
<td>
|
||||||
|
<code>@json-render/image</code>
|
||||||
|
</td>
|
||||||
<td>SVG / PNG (via Satori)</td>
|
<td>SVG / PNG (via Satori)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>React PDF</td>
|
<td>React PDF</td>
|
||||||
<td><code>@json-render/react-pdf</code></td>
|
<td>
|
||||||
|
<code>@json-render/react-pdf</code>
|
||||||
|
</td>
|
||||||
<td>PDF documents</td>
|
<td>PDF documents</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Remotion</td>
|
<td>Remotion</td>
|
||||||
<td><code>@json-render/remotion</code></td>
|
<td>
|
||||||
|
<code>@json-render/remotion</code>
|
||||||
|
</td>
|
||||||
<td>Video compositions</td>
|
<td>Video compositions</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -67,7 +95,7 @@ import { defineRegistry, Renderer } from "@json-render/react";
|
|||||||
import { schema } from "@json-render/react/schema";
|
import { schema } from "@json-render/react/schema";
|
||||||
|
|
||||||
const { registry } = defineRegistry(catalog, { components });
|
const { registry } = defineRegistry(catalog, { components });
|
||||||
<Renderer spec={spec} registry={registry} />
|
<Renderer spec={spec} registry={registry} />;
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `StateProvider`, `VisibilityProvider`, and `ActionProvider` for full interactivity. See the [@json-render/react API reference](/docs/api/react) for details.
|
Use `StateProvider`, `VisibilityProvider`, and `ActionProvider` for full interactivity. See the [@json-render/react API reference](/docs/api/react) for details.
|
||||||
@@ -90,6 +118,42 @@ const { registry } = defineRegistry(catalog, {
|
|||||||
|
|
||||||
Uses composables (`useStateStore`, `useStateBinding`, `useActions`, etc.) instead of React hooks. See the [@json-render/vue API reference](/docs/api/vue) for details.
|
Uses composables (`useStateStore`, `useStateBinding`, `useActions`, etc.) instead of React hooks. See the [@json-render/vue API reference](/docs/api/vue) for details.
|
||||||
|
|
||||||
|
## Svelte
|
||||||
|
|
||||||
|
Svelte 5 renderer with runes-compatible context helpers, visibility conditions, actions, and streaming support.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/svelte";
|
||||||
|
import { schema } from "@json-render/svelte/schema";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: ({ props, children }) => /* Svelte snippet */,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [@json-render/svelte API reference](/docs/api/svelte) for details.
|
||||||
|
|
||||||
|
## Solid
|
||||||
|
|
||||||
|
SolidJS renderer with fine-grained reactivity, state bindings, validation, visibility, and event-driven actions.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { defineRegistry, Renderer } from "@json-render/solid";
|
||||||
|
import { schema } from "@json-render/solid/schema";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
Card: (renderProps) => <div>{renderProps.children}</div>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
<Renderer spec={spec} registry={registry} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [@json-render/solid API reference](/docs/api/solid) for details.
|
||||||
|
|
||||||
## shadcn/ui
|
## shadcn/ui
|
||||||
|
|
||||||
36 pre-built components using Radix UI and Tailwind CSS. Built on top of `@json-render/react` -- no custom renderer needed.
|
36 pre-built components using Radix UI and Tailwind CSS. Built on top of `@json-render/react` -- no custom renderer needed.
|
||||||
@@ -125,7 +189,10 @@ Render specs as native mobile views. Includes 25+ standard components and standa
|
|||||||
```tsx
|
```tsx
|
||||||
import { defineCatalog } from "@json-render/core";
|
import { defineCatalog } from "@json-render/core";
|
||||||
import { schema } from "@json-render/react-native/schema";
|
import { schema } from "@json-render/react-native/schema";
|
||||||
import { standardComponentDefinitions, standardActionDefinitions } from "@json-render/react-native/catalog";
|
import {
|
||||||
|
standardComponentDefinitions,
|
||||||
|
standardActionDefinitions,
|
||||||
|
} from "@json-render/react-native/catalog";
|
||||||
import { defineRegistry, Renderer } from "@json-render/react-native";
|
import { defineRegistry, Renderer } from "@json-render/react-native";
|
||||||
|
|
||||||
const catalog = defineCatalog(schema, {
|
const catalog = defineCatalog(schema, {
|
||||||
@@ -134,7 +201,7 @@ const catalog = defineCatalog(schema, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { registry } = defineRegistry(catalog, { components: {} });
|
const { registry } = defineRegistry(catalog, { components: {} });
|
||||||
<Renderer spec={spec} registry={registry} />
|
<Renderer spec={spec} registry={registry} />;
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [@json-render/react-native API reference](/docs/api/react-native) for details.
|
See the [@json-render/react-native API reference](/docs/api/react-native) for details.
|
||||||
@@ -159,7 +226,11 @@ See the [@json-render/image API reference](/docs/api/image) for details.
|
|||||||
Generate PDF documents from JSON specs using `@react-pdf/renderer`. Render to buffer, stream, or file.
|
Generate PDF documents from JSON specs using `@react-pdf/renderer`. Render to buffer, stream, or file.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { renderToBuffer, renderToStream, renderToFile } from "@json-render/react-pdf";
|
import {
|
||||||
|
renderToBuffer,
|
||||||
|
renderToStream,
|
||||||
|
renderToFile,
|
||||||
|
} from "@json-render/react-pdf";
|
||||||
|
|
||||||
const buffer = await renderToBuffer(spec);
|
const buffer = await renderToBuffer(spec);
|
||||||
const stream = await renderToStream(spec);
|
const stream = await renderToStream(spec);
|
||||||
@@ -185,7 +256,7 @@ import { Renderer } from "@json-render/remotion";
|
|||||||
fps={spec.composition.fps}
|
fps={spec.composition.fps}
|
||||||
compositionWidth={spec.composition.width}
|
compositionWidth={spec.composition.width}
|
||||||
compositionHeight={spec.composition.height}
|
compositionHeight={spec.composition.height}
|
||||||
/>
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
Uses a timeline spec format with compositions, tracks, and clips. Includes standard components (TitleCard, TypingText, ImageSlide, etc.), transitions (fade, slide, zoom, wipe), and effects.
|
Uses a timeline spec format with compositions, tracks, and clips. Includes standard components (TitleCard, TypingText, ImageSlide, etc.), transitions (fade, slide, zoom, wipe), and effects.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { pageMetadata } from "@/lib/page-metadata"
|
import { pageMetadata } from "@/lib/page-metadata";
|
||||||
|
|
||||||
export const metadata = pageMetadata("docs/skills")
|
export const metadata = pageMetadata("docs/skills");
|
||||||
|
|
||||||
# Skills
|
# Skills
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ json-render ships with skills that teach AI coding agents how to use each packag
|
|||||||
- **remotion** — Remotion renderer for video generation from JSON timeline specs.
|
- **remotion** — Remotion renderer for video generation from JSON timeline specs.
|
||||||
- **vue** — Vue 3 renderer for Vue component trees.
|
- **vue** — Vue 3 renderer for Vue component trees.
|
||||||
- **svelte** — Svelte 5 renderer for Svelte component trees.
|
- **svelte** — Svelte 5 renderer for Svelte component trees.
|
||||||
|
- **solid** — SolidJS renderer for fine-grained reactive component trees.
|
||||||
- **codegen** — Code generation utilities for building custom exporters.
|
- **codegen** — Code generation utilities for building custom exporters.
|
||||||
- **mcp** — MCP Apps integration for Claude, ChatGPT, Cursor, and VS Code.
|
- **mcp** — MCP Apps integration for Claude, ChatGPT, Cursor, and VS Code.
|
||||||
- **redux** — Redux adapter for json-render's `StateStore` interface.
|
- **redux** — Redux adapter for json-render's `StateStore` interface.
|
||||||
@@ -38,6 +39,7 @@ npx skills add vercel-labs/json-render --skill image
|
|||||||
npx skills add vercel-labs/json-render --skill remotion
|
npx skills add vercel-labs/json-render --skill remotion
|
||||||
npx skills add vercel-labs/json-render --skill vue
|
npx skills add vercel-labs/json-render --skill vue
|
||||||
npx skills add vercel-labs/json-render --skill svelte
|
npx skills add vercel-labs/json-render --skill svelte
|
||||||
|
npx skills add vercel-labs/json-render --skill solid
|
||||||
npx skills add vercel-labs/json-render --skill codegen
|
npx skills add vercel-labs/json-render --skill codegen
|
||||||
npx skills add vercel-labs/json-render --skill mcp
|
npx skills add vercel-labs/json-render --skill mcp
|
||||||
npx skills add vercel-labs/json-render --skill redux
|
npx skills add vercel-labs/json-render --skill redux
|
||||||
@@ -88,6 +90,10 @@ Teaches agents how to render JSON specs as Vue 3 component trees. Covers the Vue
|
|||||||
|
|
||||||
Teaches agents how to render JSON specs as Svelte 5 component trees. Covers the Svelte renderer API and component registration.
|
Teaches agents how to render JSON specs as Svelte 5 component trees. Covers the Svelte renderer API and component registration.
|
||||||
|
|
||||||
|
## solid
|
||||||
|
|
||||||
|
Teaches agents how to render JSON specs as SolidJS component trees. Covers Solid-specific reactive patterns, provider wiring, bindings, actions, and streaming.
|
||||||
|
|
||||||
## codegen
|
## codegen
|
||||||
|
|
||||||
Teaches agents how to use code generation utilities to export UI specs as framework-specific source code. Covers the codegen pipeline and custom exporter creation.
|
Teaches agents how to use code generation utilities to export UI specs as framework-specific source code. Covers the codegen pipeline and custom exporter creation.
|
||||||
|
|||||||
@@ -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
|
GitHub repository: https://github.com/vercel-labs/json-render
|
||||||
Documentation: https://json-render.dev/docs
|
Documentation: https://json-render.dev/docs
|
||||||
npm packages: @json-render/core, @json-render/react, @json-render/vue, @json-render/svelte, @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
|
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, image, remotion, vue, svelte, codegen, mcp, redux, zustand, jotai, xstate. See /docs/skills for details.
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,73 @@
|
|||||||
import { streamText } from "ai";
|
import { streamText } from "ai";
|
||||||
import { headers } from "next/headers";
|
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 { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit";
|
||||||
import { playgroundCatalog } from "@/lib/render/catalog";
|
import { playgroundCatalog } from "@/lib/render/catalog";
|
||||||
|
|
||||||
export const maxDuration = 30;
|
export const maxDuration = 30;
|
||||||
|
|
||||||
const SYSTEM_PROMPT = playgroundCatalog.prompt({
|
const PLAYGROUND_RULES = [
|
||||||
customRules: [
|
"NEVER use viewport height classes (min-h-screen, h-screen) - the UI renders inside a fixed-size container.",
|
||||||
"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.",
|
||||||
"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 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.",
|
||||||
"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.",
|
||||||
"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.",
|
||||||
"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" }).',
|
||||||
'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 MAX_PROMPT_LENGTH = 500;
|
||||||
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
|
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) {
|
export async function POST(req: Request) {
|
||||||
// Get client IP for rate limiting
|
|
||||||
const headersList = await headers();
|
const headersList = await headers();
|
||||||
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
|
const ip = headersList.get("x-forwarded-for")?.split(",")[0] ?? "anonymous";
|
||||||
|
|
||||||
// Check rate limits (minute and daily)
|
|
||||||
const [minuteResult, dailyResult] = await Promise.all([
|
const [minuteResult, dailyResult] = await Promise.all([
|
||||||
minuteRateLimit.limit(ip),
|
minuteRateLimit.limit(ip),
|
||||||
dailyRateLimit.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({
|
const systemPrompt = getSystemPrompt(isYaml, editModes);
|
||||||
prompt,
|
const userPrompt = isYaml
|
||||||
currentSpec: context?.previousSpec,
|
? buildYamlUserPrompt(prompt, context?.previousSpec, editModes)
|
||||||
maxPromptLength: MAX_PROMPT_LENGTH,
|
: buildUserPrompt({
|
||||||
});
|
prompt,
|
||||||
|
currentSpec: context?.previousSpec,
|
||||||
|
maxPromptLength: MAX_PROMPT_LENGTH,
|
||||||
|
editModes,
|
||||||
|
});
|
||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
|
model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
|
||||||
system: SYSTEM_PROMPT,
|
system: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: systemPrompt,
|
||||||
|
providerOptions: {
|
||||||
|
anthropic: { cacheControl: { type: "ephemeral" } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
prompt: userPrompt,
|
prompt: userPrompt,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stream the text, then append token usage metadata at the end
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const textStream = result.textStream;
|
const textStream = result.textStream;
|
||||||
|
|
||||||
@@ -72,7 +125,6 @@ export async function POST(req: Request) {
|
|||||||
for await (const chunk of textStream) {
|
for await (const chunk of textStream) {
|
||||||
controller.enqueue(encoder.encode(chunk));
|
controller.enqueue(encoder.encode(chunk));
|
||||||
}
|
}
|
||||||
// Append usage metadata after stream completes
|
|
||||||
try {
|
try {
|
||||||
const usage = await result.usage;
|
const usage = await result.usage;
|
||||||
const meta = JSON.stringify({
|
const meta = JSON.stringify({
|
||||||
@@ -80,10 +132,12 @@ export async function POST(req: Request) {
|
|||||||
promptTokens: usage.inputTokens,
|
promptTokens: usage.inputTokens,
|
||||||
completionTokens: usage.outputTokens,
|
completionTokens: usage.outputTokens,
|
||||||
totalTokens: usage.totalTokens,
|
totalTokens: usage.totalTokens,
|
||||||
|
cachedTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
|
||||||
|
cacheWriteTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
|
||||||
});
|
});
|
||||||
controller.enqueue(encoder.encode(`\n${meta}\n`));
|
controller.enqueue(encoder.encode(`\n${meta}\n`));
|
||||||
} catch {
|
} catch {
|
||||||
// Usage not available — skip silently
|
// Usage not available
|
||||||
}
|
}
|
||||||
controller.close();
|
controller.close();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ function getHighlighter() {
|
|||||||
if (!highlighterPromise) {
|
if (!highlighterPromise) {
|
||||||
highlighterPromise = createHighlighter({
|
highlighterPromise = createHighlighter({
|
||||||
themes: [vercelLightTheme, vercelDarkTheme],
|
themes: [vercelLightTheme, vercelDarkTheme],
|
||||||
langs: ["json", "tsx", "typescript"],
|
langs: ["json", "tsx", "typescript", "yaml"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return highlighterPromise;
|
return highlighterPromise;
|
||||||
@@ -158,7 +158,7 @@ if (typeof window !== "undefined") {
|
|||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
code: string;
|
code: string;
|
||||||
lang: "json" | "tsx" | "typescript";
|
lang: "json" | "tsx" | "typescript" | "yaml";
|
||||||
fillHeight?: boolean;
|
fillHeight?: boolean;
|
||||||
hideCopyButton?: boolean;
|
hideCopyButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
function Skeleton({ className = "" }: { className?: string }) {
|
function ScheduleItem({
|
||||||
return <div className={`rounded bg-muted-foreground/10 ${className}`} />;
|
time,
|
||||||
}
|
label,
|
||||||
|
color,
|
||||||
/** Simple form wireframe reused in both diagrams */
|
}: {
|
||||||
function FormUI() {
|
time: string;
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="border border-border rounded-lg p-2.5 space-y-1.5 bg-muted/30">
|
<div className="flex items-center gap-2 py-1.5 border-t border-border/50 first:border-t-0">
|
||||||
{/* Title */}
|
<span className="text-[10px] text-muted-foreground/60 w-12 shrink-0 tabular-nums">
|
||||||
<Skeleton className="h-2.5 w-14 mb-1" />
|
{time}
|
||||||
{/* Input fields */}
|
</span>
|
||||||
<Skeleton className="h-4 w-full rounded-sm" />
|
<span
|
||||||
<Skeleton className="h-4 w-full rounded-sm" />
|
className="w-1.5 h-1.5 rounded-full shrink-0"
|
||||||
{/* Submit button */}
|
style={{ background: color }}
|
||||||
<Skeleton className="h-4 w-16 rounded-sm bg-muted-foreground/20 mt-1" />
|
/>
|
||||||
|
<span className="text-[11px] text-muted-foreground">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -22,48 +26,164 @@ function FormUI() {
|
|||||||
function InlineModeDiagram() {
|
function InlineModeDiagram() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
<div className="text-sm font-medium text-foreground text-center mb-3">
|
||||||
Inline Mode
|
Inline Mode
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-col">
|
<div className="flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-col">
|
||||||
{/* Chat area */}
|
<div className="flex-1 p-4 space-y-3 overflow-hidden">
|
||||||
<div className="flex-1 p-3 overflow-hidden flex justify-center">
|
{/* User message */}
|
||||||
<div className="w-1/3 min-w-[120px] space-y-3">
|
<div className="flex justify-end">
|
||||||
{/* User message */}
|
<div className="bg-muted-foreground/20 rounded-2xl rounded-br px-3.5 py-2 max-w-[85%]">
|
||||||
<div className="flex justify-end">
|
<span className="text-[11px] text-foreground/80">
|
||||||
<div className="bg-muted rounded-xl px-3 py-2">
|
I have back-to-back meetings today
|
||||||
<Skeleton className="h-2 w-14" />
|
</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Assistant text */}
|
{/* AI response */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="space-y-1.5">
|
<p className="text-[11px] text-muted-foreground/70">
|
||||||
<Skeleton className="h-2 w-full" />
|
Packed day! Here's what you've got:
|
||||||
<Skeleton className="h-2 w-3/4" />
|
</p>
|
||||||
</div>
|
<div className="bg-muted/40 border border-border rounded-xl p-3 w-[92%]">
|
||||||
|
<div className="text-[11px] font-semibold text-foreground/80 mb-2">
|
||||||
{/* Inline UI */}
|
Today's Schedule
|
||||||
<FormUI />
|
|
||||||
|
|
||||||
{/* More text after UI */}
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<Skeleton className="h-2 w-4/5" />
|
|
||||||
</div>
|
</div>
|
||||||
|
<ScheduleItem
|
||||||
|
time="10:00 AM"
|
||||||
|
label="Design Review"
|
||||||
|
color="#7aa2f7"
|
||||||
|
/>
|
||||||
|
<ScheduleItem
|
||||||
|
time="1:00 PM"
|
||||||
|
label="Sprint Planning"
|
||||||
|
color="#bb9af7"
|
||||||
|
/>
|
||||||
|
<ScheduleItem
|
||||||
|
time="3:30 PM"
|
||||||
|
label="Team Standup"
|
||||||
|
color="#9ece6a"
|
||||||
|
/>
|
||||||
|
<ScheduleItem
|
||||||
|
time="4:30 PM"
|
||||||
|
label="Eng All-Hands"
|
||||||
|
color="#e0af68"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input bar */}
|
{/* Prompt input */}
|
||||||
<div className="p-2 flex justify-center">
|
<div className="p-2.5">
|
||||||
<div className="w-1/3 min-w-[120px] flex items-center gap-2">
|
<div className="bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5">
|
||||||
<Skeleton className="h-7 flex-1 rounded-md" />
|
<span className="text-[10px] text-muted-foreground/40 flex-1">
|
||||||
<Skeleton className="h-7 w-7 rounded-md" />
|
Message...
|
||||||
|
</span>
|
||||||
|
<div className="w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-2.5 h-2.5 text-muted-foreground/50"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
|
||||||
|
transform="rotate(-90 12 12)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-muted-foreground/60 mt-2 text-center">
|
<div className="mt-3 flex flex-col items-center gap-2">
|
||||||
Text + UI interleaved in messages
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
|
AI decides when UI beats text
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/50">
|
||||||
|
<span>AI chatbots</span>
|
||||||
|
<span className="text-muted-foreground/30">/</span>
|
||||||
|
<span>Copilots</span>
|
||||||
|
<span className="text-muted-foreground/30">/</span>
|
||||||
|
<span>Assistants</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LandingPage() {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col bg-muted/20">
|
||||||
|
{/* Nav */}
|
||||||
|
<div className="flex items-center justify-between px-3 py-2 border-b border-border/50">
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="2"
|
||||||
|
y="14"
|
||||||
|
width="5"
|
||||||
|
height="8"
|
||||||
|
rx="1"
|
||||||
|
className="fill-muted-foreground/60"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="9.5"
|
||||||
|
y="8"
|
||||||
|
width="5"
|
||||||
|
height="14"
|
||||||
|
rx="1"
|
||||||
|
className="fill-muted-foreground/60"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="17"
|
||||||
|
y="2"
|
||||||
|
width="5"
|
||||||
|
height="20"
|
||||||
|
rx="1"
|
||||||
|
className="fill-muted-foreground/60"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[9px] text-muted-foreground/50">Pricing</span>
|
||||||
|
<span className="text-[9px] text-muted-foreground/50">Blog</span>
|
||||||
|
<span className="text-[8px] font-semibold bg-foreground text-background rounded-md px-1.5 py-0.5 whitespace-nowrap">
|
||||||
|
Get Started
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center gap-2.5 text-center px-4">
|
||||||
|
<span className="text-[8px] font-medium text-green-400 bg-green-400/10 border border-green-400/15 rounded-full px-2 py-0.5">
|
||||||
|
Trusted by 2,000+ teams
|
||||||
|
</span>
|
||||||
|
<div className="text-sm font-bold text-foreground leading-tight">
|
||||||
|
Analytics that
|
||||||
|
<br />
|
||||||
|
move the needle
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-muted-foreground/50 leading-relaxed">
|
||||||
|
Ship what matters. No setup required.
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5 mt-1">
|
||||||
|
<button className="bg-foreground text-background text-[9px] font-semibold rounded-lg px-3 py-1.5 whitespace-nowrap">
|
||||||
|
Start Free
|
||||||
|
</button>
|
||||||
|
<button className="border border-border text-muted-foreground text-[9px] font-medium rounded-lg px-3 py-1.5 whitespace-nowrap">
|
||||||
|
Book Demo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex justify-center gap-3 py-2 border-t border-border/50">
|
||||||
|
<span className="text-[8px] text-muted-foreground/30">Privacy</span>
|
||||||
|
<span className="text-[8px] text-muted-foreground/30">Terms</span>
|
||||||
|
<span className="text-[8px] text-muted-foreground/30">Status</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -72,28 +192,65 @@ function InlineModeDiagram() {
|
|||||||
function StandaloneModeDiagram() {
|
function StandaloneModeDiagram() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="text-xs font-medium text-muted-foreground mb-3 text-center">
|
<div className="text-sm font-medium text-foreground text-center mb-3">
|
||||||
Standalone Mode
|
Standalone Mode
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 border border-border rounded-lg bg-background overflow-hidden flex flex-row">
|
<div className="flex-1 border border-border rounded-2xl bg-background overflow-hidden flex flex-row">
|
||||||
{/* Left panel - prompt */}
|
{/* Left panel - conversation */}
|
||||||
<div className="w-[38%] border-r border-border flex flex-col">
|
<div className="w-[38%] border-r border-border/50 flex flex-col">
|
||||||
<div className="flex-1" />
|
<div className="flex-1 p-3 space-y-2.5">
|
||||||
<div className="p-3 space-y-2">
|
{/* User message */}
|
||||||
<Skeleton className="h-7 w-full rounded-md" />
|
<div className="flex justify-end">
|
||||||
<Skeleton className="h-5 w-16 rounded-md" />
|
<div className="bg-muted-foreground/20 rounded-2xl rounded-br px-2.5 py-1.5">
|
||||||
|
<span className="text-[10px] text-foreground/80 block leading-relaxed">
|
||||||
|
A landing page for my analytics startup
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* AI text */}
|
||||||
|
<p className="text-[10px] text-muted-foreground/60 leading-relaxed">
|
||||||
|
Here's a clean landing page with a hero, nav, and CTAs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Prompt input */}
|
||||||
|
<div className="p-2.5">
|
||||||
|
<div className="bg-muted/50 border border-border rounded-xl px-2.5 py-1.5 flex items-center gap-1.5">
|
||||||
|
<span className="text-[10px] text-muted-foreground/40 flex-1">
|
||||||
|
Message...
|
||||||
|
</span>
|
||||||
|
<div className="w-5 h-5 rounded-md bg-muted-foreground/20 flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-2.5 h-2.5 text-muted-foreground/50"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"
|
||||||
|
transform="rotate(-90 12 12)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right panel - UI preview */}
|
{/* Right panel - landing page */}
|
||||||
<div className="flex-1 p-3 flex items-center justify-center">
|
<div className="flex-1">
|
||||||
<div className="w-3/4">
|
<LandingPage />
|
||||||
<FormUI />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-muted-foreground/60 mt-2 text-center">
|
<div className="mt-3 flex flex-col items-center gap-2">
|
||||||
Prompt separate from UI preview
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
|
Prompt in, UI out
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/50">
|
||||||
|
<span>Website builders</span>
|
||||||
|
<span className="text-muted-foreground/30">/</span>
|
||||||
|
<span>Text-to-widget</span>
|
||||||
|
<span className="text-muted-foreground/30">/</span>
|
||||||
|
<span>Dashboards</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -103,10 +260,10 @@ export function GenerationModesDiagram() {
|
|||||||
return (
|
return (
|
||||||
<div className="not-prose my-8">
|
<div className="not-prose my-8">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
<div className="h-[280px]">
|
<div className="h-[420px]">
|
||||||
<InlineModeDiagram />
|
<InlineModeDiagram />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[280px]">
|
<div className="h-[420px]">
|
||||||
<StandaloneModeDiagram />
|
<StandaloneModeDiagram />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function Header() {
|
|||||||
>
|
>
|
||||||
<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" />
|
<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>
|
</svg>
|
||||||
<span>11k</span>
|
<span>12k</span>
|
||||||
</a>
|
</a>
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useCallback, useRef, useMemo } from "react";
|
import { useEffect, useState, useCallback, useRef, useMemo } from "react";
|
||||||
import { flushSync } from "react-dom";
|
import { flushSync } from "react-dom";
|
||||||
import { useUIStream, type TokenUsage } from "@json-render/react";
|
|
||||||
import type { Spec } from "@json-render/core";
|
import type { Spec } from "@json-render/core";
|
||||||
import { collectUsedComponents, serializeProps } from "@json-render/codegen";
|
import { collectUsedComponents, serializeProps } from "@json-render/codegen";
|
||||||
import { toast } from "sonner";
|
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 {
|
import {
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
@@ -22,10 +28,10 @@ import { PlaygroundRenderer } from "@/lib/render/renderer";
|
|||||||
import { playgroundCatalog } from "@/lib/render/catalog";
|
import { playgroundCatalog } from "@/lib/render/catalog";
|
||||||
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
|
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 RenderView = "preview" | "code";
|
||||||
type MobileView =
|
type MobileView =
|
||||||
| "json"
|
| "spec"
|
||||||
| "nested"
|
| "nested"
|
||||||
| "stream"
|
| "stream"
|
||||||
| "catalog"
|
| "catalog"
|
||||||
@@ -40,6 +46,79 @@ interface Version {
|
|||||||
status: "generating" | "complete" | "error";
|
status: "generating" | "complete" | "error";
|
||||||
usage: TokenUsage | null;
|
usage: TokenUsage | null;
|
||||||
rawLines: string[];
|
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,
|
null,
|
||||||
);
|
);
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [activeTab, setActiveTab] = useState<Tab>("json");
|
const [activeTab, setActiveTab] = useState<Tab>("spec");
|
||||||
const [catalogSection, setCatalogSection] = useState<
|
const [catalogSection, setCatalogSection] = useState<
|
||||||
"components" | "actions"
|
"components" | "actions"
|
||||||
>("components");
|
>("components");
|
||||||
const [renderView, setRenderView] = useState<RenderView>("preview");
|
const [renderView, setRenderView] = useState<RenderView>("preview");
|
||||||
const [mobileView, setMobileView] = useState<MobileView>("preview");
|
const [mobileView, setMobileView] = useState<MobileView>("preview");
|
||||||
const [versionsSheetOpen, setVersionsSheetOpen] = useState(false);
|
const [versionsSheetOpen, setVersionsSheetOpen] = useState(false);
|
||||||
|
const [format, setFormat] = useState<StreamFormat>("jsonl");
|
||||||
|
const [editModes, setEditModes] = useState<EditMode[]>(["patch"]);
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const mobileInputRef = useRef<HTMLTextAreaElement>(null);
|
const mobileInputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const versionsEndRef = useRef<HTMLDivElement>(null);
|
const versionsEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -124,12 +205,13 @@ export function Playground() {
|
|||||||
rawLines: streamRawLines,
|
rawLines: streamRawLines,
|
||||||
send,
|
send,
|
||||||
clear,
|
clear,
|
||||||
} = useUIStream({
|
} = usePlaygroundStream({
|
||||||
api: "/api/generate",
|
api: "/api/generate",
|
||||||
|
format,
|
||||||
|
editModes,
|
||||||
onError: (err: Error) => {
|
onError: (err: Error) => {
|
||||||
console.error("Generation error:", err);
|
console.error("Generation error:", err);
|
||||||
toast.error(err.message || "Generation failed. Please try again.");
|
toast.error(err.message || "Generation failed. Please try again.");
|
||||||
// Mark the version as errored
|
|
||||||
if (generatingVersionIdRef.current) {
|
if (generatingVersionIdRef.current) {
|
||||||
const erroredVersionId = generatingVersionIdRef.current;
|
const erroredVersionId = generatingVersionIdRef.current;
|
||||||
setVersions((prev) =>
|
setVersions((prev) =>
|
||||||
@@ -140,7 +222,7 @@ export function Playground() {
|
|||||||
generatingVersionIdRef.current = null;
|
generatingVersionIdRef.current = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} as Parameters<typeof useUIStream>[0]);
|
});
|
||||||
|
|
||||||
// Get the selected version
|
// Get the selected version
|
||||||
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
|
const selectedVersion = versions.find((v) => v.id === selectedVersionId);
|
||||||
@@ -215,6 +297,7 @@ export function Playground() {
|
|||||||
status: "generating",
|
status: "generating",
|
||||||
usage: null,
|
usage: null,
|
||||||
rawLines: [],
|
rawLines: [],
|
||||||
|
format,
|
||||||
};
|
};
|
||||||
|
|
||||||
generatingVersionIdRef.current = newVersionId;
|
generatingVersionIdRef.current = newVersionId;
|
||||||
@@ -224,7 +307,7 @@ export function Playground() {
|
|||||||
|
|
||||||
// Pass the current tree as context so the API can iterate on it
|
// Pass the current tree as context so the API can iterate on it
|
||||||
await send(inputValue.trim(), { previousSpec: currentTreeRef.current });
|
await send(inputValue.trim(), { previousSpec: currentTreeRef.current });
|
||||||
}, [inputValue, isStreaming, send]);
|
}, [inputValue, isStreaming, send, format]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
@@ -250,9 +333,16 @@ export function Playground() {
|
|||||||
[selectedVersionId, isStreaming],
|
[selectedVersionId, isStreaming],
|
||||||
);
|
);
|
||||||
|
|
||||||
const jsonCode = currentTree
|
const specCode = useMemo(() => {
|
||||||
? JSON.stringify(currentTree, null, 2)
|
if (!currentTree)
|
||||||
: "// waiting...";
|
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(() => {
|
const nestedCode = useMemo(() => {
|
||||||
if (!currentTree || !currentTree.root) return "// waiting...";
|
if (!currentTree || !currentTree.root) return "// waiting...";
|
||||||
@@ -275,7 +365,7 @@ export function Playground() {
|
|||||||
const componentName = element.type;
|
const componentName = element.type;
|
||||||
|
|
||||||
const propsObj: Record<string, unknown> = {};
|
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) {
|
if (v !== null && v !== undefined) {
|
||||||
propsObj[k] = v;
|
propsObj[k] = v;
|
||||||
}
|
}
|
||||||
@@ -321,6 +411,15 @@ ${jsx}
|
|||||||
}`;
|
}`;
|
||||||
}, [currentTree]);
|
}, [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
|
// Chat pane content
|
||||||
const chatPane = (
|
const chatPane = (
|
||||||
<div className="h-full flex flex-col border-t border-border">
|
<div className="h-full flex flex-col border-t border-border">
|
||||||
@@ -392,9 +491,15 @@ ${jsx}
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{version.usage && (
|
{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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -425,20 +530,18 @@ ${jsx}
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between items-center mt-2">
|
<div className="flex justify-between items-center mt-2">
|
||||||
{versions.length > 0 ? (
|
<PlaygroundControls
|
||||||
<button
|
format={format}
|
||||||
onClick={() => {
|
setFormat={setFormat}
|
||||||
setVersions([]);
|
editModes={editModes}
|
||||||
setSelectedVersionId(null);
|
setEditModes={setEditModes}
|
||||||
clear();
|
showClear={versions.length > 0}
|
||||||
}}
|
onClear={() => {
|
||||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
setVersions([]);
|
||||||
>
|
setSelectedVersionId(null);
|
||||||
Clear
|
clear();
|
||||||
</button>
|
}}
|
||||||
) : (
|
/>
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
{isStreaming ? (
|
{isStreaming ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => clear()}
|
onClick={() => clear()}
|
||||||
@@ -492,18 +595,18 @@ ${jsx}
|
|||||||
const copyText =
|
const copyText =
|
||||||
activeTab === "stream"
|
activeTab === "stream"
|
||||||
? currentRawLines.join("\n")
|
? currentRawLines.join("\n")
|
||||||
: activeTab === "json"
|
: activeTab === "spec"
|
||||||
? jsonCode
|
? specCode
|
||||||
: activeTab === "nested"
|
: activeTab === "nested"
|
||||||
? nestedCode
|
? nestedCode
|
||||||
: activeTab === "visual"
|
: activeTab === "visual"
|
||||||
? jsonCode
|
? specCode
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const codePane = (
|
const codePane = (
|
||||||
<div className="h-full flex flex-col border-t border-border">
|
<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">
|
<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) => (
|
(tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
@@ -514,7 +617,7 @@ ${jsx}
|
|||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab === "spec" ? (format === "yaml" ? "yaml" : "json") : tab}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
@@ -679,7 +782,7 @@ ${jsx}
|
|||||||
currentRawLines.length > 0 ? (
|
currentRawLines.length > 0 ? (
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={currentRawLines.join("\n")}
|
code={currentRawLines.join("\n")}
|
||||||
lang="json"
|
lang={streamLang}
|
||||||
fillHeight
|
fillHeight
|
||||||
hideCopyButton
|
hideCopyButton
|
||||||
/>
|
/>
|
||||||
@@ -691,7 +794,12 @@ ${jsx}
|
|||||||
) : activeTab === "nested" ? (
|
) : activeTab === "nested" ? (
|
||||||
<CodeBlock code={nestedCode} lang="json" fillHeight hideCopyButton />
|
<CodeBlock code={nestedCode} lang="json" fillHeight hideCopyButton />
|
||||||
) : (
|
) : (
|
||||||
<CodeBlock code={jsonCode} lang="json" fillHeight hideCopyButton />
|
<CodeBlock
|
||||||
|
code={specCode}
|
||||||
|
lang={specLang}
|
||||||
|
fillHeight
|
||||||
|
hideCopyButton
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -790,7 +898,7 @@ ${jsx}
|
|||||||
: 0}
|
: 0}
|
||||||
</button>
|
</button>
|
||||||
{/* Code tabs */}
|
{/* Code tabs */}
|
||||||
{(["json", "visual", "nested", "stream", "catalog"] as const).map(
|
{(["spec", "visual", "nested", "stream", "catalog"] as const).map(
|
||||||
(tab) => (
|
(tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
@@ -801,7 +909,7 @@ ${jsx}
|
|||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab === "spec" ? (format === "yaml" ? "yaml" : "json") : tab}
|
||||||
</button>
|
</button>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
@@ -982,7 +1090,7 @@ ${jsx}
|
|||||||
currentRawLines.length > 0 ? (
|
currentRawLines.length > 0 ? (
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={currentRawLines.join("\n")}
|
code={currentRawLines.join("\n")}
|
||||||
lang="json"
|
lang={streamLang}
|
||||||
fillHeight
|
fillHeight
|
||||||
hideCopyButton
|
hideCopyButton
|
||||||
/>
|
/>
|
||||||
@@ -998,8 +1106,13 @@ ${jsx}
|
|||||||
fillHeight
|
fillHeight
|
||||||
hideCopyButton
|
hideCopyButton
|
||||||
/>
|
/>
|
||||||
) : mobileView === "json" ? (
|
) : mobileView === "spec" ? (
|
||||||
<CodeBlock code={jsonCode} lang="json" fillHeight hideCopyButton />
|
<CodeBlock
|
||||||
|
code={specCode}
|
||||||
|
lang={specLang}
|
||||||
|
fillHeight
|
||||||
|
hideCopyButton
|
||||||
|
/>
|
||||||
) : mobileView === "preview" ? (
|
) : mobileView === "preview" ? (
|
||||||
currentTree && currentTree.root ? (
|
currentTree && currentTree.root ? (
|
||||||
<div className="w-full min-h-full flex items-center justify-center p-6">
|
<div className="w-full min-h-full flex items-center justify-center p-6">
|
||||||
@@ -1075,20 +1188,18 @@ ${jsx}
|
|||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between items-center mt-2">
|
<div className="flex justify-between items-center mt-2">
|
||||||
{versions.length > 0 ? (
|
<PlaygroundControls
|
||||||
<button
|
format={format}
|
||||||
onClick={() => {
|
setFormat={setFormat}
|
||||||
setVersions([]);
|
editModes={editModes}
|
||||||
setSelectedVersionId(null);
|
setEditModes={setEditModes}
|
||||||
clear();
|
showClear={versions.length > 0}
|
||||||
}}
|
onClear={() => {
|
||||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
setVersions([]);
|
||||||
>
|
setSelectedVersionId(null);
|
||||||
Clear
|
clear();
|
||||||
</button>
|
}}
|
||||||
) : (
|
/>
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
{isStreaming ? (
|
{isStreaming ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => clear()}
|
onClick={() => clear()}
|
||||||
@@ -1165,9 +1276,16 @@ ${jsx}
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{version.usage && (
|
{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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const docsNavigation: NavSection[] = [
|
|||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Renders with Vite (Vue / React / Svelte)",
|
title: "Renders with Vite (Vue / React / Svelte / Solid)",
|
||||||
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
href: "https://github.com/vercel-labs/json-render/tree/main/examples/vite-renderers",
|
||||||
external: true,
|
external: true,
|
||||||
},
|
},
|
||||||
@@ -133,12 +133,18 @@ export const docsNavigation: NavSection[] = [
|
|||||||
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
{ title: "@json-render/remotion", href: "/docs/api/remotion" },
|
||||||
{ title: "@json-render/vue", href: "/docs/api/vue" },
|
{ title: "@json-render/vue", href: "/docs/api/vue" },
|
||||||
{ title: "@json-render/svelte", href: "/docs/api/svelte" },
|
{ 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/codegen", href: "/docs/api/codegen" },
|
||||||
{ title: "@json-render/mcp", href: "/docs/api/mcp" },
|
{ title: "@json-render/mcp", href: "/docs/api/mcp" },
|
||||||
{ title: "@json-render/redux", href: "/docs/api/redux" },
|
{ title: "@json-render/redux", href: "/docs/api/redux" },
|
||||||
{ title: "@json-render/zustand", href: "/docs/api/zustand" },
|
{ title: "@json-render/zustand", href: "/docs/api/zustand" },
|
||||||
{ title: "@json-render/jotai", href: "/docs/api/jotai" },
|
{ title: "@json-render/jotai", href: "/docs/api/jotai" },
|
||||||
{ title: "@json-render/xstate", href: "/docs/api/xstate" },
|
{ title: "@json-render/xstate", href: "/docs/api/xstate" },
|
||||||
|
{ title: "@json-render/yaml", href: "/docs/api/yaml" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/api/core": "@json-render/core API",
|
"docs/api/core": "@json-render/core API",
|
||||||
"docs/api/react": "@json-render/react API",
|
"docs/api/react": "@json-render/react API",
|
||||||
"docs/api/vue": "@json-render/vue API",
|
"docs/api/vue": "@json-render/vue API",
|
||||||
|
"docs/api/solid": "@json-render/solid API",
|
||||||
"docs/api/react-pdf": "@json-render/react-pdf API",
|
"docs/api/react-pdf": "@json-render/react-pdf API",
|
||||||
"docs/api/react-email": "@json-render/react-email API",
|
"docs/api/react-email": "@json-render/react-email API",
|
||||||
"docs/api/react-native": "@json-render/react-native API",
|
"docs/api/react-native": "@json-render/react-native API",
|
||||||
@@ -55,7 +56,9 @@ export const PAGE_TITLES: Record<string, string> = {
|
|||||||
"docs/api/redux": "@json-render/redux API",
|
"docs/api/redux": "@json-render/redux API",
|
||||||
"docs/api/zustand": "@json-render/zustand API",
|
"docs/api/zustand": "@json-render/zustand API",
|
||||||
"docs/api/jotai": "@json-render/jotai 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/xstate": "@json-render/xstate API",
|
||||||
|
"docs/api/yaml": "@json-render/yaml API",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"@json-render/codegen": "workspace:*",
|
"@json-render/codegen": "workspace:*",
|
||||||
"@json-render/core": "workspace:*",
|
"@json-render/core": "workspace:*",
|
||||||
"@json-render/react": "workspace:*",
|
"@json-render/react": "workspace:*",
|
||||||
|
"@json-render/yaml": "workspace:*",
|
||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
"@mdx-js/mdx": "^3.1.1",
|
"@mdx-js/mdx": "^3.1.1",
|
||||||
"@mdx-js/react": "^3.1.1",
|
"@mdx-js/react": "^3.1.1",
|
||||||
@@ -34,9 +35,9 @@
|
|||||||
"bash-tool": "1.3.14",
|
"bash-tool": "1.3.14",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"diff": "^8.0.3",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"geist": "1.7.0",
|
"geist": "1.7.0",
|
||||||
"just-bash": "2.9.6",
|
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"unist-util-visit": "5.1.0",
|
"unist-util-visit": "5.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
|
"yaml": "^2.8.2",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-chat
|
# 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
|
## 0.1.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-chat",
|
"name": "example-chat",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-dashboard
|
# 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
|
## 0.1.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-dashboard",
|
"name": "example-dashboard",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-image
|
# 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
|
## 0.1.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-image",
|
"name": "example-image",
|
||||||
"version": "0.1.3",
|
"version": "0.1.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
# example-mcp
|
# 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
|
## 0.1.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-mcp",
|
"name": "example-mcp",
|
||||||
"version": "0.1.2",
|
"version": "0.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# example-no-ai
|
# 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
|
## 0.1.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-no-ai",
|
"name": "example-no-ai",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-email
|
# 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
|
## 0.1.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-email",
|
"name": "example-react-email",
|
||||||
"version": "0.1.2",
|
"version": "0.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-native
|
# 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
|
## 0.1.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-native",
|
"name": "example-react-native",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-react-pdf
|
# 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
|
## 0.1.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-react-pdf",
|
"name": "example-react-pdf",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { defineCatalog } from "@json-render/core";
|
||||||
|
import type { ComputedFunction } from "@json-render/core";
|
||||||
|
import { schema, defineRegistry } from "@json-render/react";
|
||||||
|
import {
|
||||||
|
threeComponentDefinitions,
|
||||||
|
threeComponents,
|
||||||
|
ThreeCanvas,
|
||||||
|
} from "@json-render/react-three-fiber";
|
||||||
|
import { scenes } from "./scenes";
|
||||||
|
|
||||||
|
const catalog = defineCatalog(schema, {
|
||||||
|
components: {
|
||||||
|
...threeComponentDefinitions,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components: {
|
||||||
|
...threeComponents,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function num(v: unknown, fallback: number): number {
|
||||||
|
return typeof v === "number" ? v : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedFunctions: Record<string, ComputedFunction> = {
|
||||||
|
halfHeight: (a) => num(a.h, 1) / 2,
|
||||||
|
|
||||||
|
helixX: (a) => {
|
||||||
|
const angle =
|
||||||
|
(num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +
|
||||||
|
num(a.strand, 0) * Math.PI;
|
||||||
|
return num(a.radius, 1.8) * Math.cos(angle);
|
||||||
|
},
|
||||||
|
helixY: (a) => num(a.i, 0) * num(a.spacing, 0.3) + num(a.offset, -6),
|
||||||
|
helixZ: (a) => {
|
||||||
|
const angle =
|
||||||
|
(num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +
|
||||||
|
num(a.strand, 0) * Math.PI;
|
||||||
|
return num(a.radius, 1.8) * Math.sin(angle);
|
||||||
|
},
|
||||||
|
helixHue: (a) => {
|
||||||
|
const t = num(a.i, 0) / num(a.count, 40);
|
||||||
|
return `hsl(${Math.round(t * 270 + 180)}, 85%, 60%)`;
|
||||||
|
},
|
||||||
|
|
||||||
|
spiralX: (a) => {
|
||||||
|
const t = num(a.i, 0) / num(a.count, 60);
|
||||||
|
const r = 0.5 + t * num(a.maxRadius, 7);
|
||||||
|
const angle = t * num(a.turns, 4) * 2 * Math.PI;
|
||||||
|
return r * Math.cos(angle);
|
||||||
|
},
|
||||||
|
spiralZ: (a) => {
|
||||||
|
const t = num(a.i, 0) / num(a.count, 60);
|
||||||
|
const r = 0.5 + t * num(a.maxRadius, 7);
|
||||||
|
const angle = t * num(a.turns, 4) * 2 * Math.PI;
|
||||||
|
return r * Math.sin(angle);
|
||||||
|
},
|
||||||
|
spiralY: (a) => {
|
||||||
|
const i = num(a.i, 0);
|
||||||
|
return Math.sin(i * 1.7) * 0.4;
|
||||||
|
},
|
||||||
|
spiralScale: (a) => {
|
||||||
|
const t = num(a.i, 0) / num(a.count, 60);
|
||||||
|
return 0.08 + (1 - t) * 0.25;
|
||||||
|
},
|
||||||
|
spiralEmissive: (a) => {
|
||||||
|
const t = num(a.i, 0) / num(a.count, 60);
|
||||||
|
return Math.max(0, 1 - t * 1.5);
|
||||||
|
},
|
||||||
|
|
||||||
|
circleX: (a) => {
|
||||||
|
const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
|
||||||
|
return num(a.radius, 3) * Math.cos(angle);
|
||||||
|
},
|
||||||
|
circleZ: (a) => {
|
||||||
|
const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
|
||||||
|
return num(a.radius, 3) * Math.sin(angle);
|
||||||
|
},
|
||||||
|
circleY: (a) => {
|
||||||
|
const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
|
||||||
|
return Math.sin(angle * num(a.freq, 3)) * num(a.amp, 0.3);
|
||||||
|
},
|
||||||
|
circleAngle: (a) => {
|
||||||
|
return (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
|
||||||
|
},
|
||||||
|
circleHue: (a) => {
|
||||||
|
const t = num(a.i, 0) / num(a.count, 8);
|
||||||
|
return `hsl(${Math.round(t * 360)}, ${num(a.sat, 70)}%, ${num(a.lit, 80)}%)`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function highlightJson(json: string): string {
|
||||||
|
return json.replace(
|
||||||
|
/("(?:\\.|[^"\\])*")\s*(:)|("(?:\\.|[^"\\])*")|([-+]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b)|([{}[\]:,])/g,
|
||||||
|
(_, key, colon, str, num, lit, punct) => {
|
||||||
|
if (key) return `<span style="color:#FF4D8D">${key}</span>${colon}`;
|
||||||
|
if (str) return `<span style="color:#00CA50">${str}</span>`;
|
||||||
|
if (num) return `<span style="color:#47A8FF">${num}</span>`;
|
||||||
|
if (lit) return `<span style="color:#47A8FF">${lit}</span>`;
|
||||||
|
if (punct) return `<span style="color:#666">${punct}</span>`;
|
||||||
|
return _;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LIST_WIDTH = 220;
|
||||||
|
const JSON_WIDTH = 380;
|
||||||
|
const HEADER_HEIGHT = 40;
|
||||||
|
|
||||||
|
const headerStyle: React.CSSProperties = {
|
||||||
|
height: HEADER_HEIGHT,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "0 16px",
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#555",
|
||||||
|
letterSpacing: "0.08em",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
fontFamily: "ui-monospace, monospace",
|
||||||
|
borderBottom: "1px solid #1e1e1e",
|
||||||
|
flexShrink: 0,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
const selected = scenes[selectedIndex]!;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: "100vh", display: "flex", background: "#0a0a0a" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: LIST_WIDTH,
|
||||||
|
flexShrink: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
borderRight: "1px solid #1e1e1e",
|
||||||
|
background: "#0f0f0f",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={headerStyle}>Scenes</div>
|
||||||
|
<div style={{ flex: 1, overflowY: "auto", padding: "6px 0" }}>
|
||||||
|
{scenes.map((scene, i) => (
|
||||||
|
<button
|
||||||
|
key={scene.name}
|
||||||
|
onClick={() => setSelectedIndex(i)}
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
width: "100%",
|
||||||
|
padding: "8px 16px",
|
||||||
|
fontSize: 13,
|
||||||
|
border: "none",
|
||||||
|
textAlign: "left",
|
||||||
|
background:
|
||||||
|
i === selectedIndex
|
||||||
|
? "rgba(255,255,255,0.08)"
|
||||||
|
: "transparent",
|
||||||
|
color: i === selectedIndex ? "#fff" : "#888",
|
||||||
|
fontWeight: i === selectedIndex ? 500 : 400,
|
||||||
|
cursor: "pointer",
|
||||||
|
borderLeft:
|
||||||
|
i === selectedIndex
|
||||||
|
? "2px solid #fff"
|
||||||
|
: "2px solid transparent",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{scene.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={headerStyle}>{selected.description}</div>
|
||||||
|
<div style={{ flex: 1, background: "#000", position: "relative" }}>
|
||||||
|
<ThreeCanvas
|
||||||
|
key={selectedIndex}
|
||||||
|
spec={selected.spec}
|
||||||
|
registry={registry}
|
||||||
|
functions={computedFunctions}
|
||||||
|
shadows
|
||||||
|
camera={{ position: [0, 0, 5], fov: 50 }}
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: JSON_WIDTH,
|
||||||
|
flexShrink: 0,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
borderLeft: "1px solid #1e1e1e",
|
||||||
|
background: "#0d0d0d",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={headerStyle}>Spec JSON</div>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
margin: 0,
|
||||||
|
padding: 14,
|
||||||
|
overflowY: "auto",
|
||||||
|
overflowX: "auto",
|
||||||
|
fontSize: 11,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
fontFamily:
|
||||||
|
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace",
|
||||||
|
color: "#EDEDED",
|
||||||
|
tabSize: 2,
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: highlightJson(JSON.stringify(selected.spec, null, 2)),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
|
||||||
|
export type Scene = { name: string; description: string; spec: Spec };
|
||||||
|
|
||||||
|
export const PI = Math.PI;
|
||||||
|
|
||||||
|
export function range(n: number) {
|
||||||
|
return Array.from({ length: n }, (_, i) => ({ i }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const clockworkOrrery: Scene = {
|
||||||
|
name: "Clockwork Orrery",
|
||||||
|
description:
|
||||||
|
"Interlocking brass/gold rings spinning at different tilts and speeds with orbiting metallic planets. Like the inside of a celestial clockwork mechanism.",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"cam",
|
||||||
|
"env",
|
||||||
|
"ambient",
|
||||||
|
"key",
|
||||||
|
"warm",
|
||||||
|
"accent",
|
||||||
|
"center",
|
||||||
|
"ring-spin-0",
|
||||||
|
"ring-spin-1",
|
||||||
|
"ring-spin-2",
|
||||||
|
"ring-spin-3",
|
||||||
|
"planet-orbit-0",
|
||||||
|
"planet-orbit-1",
|
||||||
|
"planet-orbit-2",
|
||||||
|
"sparkles",
|
||||||
|
"post",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cam: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [0, 3, 8],
|
||||||
|
fov: 50,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
type: "Environment",
|
||||||
|
props: {
|
||||||
|
preset: "dawn",
|
||||||
|
background: true,
|
||||||
|
blur: 1,
|
||||||
|
intensity: 0.5,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#332200", intensity: 0.4 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
type: "DirectionalLight",
|
||||||
|
props: {
|
||||||
|
position: [5, 10, 5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffddaa",
|
||||||
|
intensity: 2,
|
||||||
|
castShadow: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
warm: {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffaa44",
|
||||||
|
intensity: 30,
|
||||||
|
distance: 15,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [-4, 3, -4],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffcc88",
|
||||||
|
intensity: 15,
|
||||||
|
distance: 15,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.5,
|
||||||
|
rotationIntensity: 0.1,
|
||||||
|
floatIntensity: 0.2,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["center-sphere"],
|
||||||
|
},
|
||||||
|
"center-sphere": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.5,
|
||||||
|
widthSegments: 32,
|
||||||
|
heightSegments: 16,
|
||||||
|
color: "#ffddaa",
|
||||||
|
transmission: 1,
|
||||||
|
thickness: 0.8,
|
||||||
|
roughness: 0,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.8,
|
||||||
|
distortion: 0.05,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 10,
|
||||||
|
resolution: 256,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"ring-spin-0": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [0.3, 0, 0.8],
|
||||||
|
scale: null,
|
||||||
|
speed: 1.2,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["ring-torus-0"],
|
||||||
|
},
|
||||||
|
"ring-torus-0": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#ffcc44",
|
||||||
|
metalness: 0.95,
|
||||||
|
roughness: 0.1,
|
||||||
|
emissive: "#cc9900",
|
||||||
|
emissiveIntensity: 0.4,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 2,
|
||||||
|
tube: 0.04,
|
||||||
|
radialSegments: null,
|
||||||
|
tubularSegments: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"ring-spin-1": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [0.9, 0, -0.2],
|
||||||
|
scale: null,
|
||||||
|
speed: -0.7,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["ring-torus-1"],
|
||||||
|
},
|
||||||
|
"ring-torus-1": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#cc8833",
|
||||||
|
metalness: 0.95,
|
||||||
|
roughness: 0.1,
|
||||||
|
emissive: "#aa6622",
|
||||||
|
emissiveIntensity: 0.3,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 3.2,
|
||||||
|
tube: 0.035,
|
||||||
|
radialSegments: null,
|
||||||
|
tubularSegments: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"ring-spin-2": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [-0.4, 0, 0.5],
|
||||||
|
scale: null,
|
||||||
|
speed: 0.4,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["ring-torus-2"],
|
||||||
|
},
|
||||||
|
"ring-torus-2": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#bb9944",
|
||||||
|
metalness: 0.95,
|
||||||
|
roughness: 0.1,
|
||||||
|
emissive: "#886622",
|
||||||
|
emissiveIntensity: 0.25,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 4.5,
|
||||||
|
tube: 0.03,
|
||||||
|
radialSegments: null,
|
||||||
|
tubularSegments: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"ring-spin-3": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [0.1, 0, -0.1],
|
||||||
|
scale: null,
|
||||||
|
speed: -0.2,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["ring-torus-3"],
|
||||||
|
},
|
||||||
|
"ring-torus-3": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#997744",
|
||||||
|
metalness: 0.95,
|
||||||
|
roughness: 0.1,
|
||||||
|
emissive: "#664422",
|
||||||
|
emissiveIntensity: 0.15,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 6,
|
||||||
|
tube: 0.025,
|
||||||
|
radialSegments: null,
|
||||||
|
tubularSegments: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"planet-orbit-0": {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.6,
|
||||||
|
radius: 2.5,
|
||||||
|
tilt: 0.3,
|
||||||
|
},
|
||||||
|
children: ["planet-spin-0"],
|
||||||
|
},
|
||||||
|
"planet-spin-0": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 3,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["planet-sphere-0"],
|
||||||
|
},
|
||||||
|
"planet-sphere-0": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#ffffff",
|
||||||
|
metalness: 1,
|
||||||
|
roughness: 0.02,
|
||||||
|
emissive: "#ffffff",
|
||||||
|
emissiveIntensity: 0.2,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.2,
|
||||||
|
widthSegments: 32,
|
||||||
|
heightSegments: 16,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"planet-orbit-1": {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -0.9,
|
||||||
|
radius: 3.8,
|
||||||
|
tilt: 0.8,
|
||||||
|
},
|
||||||
|
children: ["planet-spin-1"],
|
||||||
|
},
|
||||||
|
"planet-spin-1": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -2,
|
||||||
|
axis: "x",
|
||||||
|
},
|
||||||
|
children: ["planet-sphere-1"],
|
||||||
|
},
|
||||||
|
"planet-sphere-1": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#ffcc88",
|
||||||
|
metalness: 0.8,
|
||||||
|
roughness: 0.15,
|
||||||
|
emissive: "#ffcc88",
|
||||||
|
emissiveIntensity: 0.2,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.15,
|
||||||
|
widthSegments: 32,
|
||||||
|
heightSegments: 16,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"planet-orbit-2": {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.35,
|
||||||
|
radius: 5.2,
|
||||||
|
tilt: -0.5,
|
||||||
|
},
|
||||||
|
children: ["planet-spin-2"],
|
||||||
|
},
|
||||||
|
"planet-spin-2": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 1.5,
|
||||||
|
axis: "z",
|
||||||
|
},
|
||||||
|
children: ["planet-sphere-2"],
|
||||||
|
},
|
||||||
|
"planet-sphere-2": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#ccaa66",
|
||||||
|
metalness: 0.9,
|
||||||
|
roughness: 0.05,
|
||||||
|
emissive: "#ccaa66",
|
||||||
|
emissiveIntensity: 0.2,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.18,
|
||||||
|
widthSegments: 32,
|
||||||
|
heightSegments: 16,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sparkles: {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: [14, 10, 14],
|
||||||
|
count: 150,
|
||||||
|
speed: 0.15,
|
||||||
|
opacity: 0.3,
|
||||||
|
color: "#ffcc66",
|
||||||
|
size: 1,
|
||||||
|
noise: 2,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 1.2,
|
||||||
|
luminanceThreshold: 0.2,
|
||||||
|
luminanceSmoothing: null,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.5, darkness: 0.5 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: true,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: true,
|
||||||
|
enablePan: null,
|
||||||
|
enableRotate: true,
|
||||||
|
minDistance: 4,
|
||||||
|
maxDistance: 18,
|
||||||
|
minPolarAngle: 0.3,
|
||||||
|
maxPolarAngle: 1.5,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 0.3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,692 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const deepSeaAbyss: Scene = {
|
||||||
|
name: "Deep Sea Abyss",
|
||||||
|
description:
|
||||||
|
"Bioluminescent deep ocean. Pitch black with glowing jellyfish-like creatures.",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"cam",
|
||||||
|
"env",
|
||||||
|
"fog",
|
||||||
|
"ambient",
|
||||||
|
"jelly-1-pulse",
|
||||||
|
"jelly-2-pulse",
|
||||||
|
"jelly-3-pulse",
|
||||||
|
"light-1",
|
||||||
|
"light-2",
|
||||||
|
"light-3",
|
||||||
|
"tentacles-1",
|
||||||
|
"tentacles-2",
|
||||||
|
"tentacles-3",
|
||||||
|
"sparkles-1",
|
||||||
|
"sparkles-2",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cam: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [0, 1, 6],
|
||||||
|
fov: 55,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
type: "Environment",
|
||||||
|
props: {
|
||||||
|
preset: "night",
|
||||||
|
background: false,
|
||||||
|
blur: 1,
|
||||||
|
intensity: 0.05,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
fog: {
|
||||||
|
type: "Fog",
|
||||||
|
props: { color: "#000510", near: 2, far: 18 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#001133", intensity: 0.15 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"jelly-1-pulse": {
|
||||||
|
type: "Pulse",
|
||||||
|
props: {
|
||||||
|
position: [0, 1, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.6,
|
||||||
|
min: 0.85,
|
||||||
|
max: 1.15,
|
||||||
|
},
|
||||||
|
children: ["jelly-1-spin"],
|
||||||
|
},
|
||||||
|
"jelly-1-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.2,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["jelly-1-body"],
|
||||||
|
},
|
||||||
|
"jelly-1-body": {
|
||||||
|
type: "DistortSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 1,
|
||||||
|
widthSegments: 64,
|
||||||
|
heightSegments: 32,
|
||||||
|
color: "#00ccff",
|
||||||
|
speed: 2,
|
||||||
|
distort: 0.5,
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.2,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"jelly-2-pulse": {
|
||||||
|
type: "Pulse",
|
||||||
|
props: {
|
||||||
|
position: [-4, -1, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.8,
|
||||||
|
min: 0.8,
|
||||||
|
max: 1.2,
|
||||||
|
},
|
||||||
|
children: ["jelly-2-spin"],
|
||||||
|
},
|
||||||
|
"jelly-2-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -0.3,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["jelly-2-body"],
|
||||||
|
},
|
||||||
|
"jelly-2-body": {
|
||||||
|
type: "DistortSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.7,
|
||||||
|
widthSegments: 64,
|
||||||
|
heightSegments: 32,
|
||||||
|
color: "#cc44ff",
|
||||||
|
speed: 2.5,
|
||||||
|
distort: 0.6,
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.2,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"jelly-3-pulse": {
|
||||||
|
type: "Pulse",
|
||||||
|
props: {
|
||||||
|
position: [3, 0.5, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.5,
|
||||||
|
min: 0.9,
|
||||||
|
max: 1.1,
|
||||||
|
},
|
||||||
|
children: ["jelly-3-spin"],
|
||||||
|
},
|
||||||
|
"jelly-3-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.15,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["jelly-3-body"],
|
||||||
|
},
|
||||||
|
"jelly-3-body": {
|
||||||
|
type: "DistortSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.5,
|
||||||
|
widthSegments: 64,
|
||||||
|
heightSegments: 32,
|
||||||
|
color: "#ff44aa",
|
||||||
|
speed: 1.5,
|
||||||
|
distort: 0.4,
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"light-1": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#00ccff",
|
||||||
|
intensity: 20,
|
||||||
|
distance: 12,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"light-2": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [-4, -1, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#cc44ff",
|
||||||
|
intensity: 15,
|
||||||
|
distance: 10,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"light-3": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [3, 0.5, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ff44aa",
|
||||||
|
intensity: 10,
|
||||||
|
distance: 8,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"tentacles-1": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [0, -1, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.3,
|
||||||
|
rotationIntensity: 0.5,
|
||||||
|
floatIntensity: 0.8,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["t1-s1", "t1-s2", "t1-s3", "t1-s4", "t1-s5", "t1-s6"],
|
||||||
|
},
|
||||||
|
"t1-s1": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [0.3, -0.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#00ccff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t1-s2": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [-0.2, -1.2, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.1,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#00ccff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t1-s3": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [0.4, -1.9, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.08,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#00ccff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t1-s4": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [-0.3, -2.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.07,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#00ccff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t1-s5": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [0.1, -3.0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.06,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#00ccff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t1-s6": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [-0.1, -3.4, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.05,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#00ccff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"tentacles-2": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [-4, -2.5, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.4,
|
||||||
|
rotationIntensity: 0.5,
|
||||||
|
floatIntensity: 0.8,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["t2-s1", "t2-s2", "t2-s3", "t2-s4", "t2-s5"],
|
||||||
|
},
|
||||||
|
"t2-s1": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [0.3, -0.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.1,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#cc44ff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t2-s2": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [-0.2, -1.1, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.08,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#cc44ff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t2-s3": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [0.4, -1.7, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.06,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#cc44ff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t2-s4": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [-0.3, -2.2, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.05,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#cc44ff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t2-s5": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [0.1, -2.6, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.05,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#cc44ff",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"tentacles-3": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [3, -1, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.35,
|
||||||
|
rotationIntensity: 0.5,
|
||||||
|
floatIntensity: 0.8,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["t3-s1", "t3-s2", "t3-s3", "t3-s4"],
|
||||||
|
},
|
||||||
|
"t3-s1": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [0.3, -0.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.09,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#ff44aa",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t3-s2": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [-0.2, -1.0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.07,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#ff44aa",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t3-s3": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [0.4, -1.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.06,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#ff44aa",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"t3-s4": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: [-0.1, -1.9, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.05,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 8,
|
||||||
|
color: "#ff44aa",
|
||||||
|
transmission: 0.9,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"sparkles-1": {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: [15, 10, 15],
|
||||||
|
count: 300,
|
||||||
|
speed: 0.1,
|
||||||
|
opacity: 0.4,
|
||||||
|
color: "#00aaff",
|
||||||
|
size: 1,
|
||||||
|
noise: 3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"sparkles-2": {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, -2, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: [12, 6, 12],
|
||||||
|
count: 150,
|
||||||
|
speed: 0.05,
|
||||||
|
opacity: 0.2,
|
||||||
|
color: "#8844ff",
|
||||||
|
size: 0.8,
|
||||||
|
noise: 2,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: true,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: true,
|
||||||
|
enablePan: null,
|
||||||
|
enableRotate: true,
|
||||||
|
minDistance: 3,
|
||||||
|
maxDistance: 18,
|
||||||
|
minPolarAngle: null,
|
||||||
|
maxPolarAngle: null,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 0.15,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,940 @@
|
|||||||
|
import { PI } from "./_helpers";
|
||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const floatingIslands: Scene = {
|
||||||
|
name: "Floating Islands",
|
||||||
|
description:
|
||||||
|
"Sky, clouds, islands with trees and bridges floating in sunset light",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"cam",
|
||||||
|
"sky",
|
||||||
|
"ambient",
|
||||||
|
"sun",
|
||||||
|
"clouds",
|
||||||
|
"island-main-float",
|
||||||
|
"island-left-float",
|
||||||
|
"island-right-float",
|
||||||
|
"island-far-float",
|
||||||
|
"island-tiny1-float",
|
||||||
|
"island-tiny2-float",
|
||||||
|
"bridge-1",
|
||||||
|
"bridge-2",
|
||||||
|
"sparkles",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cam: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [8, 6, 14],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
fov: 50,
|
||||||
|
near: 0.1,
|
||||||
|
far: 500,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sky: {
|
||||||
|
type: "Environment",
|
||||||
|
props: {
|
||||||
|
preset: "dawn",
|
||||||
|
background: true,
|
||||||
|
blur: 0.8,
|
||||||
|
intensity: 0.5,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#ffd4a0", intensity: 0.5 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sun: {
|
||||||
|
type: "DirectionalLight",
|
||||||
|
props: {
|
||||||
|
position: [8, 12, 6],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffcc88",
|
||||||
|
intensity: 2.5,
|
||||||
|
castShadow: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
clouds: {
|
||||||
|
type: "Cloud",
|
||||||
|
props: {
|
||||||
|
position: [0, 4, -8],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
seed: 7,
|
||||||
|
segments: 40,
|
||||||
|
bounds: [25, 3, 20],
|
||||||
|
volume: 10,
|
||||||
|
speed: 0.1,
|
||||||
|
fade: 25,
|
||||||
|
opacity: 0.6,
|
||||||
|
color: "#ffe8d0",
|
||||||
|
growth: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"island-main-float": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.4,
|
||||||
|
rotationIntensity: 0.02,
|
||||||
|
floatIntensity: 0.3,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["island-main"],
|
||||||
|
},
|
||||||
|
"island-main": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"main-base",
|
||||||
|
"main-top",
|
||||||
|
"main-underhang",
|
||||||
|
"tree-1-trunk",
|
||||||
|
"tree-1-canopy",
|
||||||
|
"tree-2-trunk",
|
||||||
|
"tree-2-canopy",
|
||||||
|
"tree-3-trunk",
|
||||||
|
"tree-3-canopy",
|
||||||
|
"house-base",
|
||||||
|
"house-roof",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"main-base": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, -0.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#8B6914",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.9,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 3,
|
||||||
|
radiusBottom: 1.5,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 16,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"main-top": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.55, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#4a8c3f",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 3.1,
|
||||||
|
radiusBottom: 3,
|
||||||
|
height: 0.3,
|
||||||
|
radialSegments: 16,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"main-underhang": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, -2.5, 0],
|
||||||
|
rotation: [PI, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#6B4513",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 1.5,
|
||||||
|
height: 2.5,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"tree-1-trunk": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [1, 1, 0.5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#8B5A2B",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 1,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 0.06,
|
||||||
|
radiusBottom: 0.1,
|
||||||
|
height: 1.5,
|
||||||
|
radialSegments: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"tree-1-canopy": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [1, 2.2, 0.5],
|
||||||
|
rotation: null,
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#2d7a2d",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.6,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"tree-2-trunk": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-1.2, 0.8, -0.8],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#8B5A2B",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 1,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 0.05,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 1.1,
|
||||||
|
radialSegments: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"tree-2-canopy": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-1.2, 1.7, -0.8],
|
||||||
|
rotation: null,
|
||||||
|
scale: [1, 1.3, 1],
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#3a8c3a",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.45,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"tree-3-trunk": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-0.3, 1.2, 1.5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#8B5A2B",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 1,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 0.07,
|
||||||
|
radiusBottom: 0.12,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"tree-3-canopy": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-0.3, 2.7, 1.5],
|
||||||
|
rotation: null,
|
||||||
|
scale: [1.2, 1.4, 1.2],
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#1a6b1a",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.7,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"house-base": {
|
||||||
|
type: "RoundedBox",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.9, -0.5],
|
||||||
|
rotation: [0, 0.3, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#e8d8c0",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.8,
|
||||||
|
emissive: "#ffddaa",
|
||||||
|
emissiveIntensity: 0.05,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
width: 0.8,
|
||||||
|
height: 0.7,
|
||||||
|
depth: 0.7,
|
||||||
|
radius: 0.05,
|
||||||
|
smoothness: 4,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"house-roof": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, 1.55, -0.5],
|
||||||
|
rotation: [0, 1.085, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#a04030",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.8,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.6,
|
||||||
|
height: 0.5,
|
||||||
|
radialSegments: 4,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"island-left-float": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [-6, 0.1, 2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.6,
|
||||||
|
rotationIntensity: 0.03,
|
||||||
|
floatIntensity: 0.5,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["island-left"],
|
||||||
|
},
|
||||||
|
"island-left": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"left-base",
|
||||||
|
"left-top",
|
||||||
|
"left-under",
|
||||||
|
"left-tree-t",
|
||||||
|
"left-tree-c",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"left-base": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, -0.3, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#8B6914",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.9,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 1.8,
|
||||||
|
radiusBottom: 0.8,
|
||||||
|
height: 1.2,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"left-top": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.25, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#4a8c3f",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 1.85,
|
||||||
|
radiusBottom: 1.8,
|
||||||
|
height: 0.2,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"left-under": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, -1.5, 0],
|
||||||
|
rotation: [PI, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#6B4513",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.8,
|
||||||
|
height: 1.5,
|
||||||
|
radialSegments: 10,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"left-tree-t": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0.3, 0.8, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#8B5A2B",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 1,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 0.04,
|
||||||
|
radiusBottom: 0.07,
|
||||||
|
height: 1,
|
||||||
|
radialSegments: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"left-tree-c": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [0.3, 1.6, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: [1, 1.2, 1],
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#2d7a2d",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.4,
|
||||||
|
widthSegments: 12,
|
||||||
|
heightSegments: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"island-right-float": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [5.5, 0, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.5,
|
||||||
|
rotationIntensity: 0.02,
|
||||||
|
floatIntensity: 0.4,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["island-right"],
|
||||||
|
},
|
||||||
|
"island-right": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"right-base",
|
||||||
|
"right-top",
|
||||||
|
"right-under",
|
||||||
|
"right-tree-t",
|
||||||
|
"right-tree-c",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"right-base": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, -0.3, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#8B6914",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.9,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 2,
|
||||||
|
radiusBottom: 1,
|
||||||
|
height: 1.5,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"right-top": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.4, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#4a8c3f",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 2.05,
|
||||||
|
radiusBottom: 2,
|
||||||
|
height: 0.2,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"right-under": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, -2, 0],
|
||||||
|
rotation: [PI, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#6B4513",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 1,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 10,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"right-tree-t": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-0.5, 0.9, 0.3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#8B5A2B",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 1,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 0.05,
|
||||||
|
radiusBottom: 0.09,
|
||||||
|
height: 1.3,
|
||||||
|
radialSegments: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"right-tree-c": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-0.5, 2, 0.3],
|
||||||
|
rotation: null,
|
||||||
|
scale: [1, 1.3, 1],
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#3a8c3a",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.5,
|
||||||
|
widthSegments: 12,
|
||||||
|
heightSegments: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"island-far-float": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [2, 3, -10],
|
||||||
|
rotation: null,
|
||||||
|
scale: [0.7, 0.7, 0.7],
|
||||||
|
speed: 0.3,
|
||||||
|
rotationIntensity: 0.01,
|
||||||
|
floatIntensity: 0.3,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["far-base", "far-top", "far-under"],
|
||||||
|
},
|
||||||
|
"far-base": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#8B6914",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.9,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 2.5,
|
||||||
|
radiusBottom: 1.2,
|
||||||
|
height: 1.5,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"far-top": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.65, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#3a7a3a",
|
||||||
|
metalness: 0,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radiusTop: 2.55,
|
||||||
|
radiusBottom: 2.5,
|
||||||
|
height: 0.2,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"far-under": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, -1.8, 0],
|
||||||
|
rotation: [PI, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#6B4513",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.95,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 1.2,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 10,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"island-tiny1-float": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [-3, 2.5, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: [0.4, 0.4, 0.4],
|
||||||
|
speed: 0.8,
|
||||||
|
rotationIntensity: 0.05,
|
||||||
|
floatIntensity: 0.8,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["tiny1-rock"],
|
||||||
|
},
|
||||||
|
"tiny1-rock": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: [1, 0.6, 1],
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#8B6914",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.9,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 1,
|
||||||
|
widthSegments: 12,
|
||||||
|
heightSegments: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"island-tiny2-float": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [8, -0.5, 1],
|
||||||
|
rotation: null,
|
||||||
|
scale: [0.3, 0.3, 0.3],
|
||||||
|
speed: 1,
|
||||||
|
rotationIntensity: 0.08,
|
||||||
|
floatIntensity: 1,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["tiny2-rock"],
|
||||||
|
},
|
||||||
|
"tiny2-rock": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: [1.2, 0.5, 0.8],
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#7a5a10",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.9,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 1,
|
||||||
|
widthSegments: 12,
|
||||||
|
heightSegments: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"bridge-1": {
|
||||||
|
type: "RoundedBox",
|
||||||
|
props: {
|
||||||
|
position: [-3.57, -0.1, 1.19],
|
||||||
|
rotation: [0, 0.32, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#8B6914",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.9,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
width: 3.5,
|
||||||
|
height: 0.5,
|
||||||
|
depth: 0.6,
|
||||||
|
radius: 0.05,
|
||||||
|
smoothness: 2,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"bridge-2": {
|
||||||
|
type: "RoundedBox",
|
||||||
|
props: {
|
||||||
|
position: [3.19, -0.1, -1.74],
|
||||||
|
rotation: [0, 0.5, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: true,
|
||||||
|
material: {
|
||||||
|
color: "#8B6914",
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.9,
|
||||||
|
emissive: null,
|
||||||
|
emissiveIntensity: null,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
width: 3.0,
|
||||||
|
height: 0.5,
|
||||||
|
depth: 0.6,
|
||||||
|
radius: 0.05,
|
||||||
|
smoothness: 2,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
sparkles: {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 2, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: [20, 8, 20],
|
||||||
|
count: 120,
|
||||||
|
speed: 0.1,
|
||||||
|
opacity: 0.5,
|
||||||
|
color: "#ffeeaa",
|
||||||
|
size: 1.5,
|
||||||
|
noise: 1,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: true,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: true,
|
||||||
|
enablePan: null,
|
||||||
|
enableRotate: true,
|
||||||
|
minDistance: 8,
|
||||||
|
maxDistance: 30,
|
||||||
|
minPolarAngle: 0.2,
|
||||||
|
maxPolarAngle: 1.5,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 0.3,
|
||||||
|
target: [0, 1, 0],
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const hyperspaceTunnel: Scene = {
|
||||||
|
name: "Hyperspace Tunnel",
|
||||||
|
description:
|
||||||
|
"Neon warp tunnel at breakneck speed -- bloom, camera shake, particles streaming past",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"cam",
|
||||||
|
"controls",
|
||||||
|
"tunnel",
|
||||||
|
"stars",
|
||||||
|
"sparkles-core",
|
||||||
|
"sparkles-wide",
|
||||||
|
"ambient",
|
||||||
|
"center-light",
|
||||||
|
"shake",
|
||||||
|
"post",
|
||||||
|
"fog",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cam: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
fov: 80,
|
||||||
|
near: 0.1,
|
||||||
|
far: 200,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableRotate: false,
|
||||||
|
enableZoom: false,
|
||||||
|
enablePan: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
tunnel: {
|
||||||
|
type: "WarpTunnel",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
ringCount: 100,
|
||||||
|
radius: 3,
|
||||||
|
length: 50,
|
||||||
|
speed: 12,
|
||||||
|
tubeRadius: 0.025,
|
||||||
|
color1: "#00ffff",
|
||||||
|
color2: "#ff00ff",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
stars: {
|
||||||
|
type: "Stars",
|
||||||
|
props: {
|
||||||
|
radius: 80,
|
||||||
|
depth: 60,
|
||||||
|
count: 6000,
|
||||||
|
factor: 5,
|
||||||
|
saturation: 0.4,
|
||||||
|
fade: true,
|
||||||
|
speed: 3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"sparkles-core": {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, -10],
|
||||||
|
rotation: null,
|
||||||
|
scale: [4, 4, 30],
|
||||||
|
count: 300,
|
||||||
|
speed: 3,
|
||||||
|
opacity: 1,
|
||||||
|
color: "#00ffff",
|
||||||
|
size: 2,
|
||||||
|
noise: 0.5,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"sparkles-wide": {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, -15],
|
||||||
|
rotation: null,
|
||||||
|
scale: [6, 6, 40],
|
||||||
|
count: 200,
|
||||||
|
speed: 4,
|
||||||
|
opacity: 0.7,
|
||||||
|
color: "#ff44ff",
|
||||||
|
size: 1.5,
|
||||||
|
noise: 1,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#110033", intensity: 0.2 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"center-light": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#00ccff",
|
||||||
|
intensity: 30,
|
||||||
|
distance: 30,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shake: {
|
||||||
|
type: "CameraShake",
|
||||||
|
props: {
|
||||||
|
intensity: 0.3,
|
||||||
|
maxYaw: 0.03,
|
||||||
|
maxPitch: 0.03,
|
||||||
|
maxRoll: 0.02,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 2.5,
|
||||||
|
luminanceThreshold: 0.1,
|
||||||
|
luminanceSmoothing: null,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.3, darkness: 0.9 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
fog: {
|
||||||
|
type: "Fog",
|
||||||
|
props: { color: "#030008", near: 5, far: 50 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
import { hyperspaceTunnel } from "./hyperspace-tunnel";
|
||||||
|
import { orbitalChaos } from "./orbital-chaos";
|
||||||
|
import { clockworkOrrery } from "./clockwork-orrery";
|
||||||
|
import { deepSeaAbyss } from "./deep-sea-abyss";
|
||||||
|
import { stormCell } from "./storm-cell";
|
||||||
|
import { perpetualMotion } from "./perpetual-motion";
|
||||||
|
import { floatingIslands } from "./floating-islands";
|
||||||
|
import { productShowroom } from "./product-showroom";
|
||||||
|
import { portalGallery } from "./portal-gallery";
|
||||||
|
import { pipes } from "./pipes";
|
||||||
|
import { mystify } from "./mystify";
|
||||||
|
import { starfield } from "./starfield";
|
||||||
|
export type { Scene };
|
||||||
|
|
||||||
|
export const scenes: Scene[] = [
|
||||||
|
hyperspaceTunnel,
|
||||||
|
orbitalChaos,
|
||||||
|
clockworkOrrery,
|
||||||
|
deepSeaAbyss,
|
||||||
|
stormCell,
|
||||||
|
perpetualMotion,
|
||||||
|
floatingIslands,
|
||||||
|
productShowroom,
|
||||||
|
portalGallery,
|
||||||
|
pipes,
|
||||||
|
mystify,
|
||||||
|
starfield,
|
||||||
|
];
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const mystify: Scene = {
|
||||||
|
name: "Mystify",
|
||||||
|
description:
|
||||||
|
"Neon wireframe torus knots orbiting in the void -- the Windows Mystify screensaver in 3D",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"cam",
|
||||||
|
"ambient",
|
||||||
|
"shape1",
|
||||||
|
"shape2",
|
||||||
|
"shape3",
|
||||||
|
"shape4",
|
||||||
|
"shape5",
|
||||||
|
"shape6",
|
||||||
|
"shape7",
|
||||||
|
"post",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cam: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 10],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
fov: 55,
|
||||||
|
near: 0.1,
|
||||||
|
far: 1000,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#111111", intensity: 0.1 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shape1: {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.5,
|
||||||
|
radius: 3,
|
||||||
|
tilt: 0.3,
|
||||||
|
},
|
||||||
|
children: ["s1-spin"],
|
||||||
|
},
|
||||||
|
"s1-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 1.5,
|
||||||
|
axis: "x",
|
||||||
|
},
|
||||||
|
children: ["s1-body"],
|
||||||
|
},
|
||||||
|
"s1-body": {
|
||||||
|
type: "TorusKnot",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.4,
|
||||||
|
tube: 0.02,
|
||||||
|
p: 2,
|
||||||
|
q: 3,
|
||||||
|
tubularSegments: 128,
|
||||||
|
radialSegments: 4,
|
||||||
|
material: {
|
||||||
|
color: "#00ffff",
|
||||||
|
emissive: "#00ffff",
|
||||||
|
emissiveIntensity: 2,
|
||||||
|
wireframe: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shape2: {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -0.4,
|
||||||
|
radius: 4,
|
||||||
|
tilt: -0.5,
|
||||||
|
},
|
||||||
|
children: ["s2-spin"],
|
||||||
|
},
|
||||||
|
"s2-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -1,
|
||||||
|
axis: "z",
|
||||||
|
},
|
||||||
|
children: ["s2-body"],
|
||||||
|
},
|
||||||
|
"s2-body": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.6,
|
||||||
|
tube: 0.015,
|
||||||
|
radialSegments: 4,
|
||||||
|
tubularSegments: 128,
|
||||||
|
material: {
|
||||||
|
color: "#ff00ff",
|
||||||
|
emissive: "#ff00ff",
|
||||||
|
emissiveIntensity: 2,
|
||||||
|
wireframe: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shape3: {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.7,
|
||||||
|
radius: 2.5,
|
||||||
|
tilt: 0.8,
|
||||||
|
},
|
||||||
|
children: ["s3-spin"],
|
||||||
|
},
|
||||||
|
"s3-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 2,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["s3-body"],
|
||||||
|
},
|
||||||
|
"s3-body": {
|
||||||
|
type: "TorusKnot",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.35,
|
||||||
|
tube: 0.015,
|
||||||
|
p: 3,
|
||||||
|
q: 2,
|
||||||
|
tubularSegments: 128,
|
||||||
|
radialSegments: 4,
|
||||||
|
material: {
|
||||||
|
color: "#ffff00",
|
||||||
|
emissive: "#ffff00",
|
||||||
|
emissiveIntensity: 2,
|
||||||
|
wireframe: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shape4: {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -0.6,
|
||||||
|
radius: 3.5,
|
||||||
|
tilt: -0.2,
|
||||||
|
},
|
||||||
|
children: ["s4-spin"],
|
||||||
|
},
|
||||||
|
"s4-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 1.2,
|
||||||
|
axis: "x",
|
||||||
|
},
|
||||||
|
children: ["s4-body"],
|
||||||
|
},
|
||||||
|
"s4-body": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.5,
|
||||||
|
tube: 0.012,
|
||||||
|
radialSegments: 4,
|
||||||
|
tubularSegments: 128,
|
||||||
|
material: {
|
||||||
|
color: "#00ff44",
|
||||||
|
emissive: "#00ff44",
|
||||||
|
emissiveIntensity: 2,
|
||||||
|
wireframe: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shape5: {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.3,
|
||||||
|
radius: 5,
|
||||||
|
tilt: 0.6,
|
||||||
|
},
|
||||||
|
children: ["s5-spin"],
|
||||||
|
},
|
||||||
|
"s5-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -1.8,
|
||||||
|
axis: "z",
|
||||||
|
},
|
||||||
|
children: ["s5-body"],
|
||||||
|
},
|
||||||
|
"s5-body": {
|
||||||
|
type: "TorusKnot",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.5,
|
||||||
|
tube: 0.018,
|
||||||
|
p: 2,
|
||||||
|
q: 5,
|
||||||
|
tubularSegments: 128,
|
||||||
|
radialSegments: 4,
|
||||||
|
material: {
|
||||||
|
color: "#ff4400",
|
||||||
|
emissive: "#ff4400",
|
||||||
|
emissiveIntensity: 2,
|
||||||
|
wireframe: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shape6: {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -0.8,
|
||||||
|
radius: 2,
|
||||||
|
tilt: -0.7,
|
||||||
|
},
|
||||||
|
children: ["s6-spin"],
|
||||||
|
},
|
||||||
|
"s6-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 2.5,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["s6-body"],
|
||||||
|
},
|
||||||
|
"s6-body": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.3,
|
||||||
|
tube: 0.01,
|
||||||
|
radialSegments: 4,
|
||||||
|
tubularSegments: 128,
|
||||||
|
material: {
|
||||||
|
color: "#4488ff",
|
||||||
|
emissive: "#4488ff",
|
||||||
|
emissiveIntensity: 2,
|
||||||
|
wireframe: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shape7: {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.45,
|
||||||
|
radius: 4.5,
|
||||||
|
tilt: 1,
|
||||||
|
},
|
||||||
|
children: ["s7-spin"],
|
||||||
|
},
|
||||||
|
"s7-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -0.8,
|
||||||
|
axis: "x",
|
||||||
|
},
|
||||||
|
children: ["s7-body"],
|
||||||
|
},
|
||||||
|
"s7-body": {
|
||||||
|
type: "TorusKnot",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.45,
|
||||||
|
tube: 0.015,
|
||||||
|
p: 3,
|
||||||
|
q: 4,
|
||||||
|
tubularSegments: 128,
|
||||||
|
radialSegments: 4,
|
||||||
|
material: {
|
||||||
|
color: "#ff88ff",
|
||||||
|
emissive: "#ff88ff",
|
||||||
|
emissiveIntensity: 2,
|
||||||
|
wireframe: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 3,
|
||||||
|
luminanceThreshold: 0.05,
|
||||||
|
luminanceSmoothing: 0.3,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.2, darkness: 0.8 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: true,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: true,
|
||||||
|
enablePan: true,
|
||||||
|
enableRotate: true,
|
||||||
|
minDistance: 4,
|
||||||
|
maxDistance: 18,
|
||||||
|
minPolarAngle: null,
|
||||||
|
maxPolarAngle: null,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 0.3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
import { PI } from "./_helpers";
|
||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const orbitalChaos: Scene = {
|
||||||
|
name: "Orbital Chaos",
|
||||||
|
description:
|
||||||
|
"Everything in motion -- orbiting glass, spinning knots, morphing core, all with bloom",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"cam",
|
||||||
|
"env",
|
||||||
|
"stars",
|
||||||
|
"ambient",
|
||||||
|
"core-light",
|
||||||
|
"accent-1",
|
||||||
|
"accent-2",
|
||||||
|
"core-pulse",
|
||||||
|
"orbit-1",
|
||||||
|
"orbit-2",
|
||||||
|
"orbit-3",
|
||||||
|
"orbit-4",
|
||||||
|
"orbit-5",
|
||||||
|
"orbit-6",
|
||||||
|
"ring-spin-fast",
|
||||||
|
"ring-spin-slow",
|
||||||
|
"sparkles",
|
||||||
|
"post",
|
||||||
|
"fog",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cam: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [0, 2, 8],
|
||||||
|
fov: 50,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
type: "Environment",
|
||||||
|
props: { preset: "night", background: false, blur: 1, intensity: 0.15 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
stars: {
|
||||||
|
type: "Stars",
|
||||||
|
props: {
|
||||||
|
radius: 120,
|
||||||
|
depth: 60,
|
||||||
|
count: 8000,
|
||||||
|
factor: 5,
|
||||||
|
saturation: 0.2,
|
||||||
|
fade: true,
|
||||||
|
speed: 0.5,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#110022", intensity: 0.3 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"core-light": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ff44aa",
|
||||||
|
intensity: 80,
|
||||||
|
distance: 25,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"accent-1": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [5, 3, 5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#00ffff",
|
||||||
|
intensity: 25,
|
||||||
|
distance: 20,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"accent-2": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [-5, -2, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ff00ff",
|
||||||
|
intensity: 25,
|
||||||
|
distance: 20,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"core-pulse": {
|
||||||
|
type: "Pulse",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.8,
|
||||||
|
min: 0.85,
|
||||||
|
max: 1.15,
|
||||||
|
},
|
||||||
|
children: ["core-spin"],
|
||||||
|
},
|
||||||
|
"core-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.3,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["core-blob"],
|
||||||
|
},
|
||||||
|
"core-blob": {
|
||||||
|
type: "DistortSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 1.2,
|
||||||
|
widthSegments: 64,
|
||||||
|
heightSegments: 32,
|
||||||
|
color: "#ff2288",
|
||||||
|
speed: 4,
|
||||||
|
distort: 0.6,
|
||||||
|
metalness: 0.1,
|
||||||
|
roughness: 0.1,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"orbit-1": {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.8,
|
||||||
|
radius: 4,
|
||||||
|
tilt: 0.5,
|
||||||
|
},
|
||||||
|
children: ["orb-1-spin"],
|
||||||
|
},
|
||||||
|
"orb-1-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 2,
|
||||||
|
axis: "x",
|
||||||
|
},
|
||||||
|
children: ["orb-1"],
|
||||||
|
},
|
||||||
|
"orb-1": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.4,
|
||||||
|
widthSegments: 32,
|
||||||
|
heightSegments: 16,
|
||||||
|
color: "#00ffff",
|
||||||
|
transmission: 1,
|
||||||
|
thickness: 0.5,
|
||||||
|
roughness: 0,
|
||||||
|
chromaticAberration: 0.15,
|
||||||
|
ior: 1.8,
|
||||||
|
distortion: 0.1,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"orbit-2": {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -0.6,
|
||||||
|
radius: 5.5,
|
||||||
|
tilt: 1,
|
||||||
|
},
|
||||||
|
children: ["orb-2-spin"],
|
||||||
|
},
|
||||||
|
"orb-2-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 3,
|
||||||
|
axis: "z",
|
||||||
|
},
|
||||||
|
children: ["orb-2"],
|
||||||
|
},
|
||||||
|
"orb-2": {
|
||||||
|
type: "TorusKnot",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#ff00ff",
|
||||||
|
metalness: 0.9,
|
||||||
|
roughness: 0.1,
|
||||||
|
emissive: "#ff00ff",
|
||||||
|
emissiveIntensity: 0.8,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 0.35,
|
||||||
|
tube: 0.12,
|
||||||
|
tubularSegments: 64,
|
||||||
|
radialSegments: 8,
|
||||||
|
p: 2,
|
||||||
|
q: 3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"orbit-3": {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: [0, -0.3, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 1.2,
|
||||||
|
radius: 3.2,
|
||||||
|
tilt: 0.3,
|
||||||
|
},
|
||||||
|
children: ["orb-3"],
|
||||||
|
},
|
||||||
|
"orb-3": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.3,
|
||||||
|
widthSegments: 32,
|
||||||
|
heightSegments: 16,
|
||||||
|
color: "#ff8800",
|
||||||
|
transmission: 1,
|
||||||
|
thickness: 0.4,
|
||||||
|
roughness: 0,
|
||||||
|
chromaticAberration: 0.1,
|
||||||
|
ior: 1.6,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"orbit-4": {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: [0, 1, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -1.5,
|
||||||
|
radius: 6.5,
|
||||||
|
tilt: -0.8,
|
||||||
|
},
|
||||||
|
children: ["orb-4-spin"],
|
||||||
|
},
|
||||||
|
"orb-4-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -4,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["orb-4"],
|
||||||
|
},
|
||||||
|
"orb-4": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#00ff88",
|
||||||
|
metalness: null,
|
||||||
|
roughness: null,
|
||||||
|
emissive: "#00ff88",
|
||||||
|
emissiveIntensity: 0.8,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: true,
|
||||||
|
},
|
||||||
|
radius: 0.35,
|
||||||
|
widthSegments: 16,
|
||||||
|
heightSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"orbit-5": {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: [0, -0.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.4,
|
||||||
|
radius: 8,
|
||||||
|
tilt: 0.2,
|
||||||
|
},
|
||||||
|
children: ["orb-5-spin"],
|
||||||
|
},
|
||||||
|
"orb-5-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 1.5,
|
||||||
|
axis: "z",
|
||||||
|
},
|
||||||
|
children: ["orb-5"],
|
||||||
|
},
|
||||||
|
"orb-5": {
|
||||||
|
type: "GlassBox",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
width: 0.5,
|
||||||
|
height: 0.5,
|
||||||
|
depth: 0.5,
|
||||||
|
color: "#ff44ff",
|
||||||
|
transmission: 1,
|
||||||
|
thickness: 0.4,
|
||||||
|
roughness: 0,
|
||||||
|
chromaticAberration: 0.08,
|
||||||
|
ior: 1.7,
|
||||||
|
distortion: 0,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"orbit-6": {
|
||||||
|
type: "Orbit",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.8, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 2,
|
||||||
|
radius: 2.2,
|
||||||
|
tilt: 1.5,
|
||||||
|
},
|
||||||
|
children: ["orb-6"],
|
||||||
|
},
|
||||||
|
"orb-6": {
|
||||||
|
type: "DistortSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.25,
|
||||||
|
widthSegments: 32,
|
||||||
|
heightSegments: 16,
|
||||||
|
color: "#ffcc00",
|
||||||
|
speed: 5,
|
||||||
|
distort: 0.7,
|
||||||
|
metalness: 0.5,
|
||||||
|
roughness: 0.1,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"ring-spin-fast": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [0.3, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
speed: 0.5,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["ring-inner"],
|
||||||
|
},
|
||||||
|
"ring-inner": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [PI / 2, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#00ffff",
|
||||||
|
metalness: 0.9,
|
||||||
|
roughness: 0.1,
|
||||||
|
emissive: "#00ccff",
|
||||||
|
emissiveIntensity: 0.5,
|
||||||
|
opacity: 0.3,
|
||||||
|
transparent: true,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 9,
|
||||||
|
tube: 0.015,
|
||||||
|
radialSegments: null,
|
||||||
|
tubularSegments: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"ring-spin-slow": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [-0.5, 0, 0.3],
|
||||||
|
scale: null,
|
||||||
|
speed: -0.3,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["ring-outer"],
|
||||||
|
},
|
||||||
|
"ring-outer": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [PI / 2, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#ff00ff",
|
||||||
|
metalness: 0.9,
|
||||||
|
roughness: 0.1,
|
||||||
|
emissive: "#ff00aa",
|
||||||
|
emissiveIntensity: 0.3,
|
||||||
|
opacity: 0.2,
|
||||||
|
transparent: true,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 11,
|
||||||
|
tube: 0.012,
|
||||||
|
radialSegments: null,
|
||||||
|
tubularSegments: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
sparkles: {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: [18, 18, 18],
|
||||||
|
count: 400,
|
||||||
|
speed: 0.4,
|
||||||
|
opacity: 0.5,
|
||||||
|
color: "#ffddaa",
|
||||||
|
size: 1.2,
|
||||||
|
noise: 2,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 2,
|
||||||
|
luminanceThreshold: 0.15,
|
||||||
|
luminanceSmoothing: null,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.4, darkness: 0.7 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
fog: {
|
||||||
|
type: "Fog",
|
||||||
|
props: { color: "#030008", near: 10, far: 40 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: true,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: true,
|
||||||
|
enablePan: null,
|
||||||
|
enableRotate: true,
|
||||||
|
minDistance: 5,
|
||||||
|
maxDistance: 25,
|
||||||
|
minPolarAngle: null,
|
||||||
|
maxPolarAngle: null,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 0.4,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
import { PI } from "./_helpers";
|
||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const perpetualMotion: Scene = {
|
||||||
|
name: "Perpetual Motion Machine",
|
||||||
|
description:
|
||||||
|
"A gyroscope/gimbal with 3 nested spinning rings on different axes, with a perfectly still glass sphere floating at the center. Clean, minimal, mesmerizing.",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"env",
|
||||||
|
"ambient",
|
||||||
|
"key",
|
||||||
|
"fill",
|
||||||
|
"outer-spin",
|
||||||
|
"sparkles",
|
||||||
|
"floor",
|
||||||
|
"shadows",
|
||||||
|
"post",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
type: "Environment",
|
||||||
|
props: {
|
||||||
|
preset: "studio",
|
||||||
|
background: false,
|
||||||
|
blur: null,
|
||||||
|
intensity: 1,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#ffffff", intensity: 0.3 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
type: "DirectionalLight",
|
||||||
|
props: {
|
||||||
|
position: [5, 8, 5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffffff",
|
||||||
|
intensity: 2,
|
||||||
|
castShadow: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [-3, 4, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#aaccff",
|
||||||
|
intensity: 10,
|
||||||
|
distance: 15,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"outer-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.4,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["outer-ring", "mid-spin"],
|
||||||
|
},
|
||||||
|
"outer-ring": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [PI / 2, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#cccccc",
|
||||||
|
metalness: 1,
|
||||||
|
roughness: 0.05,
|
||||||
|
emissive: "#ffffff",
|
||||||
|
emissiveIntensity: 0.05,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 4,
|
||||||
|
tube: 0.06,
|
||||||
|
radialSegments: null,
|
||||||
|
tubularSegments: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"mid-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -0.8,
|
||||||
|
axis: "x",
|
||||||
|
},
|
||||||
|
children: ["mid-ring", "inner-spin"],
|
||||||
|
},
|
||||||
|
"mid-ring": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [PI / 2, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#ffcc44",
|
||||||
|
metalness: 0.95,
|
||||||
|
roughness: 0.08,
|
||||||
|
emissive: "#cc9900",
|
||||||
|
emissiveIntensity: 0.1,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 3,
|
||||||
|
tube: 0.05,
|
||||||
|
radialSegments: null,
|
||||||
|
tubularSegments: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"inner-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 1.5,
|
||||||
|
axis: "z",
|
||||||
|
},
|
||||||
|
children: ["inner-ring", "center-float"],
|
||||||
|
},
|
||||||
|
"inner-ring": {
|
||||||
|
type: "Torus",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: [PI / 2, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: false,
|
||||||
|
receiveShadow: false,
|
||||||
|
material: {
|
||||||
|
color: "#cc8844",
|
||||||
|
metalness: 0.9,
|
||||||
|
roughness: 0.1,
|
||||||
|
emissive: "#996633",
|
||||||
|
emissiveIntensity: 0.1,
|
||||||
|
opacity: null,
|
||||||
|
transparent: null,
|
||||||
|
wireframe: null,
|
||||||
|
},
|
||||||
|
radius: 2,
|
||||||
|
tube: 0.04,
|
||||||
|
radialSegments: null,
|
||||||
|
tubularSegments: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"center-float": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.3,
|
||||||
|
rotationIntensity: 0,
|
||||||
|
floatIntensity: 0.1,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["center-orb"],
|
||||||
|
},
|
||||||
|
"center-orb": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: true,
|
||||||
|
receiveShadow: false,
|
||||||
|
radius: 0.8,
|
||||||
|
widthSegments: 64,
|
||||||
|
heightSegments: 32,
|
||||||
|
color: "#ffffff",
|
||||||
|
transmission: 1,
|
||||||
|
thickness: 1,
|
||||||
|
roughness: 0,
|
||||||
|
chromaticAberration: 0.12,
|
||||||
|
ior: 2,
|
||||||
|
distortion: 0.1,
|
||||||
|
distortionScale: 0.4,
|
||||||
|
temporalDistortion: 0.4,
|
||||||
|
samples: 10,
|
||||||
|
resolution: 256,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sparkles: {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: [10, 10, 10],
|
||||||
|
count: 100,
|
||||||
|
speed: 0.1,
|
||||||
|
opacity: 0.2,
|
||||||
|
color: "#ccddff",
|
||||||
|
size: 0.8,
|
||||||
|
noise: 2,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
floor: {
|
||||||
|
type: "ReflectorPlane",
|
||||||
|
props: {
|
||||||
|
position: [0, -4.5, 0],
|
||||||
|
rotation: [-PI / 2, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
color: "#1a1a2a",
|
||||||
|
resolution: 1024,
|
||||||
|
blur: 400,
|
||||||
|
mirror: 0.7,
|
||||||
|
mixBlur: 10,
|
||||||
|
mixStrength: 2,
|
||||||
|
depthScale: 0.2,
|
||||||
|
metalness: 0.5,
|
||||||
|
roughness: 1,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shadows: {
|
||||||
|
type: "ContactShadows",
|
||||||
|
props: {
|
||||||
|
position: [0, -4.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
opacity: 0.3,
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
blur: 2.5,
|
||||||
|
near: null,
|
||||||
|
far: 10,
|
||||||
|
smooth: true,
|
||||||
|
resolution: 512,
|
||||||
|
frames: null,
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 0.8,
|
||||||
|
luminanceThreshold: 0.25,
|
||||||
|
luminanceSmoothing: null,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.5, darkness: 0.4 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: true,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: true,
|
||||||
|
enablePan: null,
|
||||||
|
enableRotate: true,
|
||||||
|
minDistance: 5,
|
||||||
|
maxDistance: 16,
|
||||||
|
minPolarAngle: 0.3,
|
||||||
|
maxPolarAngle: 1.5,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 0.3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,764 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
const goldMaterial = {
|
||||||
|
color: "#cc8833",
|
||||||
|
metalness: 1,
|
||||||
|
roughness: 0.05,
|
||||||
|
};
|
||||||
|
|
||||||
|
const blueMaterial = {
|
||||||
|
color: "#3388cc",
|
||||||
|
metalness: 1,
|
||||||
|
roughness: 0.05,
|
||||||
|
};
|
||||||
|
|
||||||
|
const magentaMaterial = {
|
||||||
|
color: "#cc3388",
|
||||||
|
metalness: 1,
|
||||||
|
roughness: 0.05,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pipes: Scene = {
|
||||||
|
name: "Pipes",
|
||||||
|
description:
|
||||||
|
"Classic 3D Pipes screensaver -- chrome pipes snaking through space at right angles",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"camera",
|
||||||
|
"env",
|
||||||
|
"ambient",
|
||||||
|
"directional",
|
||||||
|
"pipe-system",
|
||||||
|
"post",
|
||||||
|
"fog",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
camera: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [0, 3, 12],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
fov: 50,
|
||||||
|
near: null,
|
||||||
|
far: null,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
type: "Environment",
|
||||||
|
props: {
|
||||||
|
preset: "warehouse",
|
||||||
|
background: false,
|
||||||
|
blur: 0.5,
|
||||||
|
intensity: 1,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#ffffff", intensity: 0.3 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
directional: {
|
||||||
|
type: "DirectionalLight",
|
||||||
|
props: {
|
||||||
|
position: [5, 10, 5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffffff",
|
||||||
|
intensity: 1,
|
||||||
|
castShadow: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"pipe-system": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.1,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["pipe1-group", "pipe2-group", "pipe3-group"],
|
||||||
|
},
|
||||||
|
"pipe1-group": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"p1-j0",
|
||||||
|
"p1-s1",
|
||||||
|
"p1-j1",
|
||||||
|
"p1-s2",
|
||||||
|
"p1-j2",
|
||||||
|
"p1-s3",
|
||||||
|
"p1-j3",
|
||||||
|
"p1-s4",
|
||||||
|
"p1-j4",
|
||||||
|
"p1-s5",
|
||||||
|
"p1-j5",
|
||||||
|
"p1-s6",
|
||||||
|
"p1-j6",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"p1-j0": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-3, -3, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-j1": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-3, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-j2": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [1, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-j3": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [1, 2, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-j4": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [1, 2, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-j5": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [1, 0, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-j6": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-1, 0, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-s1": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-3, -1.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 3,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-s2": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-1, 0, 0],
|
||||||
|
rotation: [0, 0, 1.5708],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 4,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-s3": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [1, 1, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-s4": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [1, 2, -1.5],
|
||||||
|
rotation: [1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 3,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-s5": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [1, 1, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-s6": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, -3],
|
||||||
|
rotation: [0, 0, 1.5708],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: goldMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"pipe2-group": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"p2-j0",
|
||||||
|
"p2-s1",
|
||||||
|
"p2-j1",
|
||||||
|
"p2-s2",
|
||||||
|
"p2-j2",
|
||||||
|
"p2-s3",
|
||||||
|
"p2-j3",
|
||||||
|
"p2-s4",
|
||||||
|
"p2-j4",
|
||||||
|
"p2-s5",
|
||||||
|
"p2-j5",
|
||||||
|
"p2-s6",
|
||||||
|
"p2-j6",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"p2-j0": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [2, -3, 2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-j1": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [2, 1, 2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-j2": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [2, 1, -2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-j3": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-1, 1, -2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-j4": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-1, 4, -2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-j5": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-3, 4, -2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-j6": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-3, 4, 1],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-s1": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [2, -1, 2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 4,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-s2": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [2, 1, 0],
|
||||||
|
rotation: [1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 4,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-s3": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0.5, 1, -2],
|
||||||
|
rotation: [0, 0, 1.5708],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 3,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-s4": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-1, 2.5, -2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 3,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-s5": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-2, 4, -2],
|
||||||
|
rotation: [0, 0, 1.5708],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-s6": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-3, 4, -0.5],
|
||||||
|
rotation: [1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: blueMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 3,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"pipe3-group": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"p3-j0",
|
||||||
|
"p3-s1",
|
||||||
|
"p3-j1",
|
||||||
|
"p3-s2",
|
||||||
|
"p3-j2",
|
||||||
|
"p3-s3",
|
||||||
|
"p3-j3",
|
||||||
|
"p3-s4",
|
||||||
|
"p3-j4",
|
||||||
|
"p3-s5",
|
||||||
|
"p3-j5",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"p3-j0": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-2, 3, 3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-j1": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-2, -2, 3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-j2": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [1, -2, 3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-j3": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [1, -2, -2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-j4": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [1, 0, -2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-j5": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-3, 0, -2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-s1": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-2, 0.5, 3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 5,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-s2": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-0.5, -2, 3],
|
||||||
|
rotation: [0, 0, 1.5708],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 3,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-s3": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [1, -2, 0.5],
|
||||||
|
rotation: [1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 5,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-s4": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [1, -1, -2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-s5": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [-1, 0, -2],
|
||||||
|
rotation: [0, 0, 1.5708],
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: magentaMaterial,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.08,
|
||||||
|
height: 4,
|
||||||
|
radialSegments: 12,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 0.4,
|
||||||
|
luminanceThreshold: 0.4,
|
||||||
|
luminanceSmoothing: null,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.3, darkness: 0.4 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
fog: {
|
||||||
|
type: "Fog",
|
||||||
|
props: { color: "#111111", near: 10, far: 30 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: true,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: null,
|
||||||
|
enablePan: null,
|
||||||
|
enableRotate: null,
|
||||||
|
minDistance: 5,
|
||||||
|
maxDistance: 20,
|
||||||
|
minPolarAngle: null,
|
||||||
|
maxPolarAngle: null,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 0.6,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,809 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const portalGallery: Scene = {
|
||||||
|
name: "Portal Gallery",
|
||||||
|
description:
|
||||||
|
"Three framed portals into different worlds -- sunset clouds, starfield, and enchanted forest",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"cam",
|
||||||
|
"env",
|
||||||
|
"ambient",
|
||||||
|
"directional",
|
||||||
|
"floor",
|
||||||
|
"shadows",
|
||||||
|
"frame1",
|
||||||
|
"frame2",
|
||||||
|
"frame3",
|
||||||
|
"gallery-sparkles",
|
||||||
|
"post",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cam: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [0, 2, 7],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
fov: 50,
|
||||||
|
near: 0.1,
|
||||||
|
far: 200,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
type: "Environment",
|
||||||
|
props: {
|
||||||
|
preset: "lobby",
|
||||||
|
background: false,
|
||||||
|
blur: 0,
|
||||||
|
intensity: 0.4,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#e8e0d8", intensity: 0.5 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
directional: {
|
||||||
|
type: "DirectionalLight",
|
||||||
|
props: {
|
||||||
|
position: [5, 8, 3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffffff",
|
||||||
|
intensity: 1,
|
||||||
|
castShadow: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
floor: {
|
||||||
|
type: "ReflectorPlane",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: [-1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
color: "#2a2a2a",
|
||||||
|
resolution: 1024,
|
||||||
|
blur: 400,
|
||||||
|
mirror: 0.4,
|
||||||
|
mixBlur: 10,
|
||||||
|
mixStrength: 1.5,
|
||||||
|
metalness: 0.5,
|
||||||
|
roughness: 0.8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shadows: {
|
||||||
|
type: "ContactShadows",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.01, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
opacity: 0.4,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
blur: 2.5,
|
||||||
|
far: 8,
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- PORTAL 1 (left, sunset world) ---
|
||||||
|
frame1: {
|
||||||
|
type: "Group",
|
||||||
|
props: {
|
||||||
|
position: [-3.5, 2, -2],
|
||||||
|
rotation: [0, 0.3, 0],
|
||||||
|
scale: null,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
"frame1-top",
|
||||||
|
"frame1-bottom",
|
||||||
|
"frame1-left",
|
||||||
|
"frame1-right",
|
||||||
|
"portal1-mesh",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"frame1-top": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [0, 1.55, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 2.3,
|
||||||
|
height: 0.15,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#8B7355", metalness: 0.3, roughness: 0.6 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"frame1-bottom": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [0, -1.55, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 2.3,
|
||||||
|
height: 0.15,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#8B7355", metalness: 0.3, roughness: 0.6 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"frame1-left": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [-1.075, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 0.15,
|
||||||
|
height: 3.25,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#8B7355", metalness: 0.3, roughness: 0.6 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"frame1-right": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [1.075, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 0.15,
|
||||||
|
height: 3.25,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#8B7355", metalness: 0.3, roughness: 0.6 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"portal1-mesh": {
|
||||||
|
type: "Plane",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 2,
|
||||||
|
height: 3,
|
||||||
|
},
|
||||||
|
children: ["portal1-mat"],
|
||||||
|
},
|
||||||
|
"portal1-mat": {
|
||||||
|
type: "MeshPortalMaterial",
|
||||||
|
props: { blend: 0, blur: 0.5, resolution: 512 },
|
||||||
|
children: [
|
||||||
|
"p1-sky",
|
||||||
|
"p1-sun-light",
|
||||||
|
"p1-cloud1",
|
||||||
|
"p1-cloud2",
|
||||||
|
"p1-cloud3",
|
||||||
|
"p1-ground",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"p1-sky": {
|
||||||
|
type: "Sky",
|
||||||
|
props: {
|
||||||
|
distance: null,
|
||||||
|
sunPosition: [100, 10, 50],
|
||||||
|
inclination: null,
|
||||||
|
azimuth: null,
|
||||||
|
rayleigh: 2,
|
||||||
|
turbidity: 10,
|
||||||
|
mieCoefficient: 0.005,
|
||||||
|
mieDirectionalG: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-sun-light": {
|
||||||
|
type: "DirectionalLight",
|
||||||
|
props: {
|
||||||
|
position: [5, 5, 3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffaa55",
|
||||||
|
intensity: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-cloud1": {
|
||||||
|
type: "Cloud",
|
||||||
|
props: {
|
||||||
|
position: [-3, 4, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
seed: 1,
|
||||||
|
segments: 20,
|
||||||
|
bounds: [6, 2, 3],
|
||||||
|
opacity: 0.6,
|
||||||
|
color: "#ffddaa",
|
||||||
|
speed: 0.1,
|
||||||
|
volume: 5,
|
||||||
|
fade: null,
|
||||||
|
growth: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-cloud2": {
|
||||||
|
type: "Cloud",
|
||||||
|
props: {
|
||||||
|
position: [4, 5, -8],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
seed: 7,
|
||||||
|
segments: 25,
|
||||||
|
bounds: [8, 2, 4],
|
||||||
|
opacity: 0.5,
|
||||||
|
color: "#ffccaa",
|
||||||
|
speed: 0.08,
|
||||||
|
volume: 6,
|
||||||
|
fade: null,
|
||||||
|
growth: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-cloud3": {
|
||||||
|
type: "Cloud",
|
||||||
|
props: {
|
||||||
|
position: [0, 3.5, -4],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
seed: 14,
|
||||||
|
segments: 15,
|
||||||
|
bounds: [5, 1.5, 2.5],
|
||||||
|
opacity: 0.7,
|
||||||
|
color: "#ffeedd",
|
||||||
|
speed: 0.12,
|
||||||
|
volume: 4,
|
||||||
|
fade: null,
|
||||||
|
growth: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p1-ground": {
|
||||||
|
type: "Plane",
|
||||||
|
props: {
|
||||||
|
position: [0, -2, 0],
|
||||||
|
rotation: [-1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
material: { color: "#886644", roughness: 1 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- PORTAL 2 (center, night/stars world) ---
|
||||||
|
frame2: {
|
||||||
|
type: "Group",
|
||||||
|
props: {
|
||||||
|
position: [0, 2, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
"frame2-top",
|
||||||
|
"frame2-bottom",
|
||||||
|
"frame2-left",
|
||||||
|
"frame2-right",
|
||||||
|
"portal2-mesh",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"frame2-top": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [0, 1.55, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 2.3,
|
||||||
|
height: 0.15,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#555566", metalness: 0.8, roughness: 0.2 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"frame2-bottom": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [0, -1.55, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 2.3,
|
||||||
|
height: 0.15,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#555566", metalness: 0.8, roughness: 0.2 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"frame2-left": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [-1.075, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 0.15,
|
||||||
|
height: 3.25,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#555566", metalness: 0.8, roughness: 0.2 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"frame2-right": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [1.075, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 0.15,
|
||||||
|
height: 3.25,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#555566", metalness: 0.8, roughness: 0.2 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"portal2-mesh": {
|
||||||
|
type: "Plane",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 2,
|
||||||
|
height: 3,
|
||||||
|
},
|
||||||
|
children: ["portal2-mat"],
|
||||||
|
},
|
||||||
|
"portal2-mat": {
|
||||||
|
type: "MeshPortalMaterial",
|
||||||
|
props: { blend: 0, blur: 0.5, resolution: 512 },
|
||||||
|
children: [
|
||||||
|
"p2-stars",
|
||||||
|
"p2-ambient",
|
||||||
|
"p2-nebula1",
|
||||||
|
"p2-nebula2",
|
||||||
|
"p2-sparkles",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"p2-stars": {
|
||||||
|
type: "Stars",
|
||||||
|
props: {
|
||||||
|
radius: 80,
|
||||||
|
depth: 40,
|
||||||
|
count: 6000,
|
||||||
|
factor: 5,
|
||||||
|
saturation: 0.5,
|
||||||
|
fade: true,
|
||||||
|
speed: 0.3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-ambient": {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#111133", intensity: 0.3 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-nebula1": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [3, 2, -8],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radius: 2,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
material: {
|
||||||
|
color: "#4400aa",
|
||||||
|
emissive: "#4400aa",
|
||||||
|
emissiveIntensity: 0.5,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-nebula2": {
|
||||||
|
type: "Sphere",
|
||||||
|
props: {
|
||||||
|
position: [-4, -1, -10],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radius: 3,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
material: {
|
||||||
|
color: "#aa0044",
|
||||||
|
emissive: "#aa0044",
|
||||||
|
emissiveIntensity: 0.4,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p2-sparkles": {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: [10, 10, 10],
|
||||||
|
count: 200,
|
||||||
|
speed: 0.2,
|
||||||
|
opacity: 0.8,
|
||||||
|
color: "#aabbff",
|
||||||
|
size: 2,
|
||||||
|
noise: 3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- PORTAL 3 (right, forest/nature world) ---
|
||||||
|
frame3: {
|
||||||
|
type: "Group",
|
||||||
|
props: {
|
||||||
|
position: [3.5, 2, -2],
|
||||||
|
rotation: [0, -0.3, 0],
|
||||||
|
scale: null,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
"frame3-top",
|
||||||
|
"frame3-bottom",
|
||||||
|
"frame3-left",
|
||||||
|
"frame3-right",
|
||||||
|
"portal3-mesh",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"frame3-top": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [0, 1.55, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 2.3,
|
||||||
|
height: 0.15,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#6B4423", metalness: 0.1, roughness: 0.8 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"frame3-bottom": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [0, -1.55, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 2.3,
|
||||||
|
height: 0.15,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#6B4423", metalness: 0.1, roughness: 0.8 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"frame3-left": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [-1.075, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 0.15,
|
||||||
|
height: 3.25,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#6B4423", metalness: 0.1, roughness: 0.8 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"frame3-right": {
|
||||||
|
type: "Box",
|
||||||
|
props: {
|
||||||
|
position: [1.075, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 0.15,
|
||||||
|
height: 3.25,
|
||||||
|
depth: 0.15,
|
||||||
|
material: { color: "#6B4423", metalness: 0.1, roughness: 0.8 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"portal3-mesh": {
|
||||||
|
type: "Plane",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
width: 2,
|
||||||
|
height: 3,
|
||||||
|
},
|
||||||
|
children: ["portal3-mat"],
|
||||||
|
},
|
||||||
|
"portal3-mat": {
|
||||||
|
type: "MeshPortalMaterial",
|
||||||
|
props: { blend: 0, blur: 0.5, resolution: 512 },
|
||||||
|
children: [
|
||||||
|
"p3-env",
|
||||||
|
"p3-ambient",
|
||||||
|
"p3-sun",
|
||||||
|
"p3-tree1",
|
||||||
|
"p3-tree2",
|
||||||
|
"p3-tree3",
|
||||||
|
"p3-tree4",
|
||||||
|
"p3-tree5",
|
||||||
|
"p3-ground",
|
||||||
|
"p3-fireflies",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"p3-env": {
|
||||||
|
type: "Environment",
|
||||||
|
props: { preset: "forest", background: false, blur: 0, intensity: 0.8 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-ambient": {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#334422", intensity: 0.4 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-sun": {
|
||||||
|
type: "DirectionalLight",
|
||||||
|
props: {
|
||||||
|
position: [-3, 8, 2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#aaffaa",
|
||||||
|
intensity: 1.5,
|
||||||
|
castShadow: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-tree1": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: [-2, -2, -4], rotation: null, scale: null },
|
||||||
|
children: ["p3-t1-trunk", "p3-t1-leaves"],
|
||||||
|
},
|
||||||
|
"p3-t1-trunk": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.6, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radiusTop: 0.08,
|
||||||
|
radiusBottom: 0.12,
|
||||||
|
height: 1.2,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#4a3520", roughness: 0.9 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-t1-leaves": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, 1.8, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radius: 0.6,
|
||||||
|
height: 1.5,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#2d5a1e", roughness: 0.8 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-tree2": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: [1.5, -2, -6], rotation: null, scale: null },
|
||||||
|
children: ["p3-t2-trunk", "p3-t2-leaves"],
|
||||||
|
},
|
||||||
|
"p3-t2-trunk": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.8, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radiusTop: 0.1,
|
||||||
|
radiusBottom: 0.15,
|
||||||
|
height: 1.6,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#3d2b15", roughness: 0.9 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-t2-leaves": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, 2.3, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radius: 0.8,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#1e4a10", roughness: 0.8 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-tree3": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: [-0.5, -2, -3], rotation: null, scale: null },
|
||||||
|
children: ["p3-t3-trunk", "p3-t3-leaves"],
|
||||||
|
},
|
||||||
|
"p3-t3-trunk": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radiusTop: 0.06,
|
||||||
|
radiusBottom: 0.1,
|
||||||
|
height: 1,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#4a3520", roughness: 0.9 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-t3-leaves": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, 1.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radius: 0.5,
|
||||||
|
height: 1.2,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#3a6b2a", roughness: 0.8 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-tree4": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: [3, -2, -8], rotation: null, scale: null },
|
||||||
|
children: ["p3-t4-trunk", "p3-t4-leaves"],
|
||||||
|
},
|
||||||
|
"p3-t4-trunk": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 1, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radiusTop: 0.12,
|
||||||
|
radiusBottom: 0.18,
|
||||||
|
height: 2,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#3d2b15", roughness: 0.9 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-t4-leaves": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, 2.8, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radius: 1,
|
||||||
|
height: 2.5,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#2d5a1e", roughness: 0.8 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-tree5": {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: [-3.5, -2, -7], rotation: null, scale: null },
|
||||||
|
children: ["p3-t5-trunk", "p3-t5-leaves"],
|
||||||
|
},
|
||||||
|
"p3-t5-trunk": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.7, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radiusTop: 0.09,
|
||||||
|
radiusBottom: 0.13,
|
||||||
|
height: 1.4,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#4a3520", roughness: 0.9 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-t5-leaves": {
|
||||||
|
type: "Cone",
|
||||||
|
props: {
|
||||||
|
position: [0, 2, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radius: 0.7,
|
||||||
|
height: 1.8,
|
||||||
|
radialSegments: 8,
|
||||||
|
material: { color: "#1e4a10", roughness: 0.8 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-ground": {
|
||||||
|
type: "Plane",
|
||||||
|
props: {
|
||||||
|
position: [0, -2, 0],
|
||||||
|
rotation: [-1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
material: { color: "#3a5a2a", roughness: 1 },
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"p3-fireflies": {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: [6, 4, 6],
|
||||||
|
count: 80,
|
||||||
|
speed: 0.3,
|
||||||
|
opacity: 0.7,
|
||||||
|
color: "#aaff66",
|
||||||
|
size: 1.5,
|
||||||
|
noise: 2,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
"gallery-sparkles": {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 3, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: [12, 5, 10],
|
||||||
|
count: 40,
|
||||||
|
speed: 0.05,
|
||||||
|
opacity: 0.2,
|
||||||
|
color: "#ffffff",
|
||||||
|
size: 0.3,
|
||||||
|
noise: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 0.6,
|
||||||
|
luminanceThreshold: 0.3,
|
||||||
|
luminanceSmoothing: null,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.4, darkness: 0.4 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: true,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: true,
|
||||||
|
enablePan: null,
|
||||||
|
enableRotate: true,
|
||||||
|
minDistance: 4,
|
||||||
|
maxDistance: 16,
|
||||||
|
minPolarAngle: null,
|
||||||
|
maxPolarAngle: null,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 0.3,
|
||||||
|
target: [0, 1.8, -2],
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const productShowroom: Scene = {
|
||||||
|
name: "Product Showroom",
|
||||||
|
description:
|
||||||
|
"Sleek glass torus knot on a pedestal with studio lighting, reflections, and contact shadows",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"camera",
|
||||||
|
"env",
|
||||||
|
"backdrop",
|
||||||
|
"ambient",
|
||||||
|
"key-light",
|
||||||
|
"fill-light",
|
||||||
|
"rim-light",
|
||||||
|
"accent-light",
|
||||||
|
"pedestal",
|
||||||
|
"hero-float",
|
||||||
|
"accent1",
|
||||||
|
"accent2",
|
||||||
|
"accent3",
|
||||||
|
"floor",
|
||||||
|
"shadows",
|
||||||
|
"sparkles",
|
||||||
|
"fx",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
camera: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [0, 2, 6],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
fov: 40,
|
||||||
|
near: null,
|
||||||
|
far: null,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
type: "Environment",
|
||||||
|
props: {
|
||||||
|
preset: "studio",
|
||||||
|
background: false,
|
||||||
|
blur: 0.5,
|
||||||
|
intensity: 0.8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
backdrop: {
|
||||||
|
type: "Backdrop",
|
||||||
|
props: {
|
||||||
|
position: [0, -0.5, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: [25, 10, 8],
|
||||||
|
floor: 0.3,
|
||||||
|
segments: 30,
|
||||||
|
receiveShadow: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#e8e0f0", intensity: 0.25 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"key-light": {
|
||||||
|
type: "SpotLight",
|
||||||
|
props: {
|
||||||
|
position: [5, 8, 3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffffff",
|
||||||
|
intensity: 80,
|
||||||
|
distance: 25,
|
||||||
|
decay: null,
|
||||||
|
angle: 0.5,
|
||||||
|
penumbra: 0.8,
|
||||||
|
castShadow: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"fill-light": {
|
||||||
|
type: "SpotLight",
|
||||||
|
props: {
|
||||||
|
position: [-4, 5, 2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#aaccff",
|
||||||
|
intensity: 30,
|
||||||
|
distance: 20,
|
||||||
|
decay: null,
|
||||||
|
angle: 0.6,
|
||||||
|
penumbra: 1,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"rim-light": {
|
||||||
|
type: "SpotLight",
|
||||||
|
props: {
|
||||||
|
position: [0, 3, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffddcc",
|
||||||
|
intensity: 40,
|
||||||
|
distance: 15,
|
||||||
|
decay: null,
|
||||||
|
angle: 0.4,
|
||||||
|
penumbra: 0.5,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"accent-light": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#ffffff",
|
||||||
|
intensity: 5,
|
||||||
|
distance: 4,
|
||||||
|
decay: null,
|
||||||
|
castShadow: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
pedestal: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: [0, 0, 0], rotation: null, scale: null },
|
||||||
|
children: ["ped-base", "ped-column", "ped-top"],
|
||||||
|
},
|
||||||
|
"ped-base": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.05, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radiusTop: 1.2,
|
||||||
|
radiusBottom: 1.3,
|
||||||
|
height: 0.1,
|
||||||
|
radialSegments: 48,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: {
|
||||||
|
color: "#1a1a1a",
|
||||||
|
metalness: 0.9,
|
||||||
|
roughness: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"ped-column": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.45, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radiusTop: 0.6,
|
||||||
|
radiusBottom: 0.8,
|
||||||
|
height: 0.7,
|
||||||
|
radialSegments: 48,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: {
|
||||||
|
color: "#111111",
|
||||||
|
metalness: 0.95,
|
||||||
|
roughness: 0.05,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"ped-top": {
|
||||||
|
type: "Cylinder",
|
||||||
|
props: {
|
||||||
|
position: [0, 0.85, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
radiusTop: 0.9,
|
||||||
|
radiusBottom: 0.7,
|
||||||
|
height: 0.1,
|
||||||
|
radialSegments: 48,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
material: {
|
||||||
|
color: "#1a1a1a",
|
||||||
|
metalness: 0.9,
|
||||||
|
roughness: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"hero-float": {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [0, 2.2, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 1.5,
|
||||||
|
rotationIntensity: 0.3,
|
||||||
|
floatIntensity: 0.4,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["hero-spin"],
|
||||||
|
},
|
||||||
|
"hero-spin": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.3,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["hero-object"],
|
||||||
|
},
|
||||||
|
"hero-object": {
|
||||||
|
type: "TorusKnot",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.6,
|
||||||
|
tube: 0.22,
|
||||||
|
p: 2,
|
||||||
|
q: 3,
|
||||||
|
tubularSegments: 128,
|
||||||
|
radialSegments: 24,
|
||||||
|
material: {
|
||||||
|
color: "#ffffff",
|
||||||
|
metalness: 1,
|
||||||
|
roughness: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
accent1: {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [-1.5, 1.8, 0.5],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 2,
|
||||||
|
rotationIntensity: 0.5,
|
||||||
|
floatIntensity: 1.2,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["accent1-body"],
|
||||||
|
},
|
||||||
|
"accent1-body": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.15,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
color: "#eeeeff",
|
||||||
|
transmission: 1,
|
||||||
|
thickness: 0.3,
|
||||||
|
roughness: 0,
|
||||||
|
chromaticAberration: 0.06,
|
||||||
|
ior: 1.5,
|
||||||
|
distortion: null,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
accent2: {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [1.2, 2.5, -0.8],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 1.8,
|
||||||
|
rotationIntensity: 0.4,
|
||||||
|
floatIntensity: 1.5,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["accent2-body"],
|
||||||
|
},
|
||||||
|
"accent2-body": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.1,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
color: "#ffeeff",
|
||||||
|
transmission: 1,
|
||||||
|
thickness: 0.2,
|
||||||
|
roughness: 0,
|
||||||
|
chromaticAberration: 0.08,
|
||||||
|
ior: 1.6,
|
||||||
|
distortion: null,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
accent3: {
|
||||||
|
type: "Float",
|
||||||
|
props: {
|
||||||
|
position: [0.8, 1.5, 1.2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 2.2,
|
||||||
|
rotationIntensity: 0.6,
|
||||||
|
floatIntensity: 0.8,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
children: ["accent3-body"],
|
||||||
|
},
|
||||||
|
"accent3-body": {
|
||||||
|
type: "GlassSphere",
|
||||||
|
props: {
|
||||||
|
position: null,
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
castShadow: null,
|
||||||
|
receiveShadow: null,
|
||||||
|
radius: 0.12,
|
||||||
|
widthSegments: null,
|
||||||
|
heightSegments: null,
|
||||||
|
color: "#eeffee",
|
||||||
|
transmission: 1,
|
||||||
|
thickness: 0.25,
|
||||||
|
roughness: 0,
|
||||||
|
chromaticAberration: 0.05,
|
||||||
|
ior: 1.4,
|
||||||
|
distortion: null,
|
||||||
|
distortionScale: null,
|
||||||
|
temporalDistortion: null,
|
||||||
|
samples: 6,
|
||||||
|
resolution: 128,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
floor: {
|
||||||
|
type: "ReflectorPlane",
|
||||||
|
props: {
|
||||||
|
position: [0, -0.5, 0],
|
||||||
|
rotation: [-1.5708, 0, 0],
|
||||||
|
scale: null,
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
color: "#0a0a0a",
|
||||||
|
resolution: 1024,
|
||||||
|
blur: 300,
|
||||||
|
mirror: 0.6,
|
||||||
|
mixBlur: 8,
|
||||||
|
mixStrength: 2,
|
||||||
|
depthScale: 0.2,
|
||||||
|
metalness: 0.8,
|
||||||
|
roughness: 0.3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shadows: {
|
||||||
|
type: "ContactShadows",
|
||||||
|
props: {
|
||||||
|
position: [0, -0.49, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
opacity: 0.5,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
blur: 2,
|
||||||
|
near: null,
|
||||||
|
far: 5,
|
||||||
|
smooth: true,
|
||||||
|
resolution: 512,
|
||||||
|
frames: null,
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sparkles: {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 2, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: [6, 4, 6],
|
||||||
|
count: 30,
|
||||||
|
speed: 0.05,
|
||||||
|
opacity: 0.15,
|
||||||
|
color: "#ffffff",
|
||||||
|
size: 0.3,
|
||||||
|
noise: 0.5,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
fx: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 0.5,
|
||||||
|
luminanceThreshold: 0.3,
|
||||||
|
luminanceSmoothing: 0.4,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.3, darkness: 0.5 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: true,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: null,
|
||||||
|
enablePan: null,
|
||||||
|
enableRotate: null,
|
||||||
|
minDistance: 3,
|
||||||
|
maxDistance: 12,
|
||||||
|
minPolarAngle: 0.3,
|
||||||
|
maxPolarAngle: 1.4,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 1,
|
||||||
|
target: [0, 1.5, 0],
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const starfield: Scene = {
|
||||||
|
name: "Starfield",
|
||||||
|
description:
|
||||||
|
"Classic Windows starfield screensaver -- flying through stars at warp speed",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"cam",
|
||||||
|
"ambient",
|
||||||
|
"tunnel",
|
||||||
|
"stars1",
|
||||||
|
"stars2",
|
||||||
|
"sparkle-dust",
|
||||||
|
"shake",
|
||||||
|
"post",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cam: {
|
||||||
|
type: "PerspectiveCamera",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
fov: 75,
|
||||||
|
near: 0.1,
|
||||||
|
far: 1000,
|
||||||
|
makeDefault: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#111122", intensity: 0.15 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
tunnel: {
|
||||||
|
type: "WarpTunnel",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
ringCount: 120,
|
||||||
|
radius: 4,
|
||||||
|
length: 40,
|
||||||
|
speed: 12,
|
||||||
|
tubeRadius: 0.008,
|
||||||
|
color1: "#ffffff",
|
||||||
|
color2: "#aabbff",
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
stars1: {
|
||||||
|
type: "Stars",
|
||||||
|
props: {
|
||||||
|
radius: 50,
|
||||||
|
depth: 80,
|
||||||
|
count: 10000,
|
||||||
|
factor: 6,
|
||||||
|
saturation: 0,
|
||||||
|
fade: true,
|
||||||
|
speed: 3,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
stars2: {
|
||||||
|
type: "Stars",
|
||||||
|
props: {
|
||||||
|
radius: 30,
|
||||||
|
depth: 50,
|
||||||
|
count: 5000,
|
||||||
|
factor: 3,
|
||||||
|
saturation: 0.1,
|
||||||
|
fade: true,
|
||||||
|
speed: 5,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"sparkle-dust": {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, -5],
|
||||||
|
rotation: null,
|
||||||
|
scale: [8, 8, 30],
|
||||||
|
count: 300,
|
||||||
|
speed: 2,
|
||||||
|
opacity: 0.6,
|
||||||
|
color: "#ffffff",
|
||||||
|
size: 1,
|
||||||
|
noise: 4,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shake: {
|
||||||
|
type: "CameraShake",
|
||||||
|
props: {
|
||||||
|
intensity: 0.15,
|
||||||
|
maxYaw: 0.02,
|
||||||
|
maxPitch: 0.02,
|
||||||
|
maxRoll: 0.01,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 1.5,
|
||||||
|
luminanceThreshold: 0.15,
|
||||||
|
luminanceSmoothing: null,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.1, darkness: 0.9 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: null,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: false,
|
||||||
|
enablePan: false,
|
||||||
|
enableRotate: false,
|
||||||
|
minDistance: null,
|
||||||
|
maxDistance: null,
|
||||||
|
minPolarAngle: null,
|
||||||
|
maxPolarAngle: null,
|
||||||
|
autoRotate: false,
|
||||||
|
autoRotateSpeed: null,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
import type { Scene } from "./_helpers";
|
||||||
|
|
||||||
|
export const stormCell: Scene = {
|
||||||
|
name: "Storm Cell",
|
||||||
|
description:
|
||||||
|
"Dark violent thunderstorm -- spinning cloud masses, pulsing lightning, rain particles, camera shake",
|
||||||
|
spec: {
|
||||||
|
root: "scene",
|
||||||
|
elements: {
|
||||||
|
scene: {
|
||||||
|
type: "Group",
|
||||||
|
props: { position: null, rotation: null, scale: null },
|
||||||
|
children: [
|
||||||
|
"sky",
|
||||||
|
"ambient",
|
||||||
|
"sun",
|
||||||
|
"cloud-spin-1",
|
||||||
|
"cloud-spin-2",
|
||||||
|
"cloud-spin-3",
|
||||||
|
"lightning-1-pulse",
|
||||||
|
"lightning-2-pulse",
|
||||||
|
"lightning-3-pulse",
|
||||||
|
"rain",
|
||||||
|
"shake",
|
||||||
|
"post",
|
||||||
|
"fog",
|
||||||
|
"controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sky: {
|
||||||
|
type: "Sky",
|
||||||
|
props: {
|
||||||
|
distance: null,
|
||||||
|
sunPosition: [10, 2, 0],
|
||||||
|
inclination: null,
|
||||||
|
azimuth: null,
|
||||||
|
turbidity: 20,
|
||||||
|
rayleigh: 0.5,
|
||||||
|
mieCoefficient: 0.01,
|
||||||
|
mieDirectionalG: 0.9,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
ambient: {
|
||||||
|
type: "AmbientLight",
|
||||||
|
props: { color: "#111122", intensity: 0.15 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
sun: {
|
||||||
|
type: "DirectionalLight",
|
||||||
|
props: {
|
||||||
|
position: [10, 3, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#334466",
|
||||||
|
intensity: 0.5,
|
||||||
|
castShadow: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"cloud-spin-1": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: [0, 4, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.08,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["cloud-1"],
|
||||||
|
},
|
||||||
|
"cloud-1": {
|
||||||
|
type: "Cloud",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
seed: 1,
|
||||||
|
segments: 40,
|
||||||
|
bounds: [25, 3, 25],
|
||||||
|
volume: 15,
|
||||||
|
speed: 0.3,
|
||||||
|
fade: 20,
|
||||||
|
opacity: 0.9,
|
||||||
|
color: "#222233",
|
||||||
|
growth: 10,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"cloud-spin-2": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: [0, 6, -3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: -0.05,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["cloud-2"],
|
||||||
|
},
|
||||||
|
"cloud-2": {
|
||||||
|
type: "Cloud",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
seed: 42,
|
||||||
|
segments: 30,
|
||||||
|
bounds: [20, 2, 20],
|
||||||
|
volume: 12,
|
||||||
|
speed: 0.2,
|
||||||
|
fade: 15,
|
||||||
|
opacity: 0.7,
|
||||||
|
color: "#1a1a2a",
|
||||||
|
growth: 8,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"cloud-spin-3": {
|
||||||
|
type: "Spin",
|
||||||
|
props: {
|
||||||
|
position: [0, 8, 2],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 0.03,
|
||||||
|
axis: "y",
|
||||||
|
},
|
||||||
|
children: ["cloud-3"],
|
||||||
|
},
|
||||||
|
"cloud-3": {
|
||||||
|
type: "Cloud",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
seed: 99,
|
||||||
|
segments: 25,
|
||||||
|
bounds: [30, 2, 30],
|
||||||
|
volume: 10,
|
||||||
|
speed: 0.15,
|
||||||
|
fade: 25,
|
||||||
|
opacity: 0.5,
|
||||||
|
color: "#2a2a3a",
|
||||||
|
growth: 6,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"lightning-1-pulse": {
|
||||||
|
type: "Pulse",
|
||||||
|
props: {
|
||||||
|
position: [2, 5, -1],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 3,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
children: ["lightning-1"],
|
||||||
|
},
|
||||||
|
"lightning-1": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#aabbff",
|
||||||
|
intensity: 80,
|
||||||
|
distance: 30,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"lightning-2-pulse": {
|
||||||
|
type: "Pulse",
|
||||||
|
props: {
|
||||||
|
position: [-3, 6, 3],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 4.5,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
children: ["lightning-2"],
|
||||||
|
},
|
||||||
|
"lightning-2": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#aaccff",
|
||||||
|
intensity: 60,
|
||||||
|
distance: 25,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
"lightning-3-pulse": {
|
||||||
|
type: "Pulse",
|
||||||
|
props: {
|
||||||
|
position: [0, 4, -4],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
speed: 2.5,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
children: ["lightning-3"],
|
||||||
|
},
|
||||||
|
"lightning-3": {
|
||||||
|
type: "PointLight",
|
||||||
|
props: {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: null,
|
||||||
|
color: "#99bbff",
|
||||||
|
intensity: 40,
|
||||||
|
distance: 20,
|
||||||
|
decay: 2,
|
||||||
|
castShadow: false,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
rain: {
|
||||||
|
type: "Sparkles",
|
||||||
|
props: {
|
||||||
|
position: [0, 5, 0],
|
||||||
|
rotation: null,
|
||||||
|
scale: [20, 15, 20],
|
||||||
|
count: 500,
|
||||||
|
speed: 5,
|
||||||
|
opacity: 0.4,
|
||||||
|
color: "#6688aa",
|
||||||
|
size: 0.5,
|
||||||
|
noise: 0.5,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
shake: {
|
||||||
|
type: "CameraShake",
|
||||||
|
props: {
|
||||||
|
intensity: 0.2,
|
||||||
|
maxYaw: 0.02,
|
||||||
|
maxPitch: 0.03,
|
||||||
|
maxRoll: 0.01,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: "EffectComposer",
|
||||||
|
props: { enabled: true, multisampling: 8 },
|
||||||
|
children: ["bloom", "vignette"],
|
||||||
|
},
|
||||||
|
bloom: {
|
||||||
|
type: "Bloom",
|
||||||
|
props: {
|
||||||
|
intensity: 0.8,
|
||||||
|
luminanceThreshold: 0.3,
|
||||||
|
luminanceSmoothing: null,
|
||||||
|
mipmapBlur: true,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
vignette: {
|
||||||
|
type: "Vignette",
|
||||||
|
props: { offset: 0.3, darkness: 0.8 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
fog: {
|
||||||
|
type: "Fog",
|
||||||
|
props: { color: "#0a0a15", near: 5, far: 30 },
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
type: "OrbitControls",
|
||||||
|
props: {
|
||||||
|
enableDamping: null,
|
||||||
|
dampingFactor: null,
|
||||||
|
enableZoom: true,
|
||||||
|
enablePan: null,
|
||||||
|
enableRotate: true,
|
||||||
|
minDistance: 3,
|
||||||
|
maxDistance: 20,
|
||||||
|
minPolarAngle: 0.2,
|
||||||
|
maxPolarAngle: 1.3,
|
||||||
|
autoRotate: true,
|
||||||
|
autoRotateSpeed: 0.1,
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { nextJsConfig } from "@internal/eslint-config/next-js";
|
||||||
|
|
||||||
|
/** @type {import("eslint").Linter.Config[]} */
|
||||||
|
export default [
|
||||||
|
...nextJsConfig,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react/no-unknown-property": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
+6
@@ -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.
|
||||||
@@ -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;
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-remotion
|
# 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
|
## 0.1.6
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-remotion",
|
"name": "example-remotion",
|
||||||
"version": "0.1.6",
|
"version": "0.1.8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>json-render solid example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "example-solid",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "portless solid-demo.json-render vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check-types": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@json-render/core": "workspace:*",
|
||||||
|
"@json-render/solid": "workspace:*",
|
||||||
|
"solid-js": "^1.9.11",
|
||||||
|
"zod": "^4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.1",
|
||||||
|
"vite-plugin-solid": "^2.11.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { StateProvider } from "@json-render/solid";
|
||||||
|
import { demoSpec } from "./lib/spec";
|
||||||
|
import { DemoRenderer } from "./DemoRenderer";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const initialState = demoSpec.state ?? {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StateProvider initialState={initialState}>
|
||||||
|
<DemoRenderer />
|
||||||
|
</StateProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
ActionProvider,
|
||||||
|
ValidationProvider,
|
||||||
|
VisibilityProvider,
|
||||||
|
Renderer,
|
||||||
|
useStateStore,
|
||||||
|
} from "@json-render/solid";
|
||||||
|
import { demoSpec } from "./lib/spec";
|
||||||
|
import { registry } from "./lib/registry";
|
||||||
|
|
||||||
|
export function DemoRenderer() {
|
||||||
|
const stateStore = useStateStore();
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
increment: async () => {
|
||||||
|
stateStore.set("/count", Number(stateStore.get("/count") || 0) + 1);
|
||||||
|
},
|
||||||
|
decrement: async () => {
|
||||||
|
stateStore.set(
|
||||||
|
"/count",
|
||||||
|
Math.max(0, Number(stateStore.get("/count") || 0) - 1),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
reset: async () => {
|
||||||
|
stateStore.set("/count", 0);
|
||||||
|
},
|
||||||
|
toggleItem: async (params: Record<string, unknown>) => {
|
||||||
|
const index = params.index as number;
|
||||||
|
const todos = (
|
||||||
|
stateStore.get("/todos") as Array<{
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
completed: boolean;
|
||||||
|
}>
|
||||||
|
).map((item, i) =>
|
||||||
|
i === index ? { ...item, completed: !item.completed } : item,
|
||||||
|
);
|
||||||
|
stateStore.set("/todos", todos);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionProvider handlers={handlers}>
|
||||||
|
<VisibilityProvider>
|
||||||
|
<ValidationProvider>
|
||||||
|
<Renderer spec={demoSpec} registry={registry} />
|
||||||
|
</ValidationProvider>
|
||||||
|
</VisibilityProvider>
|
||||||
|
</ActionProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #111827;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { render } from "solid-js/web";
|
||||||
|
import { App } from "./App";
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
|
render(() => <App />, document.getElementById("app")!);
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { schema } from "@json-render/solid/schema";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const catalog = schema.createCatalog({
|
||||||
|
components: {
|
||||||
|
Stack: {
|
||||||
|
props: z.object({
|
||||||
|
gap: z.number().optional(),
|
||||||
|
padding: z.number().optional(),
|
||||||
|
direction: z.enum(["vertical", "horizontal"]).optional(),
|
||||||
|
align: z.enum(["start", "center", "end"]).optional(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description:
|
||||||
|
"Layout container that stacks children vertically or horizontally",
|
||||||
|
},
|
||||||
|
Card: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
subtitle: z.string().optional(),
|
||||||
|
}),
|
||||||
|
slots: ["default"],
|
||||||
|
description: "A card container with optional title and subtitle",
|
||||||
|
},
|
||||||
|
Text: {
|
||||||
|
props: z.object({
|
||||||
|
content: z.string(),
|
||||||
|
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
|
||||||
|
weight: z.enum(["normal", "medium", "bold"]).optional(),
|
||||||
|
color: z.string().optional(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description: "Displays a text string",
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
variant: z.enum(["primary", "secondary", "danger"]).optional(),
|
||||||
|
disabled: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description: "A clickable button that emits a 'press' event",
|
||||||
|
},
|
||||||
|
Badge: {
|
||||||
|
props: z.object({
|
||||||
|
label: z.string(),
|
||||||
|
color: z.string().optional(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description: "A small badge/tag label",
|
||||||
|
},
|
||||||
|
ListItem: {
|
||||||
|
props: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
completed: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description: "A single item in a list",
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
props: z.object({
|
||||||
|
value: z.string().optional(),
|
||||||
|
placeholder: z.string().optional(),
|
||||||
|
}),
|
||||||
|
slots: [],
|
||||||
|
description: "A text input field that supports two-way state binding",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
increment: {
|
||||||
|
params: z.object({}),
|
||||||
|
description: "Increment the counter by 1",
|
||||||
|
},
|
||||||
|
decrement: {
|
||||||
|
params: z.object({}),
|
||||||
|
description: "Decrement the counter by 1",
|
||||||
|
},
|
||||||
|
reset: {
|
||||||
|
params: z.object({}),
|
||||||
|
description: "Reset the counter to 0",
|
||||||
|
},
|
||||||
|
toggleItem: {
|
||||||
|
params: z.object({
|
||||||
|
index: z.number(),
|
||||||
|
}),
|
||||||
|
description: "Toggle the completed state of a todo item",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { BaseComponentProps } from "@json-render/solid";
|
||||||
|
|
||||||
|
interface BadgeProps {
|
||||||
|
label: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Badge(renderProps: BaseComponentProps<BadgeProps>) {
|
||||||
|
const color = () => renderProps.props.color ?? "#6b7280";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
padding: "4px 12px",
|
||||||
|
"border-radius": "9999px",
|
||||||
|
"font-size": "13px",
|
||||||
|
"font-weight": "500",
|
||||||
|
"background-color": `${color()}20`,
|
||||||
|
color: color(),
|
||||||
|
border: `1px solid ${color()}40`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderProps.props.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { BaseComponentProps } from "@json-render/solid";
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
label: string;
|
||||||
|
variant?: "primary" | "secondary" | "danger";
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantStyles: Record<string, Record<string, string>> = {
|
||||||
|
primary: {
|
||||||
|
background: "#2563eb",
|
||||||
|
color: "#ffffff",
|
||||||
|
border: "1px solid #2563eb",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
background: "#f3f4f6",
|
||||||
|
color: "#374151",
|
||||||
|
border: "1px solid #d1d5db",
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
background: "#fee2e2",
|
||||||
|
color: "#dc2626",
|
||||||
|
border: "1px solid #fca5a5",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Button(renderProps: BaseComponentProps<ButtonProps>) {
|
||||||
|
const variant = () => renderProps.props.variant ?? "primary";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={renderProps.props.disabled}
|
||||||
|
onClick={() => renderProps.emit("press")}
|
||||||
|
style={{
|
||||||
|
padding: "8px 16px",
|
||||||
|
"border-radius": "8px",
|
||||||
|
"font-size": "14px",
|
||||||
|
"font-weight": "500",
|
||||||
|
cursor: renderProps.props.disabled ? "not-allowed" : "pointer",
|
||||||
|
opacity: renderProps.props.disabled ? "0.5" : "1",
|
||||||
|
...variantStyles[variant()],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderProps.props.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type { BaseComponentProps } from "@json-render/solid";
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card(renderProps: BaseComponentProps<CardProps>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#ffffff",
|
||||||
|
"border-radius": "12px",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
padding: "20px",
|
||||||
|
"box-shadow": "0 1px 3px rgba(0,0,0,0.06)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderProps.props.title && (
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
"font-size": "18px",
|
||||||
|
"font-weight": "600",
|
||||||
|
"margin-bottom": renderProps.props.subtitle ? "4px" : "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderProps.props.title}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{renderProps.props.subtitle && (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
"font-size": "14px",
|
||||||
|
color: "#6b7280",
|
||||||
|
"margin-bottom": "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderProps.props.subtitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{renderProps.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { BaseComponentProps } from "@json-render/solid";
|
||||||
|
import { useBoundProp } from "@json-render/solid";
|
||||||
|
|
||||||
|
interface InputProps {
|
||||||
|
value?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Input(renderProps: BaseComponentProps<InputProps>) {
|
||||||
|
const [value, setValue] = useBoundProp<string>(
|
||||||
|
renderProps.props.value,
|
||||||
|
renderProps.bindings?.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
value={String(value ?? "")}
|
||||||
|
onInput={(e) => setValue(e.currentTarget.value)}
|
||||||
|
placeholder={renderProps.props.placeholder ?? ""}
|
||||||
|
style={{
|
||||||
|
padding: "8px 12px",
|
||||||
|
"border-radius": "8px",
|
||||||
|
border: "1px solid #d1d5db",
|
||||||
|
"font-size": "14px",
|
||||||
|
outline: "none",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { BaseComponentProps } from "@json-render/solid";
|
||||||
|
|
||||||
|
interface ListItemProps {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
completed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListItem(renderProps: BaseComponentProps<ListItemProps>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => renderProps.emit("press")}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "10px 12px",
|
||||||
|
"border-radius": "8px",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
cursor: "pointer",
|
||||||
|
"user-select": "none",
|
||||||
|
background: renderProps.props.completed ? "#f0fdf4" : "#ffffff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: "22px",
|
||||||
|
height: "22px",
|
||||||
|
"border-radius": "6px",
|
||||||
|
border: renderProps.props.completed
|
||||||
|
? "2px solid #22c55e"
|
||||||
|
: "2px solid #d1d5db",
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "center",
|
||||||
|
"font-size": "13px",
|
||||||
|
color: "#22c55e",
|
||||||
|
"flex-shrink": "0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderProps.props.completed ? "\u2713" : ""}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
"text-decoration": renderProps.props.completed
|
||||||
|
? "line-through"
|
||||||
|
: "none",
|
||||||
|
color: renderProps.props.completed ? "#9ca3af" : "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderProps.props.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { BaseComponentProps } from "@json-render/solid";
|
||||||
|
|
||||||
|
interface StackProps {
|
||||||
|
gap?: number;
|
||||||
|
padding?: number;
|
||||||
|
direction?: "vertical" | "horizontal";
|
||||||
|
align?: "start" | "center" | "end";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Stack(renderProps: BaseComponentProps<StackProps>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
"flex-direction":
|
||||||
|
renderProps.props.direction === "horizontal" ? "row" : "column",
|
||||||
|
gap: renderProps.props.gap ? `${renderProps.props.gap}px` : undefined,
|
||||||
|
padding: renderProps.props.padding
|
||||||
|
? `${renderProps.props.padding}px`
|
||||||
|
: undefined,
|
||||||
|
"align-items": renderProps.props.align,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderProps.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { BaseComponentProps } from "@json-render/solid";
|
||||||
|
|
||||||
|
interface TextProps {
|
||||||
|
content: string;
|
||||||
|
size?: "sm" | "md" | "lg" | "xl";
|
||||||
|
weight?: "normal" | "medium" | "bold";
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeMap: Record<string, string> = {
|
||||||
|
sm: "12px",
|
||||||
|
md: "14px",
|
||||||
|
lg: "18px",
|
||||||
|
xl: "24px",
|
||||||
|
};
|
||||||
|
|
||||||
|
const weightMap: Record<string, string> = {
|
||||||
|
normal: "400",
|
||||||
|
medium: "500",
|
||||||
|
bold: "700",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Text(renderProps: BaseComponentProps<TextProps>) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
"font-size": sizeMap[renderProps.props.size ?? "md"],
|
||||||
|
"font-weight": weightMap[renderProps.props.weight ?? "normal"],
|
||||||
|
color: renderProps.props.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{String(renderProps.props.content ?? "")}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { type Components, defineRegistry } from "@json-render/solid";
|
||||||
|
import { catalog } from "./catalog";
|
||||||
|
import { Stack } from "./components/Stack";
|
||||||
|
import { Card } from "./components/Card";
|
||||||
|
import { Text } from "./components/Text";
|
||||||
|
import { Button } from "./components/Button";
|
||||||
|
import { Badge } from "./components/Badge";
|
||||||
|
import { ListItem } from "./components/ListItem";
|
||||||
|
import { Input } from "./components/Input";
|
||||||
|
|
||||||
|
const components: Components<typeof catalog> = {
|
||||||
|
Stack,
|
||||||
|
Card,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
Badge,
|
||||||
|
ListItem,
|
||||||
|
Input,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const { registry, handlers: makeHandlers } = defineRegistry(catalog, {
|
||||||
|
components,
|
||||||
|
actions: {
|
||||||
|
increment: async () => {},
|
||||||
|
decrement: async () => {},
|
||||||
|
reset: async () => {},
|
||||||
|
toggleItem: async () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
|
||||||
|
export const demoSpec: Spec = {
|
||||||
|
root: "root",
|
||||||
|
state: {
|
||||||
|
count: 0,
|
||||||
|
name: "",
|
||||||
|
todos: [
|
||||||
|
{ id: 1, title: "Learn SolidJS", completed: true },
|
||||||
|
{ id: 2, title: "Try @json-render/solid", completed: false },
|
||||||
|
{ id: 3, title: "Build something awesome", completed: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
root: {
|
||||||
|
type: "Stack",
|
||||||
|
props: { gap: 24, padding: 24, direction: "vertical" },
|
||||||
|
children: [
|
||||||
|
"header",
|
||||||
|
"counter-card",
|
||||||
|
"milestone-badge",
|
||||||
|
"todos-card",
|
||||||
|
"input-card",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
type: "Text",
|
||||||
|
props: {
|
||||||
|
content: "@json-render/solid demo",
|
||||||
|
size: "xl",
|
||||||
|
weight: "bold",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"counter-card": {
|
||||||
|
type: "Card",
|
||||||
|
props: {
|
||||||
|
title: "Counter",
|
||||||
|
subtitle: "Click the buttons to change the count",
|
||||||
|
},
|
||||||
|
children: ["counter-body"],
|
||||||
|
},
|
||||||
|
"counter-body": {
|
||||||
|
type: "Stack",
|
||||||
|
props: { gap: 12, direction: "horizontal", align: "center" },
|
||||||
|
children: [
|
||||||
|
"decrement-btn",
|
||||||
|
"counter-value",
|
||||||
|
"increment-btn",
|
||||||
|
"reset-btn",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"decrement-btn": {
|
||||||
|
type: "Button",
|
||||||
|
props: { label: "\u2212", variant: "secondary" },
|
||||||
|
on: { press: { action: "decrement" } },
|
||||||
|
},
|
||||||
|
"counter-value": {
|
||||||
|
type: "Text",
|
||||||
|
props: {
|
||||||
|
content: { $state: "/count" },
|
||||||
|
size: "xl",
|
||||||
|
weight: "bold",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"increment-btn": {
|
||||||
|
type: "Button",
|
||||||
|
props: { label: "+", variant: "primary" },
|
||||||
|
on: { press: { action: "increment" } },
|
||||||
|
},
|
||||||
|
"reset-btn": {
|
||||||
|
type: "Button",
|
||||||
|
props: { label: "Reset", variant: "danger" },
|
||||||
|
on: { press: { action: "reset" } },
|
||||||
|
},
|
||||||
|
"milestone-badge": {
|
||||||
|
type: "Badge",
|
||||||
|
props: { label: "Milestone reached: 10!", color: "#10b981" },
|
||||||
|
visible: { $state: "/count", gte: 10 },
|
||||||
|
},
|
||||||
|
"todos-card": {
|
||||||
|
type: "Card",
|
||||||
|
props: { title: "Todo List", subtitle: "Your tasks" },
|
||||||
|
children: ["todos-list"],
|
||||||
|
},
|
||||||
|
"todos-list": {
|
||||||
|
type: "Stack",
|
||||||
|
props: { gap: 8, direction: "vertical" },
|
||||||
|
repeat: { statePath: "/todos", key: "id" },
|
||||||
|
children: ["todo-item"],
|
||||||
|
},
|
||||||
|
"todo-item": {
|
||||||
|
type: "ListItem",
|
||||||
|
props: {
|
||||||
|
title: { $item: "title" },
|
||||||
|
completed: { $item: "completed" },
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
press: { action: "toggleItem", params: { index: { $index: true } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"input-card": {
|
||||||
|
type: "Card",
|
||||||
|
props: {
|
||||||
|
title: "Bound Input",
|
||||||
|
subtitle: "Type to update state and see reactive text",
|
||||||
|
},
|
||||||
|
children: ["input-body"],
|
||||||
|
},
|
||||||
|
"input-body": {
|
||||||
|
type: "Stack",
|
||||||
|
props: { gap: 12, direction: "vertical" },
|
||||||
|
children: ["name-input", "name-display"],
|
||||||
|
},
|
||||||
|
"name-input": {
|
||||||
|
type: "Input",
|
||||||
|
props: {
|
||||||
|
value: { $bindState: "/name" },
|
||||||
|
placeholder: "Enter your name...",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"name-display": {
|
||||||
|
type: "Text",
|
||||||
|
props: {
|
||||||
|
content: { $state: "/name" },
|
||||||
|
size: "md",
|
||||||
|
color: "#6b7280",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import solid from "vite-plugin-solid";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solid()],
|
||||||
|
});
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
# com.example.json-render-demo
|
# 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
|
## 0.0.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-demo",
|
"name": "com.example.json-render-demo",
|
||||||
"version": "0.0.7",
|
"version": "0.0.9",
|
||||||
"description": "Test",
|
"description": "Test",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# com.example.json-render-fullpage-demo
|
# 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
|
## 0.0.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "com.example.json-render-fullpage-demo",
|
"name": "com.example.json-render-fullpage-demo",
|
||||||
"version": "0.0.7",
|
"version": "0.0.9",
|
||||||
"description": "Full-page Stripe App example (alpha)",
|
"description": "Full-page Stripe App example (alpha)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "~~proprietary~~",
|
"license": "~~proprietary~~",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# svelte-chat
|
# svelte-chat
|
||||||
|
|
||||||
|
## 0.0.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/svelte@0.14.0
|
||||||
|
|
||||||
|
## 0.0.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [5b32de8]
|
||||||
|
- @json-render/core@0.13.0
|
||||||
|
- @json-render/svelte@0.13.0
|
||||||
|
|
||||||
## 0.0.3
|
## 0.0.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "svelte-chat",
|
"name": "svelte-chat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.3",
|
"version": "0.0.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# example-svelte
|
# example-svelte
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/svelte@0.14.0
|
||||||
|
|
||||||
|
## 0.1.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [5b32de8]
|
||||||
|
- @json-render/core@0.13.0
|
||||||
|
- @json-render/svelte@0.13.0
|
||||||
|
|
||||||
## 0.1.3
|
## 0.1.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "example-svelte",
|
"name": "example-svelte",
|
||||||
"version": "0.1.3",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
# vite-renderers
|
# vite-renderers
|
||||||
|
|
||||||
|
## 0.1.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [a8afd8b]
|
||||||
|
- @json-render/core@0.14.0
|
||||||
|
- @json-render/react@0.14.0
|
||||||
|
- @json-render/solid@0.14.0
|
||||||
|
- @json-render/svelte@0.14.0
|
||||||
|
- @json-render/vue@0.14.0
|
||||||
|
|
||||||
|
## 0.1.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [5b32de8]
|
||||||
|
- @json-render/core@0.13.0
|
||||||
|
- @json-render/solid@0.13.0
|
||||||
|
- @json-render/react@0.13.0
|
||||||
|
- @json-render/svelte@0.13.0
|
||||||
|
- @json-render/vue@0.13.0
|
||||||
|
|
||||||
## 0.1.4
|
## 0.1.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vite-renderers",
|
"name": "vite-renderers",
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,10 +12,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@json-render/core": "workspace:*",
|
"@json-render/core": "workspace:*",
|
||||||
"@json-render/react": "workspace:*",
|
"@json-render/react": "workspace:*",
|
||||||
|
"@json-render/solid": "workspace:*",
|
||||||
"@json-render/svelte": "workspace:*",
|
"@json-render/svelte": "workspace:*",
|
||||||
"@json-render/vue": "workspace:*",
|
"@json-render/vue": "workspace:*",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
"solid-js": "^1.9.11",
|
||||||
"svelte": "^5.49.2",
|
"svelte": "^5.49.2",
|
||||||
"vue": "^3.5.29",
|
"vue": "^3.5.29",
|
||||||
"zod": "4.3.6"
|
"zod": "4.3.6"
|
||||||
@@ -27,6 +29,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@vitejs/plugin-react": "^5.1.4",
|
||||||
"@vitejs/plugin-vue": "^6.0.4",
|
"@vitejs/plugin-vue": "^6.0.4",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1",
|
||||||
|
"vite-plugin-solid": "^2.11.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "./shared/styles.css";
|
import "./shared/styles.css";
|
||||||
import { demoSpec } from "./spec";
|
import { demoSpec } from "./spec";
|
||||||
|
|
||||||
type Renderer = "vue" | "react" | "svelte";
|
type Renderer = "vue" | "react" | "svelte" | "solid";
|
||||||
|
|
||||||
const container = document.getElementById("renderer-root") as HTMLElement;
|
const container = document.getElementById("renderer-root") as HTMLElement;
|
||||||
|
|
||||||
@@ -18,10 +18,14 @@ async function switchTo(renderer: Renderer) {
|
|||||||
const mod = await import("./react/mount.tsx");
|
const mod = await import("./react/mount.tsx");
|
||||||
mod.mount(container, renderer, demoSpec);
|
mod.mount(container, renderer, demoSpec);
|
||||||
unmountCurrent = mod.unmount;
|
unmountCurrent = mod.unmount;
|
||||||
} else {
|
} else if (renderer === "svelte") {
|
||||||
const mod = await import("./svelte/mount.ts");
|
const mod = await import("./svelte/mount.ts");
|
||||||
mod.mount(container, renderer, demoSpec);
|
mod.mount(container, renderer, demoSpec);
|
||||||
unmountCurrent = mod.unmount;
|
unmountCurrent = mod.unmount;
|
||||||
|
} else {
|
||||||
|
const mod = await import("./solid/mount.tsx");
|
||||||
|
mod.mount(container, renderer, demoSpec);
|
||||||
|
unmountCurrent = mod.unmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,9 @@ export const components: Components<AppCatalog> = {
|
|||||||
? "Rendered with Vue"
|
? "Rendered with Vue"
|
||||||
: props.renderer === "react"
|
: props.renderer === "react"
|
||||||
? "Rendered with React"
|
? "Rendered with React"
|
||||||
: "Rendered with Svelte"}
|
: props.renderer === "svelte"
|
||||||
|
? "Rendered with Svelte"
|
||||||
|
: "Rendered with Solid"}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -164,6 +166,17 @@ export const components: Components<AppCatalog> = {
|
|||||||
>
|
>
|
||||||
Svelte
|
Svelte
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => emit("pressSolid")}
|
||||||
|
className={[
|
||||||
|
"json-render-renderer-tab",
|
||||||
|
props.renderer === "solid" && "json-render-renderer-tab--active",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
Solid
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const catalogDef = {
|
|||||||
props: z.object({ renderer: z.string() }),
|
props: z.object({ renderer: z.string() }),
|
||||||
slots: [],
|
slots: [],
|
||||||
description:
|
description:
|
||||||
"Segmented tab control for switching between Vue, React, and Svelte renderers",
|
"Segmented tab control for switching between Vue, React, Svelte, and Solid renderers",
|
||||||
},
|
},
|
||||||
RendererBadge: {
|
RendererBadge: {
|
||||||
props: z.object({ renderer: z.string() }),
|
props: z.object({ renderer: z.string() }),
|
||||||
@@ -99,5 +99,9 @@ export const catalogDef = {
|
|||||||
params: z.object({}),
|
params: z.object({}),
|
||||||
description: "Switch to the Svelte renderer",
|
description: "Switch to the Svelte renderer",
|
||||||
},
|
},
|
||||||
|
switchToSolid: {
|
||||||
|
params: z.object({}),
|
||||||
|
description: "Switch to the Solid renderer",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const actionStubs = {
|
|||||||
switchToVue: async () => {},
|
switchToVue: async () => {},
|
||||||
switchToReact: async () => {},
|
switchToReact: async () => {},
|
||||||
switchToSvelte: async () => {},
|
switchToSvelte: async () => {},
|
||||||
|
switchToSolid: async () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Creates action handlers that close over the state store's get/set */
|
/** Creates action handlers that close over the state store's get/set */
|
||||||
@@ -52,5 +53,10 @@ export function makeHandlers(get: Get, set: Set) {
|
|||||||
new CustomEvent("switch-renderer", { detail: "svelte" }),
|
new CustomEvent("switch-renderer", { detail: "svelte" }),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
switchToSolid: async () => {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("switch-renderer", { detail: "solid" }),
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-render-stack--align-start { align-items: flex-start; }
|
.json-render-stack--align-start {
|
||||||
.json-render-stack--align-center { align-items: center; }
|
align-items: flex-start;
|
||||||
.json-render-stack--align-end { align-items: flex-end; }
|
}
|
||||||
|
.json-render-stack--align-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.json-render-stack--align-end {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Card ----------------------------------------------------------------- */
|
/* ---- Card ----------------------------------------------------------------- */
|
||||||
|
|
||||||
@@ -50,12 +56,22 @@
|
|||||||
color: #111827;
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-render-text--sm { font-size: 12px; }
|
.json-render-text--sm {
|
||||||
.json-render-text--lg { font-size: 16px; }
|
font-size: 12px;
|
||||||
.json-render-text--xl { font-size: 24px; }
|
}
|
||||||
|
.json-render-text--lg {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.json-render-text--xl {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.json-render-text--medium { font-weight: 500; }
|
.json-render-text--medium {
|
||||||
.json-render-text--bold { font-weight: 700; }
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.json-render-text--bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Button --------------------------------------------------------------- */
|
/* ---- Button --------------------------------------------------------------- */
|
||||||
|
|
||||||
@@ -211,7 +227,7 @@
|
|||||||
transition: background 0.15s;
|
transition: background 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.json-render-renderer-tab:first-child {
|
.json-render-renderer-tab:not(:last-child) {
|
||||||
border-right: 1px solid #e5e7eb;
|
border-right: 1px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,3 +277,18 @@
|
|||||||
background-color: #ff3e00;
|
background-color: #ff3e00;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.renderer-solid .json-render-renderer-badge {
|
||||||
|
color: #2c4f7c;
|
||||||
|
background-color: #2c4f7c18;
|
||||||
|
border-color: #2c4f7c40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderer-solid .json-render-renderer-dot {
|
||||||
|
background-color: #2c4f7c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderer-solid .json-render-renderer-tab--active {
|
||||||
|
background-color: #2c4f7c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
import { StateProvider } from "@json-render/solid";
|
||||||
|
import { DemoRenderer } from "./DemoRenderer";
|
||||||
|
|
||||||
|
export function App(props: { initialRenderer?: string; spec: Spec }) {
|
||||||
|
const renderer = props.initialRenderer ?? "solid";
|
||||||
|
const initialState = {
|
||||||
|
...props.spec.state,
|
||||||
|
renderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={`renderer-${renderer}`}>
|
||||||
|
<StateProvider initialState={initialState}>
|
||||||
|
<DemoRenderer spec={props.spec} />
|
||||||
|
</StateProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
import {
|
||||||
|
ActionProvider,
|
||||||
|
ValidationProvider,
|
||||||
|
VisibilityProvider,
|
||||||
|
Renderer,
|
||||||
|
defineRegistry,
|
||||||
|
useStateStore,
|
||||||
|
} from "@json-render/solid";
|
||||||
|
import { catalog } from "./catalog";
|
||||||
|
import { components } from "./registry";
|
||||||
|
import { actionStubs, makeHandlers } from "../shared/handlers";
|
||||||
|
|
||||||
|
const { registry } = defineRegistry(catalog, {
|
||||||
|
components,
|
||||||
|
actions: actionStubs,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function DemoRenderer(props: { spec: Spec }) {
|
||||||
|
const stateStore = useStateStore();
|
||||||
|
const handlers = makeHandlers(stateStore.get, stateStore.set);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionProvider handlers={handlers}>
|
||||||
|
<VisibilityProvider>
|
||||||
|
<ValidationProvider>
|
||||||
|
<Renderer spec={props.spec} registry={registry} />
|
||||||
|
</ValidationProvider>
|
||||||
|
</VisibilityProvider>
|
||||||
|
</ActionProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { schema } from "@json-render/solid/schema";
|
||||||
|
import { catalogDef } from "../shared/catalog-def";
|
||||||
|
|
||||||
|
export const catalog = schema.createCatalog(catalogDef);
|
||||||
|
export type AppCatalog = typeof catalog;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { render } from "solid-js/web";
|
||||||
|
import type { Spec } from "@json-render/core";
|
||||||
|
import { App } from "./App";
|
||||||
|
|
||||||
|
let dispose: (() => void) | null = null;
|
||||||
|
|
||||||
|
export function mount(container: HTMLElement, renderer: string, spec: Spec) {
|
||||||
|
dispose = render(
|
||||||
|
() => <App initialRenderer={renderer} spec={spec} />,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unmount() {
|
||||||
|
dispose?.();
|
||||||
|
dispose = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import type { Components } from "@json-render/solid";
|
||||||
|
import type { AppCatalog } from "./catalog";
|
||||||
|
|
||||||
|
export const components: Components<AppCatalog> = {
|
||||||
|
Stack: (renderProps) => (
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
"json-render-stack",
|
||||||
|
renderProps.props.direction === "horizontal" &&
|
||||||
|
"json-render-stack--horizontal",
|
||||||
|
renderProps.props.align &&
|
||||||
|
`json-render-stack--align-${renderProps.props.align}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
style={{
|
||||||
|
gap: renderProps.props.gap ? `${renderProps.props.gap}px` : undefined,
|
||||||
|
padding: renderProps.props.padding
|
||||||
|
? `${renderProps.props.padding}px`
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderProps.children}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
Card: (renderProps) => (
|
||||||
|
<div class="json-render-card">
|
||||||
|
{renderProps.props.title && (
|
||||||
|
<div class="json-render-card-title-wrap">
|
||||||
|
<h2 class="json-render-card-title">{renderProps.props.title}</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{renderProps.props.subtitle && (
|
||||||
|
<p class="json-render-card-subtitle">{renderProps.props.subtitle}</p>
|
||||||
|
)}
|
||||||
|
{renderProps.children}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
Text: (renderProps) => (
|
||||||
|
<span
|
||||||
|
class={[
|
||||||
|
"json-render-text",
|
||||||
|
renderProps.props.size !== "md" &&
|
||||||
|
renderProps.props.size &&
|
||||||
|
`json-render-text--${renderProps.props.size}`,
|
||||||
|
renderProps.props.weight !== "normal" &&
|
||||||
|
renderProps.props.weight &&
|
||||||
|
`json-render-text--${renderProps.props.weight}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
style={renderProps.props.color ? { color: renderProps.props.color } : {}}
|
||||||
|
>
|
||||||
|
{String(renderProps.props.content ?? "")}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
|
||||||
|
Button: (renderProps) => (
|
||||||
|
<button
|
||||||
|
disabled={renderProps.props.disabled}
|
||||||
|
onClick={() => renderProps.emit("press")}
|
||||||
|
class={[
|
||||||
|
"json-render-button",
|
||||||
|
renderProps.props.variant &&
|
||||||
|
`json-render-button--${renderProps.props.variant}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
{renderProps.props.label}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
|
||||||
|
Badge: (renderProps) => (
|
||||||
|
<span
|
||||||
|
class="json-render-badge"
|
||||||
|
style={
|
||||||
|
renderProps.props.color
|
||||||
|
? {
|
||||||
|
"background-color": `${renderProps.props.color}20`,
|
||||||
|
color: renderProps.props.color,
|
||||||
|
"border-color": `${renderProps.props.color}40`,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{renderProps.props.label}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
|
||||||
|
ListItem: (renderProps) => (
|
||||||
|
<div
|
||||||
|
onClick={() => renderProps.emit("press")}
|
||||||
|
class={[
|
||||||
|
"json-render-list-item",
|
||||||
|
renderProps.props.completed && "json-render-list-item--done",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
"json-render-list-item-check",
|
||||||
|
renderProps.props.completed && "json-render-list-item-check--done",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
{renderProps.props.completed ? "✓" : ""}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class={[
|
||||||
|
"json-render-list-item-text",
|
||||||
|
renderProps.props.completed && "json-render-list-item-text--done",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
{renderProps.props.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
RendererBadge: (renderProps) => (
|
||||||
|
<span class="json-render-renderer-badge">
|
||||||
|
<span class="json-render-renderer-dot" />
|
||||||
|
{renderProps.props.renderer === "vue"
|
||||||
|
? "Rendered with Vue"
|
||||||
|
: renderProps.props.renderer === "react"
|
||||||
|
? "Rendered with React"
|
||||||
|
: renderProps.props.renderer === "svelte"
|
||||||
|
? "Rendered with Svelte"
|
||||||
|
: "Rendered with Solid"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
|
||||||
|
RendererTabs: (renderProps) => (
|
||||||
|
<div class="json-render-renderer-tabs-wrapper">
|
||||||
|
<span class="json-render-renderer-tabs-label">Render</span>
|
||||||
|
<div class="json-render-renderer-tabs">
|
||||||
|
<button
|
||||||
|
onClick={() => renderProps.emit("pressVue")}
|
||||||
|
class={[
|
||||||
|
"json-render-renderer-tab",
|
||||||
|
renderProps.props.renderer === "vue" &&
|
||||||
|
"json-render-renderer-tab--active",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
Vue
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => renderProps.emit("pressReact")}
|
||||||
|
class={[
|
||||||
|
"json-render-renderer-tab",
|
||||||
|
renderProps.props.renderer === "react" &&
|
||||||
|
"json-render-renderer-tab--active",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => renderProps.emit("pressSvelte")}
|
||||||
|
class={[
|
||||||
|
"json-render-renderer-tab",
|
||||||
|
renderProps.props.renderer === "svelte" &&
|
||||||
|
"json-render-renderer-tab--active",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
Svelte
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => renderProps.emit("pressSolid")}
|
||||||
|
class={[
|
||||||
|
"json-render-renderer-tab",
|
||||||
|
renderProps.props.renderer === "solid" &&
|
||||||
|
"json-render-renderer-tab--active",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
Solid
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user