모션·인터랙션 / Cadence
Hand-Drawn Highlights
텍스트 · 스크롤 모션. 미리보기와 원본 코드·프롬프트를 이곳에서 확인하세요.
미리보기 안에서 스크롤하거나 마우스를 움직여 모션을 확인하세요.
HTML · CSS · JavaScript 전체 코드
<!doctype html>
<!--
Hand-Drawn Highlights
As a paragraph scrolls into view, its key words get highlighted, underlined, circled,
struck through, boxed and bracketed with sketchy marker strokes that draw themselves,
one after another. While in view it loops: draw, hold, fade the strokes out, draw
again. Click the text to replay; scrolling it out of view and back starts it over.
Dependencies: Rough Notation 0.5.1 (ES module from jsDelivr).
Tweak:
- <mark data-note="…">: highlight, underline, circle, strike-through, box, bracket
(also crossed-off); add class="nowrap" to keep circles, boxes and brackets on one line
- CONFIG.color / highlightColor: light grey strokes #BDBDBD, highlight #D9D9D9
- CONFIG.duration: 700ms per mark (drawn one after another); iterations: 2 passes of the pen
- CONFIG.strokeWidth 2, padding 4, showWhen 0.6 (fraction of the paragraph visible)
- HOLD / FADE: 2.5s hold, 0.5s fade before the loop (FADE matches the CSS transition)
Built with Rough Notation by Preet Shihn, https://roughnotation.com
MIT License · Copyright (c) 2020 Preet Shihn
From The Animations Index, 29 Hand-Drawn Highlights (own project).
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hand-Drawn Highlights</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #000; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.hh {
min-height: 100svh;
display: grid;
place-items: center;
padding: 64px clamp(16px, 2.5vw, 32px);
}
.hh-text {
position: relative;
max-width: 44ch;
margin: 0;
font-size: clamp(1.25rem, 2.4vw, 1.75rem);
line-height: 1.5;
cursor: pointer; /* click to replay */
-webkit-tap-highlight-color: transparent;
}
.hh-text mark { background: none; color: inherit; }
/* Circles, boxes and brackets read best around a phrase on one line */
.nowrap { white-space: nowrap; }
/* Between loops the strokes fade out before they are cleared */
.hh-text .rough-annotation { transition: opacity 0.5s ease; }
.hh-text.is-clearing .rough-annotation { opacity: 0; }
</style>
</head>
<body>
<section class="hh">
<!-- Each <mark> becomes one annotation, drawn in reading order; data-note picks the style. -->
<p class="hh-text" id="text">
Good motion <mark data-note="highlight">takes its time</mark>.
A photograph slides in, a line of type settles, and <mark data-note="underline">nothing moves without a reason</mark>.
The best of it feels like <mark data-note="circle" class="nowrap">turning a page</mark>,
never like <mark data-note="strike-through">a fireworks show</mark>: an <mark data-note="box" class="nowrap">even rhythm</mark>,
plenty of white space, and a short pause <mark data-note="bracket" class="nowrap">between the frames</mark>.
</p>
</section>
<script type="module">
import { annotate, annotationGroup } from "https://cdn.jsdelivr.net/npm/rough-notation@0.5.1/lib/rough-notation.esm.js";
const CONFIG = {
color: "#BDBDBD", // strokes
highlightColor: "#D9D9D9", // the highlight behind the text
strokeWidth: 2,
duration: 700, // ms per mark
padding: 4,
iterations: 2, // how many times the pen goes over a stroke
multiline: true,
showWhen: 0.6, // fraction of the paragraph visible before it draws
};
const HOLD = 2500; // ms the finished marks stay before replaying
const FADE = 500; // ms the strokes fade out between loops (matches the CSS transition)
const text = document.getElementById("text");
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// One annotation per mark, in reading order, drawn one after another by a group.
const annotations = [...text.querySelectorAll("mark")].map((mark) => {
const type = mark.dataset.note || "underline";
return annotate(mark, {
type,
color: type === "highlight" ? CONFIG.highlightColor : CONFIG.color,
strokeWidth: CONFIG.strokeWidth,
animationDuration: CONFIG.duration,
padding: CONFIG.padding,
iterations: CONFIG.iterations,
multiline: CONFIG.multiline,
brackets: ["left", "right"], // only used by the bracket mark
animate: !reduced,
});
});
const group = annotationGroup(annotations);
const drawTime = annotations.length * CONFIG.duration;
let timers = [];
const clearTimers = () => {
timers.forEach(clearTimeout);
timers = [];
};
function reset() {
clearTimers();
group.hide();
text.classList.remove("is-clearing");
}
// Draw, hold, fade out, draw again. Reduced motion: shown at once, and it stays.
function play() {
reset();
group.show();
if (reduced) return;
timers.push(setTimeout(() => {
text.classList.add("is-clearing");
timers.push(setTimeout(play, FADE));
}, drawTime + HOLD));
}
let inView = false;
const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (!inView && entry.intersectionRatio >= CONFIG.showWhen - 0.001) {
inView = true;
play();
} else if (inView && !entry.isIntersecting && !reduced) {
inView = false;
reset(); // fully out of view: starts over the next time it comes in
}
},
{ threshold: [0, CONFIG.showWhen] }
);
observer.observe(text);
if (!reduced) {
text.addEventListener("click", play);
// Browsers pause the stroke animations in a hidden tab but not the timers: start over on return.
document.addEventListener("visibilitychange", () => {
if (document.hidden) reset();
else if (inView) play();
});
}
</script>
<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/vce701823e78c4301a35b7e07294d87301792088814092" integrity="sha512-UKNk8GbxkABxKcXYuLkO16EleLGUkcvJWtpSNmExE0Too3ZaFAxTscNWEc1OdAv5/BikHawapSCaiD2XCmp+Bw==" data-cf-beacon='{"version":"2024.11.0","token":"c54fa41b83884ade97f239ecb41aee46","r":1,"spa":2}' crossorigin="anonymous"></script>
</body>
</html>
원본 구현 프롬프트
# Hand-Drawn Highlights: sketchy marker annotations that draw themselves over a paragraph
**Goal:** when a paragraph scrolls into view, mark its key phrases with hand-drawn strokes (highlight, underline, circle, strike-through, box, bracket) that draw themselves one after another, like someone annotating the text with a marker.
## Visual behaviour
1. A single paragraph of body text, centred on the page, with six phrases wrapped as marks. Each mark has its own annotation style, in reading order:
1. **highlight**: a rough, slightly wobbly marker fill behind the words
2. **underline**: a sketchy line under the phrase (it may wrap over two lines; each line gets its own underline)
3. **circle**: a loose hand-drawn ellipse around the phrase
4. **strike-through**: a rough line through the middle of the words
5. **box**: a sketchy rectangle around the phrase
6. **bracket**: square brackets on the left and right of the phrase
2. **Trigger.** When 60% of the paragraph is visible (IntersectionObserver, thresholds `[0, 0.6]`), the marks draw **one after another**, never at the same time: each mark takes **700 ms**, and the next starts when the previous one ends (six marks = 4.2 s).
3. **The drawing.** Every stroke is an SVG path drawn with a dash animation (`stroke-dasharray` and `stroke-dashoffset` = path length → `stroke-dashoffset: 0`), `ease-out`. Within a mark, its paths draw in sequence, each getting a share of the 700 ms in proportion to its length. The pen goes over every stroke **twice** (two slightly different rough passes), which gives the marker its hand-made look.
4. **Loop while in view.** After the last mark finishes, everything holds for **2.5 s**, the strokes fade out over **0.5 s** (`opacity` transition, `ease`), are cleared, and the sequence draws again.
5. **Replay.** Clicking the paragraph clears everything and starts drawing again immediately. When the paragraph scrolls fully out of view, clear it; it starts over the next time 60% of it is visible.
6. If the tab is hidden, clear everything (browsers pause CSS animations in hidden tabs but not timers) and start over when it becomes visible again.
## Exact values
| What | Value |
|---|---|
| Per mark | 700 ms, marks strictly sequential |
| Stroke easing | `ease-out` per path |
| Passes per stroke | 2 |
| Stroke width | 2 px |
| Padding around the words | 4 px |
| Stroke colour | `#BDBDBD` (light grey) |
| Highlight colour | `#D9D9D9` (lighter grey, sits behind the text) |
| Trigger | 60% of the paragraph visible |
| Hold / fade | 2.5 s / 0.5 s |
| Text | `clamp(1.25rem, 2.4vw, 1.75rem)`, line-height 1.5, max-width 44ch |
## Markup
```html
<p class="text">
Good motion <mark data-note="highlight">takes its time</mark>.
A photograph slides in, a line of type settles, and <mark data-note="underline">nothing moves without a reason</mark>.
The best of it feels like <mark data-note="circle" class="nowrap">turning a page</mark>,
never like <mark data-note="strike-through">a fireworks show</mark>: an <mark data-note="box" class="nowrap">even rhythm</mark>,
plenty of white space, and a short pause <mark data-note="bracket" class="nowrap">between the frames</mark>.
</p>
```
- Reset `<mark>` styling: `background: none; color: inherit`.
- Circle, box and bracket phrases get `white-space: nowrap` so each stays one shape on one line. Highlight, underline and strike-through may wrap (multiline on).
- The annotation SVGs are positioned over the text (the paragraph is `position: relative`); give them `transition: opacity 0.5s ease` and fade them out with a class on the paragraph between loops.
## Libraries
- **Rough Notation 0.5.1** (`rough-notation`, ES module). For each mark: `annotate(mark, { type, color, strokeWidth: 2, animationDuration: 700, padding: 4, iterations: 2, multiline: true, brackets: ["left", "right"], animate: true })`. Put them in `annotationGroup(annotations)` in reading order and call `group.show()` to draw them in sequence, `group.hide()` to clear.
- **Plain alternative:** draw your own rough SVG paths around each phrase's client rects (jitter the corners and control points by 1–2 px, draw each shape twice with different jitter), and animate `stroke-dashoffset` from the path length to 0 with `ease-out`, chaining the marks with animation delays of 700 ms.
## Responsive and reduced motion
- The annotations are measured from the words' rendered positions; re-measure and redraw on resize (Rough Notation does this itself).
- `prefers-reduced-motion: reduce`: show all marks at once without the drawing animation (`animate: false`), and keep them; no loop.
## Acceptance criteria
- Nothing is drawn until 60% of the paragraph is visible.
- The six marks draw strictly one after another, 700 ms each, each with a visibly hand-drawn, double-pass stroke.
- The highlight sits behind the text and never covers it; the other marks are light grey and stay readable on white.
- Circle, box and bracket each enclose their phrase on one line.
- After a 2.5 s hold the strokes fade out (0.5 s) and redraw, for as long as the paragraph is in view; clicking replays at once.
- With reduced motion, all marks are visible immediately and static.
보관·활용 메모
Cadence에서 제공하는 코드와 프롬프트, 미리보기 영상을 이 사이트에 보관했습니다. 코드는 다운로드한 원본으로 제공하며 실행 화면은 별도로 격리되어 있습니다. 코드의 외부 라이브러리·샘플 이미지 요청은 유지됩니다.
보관일: 2026-10-26 · 프로젝트 적용: 별도 검증 필요
원본 코드·프롬프트·제작자 정보를 함께 보관했습니다. 실제 납품 시 코드와 포함 소재의 이용 조건을 확인하세요.
코드 SHA-256: 2e18cda58a7004adcdce6742627d7a4d0922a81920d2c24faa80d2ef8b752a8a