cozyclay 3.1.4 → 3.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/INSTALL_FOR_AGENTS.md +3 -0
- package/README.md +5 -0
- package/bin/agent/agent-failure-codes.mjs +31 -0
- package/bin/agent/agent-routes.mjs +5 -10
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/cozyclay.mjs +16 -64
- package/bin/studio-routes.mjs +69 -0
- package/bin/update-check.mjs +53 -5
- package/bin/update-install.mjs +136 -0
- package/bin/update-routes.mjs +80 -0
- package/dist/ai-camera-control/index.html +1 -1
- package/dist/app/index.html +5 -6
- package/dist/assets/{TaskBoard-U1Yl-4F1.js → TaskBoard-DwXhsJXz.js} +1 -1
- package/dist/assets/app-BLS1Zaru.js +5144 -0
- package/dist/assets/app-CEP7jzXB.css +1 -0
- package/dist/assets/compile-worker-or7djI2v.js +6 -0
- package/dist/assets/{motion3-compile-DDG3wklD.js → motion3-compile-Du7UXrWn.js} +1 -1
- package/dist/assets/scene-asset-sync-t45pWCyq.js +13 -0
- package/dist/assets/task-board-CkLZGPkv.js +15 -0
- package/dist/assets/workflow-DJwOUzqJ.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +3 -3
- package/dist/index.html +533 -264
- package/dist/media/cozyclay-mark.png +0 -0
- package/dist/media/cozyclay-truck-demo-poster.jpg +0 -0
- package/dist/media/step-move-poster.jpg +0 -0
- package/dist/media/step-shoot-poster.jpg +0 -0
- package/dist/media/step-stage-poster.jpg +0 -0
- package/dist/media/truck-greybox-poster.jpg +0 -0
- package/dist/media/truck-seedance-poster.jpg +0 -0
- package/dist/previs-software/index.html +4 -4
- package/dist/privacy/index.html +46 -19
- package/dist/seedance-camera-control/index.html +3 -3
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +3 -4
- package/mcp/project-camera.mjs +43 -0
- package/package.json +2 -1
- package/src/App.jsx +59 -70
- package/src/analytics-controls.js +17 -0
- package/src/analytics-friction.js +124 -0
- package/src/analytics-strict.js +139 -0
- package/src/analytics-values.js +49 -0
- package/src/analytics.js +37 -133
- package/src/asset-pane.jsx +4 -5
- package/src/click-friction.js +101 -0
- package/src/command-bus-telemetry.js +155 -0
- package/src/command-bus.js +10 -8
- package/src/commands/motion3.js +7 -2
- package/src/controls.jsx +3 -2
- package/src/crash-telemetry.js +45 -0
- package/src/domains/cast.js +2 -11
- package/src/domains/motion.js +49 -38
- package/src/domains/objects.js +3 -0
- package/src/domains/scene-undo.js +27 -0
- package/src/domains/scenes.js +1 -1
- package/src/error-boundary.jsx +2 -0
- package/src/export-attempt.js +100 -0
- package/src/export-workspace.js +12 -1
- package/src/first-success-guide.jsx +11 -7
- package/src/guide-telemetry.js +60 -0
- package/src/hierarchy-panel.jsx +2 -1
- package/src/live-control.js +2 -9
- package/src/locale-zh/domains.js +1 -0
- package/src/main.jsx +5 -0
- package/src/motion/timeline.jsx +9 -5
- package/src/motion-draft-telemetry.js +62 -0
- package/src/motion3/compiler/channels.js +7 -5
- package/src/motion3/compiler/compile.js +6 -2
- package/src/motion3/compiler/finger-close.js +84 -7
- package/src/motion3/compiler/head-lead.js +70 -0
- package/src/motion3/compiler/head-steady.js +1 -1
- package/src/motion3/compiler/limbs.js +10 -3
- package/src/motion3/compiler/pose.js +1 -1
- package/src/motion3/compiler/validate.js +5 -3
- package/src/motion3/pipeline.js +4 -0
- package/src/motion3/qa/explain.js +2 -1
- package/src/motion3/qa/finger-checks.js +80 -0
- package/src/motion3/qa/gait-observe.js +88 -0
- package/src/motion3/qa/grasp-checks.js +3 -0
- package/src/motion3/qa/hints-grasp.js +8 -0
- package/src/motion3/qa/index.js +2 -1
- package/src/motion3/qa/motion-life.js +26 -3
- package/src/motion3/schema/topics.js +1 -1
- package/src/playback-telemetry.js +107 -0
- package/src/post-turn-edits.js +62 -0
- package/src/search-friction.js +49 -0
- package/src/semantic-edit.js +19 -6
- package/src/shell/BoardSlot.jsx +3 -3
- package/src/shell/BottomDock.jsx +1 -1
- package/src/shell/DetailsSlot.jsx +1 -1
- package/src/shell/MenuBar.jsx +13 -13
- package/src/shell/StudioShell.jsx +7 -1
- package/src/shell/TopBar.jsx +2 -0
- package/src/shell/ViewportToolbar.jsx +2 -2
- package/src/shortcut-friction.js +122 -0
- package/src/stage-canvas-rigs.jsx +3 -4
- package/src/studio-telemetry.js +97 -0
- package/src/thrash-friction.js +90 -0
- package/src/update/UpdateChip.jsx +86 -0
- package/src/update/UpdateDialog.jsx +147 -0
- package/src/update/update-client.js +188 -0
- package/src/update/update-telemetry.js +78 -0
- package/src/update/update.css +94 -0
- package/src/use-webgl-available.js +35 -0
- package/src/workflow/AgentPanel.jsx +29 -30
- package/src/workflow/CozySceneNode.jsx +1 -1
- package/src/workflow/LoginPanel.jsx +15 -5
- package/src/workflow/agent-client.js +10 -91
- package/src/workflow/agent-failure-codes.js +47 -0
- package/src/workflow/agent-turn-telemetry.js +222 -0
- package/src/workflow/connect-telemetry.js +86 -0
- package/src/workflow/prompt-telemetry.js +105 -0
- package/tools/dev/pages/privacy.html +44 -17
- package/tools/loc-baseline.json +10 -10
- package/tools/run-tests.mjs +8 -8
- package/tools/verify-package-tarball.mjs +29 -1
- package/dist/assets/app-BdRlMf4J.css +0 -1
- package/dist/assets/app-KXdgBC-S.js +0 -5144
- package/dist/assets/compile-worker-oy_P9HHh.js +0 -6
- package/dist/assets/landing-y3EqDgSZ.js +0 -1
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +0 -1
- package/dist/assets/scene-asset-sync-Bz59Ghdz.js +0 -13
- package/dist/assets/task-board-DFfNa0xg.js +0 -15
- package/dist/assets/workflow-BMJGd_gZ.js +0 -179
- package/dist/media/playground-poster.jpg +0 -0
package/dist/index.html
CHANGED
|
@@ -16,18 +16,18 @@
|
|
|
16
16
|
<meta property="og:title" content="CozyClay — open source 3D previs software, in your browser" />
|
|
17
17
|
<meta property="og:description" content="Block a scene, pose characters, author camera moves and cuts, and preview generated motion. Open source, runs in your browser." />
|
|
18
18
|
<meta property="og:url" content="https://cozyclay.org/" />
|
|
19
|
-
<meta property="og:image" content="https://cozyclay.org/media/cozyclay-demo-poster.jpg" />
|
|
19
|
+
<meta property="og:image" content="https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg" />
|
|
20
20
|
<meta property="og:image:width" content="1920" />
|
|
21
21
|
<meta property="og:image:height" content="1080" />
|
|
22
|
-
<meta property="og:image:alt" content="A CozyClay shot mid-wipe: the greybox
|
|
22
|
+
<meta property="og:image:alt" content="A CozyClay shot mid-wipe: the clay greybox of a truck driver on one half, the Seedance 2.5 generation on the other." />
|
|
23
23
|
<meta name="twitter:card" content="summary_large_image" />
|
|
24
24
|
<meta name="twitter:title" content="CozyClay — open source 3D previs software, in your browser" />
|
|
25
25
|
<meta name="twitter:description" content="Block a scene, pose characters, author camera moves and cuts, and preview generated motion. Open source, runs in your browser." />
|
|
26
|
-
<meta name="twitter:image" content="https://cozyclay.org/media/cozyclay-demo-poster.jpg" />
|
|
26
|
+
<meta name="twitter:image" content="https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg" />
|
|
27
27
|
|
|
28
28
|
<title>CozyClay — open source 3D previs software, in your browser</title>
|
|
29
29
|
|
|
30
|
-
<meta property="og:video" content="https://cozyclay.org/media/
|
|
30
|
+
<meta property="og:video" content="https://cozyclay.org/media/truck-seedance.mp4" />
|
|
31
31
|
<meta property="og:video:type" content="video/mp4" />
|
|
32
32
|
<meta property="og:video:width" content="1920" />
|
|
33
33
|
<meta property="og:video:height" content="1080" />
|
|
@@ -46,9 +46,10 @@
|
|
|
46
46
|
"applicationCategory": "DesignApplication",
|
|
47
47
|
"applicationSubCategory": "3D previsualization",
|
|
48
48
|
"operatingSystem": "Any (Chromium-based browser)",
|
|
49
|
+
"softwareVersion": "3.1.6",
|
|
49
50
|
"description": "CozyClay is an open-source, browser-based 3D staging studio. Block a scene, pose characters, sequence motion prompts on a timeline, and preview generated motion in one local workspace that handles like the Unity Editor.",
|
|
50
|
-
"image": "https://cozyclay.org/media/cozyclay-demo-poster.jpg",
|
|
51
|
-
"screenshot": "https://cozyclay.org/media/cozyclay-demo-poster.jpg",
|
|
51
|
+
"image": "https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg",
|
|
52
|
+
"screenshot": "https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg",
|
|
52
53
|
"logo": "https://cozyclay.org/media/cozyclay-logo.png",
|
|
53
54
|
"license": "https://github.com/NomaDamas/CozyClay/blob/main/LICENSE",
|
|
54
55
|
"codeRepository": "https://github.com/NomaDamas/CozyClay",
|
|
@@ -58,333 +59,465 @@
|
|
|
58
59
|
</script>
|
|
59
60
|
|
|
60
61
|
<style>
|
|
62
|
+
@font-face { font-family: "Instrument Serif"; font-weight: 400; font-display: swap; src: url("/fonts/instrument-serif-latin.woff2") format("woff2"); }
|
|
63
|
+
@font-face { font-family: "Instrument Serif"; font-style: italic; font-weight: 400; font-display: swap; src: url("/fonts/instrument-serif-italic-latin.woff2") format("woff2"); }
|
|
64
|
+
@font-face { font-family: "Inter"; font-weight: 100 900; font-display: swap; src: url("/fonts/inter-latin.woff2") format("woff2"); }
|
|
65
|
+
@font-face { font-family: "JetBrains Mono"; font-weight: 400; font-display: swap; src: url("/fonts/jetbrains-mono-400-latin.woff2") format("woff2"); }
|
|
61
66
|
:root {
|
|
62
|
-
--
|
|
63
|
-
--
|
|
67
|
+
--paper: #f5f5f7;
|
|
68
|
+
--paper-2: #ececf0;
|
|
64
69
|
--panel: #ffffff;
|
|
65
70
|
--line: #d2d2d7;
|
|
66
71
|
--line-strong: #c7c7cc;
|
|
67
|
-
--ink: #
|
|
72
|
+
--ink: #111111;
|
|
68
73
|
--muted: #636366;
|
|
69
|
-
--
|
|
74
|
+
--brand: #111111;
|
|
75
|
+
--brand-deep: #000000;
|
|
76
|
+
--brand-tint: #e8e8ea;
|
|
70
77
|
--ok: #2f6b3a;
|
|
71
|
-
--
|
|
78
|
+
--serif: "Instrument Serif", "Iowan Old Style", Georgia, serif;
|
|
79
|
+
--sans: "Inter", -apple-system, BlinkMacSystemFont, "Helvetica Neue", system-ui, sans-serif;
|
|
80
|
+
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
81
|
+
--radius: 16px;
|
|
82
|
+
--shadow-clay: 0 1px 2px rgba(0, 0, 0, .04), 0 8px 24px rgba(0, 0, 0, .06);
|
|
72
83
|
--shadow-window: 0 2px 6px rgba(0, 0, 0, .06), 0 30px 60px rgba(0, 0, 0, .18);
|
|
73
|
-
--
|
|
84
|
+
--header-h: 64px;
|
|
74
85
|
}
|
|
75
86
|
* { box-sizing: border-box; }
|
|
76
|
-
html { scroll-behavior: smooth; }
|
|
87
|
+
html { scroll-behavior: smooth; scroll-padding-top: calc(var(--header-h) + 12px); }
|
|
77
88
|
body {
|
|
78
89
|
margin: 0;
|
|
79
|
-
background: var(--
|
|
90
|
+
background: var(--paper);
|
|
80
91
|
color: var(--ink);
|
|
81
|
-
font: 17px/1.5
|
|
92
|
+
font: 17px/1.5 var(--sans);
|
|
82
93
|
-webkit-font-smoothing: antialiased;
|
|
94
|
+
font-feature-settings: "cv11", "ss01", "ss03";
|
|
95
|
+
letter-spacing: -.011em;
|
|
83
96
|
}
|
|
84
|
-
a { color: var(--
|
|
97
|
+
a { color: var(--brand); }
|
|
85
98
|
button { font-family: inherit; }
|
|
86
|
-
|
|
99
|
+
img, video { max-width: 100%; }
|
|
100
|
+
:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 6px; }
|
|
87
101
|
.wrap { max-width: 1160px; margin: 0 auto; padding: 0 24px; }
|
|
88
102
|
.wrap.narrow { max-width: 820px; }
|
|
89
103
|
|
|
90
|
-
|
|
91
|
-
h1 { font-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
h1 { font-family: var(--serif); font-size: clamp(40px, 6vw, 68px); line-height: 1.02; letter-spacing: -.02em; font-weight: 400; margin: 0; max-width: 900px; }
|
|
105
|
+
h1 em { font-style: italic; color: inherit; }
|
|
106
|
+
h2 { font-family: var(--serif); font-size: clamp(34px, 4.2vw, 52px); line-height: 1.05; letter-spacing: -.015em; font-weight: 400; margin: 0 0 16px; }
|
|
107
|
+
h3 { font-size: 19px; line-height: 1.3; font-weight: 600; margin: 0; letter-spacing: -.02em; }
|
|
108
|
+
.kicker { display: inline-block; margin: 0 0 14px; font: 400 12px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
109
|
+
.sub { margin: 0 0 40px; max-width: 40em; color: var(--muted); font-size: 19px; }
|
|
95
110
|
.small, figcaption { font-size: 14px; color: var(--muted); }
|
|
96
|
-
code, pre, kbd { font-family: var(--mono); font-size:
|
|
111
|
+
code, pre, kbd { font-family: var(--mono); font-size: 13.5px; letter-spacing: 0; }
|
|
97
112
|
|
|
113
|
+
/* Header: one row at every width. On phones only the brand and the two
|
|
114
|
+
actions stay; the section links are a scroll away. */
|
|
98
115
|
header.site {
|
|
99
116
|
position: sticky; top: 0; z-index: 10;
|
|
100
117
|
background: rgba(245, 245, 247, .8);
|
|
101
118
|
backdrop-filter: saturate(180%) blur(20px); -webkit-backdrop-filter: saturate(180%) blur(20px);
|
|
102
119
|
border-bottom: 1px solid var(--line);
|
|
103
120
|
}
|
|
104
|
-
header.site .wrap { display: flex; align-items: center;
|
|
105
|
-
.brand { display: flex; align-items: center; gap: 10px; min-height: 44px; font-weight: 600; color: var(--ink); text-decoration: none; }
|
|
106
|
-
.brand img { width:
|
|
107
|
-
header.site nav { display: flex; align-items: center; gap:
|
|
121
|
+
header.site .wrap { display: flex; align-items: center; gap: 16px; height: var(--header-h); }
|
|
122
|
+
.brand { display: flex; align-items: center; gap: 10px; min-height: 44px; font-weight: 600; font-size: 18px; letter-spacing: -.02em; color: var(--ink); text-decoration: none; margin-right: auto; }
|
|
123
|
+
.brand img { width: 36px; height: auto; }
|
|
124
|
+
header.site nav { display: flex; align-items: center; gap: 28px; font-size: 15px; }
|
|
108
125
|
header.site nav a { display: flex; align-items: center; min-height: 44px; color: var(--muted); text-decoration: none; }
|
|
109
126
|
header.site nav a:hover { color: var(--ink); }
|
|
110
|
-
|
|
127
|
+
.actions { display: flex; align-items: center; gap: 8px; }
|
|
128
|
+
.icon-link { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 999px; color: var(--ink); }
|
|
129
|
+
.icon-link:hover { background: var(--paper-2); }
|
|
111
130
|
|
|
112
131
|
.btn {
|
|
113
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
132
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
114
133
|
min-height: 48px; padding: 0 24px; border-radius: 8px; border: 1px solid transparent;
|
|
115
|
-
font-size: 17px; font-weight: 600; text-decoration: none; cursor: pointer;
|
|
116
|
-
transition:
|
|
134
|
+
font-size: 17px; font-weight: 600; text-decoration: none; cursor: pointer; white-space: nowrap;
|
|
135
|
+
transition: background-color .15s, transform .15s, box-shadow .15s;
|
|
117
136
|
}
|
|
118
|
-
.btn:
|
|
119
|
-
.btn
|
|
120
|
-
.btn.primary { background: var(--
|
|
137
|
+
.btn:active { transform: translateY(1px) scale(.99); }
|
|
138
|
+
.btn.primary { background: var(--brand); color: #fff; }
|
|
139
|
+
.btn.primary:hover { background: var(--brand-deep); }
|
|
121
140
|
.btn.ghost { background: var(--panel); color: var(--ink); border-color: var(--line-strong); }
|
|
141
|
+
.btn.ghost:hover { border-color: var(--ink); }
|
|
142
|
+
.btn.sm { min-height: 40px; padding: 0 18px; font-size: 15px; }
|
|
122
143
|
.cta { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
|
|
123
144
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
/* Hero: headline left, the pitch and the two ways in on the right. */
|
|
146
|
+
/* First screen: black stage, the headline and the reel. Scrolling plays
|
|
147
|
+
it out: the reel rises over the headline and grows to the full width,
|
|
148
|
+
then the page continues. --p (0..1) is set by the script below. */
|
|
149
|
+
.stage {
|
|
150
|
+
position: relative; height: 210vh; color: #f2efe8; --p: 0;
|
|
151
|
+
background:
|
|
152
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .05 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"),
|
|
153
|
+
radial-gradient(120% 90% at 50% 35%, #1f1e1c 0%, #161615 55%, #111110 100%);
|
|
154
|
+
}
|
|
155
|
+
.stage-pin {
|
|
156
|
+
position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden;
|
|
157
|
+
display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
|
|
158
|
+
padding-top: calc(var(--header-h) + 2vh);
|
|
159
|
+
}
|
|
160
|
+
.stage h1 {
|
|
161
|
+
margin: 0; padding: 0 4vw; width: 100%; max-width: none; text-align: center;
|
|
162
|
+
font-family: var(--sans); font-size: clamp(44px, 8.6vw, 140px); line-height: 1; letter-spacing: -.06em; font-weight: 600;
|
|
163
|
+
font-feature-settings: "ss01", "cv11";
|
|
164
|
+
opacity: calc(.88 * (1 - var(--p) * 1.4));
|
|
165
|
+
transform: translateY(calc(var(--p) * -12vh)) scale(calc(1 - var(--p) * .12));
|
|
166
|
+
transform-origin: 50% 0;
|
|
167
|
+
}
|
|
168
|
+
.stage-reel {
|
|
169
|
+
position: relative; z-index: 1; width: min(1280px, 94vw); margin-top: 7vh;
|
|
170
|
+
transform: translateY(calc(var(--p) * var(--rise, 0px))) scale(calc(.94 + var(--p) * var(--grow, .06)));
|
|
171
|
+
transform-origin: 50% 0;
|
|
172
|
+
}
|
|
173
|
+
.stage .window { border-color: #2c2b29; background: #1a1a19; box-shadow: 0 30px 80px rgba(0, 0, 0, .45); }
|
|
174
|
+
.stage .titlebar { background: #22211f; border-bottom-color: #2c2b29; }
|
|
175
|
+
.stage .titlebar span { color: #8a8a90; }
|
|
176
|
+
.stage-cue {
|
|
177
|
+
position: absolute; bottom: 3vh; left: 50%; margin-left: -11px; color: #8a8a90;
|
|
178
|
+
opacity: calc(1 - var(--p) * 5); animation: cue 1.8s ease-in-out infinite;
|
|
179
|
+
}
|
|
180
|
+
@keyframes cue { 50% { transform: translateY(6px); } }
|
|
181
|
+
body.on-stage header.site { background: #151514; border-bottom-color: #262624; }
|
|
182
|
+
body.on-stage header.site .brand, body.on-stage header.site .icon-link { color: #f2efe8; }
|
|
183
|
+
body.on-stage header.site nav a { color: #9a9aa0; }
|
|
184
|
+
body.on-stage header.site nav a:hover { color: #f2efe8; }
|
|
185
|
+
body.on-stage header.site .btn.primary { background: #f2efe8; color: #0b0b0c; }
|
|
186
|
+
.hero-grid { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 28px; }
|
|
187
|
+
|
|
188
|
+
.stage h1 .line { display: inline-block; white-space: nowrap; }
|
|
189
|
+
.curved .ch { display: inline-block; white-space: pre; transform-origin: 50% 55%; will-change: transform; }
|
|
190
|
+
.hero-side { display: flex; flex-direction: column; align-items: center; gap: 22px; max-width: 640px; }
|
|
191
|
+
.lede { margin: 0; color: var(--muted); font-size: 19px; }
|
|
192
|
+
.hero-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; }
|
|
127
193
|
.cmd {
|
|
128
|
-
display: inline-flex; align-items: center; gap: 4px; padding: 0 4px 0 14px;
|
|
129
|
-
background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
|
|
194
|
+
display: inline-flex; align-items: center; gap: 4px; padding: 0 4px 0 14px; min-height: 48px;
|
|
195
|
+
background: var(--panel); border: 1px solid var(--line-strong); border-radius: 8px;
|
|
130
196
|
}
|
|
131
|
-
.cmd .copy { width:
|
|
197
|
+
.cmd .copy { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; background: none; border: 0; color: var(--muted); cursor: pointer; border-radius: 6px; }
|
|
198
|
+
.cmd .copy:hover { color: var(--ink); background: var(--paper); }
|
|
132
199
|
.cmd .copy .ic-done { display: none; color: var(--ok); }
|
|
133
200
|
.cmd .copy[data-state="copied"] .ic-copy { display: none; }
|
|
134
201
|
.cmd .copy[data-state="copied"] .ic-done { display: block; }
|
|
202
|
+
.meta-line { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 16px; margin: 0; color: var(--muted); font-size: 14px; }
|
|
203
|
+
.meta-line a { color: var(--muted); }
|
|
204
|
+
.meta-line > * + *::before { content: "·"; margin-right: 8px; }
|
|
205
|
+
.meta-line { gap: 4px 8px; }
|
|
135
206
|
|
|
136
207
|
.window { border: 1px solid var(--line-strong); border-radius: 12px; overflow: hidden; background: var(--panel); box-shadow: var(--shadow-window); }
|
|
137
208
|
.titlebar { display: flex; align-items: center; gap: 8px; height: 40px; padding: 0 14px; background: #e8e8ed; border-bottom: 1px solid var(--line); }
|
|
138
209
|
.titlebar i { width: 12px; height: 12px; border-radius: 50%; background: #ff5f57; }
|
|
139
210
|
.titlebar i:nth-child(2) { background: #febc2e; }
|
|
140
211
|
.titlebar i:nth-child(3) { background: #28c840; }
|
|
141
|
-
.titlebar span { flex: 1; text-align: center; margin-right: 52px; font-size:
|
|
212
|
+
.titlebar span { flex: 1; text-align: center; margin-right: 52px; font-size: 13px; font-weight: 500; color: var(--muted); }
|
|
142
213
|
|
|
143
|
-
.proof { padding: 0 0 96px; }
|
|
144
|
-
.proof figure { margin: 0; }
|
|
145
|
-
.proof video { display: block; width: 100%; height: auto; background: #000; }
|
|
146
|
-
.proof figcaption { margin-top: 16px; text-align: center; }
|
|
147
214
|
|
|
148
|
-
section { padding: 88px 0; border-top: 1px solid var(--line); }
|
|
149
|
-
section.alt { background: var(--alt); }
|
|
150
|
-
section.hero, section.proof { border-top: 0; }
|
|
151
215
|
|
|
152
|
-
.
|
|
153
|
-
.
|
|
154
|
-
|
|
155
|
-
|
|
216
|
+
/* Before/after compare. --pos is the divider, set by the script below. */
|
|
217
|
+
.compare { position: relative; aspect-ratio: 16 / 9; background: #000; cursor: ew-resize; touch-action: pan-y; user-select: none; -webkit-user-select: none; }
|
|
218
|
+
.compare video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
|
|
219
|
+
.compare .cmp-b { clip-path: inset(0 0 0 var(--pos)); }
|
|
220
|
+
.cmp-line { position: absolute; top: 0; bottom: 0; left: var(--pos); width: 2px; margin-left: -1px; background: #fff; pointer-events: none; }
|
|
221
|
+
.cmp-knob {
|
|
222
|
+
position: absolute; top: 50%; left: 50%; width: 44px; height: 44px; margin: -22px 0 0 -22px;
|
|
223
|
+
display: flex; align-items: center; justify-content: center; border-radius: 50%;
|
|
224
|
+
background: #fff; color: var(--ink); box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
|
|
156
225
|
}
|
|
157
|
-
.
|
|
158
|
-
.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
.demo { scroll-margin-top: 76px; }
|
|
164
|
-
.demo .window { min-width: 0; box-shadow: 0 2px 6px rgba(0, 0, 0, .06), 0 24px 48px rgba(0, 0, 0, .14); }
|
|
165
|
-
.playground {
|
|
166
|
-
position: relative;
|
|
167
|
-
/* Header 64 + margin 12 + title bar 40 + bottom bar 52 + 12. */
|
|
168
|
-
height: clamp(480px, calc(100vh - 180px), 900px);
|
|
169
|
-
overflow: hidden;
|
|
170
|
-
background: #1b1b1b center / cover no-repeat;
|
|
226
|
+
.cmp-range { position: absolute; width: 1px; height: 1px; margin: 0; padding: 0; opacity: 0; pointer-events: none; }
|
|
227
|
+
.compare:has(.cmp-range:focus-visible) .cmp-knob { outline: 3px solid var(--brand); outline-offset: 2px; }
|
|
228
|
+
.compare > span {
|
|
229
|
+
position: absolute; bottom: 14px; padding: 5px 10px; border-radius: 6px; pointer-events: none;
|
|
230
|
+
background: rgba(0, 0, 0, .55); color: #fff; font: 400 11px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase;
|
|
231
|
+
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
|
|
171
232
|
}
|
|
172
|
-
.
|
|
173
|
-
.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.playground-cover small { font-size: 14px; font-weight: 600; color: var(--ink); }
|
|
183
|
-
.playground[data-state="loading"] iframe { opacity: 0; }
|
|
184
|
-
.playground-loading {
|
|
185
|
-
position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
|
|
186
|
-
color: #fff; font-size: 14px; background: rgba(0, 0, 0, .45); pointer-events: none;
|
|
233
|
+
.compare .tag-l { left: 14px; }
|
|
234
|
+
.compare .tag-r { right: 14px; }
|
|
235
|
+
|
|
236
|
+
section { padding: 88px 0; border-top: 1px solid var(--line); }
|
|
237
|
+
section.alt { background: var(--paper-2); }
|
|
238
|
+
section.stage { padding: 0; border-top: 0; }
|
|
239
|
+
|
|
240
|
+
.card {
|
|
241
|
+
min-width: 0; padding: 28px; background: var(--panel);
|
|
242
|
+
border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-clay);
|
|
187
243
|
}
|
|
188
|
-
.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
244
|
+
.card p { margin: 8px 0 0; color: var(--muted); }
|
|
245
|
+
|
|
246
|
+
/* Why */
|
|
247
|
+
.why { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 32px 64px; align-items: start; }
|
|
248
|
+
.why h2 { margin: 0; }
|
|
249
|
+
.why p { margin: 0 0 16px; color: var(--muted); font-size: 19px; }
|
|
250
|
+
.why p strong { color: var(--ink); font-weight: 600; }
|
|
251
|
+
|
|
252
|
+
/* Workflow: pick a step, see it. */
|
|
253
|
+
.flow { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); gap: 24px 40px; align-items: start; }
|
|
254
|
+
.flow-tabs, .ai-tabs { display: flex; flex-direction: column; gap: 4px; }
|
|
255
|
+
.flow-tabs button, .ai-tabs button {
|
|
256
|
+
display: flex; gap: 16px; align-items: flex-start; width: 100%; padding: 16px; text-align: left;
|
|
257
|
+
background: none; border: 1px solid transparent; border-radius: 12px; color: var(--muted); font-size: 15px; cursor: pointer;
|
|
258
|
+
transition: background-color .15s, border-color .15s;
|
|
194
259
|
}
|
|
195
|
-
.
|
|
196
|
-
.
|
|
197
|
-
.
|
|
198
|
-
.
|
|
199
|
-
|
|
200
|
-
background: var(--
|
|
260
|
+
.ai-tabs button { display: block; }
|
|
261
|
+
.flow-tabs button:hover, .ai-tabs button:hover { background: var(--paper-2); }
|
|
262
|
+
.flow-tabs button[aria-selected="true"], .ai-tabs button[aria-selected="true"] { background: var(--panel); border-color: var(--line); box-shadow: var(--shadow-clay); }
|
|
263
|
+
.flow-tabs b {
|
|
264
|
+
flex: none; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px;
|
|
265
|
+
background: var(--paper-2); color: var(--muted); font-size: 15px;
|
|
201
266
|
}
|
|
202
|
-
.
|
|
203
|
-
|
|
267
|
+
.flow-tabs button[aria-selected="true"] b { background: var(--brand); color: #fff; }
|
|
268
|
+
.flow-tabs em { font-style: normal; }
|
|
269
|
+
.flow-tabs strong, .ai-tabs strong { display: block; margin-bottom: 4px; color: var(--ink); font-size: 17px; font-weight: 600; letter-spacing: -.02em; }
|
|
270
|
+
.ai-tabs span { display: block; }
|
|
271
|
+
.flow-view figure { margin: 0; }
|
|
272
|
+
.flow-view video { display: block; width: 100%; height: auto; aspect-ratio: 1440 / 600; border-radius: var(--radius); border: 1px solid var(--line); background: var(--paper-2); }
|
|
273
|
+
.flow-view figcaption { margin-top: 12px; }
|
|
274
|
+
.flow-exports { display: flex; align-items: center; padding: 8px 28px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); }
|
|
275
|
+
.flow-exports[hidden] { display: none; }
|
|
276
|
+
.flow-exports .exports { width: 100%; }
|
|
277
|
+
.exports { margin: 0; padding: 0; list-style: none; display: grid; }
|
|
278
|
+
.exports li { display: flex; gap: 14px; align-items: baseline; padding: 14px 0; border-bottom: 1px solid var(--line); }
|
|
279
|
+
.exports li:last-child { border-bottom: 0; }
|
|
280
|
+
.exports b { flex: 0 0 9.5em; font-weight: 550; }
|
|
281
|
+
.exports span { color: var(--muted); font-size: 15px; }
|
|
282
|
+
|
|
283
|
+
/* Direct it with AI: pick a surface, get its setup. */
|
|
284
|
+
.ai { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); gap: 24px 40px; align-items: start; }
|
|
285
|
+
.ai-view p { margin: 0 0 16px; color: var(--muted); }
|
|
286
|
+
.ai-view .shortcut { margin: 0; padding: 18px 22px; background: var(--panel); border: 1px solid var(--line); border-radius: 16px; color: var(--ink); line-height: 2; }
|
|
287
|
+
.ai-view .keys { white-space: nowrap; }
|
|
288
|
+
.ai-view kbd { padding: 2px 8px; border: 1px solid var(--line-strong); border-bottom-width: 2px; border-radius: 6px; background: var(--paper); }
|
|
204
289
|
|
|
205
|
-
.
|
|
290
|
+
.code {
|
|
291
|
+
position: relative; padding: 18px 92px 18px 22px;
|
|
292
|
+
background: var(--panel); border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow-clay);
|
|
293
|
+
}
|
|
294
|
+
.code[hidden] { display: none; }
|
|
295
|
+
.code pre { margin: 0; overflow-x: auto; line-height: 1.7; }
|
|
296
|
+
.code .copy { position: absolute; right: 12px; top: 12px; min-height: 36px; padding: 0 14px; border: 0; background: none; color: var(--brand); font-weight: 600; }
|
|
297
|
+
.code .copy[data-state="copied"] { color: var(--ok); }
|
|
206
298
|
|
|
207
299
|
/* Quick start */
|
|
208
|
-
.start { display:
|
|
209
|
-
.start-head { flex: 1 1 280px; }
|
|
300
|
+
.start { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); gap: 32px 56px; align-items: start; }
|
|
210
301
|
.start-head h2 { margin-bottom: 8px; }
|
|
211
302
|
.start-head p { margin: 0 0 8px; color: var(--muted); }
|
|
212
|
-
.start-head a { display: inline-flex; align-items: center; min-height: 44px; font-size:
|
|
213
|
-
.start-body {
|
|
303
|
+
.start-head a { display: inline-flex; align-items: center; min-height: 44px; font-size: 15px; font-weight: 500; text-decoration: none; }
|
|
304
|
+
.start-body { min-width: 0; display: flex; flex-direction: column; gap: 14px; }
|
|
214
305
|
.seg { align-self: flex-start; display: flex; gap: 2px; padding: 2px; background: #dcdce1; border-radius: 10px; }
|
|
215
306
|
.seg button {
|
|
216
307
|
min-height: 44px; padding: 0 18px; border: 0; border-radius: 8px; background: transparent;
|
|
217
308
|
color: var(--ink); font-size: 14px; font-weight: 600; cursor: pointer; transition: background .15s;
|
|
218
309
|
}
|
|
219
|
-
.seg button[aria-pressed="true"] { background: var(--panel); box-shadow: 0 1px 2px rgba(0, 0, 0, .12); }
|
|
220
|
-
.code {
|
|
221
|
-
position: relative; min-height: 140px; padding: 16px 96px 16px 20px;
|
|
222
|
-
background: var(--panel); border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow-card);
|
|
223
|
-
}
|
|
224
|
-
.code[hidden] { display: none; }
|
|
225
|
-
.code pre { margin: 0; overflow-x: auto; line-height: 1.7; }
|
|
226
|
-
.code .copy { position: absolute; right: 12px; top: 12px; min-height: 44px; padding: 0 14px; border: 0; background: none; color: var(--accent); font-weight: 600; }
|
|
310
|
+
.seg button[aria-pressed="true"] { background: var(--panel); color: var(--ink); box-shadow: 0 1px 2px rgba(0, 0, 0, .12); }
|
|
227
311
|
|
|
228
312
|
table { width: 100%; border-collapse: collapse; }
|
|
229
313
|
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); }
|
|
230
314
|
th { font-weight: 600; white-space: nowrap; }
|
|
231
315
|
td { color: var(--muted); }
|
|
316
|
+
td:first-child { font-family: var(--mono); font-size: 14px; color: var(--ink); white-space: nowrap; }
|
|
232
317
|
.scroller { overflow-x: auto; }
|
|
233
318
|
|
|
234
319
|
details { border-bottom: 1px solid var(--line); padding: 6px 0; }
|
|
235
|
-
details summary { display: flex; align-items: center; min-height:
|
|
236
|
-
details
|
|
237
|
-
|
|
320
|
+
details summary { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 52px; cursor: pointer; font-weight: 600; list-style: none; }
|
|
321
|
+
details summary::-webkit-details-marker { display: none; }
|
|
322
|
+
details summary::after { content: "+"; flex: none; font: 400 22px/1 var(--mono); color: var(--muted); transition: transform .2s; }
|
|
323
|
+
details[open] summary::after { transform: rotate(45deg); }
|
|
324
|
+
details p { color: var(--muted); margin: 0 0 14px; max-width: 40em; }
|
|
325
|
+
#controls { padding: 0 0 104px; }
|
|
238
326
|
#controls .scroller { margin: 12px 0; }
|
|
239
|
-
#faq h2 { margin-bottom: 16px; }
|
|
240
327
|
|
|
241
|
-
|
|
328
|
+
/* Read */
|
|
329
|
+
.reads { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; }
|
|
330
|
+
a.card { display: flex; flex-direction: column; color: var(--ink); text-decoration: none; transition: transform .2s, border-color .2s; }
|
|
331
|
+
a.card:hover { transform: translateY(-3px); border-color: var(--line-strong); }
|
|
332
|
+
a.card p { flex: 1; font-size: 15px; }
|
|
333
|
+
a.card .more { margin-top: 20px; color: var(--brand); font-size: 15px; font-weight: 600; }
|
|
334
|
+
|
|
335
|
+
.closing { text-align: center; }
|
|
336
|
+
.closing .wrap { display: flex; flex-direction: column; align-items: center; gap: 24px; }
|
|
337
|
+
.closing img { width: 88px; height: auto; }
|
|
242
338
|
.closing h2 { margin: 0; }
|
|
339
|
+
.closing .sub { margin: 0; }
|
|
243
340
|
|
|
244
|
-
footer.site { border-top: 1px solid var(--line); padding:
|
|
245
|
-
footer.site .wrap { display: flex; flex-wrap: wrap; justify-content: space-between; gap:
|
|
341
|
+
footer.site { border-top: 1px solid var(--line); padding: 32px 0 40px; color: var(--muted); font-size: 14px; }
|
|
342
|
+
footer.site .wrap { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px 24px; }
|
|
246
343
|
footer.site p { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 20px; }
|
|
247
344
|
footer.site a { display: inline-flex; align-items: center; min-height: 44px; color: var(--muted); text-decoration: none; }
|
|
248
345
|
footer.site a:hover { color: var(--ink); }
|
|
346
|
+
footer.site p.legal { display: block; }
|
|
347
|
+
footer.site .legal a { display: inline; min-height: 0; text-decoration: underline; text-underline-offset: 2px; }
|
|
249
348
|
|
|
250
|
-
@media (max-width:
|
|
251
|
-
.
|
|
252
|
-
.
|
|
253
|
-
|
|
349
|
+
@media (max-width: 960px) {
|
|
350
|
+
.steps, .reads { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
351
|
+
.ai, .why, .start, .flow, .hero-grid { grid-template-columns: minmax(0, 1fr); }
|
|
352
|
+
}
|
|
353
|
+
@media (max-width: 760px) {
|
|
354
|
+
header.site nav { display: none; }
|
|
254
355
|
}
|
|
255
356
|
@media (max-width: 720px) {
|
|
256
|
-
|
|
257
|
-
.
|
|
258
|
-
.
|
|
259
|
-
.
|
|
260
|
-
|
|
357
|
+
:root { --header-h: 56px; }
|
|
358
|
+
.stage { height: 170vh; }
|
|
359
|
+
.stage-pin { justify-content: center; padding-top: var(--header-h); }
|
|
360
|
+
.stage-reel { margin-top: 6vh; }
|
|
361
|
+
body { font-size: 16px; }
|
|
362
|
+
.brand img { width: 32px; }
|
|
363
|
+
section { padding: 72px 0; }
|
|
364
|
+
#controls { padding-bottom: 72px; }
|
|
365
|
+
.hero-actions .btn, .hero-actions .cmd { width: 100%; }
|
|
366
|
+
.hero-actions .cmd { justify-content: space-between; }
|
|
367
|
+
.flow-tabs, .ai-tabs { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); gap: 4px; }
|
|
368
|
+
.flow-tabs button, .ai-tabs button { width: auto; padding: 10px 6px; flex-direction: column; align-items: center; gap: 6px; text-align: center; }
|
|
369
|
+
.flow-tabs em, .ai-tabs span { display: none; }
|
|
370
|
+
.flow-tabs strong, .ai-tabs strong { margin: 0; font-size: 14px; }
|
|
371
|
+
.flow-tabs b { width: 26px; height: 26px; font-size: 13px; }
|
|
372
|
+
.flow-exports { aspect-ratio: auto; padding: 4px 20px; }
|
|
261
373
|
}
|
|
262
374
|
@media (prefers-reduced-motion: reduce) {
|
|
375
|
+
.stage { height: auto; --p: 0 !important; }
|
|
376
|
+
.stage-pin { position: relative; height: auto; padding-bottom: 72px; }
|
|
377
|
+
.stage-reel { transform: none; }
|
|
378
|
+
.stage-cue { display: none; }
|
|
263
379
|
html { scroll-behavior: auto; }
|
|
264
|
-
.btn, a.card, .seg button { transition: none; }
|
|
380
|
+
.btn, a.card, .seg button, details summary::after { transition: none; }
|
|
265
381
|
}
|
|
266
382
|
</style>
|
|
267
|
-
<script type="module" crossorigin src="/assets/landing-y3EqDgSZ.js"></script>
|
|
268
|
-
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-P2Xu9kJm.js">
|
|
269
383
|
</head>
|
|
270
384
|
<body>
|
|
271
385
|
<header class="site">
|
|
272
386
|
<div class="wrap">
|
|
273
|
-
<a class="brand" href="/"><img src="/media/cozyclay-
|
|
274
|
-
<nav>
|
|
387
|
+
<a class="brand" href="/"><img src="/media/cozyclay-mark.png" width="144" height="98" alt="" />CozyClay</a>
|
|
388
|
+
<nav aria-label="Sections">
|
|
275
389
|
<a href="#how">How it works</a>
|
|
276
|
-
<a href="#
|
|
390
|
+
<a href="#ai">AI agents</a>
|
|
277
391
|
<a href="#start">Quick start</a>
|
|
278
|
-
<a href="https://github.com/NomaDamas/CozyClay">GitHub</a>
|
|
279
|
-
<a class="btn primary" href="#try">Try it</a>
|
|
280
392
|
</nav>
|
|
393
|
+
<div class="actions">
|
|
394
|
+
<a class="icon-link" href="https://github.com/NomaDamas/CozyClay" aria-label="CozyClay on GitHub">
|
|
395
|
+
<svg width="22" height="22" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
396
|
+
</a>
|
|
397
|
+
<a class="btn primary sm" href="/app/">Try it</a>
|
|
398
|
+
</div>
|
|
281
399
|
</div>
|
|
282
400
|
</header>
|
|
283
401
|
|
|
284
402
|
<main>
|
|
285
|
-
<section class="
|
|
286
|
-
<div class="
|
|
287
|
-
<h1
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
403
|
+
<section class="stage" id="stage">
|
|
404
|
+
<div class="stage-pin">
|
|
405
|
+
<h1 class="curved" aria-label="Previs the shot"><span class="line">Previs the shot</span></h1>
|
|
406
|
+
<div class="stage-reel">
|
|
407
|
+
<div class="window">
|
|
408
|
+
<div class="compare" id="compare" style="--pos: 50%">
|
|
409
|
+
<video class="cmp-a" src="/media/truck-greybox.mp4" poster="/media/truck-greybox-poster.jpg" width="1920" height="1080" autoplay muted loop playsinline preload="metadata"></video>
|
|
410
|
+
<video class="cmp-b" src="/media/truck-seedance.mp4" poster="/media/truck-seedance-poster.jpg" width="1920" height="1080" autoplay muted loop playsinline preload="metadata" aria-hidden="true"></video>
|
|
411
|
+
<div class="cmp-line" aria-hidden="true"><span class="cmp-knob"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l-6 6 6 6M15 6l6 6-6 6"/></svg></span></div>
|
|
412
|
+
<input class="cmp-range" type="range" min="0" max="100" step="1" value="50" aria-label="Compare the CozyClay greybox with the Seedance 2.5 take" />
|
|
413
|
+
<span class="tag-l" aria-hidden="true">CozyClay</span>
|
|
414
|
+
<span class="tag-r" aria-hidden="true">Seedance 2.5</span>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
292
417
|
</div>
|
|
293
|
-
<span class="
|
|
294
|
-
<code>npx cozyclay --via site</code>
|
|
295
|
-
<button type="button" class="copy" data-copy="npx cozyclay --via site" aria-label="Copy command">
|
|
296
|
-
<svg class="ic-copy" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15V6a2 2 0 0 1 2-2h9"></path></svg>
|
|
297
|
-
<svg class="ic-done" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12l5 5 9-10"></path></svg>
|
|
298
|
-
</button>
|
|
299
|
-
</span>
|
|
418
|
+
<span class="stage-cue" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg></span>
|
|
300
419
|
</div>
|
|
301
420
|
</section>
|
|
302
421
|
|
|
303
|
-
<section class="
|
|
304
|
-
<div class="wrap">
|
|
305
|
-
<
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
></video>
|
|
314
|
-
</div>
|
|
315
|
-
<figcaption>Greybox left, generated right. Same camera, every cut.</figcaption>
|
|
316
|
-
</figure>
|
|
422
|
+
<section id="why" class="alt">
|
|
423
|
+
<div class="wrap why">
|
|
424
|
+
<div>
|
|
425
|
+
<span class="kicker">Why</span>
|
|
426
|
+
<h2>A text prompt cannot hold a whole scene</h2>
|
|
427
|
+
</div>
|
|
428
|
+
<div>
|
|
429
|
+
<p>AI video costs money per take, and one miss means paying again. A prompt cannot say where everyone stands, how they move, where the camera is and when it cuts.</p>
|
|
430
|
+
<p><strong>Make those decisions first, in 3D, where they are cheap to change.</strong> The video model then gets the shot as a first frame, a greybox reference clip and a prompt that describes the real framing. It follows the blocking instead of guessing.</p>
|
|
431
|
+
</div>
|
|
317
432
|
</div>
|
|
318
433
|
</section>
|
|
319
434
|
|
|
320
435
|
<section id="how">
|
|
321
436
|
<div class="wrap">
|
|
322
|
-
<
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
<
|
|
437
|
+
<span class="kicker">How it works</span>
|
|
438
|
+
<h2>Stage, move, shoot, export</h2>
|
|
439
|
+
<p class="sub">One local workspace that handles like a 3D editor, from the empty deck to the files a video model reads.</p>
|
|
440
|
+
<div class="flow">
|
|
441
|
+
<div class="flow-tabs" role="tablist" aria-label="Workflow steps">
|
|
442
|
+
<button type="button" role="tab" id="flow-t1" aria-controls="flow-p1" aria-selected="true">
|
|
443
|
+
<b>1</b><span><strong>Stage</strong><em>Build the set from primitives. Add rigged characters, or capsule stand-ins.</em></span>
|
|
444
|
+
</button>
|
|
445
|
+
<button type="button" role="tab" id="flow-t2" aria-controls="flow-p2" aria-selected="false" tabindex="-1">
|
|
446
|
+
<b>2</b><span><strong>Move</strong><em>Describe the action. The compiler bakes every frame; QA checks collisions, foot slide and reach.</em></span>
|
|
447
|
+
</button>
|
|
448
|
+
<button type="button" role="tab" id="flow-t3" aria-controls="flow-p3" aria-selected="false" tabindex="-1">
|
|
449
|
+
<b>3</b><span><strong>Shoot</strong><em>Shots on a timeline, dolly rails and cranes, a look through the lens.</em></span>
|
|
450
|
+
</button>
|
|
451
|
+
<button type="button" role="tab" id="flow-t4" aria-controls="flow-p4" aria-selected="false" tabindex="-1">
|
|
452
|
+
<b>4</b><span><strong>Export</strong><em>Everything the video model needs, in one pack. Every edit is one undo step.</em></span>
|
|
453
|
+
</button>
|
|
328
454
|
</div>
|
|
329
|
-
<div class="
|
|
330
|
-
<
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
455
|
+
<div class="flow-view">
|
|
456
|
+
<figure role="tabpanel" id="flow-p1" aria-labelledby="flow-t1">
|
|
457
|
+
<video src="/media/step-stage.mp4" poster="/media/step-stage-poster.jpg" width="1440" height="600" muted loop playsinline preload="none" aria-label="A whole street built from cubes and cylinders in CozyClay."></video>
|
|
458
|
+
<figcaption>A whole street from cubes and cylinders, railings and lamps included.</figcaption>
|
|
459
|
+
</figure>
|
|
460
|
+
<figure role="tabpanel" id="flow-p2" aria-labelledby="flow-t2" hidden>
|
|
461
|
+
<video src="/media/step-move.mp4" poster="/media/step-move-poster.jpg" width="1440" height="600" muted loop playsinline preload="none" aria-label="A request to the agent, then the clay character walks to a ball and pushes it with both hands."></video>
|
|
462
|
+
<figcaption>Ask the agent; the character walks to the ball and pushes it with both hands.</figcaption>
|
|
463
|
+
</figure>
|
|
464
|
+
<figure role="tabpanel" id="flow-p3" aria-labelledby="flow-t3" hidden>
|
|
465
|
+
<video src="/media/step-shoot.mp4" poster="/media/step-shoot-poster.jpg" width="1440" height="600" muted loop playsinline preload="none" aria-label="The shot camera riding a four-point rail while the crane rises, with its live preview."></video>
|
|
466
|
+
<figcaption>The camera rides a rail while the crane takes it up, previewed live.</figcaption>
|
|
467
|
+
</figure>
|
|
468
|
+
<div role="tabpanel" id="flow-p4" aria-labelledby="flow-t4" class="flow-exports" hidden>
|
|
469
|
+
<ul class="exports" aria-label="Export formats">
|
|
470
|
+
<li><b>Keyframe pack</b><span>First and last frame, greybox clip, camera, prompt</span></li>
|
|
471
|
+
<li><b>Video</b><span>The shot as an mp4</span></li>
|
|
472
|
+
<li><b>Passes</b><span>Depth and normal</span></li>
|
|
473
|
+
<li><b>Contact sheet</b><span>The shots side by side</span></li>
|
|
474
|
+
<li><b>OTIO cut list</b><span>The edit, for your editing software</span></li>
|
|
475
|
+
<li><b>Storyboard</b><span>Panels with captions and an animatic</span></li>
|
|
476
|
+
</ul>
|
|
477
|
+
</div>
|
|
338
478
|
</div>
|
|
339
479
|
</div>
|
|
340
480
|
</div>
|
|
341
481
|
</section>
|
|
342
482
|
|
|
343
|
-
<section id="
|
|
483
|
+
<section id="ai">
|
|
344
484
|
<div class="wrap">
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
<span class="playground-phone">Needs a mouse and keyboard. Come back on a computer, or run <code>npx cozyclay --via site</code>.</span>
|
|
354
|
-
</button>
|
|
355
|
-
<div class="playground-loading">Loading the studio…</div>
|
|
356
|
-
</div>
|
|
357
|
-
<div class="playground-bar">
|
|
358
|
-
<span class="spacer"></span>
|
|
359
|
-
<button type="button" id="playground-full">Fullscreen</button>
|
|
360
|
-
<a href="/app/" target="_blank" rel="noopener">Open studio ↗</a>
|
|
361
|
-
<button type="button" id="playground-close">Close</button>
|
|
362
|
-
</div>
|
|
363
|
-
</div>
|
|
364
|
-
</div>
|
|
365
|
-
<p class="small demo-note">Needs a mouse and keyboard.</p>
|
|
366
|
-
</div>
|
|
367
|
-
</section>
|
|
368
|
-
|
|
369
|
-
<section id="what">
|
|
370
|
-
<div class="wrap">
|
|
371
|
-
<h2>What you can do</h2>
|
|
372
|
-
<div class="cards">
|
|
373
|
-
<div class="card">
|
|
374
|
-
<h3>Stage a scene</h3>
|
|
375
|
-
<p>Primitives, imported models, plan view.</p>
|
|
376
|
-
</div>
|
|
377
|
-
<div class="card">
|
|
378
|
-
<h3>Move through the set</h3>
|
|
379
|
-
<p>Fly, walk, orbit, frame.</p>
|
|
380
|
-
</div>
|
|
381
|
-
<div class="card">
|
|
382
|
-
<h3>Author the camera</h3>
|
|
383
|
-
<p>Lens, dolly, crane, cuts.</p>
|
|
485
|
+
<span class="kicker">Direct it with AI</span>
|
|
486
|
+
<h2>Your agent builds the scene while you watch</h2>
|
|
487
|
+
<p class="sub">The same scene can be driven from three places, live in the viewport. Bring ChatGPT, Anthropic, OpenAI, Gemini, MiniMax, OpenRouter or CLIProxyAPI.</p>
|
|
488
|
+
<div class="ai">
|
|
489
|
+
<div class="ai-tabs" role="tablist" aria-label="Ways to drive CozyClay">
|
|
490
|
+
<button type="button" role="tab" id="ai-t1" aria-controls="ai-p1" aria-selected="true"><strong>Agent panel</strong><span>In the studio</span></button>
|
|
491
|
+
<button type="button" role="tab" id="ai-t2" aria-controls="ai-p2" aria-selected="false" tabindex="-1"><strong>MCP server</strong><span>Claude, Cursor, any client</span></button>
|
|
492
|
+
<button type="button" role="tab" id="ai-t3" aria-controls="ai-p3" aria-selected="false" tabindex="-1"><strong>Terminal</strong><span>Coding agents</span></button>
|
|
384
493
|
</div>
|
|
385
|
-
<div class="
|
|
386
|
-
<
|
|
387
|
-
|
|
494
|
+
<div class="ai-view">
|
|
495
|
+
<div role="tabpanel" id="ai-p1" aria-labelledby="ai-t1">
|
|
496
|
+
<p>Chat beside the viewport and watch each change land. Sign in with ChatGPT or use your own key. Every change the agent makes is one undo step.</p>
|
|
497
|
+
<p class="shortcut">Open the studio, then press <span class="keys"><kbd>Cmd/Ctrl</kbd> + <kbd>B</kbd></span></p>
|
|
498
|
+
</div>
|
|
499
|
+
<div role="tabpanel" id="ai-p2" aria-labelledby="ai-t2" hidden>
|
|
500
|
+
<p>Add CozyClay to Claude Desktop, Claude Code, Cursor or any MCP client. The agent edits the scene in the studio you have open.</p>
|
|
501
|
+
<div class="code">
|
|
502
|
+
<pre><code>{
|
|
503
|
+
"mcpServers": {
|
|
504
|
+
"cozyclay": {
|
|
505
|
+
"command": "npx",
|
|
506
|
+
"args": ["-y", "cozyclay", "mcp"]
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}</code></pre>
|
|
510
|
+
<button type="button" class="copy" data-copy='{ "mcpServers": { "cozyclay": { "command": "npx", "args": ["-y", "cozyclay", "mcp"] } } }'>Copy</button>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
<div role="tabpanel" id="ai-p3" aria-labelledby="ai-t3" hidden>
|
|
514
|
+
<p>Coding agents drive a running studio from the shell, JSON in and out.</p>
|
|
515
|
+
<div class="code">
|
|
516
|
+
<pre><code>npx cozyclay
|
|
517
|
+
cclay live status --wait</code></pre>
|
|
518
|
+
<button type="button" class="copy" data-copy="npx cozyclay cclay live status --wait">Copy</button>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
388
521
|
</div>
|
|
389
522
|
</div>
|
|
390
523
|
</div>
|
|
@@ -393,9 +526,10 @@
|
|
|
393
526
|
<section id="start" class="alt">
|
|
394
527
|
<div class="wrap start">
|
|
395
528
|
<div class="start-head">
|
|
396
|
-
<
|
|
397
|
-
<
|
|
398
|
-
<a
|
|
529
|
+
<span class="kicker">Quick start</span>
|
|
530
|
+
<h2>One command</h2>
|
|
531
|
+
<p>Node.js 22.19+ and a Chromium browser. The studio opens at <code>127.0.0.1:5180/app/</code>.</p>
|
|
532
|
+
<a href="#controls" id="controls-link">Full control list →</a>
|
|
399
533
|
</div>
|
|
400
534
|
<div class="start-body">
|
|
401
535
|
<div class="seg" id="start-tabs">
|
|
@@ -413,6 +547,7 @@ npm install
|
|
|
413
547
|
npm run dev</code></pre>
|
|
414
548
|
<button type="button" class="copy" data-copy="git clone https://github.com/NomaDamas/CozyClay.git cd CozyClay npm install npm run dev">Copy</button>
|
|
415
549
|
</div>
|
|
550
|
+
<p class="small">Asking an AI agent to set it up? Give it the repository link; its instructions are in <a href="https://github.com/NomaDamas/CozyClay/blob/main/INSTALL_FOR_AGENTS.md">INSTALL_FOR_AGENTS.md</a>.</p>
|
|
416
551
|
</div>
|
|
417
552
|
</div>
|
|
418
553
|
</section>
|
|
@@ -444,48 +579,41 @@ npm run dev</code></pre>
|
|
|
444
579
|
|
|
445
580
|
<section id="read">
|
|
446
581
|
<div class="wrap">
|
|
582
|
+
<span class="kicker">Guides</span>
|
|
447
583
|
<h2>Read</h2>
|
|
448
|
-
<div class="
|
|
449
|
-
<a class="card" href="/greybox-to-video/">
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
584
|
+
<div class="reads">
|
|
585
|
+
<a class="card" href="/greybox-to-video/">
|
|
586
|
+
<h3>Greybox to AI video</h3>
|
|
587
|
+
<p>Why a grey blockout is the most reliable input for a video model, and the block-then-generate loop.</p>
|
|
588
|
+
<span class="more">Read →</span>
|
|
589
|
+
</a>
|
|
590
|
+
<a class="card" href="/seedance-camera-control/">
|
|
591
|
+
<h3>Seedance 2.5 camera control</h3>
|
|
592
|
+
<p>Getting the same camera on every take with a reference clip.</p>
|
|
593
|
+
<span class="more">Read →</span>
|
|
594
|
+
</a>
|
|
595
|
+
<a class="card" href="/previs-software/">
|
|
596
|
+
<h3>Previs software compared</h3>
|
|
597
|
+
<p>Free, open source and browser-based previs tools side by side.</p>
|
|
598
|
+
<span class="more">Read →</span>
|
|
599
|
+
</a>
|
|
600
|
+
<a class="card" href="/ai-camera-control/">
|
|
601
|
+
<h3>Camera control for AI video</h3>
|
|
602
|
+
<p>Why camera prompts drift and what the models actually accept.</p>
|
|
603
|
+
<span class="more">Read →</span>
|
|
604
|
+
</a>
|
|
453
605
|
</div>
|
|
454
606
|
</div>
|
|
455
607
|
</section>
|
|
456
608
|
|
|
457
|
-
<section
|
|
458
|
-
<div class="wrap narrow">
|
|
459
|
-
<h2>FAQ</h2>
|
|
460
|
-
<details>
|
|
461
|
-
<summary>What is CozyClay?</summary>
|
|
462
|
-
<p>Open source previs software in a browser: block a scene, pose characters, author camera moves and cuts, preview motion.</p>
|
|
463
|
-
</details>
|
|
464
|
-
<details>
|
|
465
|
-
<summary>Previs or previz?</summary>
|
|
466
|
-
<p>Same thing. Previsualization: the rough 3D pass where a shot gets blocked before anyone pays for final assets or renders.</p>
|
|
467
|
-
</details>
|
|
468
|
-
<details>
|
|
469
|
-
<summary>Can I use it for camera control in AI video?</summary>
|
|
470
|
-
<p>That is why it exists. Block the shot in 3D, then hand the camera move to the video model instead of describing it in a prompt. See <a href="/ai-camera-control/">camera control for AI video</a>.</p>
|
|
471
|
-
</details>
|
|
472
|
-
<details>
|
|
473
|
-
<summary>Do I need Blender or Unity?</summary>
|
|
474
|
-
<p>No. It runs in a Chromium browser. Controls follow Unity conventions, nothing to install.</p>
|
|
475
|
-
</details>
|
|
476
|
-
<details>
|
|
477
|
-
<summary>Is it free? Does it phone home?</summary>
|
|
478
|
-
<p>Free, AGPL-3.0, runs on your machine and works offline. Project content is never uploaded. The npm package sends anonymous usage metrics; <code>cclay telemetry off</code> disables them.</p>
|
|
479
|
-
</details>
|
|
480
|
-
</div>
|
|
481
|
-
</section>
|
|
482
|
-
|
|
483
|
-
<section class="closing alt">
|
|
609
|
+
<section class="closing">
|
|
484
610
|
<div class="wrap">
|
|
611
|
+
<img src="/media/cozyclay-mark.png" width="144" height="98" alt="" loading="lazy" />
|
|
485
612
|
<h2>Block your first shot</h2>
|
|
613
|
+
<p class="sub">Free, open source, and running in your browser in under a minute.</p>
|
|
486
614
|
<div class="cta">
|
|
487
|
-
<a class="btn primary" href="
|
|
488
|
-
<a class="btn ghost" href="https://
|
|
615
|
+
<a class="btn primary" href="/app/">Try it in your browser</a>
|
|
616
|
+
<a class="btn ghost" href="https://discord.gg/Hp9V5wwgjF">Join the Discord</a>
|
|
489
617
|
</div>
|
|
490
618
|
</div>
|
|
491
619
|
</section>
|
|
@@ -505,26 +633,167 @@ npm run dev</code></pre>
|
|
|
505
633
|
document.getElementById("controls-link").addEventListener("click", () => {
|
|
506
634
|
document.querySelector("#controls details").open = true;
|
|
507
635
|
});
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
636
|
+
document.querySelectorAll("button.copy[data-copy]").forEach((button) => {
|
|
637
|
+
button.addEventListener("click", async () => {
|
|
638
|
+
try { await navigator.clipboard.writeText(button.dataset.copy); button.dataset.state = "copied"; setTimeout(() => { button.dataset.state = ""; }, 1400); } catch {}
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
// Hero compare: drag (or arrow keys on the hidden range) moves the divider;
|
|
642
|
+
// the generated take follows the greybox clock so both show the same frame.
|
|
643
|
+
const compare = document.getElementById("compare");
|
|
644
|
+
const greybox = compare.querySelector(".cmp-a");
|
|
645
|
+
const take = compare.querySelector(".cmp-b");
|
|
646
|
+
const range = compare.querySelector(".cmp-range");
|
|
647
|
+
const setPos = (pct) => {
|
|
648
|
+
const p = Math.min(100, Math.max(0, pct));
|
|
649
|
+
compare.style.setProperty("--pos", p + "%");
|
|
650
|
+
range.value = String(Math.round(p));
|
|
651
|
+
};
|
|
652
|
+
const fromPointer = (event) => {
|
|
653
|
+
const box = compare.getBoundingClientRect();
|
|
654
|
+
setPos(((event.clientX - box.left) / box.width) * 100);
|
|
655
|
+
};
|
|
656
|
+
compare.addEventListener("pointerdown", (event) => {
|
|
657
|
+
compare.setPointerCapture(event.pointerId);
|
|
658
|
+
fromPointer(event);
|
|
659
|
+
});
|
|
660
|
+
compare.addEventListener("pointermove", (event) => {
|
|
661
|
+
if (compare.hasPointerCapture(event.pointerId)) fromPointer(event);
|
|
513
662
|
});
|
|
663
|
+
range.addEventListener("input", () => setPos(Number(range.value)));
|
|
664
|
+
// The greybox is the clock. Every frame the take is checked against it and
|
|
665
|
+
// re-seeked when it drifts more than about one frame; both files have a
|
|
666
|
+
// keyframe every second, so the seek lands at once. A short cooldown lets
|
|
667
|
+
// a seek finish before the next check. The take holds while the greybox
|
|
668
|
+
// buffers.
|
|
669
|
+
let settleUntil = 0;
|
|
670
|
+
const syncTake = () => {
|
|
671
|
+
if (greybox.paused !== take.paused) (greybox.paused ? take.pause() : take.play().catch(() => {}));
|
|
672
|
+
if (take.seeking || performance.now() < settleUntil) return;
|
|
673
|
+
if (Math.abs(take.currentTime - greybox.currentTime) > 0.045) {
|
|
674
|
+
take.currentTime = greybox.currentTime;
|
|
675
|
+
settleUntil = performance.now() + 400;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
const tick = () => {
|
|
679
|
+
if (!greybox.paused) syncTake();
|
|
680
|
+
if (greybox.requestVideoFrameCallback) greybox.requestVideoFrameCallback(tick);
|
|
681
|
+
else requestAnimationFrame(tick);
|
|
682
|
+
};
|
|
683
|
+
tick();
|
|
684
|
+
greybox.addEventListener("waiting", () => take.pause());
|
|
685
|
+
// Tab lists (workflow steps, agent surfaces): click or arrow keys pick a panel.
|
|
686
|
+
document.querySelectorAll("[role=tablist]").forEach((list) => {
|
|
687
|
+
const tabs = [...list.querySelectorAll("[role=tab]")];
|
|
688
|
+
const select = (tab) => tabs.forEach((other) => {
|
|
689
|
+
const on = other === tab;
|
|
690
|
+
other.setAttribute("aria-selected", String(on));
|
|
691
|
+
other.tabIndex = on ? 0 : -1;
|
|
692
|
+
document.getElementById(other.getAttribute("aria-controls")).hidden = !on;
|
|
693
|
+
});
|
|
694
|
+
tabs.forEach((tab, i) => {
|
|
695
|
+
tab.addEventListener("click", () => select(tab));
|
|
696
|
+
tab.addEventListener("keydown", (event) => {
|
|
697
|
+
const move = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
|
|
698
|
+
if (!move) return;
|
|
699
|
+
event.preventDefault();
|
|
700
|
+
const next = tabs[(i + move + tabs.length) % tabs.length];
|
|
701
|
+
select(next);
|
|
702
|
+
next.focus();
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
// How it works: the open step's clip plays while the section is on screen.
|
|
707
|
+
const flowView = document.querySelector(".flow-view");
|
|
708
|
+
let flowSeen = false;
|
|
709
|
+
const playOpenStep = () => flowView.querySelectorAll("video").forEach((clip) => {
|
|
710
|
+
if (flowSeen && !clip.closest("[role=tabpanel]").hidden) clip.play().catch(() => {});
|
|
711
|
+
else clip.pause();
|
|
712
|
+
});
|
|
713
|
+
new IntersectionObserver(([entry]) => { flowSeen = entry.isIntersecting; playOpenStep(); }).observe(flowView);
|
|
714
|
+
document.querySelectorAll(".flow-tabs [role=tab]").forEach((tab) => tab.addEventListener("click", playOpenStep));
|
|
715
|
+
document.querySelector(".flow-tabs").addEventListener("keydown", () => setTimeout(playOpenStep));
|
|
716
|
+
// Hero headline: the letters sit on the front of a cylinder, so the
|
|
717
|
+
// middle bulges toward the viewer and the ends fall away.
|
|
718
|
+
const headline = document.querySelector("h1.curved");
|
|
719
|
+
headline.querySelectorAll(".line").forEach((line) => {
|
|
720
|
+
const text = line.textContent;
|
|
721
|
+
line.textContent = "";
|
|
722
|
+
line.setAttribute("aria-hidden", "true");
|
|
723
|
+
for (const char of text) {
|
|
724
|
+
const span = document.createElement("span");
|
|
725
|
+
span.className = "ch";
|
|
726
|
+
span.textContent = char;
|
|
727
|
+
line.appendChild(span);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
const bend = () => {
|
|
731
|
+
const chars = [...headline.querySelectorAll(".ch")];
|
|
732
|
+
chars.forEach((c) => { c.style.transform = ""; });
|
|
733
|
+
const box = headline.getBoundingClientRect();
|
|
734
|
+
const centre = box.left + box.width / 2;
|
|
735
|
+
const half = Math.max(...[...headline.querySelectorAll(".line")].map((l) => l.getBoundingClientRect().width)) / 2;
|
|
736
|
+
chars.forEach((c) => {
|
|
737
|
+
const r = c.getBoundingClientRect();
|
|
738
|
+
const u = Math.max(-1, Math.min(1, (r.left + r.width / 2 - centre) / half));
|
|
739
|
+
const depth = Math.sqrt(1 - 0.75 * u * u);
|
|
740
|
+
c.style.transform = "scale(" + (0.84 + 0.16 * depth).toFixed(3) + ")";
|
|
741
|
+
});
|
|
742
|
+
};
|
|
743
|
+
bend();
|
|
744
|
+
document.fonts.ready.then(bend);
|
|
745
|
+
addEventListener("resize", bend);
|
|
746
|
+
// First screen: map the scroll through the stage to --p, and size the
|
|
747
|
+
// reel's rise and growth so it ends centred at the full width.
|
|
748
|
+
const stage = document.getElementById("stage");
|
|
749
|
+
const reel = stage.querySelector(".stage-reel");
|
|
750
|
+
const header = document.querySelector("header.site");
|
|
751
|
+
const still = matchMedia("(prefers-reduced-motion: reduce)");
|
|
752
|
+
const measure = () => {
|
|
753
|
+
const pin = stage.querySelector(".stage-pin");
|
|
754
|
+
const start = 0.94;
|
|
755
|
+
const width = reel.offsetWidth;
|
|
756
|
+
const full = Math.min(innerWidth * 0.98, (innerHeight - header.offsetHeight - 24) * 16 / 9 + 0);
|
|
757
|
+
const end = Math.max(start, full / width);
|
|
758
|
+
stage.style.setProperty("--grow", (end - start).toFixed(3));
|
|
759
|
+
const top = reel.offsetTop;
|
|
760
|
+
const target = header.offsetHeight + Math.max(12, (innerHeight - header.offsetHeight - reel.offsetHeight * end) / 2);
|
|
761
|
+
stage.style.setProperty("--rise", (target - top) + "px");
|
|
762
|
+
void pin;
|
|
763
|
+
};
|
|
764
|
+
let queued = false;
|
|
765
|
+
const onScroll = () => {
|
|
766
|
+
queued = false;
|
|
767
|
+
const box = stage.getBoundingClientRect();
|
|
768
|
+
const span = stage.offsetHeight - innerHeight;
|
|
769
|
+
const p = still.matches || span <= 0 ? 0 : Math.min(1, Math.max(0, -box.top / span));
|
|
770
|
+
stage.style.setProperty("--p", p.toFixed(4));
|
|
771
|
+
document.body.classList.toggle("on-stage", box.bottom > header.offsetHeight);
|
|
772
|
+
};
|
|
773
|
+
const queue = () => { if (!queued) { queued = true; requestAnimationFrame(onScroll); } };
|
|
774
|
+
measure();
|
|
775
|
+
onScroll();
|
|
776
|
+
addEventListener("scroll", queue, { passive: true });
|
|
777
|
+
addEventListener("resize", () => { measure(); queue(); });
|
|
778
|
+
greybox.addEventListener("seeked", syncTake);
|
|
779
|
+
greybox.addEventListener("play", syncTake);
|
|
780
|
+
greybox.addEventListener("pause", syncTake);
|
|
514
781
|
})();
|
|
515
782
|
</script>
|
|
516
783
|
|
|
517
784
|
<footer class="site">
|
|
518
785
|
<div class="wrap">
|
|
519
786
|
<p>
|
|
520
|
-
<a href="https://github.com/NomaDamas/CozyClay">
|
|
787
|
+
<a href="https://github.com/NomaDamas/CozyClay">GitHub (AGPL)</a>
|
|
521
788
|
<a href="https://www.npmjs.com/package/cozyclay">npm</a>
|
|
789
|
+
<a href="https://github.com/NomaDamas/CozyClay/blob/main/CHANGELOG.md">Changelog</a>
|
|
790
|
+
<a href="https://discord.gg/Hp9V5wwgjF">Discord</a>
|
|
522
791
|
<a href="/privacy/">Privacy</a>
|
|
523
792
|
</p>
|
|
524
|
-
<p>
|
|
525
|
-
|
|
793
|
+
<p class="legal">
|
|
794
|
+
Made by <a href="https://github.com/NomaDamas">NomaDamas</a>. Not affiliated with or endorsed by NVIDIA or Unity Technologies.
|
|
526
795
|
</p>
|
|
527
796
|
</div>
|
|
528
797
|
</footer>
|
|
529
798
|
</body>
|
|
530
|
-
</html>
|
|
799
|
+
</html>
|