@usemote/media 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +110 -2
- package/dist/index.cjs +366 -0
- package/dist/index.d.cts +145 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +355 -0
- package/package.json +53 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 George Heavenson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,111 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @usemote/media
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Window frames and media particles for [Mote](https://mote.hearvie.dev) — photos,
|
|
4
|
+
GIFs and video that float with the shapes. **5.1 kB** brotlied.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pnpm add @usemote/core @usemote/media
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { FloatingField } from '@usemote/react';
|
|
12
|
+
import { windowShape } from '@usemote/media';
|
|
13
|
+
import { esports } from '@usemote/pack-esports';
|
|
14
|
+
|
|
15
|
+
<section style={{ position: 'relative', overflow: 'hidden' }}>
|
|
16
|
+
<FloatingField
|
|
17
|
+
packs={[esports]} mode="bob" count={9} avoid="center" depth
|
|
18
|
+
media={[
|
|
19
|
+
windowShape({ id: 'nova', kind: 'caption', src: '/nova.jpg', place: 'right' }),
|
|
20
|
+
windowShape({ id: 'demo', kind: 'browser', src: '/demo.webm',
|
|
21
|
+
poster: '/demo.jpg', clip: true, place: 'left' }),
|
|
22
|
+
]}
|
|
23
|
+
/>
|
|
24
|
+
<h1 style={{ position: 'relative' }}>Your headline</h1>
|
|
25
|
+
</section>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## A particle doesn't have to be a shape
|
|
29
|
+
|
|
30
|
+
Mote's renderer only ever assigns a shape's markup as the particle's contents,
|
|
31
|
+
so a framed photo participates in the field exactly like a drawn shape: same
|
|
32
|
+
layout, same depth, same motion presets, same scheduler.
|
|
33
|
+
|
|
34
|
+
## `media`, not `packs`
|
|
35
|
+
|
|
36
|
+
Media is **singular** — you want *this* portrait, once, on the right. Shapes in
|
|
37
|
+
a pack are drawn from a pool, where the seed decides how often each appears or
|
|
38
|
+
whether it appears at all. So media is passed as `media={[…]}`: each item is
|
|
39
|
+
rendered exactly once, in addition to the `count` shapes.
|
|
40
|
+
|
|
41
|
+
## `place`, not `avoid`
|
|
42
|
+
|
|
43
|
+
`avoid` is rejection-based and assumes shapes small enough to tuck around a
|
|
44
|
+
region. Media is large: a 280px portrait on a 700px hero has *no* valid
|
|
45
|
+
position, so avoidance simply fails. Media is anchored instead —
|
|
46
|
+
`left` `right` `top` `bottom` `top-left` `top-right` `bottom-left`
|
|
47
|
+
`bottom-right` `center` — with a margin kept from the field edge so a frame
|
|
48
|
+
reads as placed rather than clipped.
|
|
49
|
+
|
|
50
|
+
## 13 frame kinds
|
|
51
|
+
|
|
52
|
+
`browser` `app` `bare` `caption` `phone` `monitor` `polaroid` `stream`
|
|
53
|
+
`post` `circle` `message` `slide` `ticket`
|
|
54
|
+
|
|
55
|
+
Each takes two colours from the active pack theme, so frames inherit whatever
|
|
56
|
+
pack they sit in.
|
|
57
|
+
|
|
58
|
+
### Chrome drops detail, it doesn't shrink
|
|
59
|
+
|
|
60
|
+
A frame in a field can be any size, and chrome that merely scales turns to
|
|
61
|
+
mush. Each tier removes detail instead:
|
|
62
|
+
|
|
63
|
+
| Tier | Width | Behaviour |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| Small | `<270px` | Controls merge into one capsule. No text, no side rail. |
|
|
66
|
+
| Medium | `270–339` | Three tonal dots, title as a bar, empty URL pill. |
|
|
67
|
+
| Large | `340+` | Real URL and title text, play chip on clips. |
|
|
68
|
+
|
|
69
|
+
Frames build at the particle's **actual** width, so the tier is always right.
|
|
70
|
+
|
|
71
|
+
## Video, poster and reduced motion
|
|
72
|
+
|
|
73
|
+
A video is emitted **without an `autoplay` attribute**, deliberately. Markup
|
|
74
|
+
must be byte-identical on the server and the client or hydration mismatches,
|
|
75
|
+
so playback is a runtime decision made by the renderer:
|
|
76
|
+
|
|
77
|
+
- `prefers-reduced-motion: reduce` → never started. The poster frame is what
|
|
78
|
+
the user sees, which is why **`poster` is strongly recommended** — a paused
|
|
79
|
+
video without one is a blank rectangle.
|
|
80
|
+
- Offscreen (via `IntersectionObserver`) → paused.
|
|
81
|
+
- Field paused or destroyed → paused, and decoders released.
|
|
82
|
+
- A refused autoplay is swallowed; the poster is a fine outcome.
|
|
83
|
+
|
|
84
|
+
Video is `muted`, `loop`, `playsinline`, `preload="metadata"`, and kept out of
|
|
85
|
+
the tab order.
|
|
86
|
+
|
|
87
|
+
## Frames are scene objects, not UI
|
|
88
|
+
|
|
89
|
+
No hover states, no buttons, nothing focusable or clickable. The layer is
|
|
90
|
+
`aria-hidden` and `pointer-events: none`. Images are `loading="lazy"` and
|
|
91
|
+
decorative by default; pass `alt` if an image carries meaning.
|
|
92
|
+
|
|
93
|
+
## Placeholders
|
|
94
|
+
|
|
95
|
+
A frame with no `src` renders hatching, optionally with a shape from the pack
|
|
96
|
+
via `art`, so an empty frame still looks intentional rather than broken.
|
|
97
|
+
|
|
98
|
+
## Known limit
|
|
99
|
+
|
|
100
|
+
Frames are HTML, not SVG, so they render through the DOM renderer only.
|
|
101
|
+
`renderFieldSVG()` — the static export for OG images — cannot include them.
|
|
102
|
+
|
|
103
|
+
## Inspiration
|
|
104
|
+
|
|
105
|
+
Architecturally modelled on **[Navii](https://github.com/uxderrick/navii)** by
|
|
106
|
+
**[Derrick Tsorme](https://github.com/uxderrick)**. The frame designs are by
|
|
107
|
+
Mote's own designer.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/copy.ts
|
|
4
|
+
var COPY = {
|
|
5
|
+
esports: {
|
|
6
|
+
url: "publication.gg/live",
|
|
7
|
+
title: "Grand Final \xB7 Lagos",
|
|
8
|
+
stream: ["K9 vs NOVA", "2\u20131"],
|
|
9
|
+
caps: [["K9", "ACCRA"], ["NOVA", "LAGOS"], ["ADA", "ACCRA"], ["DEMZY", "LAGOS"]]
|
|
10
|
+
},
|
|
11
|
+
devtools: {
|
|
12
|
+
url: "localhost:3000",
|
|
13
|
+
title: "deploy \u2014 main",
|
|
14
|
+
caps: [["v2.4.0", "SHIPPED"], ["main", "GREEN"], ["#128", "MERGED"], ["api", "200 OK"]]
|
|
15
|
+
},
|
|
16
|
+
ai: {
|
|
17
|
+
url: "chat.model.ai",
|
|
18
|
+
title: "Inference",
|
|
19
|
+
caps: [["Prompt", "TEXT"], ["Vision", "IMAGE"], ["Voice", "AUDIO"], ["Agent", "RUN"]]
|
|
20
|
+
},
|
|
21
|
+
fintech: {
|
|
22
|
+
url: "app.ledger.io",
|
|
23
|
+
title: "Portfolio",
|
|
24
|
+
caps: [["+2.4%", "TODAY"], ["Savings", "GOAL"], ["Card", "ACTIVE"], ["NGN/USD", "LIVE"]]
|
|
25
|
+
},
|
|
26
|
+
creative: {
|
|
27
|
+
url: "studio.work",
|
|
28
|
+
title: "Moodboard",
|
|
29
|
+
caps: [["Brand", "V3"], ["Poster", "DRAFT"], ["Type", "SPEC"], ["Motion", "WIP"]]
|
|
30
|
+
},
|
|
31
|
+
space: {
|
|
32
|
+
url: "mission.live",
|
|
33
|
+
title: "Telemetry",
|
|
34
|
+
caps: [["Orbit 3", "LIVE"], ["Mars", "SOL 214"], ["Launch", "T-10"], ["Lunar", "PASS"]]
|
|
35
|
+
},
|
|
36
|
+
cyber: {
|
|
37
|
+
url: "vault.secure",
|
|
38
|
+
title: "audit.log",
|
|
39
|
+
caps: [["Firewall", "ON"], ["2FA", "VERIFIED"], ["Scan", "CLEAN"], ["Vault", "LOCKED"]]
|
|
40
|
+
},
|
|
41
|
+
health: {
|
|
42
|
+
url: "care.app",
|
|
43
|
+
title: "Vitals",
|
|
44
|
+
caps: [["72 BPM", "REST"], ["Sleep", "7H 40"], ["Steps", "9,420"], ["Dose", "08:00"]]
|
|
45
|
+
},
|
|
46
|
+
education: {
|
|
47
|
+
url: "learn.school",
|
|
48
|
+
title: "Lesson 04",
|
|
49
|
+
caps: [["Algebra", "UNIT 2"], ["Physics", "LAB"], ["History", "QUIZ"], ["Art", "STUDIO"]]
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var fallbackCopy = COPY.esports;
|
|
53
|
+
|
|
54
|
+
// src/util.ts
|
|
55
|
+
var R = Math.round;
|
|
56
|
+
var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
57
|
+
var hexA = (hex, a) => /^#[0-9a-f]{6}$/i.test(hex) ? hex + R(a * 255).toString(16).padStart(2, "0") : hex;
|
|
58
|
+
var FONT = "'Schibsted Grotesk',system-ui,-apple-system,sans-serif";
|
|
59
|
+
var MONO = "'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace";
|
|
60
|
+
var tier = (w) => w >= 340 ? "L" : w >= 270 ? "M" : "S";
|
|
61
|
+
var lock = (s, col) => `<svg width="${s}" height="${R(s * 1.2)}" viewBox="0 0 10 12" style="flex:none;display:block"><path d="M2.5 5.5V3.5a2.5 2.5 0 0 1 5 0v2" fill="none" stroke="${col}" stroke-width="1.6"/><rect x="1" y="5" width="8" height="6.5" rx="1.6" fill="${col}"/></svg>`;
|
|
62
|
+
var lights = (T, dot, gap, col) => T === "S" ? `<span style="display:block;width:${dot * 3 + gap * 2}px;height:${dot}px;border-radius:${dot}px;background:${col};opacity:.55"></span>` : [1, 0.6, 0.35].map(
|
|
63
|
+
(o) => `<span style="display:block;flex:none;width:${dot}px;height:${dot}px;border-radius:50%;background:${col};opacity:${o}"></span>`
|
|
64
|
+
).join("");
|
|
65
|
+
|
|
66
|
+
// src/media.ts
|
|
67
|
+
var VIDEO_EXT = /\.(mp4|webm|mov|m4v)(\?|#|$)/i;
|
|
68
|
+
var isVideo = (src) => VIDEO_EXT.test(src);
|
|
69
|
+
var attr = (v) => v.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
70
|
+
function mediaSurface(o, mw, mh, rad) {
|
|
71
|
+
const c = o.c;
|
|
72
|
+
let inner;
|
|
73
|
+
if (o.src && isVideo(o.src)) {
|
|
74
|
+
inner = `<video data-mote-media src="${attr(o.src)}"` + (o.poster ? ` poster="${attr(o.poster)}"` : "") + ` muted loop playsinline preload="metadata" tabindex="-1" aria-hidden="true" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block"></video>`;
|
|
75
|
+
} else if (o.src) {
|
|
76
|
+
inner = `<img src="${attr(o.src)}" alt="${attr(o.alt ?? "")}" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block">`;
|
|
77
|
+
} else {
|
|
78
|
+
const s = R(Math.min(mw, mh) * 0.46);
|
|
79
|
+
inner = `<div style="position:absolute;inset:0;background:${c.d};background-image:repeating-linear-gradient(135deg,${hexA(c.f, 0.08)} 0 10px,transparent 10px 20px)"></div>` + (o.art ? `<div style="position:absolute;left:50%;top:50%;width:${s}px;height:${s}px;margin:${-s / 2}px 0 0 ${-s / 2}px">${o.art}</div>` : "");
|
|
80
|
+
}
|
|
81
|
+
if (o.clip) {
|
|
82
|
+
const bh = Math.max(4, R(mw * 0.014));
|
|
83
|
+
const p = Math.max(8, R(mw * 0.045));
|
|
84
|
+
inner += `<div style="position:absolute;left:${p}px;right:${p}px;bottom:${p}px;height:${bh}px;border-radius:${bh}px;background:${hexA(c.f, 0.35)}"><div style="width:38%;height:100%;border-radius:${bh}px;background:${c.f}"></div></div>`;
|
|
85
|
+
if (mw >= 300) {
|
|
86
|
+
const fs = clamp(R(mw * 0.032), 10, 12);
|
|
87
|
+
inner += `<div style="position:absolute;left:${p}px;top:${p}px;display:flex;align-items:center;gap:6px;background:${c.f};color:${c.d};border-radius:4px;padding:4px 7px;font:500 ${fs}px/1 ${MONO};letter-spacing:.08em"><span style="width:0;height:0;border-left:${R(fs * 0.7)}px solid ${c.d};border-top:${R(fs * 0.42)}px solid transparent;border-bottom:${R(fs * 0.42)}px solid transparent"></span>0:24</div>`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return `<div style="position:relative;width:${mw}px;height:${mh}px;overflow:hidden;border-radius:${rad};background:${c.d}">${inner}</div>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/frames.ts
|
|
94
|
+
var FORCE = {
|
|
95
|
+
phone: [9, 19.5],
|
|
96
|
+
circle: [1, 1],
|
|
97
|
+
slide: [4, 3],
|
|
98
|
+
polaroid: [1, 1]
|
|
99
|
+
};
|
|
100
|
+
var SCALE = {
|
|
101
|
+
phone: 0.5,
|
|
102
|
+
circle: 0.62,
|
|
103
|
+
slide: 0.72,
|
|
104
|
+
polaroid: 0.78,
|
|
105
|
+
message: 0.85,
|
|
106
|
+
post: 0.82,
|
|
107
|
+
ticket: 1.1,
|
|
108
|
+
monitor: 1.05
|
|
109
|
+
};
|
|
110
|
+
var KINDS = [
|
|
111
|
+
"browser",
|
|
112
|
+
"app",
|
|
113
|
+
"bare",
|
|
114
|
+
"caption",
|
|
115
|
+
"phone",
|
|
116
|
+
"monitor",
|
|
117
|
+
"polaroid",
|
|
118
|
+
"stream",
|
|
119
|
+
"post",
|
|
120
|
+
"circle",
|
|
121
|
+
"message",
|
|
122
|
+
"slide",
|
|
123
|
+
"ticket"
|
|
124
|
+
];
|
|
125
|
+
var BUILDERS = {
|
|
126
|
+
browser(o, k) {
|
|
127
|
+
const { c, w, T, ratio, copy, rr, shadow } = k;
|
|
128
|
+
const rim = R(clamp(w * 0.012, 3, 5));
|
|
129
|
+
const bar = R(clamp(w * 0.08, 20, 30));
|
|
130
|
+
const dot = R(clamp(bar * 0.34, 7, 10));
|
|
131
|
+
const gap = R(dot * 0.6);
|
|
132
|
+
const pad = R(bar * 0.45);
|
|
133
|
+
const mw = w - rim * 2;
|
|
134
|
+
const mh = R(mw * ratio[1] / ratio[0]);
|
|
135
|
+
const h = bar + mh + rim;
|
|
136
|
+
const lw = dot * 3 + gap * 2;
|
|
137
|
+
const ph = R(bar * 0.52);
|
|
138
|
+
const url = T === "L" ? `${lock(R(ph * 0.42), c.d)}<span style="font:500 ${clamp(R(bar * 0.36), 9, 11)}px/1 ${MONO};color:${c.d};opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${copy.url}</span>` : "";
|
|
139
|
+
const html = `<div style="width:${w}px;height:${h}px;display:flex;flex-direction:column;border-radius:${rr}px;background:${c.f};box-shadow:${shadow};overflow:hidden"><div style="height:${bar}px;flex:none;display:grid;grid-template-columns:${lw}px minmax(0,1fr) ${lw}px;align-items:center;gap:${pad}px;padding:0 ${pad}px"><div style="display:flex;gap:${gap}px;align-items:center">${lights(T, dot, gap, c.d)}</div><div style="display:flex;justify-content:center;min-width:0"><div style="height:${ph}px;width:${T === "S" ? "70%" : "86%"};border-radius:${ph}px;background:${hexA(c.d, 0.16)};display:flex;align-items:center;justify-content:center;gap:5px;padding:0 8px;box-sizing:border-box;min-width:0">${url}</div></div><div></div></div><div style="padding:0 ${rim}px ${rim}px">${mediaSurface(o, mw, mh, `0 0 ${Math.max(2, rr - rim)}px ${Math.max(2, rr - rim)}px`)}</div></div>`;
|
|
140
|
+
return { html, h };
|
|
141
|
+
},
|
|
142
|
+
app(o, k) {
|
|
143
|
+
const { c, w, T, ratio, copy, font, rr, shadow } = k;
|
|
144
|
+
const rim = R(clamp(w * 0.012, 3, 5));
|
|
145
|
+
const bar = R(clamp(w * 0.095, 22, 34));
|
|
146
|
+
const dot = R(clamp(bar * 0.3, 7, 10));
|
|
147
|
+
const gap = R(dot * 0.6);
|
|
148
|
+
const pad = R(bar * 0.42);
|
|
149
|
+
const rail = T === "S" ? 0 : R(clamp(w * 0.075, 20, 30));
|
|
150
|
+
const foot = R(clamp(w * 0.035, 8, 12));
|
|
151
|
+
const mw = w - rim - (rail || rim);
|
|
152
|
+
const mh = R(mw * ratio[1] / ratio[0]);
|
|
153
|
+
const h = bar + mh + foot;
|
|
154
|
+
const lw = dot * 3 + gap * 2;
|
|
155
|
+
const title = T === "L" ? `<span style="font:700 ${clamp(R(bar * 0.38), 10, 13)}px/1 ${font};color:${c.d};white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${copy.title}</span>` : T === "M" ? `<span style="display:block;width:${R(w * 0.22)}px;height:${R(dot * 0.8)}px;border-radius:${dot}px;background:${c.d};opacity:.35"></span>` : "";
|
|
156
|
+
const right = T === "S" ? "" : [0, 1].map(
|
|
157
|
+
() => `<span style="display:block;width:${dot}px;height:${dot}px;border-radius:2px;background:${c.d};opacity:.3"></span>`
|
|
158
|
+
).join("");
|
|
159
|
+
const ic = R(rail * 0.46);
|
|
160
|
+
const railHtml = rail ? `<div style="width:${rail}px;flex:none;display:flex;flex-direction:column;align-items:center;gap:${R(rail * 0.3)}px;padding-top:${R(rail * 0.15)}px">` + [0.6, 0.28, 0.28].map(
|
|
161
|
+
(op) => `<span style="display:block;width:${ic}px;height:${ic}px;border-radius:${R(ic * 0.28)}px;background:${c.d};opacity:${op}"></span>`
|
|
162
|
+
).join("") + `</div>` : "";
|
|
163
|
+
const html = `<div style="width:${w}px;height:${h}px;display:flex;flex-direction:column;border-radius:${rr}px;background:${c.f};box-shadow:${shadow};overflow:hidden"><div style="height:${bar}px;flex:none;display:grid;grid-template-columns:${lw}px minmax(0,1fr) ${lw}px;align-items:center;gap:${pad}px;padding:0 ${pad}px"><div style="display:flex;gap:${gap}px;align-items:center">${lights(T, dot, gap, c.d)}</div><div style="display:flex;justify-content:center;min-width:0">${title}</div><div style="display:flex;gap:${gap}px;justify-content:flex-end">${right}</div></div><div style="display:flex;padding:0 ${rim}px 0 ${rail ? 0 : rim}px">${railHtml}${mediaSurface(o, mw, mh, "4px")}</div><div style="height:${foot}px;flex:none"></div></div>`;
|
|
164
|
+
return { html, h };
|
|
165
|
+
},
|
|
166
|
+
caption(o, k) {
|
|
167
|
+
const { c, w, T, ratio, copy, font, rr, shadow, ring } = k;
|
|
168
|
+
const strip = R(clamp(w * 0.13, 30, 46));
|
|
169
|
+
const mh = R(w * ratio[1] / ratio[0]);
|
|
170
|
+
const h = mh + strip;
|
|
171
|
+
const cap = copy.caps[(o.idx ?? 0) % copy.caps.length];
|
|
172
|
+
const html = `<div style="width:${w}px;height:${h}px;border-radius:${rr}px;box-shadow:0 0 0 ${ring}px ${c.f},${shadow};overflow:hidden;display:flex;flex-direction:column;background:${c.f}">${mediaSurface(o, w, mh, "0")}<div style="height:${strip}px;flex:none;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 ${R(strip * 0.38)}px;color:${c.d}"><span style="font:800 ${R(strip * 0.42)}px/1 ${font};letter-spacing:-0.01em;white-space:nowrap">${cap[0]}</span>` + (T !== "S" ? `<span style="font:500 ${clamp(R(strip * 0.27), 9, 11)}px/1 ${MONO};letter-spacing:.12em;opacity:.7;white-space:nowrap">${cap[1]}</span>` : "") + `</div></div>`;
|
|
173
|
+
return { html, h };
|
|
174
|
+
},
|
|
175
|
+
phone(o, k) {
|
|
176
|
+
const { c, w, shadow } = k;
|
|
177
|
+
const b = R(clamp(w * 0.04, 5, 9));
|
|
178
|
+
const rad = R(w * 0.17);
|
|
179
|
+
const mw = w - b * 2;
|
|
180
|
+
const mh = R(mw * 19.5 / 9);
|
|
181
|
+
const h = mh + b * 2;
|
|
182
|
+
const iw = R(w * 0.3);
|
|
183
|
+
const ih = R(clamp(w * 0.07, 8, 16));
|
|
184
|
+
const hb = R(w * 0.34);
|
|
185
|
+
const btn = (side, top, hh) => `<span style="position:absolute;${side}:-3px;top:${R(h * top)}px;width:3px;height:${R(h * hh)}px;border-radius:2px;background:${c.f}"></span>`;
|
|
186
|
+
const html = `<div style="position:relative;width:${w}px;height:${h}px">${btn("left", 0.18, 0.07)}${btn("left", 0.28, 0.11)}${btn("right", 0.24, 0.13)}<div style="position:relative;width:${w}px;height:${h}px;border-radius:${rad}px;background:${c.f};padding:${b}px;box-sizing:border-box;box-shadow:${shadow}">${mediaSurface(o, mw, mh, `${rad - b}px`)}<span style="position:absolute;left:50%;top:${b + R(ih * 0.6)}px;width:${iw}px;height:${ih}px;margin-left:${-iw / 2}px;border-radius:${ih}px;background:${c.f}"></span><span style="position:absolute;left:50%;bottom:${b + R(ih * 0.5)}px;width:${hb}px;height:${Math.max(3, R(w * 0.02))}px;margin-left:${-hb / 2}px;border-radius:4px;background:${c.f};opacity:.85"></span></div></div>`;
|
|
187
|
+
return { html, h };
|
|
188
|
+
},
|
|
189
|
+
monitor(o, k) {
|
|
190
|
+
const { c, w } = k;
|
|
191
|
+
const ratio = k.ratio[1] > k.ratio[0] ? [16, 10] : k.ratio;
|
|
192
|
+
const b = R(clamp(w * 0.025, 5, 10));
|
|
193
|
+
const chin = R(clamp(w * 0.055, 12, 20));
|
|
194
|
+
const mw = w - b * 2;
|
|
195
|
+
const mh = R(mw * ratio[1] / ratio[0]);
|
|
196
|
+
const nw = R(w * 0.12);
|
|
197
|
+
const nh = R(w * 0.09);
|
|
198
|
+
const bw = R(w * 0.38);
|
|
199
|
+
const bh = R(clamp(w * 0.022, 5, 9));
|
|
200
|
+
const h = b + mh + chin + nh + bh;
|
|
201
|
+
const html = `<div style="width:${w}px;height:${h}px;display:flex;flex-direction:column;align-items:center;filter:drop-shadow(0 ${R(w * 0.03)}px ${R(w * 0.05)}px rgba(0,0,0,.3))"><div style="width:${w}px;border-radius:${R(clamp(w * 0.03, 6, 12))}px;background:${c.f};padding:${b}px ${b}px 0;box-sizing:border-box">${mediaSurface(o, mw, mh, "3px")}<div style="height:${chin}px;display:flex;align-items:center;justify-content:center"><span style="width:${R(chin * 0.32)}px;height:${R(chin * 0.32)}px;border-radius:50%;background:${c.d};opacity:.35"></span></div></div><div style="width:${nw}px;height:${nh}px;flex:none;background:${c.f};clip-path:polygon(18% 0,82% 0,100% 100%,0 100%)"></div><div style="width:${bw}px;height:${bh}px;flex:none;border-radius:${bh}px;background:${c.f}"></div></div>`;
|
|
202
|
+
return { html, h };
|
|
203
|
+
},
|
|
204
|
+
polaroid(o, k) {
|
|
205
|
+
const { c, w, T, copy, shadow } = k;
|
|
206
|
+
const p = R(w * 0.06);
|
|
207
|
+
const bot = R(w * 0.24);
|
|
208
|
+
const mw = w - p * 2;
|
|
209
|
+
const mh = mw;
|
|
210
|
+
const h = p + mh + bot;
|
|
211
|
+
const cap = copy.caps[(o.idx ?? 0) % copy.caps.length][0];
|
|
212
|
+
const tw = R(w * 0.36);
|
|
213
|
+
const th = R(clamp(w * 0.1, 14, 30));
|
|
214
|
+
const html = `<div style="position:relative;width:${w}px;height:${h}px"><div style="width:${w}px;height:${h}px;background:${c.f};border-radius:3px;padding:${p}px ${p}px 0;box-sizing:border-box;box-shadow:${shadow}">${mediaSurface(o, mw, mh, "1px")}<div style="height:${bot}px;display:flex;align-items:center;justify-content:center">` + (T === "S" && w < 180 ? "" : `<span style="font:italic 400 ${R(bot * 0.44)}px/1 'Instrument Serif',Georgia,serif;color:${c.d};opacity:.85">${cap}</span>`) + `</div></div><span style="position:absolute;left:50%;top:${-R(th * 0.45)}px;width:${tw}px;height:${th}px;margin-left:${-tw / 2}px;transform:rotate(-4deg);background:${hexA(c.d, 0.22)}"></span></div>`;
|
|
215
|
+
return { html, h };
|
|
216
|
+
},
|
|
217
|
+
stream(o, k) {
|
|
218
|
+
const { c, w, T, copy, font, rr, ring, shadow } = k;
|
|
219
|
+
const mh = R(w * k.ratio[1] / k.ratio[0]);
|
|
220
|
+
const h = mh;
|
|
221
|
+
const p = Math.max(8, R(w * 0.04));
|
|
222
|
+
const fs = clamp(R(w * 0.03), 9, 12);
|
|
223
|
+
const st = copy.stream ?? [copy.title, copy.caps[(o.idx ?? 0) % copy.caps.length][1]];
|
|
224
|
+
const live = `<div style="position:absolute;left:${p}px;top:${p}px;display:flex;align-items:center;gap:${R(fs * 0.5)}px;background:${c.f};color:${c.d};border-radius:4px;padding:${R(fs * 0.45)}px ${R(fs * 0.6)}px;font:500 ${fs}px/1 ${MONO};letter-spacing:.1em"><span style="width:${R(fs * 0.65)}px;height:${R(fs * 0.65)}px;border-radius:50%;background:${c.d}"></span>${T === "S" ? "" : "LIVE"}</div>`;
|
|
225
|
+
const viewers = T === "L" ? `<div style="position:absolute;right:${p}px;top:${p}px;background:${hexA(c.d, 0.75)};color:${c.f};border-radius:4px;padding:${R(fs * 0.45)}px ${R(fs * 0.6)}px;font:500 ${fs}px/1 ${MONO};letter-spacing:.06em">12.4K WATCHING</div>` : "";
|
|
226
|
+
const lh = R(clamp(w * 0.085, 18, 32));
|
|
227
|
+
const lower = T === "S" ? "" : `<div style="position:absolute;left:${p}px;bottom:${p}px;right:${R(w * 0.22)}px;height:${lh}px;display:flex"><div style="flex:1;min-width:0;background:${c.f};color:${c.d};display:flex;align-items:center;padding:0 ${R(lh * 0.4)}px;font:800 ${R(lh * 0.44)}px/1 ${font};white-space:nowrap;overflow:hidden">${T === "L" ? st[0] : ""}</div><div style="background:${c.d};color:${c.f};display:flex;align-items:center;padding:0 ${R(lh * 0.35)}px;font:500 ${R(lh * 0.42)}px/1 ${MONO};white-space:nowrap">${st[1]}</div></div>`;
|
|
228
|
+
const html = `<div style="position:relative;width:${w}px;height:${h}px;border-radius:${rr}px;box-shadow:0 0 0 ${ring}px ${c.f},${shadow};overflow:hidden">${mediaSurface({ ...o, clip: false }, w, mh, `${rr}px`)}${live}${viewers}${lower}</div>`;
|
|
229
|
+
return { html, h };
|
|
230
|
+
},
|
|
231
|
+
post(o, k) {
|
|
232
|
+
const { c, w, T, copy, font, rr, shadow } = k;
|
|
233
|
+
const p = R(clamp(w * 0.045, 8, 14));
|
|
234
|
+
const av = R(clamp(w * 0.1, 18, 32));
|
|
235
|
+
const mw = w - p * 2;
|
|
236
|
+
const mh = R(mw * k.ratio[1] / k.ratio[0]);
|
|
237
|
+
const cap = copy.caps[(o.idx ?? 0) % copy.caps.length];
|
|
238
|
+
const who = T === "L" ? `<div style="display:flex;flex-direction:column;gap:3px;min-width:0"><span style="font:800 ${R(av * 0.42)}px/1 ${font};color:${c.d};white-space:nowrap">${cap[0]}</span><span style="font:500 ${R(av * 0.3)}px/1 ${MONO};color:${c.d};opacity:.6;white-space:nowrap">@${cap[0].toLowerCase().replace(/[^a-z0-9]/g, "") || "user"}</span></div>` : `<div style="display:flex;flex-direction:column;gap:${R(av * 0.16)}px;flex:1"><span style="display:block;width:${T === "M" ? "46%" : "60%"};height:${R(av * 0.26)}px;border-radius:4px;background:${c.d};opacity:.55"></span>` + (T === "M" ? `<span style="display:block;width:30%;height:${R(av * 0.18)}px;border-radius:4px;background:${c.d};opacity:.3"></span>` : "") + `</div>`;
|
|
239
|
+
const head = `<div style="height:${av}px;display:flex;align-items:center;gap:${R(av * 0.35)}px;margin-bottom:${p}px"><span style="width:${av}px;height:${av}px;border-radius:50%;background:${c.d};flex:none"></span>${who}</div>`;
|
|
240
|
+
const ic = R(clamp(w * 0.055, 10, 18));
|
|
241
|
+
const icons = `<div style="display:flex;gap:${R(ic * 0.9)}px;align-items:center;height:${ic}px;margin-top:${p}px">` + [
|
|
242
|
+
"M12 21C5 16 2 12 2 8.5A4.5 4.5 0 0 1 12 6a4.5 4.5 0 0 1 10 2.5C22 12 19 16 12 21z",
|
|
243
|
+
"M4 4h16v12H9l-5 4z",
|
|
244
|
+
"M3 12l18-8-7 17-3-7z"
|
|
245
|
+
].map(
|
|
246
|
+
(d) => `<svg width="${ic}" height="${ic}" viewBox="0 0 24 24" fill="none" stroke="${c.d}" stroke-width="2.6" stroke-linejoin="round" style="display:block"><path d="${d}"/></svg>`
|
|
247
|
+
).join("") + `</div>`;
|
|
248
|
+
const h = p + av + p + mh + p + ic + p;
|
|
249
|
+
const html = `<div style="width:${w}px;height:${h}px;box-sizing:border-box;padding:${p}px;border-radius:${rr}px;background:${c.f};box-shadow:${shadow}">${head}${mediaSurface(o, mw, mh, `${R(rr * 0.6)}px`)}${icons}</div>`;
|
|
250
|
+
return { html, h };
|
|
251
|
+
},
|
|
252
|
+
circle(o, k) {
|
|
253
|
+
const { c, w, shadow } = k;
|
|
254
|
+
const rw = R(clamp(w * 0.035, 4, 8));
|
|
255
|
+
const dot = R(w * 0.2);
|
|
256
|
+
const mw = w - rw * 2;
|
|
257
|
+
const html = `<div style="position:relative;width:${w}px;height:${w}px">` + (w >= 170 ? `<div style="position:absolute;inset:${-R(w * 0.07)}px;border-radius:50%;border:2px dashed ${c.f};opacity:.5"></div>` : "") + `<div style="width:${w}px;height:${w}px;border-radius:50%;background:${c.f};padding:${rw}px;box-sizing:border-box;box-shadow:${shadow}">${mediaSurface({ ...o, clip: false }, mw, mw, "50%")}</div><span style="position:absolute;right:${R(w * 0.03)}px;bottom:${R(w * 0.03)}px;width:${dot}px;height:${dot}px;border-radius:50%;background:${c.f};display:flex;align-items:center;justify-content:center"><span style="width:${R(dot * 0.5)}px;height:${R(dot * 0.5)}px;border-radius:50%;background:${c.d}"></span></span></div>`;
|
|
258
|
+
return { html, h: w };
|
|
259
|
+
},
|
|
260
|
+
message(o, k) {
|
|
261
|
+
const { c, w, T } = k;
|
|
262
|
+
const p = R(clamp(w * 0.03, 5, 9));
|
|
263
|
+
const mw = w - p * 2;
|
|
264
|
+
const mh = R(mw * k.ratio[1] / k.ratio[0]);
|
|
265
|
+
const rad = R(clamp(w * 0.07, 12, 22));
|
|
266
|
+
const foot = T === "S" && w < 220 ? 0 : R(clamp(w * 0.075, 16, 24));
|
|
267
|
+
const h = p + mh + (foot || p);
|
|
268
|
+
const tail = R(clamp(w * 0.06, 10, 18));
|
|
269
|
+
const html = `<div style="position:relative;width:${w}px;height:${h}px;filter:drop-shadow(0 ${R(w * 0.025)}px ${R(w * 0.04)}px rgba(0,0,0,.3))"><div style="width:${w}px;height:${h}px;box-sizing:border-box;padding:${p}px ${p}px 0;border-radius:${rad}px ${rad}px ${rad}px 4px;background:${c.f}">${mediaSurface(o, mw, mh, `${rad - p}px`)}` + (foot ? `<div style="height:${foot}px;display:flex;align-items:center;justify-content:flex-end;gap:5px;font:500 ${R(foot * 0.5)}px/1 ${MONO};color:${c.d};opacity:.65">21:04 <span style="letter-spacing:-0.25em">\u2713\u2713</span></div>` : "") + `</div><svg width="${tail}" height="${tail}" viewBox="0 0 10 10" style="position:absolute;left:${-R(tail * 0.55)}px;bottom:0;display:block"><path d="M10 0v10H0c4-1 7-4 10-10z" fill="${c.f}"/></svg></div>`;
|
|
270
|
+
return { html, h };
|
|
271
|
+
},
|
|
272
|
+
slide(o, k) {
|
|
273
|
+
const { c, w, copy, shadow } = k;
|
|
274
|
+
const rad = R(w * 0.05);
|
|
275
|
+
const aw = R(w * 0.74);
|
|
276
|
+
const ah = R(aw * 0.75);
|
|
277
|
+
const top = R((w - ah) * 0.38);
|
|
278
|
+
const sx = R((w - aw) / 2);
|
|
279
|
+
const cap = copy.caps[(o.idx ?? 0) % copy.caps.length][0];
|
|
280
|
+
const html = `<div style="position:relative;width:${w}px;height:${w}px;border-radius:${rad}px;background:${c.f};box-shadow:${shadow}"><div style="position:absolute;left:${sx}px;top:${top}px;border-radius:${R(aw * 0.06)}px;overflow:hidden;box-shadow:0 0 0 2px ${hexA(c.d, 0.25)}">${mediaSurface({ ...o, clip: false }, aw, ah, `${R(aw * 0.06)}px`)}</div>` + (w >= 180 ? `<div style="position:absolute;left:${sx}px;right:${sx}px;bottom:${R(w * 0.065)}px;display:flex;justify-content:space-between;font:500 ${clamp(R(w * 0.045), 9, 12)}px/1 ${MONO};letter-spacing:.12em;color:${c.d};opacity:.7"><span>${cap.toUpperCase()}</span><span>${String((o.idx ?? 0) + 1).padStart(2, "0")}</span></div>` : "") + `<span style="position:absolute;left:${R(w * 0.06)}px;top:${R(w * 0.06)}px;width:${R(w * 0.04)}px;height:${R(w * 0.04)}px;border-radius:50%;background:${c.d};opacity:.3"></span></div>`;
|
|
281
|
+
return { html, h: w };
|
|
282
|
+
},
|
|
283
|
+
ticket(o, k) {
|
|
284
|
+
const { c, w, T, copy, font, rr } = k;
|
|
285
|
+
const sw = R(w * 0.28);
|
|
286
|
+
const mw = w - sw;
|
|
287
|
+
const ratio = k.ratio[1] > k.ratio[0] ? [4, 3] : k.ratio;
|
|
288
|
+
const mh = R(mw * ratio[1] / ratio[0]);
|
|
289
|
+
const h = mh;
|
|
290
|
+
const nr = R(clamp(w * 0.03, 6, 12));
|
|
291
|
+
const mask = `radial-gradient(circle at ${mw}px 0,transparent ${nr}px,#000 ${nr + 0.5}px) top/100% 51% no-repeat,radial-gradient(circle at ${mw}px 100%,transparent ${nr}px,#000 ${nr + 0.5}px) bottom/100% 51% no-repeat`;
|
|
292
|
+
const bars = [2, 1, 3, 1, 2, 1, 1, 3, 2, 1, 2, 3, 1, 2].map((bw) => `<span style="display:block;width:${bw}px;height:100%;background:${c.d}"></span>`).join("");
|
|
293
|
+
const fs = clamp(R(sw * 0.08), 8, 10);
|
|
294
|
+
const info = T === "L" ? `<div style="display:flex;flex-direction:column;gap:5px"><span style="font:500 ${fs}px/1 ${MONO};letter-spacing:.14em;color:${c.d};opacity:.65">ADMIT ONE</span><span style="font:800 ${R(sw * 0.13)}px/1.05 ${font};color:${c.d};letter-spacing:-0.01em">${copy.title}</span><span style="font:500 ${fs}px/1 ${MONO};letter-spacing:.12em;color:${c.d}">\u2116 00${(o.idx ?? 0) + 1}</span></div>` : "<div></div>";
|
|
295
|
+
const stub = `<div style="width:${sw}px;height:${h}px;flex:none;background:${c.f};border-left:2px dashed ${hexA(c.d, 0.35)};box-sizing:border-box;display:flex;flex-direction:column;justify-content:space-between;padding:${R(sw * 0.12)}px">${info}` + (T === "S" ? "" : `<div style="display:flex;gap:2px;height:${R(sw * 0.2)}px">${bars}</div>`) + `</div>`;
|
|
296
|
+
const html = `<div style="width:${w}px;height:${h}px;filter:drop-shadow(0 ${R(w * 0.025)}px ${R(w * 0.04)}px rgba(0,0,0,.3))"><div style="display:flex;width:${w}px;height:${h}px;border-radius:${rr}px;overflow:hidden;-webkit-mask:${mask};mask:${mask}">${mediaSurface({ ...o, clip: false }, mw, mh, "0")}${stub}</div></div>`;
|
|
297
|
+
return { html, h };
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
function frame(o) {
|
|
301
|
+
const kind = o.kind ?? "browser";
|
|
302
|
+
const c = o.c;
|
|
303
|
+
const w = R(o.w);
|
|
304
|
+
const T = tier(w);
|
|
305
|
+
const ratio = FORCE[kind] ?? o.ratio ?? [16, 9];
|
|
306
|
+
const copy = (o.pack ? COPY[o.pack] : void 0) ?? fallbackCopy;
|
|
307
|
+
const font = o.font ?? FONT;
|
|
308
|
+
const rr = R(clamp(w * 0.035, 8, 14));
|
|
309
|
+
const shadow = `0 ${R(w * 0.03)}px ${R(w * 0.08)}px rgba(0,0,0,.32)`;
|
|
310
|
+
const ring = R(clamp(w * 8e-3, 2, 3));
|
|
311
|
+
const ctx = { c, w, T, ratio, copy, font, rr, shadow, ring };
|
|
312
|
+
const build = BUILDERS[kind];
|
|
313
|
+
if (build) {
|
|
314
|
+
const { html: html2, h } = build(o, ctx);
|
|
315
|
+
return { html: html2, h, tier: T };
|
|
316
|
+
}
|
|
317
|
+
const mh = R(w * ratio[1] / ratio[0]);
|
|
318
|
+
const html = `<div style="width:${w}px;height:${mh}px;border-radius:${rr}px;box-shadow:${o.ring === false ? "" : `0 0 0 ${ring}px ${c.f},`}${shadow};overflow:hidden">${mediaSurface(o, w, mh, `${rr}px`)}</div>`;
|
|
319
|
+
return { html, h: mh, tier: T };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/index.ts
|
|
323
|
+
var PROBE_W = 320;
|
|
324
|
+
function windowShape(o) {
|
|
325
|
+
const kind = o.kind ?? "browser";
|
|
326
|
+
const probeColors = { f: "#000000", d: "#ffffff" };
|
|
327
|
+
const probe = frame({
|
|
328
|
+
...o,
|
|
329
|
+
kind,
|
|
330
|
+
w: PROBE_W,
|
|
331
|
+
c: probeColors
|
|
332
|
+
});
|
|
333
|
+
const ar = PROBE_W / probe.h;
|
|
334
|
+
const def = {
|
|
335
|
+
id: o.id,
|
|
336
|
+
label: o.label ?? o.id,
|
|
337
|
+
kind: "obj",
|
|
338
|
+
ar,
|
|
339
|
+
scale: (o.scale ?? 3) * (SCALE[kind] ?? 1),
|
|
340
|
+
/**
|
|
341
|
+
* Builds at the particle's real width, not a reference one, so chrome
|
|
342
|
+
* picks the right tier — a 240px frame merges its controls into a single
|
|
343
|
+
* capsule, a 420px one shows a URL. Building at a fixed width and scaling
|
|
344
|
+
* would both overflow the particle box and freeze the chrome at one tier.
|
|
345
|
+
*/
|
|
346
|
+
render: (c, _variant, width) => frame({ ...o, kind, w: width ?? PROBE_W, c }).html
|
|
347
|
+
};
|
|
348
|
+
return o.place === void 0 ? def : { ...def, place: o.place };
|
|
349
|
+
}
|
|
350
|
+
function image(o) {
|
|
351
|
+
return windowShape({ ...o, kind: "bare" });
|
|
352
|
+
}
|
|
353
|
+
function video(o) {
|
|
354
|
+
return windowShape({ ...o, kind: "bare", clip: o.clip ?? true });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
exports.COPY = COPY;
|
|
358
|
+
exports.KINDS = KINDS;
|
|
359
|
+
exports.SCALE = SCALE;
|
|
360
|
+
exports.frame = frame;
|
|
361
|
+
exports.image = image;
|
|
362
|
+
exports.isVideo = isVideo;
|
|
363
|
+
exports.mediaSurface = mediaSurface;
|
|
364
|
+
exports.tier = tier;
|
|
365
|
+
exports.video = video;
|
|
366
|
+
exports.windowShape = windowShape;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ShapeColors, Anchor, ShapeDef } from '@usemote/core';
|
|
2
|
+
|
|
3
|
+
type Tier = 'S' | 'M' | 'L';
|
|
4
|
+
/**
|
|
5
|
+
* Chrome detail is chosen by rendered width, not by a breakpoint.
|
|
6
|
+
*
|
|
7
|
+
* A frame floating in a field can be any size, and chrome that merely scales
|
|
8
|
+
* turns to mush below ~270px — traffic lights become 5px smudges and a URL
|
|
9
|
+
* becomes a grey streak. Each tier drops detail rather than shrinking it.
|
|
10
|
+
*/
|
|
11
|
+
declare const tier: (w: number) => Tier;
|
|
12
|
+
|
|
13
|
+
type FrameKind = 'browser' | 'app' | 'bare' | 'caption' | 'phone' | 'monitor' | 'polaroid' | 'stream' | 'post' | 'circle' | 'message' | 'slide' | 'ticket';
|
|
14
|
+
/** Realistic chrome text per pack, so a frame reads as that domain. */
|
|
15
|
+
interface PackCopy {
|
|
16
|
+
url: string;
|
|
17
|
+
title: string;
|
|
18
|
+
caps: Array<[string, string]>;
|
|
19
|
+
stream?: [string, string];
|
|
20
|
+
}
|
|
21
|
+
interface MediaSource {
|
|
22
|
+
/** Image or video URL. Video is detected by extension. */
|
|
23
|
+
src?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Poster frame, shown until playback starts — and permanently under
|
|
26
|
+
* reduced motion, where playback never starts at all. Strongly recommended
|
|
27
|
+
* for video: without it a paused `<video>` is a blank rectangle.
|
|
28
|
+
*/
|
|
29
|
+
poster?: string;
|
|
30
|
+
/** Alternative text. Decorative by default, since the layer is aria-hidden. */
|
|
31
|
+
alt?: string;
|
|
32
|
+
/** SVG markup shown in the placeholder when there is no `src`. */
|
|
33
|
+
art?: string;
|
|
34
|
+
/** Adds a progress bar and timecode chip, for clip-style content. */
|
|
35
|
+
clip?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface FrameOptions extends MediaSource {
|
|
38
|
+
kind?: FrameKind;
|
|
39
|
+
/** Rendered width in px. Height follows from the kind and ratio. */
|
|
40
|
+
w: number;
|
|
41
|
+
/** Colours, normally handed in by the field from the active theme. */
|
|
42
|
+
c: ShapeColors;
|
|
43
|
+
ratio?: [number, number];
|
|
44
|
+
/** Pack id, selecting the chrome copy. */
|
|
45
|
+
pack?: string;
|
|
46
|
+
font?: string;
|
|
47
|
+
/** Index into the pack's caption list, so sibling frames differ. */
|
|
48
|
+
idx?: number;
|
|
49
|
+
/** `bare` only: drop the outline ring. */
|
|
50
|
+
ring?: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface FrameResult {
|
|
53
|
+
html: string;
|
|
54
|
+
/** Rendered height in px, for the caller's aspect ratio. */
|
|
55
|
+
h: number;
|
|
56
|
+
tier: Tier;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Relative size, so one `size` value gives sane results across kinds. */
|
|
60
|
+
declare const SCALE: Partial<Record<FrameKind, number>>;
|
|
61
|
+
declare const KINDS: FrameKind[];
|
|
62
|
+
/** Builds one frame. Returns markup plus the height it actually occupies. */
|
|
63
|
+
declare function frame(o: FrameOptions): FrameResult;
|
|
64
|
+
|
|
65
|
+
declare const isVideo: (src: string) => boolean;
|
|
66
|
+
declare function mediaSurface(o: FrameOptions, mw: number, mh: number, rad: string): string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Per-pack chrome copy, from the design source.
|
|
70
|
+
*
|
|
71
|
+
* A browser frame in a devtools field should read `localhost:3000`, not
|
|
72
|
+
* `example.com` — the chrome is part of the pack's vocabulary, and generic
|
|
73
|
+
* placeholder text is what makes a mockup look like a mockup.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
declare const COPY: Record<string, PackCopy>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @usemote/media — window frames and media particles for Mote.
|
|
80
|
+
*
|
|
81
|
+
* A particle does not have to be a drawn shape. The renderer only ever
|
|
82
|
+
* assigns a shape's markup as the particle's contents, so a photo, a GIF or a
|
|
83
|
+
* framed video participates in the field exactly like any geometric shape:
|
|
84
|
+
* same layout, same depth, same motion presets, same scheduler.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { windowShape } from '@usemote/media';
|
|
88
|
+
*
|
|
89
|
+
* <FloatingField
|
|
90
|
+
* packs={[esports]} mode="bob" count={10}
|
|
91
|
+
* media={[
|
|
92
|
+
* windowShape({ id: 'nova', kind: 'caption', src: '/nova.jpg', place: 'right', scale: 3.4 }),
|
|
93
|
+
* windowShape({ id: 'demo', kind: 'browser', src: '/demo.webm', poster: '/demo.jpg', clip: true, place: 'bottom-left' }),
|
|
94
|
+
* ]}
|
|
95
|
+
* />
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
interface WindowShapeOptions extends MediaSource {
|
|
100
|
+
/** Unique id, as for any shape. */
|
|
101
|
+
id: string;
|
|
102
|
+
/** Human label, shown in pickers. Defaults to the id. */
|
|
103
|
+
label?: string;
|
|
104
|
+
kind?: FrameKind;
|
|
105
|
+
/** Aspect ratio of the media area. Ignored by kinds with a fixed shape. */
|
|
106
|
+
ratio?: [number, number];
|
|
107
|
+
/** Where to anchor it. Media is too large for `avoid` to place. */
|
|
108
|
+
place?: Anchor;
|
|
109
|
+
/**
|
|
110
|
+
* Size multiplier on the field's `size`. Frames want to be much larger than
|
|
111
|
+
* shapes, so this defaults to 3 rather than 1.
|
|
112
|
+
*/
|
|
113
|
+
scale?: number;
|
|
114
|
+
/** Pack id, selecting the chrome copy (`localhost:3000` vs `app.ledger.io`). */
|
|
115
|
+
pack?: string;
|
|
116
|
+
font?: string;
|
|
117
|
+
/** Index into the pack's caption list, so sibling frames read differently. */
|
|
118
|
+
idx?: number;
|
|
119
|
+
/** `bare` only: drop the outline ring. */
|
|
120
|
+
ring?: boolean;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Wraps a frame as a `ShapeDef`, so it can be passed to `media` on a field.
|
|
124
|
+
*
|
|
125
|
+
* `ar` is measured by building the frame once at a reference width: chrome is
|
|
126
|
+
* not a fixed fraction of the media area — a caption strip is clamped between
|
|
127
|
+
* 30 and 46px — so the only honest aspect ratio is the one the builder
|
|
128
|
+
* actually produces. Layout uses `ar` for spacing and collision, so guessing
|
|
129
|
+
* it would make frames crowd their neighbours.
|
|
130
|
+
*/
|
|
131
|
+
declare function windowShape(o: WindowShapeOptions): ShapeDef;
|
|
132
|
+
/** A photo or GIF with no chrome at all. Shorthand for `kind: 'bare'`. */
|
|
133
|
+
declare function image(o: Omit<WindowShapeOptions, 'kind'> & {
|
|
134
|
+
src: string;
|
|
135
|
+
}): ShapeDef;
|
|
136
|
+
/**
|
|
137
|
+
* A video with no chrome. Playback is started by the renderer, never by an
|
|
138
|
+
* `autoplay` attribute, so reduced motion and offscreen fields stay on the
|
|
139
|
+
* poster frame.
|
|
140
|
+
*/
|
|
141
|
+
declare function video(o: Omit<WindowShapeOptions, 'kind'> & {
|
|
142
|
+
src: string;
|
|
143
|
+
}): ShapeDef;
|
|
144
|
+
|
|
145
|
+
export { COPY, type FrameKind, type FrameOptions, type FrameResult, KINDS, type MediaSource, type PackCopy, SCALE, type Tier, type WindowShapeOptions, frame, image, isVideo, mediaSurface, tier, video, windowShape };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ShapeColors, Anchor, ShapeDef } from '@usemote/core';
|
|
2
|
+
|
|
3
|
+
type Tier = 'S' | 'M' | 'L';
|
|
4
|
+
/**
|
|
5
|
+
* Chrome detail is chosen by rendered width, not by a breakpoint.
|
|
6
|
+
*
|
|
7
|
+
* A frame floating in a field can be any size, and chrome that merely scales
|
|
8
|
+
* turns to mush below ~270px — traffic lights become 5px smudges and a URL
|
|
9
|
+
* becomes a grey streak. Each tier drops detail rather than shrinking it.
|
|
10
|
+
*/
|
|
11
|
+
declare const tier: (w: number) => Tier;
|
|
12
|
+
|
|
13
|
+
type FrameKind = 'browser' | 'app' | 'bare' | 'caption' | 'phone' | 'monitor' | 'polaroid' | 'stream' | 'post' | 'circle' | 'message' | 'slide' | 'ticket';
|
|
14
|
+
/** Realistic chrome text per pack, so a frame reads as that domain. */
|
|
15
|
+
interface PackCopy {
|
|
16
|
+
url: string;
|
|
17
|
+
title: string;
|
|
18
|
+
caps: Array<[string, string]>;
|
|
19
|
+
stream?: [string, string];
|
|
20
|
+
}
|
|
21
|
+
interface MediaSource {
|
|
22
|
+
/** Image or video URL. Video is detected by extension. */
|
|
23
|
+
src?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Poster frame, shown until playback starts — and permanently under
|
|
26
|
+
* reduced motion, where playback never starts at all. Strongly recommended
|
|
27
|
+
* for video: without it a paused `<video>` is a blank rectangle.
|
|
28
|
+
*/
|
|
29
|
+
poster?: string;
|
|
30
|
+
/** Alternative text. Decorative by default, since the layer is aria-hidden. */
|
|
31
|
+
alt?: string;
|
|
32
|
+
/** SVG markup shown in the placeholder when there is no `src`. */
|
|
33
|
+
art?: string;
|
|
34
|
+
/** Adds a progress bar and timecode chip, for clip-style content. */
|
|
35
|
+
clip?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface FrameOptions extends MediaSource {
|
|
38
|
+
kind?: FrameKind;
|
|
39
|
+
/** Rendered width in px. Height follows from the kind and ratio. */
|
|
40
|
+
w: number;
|
|
41
|
+
/** Colours, normally handed in by the field from the active theme. */
|
|
42
|
+
c: ShapeColors;
|
|
43
|
+
ratio?: [number, number];
|
|
44
|
+
/** Pack id, selecting the chrome copy. */
|
|
45
|
+
pack?: string;
|
|
46
|
+
font?: string;
|
|
47
|
+
/** Index into the pack's caption list, so sibling frames differ. */
|
|
48
|
+
idx?: number;
|
|
49
|
+
/** `bare` only: drop the outline ring. */
|
|
50
|
+
ring?: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface FrameResult {
|
|
53
|
+
html: string;
|
|
54
|
+
/** Rendered height in px, for the caller's aspect ratio. */
|
|
55
|
+
h: number;
|
|
56
|
+
tier: Tier;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Relative size, so one `size` value gives sane results across kinds. */
|
|
60
|
+
declare const SCALE: Partial<Record<FrameKind, number>>;
|
|
61
|
+
declare const KINDS: FrameKind[];
|
|
62
|
+
/** Builds one frame. Returns markup plus the height it actually occupies. */
|
|
63
|
+
declare function frame(o: FrameOptions): FrameResult;
|
|
64
|
+
|
|
65
|
+
declare const isVideo: (src: string) => boolean;
|
|
66
|
+
declare function mediaSurface(o: FrameOptions, mw: number, mh: number, rad: string): string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Per-pack chrome copy, from the design source.
|
|
70
|
+
*
|
|
71
|
+
* A browser frame in a devtools field should read `localhost:3000`, not
|
|
72
|
+
* `example.com` — the chrome is part of the pack's vocabulary, and generic
|
|
73
|
+
* placeholder text is what makes a mockup look like a mockup.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
declare const COPY: Record<string, PackCopy>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @usemote/media — window frames and media particles for Mote.
|
|
80
|
+
*
|
|
81
|
+
* A particle does not have to be a drawn shape. The renderer only ever
|
|
82
|
+
* assigns a shape's markup as the particle's contents, so a photo, a GIF or a
|
|
83
|
+
* framed video participates in the field exactly like any geometric shape:
|
|
84
|
+
* same layout, same depth, same motion presets, same scheduler.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { windowShape } from '@usemote/media';
|
|
88
|
+
*
|
|
89
|
+
* <FloatingField
|
|
90
|
+
* packs={[esports]} mode="bob" count={10}
|
|
91
|
+
* media={[
|
|
92
|
+
* windowShape({ id: 'nova', kind: 'caption', src: '/nova.jpg', place: 'right', scale: 3.4 }),
|
|
93
|
+
* windowShape({ id: 'demo', kind: 'browser', src: '/demo.webm', poster: '/demo.jpg', clip: true, place: 'bottom-left' }),
|
|
94
|
+
* ]}
|
|
95
|
+
* />
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
interface WindowShapeOptions extends MediaSource {
|
|
100
|
+
/** Unique id, as for any shape. */
|
|
101
|
+
id: string;
|
|
102
|
+
/** Human label, shown in pickers. Defaults to the id. */
|
|
103
|
+
label?: string;
|
|
104
|
+
kind?: FrameKind;
|
|
105
|
+
/** Aspect ratio of the media area. Ignored by kinds with a fixed shape. */
|
|
106
|
+
ratio?: [number, number];
|
|
107
|
+
/** Where to anchor it. Media is too large for `avoid` to place. */
|
|
108
|
+
place?: Anchor;
|
|
109
|
+
/**
|
|
110
|
+
* Size multiplier on the field's `size`. Frames want to be much larger than
|
|
111
|
+
* shapes, so this defaults to 3 rather than 1.
|
|
112
|
+
*/
|
|
113
|
+
scale?: number;
|
|
114
|
+
/** Pack id, selecting the chrome copy (`localhost:3000` vs `app.ledger.io`). */
|
|
115
|
+
pack?: string;
|
|
116
|
+
font?: string;
|
|
117
|
+
/** Index into the pack's caption list, so sibling frames read differently. */
|
|
118
|
+
idx?: number;
|
|
119
|
+
/** `bare` only: drop the outline ring. */
|
|
120
|
+
ring?: boolean;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Wraps a frame as a `ShapeDef`, so it can be passed to `media` on a field.
|
|
124
|
+
*
|
|
125
|
+
* `ar` is measured by building the frame once at a reference width: chrome is
|
|
126
|
+
* not a fixed fraction of the media area — a caption strip is clamped between
|
|
127
|
+
* 30 and 46px — so the only honest aspect ratio is the one the builder
|
|
128
|
+
* actually produces. Layout uses `ar` for spacing and collision, so guessing
|
|
129
|
+
* it would make frames crowd their neighbours.
|
|
130
|
+
*/
|
|
131
|
+
declare function windowShape(o: WindowShapeOptions): ShapeDef;
|
|
132
|
+
/** A photo or GIF with no chrome at all. Shorthand for `kind: 'bare'`. */
|
|
133
|
+
declare function image(o: Omit<WindowShapeOptions, 'kind'> & {
|
|
134
|
+
src: string;
|
|
135
|
+
}): ShapeDef;
|
|
136
|
+
/**
|
|
137
|
+
* A video with no chrome. Playback is started by the renderer, never by an
|
|
138
|
+
* `autoplay` attribute, so reduced motion and offscreen fields stay on the
|
|
139
|
+
* poster frame.
|
|
140
|
+
*/
|
|
141
|
+
declare function video(o: Omit<WindowShapeOptions, 'kind'> & {
|
|
142
|
+
src: string;
|
|
143
|
+
}): ShapeDef;
|
|
144
|
+
|
|
145
|
+
export { COPY, type FrameKind, type FrameOptions, type FrameResult, KINDS, type MediaSource, type PackCopy, SCALE, type Tier, type WindowShapeOptions, frame, image, isVideo, mediaSurface, tier, video, windowShape };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// src/copy.ts
|
|
2
|
+
var COPY = {
|
|
3
|
+
esports: {
|
|
4
|
+
url: "publication.gg/live",
|
|
5
|
+
title: "Grand Final \xB7 Lagos",
|
|
6
|
+
stream: ["K9 vs NOVA", "2\u20131"],
|
|
7
|
+
caps: [["K9", "ACCRA"], ["NOVA", "LAGOS"], ["ADA", "ACCRA"], ["DEMZY", "LAGOS"]]
|
|
8
|
+
},
|
|
9
|
+
devtools: {
|
|
10
|
+
url: "localhost:3000",
|
|
11
|
+
title: "deploy \u2014 main",
|
|
12
|
+
caps: [["v2.4.0", "SHIPPED"], ["main", "GREEN"], ["#128", "MERGED"], ["api", "200 OK"]]
|
|
13
|
+
},
|
|
14
|
+
ai: {
|
|
15
|
+
url: "chat.model.ai",
|
|
16
|
+
title: "Inference",
|
|
17
|
+
caps: [["Prompt", "TEXT"], ["Vision", "IMAGE"], ["Voice", "AUDIO"], ["Agent", "RUN"]]
|
|
18
|
+
},
|
|
19
|
+
fintech: {
|
|
20
|
+
url: "app.ledger.io",
|
|
21
|
+
title: "Portfolio",
|
|
22
|
+
caps: [["+2.4%", "TODAY"], ["Savings", "GOAL"], ["Card", "ACTIVE"], ["NGN/USD", "LIVE"]]
|
|
23
|
+
},
|
|
24
|
+
creative: {
|
|
25
|
+
url: "studio.work",
|
|
26
|
+
title: "Moodboard",
|
|
27
|
+
caps: [["Brand", "V3"], ["Poster", "DRAFT"], ["Type", "SPEC"], ["Motion", "WIP"]]
|
|
28
|
+
},
|
|
29
|
+
space: {
|
|
30
|
+
url: "mission.live",
|
|
31
|
+
title: "Telemetry",
|
|
32
|
+
caps: [["Orbit 3", "LIVE"], ["Mars", "SOL 214"], ["Launch", "T-10"], ["Lunar", "PASS"]]
|
|
33
|
+
},
|
|
34
|
+
cyber: {
|
|
35
|
+
url: "vault.secure",
|
|
36
|
+
title: "audit.log",
|
|
37
|
+
caps: [["Firewall", "ON"], ["2FA", "VERIFIED"], ["Scan", "CLEAN"], ["Vault", "LOCKED"]]
|
|
38
|
+
},
|
|
39
|
+
health: {
|
|
40
|
+
url: "care.app",
|
|
41
|
+
title: "Vitals",
|
|
42
|
+
caps: [["72 BPM", "REST"], ["Sleep", "7H 40"], ["Steps", "9,420"], ["Dose", "08:00"]]
|
|
43
|
+
},
|
|
44
|
+
education: {
|
|
45
|
+
url: "learn.school",
|
|
46
|
+
title: "Lesson 04",
|
|
47
|
+
caps: [["Algebra", "UNIT 2"], ["Physics", "LAB"], ["History", "QUIZ"], ["Art", "STUDIO"]]
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var fallbackCopy = COPY.esports;
|
|
51
|
+
|
|
52
|
+
// src/util.ts
|
|
53
|
+
var R = Math.round;
|
|
54
|
+
var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
55
|
+
var hexA = (hex, a) => /^#[0-9a-f]{6}$/i.test(hex) ? hex + R(a * 255).toString(16).padStart(2, "0") : hex;
|
|
56
|
+
var FONT = "'Schibsted Grotesk',system-ui,-apple-system,sans-serif";
|
|
57
|
+
var MONO = "'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace";
|
|
58
|
+
var tier = (w) => w >= 340 ? "L" : w >= 270 ? "M" : "S";
|
|
59
|
+
var lock = (s, col) => `<svg width="${s}" height="${R(s * 1.2)}" viewBox="0 0 10 12" style="flex:none;display:block"><path d="M2.5 5.5V3.5a2.5 2.5 0 0 1 5 0v2" fill="none" stroke="${col}" stroke-width="1.6"/><rect x="1" y="5" width="8" height="6.5" rx="1.6" fill="${col}"/></svg>`;
|
|
60
|
+
var lights = (T, dot, gap, col) => T === "S" ? `<span style="display:block;width:${dot * 3 + gap * 2}px;height:${dot}px;border-radius:${dot}px;background:${col};opacity:.55"></span>` : [1, 0.6, 0.35].map(
|
|
61
|
+
(o) => `<span style="display:block;flex:none;width:${dot}px;height:${dot}px;border-radius:50%;background:${col};opacity:${o}"></span>`
|
|
62
|
+
).join("");
|
|
63
|
+
|
|
64
|
+
// src/media.ts
|
|
65
|
+
var VIDEO_EXT = /\.(mp4|webm|mov|m4v)(\?|#|$)/i;
|
|
66
|
+
var isVideo = (src) => VIDEO_EXT.test(src);
|
|
67
|
+
var attr = (v) => v.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
68
|
+
function mediaSurface(o, mw, mh, rad) {
|
|
69
|
+
const c = o.c;
|
|
70
|
+
let inner;
|
|
71
|
+
if (o.src && isVideo(o.src)) {
|
|
72
|
+
inner = `<video data-mote-media src="${attr(o.src)}"` + (o.poster ? ` poster="${attr(o.poster)}"` : "") + ` muted loop playsinline preload="metadata" tabindex="-1" aria-hidden="true" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block"></video>`;
|
|
73
|
+
} else if (o.src) {
|
|
74
|
+
inner = `<img src="${attr(o.src)}" alt="${attr(o.alt ?? "")}" loading="lazy" decoding="async" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block">`;
|
|
75
|
+
} else {
|
|
76
|
+
const s = R(Math.min(mw, mh) * 0.46);
|
|
77
|
+
inner = `<div style="position:absolute;inset:0;background:${c.d};background-image:repeating-linear-gradient(135deg,${hexA(c.f, 0.08)} 0 10px,transparent 10px 20px)"></div>` + (o.art ? `<div style="position:absolute;left:50%;top:50%;width:${s}px;height:${s}px;margin:${-s / 2}px 0 0 ${-s / 2}px">${o.art}</div>` : "");
|
|
78
|
+
}
|
|
79
|
+
if (o.clip) {
|
|
80
|
+
const bh = Math.max(4, R(mw * 0.014));
|
|
81
|
+
const p = Math.max(8, R(mw * 0.045));
|
|
82
|
+
inner += `<div style="position:absolute;left:${p}px;right:${p}px;bottom:${p}px;height:${bh}px;border-radius:${bh}px;background:${hexA(c.f, 0.35)}"><div style="width:38%;height:100%;border-radius:${bh}px;background:${c.f}"></div></div>`;
|
|
83
|
+
if (mw >= 300) {
|
|
84
|
+
const fs = clamp(R(mw * 0.032), 10, 12);
|
|
85
|
+
inner += `<div style="position:absolute;left:${p}px;top:${p}px;display:flex;align-items:center;gap:6px;background:${c.f};color:${c.d};border-radius:4px;padding:4px 7px;font:500 ${fs}px/1 ${MONO};letter-spacing:.08em"><span style="width:0;height:0;border-left:${R(fs * 0.7)}px solid ${c.d};border-top:${R(fs * 0.42)}px solid transparent;border-bottom:${R(fs * 0.42)}px solid transparent"></span>0:24</div>`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return `<div style="position:relative;width:${mw}px;height:${mh}px;overflow:hidden;border-radius:${rad};background:${c.d}">${inner}</div>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/frames.ts
|
|
92
|
+
var FORCE = {
|
|
93
|
+
phone: [9, 19.5],
|
|
94
|
+
circle: [1, 1],
|
|
95
|
+
slide: [4, 3],
|
|
96
|
+
polaroid: [1, 1]
|
|
97
|
+
};
|
|
98
|
+
var SCALE = {
|
|
99
|
+
phone: 0.5,
|
|
100
|
+
circle: 0.62,
|
|
101
|
+
slide: 0.72,
|
|
102
|
+
polaroid: 0.78,
|
|
103
|
+
message: 0.85,
|
|
104
|
+
post: 0.82,
|
|
105
|
+
ticket: 1.1,
|
|
106
|
+
monitor: 1.05
|
|
107
|
+
};
|
|
108
|
+
var KINDS = [
|
|
109
|
+
"browser",
|
|
110
|
+
"app",
|
|
111
|
+
"bare",
|
|
112
|
+
"caption",
|
|
113
|
+
"phone",
|
|
114
|
+
"monitor",
|
|
115
|
+
"polaroid",
|
|
116
|
+
"stream",
|
|
117
|
+
"post",
|
|
118
|
+
"circle",
|
|
119
|
+
"message",
|
|
120
|
+
"slide",
|
|
121
|
+
"ticket"
|
|
122
|
+
];
|
|
123
|
+
var BUILDERS = {
|
|
124
|
+
browser(o, k) {
|
|
125
|
+
const { c, w, T, ratio, copy, rr, shadow } = k;
|
|
126
|
+
const rim = R(clamp(w * 0.012, 3, 5));
|
|
127
|
+
const bar = R(clamp(w * 0.08, 20, 30));
|
|
128
|
+
const dot = R(clamp(bar * 0.34, 7, 10));
|
|
129
|
+
const gap = R(dot * 0.6);
|
|
130
|
+
const pad = R(bar * 0.45);
|
|
131
|
+
const mw = w - rim * 2;
|
|
132
|
+
const mh = R(mw * ratio[1] / ratio[0]);
|
|
133
|
+
const h = bar + mh + rim;
|
|
134
|
+
const lw = dot * 3 + gap * 2;
|
|
135
|
+
const ph = R(bar * 0.52);
|
|
136
|
+
const url = T === "L" ? `${lock(R(ph * 0.42), c.d)}<span style="font:500 ${clamp(R(bar * 0.36), 9, 11)}px/1 ${MONO};color:${c.d};opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${copy.url}</span>` : "";
|
|
137
|
+
const html = `<div style="width:${w}px;height:${h}px;display:flex;flex-direction:column;border-radius:${rr}px;background:${c.f};box-shadow:${shadow};overflow:hidden"><div style="height:${bar}px;flex:none;display:grid;grid-template-columns:${lw}px minmax(0,1fr) ${lw}px;align-items:center;gap:${pad}px;padding:0 ${pad}px"><div style="display:flex;gap:${gap}px;align-items:center">${lights(T, dot, gap, c.d)}</div><div style="display:flex;justify-content:center;min-width:0"><div style="height:${ph}px;width:${T === "S" ? "70%" : "86%"};border-radius:${ph}px;background:${hexA(c.d, 0.16)};display:flex;align-items:center;justify-content:center;gap:5px;padding:0 8px;box-sizing:border-box;min-width:0">${url}</div></div><div></div></div><div style="padding:0 ${rim}px ${rim}px">${mediaSurface(o, mw, mh, `0 0 ${Math.max(2, rr - rim)}px ${Math.max(2, rr - rim)}px`)}</div></div>`;
|
|
138
|
+
return { html, h };
|
|
139
|
+
},
|
|
140
|
+
app(o, k) {
|
|
141
|
+
const { c, w, T, ratio, copy, font, rr, shadow } = k;
|
|
142
|
+
const rim = R(clamp(w * 0.012, 3, 5));
|
|
143
|
+
const bar = R(clamp(w * 0.095, 22, 34));
|
|
144
|
+
const dot = R(clamp(bar * 0.3, 7, 10));
|
|
145
|
+
const gap = R(dot * 0.6);
|
|
146
|
+
const pad = R(bar * 0.42);
|
|
147
|
+
const rail = T === "S" ? 0 : R(clamp(w * 0.075, 20, 30));
|
|
148
|
+
const foot = R(clamp(w * 0.035, 8, 12));
|
|
149
|
+
const mw = w - rim - (rail || rim);
|
|
150
|
+
const mh = R(mw * ratio[1] / ratio[0]);
|
|
151
|
+
const h = bar + mh + foot;
|
|
152
|
+
const lw = dot * 3 + gap * 2;
|
|
153
|
+
const title = T === "L" ? `<span style="font:700 ${clamp(R(bar * 0.38), 10, 13)}px/1 ${font};color:${c.d};white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${copy.title}</span>` : T === "M" ? `<span style="display:block;width:${R(w * 0.22)}px;height:${R(dot * 0.8)}px;border-radius:${dot}px;background:${c.d};opacity:.35"></span>` : "";
|
|
154
|
+
const right = T === "S" ? "" : [0, 1].map(
|
|
155
|
+
() => `<span style="display:block;width:${dot}px;height:${dot}px;border-radius:2px;background:${c.d};opacity:.3"></span>`
|
|
156
|
+
).join("");
|
|
157
|
+
const ic = R(rail * 0.46);
|
|
158
|
+
const railHtml = rail ? `<div style="width:${rail}px;flex:none;display:flex;flex-direction:column;align-items:center;gap:${R(rail * 0.3)}px;padding-top:${R(rail * 0.15)}px">` + [0.6, 0.28, 0.28].map(
|
|
159
|
+
(op) => `<span style="display:block;width:${ic}px;height:${ic}px;border-radius:${R(ic * 0.28)}px;background:${c.d};opacity:${op}"></span>`
|
|
160
|
+
).join("") + `</div>` : "";
|
|
161
|
+
const html = `<div style="width:${w}px;height:${h}px;display:flex;flex-direction:column;border-radius:${rr}px;background:${c.f};box-shadow:${shadow};overflow:hidden"><div style="height:${bar}px;flex:none;display:grid;grid-template-columns:${lw}px minmax(0,1fr) ${lw}px;align-items:center;gap:${pad}px;padding:0 ${pad}px"><div style="display:flex;gap:${gap}px;align-items:center">${lights(T, dot, gap, c.d)}</div><div style="display:flex;justify-content:center;min-width:0">${title}</div><div style="display:flex;gap:${gap}px;justify-content:flex-end">${right}</div></div><div style="display:flex;padding:0 ${rim}px 0 ${rail ? 0 : rim}px">${railHtml}${mediaSurface(o, mw, mh, "4px")}</div><div style="height:${foot}px;flex:none"></div></div>`;
|
|
162
|
+
return { html, h };
|
|
163
|
+
},
|
|
164
|
+
caption(o, k) {
|
|
165
|
+
const { c, w, T, ratio, copy, font, rr, shadow, ring } = k;
|
|
166
|
+
const strip = R(clamp(w * 0.13, 30, 46));
|
|
167
|
+
const mh = R(w * ratio[1] / ratio[0]);
|
|
168
|
+
const h = mh + strip;
|
|
169
|
+
const cap = copy.caps[(o.idx ?? 0) % copy.caps.length];
|
|
170
|
+
const html = `<div style="width:${w}px;height:${h}px;border-radius:${rr}px;box-shadow:0 0 0 ${ring}px ${c.f},${shadow};overflow:hidden;display:flex;flex-direction:column;background:${c.f}">${mediaSurface(o, w, mh, "0")}<div style="height:${strip}px;flex:none;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 ${R(strip * 0.38)}px;color:${c.d}"><span style="font:800 ${R(strip * 0.42)}px/1 ${font};letter-spacing:-0.01em;white-space:nowrap">${cap[0]}</span>` + (T !== "S" ? `<span style="font:500 ${clamp(R(strip * 0.27), 9, 11)}px/1 ${MONO};letter-spacing:.12em;opacity:.7;white-space:nowrap">${cap[1]}</span>` : "") + `</div></div>`;
|
|
171
|
+
return { html, h };
|
|
172
|
+
},
|
|
173
|
+
phone(o, k) {
|
|
174
|
+
const { c, w, shadow } = k;
|
|
175
|
+
const b = R(clamp(w * 0.04, 5, 9));
|
|
176
|
+
const rad = R(w * 0.17);
|
|
177
|
+
const mw = w - b * 2;
|
|
178
|
+
const mh = R(mw * 19.5 / 9);
|
|
179
|
+
const h = mh + b * 2;
|
|
180
|
+
const iw = R(w * 0.3);
|
|
181
|
+
const ih = R(clamp(w * 0.07, 8, 16));
|
|
182
|
+
const hb = R(w * 0.34);
|
|
183
|
+
const btn = (side, top, hh) => `<span style="position:absolute;${side}:-3px;top:${R(h * top)}px;width:3px;height:${R(h * hh)}px;border-radius:2px;background:${c.f}"></span>`;
|
|
184
|
+
const html = `<div style="position:relative;width:${w}px;height:${h}px">${btn("left", 0.18, 0.07)}${btn("left", 0.28, 0.11)}${btn("right", 0.24, 0.13)}<div style="position:relative;width:${w}px;height:${h}px;border-radius:${rad}px;background:${c.f};padding:${b}px;box-sizing:border-box;box-shadow:${shadow}">${mediaSurface(o, mw, mh, `${rad - b}px`)}<span style="position:absolute;left:50%;top:${b + R(ih * 0.6)}px;width:${iw}px;height:${ih}px;margin-left:${-iw / 2}px;border-radius:${ih}px;background:${c.f}"></span><span style="position:absolute;left:50%;bottom:${b + R(ih * 0.5)}px;width:${hb}px;height:${Math.max(3, R(w * 0.02))}px;margin-left:${-hb / 2}px;border-radius:4px;background:${c.f};opacity:.85"></span></div></div>`;
|
|
185
|
+
return { html, h };
|
|
186
|
+
},
|
|
187
|
+
monitor(o, k) {
|
|
188
|
+
const { c, w } = k;
|
|
189
|
+
const ratio = k.ratio[1] > k.ratio[0] ? [16, 10] : k.ratio;
|
|
190
|
+
const b = R(clamp(w * 0.025, 5, 10));
|
|
191
|
+
const chin = R(clamp(w * 0.055, 12, 20));
|
|
192
|
+
const mw = w - b * 2;
|
|
193
|
+
const mh = R(mw * ratio[1] / ratio[0]);
|
|
194
|
+
const nw = R(w * 0.12);
|
|
195
|
+
const nh = R(w * 0.09);
|
|
196
|
+
const bw = R(w * 0.38);
|
|
197
|
+
const bh = R(clamp(w * 0.022, 5, 9));
|
|
198
|
+
const h = b + mh + chin + nh + bh;
|
|
199
|
+
const html = `<div style="width:${w}px;height:${h}px;display:flex;flex-direction:column;align-items:center;filter:drop-shadow(0 ${R(w * 0.03)}px ${R(w * 0.05)}px rgba(0,0,0,.3))"><div style="width:${w}px;border-radius:${R(clamp(w * 0.03, 6, 12))}px;background:${c.f};padding:${b}px ${b}px 0;box-sizing:border-box">${mediaSurface(o, mw, mh, "3px")}<div style="height:${chin}px;display:flex;align-items:center;justify-content:center"><span style="width:${R(chin * 0.32)}px;height:${R(chin * 0.32)}px;border-radius:50%;background:${c.d};opacity:.35"></span></div></div><div style="width:${nw}px;height:${nh}px;flex:none;background:${c.f};clip-path:polygon(18% 0,82% 0,100% 100%,0 100%)"></div><div style="width:${bw}px;height:${bh}px;flex:none;border-radius:${bh}px;background:${c.f}"></div></div>`;
|
|
200
|
+
return { html, h };
|
|
201
|
+
},
|
|
202
|
+
polaroid(o, k) {
|
|
203
|
+
const { c, w, T, copy, shadow } = k;
|
|
204
|
+
const p = R(w * 0.06);
|
|
205
|
+
const bot = R(w * 0.24);
|
|
206
|
+
const mw = w - p * 2;
|
|
207
|
+
const mh = mw;
|
|
208
|
+
const h = p + mh + bot;
|
|
209
|
+
const cap = copy.caps[(o.idx ?? 0) % copy.caps.length][0];
|
|
210
|
+
const tw = R(w * 0.36);
|
|
211
|
+
const th = R(clamp(w * 0.1, 14, 30));
|
|
212
|
+
const html = `<div style="position:relative;width:${w}px;height:${h}px"><div style="width:${w}px;height:${h}px;background:${c.f};border-radius:3px;padding:${p}px ${p}px 0;box-sizing:border-box;box-shadow:${shadow}">${mediaSurface(o, mw, mh, "1px")}<div style="height:${bot}px;display:flex;align-items:center;justify-content:center">` + (T === "S" && w < 180 ? "" : `<span style="font:italic 400 ${R(bot * 0.44)}px/1 'Instrument Serif',Georgia,serif;color:${c.d};opacity:.85">${cap}</span>`) + `</div></div><span style="position:absolute;left:50%;top:${-R(th * 0.45)}px;width:${tw}px;height:${th}px;margin-left:${-tw / 2}px;transform:rotate(-4deg);background:${hexA(c.d, 0.22)}"></span></div>`;
|
|
213
|
+
return { html, h };
|
|
214
|
+
},
|
|
215
|
+
stream(o, k) {
|
|
216
|
+
const { c, w, T, copy, font, rr, ring, shadow } = k;
|
|
217
|
+
const mh = R(w * k.ratio[1] / k.ratio[0]);
|
|
218
|
+
const h = mh;
|
|
219
|
+
const p = Math.max(8, R(w * 0.04));
|
|
220
|
+
const fs = clamp(R(w * 0.03), 9, 12);
|
|
221
|
+
const st = copy.stream ?? [copy.title, copy.caps[(o.idx ?? 0) % copy.caps.length][1]];
|
|
222
|
+
const live = `<div style="position:absolute;left:${p}px;top:${p}px;display:flex;align-items:center;gap:${R(fs * 0.5)}px;background:${c.f};color:${c.d};border-radius:4px;padding:${R(fs * 0.45)}px ${R(fs * 0.6)}px;font:500 ${fs}px/1 ${MONO};letter-spacing:.1em"><span style="width:${R(fs * 0.65)}px;height:${R(fs * 0.65)}px;border-radius:50%;background:${c.d}"></span>${T === "S" ? "" : "LIVE"}</div>`;
|
|
223
|
+
const viewers = T === "L" ? `<div style="position:absolute;right:${p}px;top:${p}px;background:${hexA(c.d, 0.75)};color:${c.f};border-radius:4px;padding:${R(fs * 0.45)}px ${R(fs * 0.6)}px;font:500 ${fs}px/1 ${MONO};letter-spacing:.06em">12.4K WATCHING</div>` : "";
|
|
224
|
+
const lh = R(clamp(w * 0.085, 18, 32));
|
|
225
|
+
const lower = T === "S" ? "" : `<div style="position:absolute;left:${p}px;bottom:${p}px;right:${R(w * 0.22)}px;height:${lh}px;display:flex"><div style="flex:1;min-width:0;background:${c.f};color:${c.d};display:flex;align-items:center;padding:0 ${R(lh * 0.4)}px;font:800 ${R(lh * 0.44)}px/1 ${font};white-space:nowrap;overflow:hidden">${T === "L" ? st[0] : ""}</div><div style="background:${c.d};color:${c.f};display:flex;align-items:center;padding:0 ${R(lh * 0.35)}px;font:500 ${R(lh * 0.42)}px/1 ${MONO};white-space:nowrap">${st[1]}</div></div>`;
|
|
226
|
+
const html = `<div style="position:relative;width:${w}px;height:${h}px;border-radius:${rr}px;box-shadow:0 0 0 ${ring}px ${c.f},${shadow};overflow:hidden">${mediaSurface({ ...o, clip: false }, w, mh, `${rr}px`)}${live}${viewers}${lower}</div>`;
|
|
227
|
+
return { html, h };
|
|
228
|
+
},
|
|
229
|
+
post(o, k) {
|
|
230
|
+
const { c, w, T, copy, font, rr, shadow } = k;
|
|
231
|
+
const p = R(clamp(w * 0.045, 8, 14));
|
|
232
|
+
const av = R(clamp(w * 0.1, 18, 32));
|
|
233
|
+
const mw = w - p * 2;
|
|
234
|
+
const mh = R(mw * k.ratio[1] / k.ratio[0]);
|
|
235
|
+
const cap = copy.caps[(o.idx ?? 0) % copy.caps.length];
|
|
236
|
+
const who = T === "L" ? `<div style="display:flex;flex-direction:column;gap:3px;min-width:0"><span style="font:800 ${R(av * 0.42)}px/1 ${font};color:${c.d};white-space:nowrap">${cap[0]}</span><span style="font:500 ${R(av * 0.3)}px/1 ${MONO};color:${c.d};opacity:.6;white-space:nowrap">@${cap[0].toLowerCase().replace(/[^a-z0-9]/g, "") || "user"}</span></div>` : `<div style="display:flex;flex-direction:column;gap:${R(av * 0.16)}px;flex:1"><span style="display:block;width:${T === "M" ? "46%" : "60%"};height:${R(av * 0.26)}px;border-radius:4px;background:${c.d};opacity:.55"></span>` + (T === "M" ? `<span style="display:block;width:30%;height:${R(av * 0.18)}px;border-radius:4px;background:${c.d};opacity:.3"></span>` : "") + `</div>`;
|
|
237
|
+
const head = `<div style="height:${av}px;display:flex;align-items:center;gap:${R(av * 0.35)}px;margin-bottom:${p}px"><span style="width:${av}px;height:${av}px;border-radius:50%;background:${c.d};flex:none"></span>${who}</div>`;
|
|
238
|
+
const ic = R(clamp(w * 0.055, 10, 18));
|
|
239
|
+
const icons = `<div style="display:flex;gap:${R(ic * 0.9)}px;align-items:center;height:${ic}px;margin-top:${p}px">` + [
|
|
240
|
+
"M12 21C5 16 2 12 2 8.5A4.5 4.5 0 0 1 12 6a4.5 4.5 0 0 1 10 2.5C22 12 19 16 12 21z",
|
|
241
|
+
"M4 4h16v12H9l-5 4z",
|
|
242
|
+
"M3 12l18-8-7 17-3-7z"
|
|
243
|
+
].map(
|
|
244
|
+
(d) => `<svg width="${ic}" height="${ic}" viewBox="0 0 24 24" fill="none" stroke="${c.d}" stroke-width="2.6" stroke-linejoin="round" style="display:block"><path d="${d}"/></svg>`
|
|
245
|
+
).join("") + `</div>`;
|
|
246
|
+
const h = p + av + p + mh + p + ic + p;
|
|
247
|
+
const html = `<div style="width:${w}px;height:${h}px;box-sizing:border-box;padding:${p}px;border-radius:${rr}px;background:${c.f};box-shadow:${shadow}">${head}${mediaSurface(o, mw, mh, `${R(rr * 0.6)}px`)}${icons}</div>`;
|
|
248
|
+
return { html, h };
|
|
249
|
+
},
|
|
250
|
+
circle(o, k) {
|
|
251
|
+
const { c, w, shadow } = k;
|
|
252
|
+
const rw = R(clamp(w * 0.035, 4, 8));
|
|
253
|
+
const dot = R(w * 0.2);
|
|
254
|
+
const mw = w - rw * 2;
|
|
255
|
+
const html = `<div style="position:relative;width:${w}px;height:${w}px">` + (w >= 170 ? `<div style="position:absolute;inset:${-R(w * 0.07)}px;border-radius:50%;border:2px dashed ${c.f};opacity:.5"></div>` : "") + `<div style="width:${w}px;height:${w}px;border-radius:50%;background:${c.f};padding:${rw}px;box-sizing:border-box;box-shadow:${shadow}">${mediaSurface({ ...o, clip: false }, mw, mw, "50%")}</div><span style="position:absolute;right:${R(w * 0.03)}px;bottom:${R(w * 0.03)}px;width:${dot}px;height:${dot}px;border-radius:50%;background:${c.f};display:flex;align-items:center;justify-content:center"><span style="width:${R(dot * 0.5)}px;height:${R(dot * 0.5)}px;border-radius:50%;background:${c.d}"></span></span></div>`;
|
|
256
|
+
return { html, h: w };
|
|
257
|
+
},
|
|
258
|
+
message(o, k) {
|
|
259
|
+
const { c, w, T } = k;
|
|
260
|
+
const p = R(clamp(w * 0.03, 5, 9));
|
|
261
|
+
const mw = w - p * 2;
|
|
262
|
+
const mh = R(mw * k.ratio[1] / k.ratio[0]);
|
|
263
|
+
const rad = R(clamp(w * 0.07, 12, 22));
|
|
264
|
+
const foot = T === "S" && w < 220 ? 0 : R(clamp(w * 0.075, 16, 24));
|
|
265
|
+
const h = p + mh + (foot || p);
|
|
266
|
+
const tail = R(clamp(w * 0.06, 10, 18));
|
|
267
|
+
const html = `<div style="position:relative;width:${w}px;height:${h}px;filter:drop-shadow(0 ${R(w * 0.025)}px ${R(w * 0.04)}px rgba(0,0,0,.3))"><div style="width:${w}px;height:${h}px;box-sizing:border-box;padding:${p}px ${p}px 0;border-radius:${rad}px ${rad}px ${rad}px 4px;background:${c.f}">${mediaSurface(o, mw, mh, `${rad - p}px`)}` + (foot ? `<div style="height:${foot}px;display:flex;align-items:center;justify-content:flex-end;gap:5px;font:500 ${R(foot * 0.5)}px/1 ${MONO};color:${c.d};opacity:.65">21:04 <span style="letter-spacing:-0.25em">\u2713\u2713</span></div>` : "") + `</div><svg width="${tail}" height="${tail}" viewBox="0 0 10 10" style="position:absolute;left:${-R(tail * 0.55)}px;bottom:0;display:block"><path d="M10 0v10H0c4-1 7-4 10-10z" fill="${c.f}"/></svg></div>`;
|
|
268
|
+
return { html, h };
|
|
269
|
+
},
|
|
270
|
+
slide(o, k) {
|
|
271
|
+
const { c, w, copy, shadow } = k;
|
|
272
|
+
const rad = R(w * 0.05);
|
|
273
|
+
const aw = R(w * 0.74);
|
|
274
|
+
const ah = R(aw * 0.75);
|
|
275
|
+
const top = R((w - ah) * 0.38);
|
|
276
|
+
const sx = R((w - aw) / 2);
|
|
277
|
+
const cap = copy.caps[(o.idx ?? 0) % copy.caps.length][0];
|
|
278
|
+
const html = `<div style="position:relative;width:${w}px;height:${w}px;border-radius:${rad}px;background:${c.f};box-shadow:${shadow}"><div style="position:absolute;left:${sx}px;top:${top}px;border-radius:${R(aw * 0.06)}px;overflow:hidden;box-shadow:0 0 0 2px ${hexA(c.d, 0.25)}">${mediaSurface({ ...o, clip: false }, aw, ah, `${R(aw * 0.06)}px`)}</div>` + (w >= 180 ? `<div style="position:absolute;left:${sx}px;right:${sx}px;bottom:${R(w * 0.065)}px;display:flex;justify-content:space-between;font:500 ${clamp(R(w * 0.045), 9, 12)}px/1 ${MONO};letter-spacing:.12em;color:${c.d};opacity:.7"><span>${cap.toUpperCase()}</span><span>${String((o.idx ?? 0) + 1).padStart(2, "0")}</span></div>` : "") + `<span style="position:absolute;left:${R(w * 0.06)}px;top:${R(w * 0.06)}px;width:${R(w * 0.04)}px;height:${R(w * 0.04)}px;border-radius:50%;background:${c.d};opacity:.3"></span></div>`;
|
|
279
|
+
return { html, h: w };
|
|
280
|
+
},
|
|
281
|
+
ticket(o, k) {
|
|
282
|
+
const { c, w, T, copy, font, rr } = k;
|
|
283
|
+
const sw = R(w * 0.28);
|
|
284
|
+
const mw = w - sw;
|
|
285
|
+
const ratio = k.ratio[1] > k.ratio[0] ? [4, 3] : k.ratio;
|
|
286
|
+
const mh = R(mw * ratio[1] / ratio[0]);
|
|
287
|
+
const h = mh;
|
|
288
|
+
const nr = R(clamp(w * 0.03, 6, 12));
|
|
289
|
+
const mask = `radial-gradient(circle at ${mw}px 0,transparent ${nr}px,#000 ${nr + 0.5}px) top/100% 51% no-repeat,radial-gradient(circle at ${mw}px 100%,transparent ${nr}px,#000 ${nr + 0.5}px) bottom/100% 51% no-repeat`;
|
|
290
|
+
const bars = [2, 1, 3, 1, 2, 1, 1, 3, 2, 1, 2, 3, 1, 2].map((bw) => `<span style="display:block;width:${bw}px;height:100%;background:${c.d}"></span>`).join("");
|
|
291
|
+
const fs = clamp(R(sw * 0.08), 8, 10);
|
|
292
|
+
const info = T === "L" ? `<div style="display:flex;flex-direction:column;gap:5px"><span style="font:500 ${fs}px/1 ${MONO};letter-spacing:.14em;color:${c.d};opacity:.65">ADMIT ONE</span><span style="font:800 ${R(sw * 0.13)}px/1.05 ${font};color:${c.d};letter-spacing:-0.01em">${copy.title}</span><span style="font:500 ${fs}px/1 ${MONO};letter-spacing:.12em;color:${c.d}">\u2116 00${(o.idx ?? 0) + 1}</span></div>` : "<div></div>";
|
|
293
|
+
const stub = `<div style="width:${sw}px;height:${h}px;flex:none;background:${c.f};border-left:2px dashed ${hexA(c.d, 0.35)};box-sizing:border-box;display:flex;flex-direction:column;justify-content:space-between;padding:${R(sw * 0.12)}px">${info}` + (T === "S" ? "" : `<div style="display:flex;gap:2px;height:${R(sw * 0.2)}px">${bars}</div>`) + `</div>`;
|
|
294
|
+
const html = `<div style="width:${w}px;height:${h}px;filter:drop-shadow(0 ${R(w * 0.025)}px ${R(w * 0.04)}px rgba(0,0,0,.3))"><div style="display:flex;width:${w}px;height:${h}px;border-radius:${rr}px;overflow:hidden;-webkit-mask:${mask};mask:${mask}">${mediaSurface({ ...o, clip: false }, mw, mh, "0")}${stub}</div></div>`;
|
|
295
|
+
return { html, h };
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
function frame(o) {
|
|
299
|
+
const kind = o.kind ?? "browser";
|
|
300
|
+
const c = o.c;
|
|
301
|
+
const w = R(o.w);
|
|
302
|
+
const T = tier(w);
|
|
303
|
+
const ratio = FORCE[kind] ?? o.ratio ?? [16, 9];
|
|
304
|
+
const copy = (o.pack ? COPY[o.pack] : void 0) ?? fallbackCopy;
|
|
305
|
+
const font = o.font ?? FONT;
|
|
306
|
+
const rr = R(clamp(w * 0.035, 8, 14));
|
|
307
|
+
const shadow = `0 ${R(w * 0.03)}px ${R(w * 0.08)}px rgba(0,0,0,.32)`;
|
|
308
|
+
const ring = R(clamp(w * 8e-3, 2, 3));
|
|
309
|
+
const ctx = { c, w, T, ratio, copy, font, rr, shadow, ring };
|
|
310
|
+
const build = BUILDERS[kind];
|
|
311
|
+
if (build) {
|
|
312
|
+
const { html: html2, h } = build(o, ctx);
|
|
313
|
+
return { html: html2, h, tier: T };
|
|
314
|
+
}
|
|
315
|
+
const mh = R(w * ratio[1] / ratio[0]);
|
|
316
|
+
const html = `<div style="width:${w}px;height:${mh}px;border-radius:${rr}px;box-shadow:${o.ring === false ? "" : `0 0 0 ${ring}px ${c.f},`}${shadow};overflow:hidden">${mediaSurface(o, w, mh, `${rr}px`)}</div>`;
|
|
317
|
+
return { html, h: mh, tier: T };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/index.ts
|
|
321
|
+
var PROBE_W = 320;
|
|
322
|
+
function windowShape(o) {
|
|
323
|
+
const kind = o.kind ?? "browser";
|
|
324
|
+
const probeColors = { f: "#000000", d: "#ffffff" };
|
|
325
|
+
const probe = frame({
|
|
326
|
+
...o,
|
|
327
|
+
kind,
|
|
328
|
+
w: PROBE_W,
|
|
329
|
+
c: probeColors
|
|
330
|
+
});
|
|
331
|
+
const ar = PROBE_W / probe.h;
|
|
332
|
+
const def = {
|
|
333
|
+
id: o.id,
|
|
334
|
+
label: o.label ?? o.id,
|
|
335
|
+
kind: "obj",
|
|
336
|
+
ar,
|
|
337
|
+
scale: (o.scale ?? 3) * (SCALE[kind] ?? 1),
|
|
338
|
+
/**
|
|
339
|
+
* Builds at the particle's real width, not a reference one, so chrome
|
|
340
|
+
* picks the right tier — a 240px frame merges its controls into a single
|
|
341
|
+
* capsule, a 420px one shows a URL. Building at a fixed width and scaling
|
|
342
|
+
* would both overflow the particle box and freeze the chrome at one tier.
|
|
343
|
+
*/
|
|
344
|
+
render: (c, _variant, width) => frame({ ...o, kind, w: width ?? PROBE_W, c }).html
|
|
345
|
+
};
|
|
346
|
+
return o.place === void 0 ? def : { ...def, place: o.place };
|
|
347
|
+
}
|
|
348
|
+
function image(o) {
|
|
349
|
+
return windowShape({ ...o, kind: "bare" });
|
|
350
|
+
}
|
|
351
|
+
function video(o) {
|
|
352
|
+
return windowShape({ ...o, kind: "bare", clip: o.clip ?? true });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export { COPY, KINDS, SCALE, frame, image, isVideo, mediaSurface, tier, video, windowShape };
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usemote/media",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Window frames and media particles for Mote — photos, GIFs and video that float with the shapes.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "George Heavenson",
|
|
8
|
+
"url": "https://hearvie.dev"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/oti-adjei/mote.git",
|
|
13
|
+
"directory": "packages/media"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://mote.hearvie.dev",
|
|
16
|
+
"bugs": "https://github.com/oti-adjei/mote/issues",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"require": "./dist/index.cjs"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"size-limit": [
|
|
35
|
+
{
|
|
36
|
+
"path": "dist/index.js",
|
|
37
|
+
"limit": "12 kB"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@usemote/core": "^0.1.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@usemote/core": "0.1.1",
|
|
45
|
+
"happy-dom": "^20.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"size": "size-limit"
|
|
55
|
+
}
|
|
6
56
|
}
|