overlaify 0.0.0-stage → 0.1.2
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 +123 -2
- package/assets/overlay-spec.md +196 -0
- package/assets/sdk/v1/overlaify.js +437 -0
- package/assets/templates/be-right-back.html +123 -0
- package/assets/templates/catalog.json +348 -0
- package/assets/templates/chat-stack.html +68 -0
- package/assets/templates/death-counter.html +94 -0
- package/assets/templates/emote-wall.html +125 -0
- package/assets/templates/goal-bar.html +89 -0
- package/assets/templates/hug-command.html +151 -0
- package/assets/templates/neon-alert.html +84 -0
- package/assets/templates/newsroom-ticker.html +136 -0
- package/assets/templates/starting-soon.html +145 -0
- package/assets/templates/subathon-timer.html +127 -0
- package/assets/templates/webcam-frame.html +93 -0
- package/bin/overlaify.js +114 -0
- package/lib/commands.js +67 -0
- package/lib/dev.html +328 -0
- package/lib/dev.js +181 -0
- package/lib/publish.js +272 -0
- package/lib/starter.html +51 -0
- package/lib/validate.js +362 -0
- package/lib/ws.js +79 -0
- package/package.json +35 -4
- package/skill/SKILL.md +88 -0
- package/skill/examples/chat-stack.html +68 -0
- package/skill/examples/goal-bar.html +89 -0
- package/skill/examples/neon-alert.html +84 -0
- package/skill/references/overlay-spec.md +196 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<script src="https://www.overlaify.com/sdk/v1/overlaify.js"></script>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Liquid Goal Bar</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
|
|
9
|
+
<script type="application/json" id="overlaify-manifest">
|
|
10
|
+
{
|
|
11
|
+
"spec": 1,
|
|
12
|
+
"settings": [
|
|
13
|
+
{"key": "type", "label": "What to count", "type": "segmented", "default": "subs", "options": [{"value": "subs", "label": "Subs"}, {"value": "bits", "label": "Bits"}], "group": "Goal"},
|
|
14
|
+
{"key": "goal", "label": "Goal", "type": "number", "default": 50, "desc": "Defaults to 50 subs or 10,000 bits.", "group": "Goal"},
|
|
15
|
+
{"key": "start", "label": "Starting value", "type": "number", "default": 0, "desc": "E.g. subs you already have this month.", "group": "Goal"},
|
|
16
|
+
{"key": "label", "label": "Title", "type": "text", "default": "Sub goal", "group": "Look"},
|
|
17
|
+
{"key": "color", "label": "Fill color", "type": "color", "default": "#ff4f8b", "swatches": ["#ff4f8b", "#ffb547", "#c6ff3d", "#5ce1ff", "#a879ff"], "group": "Look"}
|
|
18
|
+
],
|
|
19
|
+
"actions": [
|
|
20
|
+
{"name": "add", "label": "Add", "input": {"type": "number", "label": "Amount", "default": 1, "step": 1, "min": 1}},
|
|
21
|
+
{"name": "set", "label": "Set progress", "style": "secondary", "input": {"type": "number", "label": "Value", "default": 0, "step": 1, "min": 0}},
|
|
22
|
+
{"name": "reset", "label": "Reset to 0", "style": "danger", "confirm": true, "wide": true}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
<style>
|
|
27
|
+
:root { --c: #ff4f8b; --c2: #ffb547; }
|
|
28
|
+
html, body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; background: transparent; }
|
|
29
|
+
.wrap { position: absolute; left: 50%; top: 36px; transform: translateX(-50%); width: 900px; font-family: 'Space Grotesk', system-ui, sans-serif; color: #fff; }
|
|
30
|
+
.row { display: flex; justify-content: space-between; align-items: baseline; margin: 0 6px 10px; text-shadow: 0 2px 6px rgba(0,0,0,.7); }
|
|
31
|
+
.label { font-size: 28px; font-weight: 700; letter-spacing: .02em; }
|
|
32
|
+
.count { font-size: 26px; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
33
|
+
.track { position: relative; height: 34px; border-radius: 99px; background: rgba(12,10,20,.75); border: 2px solid rgba(255,255,255,.18); overflow: hidden; box-shadow: 0 6px 24px rgba(0,0,0,.4); }
|
|
34
|
+
.fill { position: absolute; inset: 0 auto 0 0; width: 0; border-radius: 99px;
|
|
35
|
+
background: linear-gradient(90deg, var(--c), var(--c2)); transition: width 1.2s cubic-bezier(.2,1,.3,1);
|
|
36
|
+
box-shadow: 0 0 20px var(--c); }
|
|
37
|
+
.fill::after { content: ''; position: absolute; inset: 0; border-radius: inherit;
|
|
38
|
+
background: repeating-linear-gradient(115deg, rgba(255,255,255,.22) 0 14px, transparent 14px 28px); animation: flow 1s linear infinite; }
|
|
39
|
+
.bump { animation: bump .5s; }
|
|
40
|
+
.done .fill { background: linear-gradient(90deg, #c6ff3d, #4dffa6); box-shadow: 0 0 28px #c6ff3d; }
|
|
41
|
+
@keyframes flow { to { background-position: 28px 0; } }
|
|
42
|
+
@keyframes bump { 40% { transform: scaleY(1.25); } }
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<div class="wrap" id="w">
|
|
47
|
+
<div class="row"><span class="label" id="label"></span><span class="count" id="count"></span></div>
|
|
48
|
+
<div class="track" id="track"><div class="fill" id="fill"></div></div>
|
|
49
|
+
</div>
|
|
50
|
+
<script>
|
|
51
|
+
// ?type=subs|bits|follows &goal=50 &start=0 &label=Sub%20goal &color=%23ff4f8b
|
|
52
|
+
const TYPE = Overlaify.param('type', 'subs');
|
|
53
|
+
const GOAL = Overlaify.param('goal', TYPE === 'bits' ? 10000 : 50);
|
|
54
|
+
const KEY = `overlaify-goal-${Overlaify.channel}-${TYPE}-${GOAL}`;
|
|
55
|
+
const defaultLabel = { subs: 'Sub goal', bits: 'Bits goal', follows: 'Follower goal' }[TYPE] || 'Goal';
|
|
56
|
+
document.getElementById('label').textContent = Overlaify.param('label', defaultLabel);
|
|
57
|
+
document.documentElement.style.setProperty('--c', Overlaify.param('color', '#ff4f8b'));
|
|
58
|
+
|
|
59
|
+
let value = Overlaify.param('start', 0);
|
|
60
|
+
try { const saved = Number(localStorage.getItem(KEY)); if (saved > value) value = saved; } catch (e) {}
|
|
61
|
+
|
|
62
|
+
const fill = document.getElementById('fill'), count = document.getElementById('count'), track = document.getElementById('track');
|
|
63
|
+
function render(bump) {
|
|
64
|
+
const pct = Math.min(100, value / GOAL * 100);
|
|
65
|
+
fill.style.width = pct + '%';
|
|
66
|
+
count.textContent = `${value.toLocaleString()} / ${GOAL.toLocaleString()}`;
|
|
67
|
+
document.getElementById('w').classList.toggle('done', value >= GOAL);
|
|
68
|
+
if (bump) { track.classList.remove('bump'); void track.offsetWidth; track.classList.add('bump'); }
|
|
69
|
+
try { localStorage.setItem(KEY, String(value)); } catch (e) {}
|
|
70
|
+
}
|
|
71
|
+
function add(n) { if (n > 0) { const was = value < GOAL; value += n; render(true); if (was && value >= GOAL) Overlaify.chime('fanfare'); } }
|
|
72
|
+
Overlaify.action('add', n => add(Number(n) || 0));
|
|
73
|
+
Overlaify.action('set', n => { value = Math.max(0, Number(n) || 0); render(true); });
|
|
74
|
+
Overlaify.action('reset', () => { value = 0; render(true); });
|
|
75
|
+
|
|
76
|
+
if (TYPE === 'subs') {
|
|
77
|
+
Overlaify.on('sub', () => add(1));
|
|
78
|
+
Overlaify.on('gift', () => add(1));
|
|
79
|
+
// giftbomb is followed by individual 'gift' events from Twitch; count it only for simulations
|
|
80
|
+
Overlaify.on('giftbomb', e => e.simulated && add(e.count));
|
|
81
|
+
} else if (TYPE === 'bits') {
|
|
82
|
+
Overlaify.on('cheer', e => add(e.bits));
|
|
83
|
+
} else if (TYPE === 'follows') {
|
|
84
|
+
Overlaify.on('follow', () => add(1));
|
|
85
|
+
}
|
|
86
|
+
render(false);
|
|
87
|
+
</script>
|
|
88
|
+
</body>
|
|
89
|
+
</html>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<script src="https://www.overlaify.com/sdk/v1/overlaify.js"></script>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>!hug Blob Hugs</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@600;700&display=swap" rel="stylesheet">
|
|
9
|
+
<script type="application/json" id="overlaify-manifest">
|
|
10
|
+
{
|
|
11
|
+
"spec": 1,
|
|
12
|
+
"settings": [
|
|
13
|
+
{"key": "command", "label": "Chat command", "type": "text", "default": "!hug", "desc": "\"!hug @name\" hugs that chatter; a bare \"!hug\" hugs the whole chat."},
|
|
14
|
+
{"key": "duration", "label": "Time on screen", "type": "range", "default": 5, "min": 2, "max": 20, "step": 1, "unit": "s"},
|
|
15
|
+
{"key": "cooldown", "label": "Cooldown", "type": "range", "default": 0, "min": 0, "max": 300, "step": 5, "unit": "s", "desc": "Seconds before the same chatter can hug again. 0 for none."}
|
|
16
|
+
],
|
|
17
|
+
"actions": [
|
|
18
|
+
{"name": "hug", "label": "Send a hug", "wide": true, "input": {"type": "text", "label": "Hug who", "default": "chat"}}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
<style>
|
|
23
|
+
html, body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; background: transparent; }
|
|
24
|
+
#stage { position: absolute; left: 50%; bottom: 60px; width: 700px; height: 430px; margin-left: -350px; font-family: 'Fredoka', system-ui, sans-serif; }
|
|
25
|
+
.scene { position: absolute; inset: 0; opacity: 0; transition: opacity .3s; }
|
|
26
|
+
.scene.in { opacity: 1; }
|
|
27
|
+
.names { position: absolute; left: 0; right: 0; top: 0; text-align: center; font-weight: 700; font-size: 44px; color: #fff;
|
|
28
|
+
text-shadow: 0 3px 0 rgba(0,0,0,.35), 0 0 14px rgba(0,0,0,.4); transform: translateY(-20px); opacity: 0; transition: all .45s cubic-bezier(.3,1.6,.5,1) .6s; }
|
|
29
|
+
.scene.in .names { transform: none; opacity: 1; }
|
|
30
|
+
.names b { color: var(--a); } .names b + span + b { color: var(--b); }
|
|
31
|
+
.names span { display: inline-block; margin: 0 12px; font-size: 40px; }
|
|
32
|
+
.sub { position: absolute; left: 0; right: 0; top: 58px; text-align: center; font-weight: 600; font-size: 26px; color: #ffe1f0;
|
|
33
|
+
text-shadow: 0 2px 6px rgba(0,0,0,.6); opacity: 0; transition: opacity .4s 1s; }
|
|
34
|
+
.scene.in .sub { opacity: 1; }
|
|
35
|
+
.blob { position: absolute; bottom: 0; width: 230px; height: 210px; }
|
|
36
|
+
.blob svg { width: 100%; height: 100%; overflow: visible; }
|
|
37
|
+
.blob.l { left: -260px; } .blob.r { right: -260px; }
|
|
38
|
+
.scene.in .blob.l { animation: inL 1.1s cubic-bezier(.3,1.3,.5,1) forwards, squishL .7s ease-in-out 1.1s 3; }
|
|
39
|
+
.scene.in .blob.r { animation: inR 1.1s cubic-bezier(.3,1.3,.5,1) forwards, squishR .7s ease-in-out 1.1s 3; }
|
|
40
|
+
/* Both arms live in the right blob's SVG (drawn last) so each one shows wrapping around the other blob. */
|
|
41
|
+
.blob .arm { opacity: 0; transition: opacity .25s .85s; }
|
|
42
|
+
.scene.in .blob .arm { opacity: 1; }
|
|
43
|
+
.blob .shut { opacity: 0; transition: opacity .2s 1.1s; } .blob .open { transition: opacity .2s 1.1s; }
|
|
44
|
+
.scene.in .blob .shut { opacity: 1; } .scene.in .blob .open { opacity: 0; }
|
|
45
|
+
.scene.out { opacity: 0; transition: opacity .5s; }
|
|
46
|
+
.heart { position: absolute; left: 50%; top: 240px; font-size: 40px; opacity: 0; animation: heart 1.8s ease-out forwards; }
|
|
47
|
+
@keyframes inL { to { left: 132px; } }
|
|
48
|
+
@keyframes inR { to { right: 132px; } }
|
|
49
|
+
@keyframes squishL { 50% { transform: translateX(14px) scale(1.06, .94); } }
|
|
50
|
+
@keyframes squishR { 50% { transform: translateX(-14px) scale(1.06, .94); } }
|
|
51
|
+
@keyframes heart { 0% { opacity: 0; transform: translate(-50%, 0) scale(.4); } 20% { opacity: 1; } 100% { opacity: 0; transform: translate(calc(-50% + var(--dx)), -150px) scale(1.2); } }
|
|
52
|
+
</style>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<div id="stage"></div>
|
|
56
|
+
<script>
|
|
57
|
+
// ?command=!hug &duration=5 (seconds on screen) &cooldown=0 (seconds per chatter)
|
|
58
|
+
const CMD = Overlaify.param('command', '!hug').toLowerCase();
|
|
59
|
+
const DURATION = Overlaify.param('duration', 5) * 1000;
|
|
60
|
+
const COOLDOWN = Overlaify.param('cooldown', 0) * 1000;
|
|
61
|
+
const stage = document.getElementById('stage');
|
|
62
|
+
const esc = Overlaify.escape;
|
|
63
|
+
const PALETTE = ['#ff8fb8', '#8fd3ff', '#b7f28a', '#ffd06b', '#c9a6ff', '#ff9f7a', '#7ff0d2'];
|
|
64
|
+
function colorFrom(s) { let h = 0; for (const ch of s) h = (h * 31 + ch.charCodeAt(0)) >>> 0; return PALETTE[h % PALETTE.length]; }
|
|
65
|
+
// Very dark name colors make a gloomy blob, so lighten them a bit.
|
|
66
|
+
function blobColor(c, fallbackKey) {
|
|
67
|
+
const m = /^#([0-9a-f]{6})$/i.exec(c || '');
|
|
68
|
+
if (!m) return colorFrom(fallbackKey);
|
|
69
|
+
const n = parseInt(m[1], 16), r = n >> 16, g = (n >> 8) & 255, b = n & 255;
|
|
70
|
+
return (r * .3 + g * .59 + b * .11) < 90 ? colorFrom(fallbackKey) : c;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Arms stroke with a darker edge so they read against a same-colored body.
|
|
74
|
+
function arm(d, color) {
|
|
75
|
+
return `<g class="arm"><path d="${d}" stroke="rgba(0,0,0,.2)" stroke-width="36" stroke-linecap="round" fill="none"/>
|
|
76
|
+
<path d="${d}" stroke="${color}" stroke-width="28" stroke-linecap="round" fill="none"/></g>`;
|
|
77
|
+
}
|
|
78
|
+
function blob(side, color, other) {
|
|
79
|
+
// The far eye sits toward the other blob so they look at each other.
|
|
80
|
+
const dir = side === 'l' ? 1 : -1, ex = 115 + dir * 22;
|
|
81
|
+
return `<div class="blob ${side}"><svg viewBox="0 0 230 210">
|
|
82
|
+
<path d="M20 200 C 0 120, 40 30, 115 30 C 190 30, 230 120, 210 200 Z" fill="${color}"/>
|
|
83
|
+
<path d="M20 200 C 0 120, 40 30, 115 30 C 190 30, 230 120, 210 200 Z" fill="none" stroke="rgba(0,0,0,.18)" stroke-width="5"/>
|
|
84
|
+
<ellipse cx="80" cy="70" rx="22" ry="12" fill="#fff" opacity=".45" transform="rotate(-25 80 70)"/>
|
|
85
|
+
<g class="open"><circle cx="${ex - 30}" cy="110" r="10" fill="#2b2233"/><circle cx="${ex + 30}" cy="110" r="10" fill="#2b2233"/>
|
|
86
|
+
<circle cx="${ex - 27}" cy="106" r="3.5" fill="#fff"/><circle cx="${ex + 33}" cy="106" r="3.5" fill="#fff"/></g>
|
|
87
|
+
<g class="shut" stroke="#2b2233" stroke-width="5" fill="none" stroke-linecap="round"><path d="M${ex - 42} 110 q 12 -12 24 0"/><path d="M${ex + 18} 110 q 12 -12 24 0"/></g>
|
|
88
|
+
<ellipse cx="${ex - 50}" cy="132" rx="14" ry="8" fill="#ff6f9f" opacity=".55"/><ellipse cx="${ex + 50}" cy="132" rx="14" ry="8" fill="#ff6f9f" opacity=".55"/>
|
|
89
|
+
<path d="M${ex - 12} 134 q 12 12 24 0" stroke="#2b2233" stroke-width="5" fill="none" stroke-linecap="round"/>
|
|
90
|
+
${side === 'r' ? arm('M-10 168 q 40 6 62 -14', other) + arm('M40 128 q -50 -6 -78 26', color) : ''}
|
|
91
|
+
</svg></div>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hugSound() {
|
|
95
|
+
const v = Overlaify.param('volume', 0.25);
|
|
96
|
+
Overlaify.chime('pop', v * .6);
|
|
97
|
+
setTimeout(() => Overlaify.chime('coin', v * .25), 180);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let pending = 0;
|
|
101
|
+
const enqueue = Overlaify.queue(async ({ from, to }) => {
|
|
102
|
+
pending--;
|
|
103
|
+
const a = blobColor(from.color, from.user), b = colorFrom(to.toLowerCase());
|
|
104
|
+
const scene = document.createElement('div');
|
|
105
|
+
scene.className = 'scene';
|
|
106
|
+
scene.style.setProperty('--a', a); scene.style.setProperty('--b', b);
|
|
107
|
+
scene.innerHTML = `<div class="names"><b>${esc(from.name)}</b><span>🤗</span><b>${esc(to)}</b></div>
|
|
108
|
+
<div class="sub">sends a big hug!</div>${blob('l', a, b)}${blob('r', b, a)}`;
|
|
109
|
+
stage.appendChild(scene);
|
|
110
|
+
requestAnimationFrame(() => requestAnimationFrame(() => scene.classList.add('in')));
|
|
111
|
+
await Overlaify.sleep(1050);
|
|
112
|
+
hugSound();
|
|
113
|
+
['💗', '💕', '💖', '💗', '💞'].forEach((h, i) => setTimeout(() => {
|
|
114
|
+
const el = document.createElement('div');
|
|
115
|
+
el.className = 'heart'; el.textContent = h; el.style.setProperty('--dx', (Math.random() * 200 - 100) + 'px');
|
|
116
|
+
scene.appendChild(el); setTimeout(() => el.remove(), 1900);
|
|
117
|
+
}, i * 300));
|
|
118
|
+
await Overlaify.sleep(DURATION);
|
|
119
|
+
scene.classList.add('out');
|
|
120
|
+
await Overlaify.sleep(500);
|
|
121
|
+
scene.remove();
|
|
122
|
+
}, { gap: 300 });
|
|
123
|
+
|
|
124
|
+
Overlaify.action('hug', to => {
|
|
125
|
+
const name = Overlaify.channel || 'Streamer';
|
|
126
|
+
pending++;
|
|
127
|
+
enqueue({ from: { user: name.toLowerCase(), name, color: '' }, to: String(to || '').replace(/^@/, '').trim() || 'chat' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const lastUse = {};
|
|
131
|
+
const NAMES = ['PixelPanda', 'nightowl_gg', 'CaptainCrit', 'mochi_bytes', 'SpeedySloth', 'RetroRaven', 'ZeroGravityZoe'];
|
|
132
|
+
Overlaify.on('chat', e => {
|
|
133
|
+
let to;
|
|
134
|
+
const msg = e.message.trim(), lower = msg.toLowerCase();
|
|
135
|
+
if (lower === CMD || lower.startsWith(CMD + ' ')) {
|
|
136
|
+
// "!hug @name" hugs that person; a bare "!hug" hugs the whole chat.
|
|
137
|
+
const m = /^@?([A-Za-z0-9_]{1,25})/.exec(msg.slice(CMD.length).trim());
|
|
138
|
+
to = m ? m[1] : 'chat';
|
|
139
|
+
} else if (e.simulated) {
|
|
140
|
+
// Previews never type !hug, so simulated chatters hug someone at random (one waiting at most).
|
|
141
|
+
if (pending > 0) return;
|
|
142
|
+
to = NAMES.filter(n => n !== e.name)[Math.floor(Math.random() * (NAMES.length - 1))];
|
|
143
|
+
} else return;
|
|
144
|
+
if (COOLDOWN && Date.now() - (lastUse[e.user] || 0) < COOLDOWN) return;
|
|
145
|
+
lastUse[e.user] = Date.now();
|
|
146
|
+
pending++;
|
|
147
|
+
enqueue({ from: e, to });
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
150
|
+
</body>
|
|
151
|
+
</html>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<script src="https://www.overlaify.com/sdk/v1/overlaify.js"></script>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Neon Pulse Alerts</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;800&display=swap" rel="stylesheet">
|
|
9
|
+
<script type="application/json" id="overlaify-manifest">
|
|
10
|
+
{
|
|
11
|
+
"spec": 1,
|
|
12
|
+
"settings": [
|
|
13
|
+
{"key":"color","label":"Accent color","type":"color","default":"#c6ff3d","swatches":["#c6ff3d","#ff4f8b","#5ce1ff","#a879ff","#ffb547"],"desc":"Border, glow and kicker text."},
|
|
14
|
+
{"key":"duration","label":"Duration","type":"range","default":5,"min":2,"max":20,"step":0.5,"unit":"s","desc":"Seconds each alert stays on screen."}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
<style>
|
|
19
|
+
:root { --c: #c6ff3d; --c2: #9b5cff; }
|
|
20
|
+
html, body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; background: transparent; }
|
|
21
|
+
#stage { position: absolute; left: 50%; top: 120px; transform: translateX(-50%); }
|
|
22
|
+
.alert {
|
|
23
|
+
position: relative; min-width: 640px; padding: 34px 56px 38px; text-align: center;
|
|
24
|
+
font-family: 'Unbounded', system-ui, sans-serif; color: #fff;
|
|
25
|
+
background: rgba(10, 8, 18, .82); border-radius: 22px;
|
|
26
|
+
border: 3px solid var(--c);
|
|
27
|
+
box-shadow: 0 0 24px var(--c), inset 0 0 24px rgba(198,255,61,.25), 0 0 80px var(--c2);
|
|
28
|
+
opacity: 0; transform: scale(.6) translateY(-30px);
|
|
29
|
+
}
|
|
30
|
+
.alert.in { animation: pop .55s cubic-bezier(.2,1.6,.4,1) forwards; }
|
|
31
|
+
.alert.out { animation: leave .45s ease-in forwards; }
|
|
32
|
+
.kicker { font-size: 22px; font-weight: 500; letter-spacing: .32em; text-transform: uppercase; color: var(--c); text-shadow: 0 0 12px var(--c); }
|
|
33
|
+
.name { font-size: 64px; font-weight: 800; margin: 10px 0 6px; line-height: 1.05;
|
|
34
|
+
background: linear-gradient(90deg, #fff, var(--c), #fff); background-size: 200% 100%;
|
|
35
|
+
-webkit-background-clip: text; background-clip: text; color: transparent; animation: shine 1.6s linear infinite; }
|
|
36
|
+
.detail { font-size: 26px; font-weight: 500; color: #e9e4ff; }
|
|
37
|
+
.msg { margin-top: 14px; font: 500 22px system-ui, sans-serif; color: #cfc8e6; max-width: 760px; }
|
|
38
|
+
.ring { position: absolute; inset: -3px; border-radius: 22px; border: 3px solid var(--c); animation: ring 1.2s ease-out infinite; pointer-events: none; }
|
|
39
|
+
@keyframes pop { to { opacity: 1; transform: scale(1) translateY(0); } }
|
|
40
|
+
@keyframes leave { to { opacity: 0; transform: scale(.9) translateY(-40px); } }
|
|
41
|
+
@keyframes shine { to { background-position: -200% 0; } }
|
|
42
|
+
@keyframes ring { from { opacity: .9; transform: scale(1); } to { opacity: 0; transform: scale(1.12); } }
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<div id="stage"></div>
|
|
47
|
+
<script>
|
|
48
|
+
const accent = Overlaify.param('color', '#c6ff3d');
|
|
49
|
+
document.documentElement.style.setProperty('--c', accent);
|
|
50
|
+
const DURATION = Overlaify.param('duration', 5) * 1000;
|
|
51
|
+
const stage = document.getElementById('stage');
|
|
52
|
+
const esc = Overlaify.escape;
|
|
53
|
+
const tierLabel = t => t === 'prime' ? 'with Prime' : `Tier ${t}`;
|
|
54
|
+
|
|
55
|
+
function content(e) {
|
|
56
|
+
switch (e.type) {
|
|
57
|
+
case 'follow': return ['New follower', e.name, 'welcome to the squad', '', 'pop'];
|
|
58
|
+
case 'sub': return [e.months > 1 ? 'Resub' : 'New sub', e.name, `${tierLabel(e.tier)}${e.months > 1 ? ` · ${e.months} months` : ''}`, e.message, 'fanfare'];
|
|
59
|
+
case 'gift': return ['Gifted sub', e.name, `→ ${e.recipientName || e.recipient}`, '', 'alert'];
|
|
60
|
+
case 'giftbomb': return ['Gift bomb', e.name, `${e.count} subs to the community!`, '', 'fanfare'];
|
|
61
|
+
case 'cheer': return ['Cheer', e.name, `${e.bits.toLocaleString()} bits`, e.message, 'coin'];
|
|
62
|
+
case 'raid': return ['Raid incoming', e.name, `with ${e.viewers.toLocaleString()} raiders`, '', 'fanfare'];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const enqueue = Overlaify.queue(async (e) => {
|
|
67
|
+
const c = content(e); if (!c) return;
|
|
68
|
+
const [kicker, name, detail, msg, sound] = c;
|
|
69
|
+
const el = document.createElement('div');
|
|
70
|
+
el.className = 'alert';
|
|
71
|
+
el.innerHTML = `<div class="ring"></div><div class="kicker">${esc(kicker)}</div><div class="name">${esc(name)}</div><div class="detail">${esc(detail)}</div>${msg ? `<div class="msg">${esc(msg)}</div>` : ''}`;
|
|
72
|
+
stage.appendChild(el);
|
|
73
|
+
requestAnimationFrame(() => el.classList.add('in'));
|
|
74
|
+
Overlaify.chime(sound);
|
|
75
|
+
await Overlaify.sleep(DURATION);
|
|
76
|
+
el.classList.add('out');
|
|
77
|
+
await Overlaify.sleep(450);
|
|
78
|
+
el.remove();
|
|
79
|
+
}, { gap: 400 });
|
|
80
|
+
|
|
81
|
+
['follow', 'sub', 'gift', 'giftbomb', 'cheer', 'raid'].forEach(t => Overlaify.on(t, enqueue));
|
|
82
|
+
</script>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<script src="https://www.overlaify.com/sdk/v1/overlaify.js"></script>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Breaking News Ticker</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@500;700&family=Roboto+Condensed:wght@500;700&display=swap" rel="stylesheet">
|
|
9
|
+
<script type="application/json" id="overlaify-manifest">
|
|
10
|
+
{
|
|
11
|
+
"spec": 1,
|
|
12
|
+
"settings": [
|
|
13
|
+
{"key": "speed", "label": "Scroll speed", "type": "range", "default": 110, "min": 30, "max": 300, "step": 10, "unit": "px/s"},
|
|
14
|
+
{"key": "keep", "label": "Stories on the rail", "type": "range", "default": 10, "min": 2, "max": 30, "step": 1, "desc": "Older stories drop off as new ones arrive."},
|
|
15
|
+
{"key": "flash", "label": "Breaking-news banner", "type": "toggle", "default": true, "desc": "Show a banner when news breaks."}
|
|
16
|
+
],
|
|
17
|
+
"actions": [
|
|
18
|
+
{"name": "post", "label": "Post a story", "wide": true, "input": {"type": "text", "label": "Headline", "default": "Streamer spotted touching grass, more at 11"}},
|
|
19
|
+
{"name": "clear", "label": "Clear stories", "style": "danger", "confirm": "Clear the rail back to the opening stories?"}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
<style>
|
|
24
|
+
html, body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; background: transparent; }
|
|
25
|
+
.ticker { position: absolute; left: 0; right: 0; bottom: 0; height: 58px; display: flex; font-family: 'Roboto Condensed', sans-serif;
|
|
26
|
+
box-shadow: 0 -6px 20px rgba(0,0,0,.35); }
|
|
27
|
+
.tag { position: relative; z-index: 2; display: flex; align-items: center; gap: 10px; padding: 0 26px 0 22px; background: #d6001c; color: #fff;
|
|
28
|
+
font: 700 30px 'Oswald', sans-serif; letter-spacing: .06em; }
|
|
29
|
+
.tag::after { content: ''; position: absolute; right: -22px; top: 0; border-left: 22px solid #d6001c; border-top: 58px solid transparent; }
|
|
30
|
+
.tag .dot { width: 12px; height: 12px; border-radius: 50%; background: #fff; animation: live 1.2s ease-in-out infinite; }
|
|
31
|
+
.clock { position: relative; z-index: 2; display: flex; align-items: center; padding: 0 22px; background: #0c1a33; color: #fff; font: 700 24px 'Oswald', sans-serif;
|
|
32
|
+
font-variant-numeric: tabular-nums; border-left: 3px solid #d6001c; }
|
|
33
|
+
.rail { position: relative; flex: 1; overflow: hidden; background: linear-gradient(180deg, #f6f7fb, #dfe3ec); }
|
|
34
|
+
.rail::before { content: ''; position: absolute; left: 0; right: 0; top: 0; height: 4px; background: #0c1a33; z-index: 1; }
|
|
35
|
+
.track { position: absolute; top: 4px; bottom: 0; left: 0; display: flex; align-items: center; white-space: nowrap; will-change: transform; }
|
|
36
|
+
.item { display: inline-flex; align-items: center; gap: 12px; padding: 0 30px; font-size: 27px; color: #0c1a33; }
|
|
37
|
+
.item b { font-weight: 700; }
|
|
38
|
+
.item .k { font: 700 18px 'Oswald', sans-serif; letter-spacing: .1em; color: #fff; background: #0c1a33; padding: 3px 9px 2px; border-radius: 3px; }
|
|
39
|
+
.item .k.sub { background: #6a2cd8; } .item .k.gift { background: #0f8a5f; } .item .k.bits { background: #c77700; } .item .k.raid { background: #d6001c; }
|
|
40
|
+
.item::after { content: '◆'; margin-left: 30px; color: #d6001c; font-size: 16px; }
|
|
41
|
+
.item.fresh { animation: fresh 2s ease-out; }
|
|
42
|
+
/* full-width flash when news breaks */
|
|
43
|
+
.flash { position: absolute; left: 0; right: 0; bottom: 58px; height: 72px; display: flex; align-items: center; padding-left: 26px; gap: 18px;
|
|
44
|
+
background: linear-gradient(90deg, #d6001c, #a30016); color: #fff; font: 700 38px 'Oswald', sans-serif; letter-spacing: .02em;
|
|
45
|
+
transform: translateY(100%); clip-path: inset(0 0 0 0); transition: transform .4s cubic-bezier(.2,1,.3,1); }
|
|
46
|
+
.flash.on { transform: none; }
|
|
47
|
+
.flash small { font: 700 22px 'Oswald', sans-serif; letter-spacing: .2em; background: #fff; color: #d6001c; padding: 4px 10px; }
|
|
48
|
+
@keyframes live { 50% { opacity: .25; } }
|
|
49
|
+
@keyframes fresh { 0% { background: #ffe14d; } 100% { background: transparent; } }
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div class="flash" id="flash"><small>BREAKING</small><span id="flashText"></span></div>
|
|
54
|
+
<div class="ticker">
|
|
55
|
+
<div class="tag"><span class="dot"></span>BREAKING:</div>
|
|
56
|
+
<div class="rail"><div class="track" id="track"></div></div>
|
|
57
|
+
<div class="clock" id="clock"></div>
|
|
58
|
+
</div>
|
|
59
|
+
<script>
|
|
60
|
+
// ?speed=110 (pixels per second) &keep=10 (stories on the rail) &flash=1 (show a banner when news breaks)
|
|
61
|
+
const SPEED = Overlaify.param('speed', 110);
|
|
62
|
+
const KEEP = Overlaify.param('keep', 10);
|
|
63
|
+
const FLASH = Overlaify.param('flash', true);
|
|
64
|
+
const track = document.getElementById('track');
|
|
65
|
+
const esc = Overlaify.escape;
|
|
66
|
+
const name = s => `<b>${esc(s)}</b>`;
|
|
67
|
+
|
|
68
|
+
const OPENING = [
|
|
69
|
+
{ k: 'news', html: `Stream is <b>LIVE</b> — sources confirm vibes are immaculate` },
|
|
70
|
+
{ k: 'news', html: `Chat reminds streamer to hydrate, streamer reportedly "will in a sec"` },
|
|
71
|
+
];
|
|
72
|
+
const stories = OPENING.slice();
|
|
73
|
+
function story(e) {
|
|
74
|
+
const tier = e.tier === 'prime' ? 'with Prime' : `at Tier ${e.tier}`;
|
|
75
|
+
switch (e.type) {
|
|
76
|
+
case 'follow': return { k: 'news', label: 'FOLLOW', html: `${name(e.name)} joins the audience` };
|
|
77
|
+
case 'sub': return { k: 'sub', label: e.months > 1 ? 'RESUB' : 'SUB', html: e.months > 1 ? `${name(e.name)} renews for month ${e.months} ${tier}` : `${name(e.name)} subscribes ${tier}` };
|
|
78
|
+
case 'gift': return { k: 'gift', label: 'GIFT', html: `${name(e.name)} gifts a sub to ${name(e.recipientName || e.recipient)}` };
|
|
79
|
+
case 'giftbomb': return { k: 'gift', label: 'GIFT BOMB', html: `${name(e.name)} drops <b>${e.count}</b> gifted subs on the community` };
|
|
80
|
+
case 'cheer': return { k: 'bits', label: 'BITS', html: `${name(e.name)} cheers <b>${e.bits.toLocaleString()}</b> bits` };
|
|
81
|
+
case 'raid': return { k: 'raid', label: 'RAID', html: `${name(e.name)} raids with <b>${e.viewers.toLocaleString()}</b> viewers` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The rail is built from two copies of the stories so it loops without a gap.
|
|
86
|
+
let x = 0, width = 0, fresh = null;
|
|
87
|
+
function build() {
|
|
88
|
+
const one = stories.map((s, i) => `<span class="item${s === fresh ? ' fresh' : ''}">${s.label ? `<span class="k ${s.k}">${s.label}</span>` : ''}${s.html}</span>`).join('');
|
|
89
|
+
track.innerHTML = one + one;
|
|
90
|
+
width = track.scrollWidth / 2;
|
|
91
|
+
// Keep it at least as wide as the rail so one copy always covers it.
|
|
92
|
+
if (width < track.parentNode.offsetWidth) { track.innerHTML = one.repeat(Math.ceil(track.parentNode.offsetWidth / width) * 2); width = track.scrollWidth / 2; }
|
|
93
|
+
}
|
|
94
|
+
let last = performance.now();
|
|
95
|
+
function frame(t) {
|
|
96
|
+
x -= (t - last) / 1000 * SPEED; last = t;
|
|
97
|
+
if (width && -x >= width) x += width;
|
|
98
|
+
track.style.transform = `translateX(${x}px)`;
|
|
99
|
+
requestAnimationFrame(frame);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let flashTimer;
|
|
103
|
+
function flash(html) {
|
|
104
|
+
if (!FLASH) return;
|
|
105
|
+
const f = document.getElementById('flash');
|
|
106
|
+
document.getElementById('flashText').innerHTML = html;
|
|
107
|
+
f.classList.add('on');
|
|
108
|
+
clearTimeout(flashTimer);
|
|
109
|
+
flashTimer = setTimeout(() => f.classList.remove('on'), 4500);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function publish(s, sound) {
|
|
113
|
+
stories.unshift(s);
|
|
114
|
+
stories.length = Math.min(stories.length, KEEP);
|
|
115
|
+
fresh = s;
|
|
116
|
+
build();
|
|
117
|
+
x = 0; // the newest story starts right after the tag
|
|
118
|
+
flash(s.html);
|
|
119
|
+
Overlaify.chime(sound, Overlaify.param('volume', 0.25) * .6);
|
|
120
|
+
}
|
|
121
|
+
['follow', 'sub', 'gift', 'giftbomb', 'cheer', 'raid'].forEach(t => Overlaify.on(t, e => {
|
|
122
|
+
const s = story(e); if (!s) return;
|
|
123
|
+
publish(s, e.type === 'raid' ? 'fanfare' : 'pop');
|
|
124
|
+
}));
|
|
125
|
+
Overlaify.action('post', text => {
|
|
126
|
+
text = String(text || '').trim();
|
|
127
|
+
if (text) publish({ k: 'news', label: 'NEWS', html: esc(text) }, 'pop');
|
|
128
|
+
});
|
|
129
|
+
Overlaify.action('clear', () => { stories.splice(0, stories.length, ...OPENING); fresh = null; build(); x = 0; });
|
|
130
|
+
|
|
131
|
+
setInterval(() => { document.getElementById('clock').textContent = new Date().toTimeString().slice(0, 5); }, 1000);
|
|
132
|
+
document.getElementById('clock').textContent = new Date().toTimeString().slice(0, 5);
|
|
133
|
+
document.fonts.ready.then(() => { build(); requestAnimationFrame(frame); });
|
|
134
|
+
</script>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<script src="https://www.overlaify.com/sdk/v1/overlaify.js"></script>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>Starting Soon Countdown</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;600;800&family=JetBrains+Mono:wght@700&display=swap" rel="stylesheet">
|
|
9
|
+
<script type="application/json" id="overlaify-manifest">
|
|
10
|
+
{
|
|
11
|
+
"spec": 1,
|
|
12
|
+
"settings": [
|
|
13
|
+
{"key": "minutes", "label": "Countdown", "type": "range", "default": 5, "min": 1, "max": 60, "step": 1, "unit": "min", "desc": "Starts when the browser source loads.", "group": "Timing"},
|
|
14
|
+
{"key": "until", "label": "Count down to", "type": "text", "default": "", "desc": "A local clock time like 19:30. Overrides the countdown length when set.", "group": "Timing"},
|
|
15
|
+
{"key": "done", "label": "When it hits zero", "type": "text", "default": "Going live any second…", "group": "Timing"},
|
|
16
|
+
{"key": "kicker", "label": "Small heading", "type": "text", "default": "", "desc": "Leave empty to show your channel name.", "group": "Text"},
|
|
17
|
+
{"key": "title", "label": "Headline", "type": "text", "default": "Starting soon", "group": "Text"},
|
|
18
|
+
{"key": "text", "label": "Line under the timer", "type": "text", "default": "Grab a drink and say hi in chat — we start in a moment.", "group": "Text"},
|
|
19
|
+
{"key": "color", "label": "Accent color", "type": "color", "default": "#c6ff3d", "swatches": ["#c6ff3d", "#5ce1ff", "#ff4f8b", "#ffb547", "#a879ff"], "group": "Look"},
|
|
20
|
+
{"key": "bg", "label": "Background", "type": "toggle", "default": true, "desc": "Turn off for a transparent background over your own art.", "group": "Look"},
|
|
21
|
+
{"key": "max", "label": "Chat messages shown", "type": "range", "default": 5, "min": 1, "max": 12, "step": 1, "group": "Look"}
|
|
22
|
+
],
|
|
23
|
+
"actions": [
|
|
24
|
+
{"name": "add", "label": "Add time", "input": {"type": "number", "label": "Minutes", "default": 5, "step": 1, "min": 1}},
|
|
25
|
+
{"name": "pause", "label": "Pause", "style": "secondary"},
|
|
26
|
+
{"name": "resume", "label": "Resume"},
|
|
27
|
+
{"name": "restart", "label": "Restart countdown", "style": "danger", "confirm": true, "wide": true}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
<style>
|
|
32
|
+
:root { --c: #c6ff3d; --c2: #9b5cff; --c3: #ff4f8b; }
|
|
33
|
+
html, body { margin: 0; width: 1920px; height: 1080px; overflow: hidden; background: transparent; }
|
|
34
|
+
.scene { position: absolute; inset: 0; overflow: hidden; font-family: 'Sora', system-ui, sans-serif; color: #fff;
|
|
35
|
+
background: radial-gradient(120% 90% at 20% 10%, #1d1438 0%, #0b0a14 55%, #07060c 100%); }
|
|
36
|
+
.clear .scene { background: transparent; }
|
|
37
|
+
.clear .orb { display: none; }
|
|
38
|
+
.orb { position: absolute; border-radius: 50%; filter: blur(90px); opacity: .55; animation: drift 18s ease-in-out infinite alternate; }
|
|
39
|
+
.o1 { width: 760px; height: 760px; left: -160px; top: -200px; background: var(--c2); }
|
|
40
|
+
.o2 { width: 620px; height: 620px; right: -120px; bottom: -180px; background: var(--c3); animation-delay: -6s; }
|
|
41
|
+
.o3 { width: 420px; height: 420px; left: 46%; top: 58%; background: var(--c); opacity: .22; animation-delay: -11s; }
|
|
42
|
+
.grid { position: absolute; inset: 0; background-image: linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.04) 1px, transparent 1px); background-size: 64px 64px; mask-image: radial-gradient(70% 70% at 50% 50%, #000, transparent); }
|
|
43
|
+
.center { position: absolute; left: 140px; top: 250px; width: 1100px; }
|
|
44
|
+
.kicker { display: inline-flex; align-items: center; gap: 14px; font-size: 24px; font-weight: 600; letter-spacing: .3em; text-transform: uppercase; color: var(--c); }
|
|
45
|
+
.kicker i { width: 14px; height: 14px; border-radius: 50%; background: var(--c); box-shadow: 0 0 18px var(--c); animation: blink 1.4s ease-in-out infinite; }
|
|
46
|
+
h1 { margin: 22px 0 0; font-size: 148px; font-weight: 800; line-height: .95; letter-spacing: -.045em; }
|
|
47
|
+
h1 span { display: inline-block; animation: rise .9s cubic-bezier(.2,1.2,.3,1) both; }
|
|
48
|
+
.time { margin-top: 38px; font: 700 120px 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums; letter-spacing: -.02em;
|
|
49
|
+
background: linear-gradient(90deg, #fff, var(--c)); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
|
50
|
+
.time.live { font-size: 64px; animation: pulse 1s ease-in-out infinite; }
|
|
51
|
+
.text { margin-top: 18px; font-size: 32px; color: #d9d2f2; max-width: 980px; }
|
|
52
|
+
.chat { position: absolute; right: 90px; bottom: 90px; width: 520px; display: flex; flex-direction: column; gap: 12px; }
|
|
53
|
+
.chat h2 { margin: 0 0 4px; font-size: 18px; font-weight: 600; letter-spacing: .24em; text-transform: uppercase; color: rgba(255,255,255,.55); }
|
|
54
|
+
.msg { font-size: 24px; line-height: 1.35; padding: 12px 18px; border-radius: 14px; background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.1); backdrop-filter: blur(10px); animation: in .4s cubic-bezier(.2,1.2,.4,1); }
|
|
55
|
+
.msg b { color: var(--uc, var(--c)); margin-right: 8px; }
|
|
56
|
+
.msg.hype { border-color: var(--c); background: rgba(198,255,61,.12); }
|
|
57
|
+
@keyframes drift { to { transform: translate(120px, 80px) scale(1.15); } }
|
|
58
|
+
@keyframes blink { 50% { opacity: .25; } }
|
|
59
|
+
@keyframes rise { from { opacity: 0; transform: translateY(40px); } }
|
|
60
|
+
@keyframes pulse { 50% { opacity: .55; } }
|
|
61
|
+
@keyframes in { from { opacity: 0; transform: translateX(30px); } }
|
|
62
|
+
</style>
|
|
63
|
+
</head>
|
|
64
|
+
<body>
|
|
65
|
+
<div class="scene">
|
|
66
|
+
<div class="orb o1"></div><div class="orb o2"></div><div class="orb o3"></div><div class="grid"></div>
|
|
67
|
+
<div class="center">
|
|
68
|
+
<div class="kicker"><i></i><span id="kicker"></span></div>
|
|
69
|
+
<h1 id="title"></h1>
|
|
70
|
+
<div class="time" id="time"></div>
|
|
71
|
+
<div class="text" id="text"></div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="chat" id="chat"><h2>Chat while you wait</h2></div>
|
|
74
|
+
</div>
|
|
75
|
+
<script>
|
|
76
|
+
// ?minutes=5 or ?until=19:30 (local time) &title=Starting%20soon &text=... &color=%23c6ff3d &bg=0 (transparent)
|
|
77
|
+
const esc = Overlaify.escape;
|
|
78
|
+
document.documentElement.style.setProperty('--c', Overlaify.param('color', '#c6ff3d'));
|
|
79
|
+
if (Overlaify.param('bg', true) === false) document.body.classList.add('clear');
|
|
80
|
+
document.getElementById('kicker').textContent = Overlaify.param('kicker', Overlaify.channel ? Overlaify.channel : 'Stream');
|
|
81
|
+
document.getElementById('title').innerHTML = Overlaify.param('title', 'Starting soon').split(' ')
|
|
82
|
+
.map((w, i) => `<span style="animation-delay:${i * 90}ms">${esc(w)}</span>`).join(' ');
|
|
83
|
+
document.getElementById('text').textContent = Overlaify.param('text', 'Grab a drink and say hi in chat — we start in a moment.');
|
|
84
|
+
|
|
85
|
+
const until = Overlaify.param('until', '');
|
|
86
|
+
function startEnd() {
|
|
87
|
+
if (/^\d{1,2}:\d{2}$/.test(until)) {
|
|
88
|
+
const [h, m] = until.split(':').map(Number);
|
|
89
|
+
const d = new Date(); d.setHours(h, m, 0, 0);
|
|
90
|
+
if (d < Date.now() - 3600e3) d.setDate(d.getDate() + 1);
|
|
91
|
+
return d.getTime();
|
|
92
|
+
}
|
|
93
|
+
return Date.now() + Overlaify.param('minutes', 5) * 60e3;
|
|
94
|
+
}
|
|
95
|
+
// end = timestamp the countdown hits zero; while paused, pausedLeft = ms remaining.
|
|
96
|
+
let end = startEnd(), pausedLeft = null;
|
|
97
|
+
const msLeft = () => pausedLeft !== null ? pausedLeft : end - Date.now();
|
|
98
|
+
const time = document.getElementById('time');
|
|
99
|
+
const pad = n => String(n).padStart(2, '0');
|
|
100
|
+
let chimed = false;
|
|
101
|
+
(function tick() {
|
|
102
|
+
const left = Math.max(0, Math.round(msLeft() / 1000));
|
|
103
|
+
if (left > 0) {
|
|
104
|
+
const h = Math.floor(left / 3600), m = Math.floor(left / 60) % 60, s = left % 60;
|
|
105
|
+
time.textContent = (h ? h + ':' + pad(m) : pad(m)) + ':' + pad(s);
|
|
106
|
+
time.classList.remove('live'); chimed = false; // after time is added back
|
|
107
|
+
} else {
|
|
108
|
+
time.textContent = Overlaify.param('done', 'Going live any second…');
|
|
109
|
+
time.classList.add('live');
|
|
110
|
+
if (!chimed) { chimed = true; Overlaify.chime('fanfare'); }
|
|
111
|
+
}
|
|
112
|
+
setTimeout(tick, 250);
|
|
113
|
+
})();
|
|
114
|
+
|
|
115
|
+
Overlaify.action('add', min => {
|
|
116
|
+
const ms = (Number(min) || 0) * 60e3;
|
|
117
|
+
if (ms <= 0) return;
|
|
118
|
+
if (pausedLeft !== null) pausedLeft = Math.max(0, pausedLeft) + ms; else end = Math.max(end, Date.now()) + ms;
|
|
119
|
+
});
|
|
120
|
+
Overlaify.action('pause', () => { if (pausedLeft === null) pausedLeft = Math.max(0, end - Date.now()); });
|
|
121
|
+
Overlaify.action('resume', () => { if (pausedLeft !== null) { end = Date.now() + pausedLeft; pausedLeft = null; } });
|
|
122
|
+
Overlaify.action('restart', () => { end = startEnd(); pausedLeft = null; });
|
|
123
|
+
|
|
124
|
+
const MAX = Overlaify.param('max', 5);
|
|
125
|
+
const chat = document.getElementById('chat');
|
|
126
|
+
function push(html, hype, color) {
|
|
127
|
+
const el = document.createElement('div');
|
|
128
|
+
el.className = 'msg' + (hype ? ' hype' : '');
|
|
129
|
+
if (color) el.style.setProperty('--uc', color);
|
|
130
|
+
el.innerHTML = html;
|
|
131
|
+
chat.appendChild(el);
|
|
132
|
+
while (chat.querySelectorAll('.msg').length > MAX) chat.querySelector('.msg').remove();
|
|
133
|
+
}
|
|
134
|
+
Overlaify.on('chat', e => {
|
|
135
|
+
if (e.message.startsWith('!')) return;
|
|
136
|
+
push(`<b>${esc(e.name)}</b>${Overlaify.renderMessage(e.message, e.emotes)}`, false, e.color);
|
|
137
|
+
});
|
|
138
|
+
Overlaify.on('sub', e => push(`<b>${esc(e.name)}</b>${e.months > 1 ? `resubbed for ${e.months} months` : 'just subscribed'} 💜`, true));
|
|
139
|
+
Overlaify.on('gift', e => push(`<b>${esc(e.name)}</b>gifted a sub to ${esc(e.recipientName || e.recipient)}`, true));
|
|
140
|
+
Overlaify.on('giftbomb', e => push(`<b>${esc(e.name)}</b>gifted ${e.count} subs! 🎁`, true));
|
|
141
|
+
Overlaify.on('cheer', e => push(`<b>${esc(e.name)}</b>cheered ${e.bits.toLocaleString()} bits`, true));
|
|
142
|
+
Overlaify.on('raid', e => { push(`<b>${esc(e.name)}</b>is raiding with ${e.viewers.toLocaleString()} viewers!`, true); Overlaify.chime('alert'); });
|
|
143
|
+
</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|