mirror of
https://github.com/nlohmann/json.git
synced 2026-10-28 16:55:41 +00:00
docs: new design for the documentation site (#5934)
* docs: new design for the documentation site Apply the brand to the Material for MkDocs site: Inter and JetBrains Mono served by the site (upright and real italic), brand colours, blue header band with the syntax pattern, wordmark in the header, new home page (hero, cards, animated feature tour), 404 page, polish for tabs, search, footer, focus rings, tables and code blocks, and theme features (code copy, previous/next footer, TOC follow, search suggestions). Draw the number railroad diagram, the parser callback events and the iterator illustrations as inline SVGs that follow the light and dark scheme. The generator scripts are in docs/mkdocs/scripts/figures; the feature slideshow (json-light.svg, json-dark.svg, replacing json.gif on the home page) is built by docs/mkdocs/scripts/slideshow. Decision nodes in five flowcharts become hexagons, which need far less room. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: add figures, table captions and a macro chooser - Figures with numbered captions: SAX stepper, json_view layout and edit stepper, exception map, JSON pointer and patch walkers, QA pipeline, scaling and binary size charts, roadmap lanes, release timeline, integration decision tree and namespace builder - Table captions (nj-table-caption) on 9 pages - Compile times: modules and PCH charts, filesystem section, re-measured Figure 1, links between features/modules.md and compile_times.md - Interactive macro chooser for features/arbitrary_types.md - Link check: exclude URLs that answer the plugin with 400/401/419 or are dead (customers.md) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: macro chooser generates adl_serializer for non-default-constructible types Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: cross-link concept pages and API pages in See also sections Add See also links in both directions between the concept pages (features, home, integration) and the API pages, write API links in code font, give every entry a comment after a dash, and show the callback event figure on the parser callbacks page. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: address review comments on the new design - Home: add a "Latest release" button - Sponsors: update the list and group it by tier - Features: list all feature pages on the index page - Releases: add highlights per release, link the 4.0 roadmap, add the first commit to the timeline - Exceptions: prefix all example messages with the exception name - API: no code font for format names in the See also links of the to_/from_ pages - Merge Patch: show the document before, the patch, and the result - Replace "--" with typographic dashes, mark external links with an icon, center Mermaid diagrams Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
1 parent
2bb2feeef4
commit
a47170346a
417 files changed
+11100
-3041
No files matched your search
Executable
+461
@@ -0,0 +1,461 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.9"
|
||||
# dependencies = ["fonttools==4.67.0", "uharfbuzz==0.57.0"]
|
||||
# ///
|
||||
"""Animated SVG slideshow prototype for nlohmann/json, all text outlined (harfbuzz shaping, glyphs as <path> + <use>).
|
||||
|
||||
Usage: uv run --script build.py OUT.svg [seconds-per-slide] [auto|light|dark]
|
||||
|
||||
auto (the default) follows prefers-color-scheme. light and dark fix the colours, for pages with their own theme
|
||||
toggle (MkDocs Material), which an <img> cannot see.
|
||||
|
||||
The pinned versions keep the output byte-identical; a different harfbuzz may shape slightly differently.
|
||||
"""
|
||||
import re, sys, os
|
||||
import uharfbuzz as hb
|
||||
from fontTools.pens.svgPathPen import SVGPathPen
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
FONTS = os.path.join(HERE, "fonts")
|
||||
W, H = 1200, 675
|
||||
TOTAL = 0 # number of slides, set once slides.md is read (for the "02 / 20" labels and the progress segments)
|
||||
|
||||
# ---------------------------------------------------------------- glyphs
|
||||
|
||||
class Face:
|
||||
def __init__(self, file, key, feats=None):
|
||||
blob = hb.Blob.from_file_path(os.path.join(FONTS, file))
|
||||
self.face = hb.Face(blob)
|
||||
self.upem = self.face.upem
|
||||
self.key = key
|
||||
self.feats = feats or {}
|
||||
|
||||
def font(self, var):
|
||||
f = hb.Font(self.face)
|
||||
f.set_variations(var)
|
||||
return f
|
||||
|
||||
|
||||
INTER = Face("Inter.ttf", "i", {"cv11": True, "ss03": True, "kern": True})
|
||||
MONO = Face("JetBrainsMono.ttf", "m", {"calt": False, "liga": False})
|
||||
|
||||
GLYPHS = {} # (face, var, gid) -> id
|
||||
DEFS = []
|
||||
|
||||
|
||||
def gid_ref(face, font, var, gid):
|
||||
k = (face.key, tuple(sorted(var.items())), gid)
|
||||
if k not in GLYPHS:
|
||||
pen = SVGPathPen(None, ntos=lambda v: str(round(v)))
|
||||
font.draw_glyph_with_pen(gid, pen)
|
||||
d = pen.getCommands()
|
||||
gidname = f"g{len(GLYPHS):x}"
|
||||
GLYPHS[k] = gidname if d else None
|
||||
if d:
|
||||
DEFS.append(f'<path id="{gidname}" d="{d}"/>')
|
||||
return GLYPHS[k]
|
||||
|
||||
|
||||
def shape(face, text, wght, size, track=0.0):
|
||||
if not text:
|
||||
return [], 0, face.upem
|
||||
var = {"wght": wght}
|
||||
if face is INTER:
|
||||
var["opsz"] = max(14, min(32, size))
|
||||
font = face.font(var)
|
||||
buf = hb.Buffer()
|
||||
buf.add_str(text)
|
||||
buf.guess_segment_properties()
|
||||
hb.shape(font, buf, face.feats)
|
||||
out, x = [], 0
|
||||
tr = track * face.upem
|
||||
for info, pos in zip(buf.glyph_infos, buf.glyph_positions):
|
||||
out.append((gid_ref(face, font, var, info.codepoint), x + pos.x_offset, pos.y_offset))
|
||||
x += pos.x_advance + tr
|
||||
return out, x - tr, face.upem
|
||||
|
||||
|
||||
def width(text, face=INTER, wght=400, size=16, track=0.0):
|
||||
_, w, upem = shape(face, text, wght, size, track)
|
||||
return w * size / upem
|
||||
|
||||
|
||||
def run(text, x, y, size, wght=400, cls="tx", face=INTER, track=0.0):
|
||||
"""One line of text, baseline at y. Returns (svg, width_px)."""
|
||||
glyphs, w, upem = shape(face, text, wght, size, track)
|
||||
s = size / upem
|
||||
uses = "".join(f'<use href="#{g}" x="{gx}"' + (f' y="{-gy}"' if gy else "") + "/>"
|
||||
for g, gx, gy in glyphs if g)
|
||||
return (f'<g class="{cls}" transform="translate({x:g} {y:g}) scale({s:.6g} {-s:.6g})">{uses}</g>', w * s)
|
||||
|
||||
|
||||
def runs(parts, x, y, size, face=INTER, track=0.0):
|
||||
"""Several (text, wght, cls) pieces on one line."""
|
||||
out = []
|
||||
for text, wght, cls in parts:
|
||||
svg, w = run(text, x, y, size, wght, cls, face, track)
|
||||
out.append(svg)
|
||||
x += w + track * size
|
||||
return "".join(out), x
|
||||
|
||||
|
||||
def wrap(text, maxw, size, wght=400):
|
||||
lines = _wrap(text, maxw, size, wght)
|
||||
# no one-word widow: pull the last word of the previous line down if it still fits
|
||||
if len(lines) > 1 and " " not in lines[-1] and " " in lines[-2]:
|
||||
head, last = lines[-2].rsplit(" ", 1)
|
||||
if width(last + " " + lines[-1], size=size, wght=wght) <= maxw:
|
||||
lines[-2:] = [head, last + " " + lines[-1]]
|
||||
return lines
|
||||
|
||||
|
||||
def _wrap(text, maxw, size, wght=400):
|
||||
lines, cur = [], ""
|
||||
for word in text.split(" "):
|
||||
t = (cur + " " + word).strip()
|
||||
if cur and width(t, size=size, wght=wght) > maxw:
|
||||
lines.append(cur)
|
||||
cur = word
|
||||
else:
|
||||
cur = t
|
||||
return lines + [cur]
|
||||
|
||||
# ---------------------------------------------------------------- pieces
|
||||
|
||||
|
||||
def wordmark(x, y, size, on="page"):
|
||||
"""The wordmark with hand kerning around the slash (see the logo in the brand kit). Below 32 px it switches to the
|
||||
small optical master (nlohmann 250 / slash 450), because Thin falls apart at footer size."""
|
||||
a, sl = ("wm-a", "wm-s") if on == "page" else ("on-a", "on-s")
|
||||
small = size < 32
|
||||
w = (250, 450, 800) if small else (100, 400, 800)
|
||||
k = (0.004, 0.024) if small else (-0.006, 0.012)
|
||||
out = []
|
||||
for i, (t, wg, c) in enumerate([("nlohmann", w[0], a), ("/", w[1], sl), ("json", w[2], a)]):
|
||||
svg, wd = run(t, x, y, size, wg, c, track=-0.02)
|
||||
out.append(svg)
|
||||
x += wd - 0.02 * size + (k[i] * size if i < 2 else 0)
|
||||
return "".join(out), x
|
||||
|
||||
|
||||
KW = {"using", "auto", "return", "const", "struct", "for", "override"}
|
||||
TYPES = {"int", "void", "bool", "double", "char", "float"}
|
||||
CONSTANTS = {"true", "false", "nullptr"}
|
||||
TOKEN = re.compile(r'(//.*$)|(R"\(.*?\)"|R"\(.*$|"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)\')|(<[\w/.]+>(?=\s*$))|(#\w+)'
|
||||
r'|(\b\d+(?:\.\d+)?\b)|(\b[A-Za-z_]\w*\b)|(\s+)|(.)')
|
||||
|
||||
|
||||
def code_block(lines, x, y, size, lh):
|
||||
"""Lines of C++; a raw string literal may span lines."""
|
||||
out, in_raw = [], False
|
||||
for line in lines:
|
||||
if line:
|
||||
svg, in_raw = code_line(line, x, y, size, in_raw)
|
||||
out.append(svg)
|
||||
y += lh
|
||||
else:
|
||||
y += lh * BLANK
|
||||
return "".join(out)
|
||||
|
||||
|
||||
BLANK = 0.55 # an empty code line is a paragraph break, not a full line
|
||||
|
||||
|
||||
def code_height(lines, size, lh):
|
||||
n_full = sum(1 for l in lines if l)
|
||||
n_blank = len(lines) - n_full
|
||||
return lh * (n_full - 1) + lh * BLANK * n_blank + size
|
||||
|
||||
|
||||
def code_line(line, x, y, size, in_raw=False):
|
||||
if line.startswith("$ "):
|
||||
return runs([("$ ", 400, "tx3"), (line[2:], 400, "tx")], x, y, size, MONO)[0], False
|
||||
out, cx = [], x
|
||||
if in_raw:
|
||||
end = line.find(')"')
|
||||
head = line if end < 0 else line[:end + 2]
|
||||
svg, w = run(head, cx, y, size, 400, "str", MONO)
|
||||
out.append(svg)
|
||||
if end < 0:
|
||||
return "".join(out), True
|
||||
cx += w
|
||||
line = line[end + 2:]
|
||||
in_raw = False
|
||||
for m in TOKEN.finditer(line):
|
||||
com, st, inc, pp, num, ident, ws, other = m.groups()
|
||||
t = m.group(0)
|
||||
if st and st.startswith('R"(') and not st.endswith(')"'):
|
||||
in_raw = True
|
||||
face, wght, cls = MONO, 400, "tx"
|
||||
if com:
|
||||
cls = "com" # upright: JetBrains Mono Italic's cursive f reads as a florin at README size
|
||||
elif st or inc:
|
||||
cls = "str"
|
||||
elif pp:
|
||||
cls = "pp"
|
||||
elif ident in KW:
|
||||
wght, cls = 600, "kw"
|
||||
elif ident in TYPES:
|
||||
cls = "ty"
|
||||
elif num or ident in CONSTANTS:
|
||||
cls = "num"
|
||||
elif ident and line[m.end():m.end() + 1] == "(":
|
||||
cls = "fn" # a call or definition: name directly followed by "("
|
||||
svg, w = run(t, cx, y, size, wght, cls, face)
|
||||
if not ws:
|
||||
out.append(svg)
|
||||
cx += w
|
||||
return "".join(out), in_raw
|
||||
|
||||
|
||||
SEG, SEG_GAP = 20, 5
|
||||
|
||||
|
||||
def progress(idx, n_proto, y):
|
||||
"""TOTAL segments at the bottom right; earlier slides full, current one fills over the slide's time."""
|
||||
seg, gap = SEG, SEG_GAP
|
||||
x0 = W - 64 - TOTAL * seg - (TOTAL - 1) * gap
|
||||
out = []
|
||||
for k in range(TOTAL):
|
||||
x = x0 + k * (seg + gap)
|
||||
out.append(f'<rect class="seg" x="{x}" y="{y}" width="{seg}" height="3" rx="1.5"/>')
|
||||
if k < idx:
|
||||
out.append(f'<rect class="seg-on" x="{x}" y="{y}" width="{seg}" height="3" rx="1.5"/>')
|
||||
elif k == idx:
|
||||
out.append(f'<rect class="seg-on fill" x="{x}" y="{y}" width="{seg}" height="3" rx="1.5"/>')
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def pattern_def():
|
||||
tile = [("{", 14, 34), ("]", 92, 22), (":", 136, 60), ("[", 52, 82), (",", 118, 112),
|
||||
("}", 20, 140), (":", 78, 150), ("{", 140, 158)]
|
||||
glyphs = "".join(run(c, x, y, 26, 600, "pat", MONO)[0] for c, x, y in tile)
|
||||
return f'<pattern id="pat" width="160" height="160" patternUnits="userSpaceOnUse">{glyphs}</pattern>'
|
||||
|
||||
# ---------------------------------------------------------------- slides
|
||||
|
||||
|
||||
CODE_SIZES = []
|
||||
SLOGAN = "What if JSON was part of modern C++?"
|
||||
|
||||
|
||||
def band_bg():
|
||||
"""Hero gradient plus the syntax pattern, faded out towards the text side (left)."""
|
||||
return ('<rect width="1200" height="675" fill="url(#hero)"/>'
|
||||
'<rect width="1200" height="675" fill="url(#pat)" mask="url(#patfade)"/>')
|
||||
|
||||
|
||||
def footer_band(idx, n, left=None):
|
||||
out = []
|
||||
if left:
|
||||
out.append(run(left, 64, FOOT_Y, 20, 600, "on-tx")[0])
|
||||
out.append(progress(idx, n, FOOT_Y - 7.5).replace('class="seg"', 'class="seg on-band"').replace('class="seg-on', 'class="seg-on on-band'))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
FOOT_Y = 640 # one footer baseline on every slide: wordmark or URL left, progress right
|
||||
|
||||
|
||||
def slide_title(idx, n):
|
||||
out = [band_bg()]
|
||||
x = 96
|
||||
label = "VERSION 3.13.0"
|
||||
lw = width(label, MONO, 500, 15, 0.04)
|
||||
out.append(f'<rect class="chip" x="{x}" y="186" width="{lw + 28:.1f}" height="34" rx="4"/>')
|
||||
out.append(run(label, x + 14, 208.5, 15, 500, "on-tx", MONO, 0.04)[0])
|
||||
out.append(wordmark(x - 6, 336, 112, on="band")[0])
|
||||
out.append(runs([("// ", 400, "w70"), (SLOGAN, 400, "on-s")], x, 404, 26, MONO)[0])
|
||||
out.append(footer_band(idx, n, "github.com/nlohmann/json"))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def slide_feature(idx, n, title, bullets, code, badge=None):
|
||||
out = ['<rect class="bg" width="1200" height="675"/>']
|
||||
x, top, bottom = 64, 136, 596
|
||||
# title row: 48/800, -3 %
|
||||
svg, tw = run(title, x - 2, 98, 48, 800, "tx", track=-0.03)
|
||||
out.append(svg)
|
||||
if badge:
|
||||
cx = x + tw + 16
|
||||
cw = width(badge, MONO, 600, 13, 0.06) + 18
|
||||
out.append(f'<rect class="red" x="{cx:.1f}" y="68.5" width="{cw:.1f}" height="26" rx="4"/>')
|
||||
out.append(run(badge, cx + 9, 86.2, 13, 600, "on-tx", MONO, 0.06)[0])
|
||||
# code panel: fixed left edge, fixed width, fixed top on every slide; only its height follows the code
|
||||
pad, lf = 28, 1.45
|
||||
px, pw = 488, 1136 - 488
|
||||
em = max(width(l, MONO, 400, 1) for l in code)
|
||||
cs = 19.0
|
||||
while cs > 14 and (em * cs > pw - 2 * pad or code_height(code, cs, lf * cs) > bottom - top - 2 * pad):
|
||||
cs -= 0.5
|
||||
CODE_SIZES.append((title, cs))
|
||||
if cs < 18:
|
||||
print(f" warning: {title!r} code at {cs} px ({em * 19:.0f} px wide at 19 px)", file=sys.stderr)
|
||||
lh = lf * cs
|
||||
ph = max(2 * pad + code_height(code, cs, lh), 240)
|
||||
out.append(f'<rect class="panel" x="{px}" y="{top}" width="{pw}" height="{ph:.0f}" rx="6"/>')
|
||||
first = top + pad + cs * 0.78
|
||||
out.append(code_block(code, px + pad, first, cs, lh))
|
||||
# bullets: 24/400 on 34, first baseline level with the first code baseline
|
||||
colw = px - x - 40
|
||||
by = first
|
||||
for b in bullets:
|
||||
out.append(f'<rect class="blk" x="{x}" y="{by - 10.6:.1f}" width="8" height="8" rx="1.5"/>')
|
||||
for line in wrap(b, colw - 24, 24):
|
||||
out.append(run(line, x + 24, by, 24, 400, "tx2")[0])
|
||||
by += 34
|
||||
by += 16
|
||||
# footer: small-master wordmark left, progress right, no rule, no numeral
|
||||
out.append(wordmark(64, FOOT_Y, 22)[0])
|
||||
out.append(progress(idx, n, FOOT_Y - 7.5))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def slide_closing(idx, n):
|
||||
out = [band_bg()]
|
||||
x = 96
|
||||
out.append(wordmark(x - 4, 146, 80, on="band")[0])
|
||||
out.append(runs([("// ", 400, "w70"), (SLOGAN, 400, "on-s")], x, 200, 22, MONO)[0])
|
||||
info = [
|
||||
("{", None),
|
||||
(' "copyright": ', "(c) 2013-2026 Niels Lohmann"),
|
||||
(' "license": ', "MIT"),
|
||||
(' "docs": ', "json.nlohmann.me"),
|
||||
(' "github": ', "github.com/nlohmann/json"),
|
||||
(' "sponsor": ', "github.com/sponsors/nlohmann"),
|
||||
(' "mail": ', "mail@nlohmann.me"),
|
||||
("}", None),
|
||||
]
|
||||
cs, lh, pad = 21, 33, 28
|
||||
py = 244
|
||||
pw = max(width(k + '"' + (v or "") + '",', MONO, 400, cs) for k, v in info) + 2 * pad
|
||||
out.append(f'<rect class="on-panel" x="{x}" y="{py}" width="{pw:.0f}" height="{2 * pad + lh * (len(info) - 1) + cs:.0f}" rx="6"/>')
|
||||
y = py + pad + cs * 0.8
|
||||
for k, (key, val) in enumerate(info):
|
||||
if val is None:
|
||||
out.append(run(key, x + pad, y, cs, 400, "on-tx", MONO)[0])
|
||||
else:
|
||||
comma = "," if k < len(info) - 2 else ""
|
||||
out.append(runs([(key, 400, "on-tx"), (f'"{val}"', 400, "on-s"), (comma, 400, "on-tx")], x + pad, y, cs, MONO)[0])
|
||||
y += lh
|
||||
out.append(footer_band(idx, n))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def load_slides(path):
|
||||
"""Feature slides from Markdown: '# Title {badge: TEXT}', '- bullet' lines, one fenced code block."""
|
||||
text = re.sub(r"<!--.*?-->", "", open(path).read(), flags=re.S)
|
||||
slides, cur, in_code = [], None, False
|
||||
for no, line in enumerate(text.split("\n"), 1):
|
||||
if in_code:
|
||||
if line.startswith("```"):
|
||||
in_code = False
|
||||
else:
|
||||
cur["code"].append(line)
|
||||
elif line.startswith("```"):
|
||||
in_code = True
|
||||
elif line.startswith("# "):
|
||||
title, badge = line[2:].strip(), None
|
||||
m = re.search(r"\s*\{badge:\s*(.*?)\}$", title)
|
||||
if m:
|
||||
title, badge = title[:m.start()], m.group(1)
|
||||
cur = {"title": title, "bullets": [], "code": [], "badge": badge}
|
||||
slides.append(cur)
|
||||
elif line.startswith("- ") and cur:
|
||||
cur["bullets"].append(line[2:].strip())
|
||||
elif line[:1] in " \t" and line.strip() and cur and cur["bullets"]:
|
||||
cur["bullets"][-1] += " " + line.strip()
|
||||
elif line.strip():
|
||||
raise SystemExit(f"{path}:{no}: unexpected line: {line!r}")
|
||||
for sl in slides:
|
||||
while sl["code"] and not sl["code"][-1].strip():
|
||||
sl["code"].pop()
|
||||
if not sl["code"]:
|
||||
raise SystemExit(f"{path}: slide {sl['title']!r} has no code block")
|
||||
return slides
|
||||
|
||||
|
||||
FEATURES = load_slides(os.path.join(HERE, "slides.md"))
|
||||
|
||||
|
||||
def _feature(f):
|
||||
return lambda i, n: slide_feature(i, n, f["title"], f["bullets"], f["code"], f["badge"])
|
||||
|
||||
|
||||
SLIDES = [slide_title] + [_feature(f) for f in FEATURES] + [slide_closing]
|
||||
TOTAL = len(SLIDES)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- assemble
|
||||
|
||||
|
||||
def build(sec):
|
||||
n = len(SLIDES)
|
||||
T = n * sec
|
||||
fade = 0.6
|
||||
a, b, c = fade / T * 100, sec / T * 100, (sec + fade) / T * 100
|
||||
def cls(i):
|
||||
return "s s0" if i == 0 else "s sl" if i == n - 1 else "s"
|
||||
bodies = [f'<g class="{cls(i)}" style="animation-delay:{i * sec:g}s">{fn(i, n)}</g>' for i, fn in enumerate(SLIDES)]
|
||||
css = f"""
|
||||
.bg{{fill:#fff}} .tx{{fill:#0f1419}} .tx2{{fill:#3d444f}} .tx3{{fill:#8a919c}} .com{{fill:#646d7a}}
|
||||
.bl{{fill:#1747a6}} .blk{{fill:#1747a6}} .kw{{fill:#1747a6}} .ty{{fill:#0d6e72}} .fn{{fill:#6b3fb8}} .str{{fill:#23793a}} .num{{fill:#b04a07}} .pp{{fill:#a3247a}}
|
||||
.panel{{fill:#f5f7fa;stroke:#e3e6eb;stroke-width:1}} .rule{{stroke:#e3e6eb;stroke-width:1}}
|
||||
.wm-a{{fill:#0f1419}} .wm-s{{fill:#1747a6}}
|
||||
.on-a,.on-tx{{fill:#fff}} .red{{fill:#d93a3f}} .on-panel{{fill:#fff;fill-opacity:.08;stroke:#fff;stroke-opacity:.16}} .on-s{{fill:#bcd3fb}} .w70{{fill:#fff;fill-opacity:.7}} .on-tx2{{fill:#dbe6fb}} .chip{{fill:#fff;fill-opacity:.14}}
|
||||
.pat{{fill:#fff;fill-opacity:.09}}
|
||||
.h1{{stop-color:#1d55c4}} .h2{{stop-color:#123a8a}}
|
||||
.seg{{fill:#e3e6eb}} .seg-on{{fill:#1747a6}}
|
||||
.seg.on-band{{fill:#fff;fill-opacity:.25}} .seg-on.on-band{{fill:#fff;fill-opacity:1}}
|
||||
@media (prefers-color-scheme:dark){{
|
||||
.bg{{fill:#0f1218}} .tx,.wm-a{{fill:#f2f4f7}} .tx2{{fill:#c3c8d1}} .tx3{{fill:#7b828e}} .com{{fill:#8b929e}}
|
||||
.bl,.blk,.wm-s,.seg-on,.kw{{fill:#7aa7f5}} .ty{{fill:#4fd1c5}} .fn{{fill:#c4a3ff}} .str{{fill:#7ed491}} .num{{fill:#f5a65b}} .pp{{fill:#f48fc8}}
|
||||
.panel{{fill:#171b23;stroke:#272c36}} .rule{{stroke:#272c36}} .seg{{fill:#272c36}}
|
||||
.red{{fill:#c8383d}} .h1{{stop-color:#163f93}} .h2{{stop-color:#0c2556}}
|
||||
}}
|
||||
.s{{opacity:0;animation:show {T:g}s linear infinite backwards}}
|
||||
.fill{{transform-box:fill-box;transform-origin:left;animation:fill {T:g}s linear infinite backwards;animation-delay:inherit}}
|
||||
.s0{{animation-name:show0}} .sl{{animation-name:showl}}
|
||||
@keyframes show{{0%{{opacity:0;animation-timing-function:cubic-bezier(.2,.6,.2,1)}} {a:.4f}%{{opacity:1}} {c:.4f}%{{opacity:1}} {c + .01:.4f}%{{opacity:0}} 100%{{opacity:0}}}}
|
||||
@keyframes show0{{0%{{opacity:1}} {c:.4f}%{{opacity:1}} {c + .01:.4f}%{{opacity:0}} 100%{{opacity:0}}}}
|
||||
@keyframes showl{{0%{{opacity:0;animation-timing-function:cubic-bezier(.2,.6,.2,1)}} {a:.4f}%{{opacity:1}} {b:.4f}%{{opacity:1}} {c:.4f}%{{opacity:0}} 100%{{opacity:0}}}}
|
||||
@keyframes fill{{0%{{transform:scaleX(0)}} {b:.4f}%{{transform:scaleX(1)}} 100%{{transform:scaleX(1)}}}}
|
||||
@media (prefers-reduced-motion:reduce){{.s,.fill{{animation:none}} .s:first-of-type{{opacity:1}}}}
|
||||
"""
|
||||
css = re.sub(r"\n\s*", "", css)
|
||||
pat = pattern_def()
|
||||
return (f'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 {W} {H}" width="{W}" height="{H}" role="img">'
|
||||
f'<title>JSON for Modern C++: feature slideshow (animated, {n} slides, {sec:g} s each)</title>'
|
||||
f'<style>{css}</style>'
|
||||
f'<defs>{"".join(DEFS)}{pat}'
|
||||
f'<linearGradient id="hero" gradientTransform="rotate(70 .5 .5)"><stop class="h1" offset="0"/><stop class="h2" offset="1"/></linearGradient>'
|
||||
f'<linearGradient id="fadeg" x1="0" x2="1" y1="0" y2="0"><stop offset=".25" stop-color="#fff" stop-opacity="0"/><stop offset=".85" stop-color="#fff" stop-opacity="1"/></linearGradient>'
|
||||
f'<mask id="patfade" maskUnits="userSpaceOnUse" x="0" y="0" width="{W}" height="{H}"><rect width="{W}" height="{H}" fill="url(#fadeg)"/></mask></defs>'
|
||||
f'<rect class="bg" width="{W}" height="{H}"/>'
|
||||
f'{"".join(bodies)}</svg>')
|
||||
|
||||
|
||||
def fix_scheme(svg, scheme):
|
||||
"""Drop the dark-mode block (light) or apply it unconditionally (dark)."""
|
||||
if scheme == "auto":
|
||||
return svg
|
||||
start = svg.index("@media (prefers-color-scheme:dark){")
|
||||
i, depth = svg.index("{", start), 0
|
||||
for j in range(i, len(svg)):
|
||||
depth += {"{": 1, "}": -1}.get(svg[j], 0)
|
||||
if depth == 0:
|
||||
break
|
||||
inner = svg[i + 1:j] if scheme == "dark" else ""
|
||||
return svg[:start] + inner + svg[j + 1:]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out = sys.argv[1]
|
||||
sec = float(sys.argv[2]) if len(sys.argv) > 2 else 15
|
||||
scheme = sys.argv[3] if len(sys.argv) > 3 else "auto"
|
||||
if scheme not in ("auto", "light", "dark"):
|
||||
sys.exit(f"unknown colour scheme {scheme!r}: use auto, light or dark")
|
||||
svg = fix_scheme(build(sec), scheme)
|
||||
# defs are appended while slides render, so build the slides first (done inside build via f-string order)
|
||||
open(out, "w").write(svg)
|
||||
print(out, len(svg.encode()) // 1024, "KB,", len(GLYPHS), "glyphs; code px:", " ".join(f"{c:g}" for _, c in CODE_SIZES))
|
||||
Reference in new issue
Block a user