docs(logo): 新增 logo v5.1 正式资产、使用规范与构建脚本
- 白文印「标」(AI 生成原图经脚本抠图、统一为 #D9291C、矢量化) - 字标:思源宋体 SemiBold + Libre Caslon;比例 H2,英文视觉内缩 C2 - svg/、png/、favicon:横版、竖版、反白、单色、印章、各尺寸图标 - scripts/:logo_lib.py 统一排版参数,build_assets.sh 一键生成 - logo-spec.md、logo-sheet.html、字体许可证(OFL) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BIN
docs/design/logo/scripts/__pycache__/logo_lib.cpython-313.pyc
Normal file
BIN
docs/design/logo/scripts/__pycache__/logo_lib.cpython-313.pyc
Normal file
Binary file not shown.
46
docs/design/logo/scripts/build_assets.sh
Executable file
46
docs/design/logo/scripts/build_assets.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# 生成全部正式 logo 资产到 svg/、png/ 和 favicon.svg。
|
||||
# 前置:v5/ 里有 process_seal.py 处理好的印章;.work/venv 和 .work/fonts 已准备好。
|
||||
# 用法:docs/design/logo/scripts/build_assets.sh
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
RED='#D9291C'
|
||||
PY=.work/venv/bin/python
|
||||
mkdir -p svg png
|
||||
rm -f svg/*.svg png/*.png
|
||||
|
||||
# 1. 印章(来自 v5/,由 process_seal.py 生成)
|
||||
cp v5/seal-clean.svg svg/seal.svg
|
||||
cp v5/seal-textured.svg svg/seal-textured.svg
|
||||
cp v5/seal-small.svg svg/seal-small.svg
|
||||
cp v5/seal-paper.svg svg/seal-paper.svg
|
||||
|
||||
# 2. 字标与组合(H2 比例 + 思源宋体 SemiBold + Libre Caslon + C2 视觉对齐)
|
||||
"$PY" scripts/build_logo.py
|
||||
|
||||
# 3. 图标:16px 用方案 B(加粗简化的「标」字),32px 起用小尺寸印章
|
||||
sed "s/#D2282E/$RED/Ig" archive/v1/svg/favicon-16-b.svg > svg/favicon-16.svg
|
||||
cp svg/seal-small.svg favicon.svg
|
||||
|
||||
# 4. PNG:组合与印章(透明背景,由矢量渲染)
|
||||
rsvg-convert -w 1200 svg/logo-horizontal-textured.svg -o png/logo-horizontal-1200.png
|
||||
rsvg-convert -w 2400 svg/logo-horizontal-textured.svg -o png/logo-horizontal-2400.png
|
||||
rsvg-convert -w 1200 svg/logo-horizontal-textured-reverse.svg -o png/logo-horizontal-reverse-1200.png
|
||||
rsvg-convert -w 600 svg/logo-vertical-textured.svg -o png/logo-vertical-600.png
|
||||
rsvg-convert -w 600 svg/logo-vertical-textured-reverse.svg -o png/logo-vertical-reverse-600.png
|
||||
rsvg-convert -w 1024 svg/seal-textured.svg -o png/seal-1024.png
|
||||
|
||||
# 5. PNG:图标
|
||||
rsvg-convert -w 16 svg/favicon-16.svg -o png/favicon-16.png
|
||||
rsvg-convert -w 32 svg/seal-small.svg -o png/favicon-32.png
|
||||
# 应用图标:白底,印章占 80%(iOS 不支持透明图标)
|
||||
for s in 180 192 512; do
|
||||
inner=$((s * 80 / 100))
|
||||
rsvg-convert -w "$inner" svg/seal-textured.svg -o "png/.tmp-$s.png"
|
||||
magick -size "${s}x${s}" xc:white "png/.tmp-$s.png" -gravity center -composite "png/icon-$s.png"
|
||||
rm "png/.tmp-$s.png"
|
||||
done
|
||||
mv png/icon-180.png png/apple-touch-icon-180.png
|
||||
|
||||
ls svg png
|
||||
34
docs/design/logo/scripts/build_logo.py
Normal file
34
docs/design/logo/scripts/build_logo.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""生成正式字标和全部组合 SVG 到 svg/(决策 B4:H2 比例 + F2 中文 + E5 英文 + C2 视觉对齐)。
|
||||
|
||||
用法:.work/venv/bin/python scripts/build_logo.py
|
||||
通常由 scripts/build_assets.sh 调用;运行前 svg/ 里要已有 seal*.svg。
|
||||
"""
|
||||
from logo_lib import FINAL, FINAL_INSET, LOGO, NAVY, WHITE, lockup_h2, lockup_vertical, wordmark
|
||||
|
||||
SVG = LOGO / "svg"
|
||||
|
||||
|
||||
def write(name, svg):
|
||||
(SVG / name).write_text(svg)
|
||||
|
||||
|
||||
def wordmark_svg(color, pad=2):
|
||||
body, w, h = wordmark(FINAL, color, inset=FINAL_INSET)
|
||||
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{-pad} {-pad} {w + 2 * pad:.2f} {h + 2 * pad:.2f}">{body}</svg>'
|
||||
|
||||
|
||||
write("wordmark.svg", wordmark_svg(NAVY))
|
||||
write("wordmark-reverse.svg", wordmark_svg(WHITE))
|
||||
|
||||
for kind, suffix in (("textured", "-textured"), ("clean", "")):
|
||||
for color, dark, rev in ((NAVY, False, ""), (WHITE, True, "-reverse")):
|
||||
body, w, h = wordmark(FINAL, color, inset=FINAL_INSET)
|
||||
write(f"logo-horizontal{suffix}{rev}.svg", lockup_h2(body, w, h, on_dark=dark, kind=kind))
|
||||
write(f"logo-vertical{suffix}{rev}.svg", lockup_vertical(body, w, h, on_dark=dark, kind=kind))
|
||||
|
||||
# 单色:干净版印章 + 字标同色
|
||||
for color, name in (("#000000", "logo-mono-black.svg"), ("#FFFFFF", "logo-mono-white.svg")):
|
||||
body, w, h = wordmark(FINAL, color, inset=FINAL_INSET)
|
||||
write(name, lockup_h2(body, w, h, kind="clean", seal_color=color))
|
||||
|
||||
print("ok", sorted(p.name for p in SVG.glob("logo-*.svg")) + ["wordmark.svg", "wordmark-reverse.svg"])
|
||||
149
docs/design/logo/scripts/logo_lib.py
Normal file
149
docs/design/logo/scripts/logo_lib.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""标博士 logo 的公共排版逻辑:字标排字、印章几何、横版 / 竖版拼合。
|
||||
|
||||
被 build_logo.py(正式资产)和 wordmark_compare.py(方案对比)共用。
|
||||
字体文件在 .work/fonts/(不进 git),许可证见 licenses/。
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from fontTools.pens.boundsPen import BoundsPen
|
||||
from fontTools.pens.svgPathPen import SVGPathPen
|
||||
from fontTools.pens.transformPen import TransformPen
|
||||
from fontTools.ttLib import TTFont
|
||||
from PIL import Image
|
||||
|
||||
LOGO = Path(__file__).resolve().parent.parent
|
||||
FONTS = LOGO / ".work/fonts"
|
||||
NAVY, WHITE, SEAL_RED = "#1C2554", "#FFFFFF", "#D9291C"
|
||||
|
||||
CN_TEXT, PY_TEXT = "标博士", "BIAOBOSHI"
|
||||
CN_H = 100.0 # 中文字形实际高度(单位)
|
||||
PY_CAP = 0.20 # 拼音大写字母高度 / 中文高度
|
||||
GAP = 0.14 # 中文底部到拼音顶部的距离 / 中文高度
|
||||
SEAL_SCALE = 1.02 # H2:印章实心高度 = 文字块高度 × 1.02(补偿颗粒边缘显虚)
|
||||
SEAL_GAP = 0.3 # 横版:印章与文字的间距 / 印章高度
|
||||
|
||||
# 正式方案(决策 B4):F2 中文 + E5 英文 + C2 视觉对齐补偿
|
||||
FINAL = ("FINAL", "NotoSerifSC[wght].ttf", 600, 0.04, "LibreCaslonText[wght].ttf", 600)
|
||||
FINAL_INSET = (5.0, 5.8)
|
||||
|
||||
_font_cache = {}
|
||||
|
||||
|
||||
def glyphset(file, wght):
|
||||
key = (file, wght)
|
||||
if key not in _font_cache:
|
||||
f = TTFont(FONTS / file)
|
||||
loc = {"wght": wght} if wght and "fvar" in f else None
|
||||
_font_cache[key] = (f, f.getGlyphSet(location=loc) if loc else f.getGlyphSet())
|
||||
return _font_cache[key]
|
||||
|
||||
|
||||
def outline(file, wght, text, tracking_em):
|
||||
"""返回 [(glyph, x_offset)] 和整体墨迹边界(字体单位,y 向上)。"""
|
||||
font, gs = glyphset(file, wght)
|
||||
cmap = font.getBestCmap()
|
||||
upm = font["head"].unitsPerEm
|
||||
x, items, bounds = 0.0, [], None
|
||||
for ch in text:
|
||||
g = cmap[ord(ch)]
|
||||
bp = BoundsPen(gs)
|
||||
gs[g].draw(bp)
|
||||
if bp.bounds:
|
||||
b = (bp.bounds[0] + x, bp.bounds[1], bp.bounds[2] + x, bp.bounds[3])
|
||||
bounds = b if bounds is None else (min(bounds[0], b[0]), min(bounds[1], b[1]), max(bounds[2], b[2]), max(bounds[3], b[3]))
|
||||
items.append((g, x))
|
||||
x += gs[g].width + tracking_em * upm
|
||||
return gs, items, bounds
|
||||
|
||||
|
||||
def path_for(gs, items, scale, dx, dy):
|
||||
"""把字形变换到目标坐标(y 向下):X = (x+off)*s + dx,Y = -y*s + dy。"""
|
||||
pen = SVGPathPen(gs)
|
||||
for g, off in items:
|
||||
tp = TransformPen(pen, (scale, 0, 0, -scale, off * scale + dx, dy))
|
||||
gs[g].draw(tp)
|
||||
return pen.getCommands()
|
||||
|
||||
|
||||
def wordmark(pair, color, inset=(0.0, 0.0)):
|
||||
"""排出「标博士 / BIAOBOSHI」两行字标,返回 (SVG 片段, 宽, 高)。
|
||||
|
||||
pair:(编号, 中文字体, 中文字重, 中文字距 em, 拼音字体, 拼音字重, ...)
|
||||
inset:拼音左右各内缩多少(单位同中文字高 100),用于视觉对齐补偿。
|
||||
"""
|
||||
_, cnf, cnw, cntr, pyf, pyw, *_ = pair
|
||||
gs, items, b = outline(cnf, cnw, CN_TEXT, cntr)
|
||||
s = CN_H / (b[3] - b[1])
|
||||
cn_w = (b[2] - b[0]) * s
|
||||
cn_d = path_for(gs, items, s, -b[0] * s, b[3] * s)
|
||||
# 拼音:先量大写字母高度,再按(中文宽度 − 内缩)两端对齐(逐字母分配字距)
|
||||
pgs, pitems, pb = outline(pyf, pyw, PY_TEXT, 0)
|
||||
ps = CN_H * PY_CAP / (pb[3] - pb[1])
|
||||
natural = (pb[2] - pb[0]) * ps
|
||||
extra = (cn_w - inset[0] - inset[1] - natural) / (len(PY_TEXT) - 1)
|
||||
pitems = [(g, off + i * extra / ps) for i, (g, off) in enumerate(pitems)]
|
||||
py_top = CN_H * (1 + GAP)
|
||||
py_d = path_for(pgs, pitems, ps, -pb[0] * ps + inset[0], py_top + pb[3] * ps)
|
||||
height = py_top + CN_H * PY_CAP
|
||||
body = f'<path fill="{color}" d="{cn_d}"/><path fill="{color}" d="{py_d}"/>'
|
||||
return body, cn_w, height
|
||||
|
||||
|
||||
def _seal_geometry():
|
||||
"""量出印章在自身画布中的实心边界(用干净版),返回 (画布边长, 上, 下, 左, 右)。"""
|
||||
svg = (LOGO / "svg/seal.svg").read_text()
|
||||
size = float(re.search(r'viewBox="0 0 ([\d.]+)', svg).group(1))
|
||||
probe = LOGO / ".work/seal-probe.png"
|
||||
subprocess.run(["rsvg-convert", "-w", "2000", str(LOGO / "svg/seal.svg"), "-o", str(probe)], check=True)
|
||||
a = np.asarray(Image.open(probe))[..., 3] > 128
|
||||
ys, xs = np.where(a)
|
||||
k = size / 2000
|
||||
return size, ys.min() * k, (ys.max() + 1) * k, xs.min() * k, (xs.max() + 1) * k
|
||||
|
||||
|
||||
SEAL_SIZE, ST, SB, SL, SR = _seal_geometry()
|
||||
_SEAL_FILES = (("textured", "seal-textured.svg"), ("clean", "seal.svg"), ("paper", "seal-paper.svg"))
|
||||
SEAL_BODY = {k: re.sub(r"^.*?<svg[^>]*>|</svg>\s*$", "", (LOGO / f"svg/{f}").read_text(), flags=re.S) for k, f in _SEAL_FILES}
|
||||
SEAL_VB = {k: float(re.search(r'viewBox="0 0 ([\d.]+)', (LOGO / f"svg/{f}").read_text()).group(1)) for k, f in _SEAL_FILES}
|
||||
|
||||
|
||||
def seal_group(kind, x, y, solid_h, on_dark=False, seal_color=None):
|
||||
"""把印章放到 (x, y),使其实心部分左上角落在该点、实心高度为 solid_h。返回 (SVG 片段, 实心宽)。"""
|
||||
s = solid_h / (SB - ST)
|
||||
x0, y0 = x - SL * s, y - ST * s
|
||||
body = SEAL_BODY[kind]
|
||||
if seal_color:
|
||||
body = body.replace(SEAL_RED, seal_color)
|
||||
paper = (f'<g transform="translate({x0:.2f} {y0:.2f}) scale({s * SEAL_SIZE / SEAL_VB["paper"]:.6f})">{SEAL_BODY["paper"]}</g>'
|
||||
if on_dark else "")
|
||||
seal = f'<g transform="translate({x0:.2f} {y0:.2f}) scale({s * SEAL_SIZE / SEAL_VB[kind]:.6f})">{body}</g>'
|
||||
return paper + seal, (SR - SL) * s
|
||||
|
||||
|
||||
def lockup(body, wm_w, wm_h, seal_h, seal_top, on_dark=False, kind="textured", seal_color=None, pad=6):
|
||||
"""横版:印章在左、字标在右。seal_h 是印章实心高度,seal_top 是实心顶端的 y。"""
|
||||
seal, seal_w = seal_group(kind, 0, seal_top, seal_h, on_dark, seal_color)
|
||||
tx = seal_w + max(seal_h, wm_h) * SEAL_GAP
|
||||
top, bottom = min(0, seal_top), max(wm_h, seal_top + seal_h)
|
||||
w = tx + wm_w
|
||||
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{-pad:.1f} {top - pad:.1f} {w + 2 * pad:.1f} {bottom - top + 2 * pad:.1f}">'
|
||||
f'{seal}<g transform="translate({tx:.2f} 0)">{body}</g></svg>')
|
||||
|
||||
|
||||
def lockup_h2(body, wm_w, wm_h, **kw):
|
||||
"""按 H2 比例拼横版:印章实心高度 = 文字块 × SEAL_SCALE,上下居中。"""
|
||||
seal_h = wm_h * SEAL_SCALE
|
||||
return lockup(body, wm_w, wm_h, seal_h, -(seal_h - wm_h) / 2, **kw)
|
||||
|
||||
|
||||
def lockup_vertical(body, wm_w, wm_h, on_dark=False, kind="textured", seal_color=None, pad=6):
|
||||
"""竖版:印章在上(实心边长 = 字标宽度的 50%),字标居中在下,间距 = 印章边长的 18%。"""
|
||||
seal_h = wm_w * 0.5
|
||||
seal, _ = seal_group(kind, (wm_w - seal_h) / 2, 0, seal_h, on_dark, seal_color)
|
||||
gap = seal_h * 0.18
|
||||
h = seal_h + gap + wm_h
|
||||
return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{-pad:.1f} {-pad:.1f} {wm_w + 2 * pad:.1f} {h + 2 * pad:.1f}">'
|
||||
f'{seal}<g transform="translate(0 {seal_h + gap:.2f})">{body}</g></svg>')
|
||||
97
docs/design/logo/scripts/process_seal.py
Normal file
97
docs/design/logo/scripts/process_seal.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""把位图生成的印章处理成 logo 资产:抠图、统一颜色、矢量化、小尺寸版本。
|
||||
|
||||
用法:.work/venv/bin/python scripts/process_seal.py <源图> <输出目录> [品牌红 HEX]
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import potrace
|
||||
from PIL import Image
|
||||
from scipy import ndimage
|
||||
|
||||
src, out_dir = Path(sys.argv[1]), Path(sys.argv[2])
|
||||
brand = sys.argv[3] if len(sys.argv) > 3 else "#D9291C"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
rgb = tuple(int(brand[i:i + 2], 16) for i in (1, 3, 5))
|
||||
|
||||
im = np.asarray(Image.open(src).convert("RGB")).astype(float)
|
||||
redness = (im[..., 0] - (im[..., 1] + im[..., 2]) / 2) / 255
|
||||
|
||||
# 印泥浓淡 → 透明度:保留颗粒和毛边,背景完全透明
|
||||
alpha = np.clip((redness - 0.12) / (0.55 - 0.12), 0, 1)
|
||||
alpha = alpha * alpha * (3 - 2 * alpha) # smoothstep,让颗粒边缘柔和
|
||||
|
||||
ys, xs = np.where(redness > 0.35)
|
||||
y0, y1, x0, x1 = ys.min(), ys.max() + 1, xs.min(), xs.max() + 1
|
||||
side = max(y1 - y0, x1 - x0)
|
||||
pad = round(side * 0.02)
|
||||
canvas = side + 2 * pad
|
||||
cy, cx = (y0 + y1) // 2, (x0 + x1) // 2
|
||||
half = canvas // 2
|
||||
|
||||
|
||||
def crop(a):
|
||||
out = np.zeros((canvas, canvas), dtype=a.dtype)
|
||||
sy0, sx0 = cy - half, cx - half
|
||||
ys_, xs_ = slice(max(sy0, 0), min(sy0 + canvas, a.shape[0])), slice(max(sx0, 0), min(sx0 + canvas, a.shape[1]))
|
||||
out[ys_.start - sy0:ys_.stop - sy0, xs_.start - sx0:xs_.stop - sx0] = a[ys_, xs_]
|
||||
return out
|
||||
|
||||
|
||||
alpha_c = crop(alpha)
|
||||
red_c = crop(redness)
|
||||
|
||||
# 1. 质感主图(透明 PNG)
|
||||
rgba = np.zeros((canvas, canvas, 4), dtype=np.uint8)
|
||||
rgba[..., :3] = rgb
|
||||
rgba[..., 3] = (alpha_c * 255).round().astype(np.uint8)
|
||||
Image.fromarray(rgba).save(out_dir / "seal-textured.png")
|
||||
on_white = np.full((canvas, canvas, 3), 255.0)
|
||||
on_white = on_white * (1 - alpha_c[..., None]) + np.array(rgb) * alpha_c[..., None]
|
||||
Image.fromarray(on_white.round().astype(np.uint8)).save(out_dir / "seal-textured-on-white.png")
|
||||
|
||||
|
||||
def trace(mask, name, turdsize, alphamax=1.0, fill=brand):
|
||||
bm = potrace.Bitmap(~mask) # potracer 把 False 当作前景,这里取反
|
||||
path = bm.trace(turdsize=turdsize, alphamax=alphamax, opticurve=True, opttolerance=0.2)
|
||||
parts = []
|
||||
for curve in path:
|
||||
s = curve.start_point
|
||||
d = [f"M{s.x:.1f} {s.y:.1f}"]
|
||||
for seg in curve.segments:
|
||||
if seg.is_corner:
|
||||
d.append(f"L{seg.c.x:.1f} {seg.c.y:.1f}L{seg.end_point.x:.1f} {seg.end_point.y:.1f}")
|
||||
else:
|
||||
d.append(f"C{seg.c1.x:.1f} {seg.c1.y:.1f} {seg.c2.x:.1f} {seg.c2.y:.1f} {seg.end_point.x:.1f} {seg.end_point.y:.1f}")
|
||||
d.append("Z")
|
||||
parts.append("".join(d))
|
||||
svg = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {canvas} {canvas}">'
|
||||
f'<path fill="{fill}" fill-rule="evenodd" d="{"".join(parts)}"/></svg>')
|
||||
(out_dir / name).write_text(svg)
|
||||
return len(svg)
|
||||
|
||||
|
||||
# 2. 质感矢量版:放大 2 倍再描摹,保留颗粒
|
||||
big = np.asarray(Image.fromarray((alpha_c * 255).astype(np.uint8)).resize((canvas * 2, canvas * 2), Image.LANCZOS)) / 255
|
||||
n_tex = trace(big > 0.5, "seal-textured-2x.svg", turdsize=6)
|
||||
# 描摹坐标是 2 倍的,修正 viewBox
|
||||
p = out_dir / "seal-textured-2x.svg"
|
||||
p.write_text(p.read_text().replace(f'viewBox="0 0 {canvas} {canvas}"', f'viewBox="0 0 {canvas * 2} {canvas * 2}"'))
|
||||
p.rename(out_dir / "seal-textured.svg")
|
||||
|
||||
# 3. 干净版:先模糊去掉颗粒,再二值化描摹,得到平滑的印面和字形
|
||||
solid = ndimage.gaussian_filter((red_c > 0.35).astype(float), sigma=canvas * 0.006) > 0.5
|
||||
solid = ndimage.binary_fill_holes(solid) & solid # 保留白字(它们与外部不连通时是「洞」)
|
||||
solid = ndimage.binary_opening(solid, iterations=2)
|
||||
n_clean = trace(solid, "seal-clean.svg", turdsize=canvas * 0.0005 * canvas, alphamax=1.1)
|
||||
|
||||
# 4. 小尺寸版:白色笔画加粗(墨色腐蚀),保证 32px 下笔画不消失
|
||||
thick = ndimage.binary_erosion(solid, iterations=max(2, round(canvas * 0.012)))
|
||||
n_small = trace(thick, "seal-small.svg", turdsize=canvas * 0.0008 * canvas, alphamax=1.1)
|
||||
|
||||
# 5. 纸底:印面外轮廓(不含白字),向内收一点,深色背景上垫在印章下面,让白字和颗粒显示为纸色
|
||||
paper = ndimage.binary_erosion(ndimage.binary_fill_holes(solid), iterations=max(2, round(canvas * 0.006)))
|
||||
trace(paper, "seal-paper.svg", turdsize=canvas * 0.001 * canvas, alphamax=1.2, fill="#FFFFFF")
|
||||
|
||||
print(f"canvas={canvas}px brand={brand} textured.svg={n_tex // 1024}KB clean.svg={n_clean // 1024}KB small.svg={n_small // 1024}KB")
|
||||
114
docs/design/logo/scripts/vi_diagrams.py
Normal file
114
docs/design/logo/scripts/vi_diagrams.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""生成 VI 手册里的标志构成与安全区标注图(docs/design/vi/diagrams/)。
|
||||
|
||||
标注数值直接取自 logo_lib 的实际几何,保证和正式资产一致。
|
||||
用法:.work/venv/bin/python scripts/vi_diagrams.py
|
||||
"""
|
||||
from logo_lib import (FINAL, FINAL_INSET, LOGO, NAVY, SB, SEAL_GAP, SEAL_SCALE, SL, SR, ST, CN_H, GAP, PY_CAP,
|
||||
seal_group, wordmark)
|
||||
|
||||
OUT = LOGO.parent / "vi/diagrams"
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
BLUE = "#2F5BD3"
|
||||
FONT = 'font-family="PingFang SC, Noto Sans SC, sans-serif"'
|
||||
|
||||
body, W, H = wordmark(FINAL, NAVY, inset=FINAL_INSET)
|
||||
SEAL_H = H * SEAL_SCALE
|
||||
SEAL_TOP = -(SEAL_H - H) / 2
|
||||
SEAL_W = (SR - SL) * SEAL_H / (SB - ST)
|
||||
GAP_X = SEAL_H * SEAL_GAP
|
||||
TX = SEAL_W + GAP_X
|
||||
|
||||
|
||||
def label(x, y, text, anchor="middle", size=7.5):
|
||||
return f'<text x="{x:.1f}" y="{y:.1f}" fill="{BLUE}" font-size="{size}" text-anchor="{anchor}" {FONT}>{text}</text>'
|
||||
|
||||
|
||||
def hline(x1, x2, y, dash=True):
|
||||
d = ' stroke-dasharray="2 2"' if dash else ""
|
||||
return f'<line x1="{x1:.1f}" y1="{y:.1f}" x2="{x2:.1f}" y2="{y:.1f}" stroke="{BLUE}" stroke-width="0.5"{d}/>'
|
||||
|
||||
|
||||
def vline(x, y1, y2, dash=True):
|
||||
d = ' stroke-dasharray="2 2"' if dash else ""
|
||||
return f'<line x1="{x:.1f}" y1="{y1:.1f}" x2="{x:.1f}" y2="{y2:.1f}" stroke="{BLUE}" stroke-width="0.5"{d}/>'
|
||||
|
||||
|
||||
def dim_v(x, y1, y2, text, side="left"):
|
||||
"""竖向尺寸线,带两端短横。"""
|
||||
t = 2.5
|
||||
lx = x - 4 if side == "left" else x + 4
|
||||
return (vline(x, y1, y2, dash=False) + hline(x - t, x + t, y1, dash=False) + hline(x - t, x + t, y2, dash=False)
|
||||
+ label(lx, (y1 + y2) / 2 + 2.6, text, "end" if side == "left" else "start"))
|
||||
|
||||
|
||||
def dim_h(x1, x2, y, text, below=False):
|
||||
t = 2.5
|
||||
ly = y + 9 if below else y - 3.5
|
||||
return (hline(x1, x2, y, dash=False) + vline(x1, y - t, y + t, dash=False) + vline(x2, y - t, y + t, dash=False)
|
||||
+ label((x1 + x2) / 2, ly, text))
|
||||
|
||||
|
||||
def construction_h():
|
||||
seal, _ = seal_group("clean", 0, SEAL_TOP, SEAL_H)
|
||||
py_top = CN_H * (1 + GAP)
|
||||
x_end = TX + W
|
||||
parts = [seal, f'<g transform="translate({TX:.2f} 0)">{body}</g>']
|
||||
# 文字块上下边界与印章边界
|
||||
parts += [hline(-30, x_end + 12, 0), hline(-30, x_end + 12, H),
|
||||
hline(-30, SEAL_W + 4, SEAL_TOP), hline(-30, SEAL_W + 4, SEAL_TOP + SEAL_H)]
|
||||
parts.append(dim_v(-12, 0, H, "H"))
|
||||
parts.append(dim_v(-26, SEAL_TOP, SEAL_TOP + SEAL_H, "1.02H"))
|
||||
# 印章与文字间距
|
||||
parts.append(dim_h(SEAL_W, TX, H + 18, "0.3 × 印章高", below=True))
|
||||
parts += [vline(SEAL_W, SEAL_TOP + SEAL_H, H + 22), vline(TX, H, H + 22)]
|
||||
# 字标内部:中文字高、间距、拼音高
|
||||
rx = x_end + 10
|
||||
parts.append(dim_v(rx, 0, CN_H, "中文字高 100", side="right"))
|
||||
parts.append(dim_v(rx, CN_H, py_top, "间距 14", side="right"))
|
||||
parts.append(dim_v(rx, py_top, H, "拼音 20", side="right"))
|
||||
# 英文视觉内缩
|
||||
parts += [vline(TX, py_top - 6, H + 4), vline(x_end, py_top - 6, H + 4)]
|
||||
parts.append(vline(TX + FINAL_INSET[0], py_top - 6, H + 4))
|
||||
parts.append(vline(x_end - FINAL_INSET[1], py_top - 6, H + 4))
|
||||
parts.append(label(TX + 2.5, H + 12, f"内缩 {FINAL_INSET[0]:g}", "start", 6.5))
|
||||
parts.append(label(x_end - 2.5, H + 12, f"内缩 {FINAL_INSET[1]:g}", "end", 6.5))
|
||||
vb = (-60, SEAL_TOP - 14, x_end + 150, SEAL_H + 56)
|
||||
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{" ".join(f"{v:.1f}" for v in vb)}">{"".join(parts)}</svg>'
|
||||
|
||||
|
||||
def construction_v():
|
||||
seal_h = W * 0.5
|
||||
seal, _ = seal_group("clean", (W - seal_h) / 2, 0, seal_h)
|
||||
gap = seal_h * 0.18
|
||||
y_wm = seal_h + gap
|
||||
parts = [seal, f'<g transform="translate(0 {y_wm:.2f})">{body}</g>']
|
||||
parts += [vline(0, -8, y_wm + H + 6), vline(W, -8, y_wm + H + 6)]
|
||||
parts.append(dim_h(0, W, -14, "字标宽 W"))
|
||||
parts.append(dim_h((W - seal_h) / 2, (W + seal_h) / 2, seal_h + 5, "0.5W", below=True))
|
||||
parts.append(dim_v(W + 12, seal_h, y_wm, "0.18 × 印章边长", side="right"))
|
||||
parts += [hline((W + seal_h) / 2, W + 14, seal_h), hline(W, W + 14, y_wm)]
|
||||
vb = (-20, -30, W + 130, y_wm + H + 40)
|
||||
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{" ".join(f"{v:.1f}" for v in vb)}">{"".join(parts)}</svg>'
|
||||
|
||||
|
||||
def clearspace():
|
||||
seal, _ = seal_group("clean", 0, SEAL_TOP, SEAL_H)
|
||||
m = H / 4
|
||||
x0, y0 = -m, SEAL_TOP - m
|
||||
x1, y1 = TX + W + m, SEAL_TOP + SEAL_H + m
|
||||
parts = [f'<rect x="{x0:.1f}" y="{y0:.1f}" width="{x1 - x0:.1f}" height="{y1 - y0:.1f}" fill="#E7EDFB" stroke="{BLUE}" stroke-width="0.6" stroke-dasharray="3 2"/>',
|
||||
f'<rect x="0" y="{SEAL_TOP:.1f}" width="{TX + W:.1f}" height="{SEAL_H:.1f}" fill="#fff"/>',
|
||||
seal, f'<g transform="translate({TX:.2f} 0)">{body}</g>']
|
||||
# 四边标注 H/4
|
||||
parts.append(dim_h(x0, 0, SEAL_TOP + SEAL_H / 2, "H/4"))
|
||||
parts.append(dim_h(TX + W, x1, SEAL_TOP + SEAL_H / 2, "H/4"))
|
||||
parts.append(dim_v(TX + W / 2, y0, SEAL_TOP, "H/4", side="right"))
|
||||
parts.append(dim_v(TX + W / 2, SEAL_TOP + SEAL_H, y1, "H/4", side="right"))
|
||||
vb = (x0 - 10, y0 - 10, x1 - x0 + 20, y1 - y0 + 20)
|
||||
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{" ".join(f"{v:.1f}" for v in vb)}">{"".join(parts)}</svg>'
|
||||
|
||||
|
||||
(OUT / "construction-horizontal.svg").write_text(construction_h())
|
||||
(OUT / "construction-vertical.svg").write_text(construction_v())
|
||||
(OUT / "clearspace.svg").write_text(clearspace())
|
||||
print("ok", sorted(p.name for p in OUT.iterdir()))
|
||||
160
docs/design/logo/scripts/wordmark_compare.py
Normal file
160
docs/design/logo/scripts/wordmark_compare.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""生成字标字体对比与印章高度对比(docs/design/logo/compare/)。
|
||||
|
||||
用法:.work/venv/bin/python scripts/wordmark_compare.py
|
||||
字体文件在 .work/fonts/(不进 git),许可证见 compare/index.html 与 logo-spec.md。
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from logo_lib import CN_H, GAP, LOGO, NAVY, PY_CAP, WHITE, lockup, wordmark # noqa: E402
|
||||
|
||||
OUT = LOGO / "compare"
|
||||
OUT.mkdir(exist_ok=True)
|
||||
|
||||
# (编号, 中文字体, 中文字重, 中文字距(em), 拼音字体, 拼音字重, 名称, 许可证, 说明)
|
||||
PAIRS = [
|
||||
("F1", "NotoSerifSC[wght].ttf", 900, 0.00, "Cinzel[wght].ttf", 600, "思源宋体 Black + Cinzel", "OFL", "现用。最粗的宋体,庄重、有分量"),
|
||||
("F2", "NotoSerifSC[wght].ttf", 600, 0.04, "CormorantGaramond[wght].ttf", 600, "思源宋体 SemiBold + Cormorant Garamond", "OFL", "同一家族的细一档,更清秀,和印章的轻重对比更明显"),
|
||||
("F3", "ZhuqueFangsong.ttf", None, 0.06, "CormorantSC-SemiBold.ttf", None, "朱雀仿宋 + Cormorant SC", "OFL", "仿宋,招标文件和公文的正文字体,文书感最强"),
|
||||
("F4", "HuiwenMincho.ttf", None, 0.04, "LibreCaslonText[wght].ttf", 700, "汇文明朝体 + Libre Caslon", "授权需复核", "老铅字印刷的宋体,带油墨感,和印章最搭;授权说明不规范"),
|
||||
("F5", "LXGWWenKai-Medium.ttf", None, 0.06, "EBGaramond[wght].ttf", 600, "霞鹜文楷 + EB Garamond", "OFL", "楷书,书卷气,亲切但不够硬朗"),
|
||||
("F6", "ZCOOLXiaoWei-Regular.ttf", None, 0.04, "MarcellusSC-Regular.ttf", None, "站酷小薇 LOGO 体 + Marcellus SC", "OFL", "为标志设计的宋体,笔画更有个性;但「标」的木字旁写成带钩的连笔,容易被看成「扌」,不建议"),
|
||||
("F7", "NotoSansSC[wght].ttf", 900, 0.02, "Montserrat[wght].ttf", 600, "思源黑体 Black + Montserrat", "OFL", "黑体,现代、稳重,和传统印章形成反差"),
|
||||
("F8", "SmileySans-Oblique.ttf", None, 0.02, "SmileySans-Oblique.ttf", None, "得意黑(中英同一字体)", "OFL", "带倾斜的现代黑体,年轻、有速度感"),
|
||||
]
|
||||
|
||||
|
||||
# 1. 印章高度对比(字体 F1)
|
||||
body, w, h = wordmark(PAIRS[0], NAVY)
|
||||
cn_h = CN_H
|
||||
variants = [
|
||||
("H0", "现状:96%,上下没对齐", lockup(body, w, h, h * 0.961, (h - h * 0.961) / 2 + 0.6)),
|
||||
("H1", "等高对齐:印章上下边对齐中文顶端和拼音底部", lockup(body, w, h, h, 0)),
|
||||
("H2", "等高 + 光学补偿 2%:抵消印章颗粒边缘显虚", lockup(body, w, h, h * 1.02, -h * 0.01)),
|
||||
("H3", "放大到 115%:图形主导", lockup(body, w, h, h * 1.15, -h * 0.075)),
|
||||
("H4", "书法落款式:印章只与中文同高", lockup(body, w, h, cn_h, 0)),
|
||||
]
|
||||
for vid, _, svg in variants:
|
||||
(OUT / f"height-{vid}.svg").write_text(svg)
|
||||
|
||||
# 2. 字体对比(统一用 H2)
|
||||
for p in PAIRS:
|
||||
for color, dark, suffix in ((NAVY, False, ""), (WHITE, True, "-reverse")):
|
||||
body, w, h = wordmark(p, color)
|
||||
(OUT / f"font-{p[0]}{suffix}.svg").write_text(lockup(body, w, h, h * 1.02, -h * 0.01, on_dark=dark))
|
||||
|
||||
# 3. 对比页
|
||||
rows_h = "".join(f'<figure class="tile"><img src="height-{vid}.svg" alt="{t}"><figcaption><b>{vid}</b> {t}</figcaption></figure>'
|
||||
for vid, t, _ in variants)
|
||||
rows_f = "".join(
|
||||
f'<section class="font"><h3>{p[0]} · {p[6]}</h3><p>{p[8]}<span class="lic{" warn" if p[7] != "OFL" else ""}">{p[7]}</span></p>'
|
||||
f'<div class="pair"><div class="tile"><img src="font-{p[0]}.svg" alt="{p[6]} 浅色底"></div>'
|
||||
f'<div class="tile dark"><img src="font-{p[0]}-reverse.svg" alt="{p[6]} 深色底"></div></div>'
|
||||
f'<div class="small"><img src="font-{p[0]}.svg" width="200" alt=""><img src="font-{p[0]}.svg" width="120" alt=""></div></section>'
|
||||
for p in PAIRS)
|
||||
html = f"""<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>字标与比例对比</title><style>
|
||||
:root{{--ink:#1D2733;--sub:#5C6670;--line:#D9DDD8}}
|
||||
*{{box-sizing:border-box}}body{{margin:0;background:#F5F6F3;color:var(--ink);font:15px/1.7 "PingFang SC","Microsoft YaHei",system-ui,sans-serif}}
|
||||
main{{max-width:1120px;margin:0 auto;padding:40px 24px 80px}}h1{{font-size:26px;margin:0 0 4px}}h2{{font-size:19px;margin:44px 0 6px}}h3{{font-size:16px;margin:0}}
|
||||
p.note{{color:var(--sub);margin:0 0 18px;max-width:48em}}
|
||||
.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:14px}}
|
||||
.tile{{background:#fff;border:1px solid var(--line);padding:28px;margin:0}}.tile.dark{{background:#1D2733;border-color:#1D2733}}
|
||||
.tile img{{display:block;width:100%;height:auto}}figcaption{{font-size:13px;color:var(--sub);margin-top:12px}}
|
||||
.font{{background:#fff;border:1px solid var(--line);padding:20px;margin:0 0 16px}}.font p{{margin:2px 0 12px;color:var(--sub);font-size:14px}}
|
||||
.pair{{display:grid;grid-template-columns:1fr 1fr;gap:12px}}.pair .tile{{padding:20px}}
|
||||
.small{{display:flex;gap:24px;align-items:center;margin-top:12px}}
|
||||
.lic{{display:inline-block;margin-left:8px;font-size:12px;padding:0 6px;border:1px solid #2E6B4F;color:#2E6B4F;border-radius:2px}}.lic.warn{{border-color:#A66300;color:#A66300}}
|
||||
@media (max-width:700px){{.pair{{grid-template-columns:1fr}}}}
|
||||
</style></head><body><main>
|
||||
<h1>字标与比例对比</h1><p class="note">两个问题:一、印章和右边文字的高度关系;二、字标字体。所有版本都用同一枚印章,文字已转成路径。</p>
|
||||
<h2>一、印章高度</h2><p class="note">字体统一用 F1。H0 是现在的版本,H2 是建议的版本。</p>
|
||||
<div class="grid">{rows_h}</div>
|
||||
<h2>二、字标字体</h2><p class="note">比例统一用 H2。每组下面一行是 200px 和 120px 宽度的实际效果。</p>
|
||||
{rows_f}
|
||||
</main></body></html>"""
|
||||
(OUT / "index.html").write_text(html)
|
||||
print("ok", len(PAIRS), "fonts,", len(variants), "height variants")
|
||||
|
||||
# 4. 英文字体对比:中文固定为 F2(思源宋体 SemiBold),比例 H2
|
||||
# (编号, 英文字体, 字重, 名称, 对应的经典字体 / 类型, 说明)
|
||||
EN_OPTIONS = [
|
||||
("E0", "CormorantGaramond[wght].ttf", 600, "Cormorant Garamond", "Garamond 类 · 古典衬线", "现选(F2)。纤细优雅,笔画对比高"),
|
||||
("E1", "Cinzel[wght].ttf", 500, "Cinzel", "Trajan 类 · 罗马碑刻大写", "庄重、权威,金融和教育机构常用的气质"),
|
||||
("E2", "MarcellusSC-Regular.ttf", None, "Marcellus SC", "Optima / Albertus 类 · 半衬线", "笔画末端微微张开,介于衬线和无衬线之间,和宋体很合拍"),
|
||||
("E3", "TenorSans-Regular.ttf", None, "Tenor Sans", "Optima 类 · 人文无衬线", "无衬线但有书法韵味,现代而不冷"),
|
||||
("E4", "EBGaramond[wght].ttf", 600, "EB Garamond", "Garamond · 古典衬线", "比 E0 更结实、更正统的 Garamond"),
|
||||
("E5", "LibreCaslonText[wght].ttf", 600, "Libre Caslon", "Caslon · 老派衬线", "英国老字号和出版社的经典选择,沉稳"),
|
||||
("E6", "SourceSerif4[opsz,wght].ttf", 600, "Source Serif 4", "与思源宋体同属 Adobe Source 家族", "和中文出自同一设计体系,最协调、最「工整」"),
|
||||
("E7", "BodoniModa[opsz,wght].ttf", 600, "Bodoni Moda", "Didot / Bodoni 类 · 现代高对比", "时尚、精致,奢侈品和杂志常用"),
|
||||
("E8", "Jost[wght].ttf", 500, "Jost", "Futura 类 · 几何无衬线", "现代、干净,和传统印章形成反差"),
|
||||
]
|
||||
cn = PAIRS[1] # F2
|
||||
for eid, ef, ew, *_ in EN_OPTIONS:
|
||||
pair = (eid, cn[1], cn[2], cn[3], ef, ew)
|
||||
for color, dark, suffix in ((NAVY, False, ""), (WHITE, True, "-reverse")):
|
||||
body, w, h = wordmark(pair, color)
|
||||
(OUT / f"en-{eid}{suffix}.svg").write_text(lockup(body, w, h, h * 1.02, -h * 0.01, on_dark=dark))
|
||||
|
||||
rows_e = "".join(
|
||||
f'<section class="font"><h3>{e[0]} · {e[3]}</h3><p>{e[4]}。{e[5]}<span class="lic">OFL</span></p>'
|
||||
f'<div class="pair"><div class="tile"><img src="en-{e[0]}.svg" alt="{e[3]} 浅色底"></div>'
|
||||
f'<div class="tile dark"><img src="en-{e[0]}-reverse.svg" alt="{e[3]} 深色底"></div></div>'
|
||||
f'<div class="small"><img src="en-{e[0]}.svg" width="200" alt=""><img src="en-{e[0]}.svg" width="120" alt=""></div></section>'
|
||||
for e in EN_OPTIONS)
|
||||
(OUT / "english.html").write_text(html.split("<h1>")[0] + f"""<h1>英文字体对比(H2 + F2)</h1>
|
||||
<p class="note">中文固定为思源宋体 SemiBold,比例固定为 H2,只换下方英文。全部是 Google Fonts 上的 SIL OFL 1.1 字体,可以免费商用、可以用在 logo 里。</p>
|
||||
{rows_e}
|
||||
</main></body></html>""")
|
||||
print("ok", len(EN_OPTIONS), "english options")
|
||||
|
||||
# 5. 英文特写:只显示拼音这一行,放大比较字形
|
||||
py_top, py_h = CN_H * (1 + GAP), CN_H * PY_CAP
|
||||
for eid, ef, ew, *_ in EN_OPTIONS:
|
||||
body, w, h = wordmark((eid, cn[1], cn[2], cn[3], ef, ew), NAVY)
|
||||
(OUT / f"en-{eid}-detail.svg").write_text(
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 {py_top - 3:.1f} {w + 4:.1f} {py_h + 6:.1f}">{body}</svg>')
|
||||
detail = "".join(f'<figure class="tile"><img src="en-{e[0]}-detail.svg" alt="{e[3]} 特写"><figcaption><b>{e[0]}</b> {e[3]}</figcaption></figure>'
|
||||
for e in EN_OPTIONS)
|
||||
page = (OUT / "english.html").read_text()
|
||||
page = page.replace("</main>", f'<h2>英文特写</h2><p class="note">只看拼音这一行,放大比较字形细节。</p><div class="grid">{detail}</div></main>')
|
||||
(OUT / "english.html").write_text(page)
|
||||
print("ok detail")
|
||||
|
||||
# 6. 英文视觉对齐补偿(H2 + F2 + E5)
|
||||
# 实测:中文两端只有细横画尖,英文两端是 B、I 的粗竖画;按墨迹浓度算英文左超 6.5、右超 7.7(中文字高 = 100)
|
||||
E5 = next(e for e in EN_OPTIONS if e[0] == "E5")
|
||||
ALIGN = [
|
||||
("C0", (0.0, 0.0), "不补偿:几何两端对齐(现状)"),
|
||||
("C1", (3.5, 4.0), "补偿一半:左 3.5、右 4.0"),
|
||||
("C2", (5.0, 5.8), "补偿约 3/4:左 5.0、右 5.8"),
|
||||
("C3", (6.5, 7.7), "完全补偿:按墨迹浓度边缘对齐,左 6.5、右 7.7"),
|
||||
]
|
||||
CN_VISUAL = (8.6, 315.4) # 中文「浓度 ≥ 15%」的视觉边缘(实测)
|
||||
pair_e5 = ("E5", cn[1], cn[2], cn[3], E5[1], E5[2])
|
||||
for cid, ins, _ in ALIGN:
|
||||
for color, dark, suffix in ((NAVY, False, ""), (WHITE, True, "-reverse")):
|
||||
body, w, h = wordmark(pair_e5, color, inset=ins)
|
||||
(OUT / f"align-{cid}{suffix}.svg").write_text(lockup(body, w, h, h * 1.02, -h * 0.01, on_dark=dark))
|
||||
# 带辅助线的字标特写:红线 = 中文视觉边缘,灰线 = 中文几何边界
|
||||
body, w, h = wordmark(pair_e5, NAVY, inset=ins)
|
||||
guides = (f'<line x1="0" y1="-4" x2="0" y2="{h + 4:.1f}" stroke="#999" stroke-width="0.6" stroke-dasharray="2 2"/>'
|
||||
f'<line x1="{w:.1f}" y1="-4" x2="{w:.1f}" y2="{h + 4:.1f}" stroke="#999" stroke-width="0.6" stroke-dasharray="2 2"/>'
|
||||
f'<line x1="{CN_VISUAL[0]}" y1="-4" x2="{CN_VISUAL[0]}" y2="{h + 4:.1f}" stroke="#D9291C" stroke-width="0.6"/>'
|
||||
f'<line x1="{CN_VISUAL[1]}" y1="-4" x2="{CN_VISUAL[1]}" y2="{h + 4:.1f}" stroke="#D9291C" stroke-width="0.6"/>')
|
||||
(OUT / f"align-{cid}-guides.svg").write_text(
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 {w + 16:.1f} {h + 16:.1f}"><rect x="-8" y="-8" width="{w + 16:.1f}" height="{h + 16:.1f}" fill="#fff"/>{body}{guides}</svg>')
|
||||
|
||||
rows_a = "".join(
|
||||
f'<section class="font"><h3>{c[0]} · {c[2]}</h3>'
|
||||
f'<div class="pair"><div class="tile"><img src="align-{c[0]}.svg" alt="{c[0]} 浅色底"></div>'
|
||||
f'<div class="tile"><img src="align-{c[0]}-guides.svg" alt="{c[0]} 辅助线"></div></div>'
|
||||
f'<div class="small"><img src="align-{c[0]}.svg" width="240" alt=""><img src="align-{c[0]}.svg" width="160" alt=""></div></section>'
|
||||
for c in ALIGN)
|
||||
(OUT / "alignment.html").write_text(html.split("<h1>")[0] + f"""<h1>英文视觉对齐补偿(H2 + F2 + E5)</h1>
|
||||
<p class="note">右侧特写的辅助线:灰色虚线是中文的几何边界(最外的墨迹),红线是中文的视觉边缘(墨迹浓度达到行高 15% 的位置)。
|
||||
补偿越多,英文越往红线靠。建议不看辅助线、只看左侧的整体效果做判断。</p>
|
||||
{rows_a}
|
||||
</main></body></html>""")
|
||||
print("ok alignment")
|
||||
Reference in New Issue
Block a user