givemotion 0.0.0-stage → 0.1.0
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/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +492 -2
- package/SECURITY.md +40 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/blocks/bar-chart.html +63 -0
- package/blocks/browser-window.html +52 -0
- package/blocks/callout.html +57 -0
- package/blocks/captions.html +120 -0
- package/blocks/checklist.html +32 -0
- package/blocks/code-window.html +126 -0
- package/blocks/compare.html +33 -0
- package/blocks/cursor.html +157 -0
- package/blocks/end-card.html +28 -0
- package/blocks/gradient-bg.html +48 -0
- package/blocks/grid-bg.html +18 -0
- package/blocks/highlight.html +13 -0
- package/blocks/line-chart.html +36 -0
- package/blocks/logo-reveal.html +33 -0
- package/blocks/lottie.html +34 -0
- package/blocks/lower-third.html +24 -0
- package/blocks/phone.html +18 -0
- package/blocks/quote.html +19 -0
- package/blocks/stat.html +24 -0
- package/blocks/steps.html +40 -0
- package/blocks/title-card.html +22 -0
- package/blocks/word-pop.html +40 -0
- package/examples/bench/index.html +24 -0
- package/examples/data-story/data.json +9 -0
- package/examples/data-story/index.html +361 -0
- package/examples/data-story/other.json +46 -0
- package/examples/generative/index.html +60 -0
- package/examples/made-by-agents/README.md +5 -0
- package/examples/made-by-agents/photosynthesis/blocks/checklist.html +32 -0
- package/examples/made-by-agents/photosynthesis/blocks/end-card.html +25 -0
- package/examples/made-by-agents/photosynthesis/blocks/gradient-bg.html +48 -0
- package/examples/made-by-agents/photosynthesis/blocks/steps.html +40 -0
- package/examples/made-by-agents/photosynthesis/blocks/title-card.html +22 -0
- package/examples/made-by-agents/photosynthesis/index.html +217 -0
- package/examples/personalized/index.html +28 -0
- package/llms.txt +34 -0
- package/package.json +80 -4
- package/skills/givemotion/SKILL.md +152 -0
- package/skills/givemotion/references/api.md +40 -0
- package/skills/givemotion/references/camera.md +72 -0
- package/skills/givemotion/references/charts.md +138 -0
- package/skills/givemotion/references/check-codes.md +94 -0
- package/skills/givemotion/references/data-templates.md +61 -0
- package/skills/givemotion/references/diagrams.md +75 -0
- package/skills/givemotion/references/genres/code-explainer.md +15 -0
- package/skills/givemotion/references/genres/data-story.md +21 -0
- package/skills/givemotion/references/genres/explainer.md +20 -0
- package/skills/givemotion/references/genres/hand-drawn.md +18 -0
- package/skills/givemotion/references/genres/logo-sting.md +20 -0
- package/skills/givemotion/references/genres/promo.md +18 -0
- package/skills/givemotion/references/genres/ui-demo.md +21 -0
- package/skills/givemotion/references/genres/vertical.md +19 -0
- package/skills/givemotion/references/genres/year-recap.md +18 -0
- package/skills/givemotion/references/javascript.md +72 -0
- package/skills/givemotion/references/media-audio.md +57 -0
- package/skills/givemotion/references/motion-design.md +183 -0
- package/skills/givemotion/references/output.md +64 -0
- package/skills/givemotion/references/review.md +60 -0
- package/skills/givemotion/references/styles.md +133 -0
- package/skills/givemotion/references/techniques/backgrounds-and-ambient.md +85 -0
- package/skills/givemotion/references/techniques/camera-moves.md +88 -0
- package/skills/givemotion/references/techniques/captions-vertical.md +89 -0
- package/skills/givemotion/references/techniques/code-on-screen.md +125 -0
- package/skills/givemotion/references/techniques/hand-drawn.md +87 -0
- package/skills/givemotion/references/techniques/impacts-and-energy.md +106 -0
- package/skills/givemotion/references/techniques/kinetic-type.md +92 -0
- package/skills/givemotion/references/techniques/light-and-depth.md +91 -0
- package/skills/givemotion/references/techniques/logo-reveal.md +94 -0
- package/skills/givemotion/references/techniques/numbers-and-charts.md +92 -0
- package/skills/givemotion/references/techniques/paths-and-maps.md +99 -0
- package/skills/givemotion/references/techniques/scene-transitions.md +85 -0
- package/skills/givemotion/references/techniques/shapes-and-motion-graphics.md +92 -0
- package/skills/givemotion/references/techniques/text-reveals.md +94 -0
- package/skills/givemotion/references/techniques/ui-demo.md +110 -0
- package/skills/givemotion/references/timeline.md +85 -0
- package/src/analyze.js +1485 -0
- package/src/api.js +36 -0
- package/src/args.js +33 -0
- package/src/audio-cli.js +58 -0
- package/src/bench.js +153 -0
- package/src/browser.js +381 -0
- package/src/bundle.js +79 -0
- package/src/cdp.js +94 -0
- package/src/check.js +1818 -0
- package/src/cli.js +611 -0
- package/src/dev.js +26 -0
- package/src/ffmpeg.js +330 -0
- package/src/parts.js +380 -0
- package/src/player/gm-player.js +175 -0
- package/src/render.js +772 -0
- package/src/runtime/audio.js +306 -0
- package/src/runtime/chart.js +665 -0
- package/src/runtime/fonts/BebasNeue-OFL.txt +93 -0
- package/src/runtime/fonts/Caveat-OFL.txt +93 -0
- package/src/runtime/fonts/Inter-OFL.txt +93 -0
- package/src/runtime/fonts/JetBrainsMono-OFL.txt +93 -0
- package/src/runtime/fonts/Nunito-OFL.txt +93 -0
- package/src/runtime/fonts/PlayfairDisplay-OFL.txt +93 -0
- package/src/runtime/fonts/SpaceGrotesk-OFL.txt +93 -0
- package/src/runtime/fonts/bebas-neue-latin-400-normal.woff2 +0 -0
- package/src/runtime/fonts/bebas-neue-latin-ext-400-normal.woff2 +0 -0
- package/src/runtime/fonts/caveat-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/caveat-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-ext-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/nunito-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/nunito-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-ext-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-wght-italic.woff2 +0 -0
- package/src/runtime/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/space-grotesk-latin-ext-wght-normal.woff2 +0 -0
- package/src/runtime/fonts/space-grotesk-latin-wght-normal.woff2 +0 -0
- package/src/runtime/motion.js +2674 -0
- package/src/runtime/preset.css +97 -0
- package/src/runtime/runtime.js +2141 -0
- package/src/runtime/sketch.js +1249 -0
- package/src/server.js +402 -0
- package/src/studio/index.html +238 -0
- package/src/types/api.d.ts +354 -0
- package/templates/starter/index.html +70 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
givemotion's own code is under the MIT license (see `LICENSE`). It contains no code copied from other
|
|
4
|
+
video frameworks. This file lists everything else that ships with it or that it uses.
|
|
5
|
+
|
|
6
|
+
## Bundled in this repository and the npm package
|
|
7
|
+
|
|
8
|
+
### Fonts (SIL Open Font License 1.1)
|
|
9
|
+
|
|
10
|
+
The font files in `src/runtime/fonts/` are unmodified web-font files (latin and latin-ext subsets, as
|
|
11
|
+
distributed by Google Fonts / Fontsource). Each family's full license is next to the files.
|
|
12
|
+
|
|
13
|
+
| Font | Copyright | License file |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Inter | 2016 The Inter Project Authors (https://github.com/rsms/inter) | `src/runtime/fonts/Inter-OFL.txt` |
|
|
16
|
+
| JetBrains Mono | 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) | `src/runtime/fonts/JetBrainsMono-OFL.txt` |
|
|
17
|
+
| Playfair Display | 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), Reserved Font Name "Playfair Display" | `src/runtime/fonts/PlayfairDisplay-OFL.txt` |
|
|
18
|
+
| Space Grotesk | 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk) | `src/runtime/fonts/SpaceGrotesk-OFL.txt` |
|
|
19
|
+
| Bebas Neue | 2019 The Bebas Neue Project Authors (https://github.com/dharmatype/Bebas-Neue) | `src/runtime/fonts/BebasNeue-OFL.txt` |
|
|
20
|
+
| Caveat | 2014 The Caveat Project Authors (https://github.com/googlefonts/caveat) | `src/runtime/fonts/Caveat-OFL.txt` |
|
|
21
|
+
| Nunito | 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito) | `src/runtime/fonts/Nunito-OFL.txt` |
|
|
22
|
+
|
|
23
|
+
The OFL lets you use these fonts in videos you make, including commercial ones. The fonts may not be sold on
|
|
24
|
+
their own.
|
|
25
|
+
|
|
26
|
+
### Example media
|
|
27
|
+
|
|
28
|
+
The compositions and data in `examples/` and the animations in `docs/media/` were made for this project and are
|
|
29
|
+
covered by the MIT license in `LICENSE`. The repository ships no music or other audio.
|
|
30
|
+
|
|
31
|
+
## Optional peer dependencies (not bundled and not installed with givemotion; add them yourself for React)
|
|
32
|
+
|
|
33
|
+
| Package | License | Used for |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
|
|
36
|
+
## Used at run time, installed separately
|
|
37
|
+
|
|
38
|
+
givemotion does not include or redistribute these. It runs them as separate programs, under their own licenses.
|
|
39
|
+
|
|
40
|
+
| Program | License | Notes |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Chrome / Chrome for Testing / Chromium | Google Chrome terms / BSD-3-Clause and others (Chromium) | Found on your system, or downloaded from Google by `givemotion install` |
|
|
43
|
+
| FFmpeg | LGPL 2.1+ or GPL 2+, depending on how your copy was built | Must be on your `PATH` |
|
|
44
|
+
| lottie-web | MIT | Loaded by the `lottie` block from a CDN (`cdn.jsdelivr.net`) when you use it; you can point it at your own copy |
|
|
45
|
+
|
|
46
|
+
Video codecs such as H.264 and H.265 may be covered by patents in some countries. givemotion does not ship
|
|
47
|
+
an encoder; the one in your FFmpeg build (or your operating system's hardware encoder) does the encoding, under
|
|
48
|
+
whatever terms apply to it.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!-- block:
|
|
2
|
+
description: Animated bar chart. Bars grow one after another and their values count up.
|
|
3
|
+
params: values (comma list), labels (comma list), title, prefix, suffix, max (optional), color, highlight (index of bar to highlight, 0-based), height (px at 1080p; scales with the short side, default 560), direction (vertical|horizontal: rows that grow to the right with the value riding the bar's end, for comparisons), colors (comma list, one per bar)
|
|
4
|
+
usage: <gm-include src="blocks/bar-chart.html" title="Revenue (k$)" values="12,19,31,48" labels="Q1,Q2,Q3,Q4" highlight="3" data-duration="5"></gm-include>
|
|
5
|
+
-->
|
|
6
|
+
<div class="gmb-bars gmb-bars--{{direction|vertical}}" data-colors="{{colors}}" style="--c:{{color|#7c9cff}};--h:{{height|560}}" data-values="{{values|3,5,8,13}}" data-labels="{{labels}}" data-prefix="{{prefix}}" data-suffix="{{suffix}}" data-max="{{max}}" data-hl="{{highlight|-1}}">
|
|
7
|
+
<div class="gmb-bars__title" data-enter="fade-up 0.6">{{title}}</div>
|
|
8
|
+
<div class="gmb-bars__plot"></div>
|
|
9
|
+
</div>
|
|
10
|
+
<script>
|
|
11
|
+
(() => {
|
|
12
|
+
const root = document.currentScript.previousElementSibling;
|
|
13
|
+
const plot = root.querySelector('.gmb-bars__plot');
|
|
14
|
+
const vals = root.dataset.values.split(',').map((x) => parseFloat(x));
|
|
15
|
+
const labels = root.dataset.labels ? root.dataset.labels.split(',') : vals.map((_, i) => String(i + 1));
|
|
16
|
+
const max = parseFloat(root.dataset.max) || Math.max(...vals) * 1.08;
|
|
17
|
+
const hl = parseInt(root.dataset.hl, 10);
|
|
18
|
+
const colors = root.dataset.colors ? root.dataset.colors.split(',').map((c) => c.trim()) : [];
|
|
19
|
+
const horizontal = root.classList.contains('gmb-bars--horizontal');
|
|
20
|
+
vals.forEach((v, i) => {
|
|
21
|
+
if (horizontal) {
|
|
22
|
+
// a row: label, then the bar growing to the right with its value just past its end
|
|
23
|
+
const row = document.createElement('div');
|
|
24
|
+
row.className = 'gmb-bars__row' + (i === hl ? ' is-hl' : '') + (hl >= 0 && i !== hl ? ' is-dim' : '');
|
|
25
|
+
row.style.setProperty('--i', i);
|
|
26
|
+
if (colors[i]) row.style.setProperty('--c', colors[i]);
|
|
27
|
+
row.innerHTML = `<div class="gmb-bars__lbl">${(labels[i] || '').trim()}</div><div class="gmb-bars__track"><div class="gmb-bars__bar" style="width:${(v / max) * 100}%"></div><div class="gmb-bars__val"><span data-count="${v}" data-count-duration="1.2" data-count-delay="${0.3 + i * 0.12}" data-count-prefix="${root.dataset.prefix}" data-count-suffix="${root.dataset.suffix}"></span></div></div>`;
|
|
28
|
+
plot.appendChild(row);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const col = document.createElement('div');
|
|
32
|
+
col.className = 'gmb-bars__col' + (i === hl ? ' is-hl' : '') + (hl >= 0 && i !== hl ? ' is-dim' : '');
|
|
33
|
+
col.style.setProperty('--i', i);
|
|
34
|
+
if (colors[i]) col.style.setProperty('--c', colors[i]);
|
|
35
|
+
col.innerHTML = `<div class="gmb-bars__val"><span data-count="${v}" data-count-duration="1.2" data-count-delay="${0.35 + i * 0.12}" data-count-prefix="${root.dataset.prefix}" data-count-suffix="${root.dataset.suffix}"></span></div>
|
|
36
|
+
<div class="gmb-bars__bar" style="height:${(v / max) * 100}%"></div><div class="gmb-bars__lbl">${(labels[i] || '').trim()}</div>`;
|
|
37
|
+
plot.appendChild(col);
|
|
38
|
+
});
|
|
39
|
+
root.style.setProperty('--n', vals.length);
|
|
40
|
+
})();
|
|
41
|
+
</script>
|
|
42
|
+
<style>
|
|
43
|
+
.gmb-bars{display:flex;flex-direction:column;gap:calc(var(--px)*36);width:calc(var(--px)*1400);max-width:calc(var(--stage-w,1920px) - var(--px)*192)}
|
|
44
|
+
.gmb-bars__title{font-size:calc(var(--px)*48);font-weight:700;letter-spacing:-.01em}
|
|
45
|
+
.gmb-bars__title:empty{display:none}
|
|
46
|
+
.gmb-bars__plot{display:flex;align-items:flex-end;gap:calc(var(--px)*28);height:calc(var(--px)*var(--h));border-bottom:2px solid rgba(255,255,255,.18);padding:0 calc(var(--px)*8)}
|
|
47
|
+
.gmb-bars__col{flex:1;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch;position:relative}
|
|
48
|
+
.gmb-bars__bar{border-radius:calc(var(--px)*14) calc(var(--px)*14) calc(var(--px)*4) calc(var(--px)*4);background:linear-gradient(180deg,color-mix(in oklab,var(--c),white 18%),var(--c));
|
|
49
|
+
transform-origin:bottom;animation:gmb-bar-grow 1.1s var(--ease-out-expo) calc(.3s + var(--i)*.12s) both}
|
|
50
|
+
.gmb-bars__col.is-dim .gmb-bars__bar{opacity:.38}
|
|
51
|
+
.gmb-bars__col.is-hl .gmb-bars__bar{box-shadow:0 0 calc(var(--px)*40) color-mix(in oklab,var(--c),transparent 40%)}
|
|
52
|
+
.gmb-bars__val{text-align:center;font-size:calc(var(--px)*40);font-weight:700;margin-bottom:calc(var(--px)*14);font-variant-numeric:tabular-nums;animation:fade-up .6s var(--ease-out-expo) calc(.35s + var(--i)*.12s) both}
|
|
53
|
+
.gmb-bars__lbl{position:absolute;top:100%;left:0;right:0;text-align:center;margin-top:calc(var(--px)*16);font-size:calc(var(--px)*30);opacity:.7}
|
|
54
|
+
@keyframes gmb-bar-grow{from{scale:1 0}}
|
|
55
|
+
.gmb-bars--horizontal .gmb-bars__plot{flex-direction:column;align-items:stretch;justify-content:center;height:auto;gap:calc(var(--px)*26);border-bottom:0}
|
|
56
|
+
.gmb-bars__row{display:flex;align-items:center;gap:calc(var(--px)*28)}
|
|
57
|
+
.gmb-bars__row .gmb-bars__lbl{position:static;flex:0 0 calc(var(--px)*260);text-align:right;margin:0;font-size:calc(var(--px)*34)}
|
|
58
|
+
.gmb-bars__track{flex:1;display:flex;align-items:center;gap:calc(var(--px)*18);height:calc(var(--px)*72)}
|
|
59
|
+
.gmb-bars__row .gmb-bars__bar{height:100%;flex:none;border-radius:calc(var(--px)*4) calc(var(--px)*14) calc(var(--px)*14) calc(var(--px)*4);background:linear-gradient(90deg,var(--c),color-mix(in oklab,var(--c),white 18%));animation:gmb-hbar-grow 1.1s var(--ease-out-expo) calc(.3s + var(--i)*.12s) both}
|
|
60
|
+
.gmb-bars__row .gmb-bars__val{margin:0;animation:fade-in .4s ease-out calc(.3s + var(--i)*.12s) both}
|
|
61
|
+
.gmb-bars__row.is-dim .gmb-bars__bar{opacity:.38}
|
|
62
|
+
@keyframes gmb-hbar-grow{from{width:0}}
|
|
63
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!-- block:
|
|
2
|
+
description: A clean browser window frame. Shows an image (image=…) or any HTML you put between the tags. Optional slow scroll.
|
|
3
|
+
params: url, image (path), width (px at 1080p; scales with the short side, default 1500), height (default 860), scroll (pixels to scroll the page over the clip, default 0), theme (dark|light); to match a brand palette (each defaults to today's look): dots (none | one color for all three | three colors "#e5484d,#f5a524,#30a46c"), bar (the top bar's color), field (the address field's color), text (the address text color), url-size (px at 1080p, default 24), lock (on|off, default on), enter (its entrance, default "rise 0.9"; "none" when you bring it in yourself)
|
|
4
|
+
usage: <gm-include src="blocks/browser-window.html" url="example.com/pricing" image="assets/site.png" scroll="600" data-duration="5"></gm-include>
|
|
5
|
+
brand: <gm-include src="blocks/browser-window.html" theme="light" url="acme.com" dots="none" bar="#f4efe6" field="#ffffff" text="#3d2c1e" url-size="28" lock="off"></gm-include>
|
|
6
|
+
-->
|
|
7
|
+
<div class="gmb-browser gmb-browser--{{theme|dark}}" style="--w:{{width|1500}};--h:{{height|860}}" data-scroll="{{scroll|0}}" data-dots="{{dots}}" data-bar="{{bar}}" data-field="{{field}}" data-text="{{text}}" data-url-size="{{url-size}}" data-lock="{{lock|on}}" data-enter="{{enter|rise 0.9}}">
|
|
8
|
+
<div class="gmb-browser__bar"><i></i><i></i><i></i><div class="gmb-browser__url"><svg viewBox="0 0 24 24"><path d="M7 11V8a5 5 0 0 1 10 0v3M6 11h12v9H6z"/></svg>{{url|example.com}}</div></div>
|
|
9
|
+
<div class="gmb-browser__view"><div class="gmb-browser__page"><img class="gmb-browser__img" src="{{image}}" alt="">{{content}}</div></div>
|
|
10
|
+
</div>
|
|
11
|
+
<script>
|
|
12
|
+
(() => {
|
|
13
|
+
const root = document.currentScript.previousElementSibling;
|
|
14
|
+
const img = root.querySelector('.gmb-browser__img');
|
|
15
|
+
if (!img.getAttribute('src')) img.remove();
|
|
16
|
+
// brand palette: only what is given changes (the defaults are the stylesheet below)
|
|
17
|
+
const ds = root.dataset, bar = root.querySelector('.gmb-browser__bar'), url = root.querySelector('.gmb-browser__url');
|
|
18
|
+
const dots = (ds.dots || '').trim();
|
|
19
|
+
if (/^(none|off|no|false|0)$/i.test(dots)) bar.querySelectorAll(':scope > i').forEach((i) => i.remove());
|
|
20
|
+
else if (dots) { const c = dots.split(/\s*[,;]\s*|\s+(?![^(]*\))/).filter(Boolean); bar.querySelectorAll(':scope > i').forEach((i, k) => { i.style.background = c[Math.min(k, c.length - 1)]; }); }
|
|
21
|
+
if (ds.bar) bar.style.background = ds.bar;
|
|
22
|
+
if (ds.field) url.style.background = ds.field;
|
|
23
|
+
if (ds.text) url.style.color = ds.text;
|
|
24
|
+
const us = parseFloat(ds.urlSize);
|
|
25
|
+
if (us > 0) { url.style.fontSize = `calc(var(--px)*${us})`; url.style.height = `calc(var(--px)*${Math.max(40, us * 1.65)})`; if (us * 1.65 + 26 > 66) bar.style.height = `calc(var(--px)*${us * 1.65 + 26})`; const sv = url.querySelector('svg'); if (sv) sv.style.width = sv.style.height = `calc(var(--px)*${us * 0.75})`; }
|
|
26
|
+
if (/^(off|no|none|false|0)$/i.test((ds.lock || '').trim())) { const sv = url.querySelector('svg'); if (sv) sv.remove(); }
|
|
27
|
+
const px = parseFloat(root.dataset.scroll) || 0;
|
|
28
|
+
if (px) {
|
|
29
|
+
const page = root.querySelector('.gmb-browser__page');
|
|
30
|
+
gm.onFrame(root, (t, f) => {
|
|
31
|
+
const p = gm.interpolate(t, [0.8, Math.max(1.2, f.clipDuration - 0.8)], [0, 1], { easing: 'in-out-cubic' });
|
|
32
|
+
page.style.translate = `0 ${-p * px * gm.px}px`;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
</script>
|
|
37
|
+
<style>
|
|
38
|
+
.gmb-browser{width:calc(var(--px)*var(--w));max-width:calc(var(--stage-w,1920px) - var(--px)*120);border-radius:calc(var(--px)*20);overflow:hidden;background:#0f1218;
|
|
39
|
+
border:1px solid rgba(255,255,255,.12);box-shadow:0 calc(var(--px)*50) calc(var(--px)*140) rgba(0,0,0,.5)}
|
|
40
|
+
.gmb-browser__bar{display:flex;align-items:center;gap:calc(var(--px)*12);height:calc(var(--px)*66);padding:0 calc(var(--px)*24);background:#1a1e27;border-bottom:1px solid rgba(255,255,255,.06)}
|
|
41
|
+
.gmb-browser__bar i{width:calc(var(--px)*16);height:calc(var(--px)*16);border-radius:50%;background:#ff5f57}
|
|
42
|
+
.gmb-browser__bar i:nth-child(2){background:#febc2e} .gmb-browser__bar i:nth-child(3){background:#28c840}
|
|
43
|
+
.gmb-browser__url{flex:1;max-width:calc(var(--px)*720);margin:0 auto;display:flex;align-items:center;justify-content:center;gap:calc(var(--px)*10);height:calc(var(--px)*40);border-radius:99px;background:#0f1218;color:#a3acbd;font:500 calc(var(--px)*24)/1 Inter,system-ui,sans-serif}
|
|
44
|
+
.gmb-browser__url svg{width:calc(var(--px)*18);height:calc(var(--px)*18);fill:none;stroke:currentColor;stroke-width:2}
|
|
45
|
+
.gmb-browser__view{position:relative;height:calc(var(--px)*var(--h));overflow:hidden;background:#fff}
|
|
46
|
+
.gmb-browser--dark .gmb-browser__view{background:#0b0d12}
|
|
47
|
+
.gmb-browser__page{position:relative}
|
|
48
|
+
.gmb-browser__img{display:block;width:100%;height:auto}
|
|
49
|
+
.gmb-browser--light{background:#fff;border-color:rgba(0,0,0,.08)}
|
|
50
|
+
.gmb-browser--light .gmb-browser__bar{background:#f3f4f6;border-color:rgba(0,0,0,.06)}
|
|
51
|
+
.gmb-browser--light .gmb-browser__url{background:#fff;color:#6b7280}
|
|
52
|
+
</style>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!-- block:
|
|
2
|
+
description: Label with a hand-drawn style arrow pointing at a spot, plus a pulsing ring on the target. Coordinates are px in a 1080p frame.
|
|
3
|
+
params: x, y (target point), label, side (where the label sits: left|right|top|bottom, default right), distance (px, default 260), accent (color)
|
|
4
|
+
usage: <gm-include src="blocks/callout.html" x="980" y="420" label="Click Deploy" side="right" data-start="1" data-duration="3"></gm-include>
|
|
5
|
+
-->
|
|
6
|
+
<div class="gmb-call" style="--accent:{{accent|#ffd23f}}" data-x="{{x|960}}" data-y="{{y|540}}" data-side="{{side|right}}" data-dist="{{distance|260}}">
|
|
7
|
+
<div class="gmb-call__ring"></div>
|
|
8
|
+
<svg class="gmb-call__svg"><path class="gmb-call__path" pathLength="1"/><path class="gmb-call__head" pathLength="1"/></svg>
|
|
9
|
+
<div class="gmb-call__label" data-enter="pop 0.5 0.55">{{label|Look here}}</div>
|
|
10
|
+
</div>
|
|
11
|
+
<script>
|
|
12
|
+
(() => {
|
|
13
|
+
const root = document.currentScript.previousElementSibling;
|
|
14
|
+
const S = gm.px;
|
|
15
|
+
const x = +root.dataset.x * S, y = +root.dataset.y * S, d = +root.dataset.dist * S, side = root.dataset.side;
|
|
16
|
+
const dir = { right: [1, -0.35], left: [-1, -0.35], top: [0.25, -1], bottom: [0.25, 1] }[side] || [1, -0.35];
|
|
17
|
+
const n = Math.hypot(dir[0], dir[1]);
|
|
18
|
+
const lx = x + (dir[0] / n) * d, ly = y + (dir[1] / n) * d;
|
|
19
|
+
const ring = root.querySelector('.gmb-call__ring');
|
|
20
|
+
ring.style.left = x + 'px'; ring.style.top = y + 'px';
|
|
21
|
+
const label = root.querySelector('.gmb-call__label');
|
|
22
|
+
label.style.left = lx + 'px'; label.style.top = ly + 'px';
|
|
23
|
+
label.style.translate = side === 'left' ? '-100% -50%' : side === 'top' ? '-50% -100%' : side === 'bottom' ? '-50% 0' : '0 -50%';
|
|
24
|
+
const svg = root.querySelector('svg');
|
|
25
|
+
const draw = (lx2, ly2) => {
|
|
26
|
+
// arrow from the label edge to near the target, slightly curved
|
|
27
|
+
const vx = x - lx2, vy = y - ly2, len = Math.hypot(vx, vy) || 1, ux = vx / len, uy = vy / len;
|
|
28
|
+
const sx = lx2 + ux * 24 * S, sy = ly2 + uy * 24 * S;
|
|
29
|
+
const ex = x - ux * 46 * S, ey = y - uy * 46 * S;
|
|
30
|
+
const cx = (sx + ex) / 2 + uy * 50 * S, cy = (sy + ey) / 2 - ux * 50 * S;
|
|
31
|
+
const ang = Math.atan2(ey - cy, ex - cx), h = 22 * S;
|
|
32
|
+
const p = (a) => `${ex - Math.cos(ang + a) * h},${ey - Math.sin(ang + a) * h}`;
|
|
33
|
+
svg.querySelector('.gmb-call__path').setAttribute('d', `M${sx},${sy} Q${cx},${cy} ${ex},${ey}`);
|
|
34
|
+
svg.querySelector('.gmb-call__head').setAttribute('d', `M${p(0.5)} L${ex},${ey} L${p(-0.5)}`);
|
|
35
|
+
};
|
|
36
|
+
draw(lx, ly);
|
|
37
|
+
// Keep the label inside the frame (and re-aim the arrow).
|
|
38
|
+
gm.wait(document.fonts.ready.then(() => {
|
|
39
|
+
const r = label.getBoundingClientRect(), m = 40 * S;
|
|
40
|
+
let dx = 0, dy = 0;
|
|
41
|
+
if (r.right > gm.width - m) dx = gm.width - m - r.right; else if (r.left < m) dx = m - r.left;
|
|
42
|
+
if (r.bottom > gm.height - m) dy = gm.height - m - r.bottom; else if (r.top < m) dy = m - r.top;
|
|
43
|
+
if (dx || dy) { label.style.left = (lx + dx) + 'px'; label.style.top = (ly + dy) + 'px'; draw(lx + dx, ly + dy); }
|
|
44
|
+
}));
|
|
45
|
+
})();
|
|
46
|
+
</script>
|
|
47
|
+
<style>
|
|
48
|
+
.gmb-call{position:absolute;inset:0;pointer-events:none;z-index:40}
|
|
49
|
+
.gmb-call__svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
|
|
50
|
+
.gmb-call__path,.gmb-call__head{fill:none;stroke:var(--accent);stroke-width:calc(var(--px)*6);stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:1 1;filter:drop-shadow(0 calc(var(--px)*3) calc(var(--px)*6) rgba(0,0,0,.35))}
|
|
51
|
+
.gmb-call__path{animation:draw .55s var(--ease-in-out-cubic) .15s both}
|
|
52
|
+
.gmb-call__head{animation:draw .25s var(--ease-out-cubic) .65s both}
|
|
53
|
+
.gmb-call__ring{position:absolute;width:calc(var(--px)*72);height:calc(var(--px)*72);margin:calc(var(--px)*-36) 0 0 calc(var(--px)*-36);border-radius:50%;border:calc(var(--px)*5) solid var(--accent);
|
|
54
|
+
animation:pop-in .5s var(--ease-out-back) both,gmb-call-pulse 1.4s ease-out .6s infinite}
|
|
55
|
+
.gmb-call__label{position:absolute;white-space:nowrap;padding:calc(var(--px)*14) calc(var(--px)*26);border-radius:calc(var(--px)*16);background:var(--accent);color:#111;font-size:calc(var(--px)*40);font-weight:800;box-shadow:0 calc(var(--px)*12) calc(var(--px)*30) rgba(0,0,0,.3)}
|
|
56
|
+
@keyframes gmb-call-pulse{0%{box-shadow:0 0 0 0 color-mix(in oklab,var(--accent),transparent 30%)}100%{box-shadow:0 0 0 calc(var(--px)*30) transparent}}
|
|
57
|
+
</style>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!-- block:
|
|
2
|
+
description: Word-by-word captions (TikTok/Reels style) from a word-timed JSON file or an .srt file. The spoken word is highlighted.
|
|
3
|
+
params: file (captions.json or .srt; leave it out to use the page's <html data-words> transcript), color (text, default #fff), shadow (dark|light|none: dark for a dark or busy video, none or light on a light background), words (max words per line, default 5), accent (color), position (bottom|center|top), offset (distance from the top/bottom edge in px at 1080p; default 120, or 420 on vertical video to stay above the app buttons), size (px at 1080p; scales with the short side, default 64), weight (font weight, default 800), upper (true = UPPERCASE), fit (true: every line stays on one line, smaller when it is too wide)
|
|
4
|
+
hold: seconds the last line stays after its last word (default 0.8; 0 = gone with the word, so it does not run into the next scene).
|
|
5
|
+
Paging: lines break at sentence ends, pauses and commas, and are balanced, so a line never holds one lonely word.
|
|
6
|
+
usage: <gm-include src="blocks/captions.html" file="captions.json"></gm-include> JSON: [{"word":"Hello","start":0.2,"end":0.5}, …] (times in seconds, relative to the block's start)
|
|
7
|
+
-->
|
|
8
|
+
<div class="gmb-cap gmb-cap--{{position|bottom}} gmb-cap--shadow-{{shadow|dark}}" style="--accent:{{accent|#ffd23f}};--cap-color:{{color|#fff}};--size:{{size|64}};--weight:{{weight|800}}" data-file="{{file}}" data-fit="{{fit}}" data-words="{{words|5}}" data-offset="{{offset}}" data-upper="{{upper}}" data-hold="{{hold|0.8}}"><div class="gmb-cap__line"></div></div>
|
|
9
|
+
<script>
|
|
10
|
+
(() => {
|
|
11
|
+
const root = document.currentScript.previousElementSibling;
|
|
12
|
+
const line = root.firstElementChild;
|
|
13
|
+
const file = root.dataset.file;
|
|
14
|
+
const maxWords = Math.max(1, parseInt(root.dataset.words, 10) || 5);
|
|
15
|
+
const off = parseFloat(root.dataset.offset);
|
|
16
|
+
root.style.setProperty('--off', isFinite(off) ? off : gm.height > gm.width ? 420 : 120);
|
|
17
|
+
if (/^(true|1|yes)$/i.test(root.dataset.upper)) line.style.textTransform = 'uppercase';
|
|
18
|
+
let pages = [];
|
|
19
|
+
const fit = /^(true|1|yes)$/i.test(root.dataset.fit || '');
|
|
20
|
+
const parseSrt = (txt) => txt.replace(/\r/g, '').split(/\n\n+/).map((b) => {
|
|
21
|
+
const m = /(\d+):(\d+):(\d+)[,.](\d+)\s*-->\s*(\d+):(\d+):(\d+)[,.](\d+)/.exec(b);
|
|
22
|
+
if (!m) return null;
|
|
23
|
+
const t = (h, mi, s, ms) => +h * 3600 + +mi * 60 + +s + +ms / 1000;
|
|
24
|
+
const text = b.split('\n').slice(2).join(' ').trim();
|
|
25
|
+
return { start: t(m[1], m[2], m[3], m[4]), end: t(m[5], m[6], m[7], m[8]), text };
|
|
26
|
+
}).filter(Boolean);
|
|
27
|
+
const build = (words) => {
|
|
28
|
+
// 1) split into phrases at sentence ends and pauses; 2) split long phrases into even chunks (no orphan words)
|
|
29
|
+
const phrases = [];
|
|
30
|
+
let cur = [];
|
|
31
|
+
words.forEach((w, i) => {
|
|
32
|
+
const prev = words[i - 1];
|
|
33
|
+
if (cur.length && ((prev && w.start - prev.end > 0.6) || /[.!?…]$/.test(prev.word))) { phrases.push(cur); cur = []; }
|
|
34
|
+
cur.push(w);
|
|
35
|
+
});
|
|
36
|
+
if (cur.length) phrases.push(cur);
|
|
37
|
+
const out = [];
|
|
38
|
+
// Best split of each phrase into lines of at most maxWords: lines of similar length that end at
|
|
39
|
+
// punctuation, never a lonely single word, and never a line that runs across a comma.
|
|
40
|
+
const soft = (w) => /[,;:\u2013\u2014]$/.test(w.word);
|
|
41
|
+
for (const ph of phrases) {
|
|
42
|
+
const n = ph.length;
|
|
43
|
+
const best = [0], from = [0];
|
|
44
|
+
for (let i = 1; i <= n; i++) {
|
|
45
|
+
best[i] = Infinity;
|
|
46
|
+
for (let j = Math.max(0, i - maxWords); j < i; j++) {
|
|
47
|
+
const len = i - j;
|
|
48
|
+
let c = best[j] + 1 + 0.5 * (maxWords - len) ** 2;
|
|
49
|
+
if (len === 1 && n > 1) c += 50;
|
|
50
|
+
if (i < n && !soft(ph[i - 1])) c += 3;
|
|
51
|
+
for (let k = j; k < i - 1; k++) if (soft(ph[k])) c += 4;
|
|
52
|
+
if (c < best[i]) { best[i] = c; from[i] = j; }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const cuts = [];
|
|
56
|
+
for (let i = n; i > 0; i = from[i]) cuts.unshift([from[i], i]);
|
|
57
|
+
for (const [a, b] of cuts) out.push(ph.slice(a, b));
|
|
58
|
+
}
|
|
59
|
+
return out.map((ws) => ({ start: ws[0].start, end: ws[ws.length - 1].end, words: ws }));
|
|
60
|
+
};
|
|
61
|
+
// no file: the page's voice-over transcript (<html data-words="voice.json">), in video time
|
|
62
|
+
let abs = false;
|
|
63
|
+
if (!file && gm.words.length) { abs = true; pages = build(gm.words); root.__gmPages = pages; } else gm.wait(fetch(file).then((r) => { if (!r.ok) throw new Error('captions file not found: ' + file); return r.text(); }).then((txt) => {
|
|
64
|
+
let words;
|
|
65
|
+
if (/\.srt$/i.test(file)) {
|
|
66
|
+
words = [];
|
|
67
|
+
for (const seg of parseSrt(txt)) {
|
|
68
|
+
const ws = seg.text.split(/\s+/).filter(Boolean);
|
|
69
|
+
const d = (seg.end - seg.start) / ws.length;
|
|
70
|
+
ws.forEach((w, i) => words.push({ word: w, start: seg.start + i * d, end: seg.start + (i + 1) * d }));
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
const j = JSON.parse(txt);
|
|
74
|
+
words = (Array.isArray(j) ? j : j.words || j.segments?.flatMap((s) => s.words || []) || []).map((w) => ({ word: String(w.word ?? w.text ?? '').trim(), start: +(w.start ?? w.t0 ?? 0), end: +(w.end ?? w.t1 ?? 0) })).filter((w) => w.word);
|
|
75
|
+
}
|
|
76
|
+
pages = build(words);
|
|
77
|
+
root.__gmPages = pages; // read by `givemotion captions` (paging preview)
|
|
78
|
+
}));
|
|
79
|
+
let shown = -1;
|
|
80
|
+
gm.onFrame(root, (t, info) => {
|
|
81
|
+
if (abs) t = info.time;
|
|
82
|
+
const hold = parseFloat(root.dataset.hold); const H = isFinite(hold) ? hold : 0.8;
|
|
83
|
+
let pi = pages.findIndex((p, i) => t >= p.start - 0.05 && t < (pages[i + 1] ? Math.min(pages[i + 1].start - 0.05, p.end + H) : p.end + H));
|
|
84
|
+
if (pi !== shown) {
|
|
85
|
+
shown = pi;
|
|
86
|
+
line.innerHTML = pi < 0 ? '' : pages[pi].words.map((w) => `<span class="gmb-cap__w">${w.word.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]))}</span>`).join(' ');
|
|
87
|
+
// fit="true": each line on one line, made smaller when it is too wide (measured once per line, at its size)
|
|
88
|
+
if (fit && pi >= 0) {
|
|
89
|
+
line.style.fontSize = ''; line.style.whiteSpace = 'nowrap'; line.style.maxWidth = 'none';
|
|
90
|
+
const avail = root.clientWidth - parseFloat(getComputedStyle(root).paddingLeft) - parseFloat(getComputedStyle(root).paddingRight);
|
|
91
|
+
if (line.scrollWidth > avail && avail > 0) line.style.fontSize = (parseFloat(getComputedStyle(line).fontSize) * avail / line.scrollWidth).toFixed(2) + 'px';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (pi < 0) return;
|
|
95
|
+
const p = pages[pi];
|
|
96
|
+
const spans = line.children;
|
|
97
|
+
p.words.forEach((w, i) => {
|
|
98
|
+
const s = spans[i];
|
|
99
|
+
const on = t >= w.start && t < (p.words[i + 1] ? p.words[i + 1].start : w.end + H);
|
|
100
|
+
const pop = gm.interpolate(t - w.start, [0, 0.12], [0.92, 1], { easing: 'out-back' });
|
|
101
|
+
s.classList.toggle('is-on', on);
|
|
102
|
+
s.classList.toggle('is-past', t >= w.end && !on);
|
|
103
|
+
s.style.scale = on ? pop : 1;
|
|
104
|
+
s.style.opacity = t < w.start ? 0.55 : 1;
|
|
105
|
+
});
|
|
106
|
+
const enter = gm.interpolate(t - p.start, [0, 0.18], [0, 1], { easing: 'out-cubic' });
|
|
107
|
+
line.style.opacity = enter;
|
|
108
|
+
line.style.translate = `0 ${(1 - enter) * 16}px`;
|
|
109
|
+
});
|
|
110
|
+
})();
|
|
111
|
+
</script>
|
|
112
|
+
<style>
|
|
113
|
+
.gmb-cap{position:absolute;left:0;right:0;display:flex;justify-content:center;padding:0 calc(var(--px)*120);pointer-events:none;z-index:20}
|
|
114
|
+
.gmb-cap--bottom{bottom:calc(var(--px)*var(--off,120))} .gmb-cap--top{top:calc(var(--px)*var(--off,120))} .gmb-cap--center{top:50%;translate:0 -50%}
|
|
115
|
+
.gmb-cap__line{max-width:22ch;text-align:center;font-size:calc(var(--px)*var(--size));font-weight:var(--weight,800);line-height:1.18;color:var(--cap-color);letter-spacing:-.01em;
|
|
116
|
+
text-shadow:0 calc(var(--px)*4) calc(var(--px)*18) rgba(0,0,0,.55),0 0 2px rgba(0,0,0,.6);text-wrap:balance}
|
|
117
|
+
.gmb-cap--shadow-none .gmb-cap__line{text-shadow:none} .gmb-cap--shadow-light .gmb-cap__line{text-shadow:0 0 calc(var(--px)*14) rgba(255,255,255,.7)}
|
|
118
|
+
.gmb-cap__w{display:inline-block;padding:0 .12em;border-radius:.18em;transition:none}
|
|
119
|
+
.gmb-cap__w.is-on{color:#111;background:var(--accent);text-shadow:none}
|
|
120
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!-- block:
|
|
2
|
+
description: A list whose items appear one by one and get a check mark. Put <li> items between the tags.
|
|
3
|
+
params: title, every (seconds between items, default 0.6), first (seconds before the first item, default 0.4), accent (color)
|
|
4
|
+
usage: <gm-include src="blocks/checklist.html" title="Launch plan" every="0.7" data-duration="5"><li>Write the script</li><li>Record voiceover</li><li>Render</li></gm-include>
|
|
5
|
+
-->
|
|
6
|
+
<div class="gmb-check" style="--accent:{{accent|#4ade80}}" data-every="{{every|0.6}}" data-first="{{first|0.4}}">
|
|
7
|
+
<div class="gmb-check__title" data-enter="fade-up 0.6">{{title}}</div>
|
|
8
|
+
<ul class="gmb-check__list">{{content}}</ul>
|
|
9
|
+
</div>
|
|
10
|
+
<script>
|
|
11
|
+
(() => {
|
|
12
|
+
const root = document.currentScript.previousElementSibling;
|
|
13
|
+
const every = parseFloat(root.dataset.every) || 0.6, start = parseFloat(root.dataset.first) || 0;
|
|
14
|
+
[...root.querySelectorAll('li')].forEach((li, i) => {
|
|
15
|
+
const d = start + i * every;
|
|
16
|
+
li.style.setProperty('--d', d + 's');
|
|
17
|
+
li.insertAdjacentHTML('afterbegin', '<span class="gmb-check__box"><svg viewBox="0 0 24 24"><path d="M5 12.5l4.2 4.2L19 7" pathLength="1"/></svg></span>');
|
|
18
|
+
});
|
|
19
|
+
})();
|
|
20
|
+
</script>
|
|
21
|
+
<style>
|
|
22
|
+
.gmb-check{display:flex;flex-direction:column;gap:calc(var(--px)*30)}
|
|
23
|
+
.gmb-check__title{font-size:calc(var(--px)*56);font-weight:800;letter-spacing:-.02em}
|
|
24
|
+
.gmb-check__title:empty{display:none}
|
|
25
|
+
.gmb-check__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:calc(var(--px)*22)}
|
|
26
|
+
.gmb-check__list li{display:flex;align-items:center;gap:calc(var(--px)*24);font-size:calc(var(--px)*46);font-weight:500;animation:fade-left .55s var(--ease-out-expo) var(--d) both}
|
|
27
|
+
.gmb-check__box{flex:none;width:calc(var(--px)*52);height:calc(var(--px)*52);border-radius:calc(var(--px)*14);border:calc(var(--px)*3) solid rgba(255,255,255,.3);display:grid;place-items:center;position:relative;
|
|
28
|
+
animation:gmb-check-fill .35s var(--ease-out-cubic) calc(var(--d) + .35s) both}
|
|
29
|
+
.gmb-check__box svg{width:72%;height:72%;fill:none;stroke:#0b1210;stroke-width:3.2;stroke-linecap:round;stroke-linejoin:round}
|
|
30
|
+
.gmb-check__box path{stroke-dasharray:1 1;animation:draw .4s var(--ease-out-cubic) calc(var(--d) + .45s) both}
|
|
31
|
+
@keyframes gmb-check-fill{from{background:transparent;border-color:rgba(255,255,255,.3);scale:.85}to{background:var(--accent);border-color:var(--accent);scale:1}}
|
|
32
|
+
</style>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!-- block:
|
|
2
|
+
description: Editor window with syntax highlighting and an optional typing effect. Put the code between the tags as plain text (no escaping needed).
|
|
3
|
+
params: title (file name), lang (js|ts|py|bash|json|html|css|go|rust), typing (chars per second, 0 = no typing), delay (seconds before typing), width (px at 1080p; scales with the short side, default 1200), size (font px, default 34), highlight (lines to highlight: "6" or "2-4,6"), focus (lines to keep bright, the others are dimmed), gutter (true = line numbers), grow (true = the window grows while typing instead of keeping its final size), enter (its entrance, default "scale 0.7 0 out-expo"; "none" when you bring it in yourself)
|
|
4
|
+
targets: every line is <span class="gm-line" data-line="N"> (aim a camera at it, animate it with a timeline: '.gm-line[data-line="6"]'). Wrap a part of the code in ⟦ ⟧ to mark it: it becomes <span class="gm-mark" data-mark="1"> (2, 3… in order), e.g. await sleep(⟦2 ** i * 250⟧). The ⟦ ⟧ are not shown.
|
|
5
|
+
usage: <gm-include src="blocks/code-window.html" title="app.ts" lang="ts" typing="40" data-duration="6">const answer = await ai.run(prompt);</gm-include>
|
|
6
|
+
-->
|
|
7
|
+
<div class="gmb-code" style="--w:{{width|1200}};--fs:{{size|34}}" data-enter="{{enter|scale 0.7 0 out-expo}}">
|
|
8
|
+
<div class="gmb-code__bar"><i></i><i></i><i></i><span>{{title}}</span></div>
|
|
9
|
+
<textarea hidden class="gmb-code__src">{{content}}</textarea>
|
|
10
|
+
<pre class="gmb-code__pre"><code data-lang="{{lang|js}}" data-typing="{{typing|0}}" data-delay="{{delay|0.4}}" data-highlight="{{highlight}}" data-focus="{{focus}}" data-gutter="{{gutter}}" data-grow="{{grow}}"></code></pre>
|
|
11
|
+
</div>
|
|
12
|
+
<script>
|
|
13
|
+
(() => {
|
|
14
|
+
const root = document.currentScript.previousElementSibling;
|
|
15
|
+
const code = root.querySelector('code');
|
|
16
|
+
const lang = code.dataset.lang.toLowerCase();
|
|
17
|
+
// Dedent and trim
|
|
18
|
+
let lines = root.querySelector('.gmb-code__src').value.replace(/\t/g, ' ').split('\n');
|
|
19
|
+
while (lines.length && !lines[0].trim()) lines.shift();
|
|
20
|
+
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
|
|
21
|
+
const ind = Math.min(...lines.filter((l) => l.trim()).map((l) => l.match(/^ */)[0].length));
|
|
22
|
+
let src = lines.map((l) => l.slice(isFinite(ind) ? ind : 0)).join('\n');
|
|
23
|
+
// ⟦marked⟧ parts: remember where they are (in characters of the shown code) and drop the brackets
|
|
24
|
+
const marks = [];
|
|
25
|
+
// (offsets skip the newlines: each line becomes its own element below, so the DOM text has none)
|
|
26
|
+
{ let out = '', open = -1, nl = 0; for (const ch of src) { if (ch === '\u27e6') open = out.length - nl; else if (ch === '\u27e7' && open >= 0) { marks.push([open, out.length - nl]); open = -1; } else { if (ch === '\n') nl++; out += ch; } } src = out; }
|
|
27
|
+
const KW = {
|
|
28
|
+
js: 'await|async|break|case|catch|class|const|continue|default|delete|do|else|export|extends|finally|for|from|function|if|import|in|instanceof|let|new|of|return|static|super|switch|this|throw|try|typeof|var|void|while|yield|true|false|null|undefined',
|
|
29
|
+
py: 'and|as|assert|async|await|break|class|continue|def|del|elif|else|except|False|finally|for|from|global|if|import|in|is|lambda|None|nonlocal|not|or|pass|raise|return|True|try|while|with|yield|self|print',
|
|
30
|
+
bash: 'if|then|else|elif|fi|for|in|do|done|while|case|esac|function|return|export|local|echo|cd|npm|npx|git|sudo|curl|brew|pip|node',
|
|
31
|
+
go: 'break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var|nil|true|false',
|
|
32
|
+
rust: 'as|async|await|break|const|continue|crate|else|enum|extern|false|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|Self|static|struct|super|trait|true|type|unsafe|use|where|while',
|
|
33
|
+
css: '',
|
|
34
|
+
html: '', xml: '', svg: '',
|
|
35
|
+
};
|
|
36
|
+
KW.ts = KW.js + '|interface|type|enum|implements|private|public|readonly|as|keyof|namespace|declare|abstract';
|
|
37
|
+
KW.jsx = KW.js; KW.tsx = KW.ts; KW.sh = KW.bash; KW.python = KW.py;
|
|
38
|
+
const kw = KW[lang] ?? KW.js;
|
|
39
|
+
const comment = /^(py|python|bash|sh)$/.test(lang) ? '#[^\\n]*' : /^(html|xml|svg)$/.test(lang) ? '<!--[\\s\\S]*?-->' : '\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/';
|
|
40
|
+
const parts = [
|
|
41
|
+
['c', comment],
|
|
42
|
+
['s', '`(?:\\\\.|[^`])*`|"(?:\\\\.|[^"\\n])*"|\'(?:\\\\.|[^\'\\n])*\''],
|
|
43
|
+
...(/^(html|xml|svg)$/.test(lang) ? [['t', '<\\/?[A-Za-z][\\w-]*'], ['a', '\\b[\\w-]+(?==)']] : []),
|
|
44
|
+
...(lang === 'css' ? [['a', '[\\w-]+(?=\\s*:)'], ['n', '#[0-9a-fA-F]{3,8}\\b']] : []),
|
|
45
|
+
['n', '\\b\\d[\\d_]*(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b'],
|
|
46
|
+
...(kw ? [['k', '\\b(?:' + kw + ')\\b']] : []),
|
|
47
|
+
...(/^(html|xml|svg)$/.test(lang) ? [] : [['f', '\\b[A-Za-z_$][\\w$]*(?=\\s*\\()'], ['y', '\\b[A-Z][A-Za-z0-9_]*\\b']]),
|
|
48
|
+
['p', '=>|[{}()[\\];,.:=+\\-*/<>!&|?%]'],
|
|
49
|
+
];
|
|
50
|
+
const re = new RegExp(parts.map((p) => '(' + p[1] + ')').join('|'), 'g');
|
|
51
|
+
const esc = (s) => s.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]));
|
|
52
|
+
let html = '', last = 0, m;
|
|
53
|
+
if (lang === 'json') {
|
|
54
|
+
html = esc(src).replace(/("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\b\d+(?:\.\d+)?)/g, (x, s, colon, b, n) => s ? (colon ? `<span class="a">${s}</span>${colon}` : `<span class="s">${s}</span>`) : b ? `<span class="k">${b}</span>` : `<span class="n">${n}</span>`);
|
|
55
|
+
} else {
|
|
56
|
+
while ((m = re.exec(src))) {
|
|
57
|
+
html += esc(src.slice(last, m.index));
|
|
58
|
+
const gi = m.slice(1).findIndex((g) => g !== undefined);
|
|
59
|
+
html += `<span class="${parts[gi][0]}">${esc(m[0])}</span>`;
|
|
60
|
+
last = re.lastIndex;
|
|
61
|
+
if (m[0] === '') re.lastIndex++;
|
|
62
|
+
}
|
|
63
|
+
html += esc(src.slice(last));
|
|
64
|
+
}
|
|
65
|
+
// One element per line, so lines can be highlighted, focused, framed by a camera or animated.
|
|
66
|
+
{
|
|
67
|
+
const outLines = []; let cur = ''; const stack = [];
|
|
68
|
+
for (const m2 of html.matchAll(/(<span class="[^"]*">)|(<\/span>)|(\n)|([^<\n]+)/g)) {
|
|
69
|
+
if (m2[1]) { stack.push(m2[1]); cur += m2[1]; }
|
|
70
|
+
else if (m2[2]) { stack.pop(); cur += m2[2]; }
|
|
71
|
+
else if (m2[3]) { outLines.push(cur + '</span>'.repeat(stack.length)); cur = stack.join(''); }
|
|
72
|
+
else cur += m2[4];
|
|
73
|
+
}
|
|
74
|
+
outLines.push(cur + '</span>'.repeat(stack.length));
|
|
75
|
+
const sel = (v) => { const set = new Set(); for (const part of String(v || '').split(',')) { const r = /^\s*(\d+)\s*(?:-\s*(\d+))?\s*$/.exec(part); if (r) for (let i = +r[1]; i <= +(r[2] || r[1]); i++) set.add(i); } return set; };
|
|
76
|
+
const hl = sel(code.dataset.highlight), fo = sel(code.dataset.focus);
|
|
77
|
+
html = outLines.map((l, i) => `<span class="gm-line${hl.has(i + 1) ? ' is-hl' : ''}${fo.size && !fo.has(i + 1) ? ' is-dim' : ''}" data-line="${i + 1}">${l}</span>`).join('');
|
|
78
|
+
if (/^(true|1|yes)$/i.test(code.dataset.gutter)) root.classList.add('gmb-code--gutter');
|
|
79
|
+
}
|
|
80
|
+
code.innerHTML = html;
|
|
81
|
+
// Wrap the ⟦marked⟧ ranges (they may cross token colors).
|
|
82
|
+
marks.forEach(([a, b], i) => {
|
|
83
|
+
const walker = document.createTreeWalker(code, NodeFilter.SHOW_TEXT);
|
|
84
|
+
let pos = 0, n, start = null, end = null;
|
|
85
|
+
while ((n = walker.nextNode())) {
|
|
86
|
+
const len = n.data.length;
|
|
87
|
+
if (!start && a < pos + len) start = [n, a - pos];
|
|
88
|
+
if (!end && b <= pos + len) { end = [n, b - pos]; break; }
|
|
89
|
+
pos += len;
|
|
90
|
+
}
|
|
91
|
+
if (!start || !end) return;
|
|
92
|
+
const r = document.createRange(); r.setStart(...start); r.setEnd(...end);
|
|
93
|
+
const span = document.createElement('span'); span.className = 'gm-mark'; span.dataset.mark = String(i + 1);
|
|
94
|
+
span.appendChild(r.extractContents()); r.insertNode(span);
|
|
95
|
+
});
|
|
96
|
+
// Shrink the font if the longest line would not fit the window.
|
|
97
|
+
gm.wait(document.fonts.ready.then(() => {
|
|
98
|
+
const pre = root.querySelector('pre');
|
|
99
|
+
if (pre.scrollWidth > pre.clientWidth + 1) {
|
|
100
|
+
const fs = parseFloat(getComputedStyle(pre).fontSize);
|
|
101
|
+
const inner = pre.clientWidth - parseFloat(getComputedStyle(pre).paddingLeft) * 2;
|
|
102
|
+
const need = pre.scrollWidth - parseFloat(getComputedStyle(pre).paddingLeft) * 2;
|
|
103
|
+
pre.style.fontSize = Math.floor(fs * (inner / need) * 0.98) + 'px';
|
|
104
|
+
}
|
|
105
|
+
if (!/^(true|1|yes)$/i.test(code.dataset.grow)) pre.style.minHeight = pre.offsetHeight + 'px'; // keep the window size fixed while typing
|
|
106
|
+
}));
|
|
107
|
+
const cps = parseFloat(code.dataset.typing);
|
|
108
|
+
if (cps > 0) { code.setAttribute('data-type', String(cps)); code.setAttribute('data-type-delay', code.dataset.delay); code.setAttribute('data-type-caret', ''); }
|
|
109
|
+
})();
|
|
110
|
+
</script>
|
|
111
|
+
<style>
|
|
112
|
+
.gmb-code{position:relative;width:calc(var(--px)*var(--w));max-width:calc(var(--stage-w,1920px) - var(--px)*120);border-radius:calc(var(--px)*22);overflow:hidden;background:#0d1117;
|
|
113
|
+
border:1px solid rgba(255,255,255,.1);box-shadow:0 calc(var(--px)*40) calc(var(--px)*120) rgba(0,0,0,.45);text-align:left}
|
|
114
|
+
.gmb-code__bar{display:flex;align-items:center;gap:calc(var(--px)*12);height:calc(var(--px)*64);padding:0 calc(var(--px)*24);background:#161b22;border-bottom:1px solid rgba(255,255,255,.06)}
|
|
115
|
+
.gmb-code__bar i{width:calc(var(--px)*16);height:calc(var(--px)*16);border-radius:50%;background:#ff5f57}
|
|
116
|
+
.gmb-code__bar i:nth-child(2){background:#febc2e} .gmb-code__bar i:nth-child(3){background:#28c840}
|
|
117
|
+
.gmb-code__bar span{flex:1;text-align:center;margin-right:calc(var(--px)*72);color:#8b949e;font:500 calc(var(--px)*24)/1 Inter,system-ui,sans-serif}
|
|
118
|
+
.gmb-code__pre{margin:0;padding:calc(var(--px)*36) calc(var(--px)*44);font:calc(var(--px)*var(--fs))/1.55 "JetBrains Mono","SF Mono",Menlo,Consolas,"DejaVu Sans Mono",monospace;color:#e6edf3;white-space:pre;tab-size:2}
|
|
119
|
+
.gmb-code .c{color:#8b949e;font-style:italic} .gmb-code .s{color:#a5d6ff} .gmb-code .n{color:#79c0ff} .gmb-code .k{color:#ff7b72}
|
|
120
|
+
.gmb-code .f{color:#d2a8ff} .gmb-code .y{color:#ffa657} .gmb-code .p{color:#c9d1d9} .gmb-code .t{color:#7ee787} .gmb-code .a{color:#79c0ff}
|
|
121
|
+
.gmb-code .gm-caret{background:#58a6ff}
|
|
122
|
+
.gmb-code .gm-line{display:block;min-height:1lh;margin:0 calc(var(--px)*-44);padding:0 calc(var(--px)*44)}
|
|
123
|
+
.gmb-code .gm-line.is-hl{background:rgba(88,166,255,.16);box-shadow:inset calc(var(--px)*5) 0 0 #58a6ff}
|
|
124
|
+
.gmb-code .gm-line.is-dim{opacity:.35}
|
|
125
|
+
.gmb-code--gutter .gm-line::before{content:attr(data-line);display:inline-block;width:2.2em;margin-right:.8em;text-align:right;color:#6e7681}
|
|
126
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!-- block:
|
|
2
|
+
description: Before/after comparison: a divider sweeps across to reveal the "after" image. Labels on both sides.
|
|
3
|
+
params: before (image), after (image), before-label, after-label, width (px at 1080p; scales with the short side, default 1500), height (default 840), from (0-100, default 100), to (0-100, default 30)
|
|
4
|
+
usage: <gm-include src="blocks/compare.html" before="assets/old.png" after="assets/new.png" before-label="Before" after-label="After" data-duration="4"></gm-include>
|
|
5
|
+
-->
|
|
6
|
+
<div class="gmb-cmp" style="--w:{{width|1500}};--h:{{height|840}}" data-from="{{from|100}}" data-to="{{to|30}}" data-enter="scale 0.7">
|
|
7
|
+
<img class="gmb-cmp__img" src="{{after}}" alt="">
|
|
8
|
+
<div class="gmb-cmp__before"><img class="gmb-cmp__img" src="{{before}}" alt=""></div>
|
|
9
|
+
<div class="gmb-cmp__line"><div class="gmb-cmp__knob">⟷</div></div>
|
|
10
|
+
<div class="gmb-cmp__tag gmb-cmp__tag--l">{{before-label|Before}}</div>
|
|
11
|
+
<div class="gmb-cmp__tag gmb-cmp__tag--r">{{after-label|After}}</div>
|
|
12
|
+
</div>
|
|
13
|
+
<script>
|
|
14
|
+
(() => {
|
|
15
|
+
const root = document.currentScript.previousElementSibling;
|
|
16
|
+
const from = parseFloat(root.dataset.from), to = parseFloat(root.dataset.to);
|
|
17
|
+
const before = root.querySelector('.gmb-cmp__before'), line = root.querySelector('.gmb-cmp__line');
|
|
18
|
+
gm.onFrame(root, (t, f) => {
|
|
19
|
+
const p = gm.interpolate(t, [0.7, Math.min(f.clipDuration - 0.5, 2.6)], [from, to], { easing: 'in-out-cubic' });
|
|
20
|
+
before.style.clipPath = `inset(0 ${100 - p}% 0 0)`;
|
|
21
|
+
line.style.left = p + '%';
|
|
22
|
+
});
|
|
23
|
+
})();
|
|
24
|
+
</script>
|
|
25
|
+
<style>
|
|
26
|
+
.gmb-cmp{position:relative;width:calc(var(--px)*var(--w));height:calc(var(--px)*var(--h));max-width:calc(var(--stage-w,1920px) - var(--px)*120);border-radius:calc(var(--px)*24);overflow:hidden;background:#111;box-shadow:0 calc(var(--px)*40) calc(var(--px)*120) rgba(0,0,0,.45)}
|
|
27
|
+
.gmb-cmp__img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
28
|
+
.gmb-cmp__before{position:absolute;inset:0}
|
|
29
|
+
.gmb-cmp__line{position:absolute;top:0;bottom:0;width:calc(var(--px)*4);margin-left:calc(var(--px)*-2);background:#fff;box-shadow:0 0 calc(var(--px)*20) rgba(0,0,0,.5)}
|
|
30
|
+
.gmb-cmp__knob{position:absolute;top:50%;left:50%;translate:-50% -50%;width:calc(var(--px)*68);height:calc(var(--px)*68);border-radius:50%;background:#fff;color:#111;display:grid;place-items:center;font-size:calc(var(--px)*34);font-weight:700}
|
|
31
|
+
.gmb-cmp__tag{position:absolute;top:calc(var(--px)*28);padding:calc(var(--px)*10) calc(var(--px)*22);border-radius:99px;background:rgba(0,0,0,.6);color:#fff;font-size:calc(var(--px)*30);font-weight:700;backdrop-filter:blur(8px)}
|
|
32
|
+
.gmb-cmp__tag--l{left:calc(var(--px)*28)} .gmb-cmp__tag--r{right:calc(var(--px)*28)}
|
|
33
|
+
</style>
|