[verified] feat: expand public animation library across 12 industries
This commit is contained in:
1 parent
3bb9555e6a
commit
ecbbcf8ba0
144 files changed
+61323
-187
No files matched your search
+118
-16
@@ -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.')
|
||||
Reference in new issue
Block a user