useTwo — arrows, paths & circles
useTwo() gives you two Two.js drawing layers per slide: two.back renders behind the slide's HTML, two.front on top. Pass usePos anchor strings directly — endpoints stay reactive.
<script setup lang="ts">
import { onMounted } from 'vue'
import { useTwo } from 'slidev-addon-gsap'
const two = useTwo()
onMounted(() => {
two.back.mkArrow('.a@r', '.b@l', { stroke: '#0ea5e9', linewidth: 3, text: 'flows to' })
two.back.mkPath({ stroke: '#6366f1', head: 'triangle', radius: 14 })
.M('.from@r').h(28).VH('.to@l')
two.front.mkCircle('.c@center', 40, { fill: '#34d399' })
})
</script>two.mkArrow / two.mkPath / two.mkCircle are shortcuts for the front layer; use two.back.* for the back layer (or two.layer('back')).
Arrows — mkArrow(from, to, props?)
mkArrow(from, to, props) is shorthand for a straight path with an arrowhead — mkPath({ head: true, …props }).M(from).L(to). So it takes every path prop (head, radius, dashed, text, start/end, …). Endpoints are resolved every frame, and a selector matching multiple elements fans out to one arrow per element.
two.back.mkArrow('.a@r', '.b@l', {
stroke: '#0ea5e9', linewidth: 3,
head: 'stealth', // any tip name (default true = a straight barb)
dashed: true, // dashed shaft (see "Dashed strokes" below)
text: 'flows to', // label riding the shaft (string or options object)
})The arrowhead scales with linewidth like every other path head (there is no separate headlen); pass head: false for a plain line.
Paths — mkPath(props?).M(start)…
Config-first: set props, then chain SVG-style commands starting with .M(). Uppercase = absolute, lowercase = relative to the current point.
two.back.mkPath({ stroke: '#6366f1', head: 'triangle', radius: 14, text: 'route' })
.M('.from@r') // moveto (start)
.h(28) // relative horizontal line
.VH('.to@l') // orthogonal routing into the targetCommands:
| Command | Meaning |
|---|---|
M L H V Z | Standard SVG-style (move / line / h-line / v-line / close) |
HV VH | One 90° corner (horizontal-then-vertical / vertical-then-horizontal) |
HVH VHV | Two corners; optional trailing ratio 0..1 (default 0.5) sets the split, e.g. .VHV('.b@c', 0.3) |
radiusrounds all corners;head(e.g.'triangle') adds an arrowhead.text/labeldraws a label that rides the shaft.
Arrow heads
head accepts a tip name (string), true (the default straight), or false / omitted for none. The tip is drawn at the end of the shaft, points along the path direction, inherits the shaft's stroke, and scales with linewidth — thicker lines get proportionally larger tips.
two.back.mkPath({ stroke: '#6366f1', head: 'stealth', linewidth: 3 }).M('.a@r').L('.b@l')The set is inspired by the TikZ arrows.meta library and grouped the same way. Names are matched case-insensitively and space/hyphen/underscore-insensitively, so 'Turned Square', 'turned square' and 'turned-square' are equivalent. Filled tips (the geometric shapes) paint their interior with the stroke color; barbs, brackets and the like are stroke-only. Every geometric tip also has a hollow, stroke-only twin named open … (e.g. 'open triangle', 'open circle') — the same outline with no fill.
straightline>arcbar|teebracketright bracketrbracket]left bracketlbracket[parenthesisright parenthesisrparenthesis)left parenthesislparenthesis(hooksclassicaltocmtrianglestealthlatexkitediamondturned squarediamond squaresquarerectangleellipsecircleopen triangleopen stealthopen latexopen kiteopen diamondopen turned squareopen squareopen rectangleopen ellipseopen circleraysThe rays tip takes a configurable ray count in trailing brackets — e.g. 'rays[3]' or 'rays[5]' (default 'rays[6]').
You can also pass a custom Two.js Shape as head — it's placed at the tip and rotated to the path direction (author it pointing toward +x with its point at the origin).
Dashed strokes
Both arrows and paths accept dash props. The arrowhead/path-head always stays solid — only the shaft is dashed.
two.back.mkArrow('.a@r', '.b@l', { dashed: true }) // linewidth-scaled dashes
two.back.mkPath({ stroke: '#6366f1', dashes: [12, 6] }).M('.a@r').L('.b@l') // explicit [on, off] pxdashed: true— derive a pattern that scales withlinewidth(thicker lines get larger dashes).false/ omitted draws solid.dashes: [on, off, …]— explicit dash/gap lengths in px (overridesdashed).dashOffset— shift the pattern along the path. Animate it withuseTlfor a marching-ants effect:
const p = two.back.mkPath({ dashes: [10, 8] }).M('.a@r').L('.b@l')
tl.step().to(p, { dashOffset: -18, duration: 1, repeat: -1, ease: 'none' })Circles — mkCircle(center, radius, props?)
two.front.mkCircle('.c@center', 40, { fill: '#34d399', stroke: '#10b981', linewidth: 3 })Labels
The text (alias label) prop on arrows and paths accepts a string or an options object:
text: { text: 'step 1', at: 0.5, offset: 8, rotate: true, background: '#fff' }at (0–1 along the visible shaft), offset (perpendicular px or [dx, dy]), rotate, fill, size, family, weight, background, padding.
Draw-on animation
Shapes expose start / end (0–1). Animate them with useTl:
const path = two.back.mkPath({ head: 'triangle' }).M('.a@r').L('.b@l')
tl.step().from(path, { end: 0, duration: 0.8 }).step() // draws it onReplay to watch a routed path draw on, the arrowhead appear, and a circle pop in — the same primitives mkPath / mkArrow / mkCircle produce.
Deck-wide defaults
Set defaults once in the deck headmatter under twojs::
---
twojs:
defaults: { linewidth: 2, stroke: "#695FAB" }
path: { head: triangle, radius: 10 }
arrow: { head: stealth }
circle: { fill: none }
---Precedence (low → high): twojs: headmatter → useTwo(config) per slide → per-type bucket → per-call props.
See the API reference.
Troubleshooting
Shapes present in the DOM but invisible? See FAQ → Two.js shapes are invisible — usually a deck CSS rule (UnoCSS attributify) dimming the opacity attribute Two.js writes on its groups.