ink
Gives borders, shapes and headings a brush-drawn edge that redraws a few times a second, like hand-drawn animation. Text inside stays real and selectable.
- 1.0 KB gzipped
- 0 dependencies
- 1 file
- inspired by Rough.js
Try it
Tonight's sketch
The outline and the heading are inked. This sentence is left clean so it stays readable. Select it, then tab to the button.
Install
Download the file into your project and import it. There is no package and no build step, so the code is yours to change.
curl -O /proof/r/ink.jsUsage
import { ink } from './ink.js'
const card = ink(document.querySelector('.card'), { rough: 3 })
button.addEventListener('pointerenter', () => card.set({ bleed: 2.5 }))
button.addEventListener('pointerleave', () => card.set({ bleed: 0 }))API
ink(el, options)
| option | type | default | what it does |
|---|---|---|---|
| rough | number | 3 | How far edges wander, in pixels. |
| bleed | number | 0 | How far ink spreads past the edge, in pixels. |
| boil | boolean | true | Redraws the line about 8 times a second, like hand-drawn animation. |
| grain | number | 0.035 | Size of the wobble. Lower values give longer, lazier strokes. |
Returns
| set({ rough, bleed, boil }) | Changes any option while the page runs. |
| destroy() | Removes the filter and stops the boil. |
Notes
- Ink a frame or a heading, not body text. The filter softens everything inside the element, and Safari blurs small text.
- The boil stops when the reader asks for reduced motion.
Source
The whole library. Read it, copy it, change it.
// ink.js from proof, by Alok Ranjan (https://alokranjan.me). MIT license.
const NS = 'http://www.w3.org/2000/svg'
let defs
let count = 0
function host() {
if (defs) return defs
const svg = document.createElementNS(NS, 'svg')
svg.setAttribute('aria-hidden', 'true')
svg.style.cssText = 'position:absolute;width:0;height:0'
defs = document.createElementNS(NS, 'defs')
svg.append(defs)
document.body.append(svg)
return defs
}
export function ink(el, { rough = 3, bleed = 0, boil = true, grain = 0.035 } = {}) {
const id = 'ink-' + ++count
const f = document.createElementNS(NS, 'filter')
f.id = id
f.setAttribute('x', '-5%')
f.setAttribute('y', '-5%')
f.setAttribute('width', '110%')
f.setAttribute('height', '110%')
f.innerHTML = `
<feTurbulence type="fractalNoise" baseFrequency="${grain}" numOctaves="2" seed="1" result="t"/>
<feDisplacementMap in="SourceGraphic" in2="t" scale="${rough}" xChannelSelector="R" yChannelSelector="G" result="d"/>
<feMorphology in="d" operator="dilate" radius="${bleed}" result="m"/>
<feGaussianBlur in="m" stdDeviation="0.7" result="b"/>
<feComponentTransfer in="b"><feFuncA type="table" tableValues="0 0 0.9 1 1"/></feComponentTransfer>`
host().append(f)
el.style.filter = `url(#${id})`
const turb = f.querySelector('feTurbulence')
const disp = f.querySelector('feDisplacementMap')
const morph = f.querySelector('feMorphology')
let timer = 0
const still = matchMedia('(prefers-reduced-motion: reduce)').matches
const start = () => { if (!timer && !still) timer = setInterval(() => turb.setAttribute('seed', 1 + Math.floor(Math.random() * 3)), 130) }
const stop = () => { clearInterval(timer); timer = 0 }
if (boil) start()
return {
set({ rough: r, bleed: b, boil: on } = {}) {
if (r !== undefined) disp.setAttribute('scale', r)
if (b !== undefined) morph.setAttribute('radius', b)
if (on === true) start()
if (on === false) stop()
},
destroy() { stop(); f.remove(); el.style.filter = '' },
}
}