From e63be3e571bc449f4f7aadc5a34834a6aa0ffd96 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 06 Jun 2026 18:52:08 +0800
Subject: [PATCH] Build hero 3D window stack from real app screenshot
---
docs/index-en.html | 13 +-
docs/styles.css | 5
docs/assets/hero-source-grid.jpg | 0
docs/scripts/build-hero-stack.py | 225 +++++++++++++++++++++++++++++++++++++++++++++
docs/assets/hero-source-grid.png | 0
docs/assets/hero-visual-stack.jpg | 0
docs/index.html | 13 +-
7 files changed, 239 insertions(+), 17 deletions(-)
diff --git a/docs/assets/hero-source-grid.jpg b/docs/assets/hero-source-grid.jpg
new file mode 100644
index 0000000..088b4c7
--- /dev/null
+++ b/docs/assets/hero-source-grid.jpg
Binary files differ
diff --git a/docs/assets/hero-source-grid.png b/docs/assets/hero-source-grid.png
new file mode 100644
index 0000000..a797c15
--- /dev/null
+++ b/docs/assets/hero-source-grid.png
Binary files differ
diff --git a/docs/assets/hero-visual-stack.jpg b/docs/assets/hero-visual-stack.jpg
index 2259342..cc52fce 100644
--- a/docs/assets/hero-visual-stack.jpg
+++ b/docs/assets/hero-visual-stack.jpg
Binary files differ
diff --git a/docs/index-en.html b/docs/index-en.html
index 698af6a..0c7cb21 100644
--- a/docs/index-en.html
+++ b/docs/index-en.html
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TagLauncher — Tag-based Mac app launcher</title>
<meta name="description" content="TagLauncher organizes Mac apps with tags: scrollable grid, multi-tag apps, searchable notes, JSON layout backup—beyond Launchpad folders and pages.">
- <meta property="og:image" content="https://shanghai3168.github.io/taglauncher/assets/hero-design-original.jpg">
+ <meta property="og:image" content="https://shanghai3168.github.io/taglauncher/assets/hero-visual-stack.jpg">
<link rel="stylesheet" href="styles.css">
<link rel="alternate" hreflang="en" href="index-en.html">
<link rel="alternate" hreflang="zh-Hans" href="index.html">
@@ -39,13 +39,12 @@
<figure class="hero__art">
<img
- src="assets/hero-visual-only.jpg"
- alt=""
- width="592"
- height="544"
+ src="assets/hero-visual-stack.jpg"
+ alt="TagLauncher App Grid with stacked 3D window presentation"
+ width="1180"
+ height="640"
fetchpriority="high"
- decoding="async"
- role="presentation">
+ decoding="async">
</figure>
</div>
</section>
diff --git a/docs/index.html b/docs/index.html
index 2ce0855..8ea23cc 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TagLauncher — 标签化 Mac 应用启动器</title>
<meta name="description" content="在全球,这可能是Mac最好用的应用启动台。不用怀念 macOS 26 Tahoe 上消失的 Launchpad,TagLauncher 用标签管理 Mac 应用。">
- <meta property="og:image" content="https://shanghai3168.github.io/taglauncher/assets/hero-design-original.jpg">
+ <meta property="og:image" content="https://shanghai3168.github.io/taglauncher/assets/hero-visual-stack.jpg">
<link rel="stylesheet" href="styles.css">
<link rel="alternate" hreflang="en" href="index-en.html">
<link rel="alternate" hreflang="zh-Hans" href="index.html">
@@ -39,13 +39,12 @@
<figure class="hero__art">
<img
- src="assets/hero-visual-only.jpg"
- alt=""
- width="592"
- height="544"
+ src="assets/hero-visual-stack.jpg"
+ alt="TagLauncher 应用网格:多层窗口立体堆叠展示"
+ width="1180"
+ height="640"
fetchpriority="high"
- decoding="async"
- role="presentation">
+ decoding="async">
</figure>
</div>
</section>
diff --git a/docs/scripts/build-hero-stack.py b/docs/scripts/build-hero-stack.py
new file mode 100644
index 0000000..c04e2e7
--- /dev/null
+++ b/docs/scripts/build-hero-stack.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+"""Compose a 3D stacked-window hero from the real TagLauncher screenshot."""
+
+from __future__ import annotations
+
+import math
+from pathlib import Path
+
+import numpy as np
+from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageOps
+
+ROOT = Path(__file__).resolve().parents[1]
+ASSETS = ROOT / "assets"
+_jpg = ASSETS / "hero-source-grid.jpg"
+_png = ASSETS / "hero-source-grid.png"
+SRC = _jpg if _jpg.exists() else _png
+OUT = ASSETS / "hero-visual-stack.jpg"
+
+CANVAS_W, CANVAS_H = 1180, 640
+BG = (255, 255, 255, 255)
+
+
+def find_coeffs(src: list[tuple[float, float]], dst: list[tuple[float, float]]) -> list[float]:
+ matrix = []
+ for (x, y), (u, v) in zip(src, dst):
+ matrix.append([x, y, 1, 0, 0, 0, -u * x, -u * y])
+ matrix.append([0, 0, 0, x, y, 1, -v * x, -v * y])
+ a = np.matrix(matrix, dtype=float)
+ b = np.array(dst).reshape(8)
+ res = np.dot(np.linalg.inv(a.T * a) * a.T, b)
+ return np.array(res).reshape(8).tolist()
+
+
+def warp_quad(img: Image.Image, quad: list[tuple[float, float]], out_size: tuple[int, int]) -> Image.Image:
+ w, h = img.size
+ src = [(0, 0), (w, 0), (w, h), (0, h)]
+ coeffs = find_coeffs(src, quad)
+ return img.transform(out_size, Image.PERSPECTIVE, coeffs, Image.BICUBIC)
+
+
+def rounded_mask(size: tuple[int, int], radius: int) -> Image.Image:
+ mask = Image.new("L", size, 0)
+ draw = ImageDraw.Draw(mask)
+ draw.rounded_rectangle((0, 0, size[0] - 1, size[1] - 1), radius=radius, fill=255)
+ return mask
+
+
+def crop_app_content(src: Image.Image) -> Image.Image:
+ w, h = src.size
+ if w > 2000:
+ top = int(h * 0.055)
+ left = int(w * 0.018)
+ right = int(w * 0.982)
+ bottom = int(h * 0.93)
+ else:
+ top = int(h * 0.04)
+ left = int(w * 0.02)
+ right = int(w * 0.98)
+ bottom = int(h * 0.96)
+ return src.crop((left, top, right, bottom))
+
+
+def make_window(content: Image.Image, title_h: int = 34, radius: int = 14) -> Image.Image:
+ content = content.convert("RGBA")
+ cw, ch = content.size
+ frame = Image.new("RGBA", (cw, ch + title_h), (0, 0, 0, 0))
+ body = Image.new("RGBA", (cw, ch + title_h), (245, 245, 247, 255))
+ mask = rounded_mask((cw, ch + title_h), radius)
+ body.putalpha(mask)
+ frame.alpha_composite(body)
+
+ draw = ImageDraw.Draw(frame)
+ bar_h = title_h
+ draw.rounded_rectangle((0, 0, cw - 1, bar_h + 6), radius=radius, fill=(236, 236, 238, 255))
+ lights = [(18, bar_h // 2), (38, bar_h // 2), (58, bar_h // 2)]
+ colors = [(255, 95, 86, 255), (255, 189, 46, 255), (39, 201, 63, 255)]
+ for (x, y), color in zip(lights, colors):
+ r = 7
+ draw.ellipse((x - r, y - r, x + r, y + r), fill=color)
+
+ shadow = Image.new("RGBA", (cw, ch + title_h), (0, 0, 0, 0))
+ shadow_draw = ImageDraw.Draw(shadow)
+ shadow_draw.rounded_rectangle((8, 10, cw - 2, ch + title_h - 2), radius=radius, fill=(0, 0, 0, 42))
+ shadow = shadow.filter(ImageFilter.GaussianBlur(10))
+ out = Image.new("RGBA", (cw + 16, ch + title_h + 16), (0, 0, 0, 0))
+ out.alpha_composite(shadow, (0, 0))
+ out.alpha_composite(frame, (8, 4))
+ out.alpha_composite(content, (8, 4 + title_h))
+ return out
+
+
+def tint_layer(img: Image.Image, brightness: float, alpha: float) -> Image.Image:
+ layer = ImageEnhance.Brightness(img).enhance(brightness)
+ layer = ImageEnhance.Color(layer).enhance(0.75)
+ r, g, b, a = layer.split()
+ a = a.point(lambda v: int(v * alpha))
+ layer.putalpha(a)
+ return layer
+
+
+def draw_tag_sidebar(size: tuple[int, int]) -> Image.Image:
+ w, h = size
+ side = Image.new("RGBA", size, (0, 0, 0, 0))
+ draw = ImageDraw.Draw(side)
+ draw.rounded_rectangle((0, 0, w - 1, h - 1), radius=12, fill=(34, 34, 38, 235))
+ colors = [
+ (76, 175, 80),
+ (156, 39, 176),
+ (33, 150, 243),
+ (244, 67, 54),
+ (255, 152, 0),
+ (0, 188, 212),
+ (233, 30, 99),
+ ]
+ gap = 8
+ bar_w = w - 18
+ bar_h = max(18, (h - gap * (len(colors) + 1)) // len(colors))
+ y = 10
+ for color in colors:
+ draw.rounded_rectangle((9, y, 9 + bar_w, y + bar_h), radius=6, fill=(*color, 255))
+ y += bar_h + gap
+ return side
+
+
+def place_layer(
+ canvas: Image.Image,
+ layer: Image.Image,
+ quad: list[tuple[float, float]],
+ shadow_blur: int = 18,
+ shadow_alpha: int = 55,
+) -> None:
+ xs = [p[0] for p in quad]
+ ys = [p[1] for p in quad]
+ pad = 30
+ min_x, max_x = int(min(xs)) - pad, int(max(xs)) + pad
+ min_y, max_y = int(min(ys)) - pad, int(max(ys)) + pad
+ box_w = max(1, max_x - min_x)
+ box_h = max(1, max_y - min_y)
+ local_quad = [(x - min_x, y - min_y) for x, y in quad]
+ warped = warp_quad(layer, local_quad, (box_w, box_h))
+
+ shadow = Image.new("RGBA", (box_w, box_h), (0, 0, 0, 0))
+ alpha = warped.split()[3]
+ shadow.putalpha(alpha.point(lambda v: min(v, shadow_alpha)))
+ shadow = shadow.filter(ImageFilter.GaussianBlur(shadow_blur))
+ canvas.alpha_composite(shadow, (min_x + 10, min_y + 16))
+ canvas.alpha_composite(warped, (min_x, min_y))
+
+
+def add_reflection(canvas: Image.Image, subject_box: tuple[int, int, int, int], strength: float = 0.22) -> None:
+ x0, y0, x1, y1 = subject_box
+ subject = canvas.crop((x0, y0, x1, y1)).convert("RGBA")
+ refl_h = int((y1 - y0) * 0.14)
+ if refl_h < 8:
+ return
+ strip = subject.crop((0, subject.height - refl_h, subject.width, subject.height))
+ reflected = ImageOps.flip(strip)
+ grad = Image.linear_gradient("L").resize((reflected.width, reflected.height))
+ reflected.putalpha(grad.point(lambda v: int(v * strength)))
+ canvas.alpha_composite(reflected, (x0, y1 + 2))
+
+
+def build() -> Image.Image:
+ src = Image.open(SRC).convert("RGBA")
+ content = crop_app_content(src)
+ target_w = 820 if content.width < 2000 else 760
+ scale = target_w / content.width
+ target_h = int(content.height * scale)
+ content = content.resize((target_w, target_h), Image.LANCZOS)
+
+ front = make_window(content, title_h=30, radius=12)
+ back_content = content.resize((int(target_w * 0.92), int(target_h * 0.92)), Image.LANCZOS)
+ back = tint_layer(make_window(back_content, title_h=26, radius=10), brightness=0.42, alpha=0.92)
+ deep = tint_layer(make_window(back_content.resize((int(target_w * 0.84), int(target_h * 0.84)), Image.LANCZOS), title_h=22, radius=10), brightness=0.28, alpha=0.88)
+ sidebar = draw_tag_sidebar((74, int(target_h * 0.78)))
+
+ canvas = Image.new("RGBA", (CANVAS_W, CANVAS_H), BG)
+ fw, fh = front.size
+
+ ox, oy = 210, 36
+ front_quad = [
+ (ox, oy + fh * 0.1),
+ (ox + fw * 0.97, oy + fh * 0.02),
+ (ox + fw * 0.99, oy + fh * 0.96),
+ (ox + fw * 0.05, oy + fh),
+ ]
+ back_quad = [
+ (ox + 88, oy - 24),
+ (ox + fw + 62, oy - 56),
+ (ox + fw + 78, oy + fh - 42),
+ (ox + 104, oy + fh - 12),
+ ]
+ deep_quad = [
+ (ox + 138, oy - 64),
+ (ox + fw + 104, oy - 98),
+ (ox + fw + 120, oy + fh - 78),
+ (ox + 154, oy + fh - 40),
+ ]
+ side_quad = [
+ (ox + fw + 22, oy + 24),
+ (ox + fw + 98, oy + 8),
+ (ox + fw + 104, oy + fh - 52),
+ (ox + fw + 30, oy + fh - 18),
+ ]
+
+ place_layer(canvas, deep, deep_quad, shadow_blur=24, shadow_alpha=40)
+ place_layer(canvas, back, back_quad, shadow_blur=20, shadow_alpha=48)
+ place_layer(canvas, sidebar, side_quad, shadow_blur=12, shadow_alpha=70)
+ place_layer(canvas, front, front_quad, shadow_blur=22, shadow_alpha=62)
+
+ all_x = [p[0] for quad in (deep_quad, back_quad, side_quad, front_quad) for p in quad]
+ all_y = [p[1] for quad in (deep_quad, back_quad, side_quad, front_quad) for p in quad]
+ add_reflection(canvas, (int(min(all_x)), int(min(all_y)), int(max(all_x) + fw * 0.2), int(max(all_y) + fh * 0.05)))
+
+ return canvas.convert("RGB")
+
+
+def main() -> None:
+ img = build()
+ img.save(OUT, quality=93, optimize=True, progressive=True)
+ print(f"wrote {OUT} ({img.size[0]}x{img.size[1]})")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/docs/styles.css b/docs/styles.css
index 1c575b7..3a657a0 100644
--- a/docs/styles.css
+++ b/docs/styles.css
@@ -187,7 +187,7 @@
max-width: 22em;
}
-/* Right art: original design pixels — subtle reflection only, capped height */
+/* Right art: composited 3D stack from real screenshot (built asset, no extra CSS warp) */
.hero__art {
margin: 0;
line-height: 0;
@@ -198,10 +198,9 @@
.hero__art img {
display: block;
- width: auto;
+ width: 100%;
max-width: 100%;
height: auto;
- max-height: clamp(300px, 42vw, 500px);
}
/* Main content */
--
Gitblit v1.9.3