// dash-viz.jsx — live visualization engine + premium canvas variants + shared dashboard controls const { useState: useV, useEffect: useVE, useRef: useVR } = React; /* ---------- accent color (oklch -> sRGB so canvas works everywhere) ---------- */ function oklchToRgb(L, C, hDeg) { const h = hDeg * Math.PI / 180, a = C * Math.cos(h), b = C * Math.sin(h); let l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3; let m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3; let s = (L - 0.0894841775 * a - 1.2914855480 * b) ** 3; const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; const bb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; const f = x => x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(Math.max(0, x), 1 / 2.4) - 0.055; const c = x => Math.max(0, Math.min(255, Math.round(f(x) * 255))); return [c(r), c(g), c(bb)]; } function accentRGB() { const h = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--accent-h")) || 140; return oklchToRgb(0.869, 0.275, h); } const rgba = (arr, a) => `rgba(${arr[0]},${arr[1]},${arr[2]},${a})`; /* ---------- rAF driver: animates in focused browser, draws once otherwise ---------- */ function useRAF(active, cb) { const cbRef = useVR(cb); cbRef.current = cb; useVE(() => { let raf, last = performance.now(), alive = true; const tick = (now) => { if (!alive) return; const dt = Math.min(0.05, (now - last) / 1000); last = now; cbRef.current(dt, now / 1000); if (active) raf = requestAnimationFrame(tick); }; cbRef.current(0, performance.now() / 1000); // immediate seed draw if (active) raf = requestAnimationFrame(tick); return () => { alive = false; cancelAnimationFrame(raf); }; }, [active]); } /* ---------- canvas sizing (dpr-aware) ---------- */ function useCanvas(height) { const wrap = useVR(null), cv = useVR(null); const size = useVR({ w: 600, h: height }); useVE(() => { const fit = () => { const el = wrap.current, c = cv.current; if (!el || !c) return; const dpr = Math.min(2, window.devicePixelRatio || 1); const w = el.clientWidth || 600, h = height; c.width = w * dpr; c.height = h * dpr; c.style.width = w + "px"; c.style.height = h + "px"; const ctx = c.getContext("2d"); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); size.current = { w, h }; }; fit(); const ro = new ResizeObserver(fit); if (wrap.current) ro.observe(wrap.current); return () => ro.disconnect(); }, [height]); return { wrap, cv, size }; } function smoothPath(ctx, pts) { ctx.moveTo(pts[0][0], pts[0][1]); for (let i = 0; i < pts.length - 1; i++) { const [x0, y0] = pts[i], [x1, y1] = pts[i + 1]; ctx.quadraticCurveTo(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); } ctx.lineTo(pts[pts.length - 1][0], pts[pts.length - 1][1]); } function drawArea(ctx, w, h, data, max, ac, opts = {}) { ctx.clearRect(0, 0, w, h); // gridlines ctx.strokeStyle = "rgba(255,255,255,0.05)"; ctx.lineWidth = 1; for (let g = 1; g < 4; g++) { const y = (h * g) / 4; ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(w, y + 0.5); ctx.stroke(); } const n = data.length, step = w / (n - 1), pad = h * 0.12; const pts = data.map((v, i) => [i * step, h - pad - (Math.max(0, v) / max) * (h - pad * 1.4)]); // fill ctx.beginPath(); smoothPath(ctx, pts); ctx.lineTo(w, h); ctx.lineTo(0, h); ctx.closePath(); const grad = ctx.createLinearGradient(0, 0, 0, h); grad.addColorStop(0, rgba(ac, opts.fill ?? 0.34)); grad.addColorStop(1, rgba(ac, 0)); ctx.fillStyle = grad; ctx.fill(); // line ctx.beginPath(); smoothPath(ctx, pts); ctx.strokeStyle = rgba(ac, 0.95); ctx.lineWidth = 2.4; ctx.lineJoin = "round"; ctx.lineCap = "round"; ctx.shadowColor = rgba(ac, 0.55); ctx.shadowBlur = 14; ctx.stroke(); ctx.shadowBlur = 0; // leading dot const [lx, ly] = pts[pts.length - 1]; ctx.beginPath(); ctx.arc(lx, ly, 4.2, 0, Math.PI * 2); ctx.fillStyle = "#fff"; ctx.fill(); ctx.beginPath(); ctx.arc(lx, ly, 9, 0, Math.PI * 2); ctx.fillStyle = rgba(ac, 0.22); ctx.fill(); } /* ================= VIEWER VIZ — concurrent viewers area chart ================= */ function ViewerViz({ running, target, onValue, height = 248 }) { const { wrap, cv, size } = useCanvas(height); const m = useVR(null), rep = useVR(0); if (!m.current) { const N = 72, data = []; for (let i = 0; i < N; i++) { const p = i / (N - 1); const ramp = 1 - Math.pow(1 - Math.min(1, p * 1.7), 2.3); data.push(target * ramp * (0.92 + 0.05 * Math.sin(i * 0.8))); } m.current = { data, cur: data[N - 1] }; } useRAF(running, (dt, t) => { const mm = m.current, ctx = cv.current && cv.current.getContext("2d"); if (!ctx) return; const goal = running ? target : Math.min(mm.cur, target); if (dt > 0) { mm.cur += (goal - mm.cur) * Math.min(1, dt * 0.9); const noise = running && mm.cur > target * 0.35 ? (Math.sin(t * 1.6) + 0.5 * Math.sin(t * 0.7)) * target * 0.012 + (Math.random() - 0.5) * target * 0.012 : 0; const shown = Math.max(0, mm.cur + noise); mm.data.push(shown); if (mm.data.length > 72) mm.data.shift(); if (now() - rep.current > 110) { rep.current = now(); onValue && onValue(Math.round(shown)); } } drawArea(ctx, size.current.w, size.current.h, m.current.data, target * 1.18, accentRGB()); }); useVE(() => { onValue && onValue(Math.round(m.current.cur)); }, []); return (
); } function now() { return performance.now(); } /* ===== CONNECTION VIZ — stacked Connecting / Connected / Errors / Dead ===== */ const CONN_COLORS = { connected: () => accentRGB(), connecting: () => oklchToRgb(0.80, 0.135, 215), // theme cyan-blue errors: () => oklchToRgb(0.83, 0.155, 82), // warm amber dead: () => oklchToRgb(0.66, 0.20, 22), // deep red }; /* clean: Connected as a filled area (hero), other states as thin lines above */ function drawConnections(ctx, w, h, s, target) { ctx.clearRect(0, 0, w, h); ctx.strokeStyle = "rgba(255,255,255,0.05)"; ctx.lineWidth = 1; for (let g = 1; g < 4; g++) { const y = (h * g) / 4; ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(w, y + 0.5); ctx.stroke(); } const n = s.connected.length, step = w / (n - 1), ac = accentRGB(); const smooth = (pts) => { ctx.moveTo(pts[0][0], pts[0][1]); for (let i = 0; i < pts.length - 1; i++) { const [x0, y0] = pts[i], [x1, y1] = pts[i + 1]; ctx.quadraticCurveTo(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); } ctx.lineTo(pts[n - 1][0], pts[n - 1][1]); }; // ---- single shared, auto-scaling Y axis ---- // Every series is plotted against ONE max (the highest value across ALL lines), so a // line's on-screen height always reflects its real number: the bigger number sits higher, // and the whole chart rescales to whichever line is currently tallest. const padB = h * 0.08, padT = h * 0.12; let maxV = 1; // errors/dead excluded from the scale AND the drawn lines (customer view): they carry cumulative // lifetime counters that would dwarf the live connected/connecting values and flatten the real line. ["connected", "connecting"].forEach(k => { for (const v of s[k]) if (v > maxV) maxV = v; }); maxV *= 1.15; // headroom so the tallest line doesn't clip the ceiling const Ym = v => h - padB - (Math.max(0, v) / maxV) * (h - padB - padT); const pm = s.connected.map((v, i) => [i * step, Ym(v)]); ctx.beginPath(); ctx.moveTo(0, h); ctx.lineTo(pm[0][0], pm[0][1]); for (let i = 0; i < pm.length - 1; i++) { const [x0, y0] = pm[i], [x1, y1] = pm[i + 1]; ctx.quadraticCurveTo(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); } ctx.lineTo(pm[n - 1][0], pm[n - 1][1]); ctx.lineTo(w, h); ctx.closePath(); const g1 = ctx.createLinearGradient(0, 0, 0, h); g1.addColorStop(0, rgba(ac, 0.42)); g1.addColorStop(1, rgba(ac, 0)); ctx.fillStyle = g1; ctx.fill(); ctx.beginPath(); smooth(pm); ctx.strokeStyle = rgba(ac, 0.98); ctx.lineWidth = 2.6; ctx.lineJoin = "round"; ctx.lineCap = "round"; ctx.shadowColor = rgba(ac, 0.55); ctx.shadowBlur = 14; ctx.stroke(); ctx.shadowBlur = 0; const [lx, ly] = pm[n - 1]; ctx.beginPath(); ctx.arc(lx, ly, 4, 0, Math.PI * 2); ctx.fillStyle = "#fff"; ctx.fill(); ctx.beginPath(); ctx.arc(lx, ly, 9, 0, Math.PI * 2); ctx.fillStyle = rgba(ac, 0.22); ctx.fill(); // ---- sub-state lines on the SAME axis (so heights are comparable to the hero line) ---- const subs = [["connecting", CONN_COLORS.connecting()]]; subs.forEach(([k, rgb]) => { const ps = s[k].map((v, i) => [i * step, Ym(v)]); ctx.beginPath(); smooth(ps); ctx.strokeStyle = rgba(rgb, 0.95); ctx.lineWidth = 2; ctx.lineJoin = "round"; ctx.lineCap = "round"; ctx.shadowColor = rgba(rgb, 0.55); ctx.shadowBlur = 9; ctx.stroke(); ctx.shadowBlur = 0; const [dx, dy] = ps[n - 1]; ctx.beginPath(); ctx.arc(dx, dy, 3, 0, Math.PI * 2); ctx.fillStyle = rgba(rgb, 1); ctx.fill(); }); } // Driven by the REAL polled stats ({connecting, connected, errors, dead}) — each line // smoothly animates toward the live value (0 when idle), so the graph ramps up together // with the stat tiles instead of jumping to a synthetic number. `target` only feeds the // (now auto-scaling) draw routine. function ConnectionViz({ running, stats, target, height = 248 }) { const { wrap, cv, size } = useCanvas(height); const m = useVR(null); if (!m.current) { const N = 72, zeros = () => Array.from({ length: N }, () => 0); m.current = { s: { connected: zeros(), connecting: zeros(), errors: zeros(), dead: zeros() }, cur: { connected: 0, connecting: 0, errors: 0, dead: 0 } }; } useRAF(running, (dt) => { const mm = m.current, ctx = cv.current && cv.current.getContext("2d"); if (!ctx) return; const tgt = (running && stats) ? stats : { connected: 0, connecting: 0, errors: 0, dead: 0 }; if (dt > 0) { const c = mm.cur, k = Math.min(1, dt * 0.8); // smooth approach toward the real value ["connected", "connecting", "errors", "dead"].forEach((key) => { c[key] += ((Number(tgt[key]) || 0) - c[key]) * k; mm.s[key].push(Math.max(0, c[key])); if (mm.s[key].length > 72) mm.s[key].shift(); }); } drawConnections(ctx, size.current.w, size.current.h, mm.s, target); }); return (
); } /* ================= FOLLOW VIZ — cumulative followers climb ================= */ function FollowViz({ running, rate = 42, total0 = 12840, onTotal, height = 248 }) { const { wrap, cv, size } = useCanvas(height); const m = useVR(null), rep = useVR(0); if (!m.current) { const N = 64, data = []; let tot = total0 - rate * 18; for (let i = 0; i < N; i++) { tot += (rate / 60) * (18 / N) * 60 * (0.7 + 0.6 * Math.random()); data.push(tot); } m.current = { data, total: data[N - 1] }; } useRAF(running, (dt, t) => { const mm = m.current, ctx = cv.current && cv.current.getContext("2d"); if (!ctx) return; if (dt > 0 && running) { mm.total += (rate / 60) * dt * 60 * (0.6 + 0.8 * Math.random()); mm.data.push(mm.total); if (mm.data.length > 64) mm.data.shift(); if (now() - rep.current > 140) { rep.current = now(); onTotal && onTotal(Math.round(mm.total)); } } const min = m.current.data[0], max = m.current.data[m.current.data.length - 1]; const span = Math.max(1, max - min); const norm = m.current.data.map(v => v - min + span * 0.18); drawArea(ctx, size.current.w, size.current.h, norm, span * 1.3, accentRGB(), { fill: 0.3 }); }); useVE(() => { onTotal && onTotal(Math.round(m.current.total)); }, []); return (
); } /* ================= CHAT VIZ — live feed + msgs/min bars ================= */ const CHAT_USERS = ["xxNova", "kib_ttv", "ggwalrus", "p1xel", "mochi", "snypez", "deadlift", "vortex_", "lunaa", "froggy", "z3ro", "kashmir", "ttv_drips", "echoo", "binx"]; const CHAT_MSGS = ["W stream 🔥", "lets gooo", "first time here, ggs", "this dude cracked", "drop the settings", "KEKW", "+rep", "clip it!", "how many viewers rn", "insane gameplay", "poggers", "follow train?", "best on kick fr", "GG", "lets get 5k", "🔥🔥🔥", "actual cracked aim", "no way"]; function ChatViz({ running, rate = 180, height = 248 }) { const { wrap, cv, size } = useCanvas(96); const feedRef = useVR(null), m = useVR(null), acc = useVR(0), rep = useVR(0); const [feed, setFeed] = useV(() => Array.from({ length: 9 }).map((_, i) => ({ id: i, u: CHAT_USERS[i % CHAT_USERS.length], t: CHAT_MSGS[i % CHAT_MSGS.length] }))); if (!m.current) { m.current = { bars: Array.from({ length: 40 }, (_, i) => 0.3 + 0.5 * Math.abs(Math.sin(i * 0.6)) ), id: 1000 }; } useRAF(running, (dt, t) => { const mm = m.current, ctx = cv.current && cv.current.getContext("2d"); if (dt > 0 && running) { acc.current += dt; const interval = 60 / rate; if (acc.current >= interval) { acc.current = 0; const msg = { id: mm.id++, u: CHAT_USERS[(Math.random() * CHAT_USERS.length) | 0], t: CHAT_MSGS[(Math.random() * CHAT_MSGS.length) | 0] }; setFeed(f => [...f.slice(-13), msg]); mm.bars.push(0.6 + Math.random() * 0.4); if (mm.bars.length > 40) mm.bars.shift(); } else { mm.bars[mm.bars.length - 1] = Math.max(mm.bars[mm.bars.length - 1], 0.2); } } if (ctx) { const { w, h } = size.current, ac = accentRGB(); ctx.clearRect(0, 0, w, h); const mid = h / 2; // center baseline ctx.strokeStyle = rgba(ac, 0.16); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, mid + 0.5); ctx.lineTo(w, mid + 0.5); ctx.stroke(); const n = mm.bars.length, gap = 2.5, bw = w / n; mm.bars.forEach((v, i) => { const recency = i / (n - 1); // 0 = oldest (left), 1 = newest (right) const on = running || i < n - 2; const half = Math.max(2, v * (h * 0.46)); const x = i * bw + gap / 2, bwi = bw - gap, r = Math.min(2.5, bwi / 2); const a = on ? (0.4 + 0.5 * v) : 0.14; const g = ctx.createLinearGradient(0, mid - half, 0, mid + half); g.addColorStop(0, rgba(ac, a * 0.7)); g.addColorStop(0.5, rgba(ac, Math.min(1, a + 0.3))); g.addColorStop(1, rgba(ac, a * 0.7)); ctx.fillStyle = g; ctx.shadowColor = on ? rgba(ac, 0.6 * recency) : "transparent"; ctx.shadowBlur = on ? 10 * recency : 0; ctx.beginPath(); ctx.roundRect(x, mid - half, bwi, half * 2, r); ctx.fill(); }); ctx.shadowBlur = 0; // soft leading glow dot on newest bar const lastV = mm.bars[mm.bars.length - 1]; if (running) { const lx = w - bw / 2; ctx.beginPath(); ctx.arc(lx, mid - lastV * h * 0.46, 3, 0, Math.PI * 2); ctx.fillStyle = "#fff"; ctx.fill(); } } }); useVE(() => { const el = feedRef.current; if (el) el.scrollTop = el.scrollHeight; }, [feed]); return (
{feed.map(msg => (
{msg.u} {msg.t}
))}
); } /* ================= shared dashboard controls ================= */ function Toggle({ on, onChange }) { return )} ); } // Customers paste the whole profile URL into channel fields. "https://kick.com/name" is not a // username, so the job runs against nothing, delivers zero, and they still get charged. Pass // `channel` to a DField and it strips the link as they type AND tells them what we actually need - // silently rewriting their input would fix the order but teach them nothing. function normalizeKickChannel(raw) { const original = String(raw == null ? "" : raw); let v = original.trim(); const looksLikeUrl = /^(https?:)?\/\//i.test(v) || /(^|\.)kick\.com/i.test(v) || v.indexOf("/") !== -1; if (looksLikeUrl) { v = v.replace(/^(https?:)?\/\//i, ""); v = v.replace(/^(www\.)?kick\.com\/?/i, ""); v = v.split(/[/?#]/)[0]; } v = v.replace(/^@+/, "").trim(); return { value: v, wasUrl: looksLikeUrl && v !== original.trim() }; } const KVB_CHANNEL_HINT = { marginTop: 8, padding: "9px 12px", borderRadius: 9, background: "rgba(255, 190, 60, 0.10)", border: "1px solid rgba(255, 190, 60, 0.34)", color: "var(--ink-2)", fontSize: 12.5, lineHeight: 1.5, }; function DField({ icon, value, onChange, placeholder, mono, channel }) { const [urlHint, setUrlHint] = useV(false); const handleChange = function (raw) { if (!channel) { onChange && onChange(raw); return; } const r = normalizeKickChannel(raw); if (r.wasUrl) { setUrlHint(true); } else if (!r.value) { setUrlHint(false); } onChange && onChange(r.value); }; if (channel) { return (
{icon && } handleChange(e.target.value)} />
{urlHint && (
We only need the username. We removed the kick.com link for you — for kick.com/yourname just enter yourname.
)}
); } return (
{icon && } onChange && onChange(e.target.value)} />
); } function PanelHead({ eyebrow, title, right }) { return (
{eyebrow &&
{eyebrow}
}

{title}

{right}
); } Object.assign(window, { ViewerViz, ConnectionViz, FollowViz, ChatViz, Toggle, Seg, DField, PanelHead, accentRGB, normalizeKickChannel });