Shereen Lee commited on
Commit
387e3e7
·
1 Parent(s): 3349b34

feat: immersive Cesium watercolour (stamen imagery + paint post-process + chase cam), polaroid polish (tiny-mono dd/mm/yyyy, 4/5 aspect, longer caption), timeline by date, fix 2D map expand collapse, stadia key config

Browse files
Files changed (2) hide show
  1. app.py +5 -0
  2. index.html +72 -26
app.py CHANGED
@@ -1259,6 +1259,10 @@ async def phoenix_proxy_post(request: Request, path: str):
1259
  from dotenv import load_dotenv
1260
  load_dotenv()
1261
  CESIUM_ION_TOKEN = os.environ.get("SOZAI_CESIUM_ION_TOKEN", "")
 
 
 
 
1262
 
1263
 
1264
  @app.get("/api/config")
@@ -1271,6 +1275,7 @@ def client_config():
1271
  return {
1272
  "cesiumIonToken": CESIUM_ION_TOKEN,
1273
  "cesiumEnabled": bool(CESIUM_ION_TOKEN),
 
1274
  "nsfwEnabled": True, # the gate always runs (open if model absent)
1275
  "asrEnabled": True, # the endpoint always exists (may be unavailable)
1276
  "asrModel": ASR_MODEL_ID,
 
1259
  from dotenv import load_dotenv
1260
  load_dotenv()
1261
  CESIUM_ION_TOKEN = os.environ.get("SOZAI_CESIUM_ION_TOKEN", "")
1262
+ # Free Stadia Maps API key (https://client.stadiamaps.com) for the Stamen
1263
+ # Watercolor basemap on the Cesium globe. Optional — without it the globe keeps
1264
+ # its default imagery.
1265
+ STADIA_MAPS_KEY = os.environ.get("SOZAI_STADIA_KEY", "")
1266
 
1267
 
1268
  @app.get("/api/config")
 
1275
  return {
1276
  "cesiumIonToken": CESIUM_ION_TOKEN,
1277
  "cesiumEnabled": bool(CESIUM_ION_TOKEN),
1278
+ "stadiaMapsKey": STADIA_MAPS_KEY,
1279
  "nsfwEnabled": True, # the gate always runs (open if model absent)
1280
  "asrEnabled": True, # the endpoint always exists (may be unavailable)
1281
  "asrModel": ASR_MODEL_ID,
index.html CHANGED
@@ -272,9 +272,9 @@
272
  transition: transform .2s; cursor:grab; }
273
  .sozai-polaroid-wrap.is-dragging .sozai-polaroid { cursor:grabbing; }
274
  .sozai-polaroid:hover { z-index:50; }
275
- .sozai-polaroid img { display:block; width:84px; height:84px; object-fit:cover; }
276
- .sozai-polaroid .cap { font-family:'Inkwell','Caveat',cursive; font-size:13px; color:var(--foreground);
277
- text-align:center; padding:3px 2px 5px; max-width:84px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
278
  .sozai-polaroid .pdel { position:absolute; top:-8px; right:-8px; width:18px; height:18px; border-radius:50%;
279
  background:#fff; border:1.5px solid var(--primary,#7c3aed); color:var(--primary,#7c3aed);
280
  font-size:11px; line-height:1; display:none; align-items:center; justify-content:center; cursor:pointer; z-index:3; }
@@ -1622,7 +1622,7 @@
1622
  className={cn("relative rounded-md border-2 border-primary bg-card p-3 shadow-sm",
1623
  destroying ? "sozai-destroy" : (slideDir === "left" ? "sozai-slide-left" : "sozai-slide-right"))}>
1624
  {destroying ? <span className="sozai-tear" /> : null}
1625
- <div className="relative aspect-[4/3] overflow-hidden rounded-sm border-2 border-primary/70">
1626
  <DraggableImage src={photo.src} alt={photo.alt}
1627
  focusX={photo.focusX != null ? photo.focusX : 50}
1628
  focusY={photo.focusY != null ? photo.focusY : 50}
@@ -2427,22 +2427,26 @@
2427
  // components/map/timeline.tsx
2428
  // ----------------------------------------------------------------- //
2429
  function Timeline({ date, moments, selectedId, onSelect }) {
 
 
 
 
2430
  return (
2431
  <div className="flex flex-col gap-4">
2432
  <div className="flex items-center justify-between gap-4">
2433
  <h2 className="font-serif text-sm font-semibold text-foreground">{date}</h2>
2434
- <span className="font-serif text-xs italic text-muted-foreground">a day to remember</span>
2435
  </div>
2436
  <div className="relative">
2437
  <span aria-hidden="true" className="absolute left-0 right-0 top-3 h-0.5 bg-primary/30" />
2438
  <ol className="relative flex items-start justify-between">
2439
- {moments.map((m) => {
2440
  const isActive = m.id === selectedId;
2441
  return (
2442
  <li key={m.id} className="flex flex-1 flex-col items-center gap-2">
2443
- <button type="button" onClick={() => onSelect && onSelect(m.id)} aria-current={isActive ? "true" : undefined} aria-label={`${m.title} at ${m.time}`}
2444
- className={cn("z-10 flex size-6 items-center justify-center rounded-full border-2 border-primary bg-card font-mono text-xs font-bold text-primary transition-colors hover:bg-accent", isActive && "bg-primary text-primary-foreground")}>{m.id}</button>
2445
- <span className="text-center font-mono text-[10px] leading-tight text-muted-foreground">{m.time}</span>
2446
  </li>
2447
  );
2448
  })}
@@ -3902,7 +3906,8 @@
3902
  el.className = "sozai-polaroid";
3903
  el.style.setProperty("--rot", rot + "deg");
3904
  el.style.setProperty("--scale", scale);
3905
- const meta = [p.date, p.time].filter(Boolean).join(" · ");
 
3906
  const objPos = (p.focusX != null ? p.focusX : 50) + "% " + (p.focusY != null ? p.focusY : 50) + "%";
3907
  el.innerHTML =
3908
  '<img src="' + p.src + '" alt="" style="object-position:' + objPos + '">' +
@@ -3979,8 +3984,12 @@
3979
  }
3980
  }, [isFullscreen]);
3981
  useEffect(() => {
3982
- const t = setTimeout(() => { if (mapRef.current) mapRef.current.resize(); }, 80);
3983
- return () => clearTimeout(t);
 
 
 
 
3984
  }, [isFullscreen]);
3985
  function toggleFullscreen() { setIsFullscreen((v) => !v); }
3986
 
@@ -4284,7 +4293,7 @@
4284
  ) : null}
4285
 
4286
  {/* the map itself, with the authentic watercolor paper + vignette */}
4287
- <div className={cn("sozai-map-dropzone relative w-full overflow-hidden rounded-lg border-2 border-primary/40", isFullscreen && "flex-1", dropArmed && "drop-armed")}
4288
  onDragOver={(e) => { if (e.dataTransfer.types.includes("application/sozai-photo")) { e.preventDefault(); setDropArmed(true); } }}
4289
  onDragLeave={(e) => { if (e.currentTarget === e.target) setDropArmed(false); }}
4290
  onDrop={(e) => {
@@ -4574,6 +4583,37 @@
4574
  destination: Cesium.Cartesian3.fromDegrees(-123.1207, 49.2827, 1200),
4575
  orientation: { heading: Cesium.Math.toRadians(10), pitch: Cesium.Math.toRadians(-25) },
4576
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4577
  try {
4578
  const tileset = await Cesium.createOsmBuildingsAsync();
4579
  if (cancelled) { viewer.destroy(); return; }
@@ -4795,18 +4835,24 @@
4795
  const onTick = viewer.clock.onTick.addEventListener((clock) => {
4796
  if (token.stop) return;
4797
  const t = Cesium.JulianDate.secondsDifference(clock.currentTime, start);
4798
- // chase camera just behind & above the model
 
 
4799
  try {
4800
- const pos = sampled.getValue(clock.currentTime);
4801
- if (pos) {
4802
- const carto = Cesium.Cartographic.fromCartesian(pos);
4803
- viewer.scene.camera.setView({
4804
- destination: Cesium.Cartesian3.fromDegrees(
4805
- Cesium.Math.toDegrees(carto.longitude),
4806
- Cesium.Math.toDegrees(carto.latitude) - (isPlane ? 0.004 : 0.0016),
4807
- (isPlane ? 1200 : 300)),
4808
- orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(isPlane ? -38 : -32) },
4809
- });
 
 
 
 
4810
  }
4811
  } catch (e) {}
4812
  if (nextStop < stopTimes.length && t >= stopTimes[nextStop] - 0.2) {
@@ -6544,7 +6590,7 @@
6544
  <li className="flex w-40 flex-col items-center sm:w-44">
6545
  <div className="relative z-10 h-4 w-3 rounded-sm bg-[oklch(0.45_0.06_60)] shadow-sm" aria-hidden="true" />
6546
  <figure
6547
- className={cn("group relative -mt-1 select-none rounded-sm bg-[oklch(0.99_0.01_85)] p-2 pb-9 shadow-xl",
6548
  dragging ? "" : "dr-sway", revealed ? "cursor-default" : "cursor-grab active:cursor-grabbing")}
6549
  style={{ animationDelay: swayDelay + "s",
6550
  transform: dragging ? "rotate(" + (tilt * 0.3) + "deg) translateX(" + (tilt * 0.4) + "px)" : undefined,
@@ -6569,7 +6615,7 @@
6569
  </div>
6570
  )}
6571
  </div>
6572
- <figcaption className="absolute inset-x-0 bottom-2 line-clamp-2 px-2 text-center font-hand text-[12px] leading-tight text-[oklch(0.3_0.04_264)]"
6573
  style={revealed ? { animation: "dr-cap-in .6s ease-out both" } : undefined}>
6574
  {revealed ? photo.defaultCaption : ""}
6575
  </figcaption>
 
272
  transition: transform .2s; cursor:grab; }
273
  .sozai-polaroid-wrap.is-dragging .sozai-polaroid { cursor:grabbing; }
274
  .sozai-polaroid:hover { z-index:50; }
275
+ .sozai-polaroid img { display:block; width:100px; height:125px; object-fit:cover; }
276
+ .sozai-polaroid .cap { font-family:'Martian Mono',monospace; font-size:9px; line-height:1.25; color:var(--foreground);
277
+ text-align:center; padding:4px 3px 5px; max-width:100px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
278
  .sozai-polaroid .pdel { position:absolute; top:-8px; right:-8px; width:18px; height:18px; border-radius:50%;
279
  background:#fff; border:1.5px solid var(--primary,#7c3aed); color:var(--primary,#7c3aed);
280
  font-size:11px; line-height:1; display:none; align-items:center; justify-content:center; cursor:pointer; z-index:3; }
 
1622
  className={cn("relative rounded-md border-2 border-primary bg-card p-3 shadow-sm",
1623
  destroying ? "sozai-destroy" : (slideDir === "left" ? "sozai-slide-left" : "sozai-slide-right"))}>
1624
  {destroying ? <span className="sozai-tear" /> : null}
1625
+ <div className="relative aspect-[4/5] overflow-hidden rounded-sm border-2 border-primary/70">
1626
  <DraggableImage src={photo.src} alt={photo.alt}
1627
  focusX={photo.focusX != null ? photo.focusX : 50}
1628
  focusY={photo.focusY != null ? photo.focusY : 50}
 
2427
  // components/map/timeline.tsx
2428
  // ----------------------------------------------------------------- //
2429
  function Timeline({ date, moments, selectedId, onSelect }) {
2430
+ // Order chronologically by calendar DATE (photos span multiple years) and
2431
+ // label each stop with its date — not the literal time of day.
2432
+ const shortDate = (s) => { const d = new Date(s); return isNaN(d) ? (s || "") : d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); };
2433
+ const sorted = (moments || []).slice().sort((a, b) => (new Date(a.date).getTime() || 0) - (new Date(b.date).getTime() || 0));
2434
  return (
2435
  <div className="flex flex-col gap-4">
2436
  <div className="flex items-center justify-between gap-4">
2437
  <h2 className="font-serif text-sm font-semibold text-foreground">{date}</h2>
2438
+ <span className="font-serif text-xs italic text-muted-foreground">your journey, in order</span>
2439
  </div>
2440
  <div className="relative">
2441
  <span aria-hidden="true" className="absolute left-0 right-0 top-3 h-0.5 bg-primary/30" />
2442
  <ol className="relative flex items-start justify-between">
2443
+ {sorted.map((m, i) => {
2444
  const isActive = m.id === selectedId;
2445
  return (
2446
  <li key={m.id} className="flex flex-1 flex-col items-center gap-2">
2447
+ <button type="button" onClick={() => onSelect && onSelect(m.id)} aria-current={isActive ? "true" : undefined} aria-label={`${m.title} on ${shortDate(m.date)}`}
2448
+ className={cn("z-10 flex size-6 items-center justify-center rounded-full border-2 border-primary bg-card font-mono text-xs font-bold text-primary transition-colors hover:bg-accent", isActive && "bg-primary text-primary-foreground")}>{i + 1}</button>
2449
+ <span className="text-center font-mono text-[10px] leading-tight text-muted-foreground">{shortDate(m.date)}</span>
2450
  </li>
2451
  );
2452
  })}
 
3906
  el.className = "sozai-polaroid";
3907
  el.style.setProperty("--rot", rot + "deg");
3908
  el.style.setProperty("--scale", scale);
3909
+ const _dmy = (s) => { const d = new Date(s); if (isNaN(d)) return s; const z = (n) => String(n).padStart(2, "0"); return z(d.getDate()) + "/" + z(d.getMonth() + 1) + "/" + d.getFullYear(); };
3910
+ const meta = p.date ? _dmy(p.date) : "";
3911
  const objPos = (p.focusX != null ? p.focusX : 50) + "% " + (p.focusY != null ? p.focusY : 50) + "%";
3912
  el.innerHTML =
3913
  '<img src="' + p.src + '" alt="" style="object-position:' + objPos + '">' +
 
3984
  }
3985
  }, [isFullscreen]);
3986
  useEffect(() => {
3987
+ // Resize the GL canvas several times as the expand/collapse layout
3988
+ // settles a single early resize() leaves the map blank/collapsed.
3989
+ const doResize = () => { if (mapRef.current) { try { mapRef.current.resize(); } catch (e) {} } };
3990
+ const raf = requestAnimationFrame(doResize);
3991
+ const ts = [setTimeout(doResize, 120), setTimeout(doResize, 350)];
3992
+ return () => { cancelAnimationFrame(raf); ts.forEach(clearTimeout); };
3993
  }, [isFullscreen]);
3994
  function toggleFullscreen() { setIsFullscreen((v) => !v); }
3995
 
 
4293
  ) : null}
4294
 
4295
  {/* the map itself, with the authentic watercolor paper + vignette */}
4296
+ <div className={cn("sozai-map-dropzone relative w-full overflow-hidden rounded-lg border-2 border-primary/40", isFullscreen && "flex-1 min-h-0", dropArmed && "drop-armed")}
4297
  onDragOver={(e) => { if (e.dataTransfer.types.includes("application/sozai-photo")) { e.preventDefault(); setDropArmed(true); } }}
4298
  onDragLeave={(e) => { if (e.currentTarget === e.target) setDropArmed(false); }}
4299
  onDrop={(e) => {
 
4583
  destination: Cesium.Cartesian3.fromDegrees(-123.1207, 49.2827, 1200),
4584
  orientation: { heading: Cesium.Math.toRadians(10), pitch: Cesium.Math.toRadians(-25) },
4585
  });
4586
+ // Immersive watercolour basemap: Stamen Watercolor tiles (via Stadia
4587
+ // Maps). Needs a free Stadia API key (SOZAI_STADIA_KEY) — without it
4588
+ // the tiles 401 on dynamic domains, so we keep the default imagery.
4589
+ try {
4590
+ if (cfg.stadiaMapsKey) {
4591
+ viewer.imageryLayers.removeAll();
4592
+ viewer.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
4593
+ url: "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg?api_key=" + cfg.stadiaMapsKey,
4594
+ maximumLevel: 16, // watercolour tiles thin out past z16
4595
+ credit: "Map tiles by Stamen Design, hosted by Stadia Maps; data © OpenStreetMap",
4596
+ }));
4597
+ }
4598
+ } catch (e) { console.warn("[cesium] watercolor imagery failed", e); }
4599
+ // Painterly post-process: posterize into flat washes + darken edges.
4600
+ try {
4601
+ viewer.scene.postProcessStages.add(new Cesium.PostProcessStage({
4602
+ fragmentShader: `
4603
+ uniform sampler2D colorTexture;
4604
+ in vec2 v_textureCoordinates;
4605
+ void main() {
4606
+ vec2 uv = v_textureCoordinates;
4607
+ vec3 c = texture(colorTexture, uv).rgb;
4608
+ c = floor(c * 6.0 + 0.5) / 6.0; // flat washes
4609
+ float o = 1.0 / 1024.0;
4610
+ vec3 dx = texture(colorTexture, uv+vec2(o,0.)).rgb - texture(colorTexture, uv-vec2(o,0.)).rgb;
4611
+ vec3 dy = texture(colorTexture, uv+vec2(0.,o)).rgb - texture(colorTexture, uv-vec2(0.,o)).rgb;
4612
+ float edge = clamp(length(dx)+length(dy), 0.0, 1.0);
4613
+ out_FragColor = vec4(c * (1.0 - 0.35*edge), 1.0);
4614
+ }`,
4615
+ }));
4616
+ } catch (e) { console.warn("[cesium] watercolor post-process failed", e); }
4617
  try {
4618
  const tileset = await Cesium.createOsmBuildingsAsync();
4619
  if (cancelled) { viewer.destroy(); return; }
 
4835
  const onTick = viewer.clock.onTick.addEventListener((clock) => {
4836
  if (token.stop) return;
4837
  const t = Cesium.JulianDate.secondsDifference(clock.currentTime, start);
4838
+ // immersive chase camera: sit behind the moving point along the
4839
+ // direction of travel and a little above it, looking ahead down the
4840
+ // route (vs. a fixed south-facing top-down view).
4841
  try {
4842
+ const lookAhead = isPlane ? 4.0 : 2.0; // s ahead to aim at
4843
+ const chaseDist = isPlane ? 1400 : 260; // m behind the point
4844
+ const chaseHeight = isPlane ? 700 : 140; // m above the point
4845
+ const now = clock.currentTime;
4846
+ const pos = sampled.getValue(now);
4847
+ const ahead = sampled.getValue(Cesium.JulianDate.addSeconds(now, lookAhead, new Cesium.JulianDate())) || pos;
4848
+ if (pos && ahead) {
4849
+ const fwd = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(ahead, pos, new Cesium.Cartesian3()), new Cesium.Cartesian3());
4850
+ const up = Cesium.Cartesian3.normalize(pos, new Cesium.Cartesian3()); // geocentric up
4851
+ const cam = Cesium.Cartesian3.clone(pos);
4852
+ Cesium.Cartesian3.add(cam, Cesium.Cartesian3.multiplyByScalar(fwd, -chaseDist, new Cesium.Cartesian3()), cam);
4853
+ Cesium.Cartesian3.add(cam, Cesium.Cartesian3.multiplyByScalar(up, chaseHeight, new Cesium.Cartesian3()), cam);
4854
+ const dir = Cesium.Cartesian3.normalize(Cesium.Cartesian3.subtract(ahead, cam, new Cesium.Cartesian3()), new Cesium.Cartesian3());
4855
+ viewer.scene.camera.setView({ destination: cam, orientation: { direction: dir, up: up } });
4856
  }
4857
  } catch (e) {}
4858
  if (nextStop < stopTimes.length && t >= stopTimes[nextStop] - 0.2) {
 
6590
  <li className="flex w-40 flex-col items-center sm:w-44">
6591
  <div className="relative z-10 h-4 w-3 rounded-sm bg-[oklch(0.45_0.06_60)] shadow-sm" aria-hidden="true" />
6592
  <figure
6593
+ className={cn("group relative -mt-1 select-none rounded-sm bg-[oklch(0.99_0.01_85)] p-2 pb-12 shadow-xl",
6594
  dragging ? "" : "dr-sway", revealed ? "cursor-default" : "cursor-grab active:cursor-grabbing")}
6595
  style={{ animationDelay: swayDelay + "s",
6596
  transform: dragging ? "rotate(" + (tilt * 0.3) + "deg) translateX(" + (tilt * 0.4) + "px)" : undefined,
 
6615
  </div>
6616
  )}
6617
  </div>
6618
+ <figcaption className="absolute inset-x-0 bottom-2 line-clamp-3 px-2 text-center font-hand text-[12px] leading-tight text-[oklch(0.3_0.04_264)]"
6619
  style={revealed ? { animation: "dr-cap-in .6s ease-out both" } : undefined}>
6620
  {revealed ? photo.defaultCaption : ""}
6621
  </figcaption>