@remixmate/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -83
- package/README.zh-CN.md +183 -0
- package/dist/handlers/gen-digital-human.d.ts +12 -0
- package/dist/handlers/gen-digital-human.js +157 -0
- package/dist/handlers/gen-image.js +5 -41
- package/dist/handlers/gen-video.d.ts +18 -0
- package/dist/handlers/gen-video.js +153 -0
- package/dist/handlers/gen-voice.js +2 -12
- package/dist/handlers/index.js +4 -2
- package/dist/handlers/shared.d.ts +26 -0
- package/dist/handlers/shared.js +66 -0
- package/dist/http.d.ts +4 -2
- package/dist/http.js +5 -3
- package/dist/manifest.json +27 -24
- package/dist/registry.d.ts +5 -16
- package/dist/registry.js +11 -17
- package/dist/skill-schema.d.ts +47 -0
- package/dist/skill-schema.js +49 -0
- package/package.json +5 -4
- package/skills/export-jianying/scripts/gen_jianying_draft.py +4 -4
- package/skills/gen-digital-human/SKILL.md +23 -46
- package/skills/gen-digital-human/skill.json +3 -2
- package/skills/gen-digital-human/version.json +1 -1
- package/skills/gen-image/SKILL.md +2 -2
- package/skills/gen-image/skill.json +2 -1
- package/skills/gen-image/version.json +1 -1
- package/skills/gen-script/SKILL.md +13 -13
- package/skills/gen-script/scripts/gen_script.py +11 -12
- package/skills/gen-video/SKILL.md +22 -33
- package/skills/gen-video/skill.json +3 -2
- package/skills/gen-video/version.json +1 -1
- package/skills/gen-voice/SKILL.md +1 -1
- package/skills/gen-voice/version.json +1 -1
- package/skills/prepare-video-assets/SKILL.md +2 -2
- package/skills/prepare-video-assets/skill.json +1 -1
- package/skills/render-video/scripts/render_video.py +8 -8
- package/skills/{template-bind → template-registry}/README.md +4 -4
- package/skills/{template-bind → template-registry}/SKILL.md +14 -12
- package/skills/template-registry/scripts/list_templates.py +129 -0
- package/skills/{template-bind → template-registry}/scripts/match_template.py +4 -3
- package/skills/{template-bind → template-registry}/scripts/registry_loader.py +1 -1
- package/skills/{template-bind → template-registry}/scripts/template_paths.py +7 -7
- package/skills/{template-bind → template-registry}/skill.json +6 -5
- package/skills/template-registry/version.json +7 -0
- package/skills/{template-bind → template-registry}/video_dsl/README.md +2 -2
- package/skills/{template-bind → template-registry}/video_dsl/runtime/__init__.py +1 -1
- package/skills/video-parser/SKILL.md +47 -160
- package/skills/video-parser/skill.json +3 -4
- package/skills/video-parser/version.json +1 -1
- package/skills/web-capture/SKILL.md +407 -116
- package/skills/web-capture/scripts/_media_screenshot/__init__.py +21 -0
- package/skills/web-capture/scripts/_media_screenshot/bootstrap.py +34 -0
- package/skills/web-capture/scripts/_media_screenshot/browser.py +86 -0
- package/skills/web-capture/scripts/_media_screenshot/cli_args.py +111 -0
- package/skills/web-capture/scripts/_media_screenshot/js/arrow.js +86 -0
- package/skills/web-capture/scripts/_media_screenshot/js/caption.js +27 -0
- package/skills/web-capture/scripts/_media_screenshot/js/clear_overlay.js +5 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_bootstrap.js +26 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_hide.js +6 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_move.js +12 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_ripple.js +25 -0
- package/skills/web-capture/scripts/_media_screenshot/js/element_zoom.js +72 -0
- package/skills/web-capture/scripts/_media_screenshot/js/element_zoom_restore.js +21 -0
- package/skills/web-capture/scripts/_media_screenshot/js/fade_out_overlays.js +10 -0
- package/skills/web-capture/scripts/_media_screenshot/js/focus.js +38 -0
- package/skills/web-capture/scripts/_media_screenshot/js/highlight.js +97 -0
- package/skills/web-capture/scripts/_media_screenshot/js/overlay_bootstrap.js +19 -0
- package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_in.js +54 -0
- package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_out.js +11 -0
- package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_restore.js +13 -0
- package/skills/web-capture/scripts/_media_screenshot/js/redact.js +33 -0
- package/skills/web-capture/scripts/_media_screenshot/js/scroll.js +26 -0
- package/skills/web-capture/scripts/_media_screenshot/js/scroll_through.js +18 -0
- package/skills/web-capture/scripts/_media_screenshot/js/sequence.js +76 -0
- package/skills/web-capture/scripts/_media_screenshot/js/settled_check.js +15 -0
- package/skills/web-capture/scripts/_media_screenshot/js/title_card.js +71 -0
- package/skills/web-capture/scripts/_media_screenshot/js_loader.py +14 -0
- package/skills/web-capture/scripts/_media_screenshot/overlay.py +17 -0
- package/skills/web-capture/scripts/_media_screenshot/recording.py +100 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/__init__.py +39 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/arrow.py +50 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/caption.py +20 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/click.py +35 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/code_lines.py +54 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/focus.py +24 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/highlight.py +54 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/hover.py +28 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/redact.py +32 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/scroll.py +32 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/sequence.py +43 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/title_card.py +20 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/type.py +42 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/wait.py +9 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/zoom.py +72 -0
- package/skills/web-capture/scripts/_media_screenshot/screenshot.py +166 -0
- package/skills/web-capture/scripts/_media_screenshot/storyboard.py +88 -0
- package/skills/web-capture/scripts/_media_screenshot/template.py +119 -0
- package/skills/web-capture/scripts/_media_screenshot/trim.py +82 -0
- package/skills/web-capture/scripts/record.py +186 -0
- package/skills/web-capture/scripts/screenshot.py +94 -0
- package/skills/web-capture/skill.json +14 -14
- package/skills/web-capture/templates/github-code-tour.json +59 -0
- package/skills/web-capture/templates/github-readme-tour.json +77 -0
- package/skills/web-capture/templates/github-repo-intro.json +74 -0
- package/skills/web-capture/version.json +2 -2
- package/dist/handlers/template-bind.d.ts +0 -15
- package/dist/handlers/template-bind.js +0 -149
- package/skills/gen-digital-human/scripts/gen_digital_human_video.py +0 -574
- package/skills/gen-video/scripts/gen_video.py +0 -591
- package/skills/template-bind/version.json +0 -7
- package/skills/web-capture/scripts/web_capture.py +0 -461
- /package/skills/{template-bind → template-registry}/scripts/check_contracts.py +0 -0
- /package/skills/{template-bind → template-registry}/scripts/render_job_client.py +0 -0
- /package/skills/{template-bind → template-registry}/scripts/sync_registry.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/dsl_validator.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/prompt_enhancer.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/template_binder.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/timeline_compiler.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/render-plan-v1alpha1.json +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/template-binding-v1alpha1.json +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/template-definition-v1alpha1.json +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/video-dsl-v1alpha1.json +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Browser context / locator / wait helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from . import js_loader
|
|
7
|
+
from .bootstrap import ensure_chromium
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pick_browser_type(p, name: str | None):
|
|
11
|
+
if name == "firefox":
|
|
12
|
+
return p.firefox
|
|
13
|
+
if name == "webkit":
|
|
14
|
+
return p.webkit
|
|
15
|
+
return p.chromium
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_context_options(p, cfg: dict, record_video_dir: str | None) -> dict:
|
|
19
|
+
opts: dict[str, Any] = {}
|
|
20
|
+
if cfg.get("device"):
|
|
21
|
+
d = p.devices.get(cfg["device"])
|
|
22
|
+
if not d:
|
|
23
|
+
raise SystemExit(f"unknown device: {cfg['device']}")
|
|
24
|
+
opts.update(d)
|
|
25
|
+
elif isinstance(cfg.get("viewport"), (list, tuple)) and len(cfg["viewport"]) == 2:
|
|
26
|
+
opts["viewport"] = {
|
|
27
|
+
"width": int(cfg["viewport"][0]),
|
|
28
|
+
"height": int(cfg["viewport"][1]),
|
|
29
|
+
}
|
|
30
|
+
if cfg.get("colorScheme"):
|
|
31
|
+
opts["color_scheme"] = cfg["colorScheme"]
|
|
32
|
+
if cfg.get("userAgent"):
|
|
33
|
+
opts["user_agent"] = cfg["userAgent"]
|
|
34
|
+
if cfg.get("ignoreHttpsErrors"):
|
|
35
|
+
opts["ignore_https_errors"] = True
|
|
36
|
+
if cfg.get("storageState"):
|
|
37
|
+
opts["storage_state"] = cfg["storageState"]
|
|
38
|
+
if record_video_dir:
|
|
39
|
+
opts["record_video_dir"] = record_video_dir
|
|
40
|
+
if opts.get("viewport"):
|
|
41
|
+
opts["record_video_size"] = opts["viewport"]
|
|
42
|
+
return opts
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def launch_with_browser_install(p, browser_name: str | None, launch_kwargs: dict | None = None):
|
|
46
|
+
btype = pick_browser_type(p, browser_name)
|
|
47
|
+
try:
|
|
48
|
+
return btype.launch(**(launch_kwargs or {}))
|
|
49
|
+
except Exception as e:
|
|
50
|
+
msg = str(e)
|
|
51
|
+
if "Executable doesn't exist" in msg or "playwright install" in msg.lower():
|
|
52
|
+
ensure_chromium()
|
|
53
|
+
return btype.launch(**(launch_kwargs or {}))
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def apply_pre_action_waits(page, cfg: dict) -> None:
|
|
58
|
+
if cfg.get("waitForSelector"):
|
|
59
|
+
page.wait_for_selector(cfg["waitForSelector"], timeout=cfg.get("timeout", 0))
|
|
60
|
+
if isinstance(cfg.get("waitForTimeout"), (int, float)):
|
|
61
|
+
page.wait_for_timeout(cfg["waitForTimeout"])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def wait_until_settled(page, cfg: dict) -> None:
|
|
65
|
+
try:
|
|
66
|
+
page.wait_for_load_state("networkidle", timeout=20000)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
try:
|
|
70
|
+
page.wait_for_function(js_loader.load("settled_check"), timeout=15000)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
for sel in cfg.get("waitForReadySelectors") or []:
|
|
74
|
+
try:
|
|
75
|
+
page.wait_for_selector(sel, state="visible", timeout=10000)
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
page.wait_for_timeout(cfg.get("settleMs", 800))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def resolve_locator(page, selector: str):
|
|
82
|
+
locator = page.locator(selector).first
|
|
83
|
+
locator.wait_for(state="visible", timeout=15000)
|
|
84
|
+
locator.scroll_into_view_if_needed()
|
|
85
|
+
page.wait_for_timeout(250)
|
|
86
|
+
return locator
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Argparse helpers shared by ``screenshot.py`` and ``record.py``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_common_args(ap: argparse.ArgumentParser) -> None:
|
|
12
|
+
"""Add the args common to both screenshot and record entry points."""
|
|
13
|
+
ap.add_argument(
|
|
14
|
+
"-b", "--browser", choices=["chromium", "firefox", "webkit"],
|
|
15
|
+
default="chromium", help="浏览器内核(默认 chromium)",
|
|
16
|
+
)
|
|
17
|
+
ap.add_argument("--device", help='设备模拟名,如 "iPhone 15 Pro"、"Pixel 7"')
|
|
18
|
+
ap.add_argument(
|
|
19
|
+
"--viewport",
|
|
20
|
+
help='viewport "宽,高",如 "1280,800";与 --device 互斥时以 --device 为准',
|
|
21
|
+
)
|
|
22
|
+
ap.add_argument("--wait-for-selector", help="动作前等待该 CSS 选择器出现")
|
|
23
|
+
ap.add_argument("--wait-for-timeout", type=int, help="动作前固定等待毫秒数")
|
|
24
|
+
ap.add_argument(
|
|
25
|
+
"--color-scheme", choices=["light", "dark", "no-preference"],
|
|
26
|
+
help="模拟 prefers-color-scheme",
|
|
27
|
+
)
|
|
28
|
+
ap.add_argument("--user-agent", help="覆盖 User-Agent")
|
|
29
|
+
ap.add_argument(
|
|
30
|
+
"--timeout", type=int, help="Playwright 全局动作超时(ms,默认无超时)",
|
|
31
|
+
)
|
|
32
|
+
ap.add_argument(
|
|
33
|
+
"--ignore-https-errors", action="store_true", help="忽略 HTTPS 证书错误",
|
|
34
|
+
)
|
|
35
|
+
ap.add_argument("--storage-state", help="storageState JSON 文件路径")
|
|
36
|
+
ap.add_argument(
|
|
37
|
+
"--cookies",
|
|
38
|
+
help='Playwright cookies JSON 字符串或 JSON 文件路径(顶层为数组)',
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_cookies(raw: str) -> list[dict]:
|
|
43
|
+
p = Path(raw)
|
|
44
|
+
if p.exists() and p.is_file():
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(p.read_text())
|
|
47
|
+
except json.JSONDecodeError as e:
|
|
48
|
+
raise SystemExit(f"--cookies 文件不是合法 JSON:{e}")
|
|
49
|
+
else:
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(raw)
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
raise SystemExit(
|
|
54
|
+
"--cookies 需为 JSON 字符串或 JSON 文件路径,顶层为 cookie 数组。"
|
|
55
|
+
)
|
|
56
|
+
if not isinstance(data, list):
|
|
57
|
+
raise SystemExit("--cookies 顶层应为数组(Playwright cookies 格式)。")
|
|
58
|
+
return data
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_storage(args) -> tuple[str | None, bool]:
|
|
62
|
+
"""Returns (storage_state_path, is_temp). Caller must unlink if is_temp."""
|
|
63
|
+
if getattr(args, "storage_state", None):
|
|
64
|
+
return args.storage_state, False
|
|
65
|
+
if getattr(args, "cookies", None):
|
|
66
|
+
cookies = parse_cookies(args.cookies)
|
|
67
|
+
fd, path = tempfile.mkstemp(prefix="pw_storage_", suffix=".json")
|
|
68
|
+
with os.fdopen(fd, "w") as f:
|
|
69
|
+
json.dump({"cookies": cookies, "origins": []}, f)
|
|
70
|
+
return path, True
|
|
71
|
+
return None, False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_viewport(raw: str) -> list[int]:
|
|
75
|
+
parts = [s.strip() for s in raw.split(",")]
|
|
76
|
+
if len(parts) != 2:
|
|
77
|
+
raise SystemExit('--viewport 需为 "宽,高"')
|
|
78
|
+
try:
|
|
79
|
+
return [int(p) for p in parts]
|
|
80
|
+
except ValueError:
|
|
81
|
+
raise SystemExit("--viewport 必须是整数")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_clip(raw: str) -> list[float]:
|
|
85
|
+
parts = [s.strip() for s in raw.split(",")]
|
|
86
|
+
if len(parts) != 4:
|
|
87
|
+
raise SystemExit('--clip 需为 "x,y,w,h" 四个数字')
|
|
88
|
+
try:
|
|
89
|
+
return [float(p) for p in parts]
|
|
90
|
+
except ValueError:
|
|
91
|
+
raise SystemExit("--clip 的四个值必须是数字")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def common_args_to_cfg(args, cfg: dict) -> None:
|
|
95
|
+
"""Merge the common-args values into a cfg dict (shared by entry points)."""
|
|
96
|
+
if args.device:
|
|
97
|
+
cfg["device"] = args.device
|
|
98
|
+
if args.viewport:
|
|
99
|
+
cfg["viewport"] = parse_viewport(args.viewport)
|
|
100
|
+
if args.color_scheme:
|
|
101
|
+
cfg["colorScheme"] = args.color_scheme
|
|
102
|
+
if args.user_agent:
|
|
103
|
+
cfg["userAgent"] = args.user_agent
|
|
104
|
+
if args.timeout is not None:
|
|
105
|
+
cfg["timeout"] = args.timeout
|
|
106
|
+
if args.ignore_https_errors:
|
|
107
|
+
cfg["ignoreHttpsErrors"] = True
|
|
108
|
+
if args.wait_for_selector:
|
|
109
|
+
cfg["waitForSelector"] = args.wait_for_selector
|
|
110
|
+
if args.wait_for_timeout is not None:
|
|
111
|
+
cfg["waitForTimeout"] = args.wait_for_timeout
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Draw a curved SVG arrow from `from` to `to` with an arrowhead, optional label.
|
|
2
|
+
// Both endpoints are page-fixed coordinates (computed in Python from bounding boxes).
|
|
3
|
+
({ from, to, color, lineWidth, label }) => {
|
|
4
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
5
|
+
if (!root) return;
|
|
6
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
7
|
+
|
|
8
|
+
const svg = document.createElementNS(NS, 'svg');
|
|
9
|
+
Object.assign(svg.style, {
|
|
10
|
+
position: 'fixed',
|
|
11
|
+
left: '0',
|
|
12
|
+
top: '0',
|
|
13
|
+
width: '100%',
|
|
14
|
+
height: '100%',
|
|
15
|
+
pointerEvents: 'none',
|
|
16
|
+
overflow: 'visible',
|
|
17
|
+
});
|
|
18
|
+
svg.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`);
|
|
19
|
+
svg.setAttribute('preserveAspectRatio', 'none');
|
|
20
|
+
|
|
21
|
+
const markerId = `__pw_arrow_head_${Math.random().toString(36).slice(2, 8)}`;
|
|
22
|
+
const defs = document.createElementNS(NS, 'defs');
|
|
23
|
+
const marker = document.createElementNS(NS, 'marker');
|
|
24
|
+
marker.setAttribute('id', markerId);
|
|
25
|
+
marker.setAttribute('viewBox', '0 0 10 10');
|
|
26
|
+
marker.setAttribute('refX', '8');
|
|
27
|
+
marker.setAttribute('refY', '5');
|
|
28
|
+
marker.setAttribute('markerWidth', '6');
|
|
29
|
+
marker.setAttribute('markerHeight', '6');
|
|
30
|
+
marker.setAttribute('orient', 'auto-start-reverse');
|
|
31
|
+
const tri = document.createElementNS(NS, 'path');
|
|
32
|
+
tri.setAttribute('d', 'M0,0 L10,5 L0,10 z');
|
|
33
|
+
tri.setAttribute('fill', color);
|
|
34
|
+
marker.appendChild(tri);
|
|
35
|
+
defs.appendChild(marker);
|
|
36
|
+
svg.appendChild(defs);
|
|
37
|
+
|
|
38
|
+
const dx = to.x - from.x;
|
|
39
|
+
const dy = to.y - from.y;
|
|
40
|
+
const mx = (from.x + to.x) / 2;
|
|
41
|
+
const my = (from.y + to.y) / 2;
|
|
42
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
43
|
+
const curve = Math.min(80, len * 0.22);
|
|
44
|
+
const ox = (-dy / len) * curve;
|
|
45
|
+
const oy = (dx / len) * curve;
|
|
46
|
+
|
|
47
|
+
const path = document.createElementNS(NS, 'path');
|
|
48
|
+
path.setAttribute('d', `M ${from.x} ${from.y} Q ${mx + ox} ${my + oy} ${to.x} ${to.y}`);
|
|
49
|
+
path.setAttribute('stroke', color);
|
|
50
|
+
path.setAttribute('stroke-width', String(lineWidth));
|
|
51
|
+
path.setAttribute('fill', 'none');
|
|
52
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
53
|
+
path.setAttribute('marker-end', `url(#${markerId})`);
|
|
54
|
+
|
|
55
|
+
const dashLen = len * 1.15 + curve;
|
|
56
|
+
path.setAttribute('stroke-dasharray', String(dashLen));
|
|
57
|
+
path.setAttribute('stroke-dashoffset', String(dashLen));
|
|
58
|
+
path.style.transition = 'stroke-dashoffset 600ms ease-out';
|
|
59
|
+
svg.appendChild(path);
|
|
60
|
+
root.appendChild(svg);
|
|
61
|
+
requestAnimationFrame(() => path.setAttribute('stroke-dashoffset', '0'));
|
|
62
|
+
|
|
63
|
+
if (label) {
|
|
64
|
+
const lbl = document.createElement('div');
|
|
65
|
+
lbl.textContent = label;
|
|
66
|
+
Object.assign(lbl.style, {
|
|
67
|
+
position: 'fixed',
|
|
68
|
+
left: `${from.x}px`,
|
|
69
|
+
top: `${from.y - 36}px`,
|
|
70
|
+
transform: 'translateX(-50%)',
|
|
71
|
+
padding: '6px 12px',
|
|
72
|
+
background: color,
|
|
73
|
+
color: '#fff',
|
|
74
|
+
fontSize: '15px',
|
|
75
|
+
fontWeight: '600',
|
|
76
|
+
borderRadius: '6px',
|
|
77
|
+
fontFamily: 'system-ui, -apple-system, "PingFang SC", "Helvetica Neue", sans-serif',
|
|
78
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.25)',
|
|
79
|
+
whiteSpace: 'nowrap',
|
|
80
|
+
opacity: '0',
|
|
81
|
+
transition: 'opacity 400ms ease 200ms',
|
|
82
|
+
});
|
|
83
|
+
root.appendChild(lbl);
|
|
84
|
+
requestAnimationFrame(() => (lbl.style.opacity = '1'));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Slide a caption strip in from the top or bottom edge.
|
|
2
|
+
({ text, position, bg, fg, fontSize }) => {
|
|
3
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
4
|
+
if (!root) return;
|
|
5
|
+
const isTop = position === 'top';
|
|
6
|
+
const strip = document.createElement('div');
|
|
7
|
+
strip.textContent = text;
|
|
8
|
+
Object.assign(strip.style, {
|
|
9
|
+
position: 'fixed',
|
|
10
|
+
left: '0',
|
|
11
|
+
right: '0',
|
|
12
|
+
[isTop ? 'top' : 'bottom']: '0',
|
|
13
|
+
padding: '14px 28px',
|
|
14
|
+
background: bg,
|
|
15
|
+
color: fg,
|
|
16
|
+
fontSize: `${fontSize}px`,
|
|
17
|
+
fontWeight: '600',
|
|
18
|
+
fontFamily: 'system-ui, -apple-system, "PingFang SC", "Helvetica Neue", sans-serif',
|
|
19
|
+
textAlign: 'center',
|
|
20
|
+
transform: `translateY(${isTop ? '-100%' : '100%'})`,
|
|
21
|
+
transition: 'transform 400ms cubic-bezier(0.22, 0.61, 0.36, 1)',
|
|
22
|
+
boxShadow: isTop ? '0 4px 16px rgba(0,0,0,0.3)' : '0 -4px 16px rgba(0,0,0,0.3)',
|
|
23
|
+
lineHeight: '1.4',
|
|
24
|
+
});
|
|
25
|
+
root.appendChild(strip);
|
|
26
|
+
requestAnimationFrame(() => (strip.style.transform = 'translateY(0)'));
|
|
27
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Mount a singleton fake cursor on documentElement (outside overlay root so it
|
|
2
|
+
// survives inter-scene fades). Idempotent.
|
|
3
|
+
() => {
|
|
4
|
+
if (window.__pwCursor && window.__pwCursor.el && document.body.contains(window.__pwCursor.el)) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const c = document.createElement('div');
|
|
8
|
+
c.id = '__pw_cursor';
|
|
9
|
+
Object.assign(c.style, {
|
|
10
|
+
position: 'fixed',
|
|
11
|
+
left: '20px',
|
|
12
|
+
top: '20px',
|
|
13
|
+
width: '28px',
|
|
14
|
+
height: '28px',
|
|
15
|
+
pointerEvents: 'none',
|
|
16
|
+
zIndex: '2147483647',
|
|
17
|
+
transition: 'left 0ms linear, top 0ms linear',
|
|
18
|
+
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.45))',
|
|
19
|
+
});
|
|
20
|
+
c.innerHTML =
|
|
21
|
+
'<svg width="28" height="28" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg">' +
|
|
22
|
+
'<path d="M3 2 L3 22 L9 16 L13 25 L17 23 L13 14 L21 14 Z" ' +
|
|
23
|
+
'fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/></svg>';
|
|
24
|
+
document.documentElement.appendChild(c);
|
|
25
|
+
window.__pwCursor = { el: c, pos: { x: 20, y: 20 } };
|
|
26
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Animate the singleton cursor to (x, y) over `ms`. Python side then waits the
|
|
2
|
+
// same amount; this JS just kicks off the CSS transition.
|
|
3
|
+
({ x, y, ms }) => {
|
|
4
|
+
const c = window.__pwCursor;
|
|
5
|
+
if (!c) return;
|
|
6
|
+
c.el.style.transition =
|
|
7
|
+
`left ${ms}ms cubic-bezier(0.22,0.61,0.36,1), top ${ms}ms cubic-bezier(0.22,0.61,0.36,1)`;
|
|
8
|
+
c.el.getBoundingClientRect();
|
|
9
|
+
c.el.style.left = `${x}px`;
|
|
10
|
+
c.el.style.top = `${y}px`;
|
|
11
|
+
c.pos = { x, y };
|
|
12
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Emit a ring-shaped ripple at the cursor's current position.
|
|
2
|
+
({ color, ms }) => {
|
|
3
|
+
const c = window.__pwCursor;
|
|
4
|
+
if (!c) return;
|
|
5
|
+
const r = document.createElement('div');
|
|
6
|
+
Object.assign(r.style, {
|
|
7
|
+
position: 'fixed',
|
|
8
|
+
left: `${c.pos.x - 4}px`,
|
|
9
|
+
top: `${c.pos.y - 4}px`,
|
|
10
|
+
width: '8px',
|
|
11
|
+
height: '8px',
|
|
12
|
+
borderRadius: '999px',
|
|
13
|
+
border: `2px solid ${color}`,
|
|
14
|
+
pointerEvents: 'none',
|
|
15
|
+
zIndex: '2147483647',
|
|
16
|
+
opacity: '0.9',
|
|
17
|
+
transition: `transform ${ms}ms ease-out, opacity ${ms}ms ease-out`,
|
|
18
|
+
});
|
|
19
|
+
document.documentElement.appendChild(r);
|
|
20
|
+
requestAnimationFrame(() => {
|
|
21
|
+
r.style.transform = 'scale(8)';
|
|
22
|
+
r.style.opacity = '0';
|
|
23
|
+
});
|
|
24
|
+
setTimeout(() => r.remove(), ms + 100);
|
|
25
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Element-level zoom: scale the element itself with a spring transition,
|
|
2
|
+
// optionally dimming the rest of the page. Original styles are stashed on
|
|
3
|
+
// the element's dataset for later restoration by element_zoom_restore.js.
|
|
4
|
+
({ selector, targetScale, rampMs, color, label, dim, lineWidth }) => {
|
|
5
|
+
const el = document.querySelector(selector);
|
|
6
|
+
if (!el) return;
|
|
7
|
+
const rect = el.getBoundingClientRect();
|
|
8
|
+
const cx = rect.left + rect.width / 2;
|
|
9
|
+
const cy = rect.top + rect.height / 2;
|
|
10
|
+
el.dataset.__pwOrigTransform = el.style.transform || '';
|
|
11
|
+
el.dataset.__pwOrigTransition = el.style.transition || '';
|
|
12
|
+
el.dataset.__pwOrigOrigin = el.style.transformOrigin || '';
|
|
13
|
+
el.dataset.__pwOrigZ = el.style.zIndex || '';
|
|
14
|
+
el.dataset.__pwOrigPos = el.style.position || '';
|
|
15
|
+
el.dataset.__pwOrigBg = el.style.backgroundColor || '';
|
|
16
|
+
el.dataset.__pwOrigOutline = el.style.outline || '';
|
|
17
|
+
el.dataset.__pwOrigRadius = el.style.borderRadius || '';
|
|
18
|
+
|
|
19
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
20
|
+
if (dim) {
|
|
21
|
+
const dimEl = document.createElement('div');
|
|
22
|
+
dimEl.id = '__pw_zoom_dim';
|
|
23
|
+
Object.assign(dimEl.style, {
|
|
24
|
+
position: 'fixed', inset: '0', background: 'rgba(0,0,0,0)',
|
|
25
|
+
transition: `background ${rampMs}ms ease`, zIndex: '2147483640',
|
|
26
|
+
pointerEvents: 'none',
|
|
27
|
+
});
|
|
28
|
+
root.appendChild(dimEl);
|
|
29
|
+
requestAnimationFrame(() => {
|
|
30
|
+
dimEl.style.background = 'rgba(0,0,0,0.55)';
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const computed = window.getComputedStyle(el);
|
|
35
|
+
if (computed.position === 'static') el.style.position = 'relative';
|
|
36
|
+
el.style.zIndex = '2147483645';
|
|
37
|
+
el.style.transformOrigin = `${cx - rect.left}px ${cy - rect.top}px`;
|
|
38
|
+
el.style.transition = `transform ${rampMs}ms cubic-bezier(0.34, 1.4, 0.64, 1), background-color ${rampMs}ms ease, outline-color ${rampMs}ms ease`;
|
|
39
|
+
el.style.transform = 'scale(1)';
|
|
40
|
+
el.style.outline = `${lineWidth}px solid rgba(255,149,0,0)`;
|
|
41
|
+
el.style.borderRadius = el.dataset.__pwOrigRadius || '6px';
|
|
42
|
+
requestAnimationFrame(() => {
|
|
43
|
+
el.style.transform = `scale(${targetScale})`;
|
|
44
|
+
el.style.outline = `${lineWidth}px solid ${color}`;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (label) {
|
|
48
|
+
const lbl = document.createElement('div');
|
|
49
|
+
lbl.id = '__pw_zoom_label';
|
|
50
|
+
lbl.textContent = label;
|
|
51
|
+
const finalH = rect.height * targetScale;
|
|
52
|
+
Object.assign(lbl.style, {
|
|
53
|
+
position: 'fixed',
|
|
54
|
+
left: `${cx}px`,
|
|
55
|
+
top: `${cy + finalH / 2 + 24}px`,
|
|
56
|
+
transform: 'translateX(-50%) translateY(8px)',
|
|
57
|
+
padding: '10px 18px',
|
|
58
|
+
background: color, color: '#fff',
|
|
59
|
+
fontSize: '20px', fontWeight: '700', borderRadius: '8px',
|
|
60
|
+
fontFamily: 'system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
|
61
|
+
boxShadow: '0 6px 18px rgba(0,0,0,0.4)',
|
|
62
|
+
whiteSpace: 'nowrap', opacity: '0',
|
|
63
|
+
transition: 'opacity 280ms ease, transform 280ms ease',
|
|
64
|
+
zIndex: '2147483646', pointerEvents: 'none',
|
|
65
|
+
});
|
|
66
|
+
root.appendChild(lbl);
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
lbl.style.opacity = '1';
|
|
69
|
+
lbl.style.transform = 'translateX(-50%) translateY(0)';
|
|
70
|
+
}, Math.max(0, rampMs * 0.6));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Restore the inline styles stashed by element_zoom.js on the target element.
|
|
2
|
+
(selector) => {
|
|
3
|
+
const el = document.querySelector(selector);
|
|
4
|
+
if (!el) return;
|
|
5
|
+
el.style.transform = el.dataset.__pwOrigTransform || '';
|
|
6
|
+
el.style.transition = el.dataset.__pwOrigTransition || '';
|
|
7
|
+
el.style.transformOrigin = el.dataset.__pwOrigOrigin || '';
|
|
8
|
+
el.style.zIndex = el.dataset.__pwOrigZ || '';
|
|
9
|
+
el.style.position = el.dataset.__pwOrigPos || '';
|
|
10
|
+
el.style.backgroundColor = el.dataset.__pwOrigBg || '';
|
|
11
|
+
el.style.outline = el.dataset.__pwOrigOutline || '';
|
|
12
|
+
el.style.borderRadius = el.dataset.__pwOrigRadius || '';
|
|
13
|
+
delete el.dataset.__pwOrigTransform;
|
|
14
|
+
delete el.dataset.__pwOrigTransition;
|
|
15
|
+
delete el.dataset.__pwOrigOrigin;
|
|
16
|
+
delete el.dataset.__pwOrigZ;
|
|
17
|
+
delete el.dataset.__pwOrigPos;
|
|
18
|
+
delete el.dataset.__pwOrigBg;
|
|
19
|
+
delete el.dataset.__pwOrigOutline;
|
|
20
|
+
delete el.dataset.__pwOrigRadius;
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Fade every child of the overlay root to opacity 0 over `duration` ms.
|
|
2
|
+
// Page content underneath stays visible — this is the inter-scene transition.
|
|
3
|
+
(duration) => {
|
|
4
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
5
|
+
if (!root) return;
|
|
6
|
+
for (const child of root.children) {
|
|
7
|
+
child.style.transition = `opacity ${duration}ms ease, transform ${duration}ms ease`;
|
|
8
|
+
child.style.opacity = '0';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Spotlight effect: dim everything except a rounded cut-out around `box`.
|
|
2
|
+
({ box, pad, label, color }) => {
|
|
3
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
4
|
+
if (!root) return;
|
|
5
|
+
const hole = document.createElement('div');
|
|
6
|
+
Object.assign(hole.style, {
|
|
7
|
+
position: 'fixed',
|
|
8
|
+
left: `${box.x - pad}px`,
|
|
9
|
+
top: `${box.y - pad}px`,
|
|
10
|
+
width: `${box.width + pad * 2}px`,
|
|
11
|
+
height: `${box.height + pad * 2}px`,
|
|
12
|
+
borderRadius: '12px',
|
|
13
|
+
boxShadow: '0 0 0 9999px rgba(0,0,0,0.65)',
|
|
14
|
+
border: `3px solid ${color}`,
|
|
15
|
+
transition: 'box-shadow 400ms ease',
|
|
16
|
+
});
|
|
17
|
+
root.appendChild(hole);
|
|
18
|
+
if (label) {
|
|
19
|
+
const lbl = document.createElement('div');
|
|
20
|
+
lbl.textContent = label;
|
|
21
|
+
Object.assign(lbl.style, {
|
|
22
|
+
position: 'fixed',
|
|
23
|
+
left: `${box.x + box.width / 2}px`,
|
|
24
|
+
top: `${box.y + box.height + pad + 16}px`,
|
|
25
|
+
transform: 'translateX(-50%)',
|
|
26
|
+
padding: '10px 18px',
|
|
27
|
+
background: color,
|
|
28
|
+
color: '#fff',
|
|
29
|
+
fontSize: '20px',
|
|
30
|
+
fontWeight: '700',
|
|
31
|
+
borderRadius: '8px',
|
|
32
|
+
fontFamily: 'system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
|
33
|
+
boxShadow: '0 6px 18px rgba(0,0,0,0.4)',
|
|
34
|
+
whiteSpace: 'nowrap',
|
|
35
|
+
});
|
|
36
|
+
root.appendChild(lbl);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Draw a rectangle around `box` with an optional label badge.
|
|
2
|
+
// drawOnMs > 0 → SVG rect with stroke-dashoffset animation: traces from the
|
|
3
|
+
// top-left clockwise (top edge sweeps left→right first).
|
|
4
|
+
// drawOnMs <= 0 → original CSS-border box with a soft pulse loop.
|
|
5
|
+
({ box, pad, color, lw, label, drawOnMs }) => {
|
|
6
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
7
|
+
if (!root) return;
|
|
8
|
+
|
|
9
|
+
const x = box.x - pad;
|
|
10
|
+
const y = box.y - pad;
|
|
11
|
+
const w = box.width + pad * 2;
|
|
12
|
+
const h = box.height + pad * 2;
|
|
13
|
+
const rx = 8;
|
|
14
|
+
|
|
15
|
+
const animated = drawOnMs && drawOnMs > 0;
|
|
16
|
+
|
|
17
|
+
if (animated) {
|
|
18
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
19
|
+
const svg = document.createElementNS(NS, 'svg');
|
|
20
|
+
Object.assign(svg.style, {
|
|
21
|
+
position: 'fixed',
|
|
22
|
+
left: `${x - lw}px`,
|
|
23
|
+
top: `${y - lw}px`,
|
|
24
|
+
width: `${w + lw * 2}px`,
|
|
25
|
+
height: `${h + lw * 2}px`,
|
|
26
|
+
overflow: 'visible',
|
|
27
|
+
pointerEvents: 'none',
|
|
28
|
+
filter: `drop-shadow(0 0 14px ${color})`,
|
|
29
|
+
});
|
|
30
|
+
const rect = document.createElementNS(NS, 'rect');
|
|
31
|
+
rect.setAttribute('x', String(lw));
|
|
32
|
+
rect.setAttribute('y', String(lw));
|
|
33
|
+
rect.setAttribute('width', String(w));
|
|
34
|
+
rect.setAttribute('height', String(h));
|
|
35
|
+
rect.setAttribute('rx', String(rx));
|
|
36
|
+
rect.setAttribute('ry', String(rx));
|
|
37
|
+
rect.setAttribute('fill', 'none');
|
|
38
|
+
rect.setAttribute('stroke', color);
|
|
39
|
+
rect.setAttribute('stroke-width', String(lw));
|
|
40
|
+
rect.setAttribute('stroke-linecap', 'round');
|
|
41
|
+
|
|
42
|
+
const dashLen = 2 * (w + h);
|
|
43
|
+
rect.style.strokeDasharray = String(dashLen);
|
|
44
|
+
rect.style.strokeDashoffset = String(dashLen);
|
|
45
|
+
svg.appendChild(rect);
|
|
46
|
+
root.appendChild(svg);
|
|
47
|
+
// Web Animations API: way more reliable than CSS transitions on SVG
|
|
48
|
+
// presentation attributes (Chrome quietly drops the transition if the
|
|
49
|
+
// initial state hasn't been painted yet).
|
|
50
|
+
rect.animate(
|
|
51
|
+
[{ strokeDashoffset: dashLen }, { strokeDashoffset: 0 }],
|
|
52
|
+
{ duration: drawOnMs, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', fill: 'forwards' }
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
const rect = document.createElement('div');
|
|
56
|
+
Object.assign(rect.style, {
|
|
57
|
+
position: 'fixed',
|
|
58
|
+
left: `${x}px`,
|
|
59
|
+
top: `${y}px`,
|
|
60
|
+
width: `${w}px`,
|
|
61
|
+
height: `${h}px`,
|
|
62
|
+
border: `${lw}px solid ${color}`,
|
|
63
|
+
borderRadius: '8px',
|
|
64
|
+
boxShadow: `0 0 0 9999px rgba(0,0,0,0.0), 0 0 24px ${color}`,
|
|
65
|
+
animation: 'pwPulse 1.2s ease-in-out infinite',
|
|
66
|
+
});
|
|
67
|
+
const style = document.createElement('style');
|
|
68
|
+
style.textContent =
|
|
69
|
+
'@keyframes pwPulse { 0%,100%{transform:scale(1);opacity:1} 50%{transform:scale(1.04);opacity:0.85} }';
|
|
70
|
+
root.appendChild(style);
|
|
71
|
+
root.appendChild(rect);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (label) {
|
|
75
|
+
const lbl = document.createElement('div');
|
|
76
|
+
lbl.textContent = label;
|
|
77
|
+
Object.assign(lbl.style, {
|
|
78
|
+
position: 'fixed',
|
|
79
|
+
left: `${x}px`,
|
|
80
|
+
top: `${Math.max(8, y - 40)}px`,
|
|
81
|
+
padding: '6px 12px',
|
|
82
|
+
background: color,
|
|
83
|
+
color: '#fff',
|
|
84
|
+
fontSize: '16px',
|
|
85
|
+
fontWeight: '600',
|
|
86
|
+
borderRadius: '6px',
|
|
87
|
+
fontFamily:
|
|
88
|
+
'system-ui, -apple-system, "PingFang SC", "Helvetica Neue", sans-serif',
|
|
89
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.25)',
|
|
90
|
+
whiteSpace: 'nowrap',
|
|
91
|
+
opacity: '0',
|
|
92
|
+
transition: 'opacity 300ms ease',
|
|
93
|
+
});
|
|
94
|
+
root.appendChild(lbl);
|
|
95
|
+
setTimeout(() => (lbl.style.opacity = '1'), animated ? Math.floor(drawOnMs * 0.7) : 0);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Inject the fixed overlay layers used by every storyboard scene.
|
|
2
|
+
// Idempotent: a no-op if already mounted (e.g. across multiple scenes).
|
|
3
|
+
(() => {
|
|
4
|
+
if (window.__pwOverlayReady) return;
|
|
5
|
+
const root = document.createElement('div');
|
|
6
|
+
root.id = '__pw_overlay_root';
|
|
7
|
+
Object.assign(root.style, {
|
|
8
|
+
position: 'fixed', inset: '0', pointerEvents: 'none', zIndex: '2147483646',
|
|
9
|
+
});
|
|
10
|
+
const fade = document.createElement('div');
|
|
11
|
+
fade.id = '__pw_overlay_fade';
|
|
12
|
+
Object.assign(fade.style, {
|
|
13
|
+
position: 'fixed', inset: '0', background: '#000', opacity: '0',
|
|
14
|
+
transition: 'opacity 400ms ease', pointerEvents: 'none', zIndex: '2147483647',
|
|
15
|
+
});
|
|
16
|
+
document.documentElement.appendChild(root);
|
|
17
|
+
document.documentElement.appendChild(fade);
|
|
18
|
+
window.__pwOverlayReady = true;
|
|
19
|
+
})();
|