Spaces:
Runtime error
Runtime error
Commit ·
4c105cb
1
Parent(s): bfe94d3
adding new demo + landing page improvements
Browse files- frontend/src/components/FeatureShowcase.tsx +244 -46
- frontend/src/pages/Landing.tsx +19 -5
- frontend/src/styles/showcase.css +207 -12
- renderer/src/compositions/LandingDemo.tsx +105 -72
frontend/src/components/FeatureShowcase.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import
|
| 2 |
import { Link } from "react-router-dom";
|
| 3 |
|
| 4 |
/** Landing page feature tour: one full section per feature, alternating sides, each with a small
|
|
@@ -33,36 +33,180 @@ function ChatVisual() {
|
|
| 33 |
);
|
| 34 |
}
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
function MapVisual() {
|
| 37 |
-
const
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
[0, 1],
|
| 46 |
[0, 2],
|
|
|
|
| 47 |
[0, 4],
|
| 48 |
-
[1,
|
| 49 |
-
[2,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
];
|
| 51 |
return (
|
| 52 |
-
<div className="fx fx-map">
|
| 53 |
-
<
|
| 54 |
-
{
|
| 55 |
-
<
|
|
|
|
|
|
|
| 56 |
))}
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
</text>
|
| 64 |
</g>
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
</svg>
|
| 67 |
</div>
|
| 68 |
);
|
|
@@ -89,19 +233,57 @@ function AudioVisual() {
|
|
| 89 |
}
|
| 90 |
|
| 91 |
function VideoVisual() {
|
|
|
|
| 92 |
return (
|
| 93 |
<div className="fx fx-video">
|
| 94 |
-
<div className="fx-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
</div>
|
| 98 |
<div className="fx-frame v169">
|
| 99 |
-
<span className="mono">
|
| 100 |
-
<b>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
</div>
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
);
|
|
@@ -178,33 +360,49 @@ const FEATURES: Feature[] = [
|
|
| 178 |
eyebrow: "Research notebook",
|
| 179 |
title: "Ask your archive anything. It answers with receipts.",
|
| 180 |
body: "Notestack reads your posts the way a careful researcher would: it searches, opens the right post, and cites the exact lines. If you never wrote it, it tells you so instead of guessing.",
|
| 181 |
-
points: ["Every sentence links to the lines it came from", "Chats are saved per notebook", "
|
| 182 |
visual: <ChatVisual />,
|
| 183 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
{
|
| 185 |
id: "map",
|
| 186 |
-
eyebrow: "
|
| 187 |
-
title: "
|
| 188 |
-
body: "
|
| 189 |
-
points: ["
|
| 190 |
visual: <MapVisual />,
|
| 191 |
},
|
| 192 |
{
|
| 193 |
id: "audio",
|
| 194 |
eyebrow: "Audio overview",
|
| 195 |
title: "Two hosts talk through your ideas.",
|
| 196 |
-
body: "Turn a post or a whole notebook into a natural conversation, voiced by ElevenLabs. Pick from a library of
|
| 197 |
-
points: ["Deep dive, brief or debate formats", "Every line grounded in your posts", "
|
| 198 |
visual: <AudioVisual />,
|
| 199 |
},
|
| 200 |
{
|
| 201 |
id: "video",
|
| 202 |
eyebrow: "Video studio",
|
| 203 |
-
title: "
|
| 204 |
-
body: "
|
| 205 |
-
points: ["
|
| 206 |
visual: <VideoVisual />,
|
| 207 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
{
|
| 209 |
id: "kit",
|
| 210 |
eyebrow: "Launch Kit",
|
|
@@ -214,12 +412,12 @@ const FEATURES: Feature[] = [
|
|
| 214 |
visual: <KitVisual />,
|
| 215 |
},
|
| 216 |
{
|
| 217 |
-
id: "
|
| 218 |
-
eyebrow: "
|
| 219 |
-
title: "
|
| 220 |
-
body: "
|
| 221 |
-
points: ["
|
| 222 |
-
visual: <
|
| 223 |
},
|
| 224 |
{
|
| 225 |
id: "pad",
|
|
|
|
| 1 |
+
import { useEffect, useRef, useState, type ReactNode, type RefObject } from "react";
|
| 2 |
import { Link } from "react-router-dom";
|
| 3 |
|
| 4 |
/** Landing page feature tour: one full section per feature, alternating sides, each with a small
|
|
|
|
| 33 |
);
|
| 34 |
}
|
| 35 |
|
| 36 |
+
const SHAPES = ["Orbit", "Spiral", "Figure", "Cluster"];
|
| 37 |
+
const SHAPE_MS = 3500; // how long each shape is shown on its own
|
| 38 |
+
const MORPH_MS = 1300;
|
| 39 |
+
|
| 40 |
+
/** Where the four outer topics sit in each shape (the hub, Gravity, stays put so the dive lines up). */
|
| 41 |
+
const LAYOUTS: [number, number][][] = [
|
| 42 |
+
[[74, 30], [20, 24], [26, 82], [78, 76]], // orbit: around the hub
|
| 43 |
+
[[55, 56], [42, 74], [16, 62], [18, 30]], // spiral: winding out
|
| 44 |
+
[[56, 36], [72, 48], [66, 68], [84, 78]], // figure: one long chain of stars
|
| 45 |
+
[[72, 28], [84, 36], [20, 76], [30, 86]], // cluster: two tight groups
|
| 46 |
+
];
|
| 47 |
+
|
| 48 |
+
/** Cycles the shape on a timer while the visual is on screen. Clicking a pill jumps to that shape and pauses
|
| 49 |
+
* the cycle for a while. With reduced motion the shape only changes when the reader clicks. */
|
| 50 |
+
function useShapeCycle(ref: RefObject<HTMLElement>) {
|
| 51 |
+
const [view, setView] = useState({ shape: 0, prev: 0, p: 1, t: 0 });
|
| 52 |
+
const go = useRef<(i: number) => void>(() => undefined);
|
| 53 |
+
useEffect(() => {
|
| 54 |
+
const node = ref.current;
|
| 55 |
+
if (!node) return;
|
| 56 |
+
const still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
| 57 |
+
let raf = 0;
|
| 58 |
+
let last = 0;
|
| 59 |
+
let shape = 0;
|
| 60 |
+
let prev = 0;
|
| 61 |
+
let t0 = -MORPH_MS;
|
| 62 |
+
let nextAuto = 0;
|
| 63 |
+
const show = (now: number) => setView({ shape, prev, p: Math.min(1, (now - t0) / MORPH_MS), t: now / 1000 });
|
| 64 |
+
const jump = (i: number, now: number) => {
|
| 65 |
+
if (i === shape) return;
|
| 66 |
+
prev = shape;
|
| 67 |
+
shape = i;
|
| 68 |
+
t0 = now;
|
| 69 |
+
};
|
| 70 |
+
go.current = (i) => {
|
| 71 |
+
const now = performance.now();
|
| 72 |
+
jump(i, now);
|
| 73 |
+
nextAuto = now + 9000;
|
| 74 |
+
show(now);
|
| 75 |
+
if (still) setTimeout(() => show(performance.now() + MORPH_MS), MORPH_MS);
|
| 76 |
+
};
|
| 77 |
+
const tick = (now: number) => {
|
| 78 |
+
if (now >= nextAuto) {
|
| 79 |
+
jump((shape + 1) % SHAPES.length, now);
|
| 80 |
+
nextAuto = now + SHAPE_MS;
|
| 81 |
+
}
|
| 82 |
+
if (now - last > 33) {
|
| 83 |
+
last = now;
|
| 84 |
+
show(now);
|
| 85 |
+
}
|
| 86 |
+
raf = requestAnimationFrame(tick);
|
| 87 |
+
};
|
| 88 |
+
const io = new IntersectionObserver(([entry]) => {
|
| 89 |
+
cancelAnimationFrame(raf);
|
| 90 |
+
if (entry.isIntersecting && !still) {
|
| 91 |
+
nextAuto = performance.now() + SHAPE_MS;
|
| 92 |
+
raf = requestAnimationFrame(tick);
|
| 93 |
+
}
|
| 94 |
+
});
|
| 95 |
+
io.observe(node);
|
| 96 |
+
return () => {
|
| 97 |
+
io.disconnect();
|
| 98 |
+
cancelAnimationFrame(raf);
|
| 99 |
+
};
|
| 100 |
+
}, [ref]);
|
| 101 |
+
return { view, go };
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
function MapVisual() {
|
| 105 |
+
const ref = useRef<HTMLDivElement>(null);
|
| 106 |
+
const { view, go } = useShapeCycle(ref);
|
| 107 |
+
const e = view.p < 0.5 ? 2 * view.p * view.p : 1 - (-2 * view.p + 2) ** 2 / 2;
|
| 108 |
+
// Three levels of depth: topics, the sub-topics inside Gravity, and the posts behind each sub-topic.
|
| 109 |
+
const hub = { x: 38, y: 52, r: 9, label: "Gravity" };
|
| 110 |
+
const names = ["Quantum", "Entropy", "Optics", "Relativity"];
|
| 111 |
+
const outer = names.map((label, i) => {
|
| 112 |
+
const [fx, fy] = LAYOUTS[view.prev][i];
|
| 113 |
+
const [tx, ty] = LAYOUTS[view.shape][i];
|
| 114 |
+
return { label, r: [7, 6, 6, 7][i], x: fx + (tx - fx) * e, y: fy + (ty - fy) * e };
|
| 115 |
+
});
|
| 116 |
+
const topics = [hub, ...outer];
|
| 117 |
+
const links = [
|
| 118 |
[0, 1],
|
| 119 |
[0, 2],
|
| 120 |
+
[0, 3],
|
| 121 |
[0, 4],
|
| 122 |
+
[1, 4],
|
| 123 |
+
[2, 3],
|
| 124 |
+
];
|
| 125 |
+
const subs = [
|
| 126 |
+
{ x: 52, y: 40, label: "Spacetime", posts: [[59, 35], [58, 45], [47, 35]] },
|
| 127 |
+
{ x: 24, y: 62, label: "Black holes", posts: [[17, 66], [22, 70], [17, 57]] },
|
| 128 |
+
{ x: 50, y: 67, label: "Orbits", posts: [[58, 70], [46, 73], [56, 62]] },
|
| 129 |
];
|
| 130 |
return (
|
| 131 |
+
<div className="fx fx-map" ref={ref}>
|
| 132 |
+
<div className="fx-pills mono" role="group" aria-label="Shape of the map">
|
| 133 |
+
{SHAPES.map((name, i) => (
|
| 134 |
+
<button key={name} type="button" className={i === view.shape ? "on" : ""} onClick={() => go.current(i)}>
|
| 135 |
+
{name}
|
| 136 |
+
</button>
|
| 137 |
))}
|
| 138 |
+
</div>
|
| 139 |
+
<div className="fx-crumb mono">
|
| 140 |
+
<span>Archive</span>
|
| 141 |
+
<span className="fx-crumb-deep"> › Gravity › Spacetime</span>
|
| 142 |
+
</div>
|
| 143 |
+
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
|
| 144 |
+
<g className="fx-cam">
|
| 145 |
+
<g className="fx-dim">
|
| 146 |
+
{links.map(([a, b], i) => (
|
| 147 |
+
<line key={i} className="fx-edge" style={{ ["--d" as string]: `${i * 0.15}s` }} x1={topics[a].x} y1={topics[a].y} x2={topics[b].x} y2={topics[b].y} />
|
| 148 |
+
))}
|
| 149 |
+
{links.map(([a, b], i) => {
|
| 150 |
+
const u = (view.t * 0.3 + i * 0.37) % 1; // a pulse of light travelling along the link
|
| 151 |
+
return <circle key={`p${i}`} className="fx-pulse" r="1" fill="#fff" fillOpacity={Math.sin(u * Math.PI)} cx={topics[b].x + (topics[a].x - topics[b].x) * u} cy={topics[b].y + (topics[a].y - topics[b].y) * u} />;
|
| 152 |
+
})}
|
| 153 |
+
{outer.map((n, i) => (
|
| 154 |
+
<g key={n.label}>
|
| 155 |
+
{[0, 1, 2].map((k) => {
|
| 156 |
+
const angle = i * 1.7 + k * 2.1;
|
| 157 |
+
return <circle key={k} className="fx-sat" cx={n.x + Math.cos(angle) * (n.r + 6)} cy={n.y + Math.sin(angle) * (n.r + 6)} r="1.1" />;
|
| 158 |
+
})}
|
| 159 |
+
<g className="fx-node" style={{ ["--d" as string]: `${0.5 + i * 0.12}s` }}>
|
| 160 |
+
<circle cx={n.x} cy={n.y} r={n.r * 0.9} className="fx-halo" style={{ ["--t" as string]: `${(i + 1) * 0.7}s` }} />
|
| 161 |
+
<circle cx={n.x} cy={n.y} r={n.r * 0.3} fill="#fff" />
|
| 162 |
+
<text x={n.x} y={n.y + n.r * 0.9 + 5} textAnchor="middle">
|
| 163 |
+
{n.label}
|
| 164 |
+
</text>
|
| 165 |
+
</g>
|
| 166 |
+
</g>
|
| 167 |
+
))}
|
| 168 |
+
</g>
|
| 169 |
+
<g className="fx-deep">
|
| 170 |
+
<ellipse className="fx-ring" cx={hub.x} cy={hub.y} rx="15" ry="12.5" />
|
| 171 |
+
{subs.map((sub) => (
|
| 172 |
+
<g key={sub.label}>
|
| 173 |
+
<line className="fx-deep-edge" x1={hub.x} y1={hub.y} x2={sub.x} y2={sub.y} />
|
| 174 |
+
{sub.posts.map(([px, py]) => (
|
| 175 |
+
<g key={`${px}-${py}`}>
|
| 176 |
+
<line className="fx-deep-edge" x1={sub.x} y1={sub.y} x2={px} y2={py} />
|
| 177 |
+
<circle className="fx-post" cx={px} cy={py} r="0.9" />
|
| 178 |
+
</g>
|
| 179 |
+
))}
|
| 180 |
+
<circle className="fx-halo" cx={sub.x} cy={sub.y} r="3.4" />
|
| 181 |
+
<circle cx={sub.x} cy={sub.y} r="1.5" fill="#fff" />
|
| 182 |
+
<text className="fx-sub" x={sub.x} y={sub.y + 5.4} textAnchor="middle">
|
| 183 |
+
{sub.label}
|
| 184 |
+
</text>
|
| 185 |
+
</g>
|
| 186 |
+
))}
|
| 187 |
+
</g>
|
| 188 |
+
<g className="fx-node" style={{ ["--d" as string]: "0.3s" }}>
|
| 189 |
+
<circle cx={hub.x} cy={hub.y} r={hub.r * 0.9} className="fx-halo" />
|
| 190 |
+
<circle cx={hub.x} cy={hub.y} r={hub.r * 0.3} fill="#fff" />
|
| 191 |
+
<text x={hub.x} y={hub.y - hub.r * 0.9 - 2} textAnchor="middle">
|
| 192 |
+
{hub.label}
|
| 193 |
</text>
|
| 194 |
</g>
|
| 195 |
+
</g>
|
| 196 |
+
<g className="fx-panel">
|
| 197 |
+
<rect x="68" y="12" width="30" height="34" rx="2" />
|
| 198 |
+
<text className="fx-panel-h" x="71" y="19">
|
| 199 |
+
Gravity
|
| 200 |
+
</text>
|
| 201 |
+
<text className="fx-panel-k" x="71" y="26">
|
| 202 |
+
FROM YOUR POSTS
|
| 203 |
+
</text>
|
| 204 |
+
{["Why Things Fall", "Curved Space", "Notes on Orbits"].map((title, i) => (
|
| 205 |
+
<text key={title} className="fx-panel-t" x="71" y={32 + i * 5}>
|
| 206 |
+
{title}
|
| 207 |
+
</text>
|
| 208 |
+
))}
|
| 209 |
+
</g>
|
| 210 |
</svg>
|
| 211 |
</div>
|
| 212 |
);
|
|
|
|
| 233 |
}
|
| 234 |
|
| 235 |
function VideoVisual() {
|
| 236 |
+
const scenes = ["Price is a signal", "The numbers", "Why it worked"];
|
| 237 |
return (
|
| 238 |
<div className="fx fx-video">
|
| 239 |
+
<div className="fx-scenes">
|
| 240 |
+
{scenes.map((title, i) => (
|
| 241 |
+
<span key={title} className={i === 1 ? "on" : ""}>
|
| 242 |
+
<i className="mono">{i + 1}</i>
|
| 243 |
+
{title}
|
| 244 |
+
</span>
|
| 245 |
+
))}
|
| 246 |
</div>
|
| 247 |
<div className="fx-frame v169">
|
| 248 |
+
<span className="mono">Live preview</span>
|
| 249 |
+
<b>Paid readers, doubled</b>
|
| 250 |
+
</div>
|
| 251 |
+
<div className="fx-render mono">
|
| 252 |
+
<i /> Rendering MP4
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
);
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
function MemoryVisual() {
|
| 259 |
+
const notes = [
|
| 260 |
+
["audience", "I write for indie founders"],
|
| 261 |
+
["tone", "Short sentences, no jargon"],
|
| 262 |
+
["avoid", "Never name competitors"],
|
| 263 |
+
];
|
| 264 |
+
return (
|
| 265 |
+
<div className="fx fx-memory">
|
| 266 |
+
{notes.map(([key, value]) => (
|
| 267 |
+
<div key={key} className="fx-profile">
|
| 268 |
+
<span className="mono">{key}</span>
|
| 269 |
+
<p>{value}</p>
|
| 270 |
+
</div>
|
| 271 |
+
))}
|
| 272 |
+
<div className="fx-clone mono">
|
| 273 |
+
<span className="fx-rec" /> Saved from your last chat
|
| 274 |
</div>
|
| 275 |
+
</div>
|
| 276 |
+
);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
function HelpVisual() {
|
| 280 |
+
return (
|
| 281 |
+
<div className="fx fx-help">
|
| 282 |
+
<div className="fx-bubble">How do I make a video from one post?</div>
|
| 283 |
+
<p className="fx-answer">Open Create, choose Video, then pick the post, a template and a voice. You can edit every scene before you render.</p>
|
| 284 |
+
<div className="fx-source">
|
| 285 |
+
<span className="mono">Still stuck?</span>
|
| 286 |
+
<span className="fx-line">Send it to the team. We will email you back.</span>
|
| 287 |
</div>
|
| 288 |
</div>
|
| 289 |
);
|
|
|
|
| 360 |
eyebrow: "Research notebook",
|
| 361 |
title: "Ask your archive anything. It answers with receipts.",
|
| 362 |
body: "Notestack reads your posts the way a careful researcher would: it searches, opens the right post, and cites the exact lines. If you never wrote it, it tells you so instead of guessing.",
|
| 363 |
+
points: ["Every sentence links to the lines it came from", "Chats are saved per notebook", "Thumbs up or down on any answer"],
|
| 364 |
visual: <ChatVisual />,
|
| 365 |
},
|
| 366 |
+
{
|
| 367 |
+
id: "memory",
|
| 368 |
+
eyebrow: "Memory",
|
| 369 |
+
title: "It remembers who you write for.",
|
| 370 |
+
body: "Notestack keeps short notes about your audience, tone and preferences, learned as you chat, and uses them in every answer and draft. Read, edit or delete any note in Settings.",
|
| 371 |
+
points: ["Notes saved from your conversations", "Edit or delete any note, anytime", "Shared across your notebooks"],
|
| 372 |
+
visual: <MemoryVisual />,
|
| 373 |
+
},
|
| 374 |
{
|
| 375 |
id: "map",
|
| 376 |
+
eyebrow: "Mind Constellation",
|
| 377 |
+
title: "Fly through the galaxy of your ideas.",
|
| 378 |
+
body: "Open a notebook as a constellation: topics orbit the ideas they share, and every star lists the posts it comes from. Switch the shape, dive in and see what you keep coming back to.",
|
| 379 |
+
points: ["Orbit, spiral, figure or cluster layouts", "Scroll to dive in, click a star to fly to it", "Every star lists the posts behind it"],
|
| 380 |
visual: <MapVisual />,
|
| 381 |
},
|
| 382 |
{
|
| 383 |
id: "audio",
|
| 384 |
eyebrow: "Audio overview",
|
| 385 |
title: "Two hosts talk through your ideas.",
|
| 386 |
+
body: "Turn a post or a whole notebook into a natural conversation, voiced by ElevenLabs. Pick two hosts from a library of voices, or use a voice you made yourself.",
|
| 387 |
+
points: ["Deep dive, brief or debate formats", "Every line grounded in your posts", "Square audiograms from any overview"],
|
| 388 |
visual: <AudioVisual />,
|
| 389 |
},
|
| 390 |
{
|
| 391 |
id: "video",
|
| 392 |
eyebrow: "Video studio",
|
| 393 |
+
title: "Narrated videos from one post, edited scene by scene.",
|
| 394 |
+
body: "Pick a post or paste a link, choose a template, style and voice, and Notestack writes the script, narrates it and builds the scenes. Edit any scene and watch the live preview update, then render an MP4.",
|
| 395 |
+
points: ["Landscape for YouTube, vertical for Reels and TikTok", "Templates, stock footage, captions and 16 languages", "Square audiograms from any audio overview"],
|
| 396 |
visual: <VideoVisual />,
|
| 397 |
},
|
| 398 |
+
{
|
| 399 |
+
id: "voice",
|
| 400 |
+
eyebrow: "Voices",
|
| 401 |
+
title: "Sounds like you, on the page and out loud.",
|
| 402 |
+
body: "Notestack learns your writing voice from your own posts. For narration, describe a voice, pick its options, or record a couple of minutes to clone your own, then reuse it in every audio overview and video.",
|
| 403 |
+
points: ["Describe it, pick options or clone yours", "Fine tune stability, similarity and style", "Consented cloning you can revoke anytime"],
|
| 404 |
+
visual: <VoiceVisual />,
|
| 405 |
+
},
|
| 406 |
{
|
| 407 |
id: "kit",
|
| 408 |
eyebrow: "Launch Kit",
|
|
|
|
| 412 |
visual: <KitVisual />,
|
| 413 |
},
|
| 414 |
{
|
| 415 |
+
id: "help",
|
| 416 |
+
eyebrow: "Help",
|
| 417 |
+
title: "Stuck? Ask Notestack.",
|
| 418 |
+
body: "A help assistant in the app answers how-to questions about every screen. If it can't, send the question to the team and we will email you back.",
|
| 419 |
+
points: ["Answers about any feature, in plain words", "Hand off to a person in one click", "Always one click away"],
|
| 420 |
+
visual: <HelpVisual />,
|
| 421 |
},
|
| 422 |
{
|
| 423 |
id: "pad",
|
frontend/src/pages/Landing.tsx
CHANGED
|
@@ -9,9 +9,11 @@ import { blogPosts } from "../content/blogPosts";
|
|
| 9 |
import { useAuth } from "../hooks/useAuth";
|
| 10 |
import {
|
| 11 |
ArrowRightIcon,
|
|
|
|
| 12 |
HelmetIcon,
|
| 13 |
LaunchWindowIcon,
|
| 14 |
LaunchpadIcon,
|
|
|
|
| 15 |
RocketIcon,
|
| 16 |
SignalIcon,
|
| 17 |
TelescopeIcon,
|
|
@@ -19,7 +21,7 @@ import {
|
|
| 19 |
|
| 20 |
export const PENDING_SOURCE_KEY = "ns_pending_source";
|
| 21 |
// Bump after re-rendering public/demo.mp4 so browsers and the CDN fetch the new file.
|
| 22 |
-
const DEMO_VERSION =
|
| 23 |
|
| 24 |
const FEATURES = [
|
| 25 |
{
|
|
@@ -28,6 +30,12 @@ const FEATURES = [
|
|
| 28 |
title: "Ask your archive anything",
|
| 29 |
body: "Every answer cites the exact passage it came from. If you never wrote it, Notestack says so.",
|
| 30 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
{
|
| 32 |
icon: SignalIcon,
|
| 33 |
label: "Audio overview",
|
|
@@ -37,8 +45,8 @@ const FEATURES = [
|
|
| 37 |
{
|
| 38 |
icon: LaunchWindowIcon,
|
| 39 |
label: "Video",
|
| 40 |
-
title: "
|
| 41 |
-
body: "
|
| 42 |
},
|
| 43 |
{
|
| 44 |
icon: RocketIcon,
|
|
@@ -48,9 +56,15 @@ const FEATURES = [
|
|
| 48 |
},
|
| 49 |
{
|
| 50 |
icon: HelmetIcon,
|
| 51 |
-
label: "
|
| 52 |
title: "Sounds like you",
|
| 53 |
-
body: "Notestack learns your tone from your
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
},
|
| 55 |
{
|
| 56 |
icon: LaunchpadIcon,
|
|
|
|
| 9 |
import { useAuth } from "../hooks/useAuth";
|
| 10 |
import {
|
| 11 |
ArrowRightIcon,
|
| 12 |
+
ConstellationIcon,
|
| 13 |
HelmetIcon,
|
| 14 |
LaunchWindowIcon,
|
| 15 |
LaunchpadIcon,
|
| 16 |
+
OrbitIcon,
|
| 17 |
RocketIcon,
|
| 18 |
SignalIcon,
|
| 19 |
TelescopeIcon,
|
|
|
|
| 21 |
|
| 22 |
export const PENDING_SOURCE_KEY = "ns_pending_source";
|
| 23 |
// Bump after re-rendering public/demo.mp4 so browsers and the CDN fetch the new file.
|
| 24 |
+
const DEMO_VERSION = 5;
|
| 25 |
|
| 26 |
const FEATURES = [
|
| 27 |
{
|
|
|
|
| 30 |
title: "Ask your archive anything",
|
| 31 |
body: "Every answer cites the exact passage it came from. If you never wrote it, Notestack says so.",
|
| 32 |
},
|
| 33 |
+
{
|
| 34 |
+
icon: ConstellationIcon,
|
| 35 |
+
label: "Mind Constellation",
|
| 36 |
+
title: "Fly through your ideas",
|
| 37 |
+
body: "Open a notebook as a galaxy of topics. Switch the shape, dive in and see the posts behind every star.",
|
| 38 |
+
},
|
| 39 |
{
|
| 40 |
icon: SignalIcon,
|
| 41 |
label: "Audio overview",
|
|
|
|
| 45 |
{
|
| 46 |
icon: LaunchWindowIcon,
|
| 47 |
label: "Video",
|
| 48 |
+
title: "Narrated videos you can edit",
|
| 49 |
+
body: "Pick a post and a template, then edit every scene with a live preview. Landscape for YouTube, vertical for Reels.",
|
| 50 |
},
|
| 51 |
{
|
| 52 |
icon: RocketIcon,
|
|
|
|
| 56 |
},
|
| 57 |
{
|
| 58 |
icon: HelmetIcon,
|
| 59 |
+
label: "Voices",
|
| 60 |
title: "Sounds like you",
|
| 61 |
+
body: "Notestack learns your tone from your posts, and you can describe, pick or clone a voice for narration.",
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
icon: OrbitIcon,
|
| 65 |
+
label: "Memory",
|
| 66 |
+
title: "Remembers who you write for",
|
| 67 |
+
body: "Short notes about your audience and style, learned from your chats and yours to edit anytime.",
|
| 68 |
},
|
| 69 |
{
|
| 70 |
icon: LaunchpadIcon,
|
frontend/src/styles/showcase.css
CHANGED
|
@@ -185,8 +185,8 @@
|
|
| 185 |
stroke: var(--white);
|
| 186 |
stroke-opacity: 0.3;
|
| 187 |
stroke-width: 0.4;
|
| 188 |
-
stroke-dasharray:
|
| 189 |
-
stroke-dashoffset:
|
| 190 |
transition-delay: var(--d);
|
| 191 |
}
|
| 192 |
|
|
@@ -216,6 +216,141 @@
|
|
| 216 |
|
| 217 |
.showcase-row.arrived .fx-halo {
|
| 218 |
animation: pulse-halo 4s ease-in-out infinite;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
}
|
| 220 |
|
| 221 |
@keyframes pulse-halo {
|
|
@@ -290,10 +425,31 @@
|
|
| 290 |
|
| 291 |
/* Video */
|
| 292 |
.fx-video {
|
| 293 |
-
grid-template-columns: 0.
|
| 294 |
align-items: center;
|
| 295 |
}
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
.fx-frame {
|
| 298 |
display: grid;
|
| 299 |
gap: 6px;
|
|
@@ -311,22 +467,51 @@
|
|
| 311 |
line-height: 1.2;
|
| 312 |
}
|
| 313 |
|
| 314 |
-
.v916 {
|
| 315 |
-
aspect-ratio: 9 / 16;
|
| 316 |
-
}
|
| 317 |
-
|
| 318 |
.v169 {
|
| 319 |
aspect-ratio: 16 / 9;
|
| 320 |
}
|
| 321 |
|
| 322 |
-
.
|
| 323 |
-
|
|
|
|
|
|
|
| 324 |
}
|
| 325 |
|
| 326 |
-
.fx-
|
| 327 |
display: block;
|
| 328 |
-
height:
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
}
|
| 331 |
|
| 332 |
/* Launch Kit */
|
|
@@ -469,9 +654,19 @@
|
|
| 469 |
|
| 470 |
.fx-wave span,
|
| 471 |
.fx-halo,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
.fx-rec {
|
| 473 |
animation: none !important;
|
| 474 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
}
|
| 476 |
|
| 477 |
/* Demo video sound controls */
|
|
|
|
| 185 |
stroke: var(--white);
|
| 186 |
stroke-opacity: 0.3;
|
| 187 |
stroke-width: 0.4;
|
| 188 |
+
stroke-dasharray: 150;
|
| 189 |
+
stroke-dashoffset: 150;
|
| 190 |
transition-delay: var(--d);
|
| 191 |
}
|
| 192 |
|
|
|
|
| 216 |
|
| 217 |
.showcase-row.arrived .fx-halo {
|
| 218 |
animation: pulse-halo 4s ease-in-out infinite;
|
| 219 |
+
animation-delay: var(--t, 0s);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* Depth: the camera dives into Gravity, the other topics fall back, its sub-topics and posts light up,
|
| 223 |
+
then it pulls back out. One 14 s loop, started once the row has arrived. */
|
| 224 |
+
.fx-cam {
|
| 225 |
+
transform-origin: 38px 52px;
|
| 226 |
+
transform-box: view-box;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.fx-deep,
|
| 230 |
+
.fx-panel,
|
| 231 |
+
.fx-crumb-deep {
|
| 232 |
+
opacity: 0;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.showcase-row.arrived .fx-cam {
|
| 236 |
+
animation: fx-dive 14s ease-in-out 2.5s infinite;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.showcase-row.arrived .fx-dim {
|
| 240 |
+
animation: fx-dim 14s ease-in-out 2.5s infinite;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.showcase-row.arrived .fx-deep,
|
| 244 |
+
.showcase-row.arrived .fx-panel,
|
| 245 |
+
.showcase-row.arrived .fx-crumb-deep {
|
| 246 |
+
animation: fx-in 14s ease-in-out 2.5s infinite;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
@keyframes fx-dive {
|
| 250 |
+
0%,
|
| 251 |
+
26%,
|
| 252 |
+
86%,
|
| 253 |
+
100% {
|
| 254 |
+
transform: none;
|
| 255 |
+
}
|
| 256 |
+
40%,
|
| 257 |
+
72% {
|
| 258 |
+
transform: translate(-4px, -2px) scale(2.1);
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
@keyframes fx-dim {
|
| 263 |
+
0%,
|
| 264 |
+
26%,
|
| 265 |
+
86%,
|
| 266 |
+
100% {
|
| 267 |
+
opacity: 1;
|
| 268 |
+
}
|
| 269 |
+
40%,
|
| 270 |
+
72% {
|
| 271 |
+
opacity: 0.1;
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
@keyframes fx-in {
|
| 276 |
+
0%,
|
| 277 |
+
32%,
|
| 278 |
+
80%,
|
| 279 |
+
100% {
|
| 280 |
+
opacity: 0;
|
| 281 |
+
}
|
| 282 |
+
42%,
|
| 283 |
+
70% {
|
| 284 |
+
opacity: 1;
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.fx-sat {
|
| 289 |
+
fill: var(--white);
|
| 290 |
+
opacity: 0.5;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.fx-ring {
|
| 294 |
+
fill: none;
|
| 295 |
+
stroke: var(--blue);
|
| 296 |
+
stroke-opacity: 0.5;
|
| 297 |
+
stroke-width: 0.25;
|
| 298 |
+
stroke-dasharray: 1.2 1.6;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.fx-deep-edge {
|
| 302 |
+
stroke: var(--white);
|
| 303 |
+
stroke-opacity: 0.35;
|
| 304 |
+
stroke-width: 0.2;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.fx-post {
|
| 308 |
+
fill: var(--white);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.fx-sub {
|
| 312 |
+
fill: var(--white);
|
| 313 |
+
font: 500 2.4px var(--font-body);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.fx-panel rect {
|
| 317 |
+
fill: rgba(0, 0, 0, 0.88);
|
| 318 |
+
stroke: var(--blue);
|
| 319 |
+
stroke-opacity: 0.5;
|
| 320 |
+
stroke-width: 0.25;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.fx-panel text {
|
| 324 |
+
fill: var(--white);
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.fx-panel-h {
|
| 328 |
+
font: 600 3.8px var(--font-display);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.fx-panel-k {
|
| 332 |
+
fill: var(--blue) !important;
|
| 333 |
+
font: 500 1.9px var(--font-mono);
|
| 334 |
+
letter-spacing: 0.2px;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.fx-panel-t {
|
| 338 |
+
font: 500 2.7px var(--font-body);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.fx-crumb {
|
| 342 |
+
display: flex;
|
| 343 |
+
min-height: 1.2em;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.fx-pulse {
|
| 347 |
+
opacity: 0;
|
| 348 |
+
filter: drop-shadow(0 0 1.5px var(--blue));
|
| 349 |
+
transition: opacity 1s ease 1.5s;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.showcase-row.arrived .fx-pulse {
|
| 353 |
+
opacity: 0.9;
|
| 354 |
}
|
| 355 |
|
| 356 |
@keyframes pulse-halo {
|
|
|
|
| 425 |
|
| 426 |
/* Video */
|
| 427 |
.fx-video {
|
| 428 |
+
grid-template-columns: 0.8fr 1.3fr;
|
| 429 |
align-items: center;
|
| 430 |
}
|
| 431 |
|
| 432 |
+
.fx-scenes {
|
| 433 |
+
display: grid;
|
| 434 |
+
gap: 8px;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.fx-scenes span {
|
| 438 |
+
display: flex;
|
| 439 |
+
align-items: center;
|
| 440 |
+
gap: 10px;
|
| 441 |
+
padding: 9px 12px;
|
| 442 |
+
border-radius: 10px;
|
| 443 |
+
border: 1px solid var(--line-strong);
|
| 444 |
+
font-size: 0.8rem;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
.fx-scenes span.on {
|
| 448 |
+
border-color: var(--blue);
|
| 449 |
+
background: var(--blue-10);
|
| 450 |
+
box-shadow: 0 0 18px var(--blue-20);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
.fx-frame {
|
| 454 |
display: grid;
|
| 455 |
gap: 6px;
|
|
|
|
| 467 |
line-height: 1.2;
|
| 468 |
}
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
.v169 {
|
| 471 |
aspect-ratio: 16 / 9;
|
| 472 |
}
|
| 473 |
|
| 474 |
+
.fx-render {
|
| 475 |
+
grid-column: 1 / -1;
|
| 476 |
+
display: grid;
|
| 477 |
+
gap: 6px;
|
| 478 |
}
|
| 479 |
|
| 480 |
+
.fx-render i {
|
| 481 |
display: block;
|
| 482 |
+
height: 4px;
|
| 483 |
+
border-radius: 99px;
|
| 484 |
+
background: linear-gradient(90deg, var(--blue) 70%, var(--surface-2) 70%);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
/* Mind Constellation shapes */
|
| 488 |
+
.fx-pills {
|
| 489 |
+
display: flex;
|
| 490 |
+
gap: 6px;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
.fx-pills button {
|
| 494 |
+
padding: 3px 10px;
|
| 495 |
+
border-radius: 99px;
|
| 496 |
+
border: 1px solid var(--line-strong);
|
| 497 |
+
background: transparent;
|
| 498 |
+
color: var(--text-2);
|
| 499 |
+
font: inherit;
|
| 500 |
+
cursor: pointer;
|
| 501 |
+
transition:
|
| 502 |
+
border-color 0.3s ease,
|
| 503 |
+
background 0.3s ease,
|
| 504 |
+
color 0.3s ease;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.fx-pills button:hover {
|
| 508 |
+
color: var(--white);
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
.fx-pills button.on {
|
| 512 |
+
border-color: var(--blue);
|
| 513 |
+
background: var(--blue-10);
|
| 514 |
+
color: var(--white);
|
| 515 |
}
|
| 516 |
|
| 517 |
/* Launch Kit */
|
|
|
|
| 654 |
|
| 655 |
.fx-wave span,
|
| 656 |
.fx-halo,
|
| 657 |
+
.fx-cam,
|
| 658 |
+
.fx-dim,
|
| 659 |
+
.fx-deep,
|
| 660 |
+
.fx-panel,
|
| 661 |
+
.fx-crumb-deep,
|
| 662 |
.fx-rec {
|
| 663 |
animation: none !important;
|
| 664 |
}
|
| 665 |
+
|
| 666 |
+
.fx-pulse {
|
| 667 |
+
display: none;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
}
|
| 671 |
|
| 672 |
/* Demo video sound controls */
|
renderer/src/compositions/LandingDemo.tsx
CHANGED
|
@@ -296,8 +296,8 @@ function Warp() {
|
|
| 296 |
const SOURCES = ["Substack", "Ghost", "WordPress", "Medium", "RSS", "Markdown"];
|
| 297 |
|
| 298 |
const POSTS = [
|
| 299 |
-
"
|
| 300 |
-
"
|
| 301 |
];
|
| 302 |
const POST_DATES = ["Mar 12", "Mar 04", "Feb 21", "Feb 09", "Jan 30", "Jan 18", "Jan 03", "Dec 20"];
|
| 303 |
|
|
@@ -361,12 +361,12 @@ function Sync() {
|
|
| 361 |
function Ask() {
|
| 362 |
const frame = useCurrentFrame();
|
| 363 |
const { fps } = useVideoConfig();
|
| 364 |
-
const q = typed("What have I written about
|
| 365 |
const answer = spring({ frame: frame - 66, fps, config: { damping: 18 } });
|
| 366 |
const cite = spring({ frame: frame - 92, fps, config: { damping: 16 } });
|
| 367 |
const open = spring({ frame: frame - 114, fps, config: { damping: 18 } });
|
| 368 |
-
const steps = ["Searching for
|
| 369 |
-
const context = ["
|
| 370 |
return (
|
| 371 |
<AbsoluteFill>
|
| 372 |
<WarpField speed={0.7} count={300} />
|
|
@@ -384,19 +384,19 @@ function Ask() {
|
|
| 384 |
))}
|
| 385 |
</div>
|
| 386 |
<p style={{ margin: 0, fontFamily: theme.body, fontSize: 32, lineHeight: 1.5, color: theme.white, opacity: answer, transform: `translateY(${(1 - answer) * 16}px)` }}>
|
| 387 |
-
You
|
| 388 |
</p>
|
| 389 |
</div>
|
| 390 |
{[
|
| 391 |
-
{ n: 1, label: "
|
| 392 |
-
{ n: 2, label: "
|
| 393 |
].map((c, i) => (
|
| 394 |
<Chip key={c.n} on={cite > 0.5 && (i === 0 ? frame >= 112 : false)} press={i === 0 ? pulse(frame, 112) : 0} style={{ position: "absolute", left: 80 + i * 450, top: 460, width: 420, height: 60, fontFamily: theme.mono, fontSize: 21, opacity: cite, gap: 10 }}>
|
| 395 |
<span style={{ color: BLUE }}>[{c.n}]</span> {c.label}
|
| 396 |
</Chip>
|
| 397 |
))}
|
| 398 |
<div style={{ ...panel, position: "absolute", left: 1010, top: 36, width: 560, padding: 30, opacity: open, transform: `translateX(${(1 - open) * 60}px)` }}>
|
| 399 |
-
<p style={{ margin: 0, fontFamily: theme.mono, fontSize: 20, color: BLUE, letterSpacing: 2 }}>[1]
|
| 400 |
{context.map((l, i) => {
|
| 401 |
const hit = i >= 1 && i <= 3;
|
| 402 |
return (
|
|
@@ -426,11 +426,11 @@ function Marker({ n, glow }: { n: number; glow: number }) {
|
|
| 426 |
}
|
| 427 |
|
| 428 |
const TOPICS = [
|
| 429 |
-
"
|
| 430 |
-
"
|
| 431 |
];
|
| 432 |
const MAP_SHAPES = ["Orbit", "Spiral", "Figure", "Cluster"];
|
| 433 |
-
type Shape = "orbit" | "spiral" | "cluster";
|
| 434 |
|
| 435 |
/** Where topic `i` sits in each shape of the Mind Constellation (window coordinates). */
|
| 436 |
function place(shape: Shape, i: number): { x: number; y: number } {
|
|
@@ -445,10 +445,14 @@ function place(shape: Shape, i: number): { x: number; y: number } {
|
|
| 445 |
}
|
| 446 |
if (shape === "spiral") {
|
| 447 |
const t = i / (n - 1);
|
| 448 |
-
const a = t * Math.PI * 2.
|
| 449 |
-
const r =
|
| 450 |
return { x: cx + Math.cos(a) * r * 1.5, y: cy + Math.sin(a) * r * 0.58 };
|
| 451 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
const centers = [{ x: 330, y: 250 }, { x: 800, y: 220 }, { x: 640, y: 490 }];
|
| 453 |
const c = centers[i % 3];
|
| 454 |
const k = Math.floor(i / 3);
|
|
@@ -457,70 +461,97 @@ function place(shape: Shape, i: number): { x: number; y: number } {
|
|
| 457 |
return { x: c.x + Math.cos(a) * r * 1.3, y: c.y + Math.sin(a) * r };
|
| 458 |
}
|
| 459 |
|
|
|
|
|
|
|
|
|
|
| 460 |
function mapPos(i: number, frame: number): { x: number; y: number } {
|
| 461 |
-
|
| 462 |
-
const
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
|
|
|
|
|
|
| 467 |
}
|
| 468 |
|
| 469 |
/** 03: the Mind Constellation, switching shapes and diving into a topic. */
|
| 470 |
function MindMap() {
|
| 471 |
const frame = useCurrentFrame();
|
| 472 |
const { fps } = useVideoConfig();
|
| 473 |
-
const activeShape = frame <
|
| 474 |
const panelIn = spring({ frame: frame - 158, fps, config: { damping: 18 } });
|
| 475 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
return (
|
| 477 |
<AbsoluteFill>
|
| 478 |
<WarpField speed={0.6} count={260} />
|
| 479 |
<Caption eyebrow="03 · Map" title="See your whole archive." />
|
| 480 |
<Win>
|
| 481 |
<svg width={1640} height={668} style={{ position: "absolute", inset: 0 }}>
|
| 482 |
-
{
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
<
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
<
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
</svg>
|
| 506 |
<div style={{ position: "absolute", left: 40, top: 26, display: "flex", gap: 10 }}>
|
| 507 |
{MAP_SHAPES.map((name, i) => (
|
| 508 |
-
<Chip key={name} on={i === activeShape} press={i ===
|
| 509 |
{name}
|
| 510 |
</Chip>
|
| 511 |
))}
|
| 512 |
</div>
|
| 513 |
-
<p style={{ position: "absolute", left: 40, bottom: 20, margin: 0, fontFamily: theme.mono, fontSize: 18, color: "rgba(255,255,255,0.5)" }}>Scroll to dive in. Drag to move. Click a star to fly to it.</p>
|
| 514 |
<div style={{ ...panel, position: "absolute", right: 40, top: 26, width: 380, padding: 28, opacity: panelIn, transform: `translateX(${(1 - panelIn) * 60}px)` }}>
|
| 515 |
-
<p style={{ margin: 0, fontFamily: theme.display, fontSize: 40, color: theme.white }}>
|
| 516 |
<Label style={{ position: "static", display: "block", marginTop: 22, fontSize: 16 }}>Orbiting here</Label>
|
| 517 |
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 10 }}>
|
| 518 |
-
{["
|
| 519 |
<Chip key={t} on style={{ padding: "4px 16px", fontSize: 20 }}>{t}</Chip>
|
| 520 |
))}
|
| 521 |
</div>
|
| 522 |
<Label style={{ position: "static", display: "block", marginTop: 22, fontSize: 16 }}>From your posts</Label>
|
| 523 |
-
{["
|
| 524 |
<p key={t} style={{ margin: "10px 0 0", fontFamily: theme.body, fontSize: 24, color: theme.white }}>{t}</p>
|
| 525 |
))}
|
| 526 |
</div>
|
|
@@ -528,14 +559,16 @@ function MindMap() {
|
|
| 528 |
<Cursor
|
| 529 |
keys={[
|
| 530 |
{ f: 0, x: 1560, y: 880 },
|
| 531 |
-
{ f:
|
| 532 |
-
{ f:
|
| 533 |
-
{ f:
|
| 534 |
-
{ f:
|
| 535 |
-
{ f:
|
| 536 |
-
{ f:
|
|
|
|
|
|
|
| 537 |
]}
|
| 538 |
-
clicks={[
|
| 539 |
/>
|
| 540 |
</AbsoluteFill>
|
| 541 |
);
|
|
@@ -550,9 +583,9 @@ function Audio() {
|
|
| 550 |
const bar = clamp01(frame, 52, 84);
|
| 551 |
const playing = clamp01(frame, 84, 96);
|
| 552 |
const lines = [
|
| 553 |
-
{ who: "A", text: "So the big idea here is that
|
| 554 |
-
{ who: "B", text: "Right, and
|
| 555 |
-
{ who: "A", text: "Which she wrote about back in March, in
|
| 556 |
];
|
| 557 |
const active = Math.min(lines.length - 1, Math.floor(Math.max(0, frame - 92) / 24));
|
| 558 |
return (
|
|
@@ -602,9 +635,9 @@ const TEMPLATE_LOOKS: CSSProperties[] = [
|
|
| 602 |
{ background: "linear-gradient(135deg, #000, #0b1730)" },
|
| 603 |
{ background: `linear-gradient(135deg, ${BLUE}, #0a2a66)` },
|
| 604 |
{ background: "linear-gradient(135deg, #ffffff, #cfe0ff)" },
|
| 605 |
-
{ background: "
|
| 606 |
];
|
| 607 |
-
const TEMPLATE_NAMES = ["Night", "Signal", "Paper", "
|
| 608 |
const VOICES = ["Nora Vale", "Your voice (clone)", "+ Create a voice"];
|
| 609 |
|
| 610 |
/** 05: wizard: pick a style, a template and a voice, create. */
|
|
@@ -669,7 +702,7 @@ function VideoCreate() {
|
|
| 669 |
);
|
| 670 |
}
|
| 671 |
|
| 672 |
-
const SCENE_TITLES = ["
|
| 673 |
|
| 674 |
/** 06: the scene editor, then render the MP4. */
|
| 675 |
function VideoEdit() {
|
|
@@ -709,15 +742,15 @@ function VideoEdit() {
|
|
| 709 |
{active === 0 ? (
|
| 710 |
<div style={{ position: "absolute", left: 70, top: 160, opacity: pop, transform: `translateY(${(1 - pop) * 30}px)` }}>
|
| 711 |
<p style={{ margin: 0, fontFamily: theme.mono, fontSize: 22, letterSpacing: 5, color: BLUE }}>SCENE 1</p>
|
| 712 |
-
<p style={{ margin: "14px 0 0", fontFamily: theme.display, fontSize: 86, lineHeight: 1.05, color: theme.white, textShadow: `0 0 30px ${BLUE}` }}>
|
| 713 |
</div>
|
| 714 |
) : (
|
| 715 |
<>
|
| 716 |
-
<p style={{ position: "absolute", left: 70, top: 56, margin: 0, fontFamily: theme.display, fontSize: 52, color: theme.white, opacity: pop }}>
|
| 717 |
-
{[
|
| 718 |
-
<div key={i} style={{ position: "absolute", left: 90 + i * 250, bottom: 90, width: 170, height: 340 * h * ease(clamp01(since, 8 + i * 7, 34 + i * 7)), borderRadius: "12px 12px 0 0", background: i ===
|
| 719 |
))}
|
| 720 |
-
<p style={{ position: "absolute", left: 70, bottom: 30, margin: 0, fontFamily: theme.body, fontSize: 26, color: "rgba(255,255,255,0.85)", opacity: pop }}>
|
| 721 |
</>
|
| 722 |
)}
|
| 723 |
{render > 0 && (
|
|
@@ -750,10 +783,10 @@ function LaunchKit() {
|
|
| 750 |
const frame = useCurrentFrame();
|
| 751 |
const { fps } = useVideoConfig();
|
| 752 |
const cards = [
|
| 753 |
-
{ label: "X THREAD", body: "
|
| 754 |
-
{ label: "LINKEDIN", body: "Most
|
| 755 |
-
{ label: "SUBSTACK NOTES", body: "
|
| 756 |
-
{ label: "BLUESKY", body: "
|
| 757 |
];
|
| 758 |
const days = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"];
|
| 759 |
const slots: Record<number, string> = { 0: "X", 1: "LinkedIn", 2: "Notes", 3: "Bluesky", 4: "X", 5: "LinkedIn", 6: "Notes" };
|
|
|
|
| 296 |
const SOURCES = ["Substack", "Ghost", "WordPress", "Medium", "RSS", "Markdown"];
|
| 297 |
|
| 298 |
const POSTS = [
|
| 299 |
+
"Why Things Fall", "Curved Space", "Notes on Orbits", "What Is Entropy?",
|
| 300 |
+
"The Quantum Leap", "Light as a Wave", "Time Is Strange", "Black Hole Basics",
|
| 301 |
];
|
| 302 |
const POST_DATES = ["Mar 12", "Mar 04", "Feb 21", "Feb 09", "Jan 30", "Jan 18", "Jan 03", "Dec 20"];
|
| 303 |
|
|
|
|
| 361 |
function Ask() {
|
| 362 |
const frame = useCurrentFrame();
|
| 363 |
const { fps } = useVideoConfig();
|
| 364 |
+
const q = typed("What have I written about gravity?", frame, 8, 30);
|
| 365 |
const answer = spring({ frame: frame - 66, fps, config: { damping: 18 } });
|
| 366 |
const cite = spring({ frame: frame - 92, fps, config: { damping: 16 } });
|
| 367 |
const open = spring({ frame: frame - 114, fps, config: { damping: 18 } });
|
| 368 |
+
const steps = ["Searching for gravity|spacetime", "Reading Why Things Fall, lines 12 to 20", "Reading Curved Space, lines 4 to 9"];
|
| 369 |
+
const context = ["Newton called it a force pulling things down.", "Einstein saw something different: gravity is", "the curvature of spacetime itself.", "Mass tells space how to bend.", "Objects follow the straightest path."];
|
| 370 |
return (
|
| 371 |
<AbsoluteFill>
|
| 372 |
<WarpField speed={0.7} count={300} />
|
|
|
|
| 384 |
))}
|
| 385 |
</div>
|
| 386 |
<p style={{ margin: 0, fontFamily: theme.body, fontSize: 32, lineHeight: 1.5, color: theme.white, opacity: answer, transform: `translateY(${(1 - answer) * 16}px)` }}>
|
| 387 |
+
You wrote that <b>gravity is the curvature of spacetime</b> <Marker n={1} glow={cite} /> and that orbits are objects falling around that curve <Marker n={2} glow={cite} />.
|
| 388 |
</p>
|
| 389 |
</div>
|
| 390 |
{[
|
| 391 |
+
{ n: 1, label: "Why Things Fall · L12 to 14" },
|
| 392 |
+
{ n: 2, label: "Curved Space · L4 to 9" },
|
| 393 |
].map((c, i) => (
|
| 394 |
<Chip key={c.n} on={cite > 0.5 && (i === 0 ? frame >= 112 : false)} press={i === 0 ? pulse(frame, 112) : 0} style={{ position: "absolute", left: 80 + i * 450, top: 460, width: 420, height: 60, fontFamily: theme.mono, fontSize: 21, opacity: cite, gap: 10 }}>
|
| 395 |
<span style={{ color: BLUE }}>[{c.n}]</span> {c.label}
|
| 396 |
</Chip>
|
| 397 |
))}
|
| 398 |
<div style={{ ...panel, position: "absolute", left: 1010, top: 36, width: 560, padding: 30, opacity: open, transform: `translateX(${(1 - open) * 60}px)` }}>
|
| 399 |
+
<p style={{ margin: 0, fontFamily: theme.mono, fontSize: 20, color: BLUE, letterSpacing: 2 }}>[1] WHY THINGS FALL · L12 TO 14</p>
|
| 400 |
{context.map((l, i) => {
|
| 401 |
const hit = i >= 1 && i <= 3;
|
| 402 |
return (
|
|
|
|
| 426 |
}
|
| 427 |
|
| 428 |
const TOPICS = [
|
| 429 |
+
"Gravity", "Spacetime", "Black holes", "Orbits", "Relativity", "Quantum", "Entropy", "Optics",
|
| 430 |
+
"Waves", "Energy", "Chaos", "Cosmology", "Fields", "Symmetry", "Time",
|
| 431 |
];
|
| 432 |
const MAP_SHAPES = ["Orbit", "Spiral", "Figure", "Cluster"];
|
| 433 |
+
type Shape = "orbit" | "spiral" | "figure" | "cluster";
|
| 434 |
|
| 435 |
/** Where topic `i` sits in each shape of the Mind Constellation (window coordinates). */
|
| 436 |
function place(shape: Shape, i: number): { x: number; y: number } {
|
|
|
|
| 445 |
}
|
| 446 |
if (shape === "spiral") {
|
| 447 |
const t = i / (n - 1);
|
| 448 |
+
const a = t * Math.PI * 2.4 + 0.4;
|
| 449 |
+
const r = 110 + t * 260;
|
| 450 |
return { x: cx + Math.cos(a) * r * 1.5, y: cy + Math.sin(a) * r * 0.58 };
|
| 451 |
}
|
| 452 |
+
if (shape === "figure") {
|
| 453 |
+
// A constellation figure: one long wave of stars across the sky.
|
| 454 |
+
return { x: 140 + i * 78, y: cy + (i % 2 ? -1 : 1) * (90 + (i % 3) * 30) };
|
| 455 |
+
}
|
| 456 |
const centers = [{ x: 330, y: 250 }, { x: 800, y: 220 }, { x: 640, y: 490 }];
|
| 457 |
const c = centers[i % 3];
|
| 458 |
const k = Math.floor(i / 3);
|
|
|
|
| 461 |
return { x: c.x + Math.cos(a) * r * 1.3, y: c.y + Math.sin(a) * r };
|
| 462 |
}
|
| 463 |
|
| 464 |
+
/** Frame windows in which the layout morphs to the next shape (after each pill click). */
|
| 465 |
+
const MORPHS: [Shape, number, number][] = [["spiral", 58, 82], ["figure", 94, 118], ["cluster", 126, 150]];
|
| 466 |
+
|
| 467 |
function mapPos(i: number, frame: number): { x: number; y: number } {
|
| 468 |
+
let { x, y } = place("orbit", i);
|
| 469 |
+
for (const [shape, from, to] of MORPHS) {
|
| 470 |
+
const t = ease(clamp01(frame, from, to));
|
| 471 |
+
const next = place(shape, i);
|
| 472 |
+
x += (next.x - x) * t;
|
| 473 |
+
y += (next.y - y) * t;
|
| 474 |
+
}
|
| 475 |
+
return { x, y };
|
| 476 |
}
|
| 477 |
|
| 478 |
/** 03: the Mind Constellation, switching shapes and diving into a topic. */
|
| 479 |
function MindMap() {
|
| 480 |
const frame = useCurrentFrame();
|
| 481 |
const { fps } = useVideoConfig();
|
| 482 |
+
const activeShape = frame < 56 ? 0 : frame < 92 ? 1 : frame < 124 ? 2 : 3;
|
| 483 |
const panelIn = spring({ frame: frame - 158, fps, config: { damping: 18 } });
|
| 484 |
+
const hubWin = place("cluster", 0);
|
| 485 |
+
const hub = at(hubWin.x, hubWin.y);
|
| 486 |
+
const selected = frame >= 158;
|
| 487 |
+
const ring = clamp01(frame, 4, 40);
|
| 488 |
+
const dive = 1 + 0.14 * ease(clamp01(frame, 158, 195)); // the camera drifts in on the picked star
|
| 489 |
+
const burst = ease(clamp01(frame, 158, 182));
|
| 490 |
return (
|
| 491 |
<AbsoluteFill>
|
| 492 |
<WarpField speed={0.6} count={260} />
|
| 493 |
<Caption eyebrow="03 · Map" title="See your whole archive." />
|
| 494 |
<Win>
|
| 495 |
<svg width={1640} height={668} style={{ position: "absolute", inset: 0 }}>
|
| 496 |
+
<g transform={`translate(${hubWin.x} ${hubWin.y}) scale(${dive}) translate(${-hubWin.x} ${-hubWin.y})`}>
|
| 497 |
+
{/* Orbit rings draw in, then fade as the stars leave them. */}
|
| 498 |
+
{[170, 270, 360].map((r, i) => (
|
| 499 |
+
<ellipse key={r} cx={620} cy={350} rx={r * 1.5} ry={r * 0.58} fill="none" stroke={i % 2 ? "#ffffff" : BLUE} strokeOpacity={0.22 * ring * (1 - ease(clamp01(frame, 58, 82)))} strokeWidth={1.5} strokeDasharray="6 10" />
|
| 500 |
+
))}
|
| 501 |
+
{TOPICS.map((_, i) => {
|
| 502 |
+
if (i === 0) return null;
|
| 503 |
+
const parent = i < 4 ? 0 : i - 3;
|
| 504 |
+
const from = mapPos(i, frame);
|
| 505 |
+
const to = mapPos(parent, frame);
|
| 506 |
+
const on = clamp01(frame, 6 + i * 3, 24 + i * 3);
|
| 507 |
+
const lit = !selected || parent === 0;
|
| 508 |
+
const t = (frame * 0.018 + i * 0.37) % 1; // a pulse of light travelling toward the hub
|
| 509 |
+
return (
|
| 510 |
+
<g key={i} opacity={lit ? 1 : 0.25}>
|
| 511 |
+
<line x1={from.x} y1={from.y} x2={to.x} y2={to.y} pathLength={1} stroke={i % 2 ? BLUE : "#ffffff"} strokeOpacity={selected && parent === 0 ? 0.8 : 0.3} strokeWidth={selected && parent === 0 ? 2.5 : 1.5} strokeDasharray={1} strokeDashoffset={1 - on} />
|
| 512 |
+
{on >= 1 && <circle cx={from.x + (to.x - from.x) * t} cy={from.y + (to.y - from.y) * t} r={3.5} fill="#ffffff" opacity={Math.sin(t * Math.PI)} style={{ filter: `drop-shadow(0 0 6px ${BLUE})` }} />}
|
| 513 |
+
</g>
|
| 514 |
+
);
|
| 515 |
+
})}
|
| 516 |
+
{TOPICS.map((name, i) => {
|
| 517 |
+
const base = mapPos(i, frame);
|
| 518 |
+
const p = { x: base.x + Math.sin(frame * 0.03 + i * 1.9) * 5, y: base.y + Math.cos(frame * 0.025 + i * 1.3) * 5 };
|
| 519 |
+
const born = spring({ frame: frame - 2 - i * 3, fps, config: { damping: 12 } });
|
| 520 |
+
const r = (i === 0 ? 17 : 7 + ((i * 7) % 5)) * born;
|
| 521 |
+
const twinkle = 1 + 0.25 * Math.sin(frame * 0.14 + i * 1.7);
|
| 522 |
+
const near = i === 0 || (i >= 1 && i <= 3);
|
| 523 |
+
const dim = selected && !near ? 0.3 : 1;
|
| 524 |
+
return (
|
| 525 |
+
<g key={name} opacity={dim}>
|
| 526 |
+
<circle cx={p.x} cy={p.y} r={r * 2.6 * twinkle} fill={BLUE} opacity={0.18 * born} style={{ filter: "blur(8px)" }} />
|
| 527 |
+
<circle cx={p.x} cy={p.y} r={r} fill="#ffffff" style={{ filter: `drop-shadow(0 0 10px ${BLUE})` }} />
|
| 528 |
+
{i === 0 && selected && <circle cx={p.x} cy={p.y} r={r + 14 + 22 * burst} fill="none" stroke={BLUE} strokeWidth={3} opacity={1 - 0.6 * burst} />}
|
| 529 |
+
<text x={p.x} y={p.y + r + 26} textAnchor="middle" fontFamily={theme.body} fontSize={i === 0 ? 26 : 21} fill="#ffffff" opacity={0.85 * born}>
|
| 530 |
+
{name}
|
| 531 |
+
</text>
|
| 532 |
+
</g>
|
| 533 |
+
);
|
| 534 |
+
})}
|
| 535 |
+
</g>
|
| 536 |
</svg>
|
| 537 |
<div style={{ position: "absolute", left: 40, top: 26, display: "flex", gap: 10 }}>
|
| 538 |
{MAP_SHAPES.map((name, i) => (
|
| 539 |
+
<Chip key={name} on={i === activeShape} press={i === 0 ? 0 : pulse(frame, [0, 56, 92, 124][i])} style={{ width: 120, height: 48, fontSize: 22 }}>
|
| 540 |
{name}
|
| 541 |
</Chip>
|
| 542 |
))}
|
| 543 |
</div>
|
| 544 |
+
<p style={{ position: "absolute", left: 40, bottom: 20, margin: 0, fontFamily: theme.mono, fontSize: 18, color: "rgba(255,255,255,0.5)", opacity: 1 - clamp01(frame, 150, 165) }}>Scroll to dive in. Drag to move. Click a star to fly to it.</p>
|
| 545 |
<div style={{ ...panel, position: "absolute", right: 40, top: 26, width: 380, padding: 28, opacity: panelIn, transform: `translateX(${(1 - panelIn) * 60}px)` }}>
|
| 546 |
+
<p style={{ margin: 0, fontFamily: theme.display, fontSize: 40, color: theme.white }}>Gravity</p>
|
| 547 |
<Label style={{ position: "static", display: "block", marginTop: 22, fontSize: 16 }}>Orbiting here</Label>
|
| 548 |
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 10 }}>
|
| 549 |
+
{["Spacetime", "Black holes", "Orbits"].map((t) => (
|
| 550 |
<Chip key={t} on style={{ padding: "4px 16px", fontSize: 20 }}>{t}</Chip>
|
| 551 |
))}
|
| 552 |
</div>
|
| 553 |
<Label style={{ position: "static", display: "block", marginTop: 22, fontSize: 16 }}>From your posts</Label>
|
| 554 |
+
{["Why Things Fall", "Curved Space", "Notes on Orbits"].map((t) => (
|
| 555 |
<p key={t} style={{ margin: "10px 0 0", fontFamily: theme.body, fontSize: 24, color: theme.white }}>{t}</p>
|
| 556 |
))}
|
| 557 |
</div>
|
|
|
|
| 559 |
<Cursor
|
| 560 |
keys={[
|
| 561 |
{ f: 0, x: 1560, y: 880 },
|
| 562 |
+
{ f: 42, ...at(230, 50) },
|
| 563 |
+
{ f: 58, ...at(230, 50) },
|
| 564 |
+
{ f: 84, ...at(360, 50) },
|
| 565 |
+
{ f: 94, ...at(360, 50) },
|
| 566 |
+
{ f: 114, ...at(490, 50) },
|
| 567 |
+
{ f: 126, ...at(490, 50) },
|
| 568 |
+
{ f: 152, x: hub.x, y: hub.y },
|
| 569 |
+
{ f: 172, x: hub.x + 30, y: hub.y + 120 },
|
| 570 |
]}
|
| 571 |
+
clicks={[56, 92, 124, 156]}
|
| 572 |
/>
|
| 573 |
</AbsoluteFill>
|
| 574 |
);
|
|
|
|
| 583 |
const bar = clamp01(frame, 52, 84);
|
| 584 |
const playing = clamp01(frame, 84, 96);
|
| 585 |
const lines = [
|
| 586 |
+
{ who: "A", text: "So the big idea here is that gravity is curved spacetime." },
|
| 587 |
+
{ who: "B", text: "Right, and orbits are just objects falling around the curve." },
|
| 588 |
+
{ who: "A", text: "Which she wrote about back in March, in Curved Space." },
|
| 589 |
];
|
| 590 |
const active = Math.min(lines.length - 1, Math.floor(Math.max(0, frame - 92) / 24));
|
| 591 |
return (
|
|
|
|
| 635 |
{ background: "linear-gradient(135deg, #000, #0b1730)" },
|
| 636 |
{ background: `linear-gradient(135deg, ${BLUE}, #0a2a66)` },
|
| 637 |
{ background: "linear-gradient(135deg, #ffffff, #cfe0ff)" },
|
| 638 |
+
{ background: "linear-gradient(135deg, #0b1730, #217cff55)" },
|
| 639 |
];
|
| 640 |
+
const TEMPLATE_NAMES = ["Night", "Signal", "Paper", "Studio"];
|
| 641 |
const VOICES = ["Nora Vale", "Your voice (clone)", "+ Create a voice"];
|
| 642 |
|
| 643 |
/** 05: wizard: pick a style, a template and a voice, create. */
|
|
|
|
| 702 |
);
|
| 703 |
}
|
| 704 |
|
| 705 |
+
const SCENE_TITLES = ["Gravity is geometry", "Inverse square", "Orbits", "Try it yourself"];
|
| 706 |
|
| 707 |
/** 06: the scene editor, then render the MP4. */
|
| 708 |
function VideoEdit() {
|
|
|
|
| 742 |
{active === 0 ? (
|
| 743 |
<div style={{ position: "absolute", left: 70, top: 160, opacity: pop, transform: `translateY(${(1 - pop) * 30}px)` }}>
|
| 744 |
<p style={{ margin: 0, fontFamily: theme.mono, fontSize: 22, letterSpacing: 5, color: BLUE }}>SCENE 1</p>
|
| 745 |
+
<p style={{ margin: "14px 0 0", fontFamily: theme.display, fontSize: 86, lineHeight: 1.05, color: theme.white, textShadow: `0 0 30px ${BLUE}` }}>Gravity is<br />a curve.</p>
|
| 746 |
</div>
|
| 747 |
) : (
|
| 748 |
<>
|
| 749 |
+
<p style={{ position: "absolute", left: 70, top: 56, margin: 0, fontFamily: theme.display, fontSize: 52, color: theme.white, opacity: pop }}>Gravity fades with distance</p>
|
| 750 |
+
{[1, 0.25, 0.11].map((h, i) => (
|
| 751 |
+
<div key={i} style={{ position: "absolute", left: 90 + i * 250, bottom: 90, width: 170, height: 340 * h * ease(clamp01(since, 8 + i * 7, 34 + i * 7)), borderRadius: "12px 12px 0 0", background: i === 0 ? "#ffffff" : BLUE, boxShadow: `0 0 30px ${BLUE}` }} />
|
| 752 |
))}
|
| 753 |
+
<p style={{ position: "absolute", left: 70, bottom: 30, margin: 0, fontFamily: theme.body, fontSize: 26, color: "rgba(255,255,255,0.85)", opacity: pop }}>Double the distance and gravity drops to a quarter.</p>
|
| 754 |
</>
|
| 755 |
)}
|
| 756 |
{render > 0 && (
|
|
|
|
| 783 |
const frame = useCurrentFrame();
|
| 784 |
const { fps } = useVideoConfig();
|
| 785 |
const cards = [
|
| 786 |
+
{ label: "X THREAD", body: "Gravity is not a force. It is geometry. A thread on how Einstein changed everything." },
|
| 787 |
+
{ label: "LINKEDIN", body: "Most of us learned gravity wrong. Here is the picture physicists actually use." },
|
| 788 |
+
{ label: "SUBSTACK NOTES", body: "Mass tells space how to bend. Space tells mass how to move." },
|
| 789 |
+
{ label: "BLUESKY", body: "Orbits are just falling, forever, around a curve." },
|
| 790 |
];
|
| 791 |
const days = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"];
|
| 792 |
const slots: Record<number, string> = { 0: "X", 1: "LinkedIn", 2: "Notes", 3: "Bluesky", 4: "X", 5: "LinkedIn", 6: "Notes" };
|