Files
hyperframes/registry/components/hw-callout-circle/hw-callout-circle.html
T
5b45bcc16d feat(registry): hw write-on wave — hw-write-title block + control surfaces for the handwritten family (#3557)
* feat(registry): hw write-on wave — hw-write-title block + control surfaces for four hw components

Adds hw-write-title (true glyph write-on: pen-traced Caveat via baked
centerline masks, curvature-adaptive pen velocity, pen lifts, underline)
and grows the handwritten family's four components with declared control
surfaces (controls per the #3227 convention), the completed stroke-texture
matrix (sharp + deterministic seeded spray — no feTurbulence), boil poses,
and physically-derived arrival deformation (travel-aligned squash with
volume preserved and spring recovery). Shipped single-path callers keep
working unchanged (legacy helper bodies preserved; proven in a legacy
wiring harness).

Validated in one reference build: check clean, double-render framemd5
910/910 bit-identical, seek-shuffle 8/8, WCAG 2.3.1 flash-scan zero
violations, physics burn-ins hand-recomputed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(catalog): index hw-write-title for meaning search

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-08-30 23:46:39 -04:00

624 lines
26 KiB
HTML
Vendored
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
@font-face {
font-family: "Caveat";
src: url("assets/fonts/Caveat-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: transparent;
overflow: hidden;
}
/* ---- hw-callout-circle ----------------------------------------------
The full callout anatomy: wobbled ellipse outline draws around a
target, optional scribble fill sketches in, a curved connector leads
to a handwritten label that POPS in with momentum — the drawn shapes
absorb the arrival with a volume-preserving contact squash (g21).
Ships the full family runtime it needs (hw-boil + boil poses +
stroke matrix + springEase) — copy the helpers once per host.
WRAPPER RULES: boil owns x/y/rotation of .hw-co-boil; the squash
deforms .hw-co-deform (outline + scribble only); the label rides
the uniform-scale .hw-co-pop OUTSIDE the deform — non-uniform
scale never reaches glyphs. One timeline has ONE onUpdate
(everything registers through hwOnUpdate). */
.hw-callout {
position: absolute;
pointer-events: none;
}
.hw-callout .hw-co-boil,
.hw-callout .hw-co-deform,
.hw-callout .hw-co-conn-layer,
.hw-callout .hw-co-pop {
position: absolute;
inset: 0;
}
.hw-callout .hw-co-pop {
opacity: 0;
}
.hw-callout svg {
width: 100%;
height: 100%;
overflow: visible;
}
.hw-callout path {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.hw-callout .hw-co-outline {
stroke: var(--hw-co-ink, var(--hw-ink, #f4f2ec));
stroke-width: var(--hw-co-w, 6);
}
.hw-callout .hw-co-scribble {
/* Warm marker accent to match hw-frame / hw-title / hw-underline; the
previous blue read as a default UI colour. Lighter weight and alpha
so the hatch sits behind the labelled subject instead of over it. */
stroke: var(--hw-co-accent, var(--hw-accent, #ffb020));
stroke-width: 3;
opacity: 0.42;
}
.hw-callout .hw-co-connector {
stroke: var(--hw-co-ink, var(--hw-ink, #f4f2ec));
stroke-width: 4;
}
.hw-callout .hw-co-label {
position: absolute;
font-family: var(--hw-font-print, "Caveat", cursive);
font-weight: 700;
font-size: 54px;
color: var(--hw-co-ink, var(--hw-ink, #f4f2ec));
white-space: nowrap;
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.45);
}
/* self-preview scene */
#hw-co-scene {
position: absolute;
inset: 0;
background: linear-gradient(150deg, #35604a 0%, #14231c 100%);
}
#hw-co-target {
position: absolute;
left: 700px;
top: 420px;
width: 220px;
height: 150px;
border-radius: 18px;
background: rgba(244, 242, 236, 0.18);
display: flex;
align-items: center;
justify-content: center;
font-family: "Inter", ui-sans-serif, sans-serif;
font-weight: 600;
font-size: 30px;
color: rgba(244, 242, 236, 0.8);
}
</style>
</head>
<body>
<!-- Self-preview. WIRING: copy the .hw-callout CSS + markup pattern (the
boil / deform / conn-layer / pop wrapper stack is REQUIRED — boil
owns x/y/rotation, the squash owns the deform, the label pops on
.hw-co-pop) and ALL helpers below. Size the wrapper around your
target, then:
hwCalloutBuild("#hw-my-callout", { scribble: true, seed: 4,
label: "look here!", labelAt: "right",
strokeType: "plain", boil: "calm" });
hwCalloutOn(tl, "#hw-my-callout", 2.0);
hwCalloutOff(tl, "#hw-my-callout", 8.0);
hwCalloutOn applies the boil pose internally — do NOT add a host
hwBoil call on the callout (it would double-boil).
-->
<div
id="hw-co-root"
data-composition-id="hw-callout-circle"
data-start="0"
data-duration="4"
data-width="1920"
data-height="1080"
>
<div id="hw-co-scene"></div>
<div id="hw-co-target">subject</div>
<div
class="hw-callout"
id="hw-co-demo"
style="left: 640px; top: 370px; width: 340px; height: 250px"
>
<div class="hw-co-boil">
<div class="hw-co-deform">
<svg viewBox="0 0 340 250">
<path class="hw-co-scribble" id="hw-co-demo-scribble"></path>
<path class="hw-co-outline" id="hw-co-demo-outline"></path>
</svg>
</div>
<svg class="hw-co-conn-layer" viewBox="0 0 340 250">
<path class="hw-co-connector" id="hw-co-demo-connector"></path>
</svg>
<div class="hw-co-pop">
<!-- labels intentionally hang outside the wrapper box -->
<div class="hw-co-label" id="hw-co-demo-label" data-layout-allow-overflow></div>
</div>
</div>
</div>
<div
id="hw-co-drv"
class="clip"
data-start="0"
data-duration="4"
data-track-index="0"
style="position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none"
></div>
</div>
<script>
(function () {
window.__timelines = window.__timelines || {};
var CONFIG = {
// ── Content (freely editable — not counted) ─────────────────────
label: "look here!",
scribble: true, // which anatomy pieces exist (content, not a feel lever)
connector: true, // (connector: false also suppresses the label anchor)
// ── Controls (family base + 2 item-specific; comment grammar is the published-parameter list) ──
labelAt: "right", // variant: "left" | "right" | "below" — connector start point, sweep direction + label anchor pose (6 geometry params)
strokeType: "plain", // variant: "plain" | "soft" | "sharp" | "spray" — outline + connector stroke texture (authored constants per type); the scribble fill stays plain — texture-on-texture reads as mud
boil: "calm", // variant: "off" | "calm" | "lively" — authored amp/rot pairs over the family-locked frameDrop 3; seed locked
// ── Locked (edit the item source to change) ─────────────────────
// draw durations/eases, pop ease + contact-squash constants
// (softness 0.08 light-drawn-line tier, snappy recovery),
// stroke-type constants, boil pose tables, seeds
};
var SEED = 4; // locked — determinism (hosts pass a distinct seed per callout instance)
/* ---- hw-boil helpers (shared family runtime — copy once per host) ---- */
window.hwOnUpdate = function (tl, fn) {
if (!tl.__hwRenders) {
tl.__hwRenders = [];
tl.eventCallback("onUpdate", function () {
for (var i = 0; i < tl.__hwRenders.length; i++) tl.__hwRenders[i]();
});
}
tl.__hwRenders.push(fn);
fn();
};
window.hwHash = function (n, seed) {
var x = Math.sin(n * 127.1 + (seed || 1) * 311.7) * 43758.5453;
return (x - Math.floor(x)) * 2 - 1;
};
window.hwBoil = function (tl, target, opts) {
opts = opts || {};
var amp = opts.amp !== undefined ? opts.amp : 1.6;
var rot = opts.rot !== undefined ? opts.rot : 0.5;
var fps = opts.fps || 30;
var drop = opts.frameDrop || 3;
var seed = opts.seed || 1;
var els = gsap.utils.toArray(target);
window.hwOnUpdate(tl, function () {
var step = Math.floor((tl.time() * fps) / drop);
for (var i = 0; i < els.length; i++) {
gsap.set(els[i], {
x: window.hwHash(step * 3 + i * 97, seed) * amp,
y: window.hwHash(step * 3 + 1 + i * 97, seed) * amp,
rotation: (opts.baseRot || 0) + window.hwHash(step * 3 + 2 + i * 97, seed) * rot,
});
}
});
};
// boil control: authored poses over the family-locked frameDrop.
// Unknown pose → default + console error (bounded-controls law).
window.hwBoilPose = function (tl, target, pose, opts) {
opts = opts || {};
var poses = { calm: { amp: 1.6, rot: 0.5 }, lively: { amp: 2.6, rot: 0.9 } };
if (pose === "off") return;
var p = poses[pose];
if (!p) {
console.error('hw: unknown boil pose "' + pose + '" — falling back to "calm"');
p = poses.calm;
}
window.hwBoil(tl, target, { amp: p.amp, rot: p.rot, frameDrop: 3, seed: opts.seed || 1 });
};
/* ---- spring library (spring-lab, validated 2026-08-04 — verbatim) ---- */
var LN1000 = Math.log(1000);
var FEELS = {
snappy: { zeta: 0.9, response: 0.22 },
"heavy-settle": { zeta: 1.0, response: 0.8 },
bouncy: { zeta: 0.5, response: 0.4 },
wobbly: { zeta: 0.28, response: 0.5 },
};
window.springEase = function (opts) {
var f = opts.feel ? FEELS[opts.feel] : opts;
var zeta = f.zeta,
response = f.response;
var w0 = (2 * Math.PI) / response;
var T = LN1000 / (zeta * w0);
var x;
if (zeta < 1) {
var wd = w0 * Math.sqrt(1 - zeta * zeta);
x = function (t) {
return (
1 -
Math.exp(-zeta * w0 * t) *
(Math.cos(wd * t) + ((zeta * w0) / wd) * Math.sin(wd * t))
);
};
} else {
x = function (t) {
return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
};
}
var xT = x(T);
return {
ease: function (p) {
return p >= 1 ? 1 : x(p * T) / xT;
},
duration: T,
zeta: zeta,
response: response,
};
};
window.hwWobbleEllipse = function (cx, cy, rx, ry, seed, wobblePct) {
wobblePct = wobblePct === undefined ? 3 : wobblePct;
var N = 14,
pts = [];
for (var i = 0; i < N; i++) {
var a = (i / N) * Math.PI * 2;
var wr = 1 + window.hwHash(i * 13 + 5, seed) * (wobblePct / 100);
pts.push([cx + Math.cos(a) * rx * wr, cy + Math.sin(a) * ry * wr]);
}
var mx = (pts[0][0] + pts[N - 1][0]) / 2,
my = (pts[0][1] + pts[N - 1][1]) / 2;
var d = "M" + mx.toFixed(1) + " " + my.toFixed(1);
for (var j = 0; j < N; j++) {
var p = pts[j],
q = pts[(j + 1) % N];
d +=
" Q" +
p[0].toFixed(1) +
" " +
p[1].toFixed(1) +
" " +
((p[0] + q[0]) / 2).toFixed(1) +
" " +
((p[1] + q[1]) / 2).toFixed(1);
}
return d;
};
/* ---- stroke matrix (family runtime): plain | soft | sharp | spray ----
strokeType is a g09 variant control. Authored constants per type;
unknown value → default ("plain") + console error (bounded-controls
law). sharp = micro stroke-dasharray gaps (dry marker) — draw-on
must therefore ride a MASK clone (the visible dasharray never
animates). spray = index-seeded deterministic dots along the path
via hwHash: position a pure function of path parameter + seed; NO
feTurbulence anywhere.
Returns { draw: pathToDashAnimate, group: maskedGroupOrNull } */
window.hwStrokeTypes = {
plain: {},
soft: { blur: 0.6 },
sharp: { dash: [3.0, 1.55] }, // × stroke-width
spray: {
coreOpacity: 0.5,
coreBlur: 0.4,
density: 0.16,
scatter: 2.6,
rMin: 1.1,
rMax: 2.6,
},
};
window.hwStrokeApply = function (pathEl, type, opts) {
opts = opts || {};
var cfg = window.hwStrokeTypes[type];
if (!cfg) {
console.error('hw: unknown strokeType "' + type + '" — falling back to "plain"');
type = "plain";
cfg = window.hwStrokeTypes.plain;
}
var svg = pathEl.ownerSVGElement;
var ns = "http://www.w3.org/2000/svg";
var w = parseFloat(getComputedStyle(pathEl).strokeWidth) || 6;
if (type === "plain") return { draw: pathEl, group: null, type: type };
if (type === "soft") {
pathEl.style.filter = "blur(" + cfg.blur + "px)";
return { draw: pathEl, group: null, type: type };
}
// sharp + spray: wrap in a masked group, draw-on animates the mask clone
var id =
opts.id ||
"hwsm-" + Math.abs(Math.round(window.hwHash((opts.seed || 1) * 13.7, 5) * 1e6));
var defs = svg.querySelector("defs");
if (!defs) {
defs = document.createElementNS(ns, "defs");
svg.insertBefore(defs, svg.firstChild);
}
var mask = document.createElementNS(ns, "mask");
mask.setAttribute("id", id);
mask.setAttribute("maskUnits", "userSpaceOnUse");
var clone = document.createElementNS(ns, "path");
clone.setAttribute("d", pathEl.getAttribute("d"));
clone.setAttribute("fill", "none");
clone.setAttribute("stroke", "#fff");
clone.setAttribute(
"stroke-width",
w * (type === "spray" ? 2 + (cfg.scatter * 2) / w : 1.7),
);
clone.setAttribute("stroke-linecap", "round");
clone.setAttribute("stroke-linejoin", "round");
mask.appendChild(clone);
defs.appendChild(mask);
var group = document.createElementNS(ns, "g");
group.setAttribute("mask", "url(#" + id + ")");
pathEl.parentNode.insertBefore(group, pathEl);
group.appendChild(pathEl);
if (type === "sharp") {
pathEl.setAttribute(
"stroke-dasharray",
(cfg.dash[0] * w).toFixed(1) + " " + (cfg.dash[1] * w).toFixed(1),
);
} else {
// spray: faint core + seeded dots
pathEl.setAttribute("stroke-opacity", cfg.coreOpacity);
pathEl.style.filter = "blur(" + cfg.coreBlur + "px)";
var len = pathEl.getTotalLength();
var n = Math.round(len * cfg.density * (opts.densityScale || 1));
var seed = opts.seed || 1;
var dots = document.createElementNS(ns, "g");
for (var i = 0; i < n; i++) {
var t = Math.min(
1,
Math.max(0, (i + 0.5) / n + (window.hwHash(i * 7, seed) * 0.35) / n),
);
var pt = pathEl.getPointAtLength(t * len);
var pt2 = pathEl.getPointAtLength(Math.min(len, t * len + 0.5));
var tx = pt2.x - pt.x,
ty = pt2.y - pt.y;
var tl2 = Math.hypot(tx, ty) || 1;
var nx = -ty / tl2,
nyv = tx / tl2;
var off = window.hwHash(i * 7 + 1, seed) * (cfg.scatter + w * 0.55);
var r = cfg.rMin + Math.abs(window.hwHash(i * 7 + 2, seed)) * (cfg.rMax - cfg.rMin);
var c = document.createElementNS(ns, "circle");
c.setAttribute("cx", (pt.x + nx * off).toFixed(1));
c.setAttribute("cy", (pt.y + nyv * off).toFixed(1));
c.setAttribute("r", r.toFixed(2));
c.setAttribute("fill", getComputedStyle(pathEl).stroke);
dots.appendChild(c);
}
group.appendChild(dots);
}
return { draw: clone, group: group, type: type };
};
/* Draw-on across the stroke matrix: dash-animate whatever hwStrokeApply
says. (The pen-velocity ease variant rides the write-on item, not
this one.) */
window.hwDrawOn = function (tl, applied, at, dur, opts) {
opts = opts || {};
var p = applied.draw;
var len = p.getTotalLength();
// the draw target is always solid (plain/soft: the path itself; sharp/spray:
// the mask clone — the visible path keeps its texture dasharray untouched)
p.setAttribute("stroke-dasharray", len + " " + len);
p.setAttribute("stroke-dashoffset", len);
gsap.set(p, { opacity: 0 }); // kill the round-cap start nub pre-draw
var ease = opts.ease || "power2.inOut";
tl.set(p, { opacity: 1 }, at);
tl.to(
p,
{ strokeDashoffset: 0, duration: dur === undefined ? 0.7 : dur, ease: ease },
at,
);
};
// ---- hw-callout helpers (copy these too) ----
// Builds outline + scribble + connector + label inside the wrapper and
// stores the draw records + control poses hwCalloutOn consumes.
// opts — content (uncounted): scribble (bool), seed, label, connector (bool)
// controls (g09 — see the CONFIG block, the published-parameter
// list): labelAt, strokeType, boil
window.hwCalloutBuild = function (target, opts) {
opts = opts || {};
var seed = opts.seed || 1;
var el = document.querySelector(target);
var svg = el.querySelector("svg");
var vb = svg.getAttribute("viewBox").split(" ");
var W = parseFloat(vb[2]),
H = parseFloat(vb[3]);
var cx = W / 2,
cy = H / 2,
rx = W * 0.42,
ry = H * 0.36;
var outline = el.querySelector(".hw-co-outline");
outline.setAttribute("d", window.hwWobbleEllipse(cx, cy, rx, ry, seed, 4));
var scr = el.querySelector(".hw-co-scribble");
if (opts.scribble === false) {
scr.style.display = "none";
} else {
/* Denser rows so the zigzag reads as a scribbled hatch. At 6 rows
the near-horizontal runs dominate and it renders as stacked bars. */
var d = "",
rows = 11;
for (var i = 0; i < rows; i++) {
var t = (i + 0.5) / rows;
var yy = cy - ry + t * ry * 2;
var half = Math.sqrt(Math.max(0.05, 1 - Math.pow((yy - cy) / ry, 2)));
var xw = rx * half * 0.86;
var xa = cx - xw + window.hwHash(i * 7 + 2, seed) * 8;
var xb = cx + xw + window.hwHash(i * 7 + 3, seed) * 8;
d += (i === 0 ? "M " : " L ") + (i % 2 ? xa : xb).toFixed(1) + " " + yy.toFixed(1);
d +=
" L " +
(i % 2 ? xb : xa).toFixed(1) +
" " +
(yy + window.hwHash(i * 7 + 4, seed) * 4).toFixed(1);
}
scr.setAttribute("d", d);
}
var conn = el.querySelector(".hw-co-connector");
var label = el.querySelector(".hw-co-label");
// labelAt is a declared variant control — bounded: unknown value
// falls back to the default WITH a console error. Validated BEFORE
// the connector branch so a malformed value always logs, even when
// connector: false leaves the control inert.
var at = opts.labelAt || "right";
if (at !== "left" && at !== "right" && at !== "below") {
console.error('hw: unknown labelAt "' + at + '" — falling back to "right"');
at = "right";
}
var ex = null,
ey = null;
if (opts.connector === false) {
conn.style.display = "none";
} else {
var sx = at === "left" ? cx - rx : at === "below" ? cx : cx + rx;
var sy = at === "below" ? cy + ry : cy + ry * 0.35;
ex = at === "left" ? sx - 70 : at === "below" ? sx + 40 : sx + 70;
ey = sy + 55;
conn.setAttribute(
"d",
"M " +
sx.toFixed(1) +
" " +
sy.toFixed(1) +
" Q " +
((sx + ex) / 2 + window.hwHash(21, seed) * 14).toFixed(1) +
" " +
(sy + 46).toFixed(1) +
" " +
ex.toFixed(1) +
" " +
ey.toFixed(1),
);
label.style.left = (at === "left" ? ex - 180 : ex + 12) + "px";
label.style.top = ey - 24 + "px";
}
label.textContent = opts.label || "";
// strokeType control: OUTLINE + CONNECTOR take the texture (the
// scribble stays plain). Bounding lives inside hwStrokeApply.
var strokeType = opts.strokeType || "plain";
var idBase = (el.id || "hw-co") + "-sm";
el.__hwCo = {
rx: rx,
seed: seed,
boil: opts.boil || "calm",
// the label pops out of the connector tip (falls back to center
// when the connector anatomy is off)
popOrigin: ex === null ? "50% 50%" : ex.toFixed(1) + "px " + ey.toFixed(1) + "px",
draw: {
outline: window.hwStrokeApply(outline, strokeType, {
seed: seed,
id: idBase + "-outline",
}),
scribble:
opts.scribble === false ? null : window.hwStrokeApply(scr, "plain", { seed: seed }),
connector:
opts.connector === false
? null
: window.hwStrokeApply(conn, strokeType, {
seed: seed,
id: idBase + "-connector",
}),
},
};
};
// Draw sequence: outline -> scribble -> connector -> label pop, with
// the g21 contact squash on the shapes group at pop arrival. The boil
// pose registers here too — the host wires a position only.
window.hwCalloutOn = function (tl, target, at) {
var el = document.querySelector(target);
var co = el.__hwCo;
var t = at;
["outline", "scribble", "connector"].forEach(function (k) {
var rec = co.draw[k];
if (!rec) return;
var dur = k === "scribble" ? 0.55 : k === "connector" ? 0.35 : 0.7;
window.hwDrawOn(tl, rec, t, dur);
t += dur * 0.85;
});
// label pop: the pop ARRIVES WITH MOMENTUM (accelerating into
// contact) — the squash absorbs it; a settling back.out crosses
// scale 1.0 at ~zero speed and the computed squash is invisible
// (measured, not hand-picked)
var pop = el.querySelector(".hw-co-pop");
var popEase = gsap.parseEase("power2.in");
gsap.set(pop, { scale: 0.001, opacity: 0, transformOrigin: co.popOrigin });
tl.to(pop, { opacity: 1, duration: 0.08, ease: "none" }, t);
tl.to(pop, { scale: 1, duration: 0.3, ease: popEase, immediateRender: false }, t);
// arrival velocity in px of radial (rx) travel, measured at the
// FIRST crossing of 1.0 — a settling ease's end-slope is ~0 and is
// NOT the arrival velocity
var eps = 1e-4;
var pc = 1;
for (var k2 = 1; k2 <= 1000; k2++) {
if (popEase(k2 / 1000) >= 1) {
pc = k2 / 1000;
break;
}
}
var vPop =
((popEase(Math.min(pc + eps, 1)) - popEase(pc - eps)) / (2 * eps)) * (co.rx / 0.3);
var S = Math.min(0.08 * (Math.abs(vPop) / 2000), 0.25); // 0.08 = light drawn line tier
var rec2 = window.springEase({ feel: "snappy" });
var st = { s: 0 };
var deform = el.querySelector(".hw-co-deform");
gsap.set(deform, { transformOrigin: "50% 100%" });
// volume-preserving squash on the SHAPES wrapper only — the label
// rides the uniform-scale pop OUTSIDE the deform, so non-uniform
// scale never reaches glyphs. Read-back assertion enforces the law.
window.hwOnUpdate(tl, function () {
var along = 1 - st.s;
gsap.set(deform, { scaleY: along, scaleX: 1 / along });
var ax = parseFloat(gsap.getProperty(deform, "scaleX"));
var ay = parseFloat(gsap.getProperty(deform, "scaleY"));
if (Math.abs(ax * ay - 1) > 0.01)
console.error("hw volume law violated (callout): " + (ax * ay).toFixed(4));
});
// contact: squash in 2 frames, 1-frame hold, spring home (factory
// duration consumed verbatim)
tl.to(st, { s: S, duration: 2 / 30, ease: "power3.out" }, t + 0.3);
tl.to(st, { s: 0, duration: rec2.duration, ease: rec2.ease }, t + 0.3 + 3 / 30);
// boil control (family base) — x/y/rotation on the boil wrapper only
window.hwBoilPose(tl, el.querySelector(".hw-co-boil"), co.boil, { seed: co.seed });
};
window.hwCalloutOff = function (tl, target, at) {
tl.to(target, { opacity: 0, duration: 0.35, ease: "power2.in" }, at);
};
// ---- self-preview (shipped-default CONFIG) ----
window.hwCalloutBuild("#hw-co-demo", {
scribble: CONFIG.scribble,
seed: SEED,
label: CONFIG.label,
connector: CONFIG.connector,
labelAt: CONFIG.labelAt,
strokeType: CONFIG.strokeType,
boil: CONFIG.boil,
});
var tl = gsap.timeline({ paused: true });
window.hwCalloutOn(tl, "#hw-co-demo", 0.4);
window.hwCalloutOff(tl, "#hw-co-demo", 3.3);
tl.set("#hw-co-root", { visibility: "hidden" }, 3.98);
window.__timelines["hw-callout-circle"] = tl;
})();
</script>
</body>
</html>