UNPKG

rough-notation

Version:

Create and animate hand-drawn annotations on a web page

260 lines (259 loc) 9.26 kB
import { SVG_NS } from './model.js'; import { line, rectangle, ellipse, linearPath } from 'roughjs/bin/renderer'; function getOptions(type, seed) { return { maxRandomnessOffset: 2, roughness: type === 'highlight' ? 3 : 1.5, bowing: 1, stroke: '#000', strokeWidth: 1.5, curveTightness: 0, curveFitting: 0.95, curveStepCount: 9, fillStyle: 'hachure', fillWeight: -1, hachureAngle: -41, hachureGap: -1, dashOffset: -1, dashGap: -1, zigzagOffset: -1, combineNestedSvgPaths: false, disableMultiStroke: type !== 'double', disableMultiStrokeFill: false, seed }; } function parsePadding(config) { const p = config.padding; if (p || (p === 0)) { if (typeof p === 'number') { return [p, p, p, p]; } else if (Array.isArray(p)) { const pa = p; if (pa.length) { switch (pa.length) { case 4: return [...pa]; case 1: return [pa[0], pa[0], pa[0], pa[0]]; case 2: return [...pa, ...pa]; case 3: return [...pa, pa[1]]; default: return [pa[0], pa[1], pa[2], pa[3]]; } } } } return [5, 5, 5, 5]; } export function renderAnnotation(svg, rect, config, animationGroupDelay, animationDuration, seed) { const opList = []; let strokeWidth = config.strokeWidth || 2; const padding = parsePadding(config); const animate = (config.animate === undefined) ? true : (!!config.animate); const iterations = config.iterations || 2; const rtl = config.rtl ? 1 : 0; const o = getOptions('single', seed); switch (config.type) { case 'underline': { const y = rect.y + rect.h + padding[2]; for (let i = rtl; i < iterations + rtl; i++) { if (i % 2) { opList.push(line(rect.x + rect.w, y, rect.x, y, o)); } else { opList.push(line(rect.x, y, rect.x + rect.w, y, o)); } } break; } case 'strike-through': { const y = rect.y + (rect.h / 2); for (let i = rtl; i < iterations + rtl; i++) { if (i % 2) { opList.push(line(rect.x + rect.w, y, rect.x, y, o)); } else { opList.push(line(rect.x, y, rect.x + rect.w, y, o)); } } break; } case 'box': { const x = rect.x - padding[3]; const y = rect.y - padding[0]; const width = rect.w + (padding[1] + padding[3]); const height = rect.h + (padding[0] + padding[2]); for (let i = 0; i < iterations; i++) { opList.push(rectangle(x, y, width, height, o)); } break; } case 'bracket': { const brackets = Array.isArray(config.brackets) ? config.brackets : (config.brackets ? [config.brackets] : ['right']); const lx = rect.x - padding[3] * 2; const rx = rect.x + rect.w + padding[1] * 2; const ty = rect.y - padding[0] * 2; const by = rect.y + rect.h + padding[2] * 2; for (const br of brackets) { let points; switch (br) { case 'bottom': points = [ [lx, rect.y + rect.h], [lx, by], [rx, by], [rx, rect.y + rect.h] ]; break; case 'top': points = [ [lx, rect.y], [lx, ty], [rx, ty], [rx, rect.y] ]; break; case 'left': points = [ [rect.x, ty], [lx, ty], [lx, by], [rect.x, by] ]; break; case 'right': points = [ [rect.x + rect.w, ty], [rx, ty], [rx, by], [rect.x + rect.w, by] ]; break; } if (points) { opList.push(linearPath(points, false, o)); } } break; } case 'crossed-off': { const x = rect.x; const y = rect.y; const x2 = x + rect.w; const y2 = y + rect.h; for (let i = rtl; i < iterations + rtl; i++) { if (i % 2) { opList.push(line(x2, y2, x, y, o)); } else { opList.push(line(x, y, x2, y2, o)); } } for (let i = rtl; i < iterations + rtl; i++) { if (i % 2) { opList.push(line(x, y2, x2, y, o)); } else { opList.push(line(x2, y, x, y2, o)); } } break; } case 'circle': { const doubleO = getOptions('double', seed); const width = rect.w + (padding[1] + padding[3]); const height = rect.h + (padding[0] + padding[2]); const x = rect.x - padding[3] + (width / 2); const y = rect.y - padding[0] + (height / 2); const fullItr = Math.floor(iterations / 2); const singleItr = iterations - (fullItr * 2); for (let i = 0; i < fullItr; i++) { opList.push(ellipse(x, y, width, height, doubleO)); } for (let i = 0; i < singleItr; i++) { opList.push(ellipse(x, y, width, height, o)); } break; } case 'highlight': { const o = getOptions('highlight', seed); strokeWidth = rect.h * 0.95; const y = rect.y + (rect.h / 2); for (let i = rtl; i < iterations + rtl; i++) { if (i % 2) { opList.push(line(rect.x + rect.w, y, rect.x, y, o)); } else { opList.push(line(rect.x, y, rect.x + rect.w, y, o)); } } break; } } if (opList.length) { const pathStrings = opsToPath(opList); const lengths = []; const pathElements = []; let totalLength = 0; const setAttr = (p, an, av) => p.setAttribute(an, av); for (const d of pathStrings) { const path = document.createElementNS(SVG_NS, 'path'); setAttr(path, 'd', d); setAttr(path, 'fill', 'none'); setAttr(path, 'stroke', config.color || 'currentColor'); setAttr(path, 'stroke-width', `${strokeWidth}`); if (animate) { const length = path.getTotalLength(); lengths.push(length); totalLength += length; } svg.appendChild(path); pathElements.push(path); } if (animate) { let durationOffset = 0; for (let i = 0; i < pathElements.length; i++) { const path = pathElements[i]; const length = lengths[i]; const duration = totalLength ? (animationDuration * (length / totalLength)) : 0; const delay = animationGroupDelay + durationOffset; const style = path.style; style.strokeDashoffset = `${length}`; style.strokeDasharray = `${length}`; style.animation = `rough-notation-dash ${duration}ms ease-out ${delay}ms forwards`; durationOffset += duration; } } } } function opsToPath(opList) { const paths = []; for (const drawing of opList) { let path = ''; for (const item of drawing.ops) { const data = item.data; switch (item.op) { case 'move': if (path.trim()) { paths.push(path.trim()); } path = `M${data[0]} ${data[1]} `; break; case 'bcurveTo': path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `; break; case 'lineTo': path += `L${data[0]} ${data[1]} `; break; } } if (path.trim()) { paths.push(path.trim()); } } return paths; }