Skip to content
Open
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
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ CLI password (export / import): prefer the `ZSW_PASSWORD` environment variable (
```bash
npm install
npm run tauri dev # development (HMR)
`npm run dev # frontend-only preview (open preview.html / preview-settings.html in a browser, mocked data, no Tauri needed)`
npm run tauri build # NSIS installer
cd src-tauri && cargo test # unit tests (incl. node↔Rust cross-language crypto vectors)
```
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ CLI 密码(export / import):优先环境变量 `ZSW_PASSWORD`(不出现
```bash
npm install
npm run tauri dev # 开发(HMR)
`npm run dev # 仅前端预览(浏览器打开 preview.html / preview-settings.html,内置 mock 数据,无需 Tauri)`
npm run tauri build # NSIS 安装包
cd src-tauri && cargo test # 单元测试(含 node↔Rust 跨语言加密向量)
```
Expand Down
3 changes: 2 additions & 1 deletion captcha.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Z·SWITCH</title>
<style>
html, body { background: #0a0a0c; }
html, body { background: #101012; }
</style>
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body class="captcha-win">
<div id="cap-app">
<img class="cap-mark" src="/logo.png" width="46" height="46" alt="" draggable="false">
<div class="cap-status">
<div id="cap-dot" class="cap-dot"></div>
<!-- 文案由 captcha.js 按语言注入(避免 en 模式首帧闪中文) -->
Expand Down
Binary file modified docs/screenshot.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 icon-src-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 9 additions & 7 deletions index.html

Large diffs are not rendered by default.

Binary file added logo-src.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
283 changes: 147 additions & 136 deletions make_icon.py
Original file line number Diff line number Diff line change
@@ -1,138 +1,149 @@
import zlib, struct, math, io, os

SS = 2
N = 1024 * SS
R_CORNER = int(N * 0.19)

BG_TOP = (44, 47, 56)
BG_BOT = (26, 28, 34)

def lerp(a, b, t):
return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(3))

def clamp01(v):
return 0.0 if v < 0 else 1.0 if v > 1 else v

def inside_rounded(x, y, n, r):
if x < 0 or y < 0 or x >= n or y >= n:
return False
cx = min(max(x, r), n - r)
cy = min(max(y, r), n - r)
dx, dy = x - cx, y - cy
return dx * dx + dy * dy <= r * r

def poly_contains(pts, px, py):
inside = False
j = len(pts) - 1
for i in range(len(pts)):
xi, yi = pts[i]; xj, yj = pts[j]
if (yi > py) != (yj > py) and px < (xj - xi) * (py - yi) / (yj - yi) + xi:
inside = not inside
j = i
return inside

pixels = [[None] * N for _ in range(N)]

for y in range(N):
t = y / N
base = lerp(BG_TOP, BG_BOT, t ** 1.15)
if t < 0.12:
base = lerp((58, 62, 74), base, t / 0.12)
for x in range(N):
if inside_rounded(x, y, N, R_CORNER):
pixels[y][x] = base

u = N / 2048.0

def put_poly(pts, color_fn):
"""color_fn(x, y) -> (r,g,b),支持笔画内渐变"""
xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
x0, x1 = max(0, int(min(xs))), min(N, int(max(xs)) + 1)
y0, y1 = max(0, int(min(ys))), min(N, int(max(ys)) + 1)
for y in range(y0, y1):
for x in range(x0, x1):
if poly_contains(pts, x, y):
pixels[y][x] = color_fn(x, y)

border = 58 * u
BORDER_C = (244, 192, 104)
def inner_rounded(x, y):
b = border
n2 = N - 2 * b
r2 = max(1, R_CORNER - b)
xx, yy = x - b, y - b
if xx < 0 or yy < 0 or xx >= n2 or yy >= n2:
return False
cx = min(max(xx, r2), n2 - r2)
cy = min(max(yy, r2), n2 - r2)
dx, dy = xx - cx, yy - cy
return dx * dx + dy * dy <= r2 * r2
for y in range(N):
for x in range(N):
if pixels[y][x] is not None and not inner_rounded(x, y):
bg = pixels[y][x]
xx = min(x, N - x); yy = min(y, N - y)
edge_t = clamp01(1 - (min(xx, yy) / (border * 0.8)))
c = lerp(BORDER_C, (255, 219, 150), edge_t * 0.5)
pixels[y][x] = lerp(bg, c, 0.92)

bar_h = 340 * u
y_c = 620 * u
x_l, x_r, tip_x = 400 * u, 1330 * u, 1690 * u
def top_color(x, y):
t = clamp01((x - x_l) / (tip_x - x_l))
return lerp((228, 152, 54), (255, 216, 134), t)
put_poly([
(x_l, y_c - bar_h/2), (x_r, y_c - bar_h/2),
(x_r, y_c - bar_h*1.32), (tip_x, y_c),
(x_r, y_c + bar_h*1.32), (x_r, y_c + bar_h/2),
(x_l, y_c + bar_h/2)], top_color)

y_c2 = 1440 * u
x_l2, x_r2, tip_x2 = 700 * u, 1660 * u, 360 * u
def bot_color(x, y):
t = clamp01((x_r2 - x) / (x_r2 - tip_x2))
return lerp((96, 201, 111), (162, 243, 174), t)
put_poly([
(x_r2, y_c2 - bar_h/2), (x_l2, y_c2 - bar_h/2),
(x_l2, y_c2 - bar_h*1.32), (tip_x2, y_c2),
(x_l2, y_c2 + bar_h*1.32), (x_l2, y_c2 + bar_h/2),
(x_r2, y_c2 + bar_h/2)], bot_color)

w = 250 * u
ax, ay = 1470 * u, 840 * u
bx, by = 590 * u, 1230 * u
L = math.hypot(bx - ax, by - ay)
nx, ny = -(by - ay) / L * w / 2, (bx - ax) / L * w / 2
def diag_color(x, y):
t = clamp01(((x - ax) * (bx - ax) + (y - ay) * (by - ay)) / (L * L))
return lerp((247, 197, 109), (150, 232, 163), t)
put_poly([
(ax + nx, ay + ny), (bx + nx, by + ny),
(bx - nx, by - ny), (ax - nx, ay - ny)], diag_color)

out_n = N
rows = []
for y4 in range(out_n):
row = bytearray([0])
for x4 in range(out_n):
r = g = b = cnt = 0
for dyy in range(SS):
for dxx in range(SS):
c = pixels[y4 * SS + dyy][x4 * SS + dxx]
if c is not None:
r += c[0]; g += c[1]; b += c[2]; cnt += 1
row += bytes([r
rows.append(bytes(row))

def chunk(tag, data):
c = struct.pack(">I", len(data)) + tag + data
return c + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)

ihdr = struct.pack(">IIBBBBB", out_n, out_n, 8, 6, 0, 0, 0)
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(b"".join(rows), 9)) + chunk(b"IEND", b"")

out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon-src-1024.png")
with io.open(out, "wb") as f:
f.write(png)
print("written", out, len(png), "bytes")
# Z·SWITCH 应用图标生成:
# 读取 logo-src.png(品牌 Z 字箭头 logo,1024 RGBA),
# 应用 iOS 风格圆角裁切(1024 坐标,半径 232,超采样抗锯齿),
# 输出 icon-src-1024.png 供 `npx tauri icon` 生成全套平台图标。

SRC = "logo-src.png"
OUT_NAME = "icon-src-1024.png"
OUT = 1024
CORNER = 232.0
SS = 4 # 圆角抗锯齿超采样
AA = CORNER / SS # 覆盖率过渡宽度(输出像素)

# ---------- PNG 解码(纯标准库) ----------

def read_png(path):
with io.open(path, "rb") as f:
data = f.read()
assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a PNG"
pos = 8
idat = bytearray()
meta = None
palette = None
while pos < len(data):
length = struct.unpack(">I", data[pos:pos + 4])[0]
tag = data[pos + 4:pos + 8]
body = data[pos + 8:pos + 8 + length]
if tag == b"IHDR":
w, h, bit, ctype, comp, filt, inter = struct.unpack(">IIBBBBB", body)
assert bit == 8 and inter == 0, "unsupported PNG variant"
meta = (w, h, ctype)
elif tag == b"PLTE":
palette = body
elif tag == b"IDAT":
idat += body
elif tag == b"IEND":
break
pos += 12 + length
w, h, ctype = meta
channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[ctype]
raw = zlib.decompress(bytes(idat))
stride = w * channels
out = bytearray(w * h * 4)
prev = bytearray(stride)
p = 0
for y in range(h):
ftype = raw[p]; p += 1
line = bytearray(raw[p:p + stride]); p += stride
if ftype == 1: # Sub
for i in range(channels, stride):
line[i] = (line[i] + line[i - channels]) & 0xFF
elif ftype == 2: # Up
for i in range(stride):
line[i] = (line[i] + prev[i]) & 0xFF
elif ftype == 3: # Average
for i in range(stride):
a = line[i - channels] if i >= channels else 0
line[i] = (line[i] + ((a + prev[i]) >> 1)) & 0xFF
elif ftype == 4: # Paeth
for i in range(stride):
a = line[i - channels] if i >= channels else 0
b = prev[i]
c = prev[i - channels] if i >= channels else 0
pa = abs(b - c); pb = abs(a - c); pc = abs(a + b - 2 * c)
pr = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
line[i] = (line[i] + pr) & 0xFF
prev = line
for x in range(w):
o = (y * w + x) * 4
if ctype == 6:
out[o:o + 4] = line[x * 4:x * 4 + 4]
elif ctype == 2:
out[o:o + 3] = line[x * 3:x * 3 + 3]; out[o + 3] = 255
elif ctype == 0:
g = line[x]; out[o] = out[o + 1] = out[o + 2] = g; out[o + 3] = 255
elif ctype == 4:
out[o] = out[o + 1] = out[o + 2] = line[x * 2]; out[o + 3] = line[x * 2 + 1]
elif ctype == 3:
idx = line[x]
out[o:o + 3] = palette[idx * 3:idx * 3 + 3]; out[o + 3] = 255
return w, h, out

# ---------- 圆角遮罩 + 重编码 ----------

def inside_rounded_aa(x, y, n, r):
"""4×4 超采样的圆角覆盖率"""
cnt = 0
for sy in range(SS):
for sx in range(SS):
px = x + (sx + 0.5) / SS
py = y + (sy + 0.5) / SS
cx = min(max(px, r), n - r)
cy = min(max(py, r), n - r)
if (px - cx) ** 2 + (py - cy) ** 2 <= r * r:
cnt += 1
return cnt / (SS * SS)

def write_png(path, w, h, rgba):
def chunk(tag, payload):
c = struct.pack(">I", len(payload)) + tag + payload
return c + struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF)
stride = w * 4
raw = bytearray()
for y in range(h):
raw.append(0)
raw += rgba[y * stride:(y + 1) * stride]
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + chunk(b"IEND", b"")
with io.open(path, "wb") as f:
f.write(png)

w, h, px = read_png(SRC)
assert w == OUT and h == OUT, f"logo-src.png 应为 {OUT}x{OUT}"
r = CORNER
for y in range(OUT):
# 仅在可能受圆角影响的边缘区域计算遮罩,加速中心区域
near_edge = y < r or y >= OUT - r
for x in range(OUT):
o = (y * OUT + x) * 4
if near_edge or x < r or x >= OUT - r:
a = inside_rounded_aa(x, y, OUT, r)
if a < 1.0:
px[o + 3] = int(px[o + 3] * a)
here = os.path.dirname(os.path.abspath(__file__))
out = os.path.join(here, OUT_NAME)
write_png(out, OUT, OUT, px)
print("written", out, os.path.getsize(out), "bytes")

# 额外输出 UI 用小尺寸 logo(public/logo.png,供前端 <img> 引用)
UI = 128
step = OUT // UI
os.makedirs(os.path.join(here, "public"), exist_ok=True)
ui = bytearray()
for y in range(UI):
for x in range(UI):
r = g = b = a = 0
for sy in range(step):
for sx in range(step):
o = ((y * step + sy) * OUT + (x * step + sx)) * 4
pa = px[o + 3]
r += px[o] * pa; g += px[o + 1] * pa; b += px[o + 2] * pa; a += pa
if a == 0:
ui += bytes(4)
else:
ui += bytes([r // a, g // a, b // a, a // (step * step)])
ui_out = os.path.join(here, "public", "logo.png")
write_png(ui_out, UI, UI, ui)
print("written", ui_out, os.path.getsize(ui_out), "bytes")
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zcode-switch",
"private": true,
"version": "1.5.3",
"version": "1.6.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
14 changes: 14 additions & 0 deletions preview-settings.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Z·SWITCH settings — preview</title>
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body class="settings-win">
<div id="app"></div>
<script src="/src/preview-mock.js"></script>
<script type="module" src="/src/settings.js"></script>
</body>
</html>
14 changes: 14 additions & 0 deletions preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Z·SWITCH — preview</title>
<link rel="stylesheet" href="/src/styles.css" />
</head>
<body>
<div id="app"></div>
<script src="/src/preview-mock.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Binary file added public/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading