page.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. "use client";
  2. import { useState } from "react";
  3. import { defineCatalog } from "@json-render/core";
  4. import type { ComputedFunction } from "@json-render/core";
  5. import { schema, defineRegistry } from "@json-render/react";
  6. import {
  7. threeComponentDefinitions,
  8. threeComponents,
  9. ThreeCanvas,
  10. } from "@json-render/react-three-fiber";
  11. import { scenes } from "./scenes";
  12. const catalog = defineCatalog(schema, {
  13. components: {
  14. ...threeComponentDefinitions,
  15. },
  16. actions: {},
  17. });
  18. const { registry } = defineRegistry(catalog, {
  19. components: {
  20. ...threeComponents,
  21. },
  22. });
  23. function num(v: unknown, fallback: number): number {
  24. return typeof v === "number" ? v : fallback;
  25. }
  26. const computedFunctions: Record<string, ComputedFunction> = {
  27. halfHeight: (a) => num(a.h, 1) / 2,
  28. helixX: (a) => {
  29. const angle =
  30. (num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +
  31. num(a.strand, 0) * Math.PI;
  32. return num(a.radius, 1.8) * Math.cos(angle);
  33. },
  34. helixY: (a) => num(a.i, 0) * num(a.spacing, 0.3) + num(a.offset, -6),
  35. helixZ: (a) => {
  36. const angle =
  37. (num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI +
  38. num(a.strand, 0) * Math.PI;
  39. return num(a.radius, 1.8) * Math.sin(angle);
  40. },
  41. helixHue: (a) => {
  42. const t = num(a.i, 0) / num(a.count, 40);
  43. return `hsl(${Math.round(t * 270 + 180)}, 85%, 60%)`;
  44. },
  45. spiralX: (a) => {
  46. const t = num(a.i, 0) / num(a.count, 60);
  47. const r = 0.5 + t * num(a.maxRadius, 7);
  48. const angle = t * num(a.turns, 4) * 2 * Math.PI;
  49. return r * Math.cos(angle);
  50. },
  51. spiralZ: (a) => {
  52. const t = num(a.i, 0) / num(a.count, 60);
  53. const r = 0.5 + t * num(a.maxRadius, 7);
  54. const angle = t * num(a.turns, 4) * 2 * Math.PI;
  55. return r * Math.sin(angle);
  56. },
  57. spiralY: (a) => {
  58. const i = num(a.i, 0);
  59. return Math.sin(i * 1.7) * 0.4;
  60. },
  61. spiralScale: (a) => {
  62. const t = num(a.i, 0) / num(a.count, 60);
  63. return 0.08 + (1 - t) * 0.25;
  64. },
  65. spiralEmissive: (a) => {
  66. const t = num(a.i, 0) / num(a.count, 60);
  67. return Math.max(0, 1 - t * 1.5);
  68. },
  69. circleX: (a) => {
  70. const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
  71. return num(a.radius, 3) * Math.cos(angle);
  72. },
  73. circleZ: (a) => {
  74. const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
  75. return num(a.radius, 3) * Math.sin(angle);
  76. },
  77. circleY: (a) => {
  78. const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
  79. return Math.sin(angle * num(a.freq, 3)) * num(a.amp, 0.3);
  80. },
  81. circleAngle: (a) => {
  82. return (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI;
  83. },
  84. circleHue: (a) => {
  85. const t = num(a.i, 0) / num(a.count, 8);
  86. return `hsl(${Math.round(t * 360)}, ${num(a.sat, 70)}%, ${num(a.lit, 80)}%)`;
  87. },
  88. };
  89. function highlightJson(json: string): string {
  90. return json.replace(
  91. /("(?:\\.|[^"\\])*")\s*(:)|("(?:\\.|[^"\\])*")|([-+]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b)|([{}[\]:,])/g,
  92. (_, key, colon, str, num, lit, punct) => {
  93. if (key) return `<span style="color:#FF4D8D">${key}</span>${colon}`;
  94. if (str) return `<span style="color:#00CA50">${str}</span>`;
  95. if (num) return `<span style="color:#47A8FF">${num}</span>`;
  96. if (lit) return `<span style="color:#47A8FF">${lit}</span>`;
  97. if (punct) return `<span style="color:#666">${punct}</span>`;
  98. return _;
  99. },
  100. );
  101. }
  102. const LIST_WIDTH = 220;
  103. const JSON_WIDTH = 380;
  104. const HEADER_HEIGHT = 40;
  105. const headerStyle: React.CSSProperties = {
  106. height: HEADER_HEIGHT,
  107. display: "flex",
  108. alignItems: "center",
  109. padding: "0 16px",
  110. fontSize: 11,
  111. fontWeight: 600,
  112. color: "#555",
  113. letterSpacing: "0.08em",
  114. textTransform: "uppercase",
  115. fontFamily: "ui-monospace, monospace",
  116. borderBottom: "1px solid #1e1e1e",
  117. flexShrink: 0,
  118. whiteSpace: "nowrap",
  119. overflow: "hidden",
  120. textOverflow: "ellipsis",
  121. boxSizing: "border-box",
  122. };
  123. export default function Page() {
  124. const [selectedIndex, setSelectedIndex] = useState(0);
  125. const selected = scenes[selectedIndex]!;
  126. return (
  127. <div style={{ height: "100vh", display: "flex", background: "#0a0a0a" }}>
  128. <div
  129. style={{
  130. width: LIST_WIDTH,
  131. flexShrink: 0,
  132. display: "flex",
  133. flexDirection: "column",
  134. borderRight: "1px solid #1e1e1e",
  135. background: "#0f0f0f",
  136. }}
  137. >
  138. <div style={headerStyle}>Scenes</div>
  139. <div style={{ flex: 1, overflowY: "auto", padding: "6px 0" }}>
  140. {scenes.map((scene, i) => (
  141. <button
  142. key={scene.name}
  143. onClick={() => setSelectedIndex(i)}
  144. style={{
  145. display: "block",
  146. width: "100%",
  147. padding: "8px 16px",
  148. fontSize: 13,
  149. border: "none",
  150. textAlign: "left",
  151. background:
  152. i === selectedIndex
  153. ? "rgba(255,255,255,0.08)"
  154. : "transparent",
  155. color: i === selectedIndex ? "#fff" : "#888",
  156. fontWeight: i === selectedIndex ? 500 : 400,
  157. cursor: "pointer",
  158. borderLeft:
  159. i === selectedIndex
  160. ? "2px solid #fff"
  161. : "2px solid transparent",
  162. fontFamily: "inherit",
  163. }}
  164. >
  165. {scene.name}
  166. </button>
  167. ))}
  168. </div>
  169. </div>
  170. <div
  171. style={{
  172. flex: 1,
  173. display: "flex",
  174. flexDirection: "column",
  175. minWidth: 0,
  176. }}
  177. >
  178. <div style={headerStyle}>{selected.description}</div>
  179. <div style={{ flex: 1, background: "#000", position: "relative" }}>
  180. <ThreeCanvas
  181. key={selectedIndex}
  182. spec={selected.spec}
  183. registry={registry}
  184. functions={computedFunctions}
  185. shadows
  186. camera={{ position: [0, 0, 5], fov: 50 }}
  187. style={{ width: "100%", height: "100%" }}
  188. />
  189. </div>
  190. </div>
  191. <div
  192. style={{
  193. width: JSON_WIDTH,
  194. flexShrink: 0,
  195. display: "flex",
  196. flexDirection: "column",
  197. borderLeft: "1px solid #1e1e1e",
  198. background: "#0d0d0d",
  199. }}
  200. >
  201. <div style={headerStyle}>Spec JSON</div>
  202. <pre
  203. style={{
  204. flex: 1,
  205. margin: 0,
  206. padding: 14,
  207. overflowY: "auto",
  208. overflowX: "auto",
  209. fontSize: 11,
  210. lineHeight: 1.6,
  211. fontFamily:
  212. "ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace",
  213. color: "#EDEDED",
  214. tabSize: 2,
  215. }}
  216. dangerouslySetInnerHTML={{
  217. __html: highlightJson(JSON.stringify(selected.spec, null, 2)),
  218. }}
  219. />
  220. </div>
  221. </div>
  222. );
  223. }