Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,21 @@ a 1080p projector and a phone before anyone walks into the room.

</div>

![One brief becomes three structurally different real previews; the chosen direction is built into a six-slide deck and verified at projector and phone sizes](./assets/hero-concept-gate.gif?v=20260917-cinematic)
![One brief becomes three structurally different real previews; the chosen direction is built into a six-slide deck and verified at projector and phone sizes](./assets/hero-concept-gate.gif?v=20260918-premium-readme)

<p align="center"><sub>
20 seconds · intake → three real directions → chosen deck → dual-viewport verification.
Every frame is this repo's own output — the concept-gate previews, the six-slide
<a href="./examples/preflight-decks-pitch.html">example deck</a>, and a real verifier
report: <b>6 slides · 2 viewports · 0 errors · 0 warnings</b>.
Shot on a stage, not on a page: a lateral pan across the three directions, one
continuous push from a 52% panel to full bleed as the chosen direction becomes the
deck, and a pull-back to a static full view. The stage now carries the same
OLED gradient, sparse particle field, perspective grid and fixed grain as the
live launch page. The grammar is measured from
The cut uses floating glass screens, soft floor shadows, and one restrained
signal-red accent: a lateral rail pan, one continuous push from a 52% panel to
full bleed, a projector/phone verification receipt, and a pull-back to a static
full view. The OLED gradient, sparse particle field, perspective grid and fixed
grain match the live launch page. The camera grammar is measured from
apple.com's product pages — see
<a href="./references/scroll-camera-recipes.md">the shot catalogue</a>.
<a href="./assets/hero-concept-gate.mp4?v=20260917-cinematic">MP4 version</a> ·
<a href="./assets/hero-concept-gate.mp4?v=20260918-premium-readme">MP4 version</a> ·
<a href="./docs/hero-film-storyboard.md">storyboard</a>
</sub></p>

Expand Down Expand Up @@ -120,7 +120,7 @@ editing, PDF/URL export, PPTX conversion.

The same brief rendered three ways. Only the chosen one gets built.

![Three concept-gate previews: safe preset, bold template, wildcard field manual, with the wildcard marked chosen](./assets/demo-previews.png)
![Three concept-gate previews: safe preset, bold template, wildcard field manual, with the wildcard marked chosen](./assets/demo-previews.png?v=20260918-staged)

### 2. The chosen direction becomes the deck

Expand Down
13 changes: 7 additions & 6 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@

</div>

![同一个需求变成三个结构真正不同的真实预览,被选中的方向做成六页 deck,并在投影仪和手机尺寸下通过验收](./assets/hero-concept-gate.gif?v=20260917-cinematic)
![同一个需求变成三个结构真正不同的真实预览,被选中的方向做成六页 deck,并在投影仪和手机尺寸下通过验收](./assets/hero-concept-gate.gif?v=20260918-premium-readme)

<p align="center"><sub>
20 秒 · 需求 → 三个真实方向 → 选定后成稿 → 双视口验收。
每一帧都是本仓库自己的产物——概念门预览、六页
<a href="./examples/preflight-decks-pitch.html">示例 deck</a>,以及真实的验收报告:
<b>6 页 · 2 个视口 · 0 错误 · 0 警告</b>。
用摄影机拍的:开场定场微推、一次对准选中方向的 1.3x 推近、验收墙上的视差漂移,
最后拉回全景停稳。背景也换成与在线落地页一致的 OLED 渐变、稀疏粒子场、
透视网格和固定颗粒。
<a href="./assets/hero-concept-gate.mp4?v=20260917-cinematic">MP4 版</a> ·
这不是直录屏幕:真实截图被放进悬浮玻璃屏与柔和落地阴影里,只用克制的信号红
标记被选中的方向。镜头依次完成横向滑轨、52% 到满幅的连续推近、投影仪/手机
验收凭据,最后拉回全景停稳;OLED 渐变、稀疏粒子场、透视网格和固定颗粒都与在线
落地页统一。
<a href="./assets/hero-concept-gate.mp4?v=20260918-premium-readme">MP4 版</a> ·
<a href="./docs/hero-film-storyboard.md">分镜卡</a>
</sub></p>

Expand Down Expand Up @@ -95,7 +96,7 @@ Preflight Decks 让这三件事*在结构上不可能发生*:三个**结构不

同一个需求渲染三版。只有被选中的那一版会被做成完整 deck。

![三个概念门预览:安全预设、大胆模板、自定义飞行手册,其中自定义方向被标记为选中](./assets/demo-previews.png)
![三个概念门预览:安全预设、大胆模板、自定义飞行手册,其中自定义方向被标记为选中](./assets/demo-previews.png?v=20260918-staged)

### 2. 被选中的方向变成 deck

Expand Down
Binary file modified assets/demo-previews.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/hero-concept-gate.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/hero-concept-gate.mp4
Binary file not shown.
33 changes: 19 additions & 14 deletions docs/hero-film-storyboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,38 @@ from apple.com's product pages.

**Direction C · cinematic instrument stage.** OLED near-black gradient, warm
signal light, one cool counter-light, SF Pro for display type, SF Mono for
labels, amber eyebrows, signal-red accent. The set also carries a sparse
particle/constellation field, a masked perspective grid and fixed film grain,
matching the live launch page while leaving the product screenshots as the
dominant focal points.
labels, amber eyebrows, signal-red accent. Product screenshots sit in nested
glass bezels with soft floor shadows; the selected card gets a restrained red
halo. The set also carries a sparse particle/constellation field, a masked
perspective grid and fixed film grain, matching the live launch page while
leaving the real slides as the dominant focal points.

1280×720 master at 25fps, 496 frames (19.84s); GIF export 960×540 at 12.5fps,
48-colour palette, no dither (measured: text and screenshots stay legible;
the richer stage is ~5.1MB, still suitable for a GitHub README).
1280×720 master at 25fps, 496 frames (19.84s, ~1.9 MiB MP4); GIF export
960×540 at 12.5fps, 48-colour palette, no dither (measured: text and
screenshots stay legible; the richer stage is ~5.8 MiB, suitable for a GitHub
README).

| # | Time | Shot | Camera move + motive | Composition | Transition to next | Acceptance frames |
|---|---|---|---|---|---|---|
| 1 | 0.0–3.6s | 全景 1x (from 1.06x) | Establishing micro-push, 1.06x → 1.0 over 3.0s — the frame is alive before anything happens; below-1.25x is legal only here | Brief panel on the stage, copy column clear of it | Defocus handoff (0→8px, 8f overlap) | f00, f89 |
| 2 | 3.6–8.2s | 全景 1x → 中景 1.3x | **Lateral pan** across the rail (+320px, 40f) then a 1.3x push anchored on the chosen direction — Apple's horizontal-rail language | Three previews across the mid band; losers dim into the stage; chosen keeps the signal border | Flash-wash (white peak, 5f ramps) | f135, f204 |
| 3 | 8.2–12.4s | 52% → 100% | **The push**: one continuous 52% → 100% move (ease-in-out, 0.18–0.72 of the shot) as the chosen direction becomes the deck — the move is authored into the render, not faked with a scale | Deck panel centred, shadow and hairline bezel; copy has left the frame by the time the panel is large | Defocus handoff (8f) | f240, f310 |
| 4 | 12.4–16.6s | 88% | Static, parallax drift on the verification wall (far/mid/near layers) | Projector capture, phone, and the verifier receipt with the real numbers | Dip-to-dark (≤0.6s total) | f360, f414 |
| 5 | 16.6–19.8s | 全景 1x (from 1.15x) | Pull-back 1.15x → 1.0, then a static full-view hold ≥0.8s — never end pushed-in | Two-line statement + URL chip, everything else empty stage | (end) | f430, f495 |
| 1 | 0.0–3.6s | 全景 1x (from 1.06x) | Establishing micro-push, 1.06x → 1.0 over 3.0s — the frame is alive before anything happens; below-1.25x is legal only here | Split intake: the three failure modes on the left, the actual brief in a nested editor chrome on the right | Defocus handoff (0→8px, 8f overlap) | f00, f89 |
| 2 | 3.6–8.2s | 全景 1x → 中景 1.3x | **Lateral pan** across the rail (+392px, 40f) then a 1.3x push anchored on the chosen direction — Apple's horizontal-rail language | Three floating previews across the mid band; losers dim into the stage; selection is expressed only by the red halo, raised bezel and caption, never by a stamp over the UI | Warm light wash (38% peak, 5f ramps) | f135, f204 |
| 3 | 8.2–12.4s | 52% → 100% | **The push**: one continuous 52% → 100% move (ease-in-out, 0.18–0.72 of the shot), with a cross-dissolve from chosen preview to deck during 0.12–0.42 of the shot — the move is authored into the render, not faked with CSS | Real preview resolves into the built deck inside a glass bezel that reaches full bleed; copy has left the frame by the time the panel is large | Defocus handoff (8f) | f240, f306 |
| 4 | 12.4–16.6s | 88% | Static, parallax drift on the verification wall (far/mid/near layers) | Projector screen, physical phone, and a compact verifier receipt share the same bezel language, with the real numbers | Dip-to-OLED (≤0.6s total) | f360, f414 |
| 5 | 16.6–19.8s | 全景 1x (from 1.15x) | Pull-back 1.15x → 1.0, then a static full-view hold ≥0.8s — never end pushed-in | Two-line statement, three mechanism chips and a URL chip on an otherwise empty stage | (end) | f430, f495 |

Budget check: four camera events across 19.8s; every push-in duration comes
from the log formula; all four seams carry a designed transition; the film
ends on a static full view. Verified after render: the build shot measures
52.7% → 57.0% → 99.7% panel width across frames 210/240/275, and no text box
intersects an image box in any shot.
52.0% → 65.6% → 95.2% → 100% panel width across frames 210/240/275/310. The
verification wall reserves separate bands for the projector, phone, captions
and terminal receipt; no label or receipt intersects a screen at the maximum
parallax offset.

## Rebuilding

```bash
python3 scripts/render_readme_hero.py
python3 scripts/render_concept_gate_image.py
```

The verifier screenshots the film is cut from are generated, not committed; the
Expand Down
166 changes: 84 additions & 82 deletions scripts/render_concept_gate_image.py
Original file line number Diff line number Diff line change
@@ -1,110 +1,112 @@
#!/usr/bin/env python3
"""Render assets/demo-previews.png — the concept-gate comparison image.
"""Render the README concept-gate contact sheet on the cinematic stage."""
from __future__ import annotations

The three source previews are 1280x720 landscape slides. A 3-up strip at
README width makes each slide unreadable, so this renders them as three
full-width labeled rows in the field-manual visual language.
"""
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

from sitestage import add_glow, AMBER, CINNABAR, COOL, INK, MUTE, plate, stage_screen

ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "assets" / "demo-previews.png"
SRCS = [
(ROOT / "examples/previews/01-safe-swiss.png", "01",
"SAFE PRESET", "competent, forgettable — the template default", False),
(ROOT / "examples/previews/02-bold-signal.png", "02",
"BOLD TEMPLATE", "louder and confident, but the same skeleton", False),
(ROOT / "examples/previews/03-wildcard-manual.png", "03",
"WILDCARD — LAUNCH PREFLIGHT FIELD MANUAL", "chosen, then built into the full deck", True),
]

AVENIR = "/System/Library/Fonts/Supplemental/Avenir Next.ttc"
MENLO = "/System/Library/Fonts/Menlo.ttc"

PAPER = (244, 240, 230)
INK = (26, 24, 21)
INK2 = (92, 85, 74)
INK3 = (150, 143, 128)
CINNABAR = (170, 48, 26)
LINE = (202, 194, 178)
SF = "/System/Library/Fonts/SFNS.ttf"
SFMONO = "/System/Library/Fonts/SFNSMono.ttf"

W = 1600
PAD = 64
IMG_W = W - PAD * 2
IMG_H = IMG_W * 9 // 16
ROW_HEAD = 78
ROW_GAP = 40
TOP = 238
BOTTOM = 96
TOP = 250
CARD_X, CARD_W, CARD_H = 240, 1120, 630
PITCH = 730
H = TOP + 3 * PITCH + 135

ROWS = [
("01", "01-safe-swiss.png", ("SAFE", "PRESET"),
"Competent, but template-default", False),
("02", "02-bold-signal.png", ("BOLD", "TEMPLATE"),
"Louder, but the same skeleton", False),
("03", "03-wildcard-manual.png", ("WILDCARD", "FIELD MANUAL"),
"Chosen, then built into the full deck", True),
]


def sf(size, weight="Regular"):
font = ImageFont.truetype(SF, size=size)
try:
font.set_variation_by_name(weight)
except OSError:
pass
return font

def font(path, size, index=0):
return ImageFont.truetype(path, size=size, index=index)

def mono(size, weight="Regular"):
font = ImageFont.truetype(SFMONO, size=size)
try:
font.set_variation_by_name(weight)
except OSError:
pass
return font

def tracked(d, xy, text, fnt, fill, tracking=0):

def tracked(draw, xy, text, font, fill, tracking=0):
x, y = xy
for ch in text:
d.text((x, y), ch, font=fnt, fill=fill)
x += d.textlength(ch, font=fnt) + tracking
draw.text((x, y), ch, font=font, fill=fill)
x += draw.textlength(ch, font=font) + tracking


def text_h(d, text, fnt):
b = d.textbbox((0, 0), text, font=fnt)
return b[3] - b[1]
def wrap(draw, text, font, width):
words, lines, cur = text.split(), [], ""
for word in words:
trial = f"{cur} {word}".strip()
if draw.textlength(trial, font=font) <= width:
cur = trial
else:
lines.append(cur)
cur = word
if cur:
lines.append(cur)
return lines


def main():
f_mono_xs = font(MENLO, 22)
f_mono_s = font(MENLO, 26)
f_mono_b = font(MENLO, 28)
f_idx = font(MENLO, 40)
f_h = font(AVENIR, 52, index=4) # Avenir Next Medium, ttc index varies

H = TOP + 3 * (ROW_HEAD + IMG_H + ROW_GAP) - ROW_GAP + BOTTOM
canvas = Image.new("RGB", (W, H), PAPER)
d = ImageDraw.Draw(canvas)

# Header
tracked(d, (PAD, 56), "CONCEPT GATE", f_mono_xs, CINNABAR, 4)
d.text((PAD, 92), "Same brief, three real directions.", font=f_h, fill=INK)
d.text((PAD, 156), "Each is a structurally different slide — not one skeleton recolored",
font=f_mono_s, fill=INK2)
d.text((PAD, 192), "three times. You choose on pixels before a full deck exists.",
font=f_mono_s, fill=INK2)

y = TOP
for src, idx, title, sub, chosen in SRCS:
tracked(d, (PAD, y + 6), idx, f_idx, CINNABAR if chosen else INK3, 2)
d.text((PAD + 110, y), title, font=f_mono_b, fill=INK)
d.text((PAD + 110, y + 38), sub, font=f_mono_s, fill=INK2)

slide = Image.open(src).convert("RGB").resize((IMG_W, IMG_H), Image.LANCZOS)
sy = y + ROW_HEAD
canvas.paste(slide, (PAD, sy))
border_w = 4 if chosen else 2
d.rectangle((PAD - border_w // 2, sy - border_w // 2,
PAD + IMG_W + border_w // 2 - 1, sy + IMG_H + border_w // 2 - 1),
outline=CINNABAR if chosen else LINE, width=border_w)
base = plate(W, H)
add_glow(base, AMBER, 0.14, -0.02, 0.48, 0.11)
add_glow(base, CINNABAR, 0.92, -0.08, 0.52, 0.14)
add_glow(base, COOL, 0.48, 1.08, 0.54, 0.08)
d = ImageDraw.Draw(base)

tracked(d, (240, 70), "CONCEPT GATE", mono(22, "Medium"), AMBER + (255,), 4)
d.text((236, 112), "Same brief, three real directions.",
font=sf(66, "Bold"), fill=INK + (255,))
d.text((240, 196), "Each is structurally different — not one skeleton recolored three times.",
font=sf(27), fill=MUTE + (255,))

f_index, f_title, f_note = mono(44, "Medium"), mono(20, "Medium"), sf(22)
for i, (idx, filename, title_lines, note, chosen) in enumerate(ROWS):
y = TOP + i * PITCH
slide = Image.open(ROOT / "examples" / "previews" / filename)
stage_screen(base, slide, CARD_X, y, CARD_W, CARD_H,
radius=16, chosen=chosen, dark_slide=i == 1)

color = CINNABAR if chosen else (150, 156, 168)
d.text((72, y + 205), idx, font=f_index, fill=color + (255,))
for j, line in enumerate(title_lines):
tracked(d, (72, y + 278 + j * 30), line, f_title, INK + (255,), 2)
for j, line in enumerate(wrap(d, note, f_note, 142)):
d.text((72, y + 352 + j * 30), line, font=f_note, fill=MUTE + (255,))

if chosen:
tag = "CHOSEN"
tw = int(d.textlength(tag, font=f_mono_s)) + 34
th = 42
tx = PAD + IMG_W - tw - 24
ty = sy + 24
d.rectangle((tx, ty, tx + tw, ty + th), fill=CINNABAR)
d.text((tx + 17, ty + 5), tag, font=f_mono_s, fill=PAPER)

y += ROW_HEAD + IMG_H + ROW_GAP

tracked(d, (PAD, H - 58), "ONLY THE CHOSEN DIRECTION IS BUILT INTO THE FULL DECK",
f_mono_xs, INK3, 3)

x1 = 72
d.rounded_rectangle((x1, y + 24, x1 + 164, y + 62), radius=19,
fill=CINNABAR + (238,))
tracked(d, (x1 + 21, y + 36), "CHOSEN DIRECTION", mono(15, "Medium"),
(255, 255, 255, 255), 1)

tracked(d, (240, H - 72), "ONLY THE CHOSEN DIRECTION IS BUILT INTO THE FULL DECK",
mono(18, "Medium"), (150, 156, 168, 255), 3)
OUT.parent.mkdir(parents=True, exist_ok=True)
canvas.save(OUT, optimize=True)
base.convert("RGB").save(OUT, optimize=True)
print(f"wrote {OUT} {W}x{H}")


Expand Down
Loading
Loading