[verified] feat: expand public animation library across 12 industries

This commit is contained in:
jarvis-at-skic committed 2026-10-25 18:14:35 +00:00
1 parent 3bb9555e6a
commit ecbbcf8ba0
144 files changed
+61323 -187

No files matched your search

+118 -16
View File
@@ -1,37 +1,139 @@
"""Build an offline hierarchy; individual pages embed every runtime dependency."""
"""Generate catalog wrappers and self-contained offline pages (Python stdlib only)."""
from pathlib import Path
import argparse
import hashlib
import html
import json
import re
root = Path(__file__).resolve().parents[1]
source = root / 'assets/animejs'
vendor = root / 'assets/vendor/anime.umd.min.js'
entries = [entry for entry in json.loads((root / 'vendor-manifest.json').read_text(encoding='utf-8'))
if entry['path'] == 'assets/vendor/anime.umd.min.js']
VERSION = '0.6.0'
BASELINE = {
'oil-gas': ['separator-upset', 'pipeline-reroute'],
'pharma': ['batch-genealogy', 'cold-chain-quarantine'],
'government': ['permit-review-loop', 'budget-execution'],
'retail': ['inventory-reservation', 'return-disposition'],
'logistics': ['hub-connection-recovery', 'port-crane-yard'],
'manufacturing': ['takt-buffer', 'fault-rework'],
'energy': ['microgrid-storage', 'feeder-restoration'],
'healthcare': ['care-handoffs', 'sample-rejection'],
'finance': ['payment-reconciliation', 'insurance-evidence'],
'telecom': ['congestion-reroute', 'cell-handover'],
'agriculture': ['irrigation-zones', 'harvest-grading'],
'infrastructure': ['bridge-inspection', 'construction-resequence'],
}
SECTORS = list(BASELINE)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--allow-partial', action='store_true', help='Development only: build available modules and record missing groups')
parser.add_argument('--require-complete', action='store_true', help='Explicit release gate (the default)')
args = parser.parse_args()
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
entries = [e for e in json.loads((root / 'vendor-manifest.json').read_text()) if e['path'] == 'assets/vendor/anime.umd.min.js']
if len(entries) != 1 or digest(vendor) != entries[0]['sha256']:
raise ValueError('Missing, duplicate or mismatched Anime vendor fingerprint')
out = root / 'dist/animejs-gallery-0.5.0'
# examples.js deliberately contains a JSON array, preserving all five legacy configs.
legacy_text = (source / 'examples.js').read_text()
legacy = json.loads(legacy_text[legacy_text.index('['):legacy_text.rindex(']') + 1])
# Legacy beat positions belong to the mounted recipe, never the Python builder.
legacy_ids = ['svg-draw', 'motion-path', 'spatial-stagger', 'dependency-flow', 'assemble-explode']
for c in legacy:
c['group'] = 'elements' if c['kind'] == 'element' else 'compositions'
c['groupTitle'] = c['group'].title()
c['beatSource'] = 'runtime'
c['useWhen'] = 'Explain ' + c['description'][0].lower() + c['description'][1:]
c['avoid'] = 'Avoid decorative motion or treating illustrative timing and geometry as measured performance.'
c['assumptions'] = 'Original conceptual example; all timing and geometry are illustrative. Retest edited parameters.'
records = list(legacy)
missing = []
for group in SECTORS + sorted(p.stem for p in (source / 'catalogs').glob('*.json') if p.stem not in SECTORS):
if not re.fullmatch(r'[a-z0-9]+(?:-[a-z0-9]+)*', group):
raise ValueError('Invalid group slug: ' + group)
data = source / 'catalogs' / (group + '.json')
module = source / 'industries' / (group + '.js')
if not data.exists() or not module.exists():
missing.append(group)
continue
sector = json.loads(data.read_text())
if not isinstance(sector, list) or not sector:
raise ValueError(f'{group}: expected a nonempty descriptor list')
for c in sector:
for field in ['id', 'group', 'groupTitle', 'title', 'description', 'useWhen', 'avoid', 'assumptions']:
if not isinstance(c.get(field), str) or not c[field].strip():
raise ValueError(f'{group}: missing string {field}')
if c['group'] != group or not re.fullmatch(re.escape(group) + r'-[a-z0-9-]+', c['id']):
raise ValueError(f'{group}: invalid group/id')
if not isinstance(c.get('config'), dict) or not isinstance(c.get('beats'), list) or not c['beats']:
raise ValueError(f'{group}: missing config/beats')
previous = -1
for beat in c['beats']:
at = beat.get('at')
if not isinstance(at, (int, float)) or not 0 <= at < float('inf') or at < previous or not isinstance(beat.get('label'), str):
raise ValueError(f'{group}: invalid beat')
previous = at
c['kind'] = 'industry'
if len({c['groupTitle'] for c in sector}) != 1:
raise ValueError(f'{group}: inconsistent groupTitle')
records.extend(sector)
required_ids = legacy_ids + [group + '-' + scenario for group, scenarios in BASELINE.items() for scenario in scenarios]
missing_ids = [id for id in required_ids if id not in {c['id'] for c in records}]
if (missing or missing_ids) and (args.require_complete or not args.allow_partial):
raise ValueError('Incomplete release baseline. Missing groups: ' + ', '.join(missing) + '; missing IDs: ' + ', '.join(missing_ids))
if len({c['id'] for c in records}) != len(records):
raise ValueError('Duplicate catalog IDs')
groups = list(dict.fromkeys(c['group'] for c in records))
# Static script assignment works from file:// without ESM, fetch or a server.
(source / 'catalog-data.js').write_text('/* Generated by scripts/build-gallery.py; edit catalogs or examples.js. */\nwindow.animeExamples = ' + json.dumps(records, ensure_ascii=True, indent=2) + ';\n')
license_text = html.escape((root / 'assets/vendor/anime-LICENSE.md').read_text())
esc = html.escape
pages = {}
def page(rel, title, body_attr, nav, description, modules=()):
prefix = '../' if '/' in rel else ''
scripts = []
if modules:
scripts.append(prefix + '../vendor/anime.umd.min.js')
scripts.append(prefix + 'catalog-data.js')
scripts.extend(prefix + m for m in modules if m)
scripts.append(prefix + 'gallery.js')
text = f'''<!doctype html>
<!-- Generated from catalog metadata by scripts/build-gallery.py. -->
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{esc(title)} — Motion that explains</title><link rel="stylesheet" href="{prefix}gallery.css"></head>
<body {body_attr}><header><nav aria-label="Breadcrumb">{nav}</nav><h1>{esc(title)}</h1><p>{esc(description)}</p></header><main aria-label="Animation examples"></main><footer><p>Original conceptual examples · Anime.js 4.5.0 · Offline library {VERSION}. Not operational, clinical or engineering advice.</p><details><summary>Anime.js license</summary><pre>{license_text}</pre></details></footer>
''' + ''.join(f'<script src="{s}"></script>' for s in scripts) + '</body></html>\n'
pages[rel] = text
page('index.html', 'Motion that explains', 'data-gallery="all"', '<span aria-current="page">All examples</span>', f'{len(records)} editable examples across {len(groups)} registries. Choose a question or industry; inspect meaningful timeline beats. All scenes use illustrative models.')
for group in groups:
title = next(c['groupTitle'] for c in records if c['group'] == group)
page(group + '/index.html', title, f'data-gallery="{group}"', f'<a href="../index.html">All examples</a> / <span aria-current="page">{esc(title)}</span>', 'Open an example for use/avoid guidance, assumptions and named timeline beats.')
for c in records:
nav = f'<a href="../index.html">All examples</a> / <a href="../{c["group"]}/index.html">{esc(c["groupTitle"])}</a> / <span aria-current="page">{esc(c["title"])}</span>'
modules = ['industries/' + c['group'] + '.js'] if c['kind'] == 'industry' else ['']
page('examples/' + c['id'] + '.html', c['title'], f'data-example="{c["id"]}"', nav, 'Scenes start complete. Play or replay; seek a named beat to inspect a phase. Reduced motion keeps the scene complete.', modules)
out = root / ('dist/animejs-gallery-' + VERSION)
files = {}
for page in sorted(source.rglob('*.html')):
text = page.read_text(encoding='utf-8')
for rel, text in pages.items():
original = source / rel
original.parent.mkdir(parents=True, exist_ok=True)
original.write_text(text, encoding='utf-8')
def style(match):
return '<style>' + (page.parent / match[1]).read_text(encoding='utf-8') + '</style>'
return '<style>' + (original.parent / match[1]).read_text() + '</style>'
def script(match):
js = re.sub(r'//# sourceMappingURL=.*', '', (page.parent / match[1]).read_text(encoding='utf-8'))
js = re.sub(r'//# sourceMappingURL=.*', '', (original.parent / match[1]).read_text())
return '<script>' + js.replace('</script', r'<\/script') + '</script>'
text = re.sub(r'<link rel="stylesheet" href="([^"]+)">', style, text)
text = re.sub(r'<script src="([^"]+)"></script>', script, text)
if 'ANIME_LICENSE' in text:
raise ValueError('Unresolved license placeholder')
target = out / page.relative_to(source)
target = out / rel
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(text, encoding='utf-8')
files[str(target.relative_to(out))] = digest(target)
inputs = [*sorted(source.rglob('*')), vendor, root / 'assets/vendor/anime-LICENSE.md']
report = {'package': out.name, 'entry': 'index.html', 'anime': '4.5.0', 'examples': 5,
'files': files, 'inputs': {str(p.relative_to(root)): digest(p) for p in inputs if p.is_file()}}
files[rel] = digest(target)
inputs = [*sorted(source.rglob('*')), vendor, root / 'assets/vendor/anime-LICENSE.md', Path(__file__)]
report = {'package': out.name, 'entry': 'index.html', 'anime': '4.5.0', 'examples': len(records),
'groups': groups, 'ids': [c['id'] for c in records], 'missingGroups': missing,
'complete': not missing and not missing_ids, 'requiredGroups': SECTORS,
'requiredIds': required_ids, 'missingIds': missing_ids, 'files': files,
'inputs': {str(p.relative_to(root)): digest(p) for p in inputs if p.is_file()}}
(root / 'dist/animejs-gallery-manifest.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(report, indent=2))
print(json.dumps({k: report[k] for k in ['package', 'examples', 'groups', 'complete', 'missingGroups']}))
print(f'Built {len(files)} HTML pages; previous dist versions preserved.')