mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 07:09:59 +00:00
## Summary - **New skill:** **`marker-highlight`** — integrates [MarkerHighlight.js](https://github.com/Robincodes-Sandbox/marker-highlight) into HyperFrames compositions. Canvas-based animated text highlighting with 5 drawing modes: marker pen, circle, burst, scribble, and sketchout. - **Studio fix:** added missing `captionSync` to useEffect dependency array (oxlint exhaustive-deps) - **Studio fix:** `loadOverrides` now checks `res.ok` before parsing, preventing 404 console noise on projects without captions ## Skill details The skill documents the non-obvious GSAP integration pattern discovered during development: 1. **One highlighter per container** — the library clears ALL `.highlight` divs from the shared parent on init, so multiple instances on sibling marks conflict 2. **`data-color`** **\+** **`data-original-bgcolor`** — prevents the CSS background-color flash that occurs when the library reads and clears the mark's background 3. **Canvas pre-draw + clear + reanimate** — `animate: false` pre-draws statically, canvases are hidden, then cleared and shown with `reanimateMark()` at trigger time for clean animated reveals 4. **`onReverseComplete`** **for rewind** — hides highlight divs when the timeline seeks backward past the trigger point ## Test plan - [ ] `npx hyperframes lint` passes on test-composition - [ ] Studio preview shows marker highlight on "something" at 1s, circle on "love" at 2.2s - [ ] Rewind past trigger points hides highlights - [ ] No 404 console errors for caption-overrides.json on non-caption projects [Screen Recording 2026-04-02 at 1.56.30 AM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.com/user-attachments/thumbnails/53b03f4e-538e-477a-b738-7a033b99a84e.mov" />](https://app.graphite.com/user-attachments/video/53b03f4e-538e-477a-b738-7a033b99a84e.mov) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
147 lines
3.8 KiB
Markdown
147 lines
3.8 KiB
Markdown
# Marker Highlight Examples
|
|
|
|
## Recipes
|
|
|
|
### Underline
|
|
|
|
```html
|
|
<mark
|
|
data-height="0.15"
|
|
data-offset="0.8"
|
|
data-padding="0"
|
|
data-highlight='{"amplitude":0.2,"wavelength":5,"roughEnds":0}'
|
|
data-color="rgba(30, 136, 229, 0.6)"
|
|
>important</mark
|
|
>
|
|
```
|
|
|
|
### Strikethrough
|
|
|
|
```html
|
|
<mark
|
|
data-drawing-mode="highlight"
|
|
data-height="0.1"
|
|
data-offset="0"
|
|
data-highlight='{"amplitude":0.1,"wavelength":3}'
|
|
data-color="rgba(229, 57, 53, 0.8)"
|
|
>wrong answer</mark
|
|
>
|
|
```
|
|
|
|
### Circled Annotation
|
|
|
|
```html
|
|
<mark
|
|
data-drawing-mode="circle"
|
|
data-circle='{"curve":0.8,"wobble":0.4,"loops":2,"thickness":3}'
|
|
data-animation-speed="1200"
|
|
data-color="rgba(229, 57, 53, 0.6)"
|
|
>this one</mark
|
|
>
|
|
```
|
|
|
|
## Full Example in a Composition
|
|
|
|
```html
|
|
<div data-composition-id="highlight-demo" data-width="1920" data-height="1080">
|
|
<div
|
|
id="content"
|
|
style="
|
|
position: absolute; inset: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-family: 'Inter', sans-serif; font-size: 72px; color: #fff;
|
|
background: #111;
|
|
"
|
|
>
|
|
<p id="hero">
|
|
The <mark id="m1" data-color="rgba(255, 220, 50, 0.5)">fastest</mark> way to
|
|
<mark
|
|
id="m2"
|
|
data-drawing-mode="circle"
|
|
data-circle='{"curve":0.8,"wobble":0.3,"loops":2,"thickness":3}'
|
|
data-color="rgba(229, 57, 53, 0.6)"
|
|
>ship</mark
|
|
>
|
|
</p>
|
|
</div>
|
|
|
|
<style>
|
|
[data-composition-id="highlight-demo"] mark {
|
|
background-color: transparent;
|
|
color: inherit;
|
|
}
|
|
</style>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
<script src="marker-highlight.global.js"></script>
|
|
<script>
|
|
window.__timelines = window.__timelines || {};
|
|
var tl = gsap.timeline({ paused: true });
|
|
|
|
// Set colors via data attribute (no visible flash)
|
|
document.querySelectorAll("mark[data-color]").forEach(function (m) {
|
|
m.setAttribute("data-original-bgcolor", m.getAttribute("data-color"));
|
|
});
|
|
|
|
// Init once after fonts, then hide all canvases
|
|
var hl;
|
|
document.fonts.ready.then(function () {
|
|
setTimeout(function () {
|
|
hl = new MarkerHighlighter(document.getElementById("hero"), {
|
|
animate: false,
|
|
animationSpeed: 800,
|
|
padding: 0.3,
|
|
highlight: { amplitude: 0.3, wavelength: 5 },
|
|
});
|
|
setTimeout(function () {
|
|
document.querySelectorAll(".highlight").forEach(function (d) {
|
|
d.style.opacity = "0";
|
|
});
|
|
}, 100);
|
|
}, 50);
|
|
});
|
|
|
|
function addHighlight(markId, time) {
|
|
tl.to(
|
|
{},
|
|
{
|
|
duration: 0.001,
|
|
onStart: function () {
|
|
var mark = document.getElementById(markId);
|
|
var ref = mark.getAttribute("data-mark-ref");
|
|
if (!ref || !hl) return;
|
|
mark.parentElement
|
|
.querySelectorAll('.highlight[data-mark-id="' + ref + '"]')
|
|
.forEach(function (div) {
|
|
var c = div.querySelector("canvas");
|
|
if (c) c.getContext("2d").clearRect(0, 0, c.width, c.height);
|
|
div.style.opacity = "1";
|
|
});
|
|
hl.reanimateMark(mark);
|
|
},
|
|
onReverseComplete: function () {
|
|
var mark = document.getElementById(markId);
|
|
var ref = mark.getAttribute("data-mark-ref");
|
|
if (!ref) return;
|
|
mark.parentElement
|
|
.querySelectorAll('.highlight[data-mark-id="' + ref + '"]')
|
|
.forEach(function (div) {
|
|
div.style.opacity = "0";
|
|
});
|
|
},
|
|
},
|
|
time,
|
|
);
|
|
}
|
|
|
|
gsap.set("#hero", { opacity: 0 });
|
|
tl.to("#hero", { opacity: 1, duration: 0.6 }, 0);
|
|
|
|
addHighlight("m1", 0.8);
|
|
addHighlight("m2", 1.6);
|
|
|
|
window.__timelines["highlight-demo"] = tl;
|
|
</script>
|
|
</div>
|
|
```
|