Build gradients, patterns, strokes, markers, clips, masks, filters, interaction states and animation deterministically — then verify every one of them.
Install
npm i kurviq @tecsteps/kurviq-toolbelt
Every graphic on this page was generated by kurviq/author and verified through
@tecsteps/kurviq-toolbelt. None of it is hand-written SVG.
Paint servers
Gradients, patterns and reused symbols. Units are mandatory — the SVG
default differs per element and silently changes the result, so an omitted value is a bug
rather than a convenience.
Width, caps, joins, dashes and arrowheads. There is one
stroke-width per element — variable width along a path is not interoperable
SVG, so vary it with multiple elements.
Dashes and an arrowhead
const { strokedPath, arrowhead, withMarkers } = require('kurviq/author-stroke');const head = arrowhead(scene, { size: 7, color: '#fb7185' });scene.add(withMarkers( strokedPath('M24 40L250 40', { color: '#fb7185', width: 3 }), { end: head },));// strokedPath sets fill="none" explicitly: the SVG default is black,// and a filled open path is the classic surprise.
Clipping and masking
A clip is a hard geometric boundary; a mask is per-pixel. The mask
type is mandatory because the same content behaves completely differently
under luminance and alpha.
Blur, drop shadow and colour matrices. The filter region is mandatory:
the SVG default clips blurs and shadows, and the symptom — a cut-off edge — never points
at the cause.
Blur with an explicit region
const { blur, dropShadow, colorMatrix } = require('kurviq/author-filter');// elementSize gives a tight filter region. Without it the default is// deliberately generous: in objectBoundingBox units the pad is a fraction// of the ELEMENT, so a small shape with a big blur needs a big fraction.const soft = blur(scene, { stdDeviation: 6, elementSize: 70 });scene.add(soft.apply(shapes.circle(66, 90, 34, { fill: '#22d3ee' })));
Interaction states
Hover and keyboard focus, no JavaScript. Interactivity is a property of the
embedding, not the file: an <img>-embedded SVG receives
no pointer events, so :hover there is dead markup.
assertEmbedding() makes that an error instead of a silent no-op — which is why
this demo is inlined rather than loaded as an image.
Hover with an embedding assertion
const I = require('kurviq/author-interactive');const feature = I.hoverState(scene, { selector: '.chip', base: { fill: '#1e293b', stroke: '#94a3b8' }, on: { fill: '#22d3ee', stroke: '#22d3ee' },});// Throws if this document would be shipped as <img>, where no pointer// events arrive and :hover is dead markup.I.assertEmbedding([feature], I.EMBEDDING.INLINE);
Animation
An AnimationPlan serializes to CSS or SMIL and can be sampled at a given
time. The initial state lives only in the keyframes, so a renderer that applies CSS
without animating paints the correct final frame rather than a blank image. Reduced motion
settles on the final value.
A staggered entrance
const A = require('kurviq/author-animate');const plan = A.animationPlan({ duration: 900, stagger: 120, easing: 'ease-out' });[0, 1, 2, 3].forEach((i) => plan.animate({ selector: `.bar-${i}`, property: 'opacity', from: 0, to: 1 }));A.applyPlan(scene, plan); // status: 'declared' — never more than that
Verify it, do not hope
Rendering, comparison and state verification live in @tecsteps/kurviq-toolbelt, so
kurviq itself stays dependency-free. A state is verified by compiling it into
a static document, rendering, and diffing — no browser required.
Verification
const { evaluate, validateTarget } = require('@tecsteps/kurviq-toolbelt');// Verify an interaction state without a browser: the state is compiled// into a static document, rendered, and diffed against the base.const { report } = evaluate({ svg, width: 320, states: ['base', 'hover'] });report.states.hover.visiblyDiffers; // false means the rule paints nothingreport.states.hover.changedPixels; // 13992 for the demo abovevalidateTarget(svg, 'browser-img').ok; // false: hover needs pointer events
Measured target support
Established by rendering, not read from documentation. The renderer column is our own
verification engine, pinned to a version.