diff --git a/examples/app_hub/README.md b/examples/app_hub/README.md index f7a9beb..0615c26 100644 --- a/examples/app_hub/README.md +++ b/examples/app_hub/README.md @@ -2,7 +2,16 @@ **Infra overview:** [docs/INFRA.md](../../docs/INFRA.md) -Static landing: `index.html`, `hub.css`, `assets/hub-logo.svg` (globe + rocket; palette matches crash console). +Static landing: `index.html`, `hub.css`, `hub-logo-layers.css`. + +Logo assets in `assets/` (editable fragments, loaded on demand): +- `hub-logo-fragment-space.svg` — cosmic background (#space-bg, #stars-layer, #nebula-*) +- `hub-logo-fragment-globe.svg` — Earth (round border + Mercator grid 3x3, no continents) +- `hub-logo-transparent.svg` — header mark (globe only) + +Review one layer: `?logoPart=space|globe|full` + +Compare Earth grids: `?logoPart=globe&grid=3` (default) or `?logoPart=full&grid=4` **BE only.** FE `apps.f0xx.org` sends `location /` → `http://artc0.intra.raptor.org:80` (not :8089). diff --git a/examples/app_hub/assets/_snapshot-globe-grid3.png b/examples/app_hub/assets/_snapshot-globe-grid3.png new file mode 100644 index 0000000..95584d7 Binary files /dev/null and b/examples/app_hub/assets/_snapshot-globe-grid3.png differ diff --git a/examples/app_hub/assets/extract_red_polygons.py b/examples/app_hub/assets/extract_red_polygons.py new file mode 100644 index 0000000..bf3029e --- /dev/null +++ b/examples/app_hub/assets/extract_red_polygons.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""Extract red land polygons from Mercator reference image -> SVG paths.""" + +from __future__ import annotations + +import json +import math +from collections import deque +from pathlib import Path + +import numpy as np +from PIL import Image + +ROOT = Path(__file__).resolve().parent +SRC = ROOT / "mercator-source.jpg" +OUT_FLAT = ROOT / "hub-logo-continents-mercator-flat.svg" +OUT_META = ROOT / "continents-meta.json" + +# Source image is 800x519 Mercator-style plate. +W, H = 800, 519 + + +def red_mask(rgb: np.ndarray) -> np.ndarray: + r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2] + return (r.astype(np.int16) > 150) & (g.astype(np.int16) < 120) & (b.astype(np.int16) < 120) + + +def dilate(mask: np.ndarray, radius: int = 3) -> np.ndarray: + """Bridge thin white grid lines between red land cells.""" + h, w = mask.shape + out = mask.copy() + ys, xs = np.where(mask) + for y, x in zip(ys, xs): + y0 = max(0, y - radius) + y1 = min(h, y + radius + 1) + x0 = max(0, x - radius) + x1 = min(w, x + radius + 1) + if mask[y0:y1, x0:x1].any(): + out[y0:y1, x0:x1] = True + return out + + +def label_components(mask: np.ndarray) -> tuple[np.ndarray, int]: + h, w = mask.shape + labels = np.zeros((h, w), dtype=np.int32) + current = 0 + for y in range(h): + for x in range(w): + if not mask[y, x] or labels[y, x]: + continue + current += 1 + q = deque([(x, y)]) + labels[y, x] = current + while q: + cx, cy = q.popleft() + for nx, ny in ((cx + 1, cy), (cx - 1, cy), (cx, cy + 1), (cx, cy - 1)): + if 0 <= nx < w and 0 <= ny < h and mask[ny, nx] and labels[ny, nx] == 0: + labels[ny, nx] = current + q.append((nx, ny)) + return labels, current + + +def trace_boundary(labels: np.ndarray, label_id: int) -> list[tuple[int, int]]: + """Moore-neighbor contour for one connected component.""" + h, w = labels.shape + comp = labels == label_id + if not comp.any(): + return [] + + # Start at topmost-leftmost pixel of component. + ys, xs = np.where(comp) + start = (int(xs[0]), int(ys[0])) + x, y = start + path = [start] + # Directions: E, SE, S, SW, W, NW, N, NE + dirs = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)] + dir_idx = 0 + + def is_land(px: int, py: int) -> bool: + return 0 <= px < w and 0 <= py < h and labels[py, px] == label_id + + for _ in range(w * h * 4): + found = False + for i in range(8): + nd = (dir_idx + i) % 8 + dx, dy = dirs[nd] + nx, ny = x + dx, y + dy + if is_land(nx, ny): + x, y = nx, ny + dir_idx = (nd + 6) % 8 + path.append((x, y)) + found = True + break + if not found: + break + if (x, y) == start and len(path) > 8: + break + return path + + +def simplify(points: list[tuple[int, int]], min_dist: float = 2.0) -> list[tuple[int, int]]: + if len(points) < 3: + return points + out = [points[0]] + for p in points[1:]: + ox, oy = out[-1] + if math.hypot(p[0] - ox, p[1] - oy) >= min_dist: + out.append(p) + if out[0] != out[-1]: + out.append(out[0]) + return out + + +def path_d(points: list[tuple[float, float]]) -> str: + if len(points) < 3: + return "" + x0, y0 = points[0] + parts = [f"M {x0:.1f} {y0:.1f}"] + for x, y in points[1:]: + parts.append(f"L {x:.1f} {y:.1f}") + parts.append("Z") + return " ".join(parts) + + +def mercator_y_to_lat(y: float, h: float) -> float: + # Map top->north, bottom->south; clamp to avoid infinities at poles. + t = 1.0 - (y / h) + t = min(max(t, 0.02), 0.98) + return math.degrees(math.atan(math.sinh(math.pi * (2 * t - 1)))) + + +def lat_lon_to_globe(lat: float, lon: float, cx: float, cy: float, r: float) -> tuple[float, float]: + """Simple orthographic-ish placement on visible globe disk.""" + lat_r = math.radians(lat) + lon_r = math.radians(lon) + # Visible hemisphere centered around lon 20, lat 20 for this logo framing. + lon0 = math.radians(20.0) + lat0 = math.radians(15.0) + # Rotate lon relative to center. + dlon = lon_r - lon0 + x = cx + r * math.cos(lat_r) * math.sin(dlon) + y = cy - r * math.sin(lat_r) + return x, y + + +def map_to_globe(points: list[tuple[int, int]], w: int, h: int) -> list[tuple[float, float]]: + out: list[tuple[float, float]] = [] + cx, cy, r = 720.0, 560.0, 318.0 + for x, y in points: + lon = (x / w) * 360.0 - 180.0 + lat = mercator_y_to_lat(float(y), float(h)) + gx, gy = lat_lon_to_globe(lat, lon, cx, cy, r) + out.append((gx, gy)) + return out + + +def main() -> None: + img = Image.open(SRC).convert("RGB") + rgb = np.array(img) + w, h = rgb.shape[1], rgb.shape[0] + mask = dilate(red_mask(rgb), radius=3) + labels, n = label_components(mask) + + components: list[dict] = [] + for label_id in range(1, n + 1): + area = int((labels == label_id).sum()) + if area < 500: # drop tiny noise / watermark specks + continue + boundary = trace_boundary(labels, label_id) + if len(boundary) < 20: + continue + boundary = simplify(boundary, min_dist=3.5) + ys, xs = np.where(labels == label_id) + bbox = [int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())] + components.append( + { + "id": label_id, + "area": area, + "bbox": bbox, + "flat": path_d([(float(x), float(y)) for x, y in boundary]), + "globe": path_d(map_to_globe(boundary, w, h)), + } + ) + + components.sort(key=lambda c: c["area"], reverse=True) + + flat_paths = "\n ".join( + f'' + for c in components + ) + globe_paths = "\n ".join( + f'' + for c in components + ) + + OUT_FLAT.write_text( + f""" + + + {flat_paths} + + +""", + encoding="utf-8", + ) + + globe_svg = ROOT.parent.parent / "examples" / "app_hub" / "assets" / "hub-logo-continents-mercator-globe.svg" + globe_svg.write_text( + f""" + + + + + {globe_paths} + + +""", + encoding="utf-8", + ) + + OUT_META.write_text( + json.dumps( + { + "source": str(SRC.name), + "size": [w, h], + "components": len(components), + "areas_top10": [c["area"] for c in components[:10]], + }, + indent=2, + ), + encoding="utf-8", + ) + + print(f"components={len(components)}") + print(f"wrote {OUT_FLAT}") + print(f"wrote {globe_svg}") + + +if __name__ == "__main__": + main() diff --git a/examples/app_hub/assets/hub-logo-continents-mercator-flat.svg b/examples/app_hub/assets/hub-logo-continents-mercator-flat.svg new file mode 100644 index 0000000..eecc1f6 --- /dev/null +++ b/examples/app_hub/assets/hub-logo-continents-mercator-flat.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/examples/app_hub/assets/hub-logo-continents-mercator-globe.svg b/examples/app_hub/assets/hub-logo-continents-mercator-globe.svg new file mode 100644 index 0000000..e09b66c --- /dev/null +++ b/examples/app_hub/assets/hub-logo-continents-mercator-globe.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/examples/app_hub/assets/hub-logo-fragment-globe-4x4.svg b/examples/app_hub/assets/hub-logo-fragment-globe-4x4.svg new file mode 100644 index 0000000..2a39497 --- /dev/null +++ b/examples/app_hub/assets/hub-logo-fragment-globe-4x4.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/app_hub/assets/hub-logo-fragment-globe-grid-overlay.svg b/examples/app_hub/assets/hub-logo-fragment-globe-grid-overlay.svg new file mode 100644 index 0000000..d380efe --- /dev/null +++ b/examples/app_hub/assets/hub-logo-fragment-globe-grid-overlay.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + N + S + W + E + + + + diff --git a/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260526e.svg b/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260526e.svg new file mode 100644 index 0000000..bec4c73 --- /dev/null +++ b/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260526e.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260527g.svg b/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260527g.svg new file mode 100644 index 0000000..c61188d --- /dev/null +++ b/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260527g.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260527i.svg b/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260527i.svg new file mode 100644 index 0000000..9878e2f --- /dev/null +++ b/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260527i.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260527j.svg b/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260527j.svg new file mode 100644 index 0000000..83778c7 --- /dev/null +++ b/examples/app_hub/assets/hub-logo-fragment-globe-snapshot-20260527j.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/app_hub/assets/hub-logo-fragment-globe.svg b/examples/app_hub/assets/hub-logo-fragment-globe.svg index a40e620..80a644f 100644 --- a/examples/app_hub/assets/hub-logo-fragment-globe.svg +++ b/examples/app_hub/assets/hub-logo-fragment-globe.svg @@ -1,38 +1,62 @@ - + + - - - - + + + + - - - - - + + + + - - - - - - - + + + + - + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + - - - - + + + + + + + + + N + S + W + E - - - + + diff --git a/examples/app_hub/assets/hub-logo-fragment-rocket.svg b/examples/app_hub/assets/hub-logo-fragment-rocket.svg index dea023b..6d8099d 100644 --- a/examples/app_hub/assets/hub-logo-fragment-rocket.svg +++ b/examples/app_hub/assets/hub-logo-fragment-rocket.svg @@ -1,40 +1,110 @@ - + - - - - - + + + + - - - + + + + + + + + + + + + - + - - - - + + + + + + + + - - - + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/app_hub/assets/hub-logo-fragment-space.svg b/examples/app_hub/assets/hub-logo-fragment-space.svg index e9df45b..4c68538 100644 --- a/examples/app_hub/assets/hub-logo-fragment-space.svg +++ b/examples/app_hub/assets/hub-logo-fragment-space.svg @@ -1,28 +1,75 @@ - + + - + - - + + - - + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/app_hub/assets/hub-logo-fragment-tail.svg b/examples/app_hub/assets/hub-logo-fragment-tail.svg index edf0691..09c2813 100644 --- a/examples/app_hub/assets/hub-logo-fragment-tail.svg +++ b/examples/app_hub/assets/hub-logo-fragment-tail.svg @@ -1,22 +1,58 @@ - + - + - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + diff --git a/examples/app_hub/assets/hub-logo-space-bg.png b/examples/app_hub/assets/hub-logo-space-bg.png deleted file mode 100644 index e3ffa4c..0000000 Binary files a/examples/app_hub/assets/hub-logo-space-bg.png and /dev/null differ diff --git a/examples/app_hub/assets/hub-logo-space-bg.svg b/examples/app_hub/assets/hub-logo-space-bg.svg deleted file mode 100644 index 25c6f14..0000000 --- a/examples/app_hub/assets/hub-logo-space-bg.svg +++ /dev/null @@ -1,441 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/app_hub/assets/hub-logo-transparent.png b/examples/app_hub/assets/hub-logo-transparent.png deleted file mode 100644 index 902a6d4..0000000 Binary files a/examples/app_hub/assets/hub-logo-transparent.png and /dev/null differ diff --git a/examples/app_hub/assets/hub-logo-transparent.svg b/examples/app_hub/assets/hub-logo-transparent.svg index d760584..9c1be94 100644 --- a/examples/app_hub/assets/hub-logo-transparent.svg +++ b/examples/app_hub/assets/hub-logo-transparent.svg @@ -1,206 +1,3 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/examples/app_hub/assets/hub-logo.svg b/examples/app_hub/assets/hub-logo.svg deleted file mode 100644 index 7158485..0000000 --- a/examples/app_hub/assets/hub-logo.svg +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/app_hub/assets/hub-thunder.svg b/examples/app_hub/assets/hub-thunder.svg new file mode 100644 index 0000000..8cb8b54 --- /dev/null +++ b/examples/app_hub/assets/hub-thunder.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/examples/app_hub/assets/mercator-source.jpg b/examples/app_hub/assets/mercator-source.jpg new file mode 100644 index 0000000..c51e2ea Binary files /dev/null and b/examples/app_hub/assets/mercator-source.jpg differ diff --git a/examples/app_hub/hub-logo-layers.css b/examples/app_hub/hub-logo-layers.css index c646c85..e71bd29 100644 --- a/examples/app_hub/hub-logo-layers.css +++ b/examples/app_hub/hub-logo-layers.css @@ -1,44 +1,51 @@ -/* Progressive logo loader (on-demand, fragment-friendly) */ -.hub-page::before { - content: none; -} +/* Logo stage: fragments are injected as layers (same box, object-fit: cover). */ -.hub-page.logo-enabled::before { - content: ""; +.hub-logo-stage { position: fixed; inset: 0; z-index: 0; pointer-events: none; - background-repeat: no-repeat, no-repeat, no-repeat, no-repeat; - background-position: center center, center center, center center, center center; - background-size: cover, cover, cover, cover; - background-image: var(--hub-layer-space, none), var(--hub-layer-globe, none), var(--hub-layer-tail, none), var(--hub-layer-rocket, none); + overflow: hidden; opacity: 0.75; } -[data-theme="light"] .hub-page.logo-enabled::before { +.hub-logo-stage[hidden] { + display: none; +} + +.hub-logo-layer { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center center; +} + +.hub-logo-layer--space { + z-index: 1; +} + +.hub-logo-layer--globe { + z-index: 2; +} + +.hub-logo-layer--continents { + z-index: 3; +} + +.hub-logo-layer--gridOverlay { + z-index: 4; +} + +.hub-logo-layer--tail { + z-index: 5; +} + +.hub-logo-layer--rocket { + z-index: 6; +} + +[data-theme="light"] .hub-logo-stage { opacity: 0.75; } - -.hub-page.logo-full { - --hub-layer-space: url("assets/hub-logo-fragment-space.svg"); - --hub-layer-globe: url("assets/hub-logo-fragment-globe.svg"); - --hub-layer-tail: url("assets/hub-logo-fragment-tail.svg"); - --hub-layer-rocket: url("assets/hub-logo-fragment-rocket.svg"); -} - -.hub-page.logo-space { - --hub-layer-space: url("assets/hub-logo-fragment-space.svg"); -} - -.hub-page.logo-globe { - --hub-layer-globe: url("assets/hub-logo-fragment-globe.svg"); -} - -.hub-page.logo-tail { - --hub-layer-tail: url("assets/hub-logo-fragment-tail.svg"); -} - -.hub-page.logo-rocket { - --hub-layer-rocket: url("assets/hub-logo-fragment-rocket.svg"); -} diff --git a/examples/app_hub/hub.css b/examples/app_hub/hub.css index e69db9b..5ae8d07 100644 --- a/examples/app_hub/hub.css +++ b/examples/app_hub/hub.css @@ -8,10 +8,7 @@ isolation: isolate; } -/* Logo background is loaded progressively by hub-logo-layers.css when supported. */ -.hub-page::before { - content: none; -} +/* Logo background is injected on demand into #hub-logo-stage (see index.html). */ .hub-shell { flex: 1; @@ -51,9 +48,17 @@ .hub-title { margin: 0 0 8px; - font-size: 1.75rem; - font-weight: 600; - letter-spacing: -0.02em; + font-size: 2.45rem; + font-weight: 800; + line-height: 1.02; + letter-spacing: -0.015em; +} + +.hub-thunder { + width: clamp(220px, 34vw, 340px); + height: auto; + margin-top: -2px; + filter: drop-shadow(0 0 8px rgba(170, 220, 255, 0.35)); } .hub-subtitle { diff --git a/examples/app_hub/index.html b/examples/app_hub/index.html index 2fc1bea..d97df20 100644 --- a/examples/app_hub/index.html +++ b/examples/app_hub/index.html @@ -12,14 +12,17 @@ + +

Android Cast

+ Thunder underline

Project hub