gsap-canvas — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited gsap-canvas (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Flow: gsap-setup → gsap-canvas → gsap-optimise → gsap-test
Companion: For GSAP core API reference, invoke gsap-core. For timeline sequencing, invoke gsap-timeline. This skill covers canvas rendering recipes only. Requires: greensock/gsap-skillsCanvas animation with GSAP is fundamentally different from DOM animation. GSAP never touches a DOM element directly. Instead:
Plain objects → GSAP tweens numbers → onUpdate draws to canvas
↑ ↓
{ x, y, scale } ctx.drawImage(img, x, y, w*scale, h*scale)This means GSAP handles easing, stagger, repeat, yoyo, and timeline sequencing — while you own the rendering pipeline entirely.
// Minimal example: animate a circle across canvas
const dot = { x: 0, y: 250, radius: 20 }
gsap.to(dot, {
x: 800, duration: 2, ease: 'power2.inOut',
onUpdate() {
ctx.clearRect(0, 0, cw, ch)
ctx.beginPath()
ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2)
ctx.fill()
},
})Animate an array of particles with sprite images orbiting inward. GSAP handles all motion; onUpdate redraws every frame.
const particles = Array.from({ length: 99 }, (_, i) => ({
x: 0, y: 0, scale: 0, rotate: 0,
img: Object.assign(new Image(), {
src: `https://assets.codepen.io/16327/flair-${2 + (i % 21)}.png`,
}),
}))const radius = Math.max(cw, ch)
const tl = gsap.timeline({ onUpdate: draw })
.fromTo(particles, {
x: (i) => {
const angle = (i / particles.length) * Math.PI * 2 - Math.PI / 2
return Math.cos(angle * 10) * radius
},
y: (i) => {
const angle = (i / particles.length) * Math.PI * 2 - Math.PI / 2
return Math.sin(angle * 10) * radius
},
scale: 1.1,
rotate: 0,
}, {
duration: 5, ease: 'sine',
x: 0, y: 0, scale: 0, rotate: -3,
stagger: { each: -0.05, repeat: -1 },
}, 0)
.seek(99) // jump ahead so repeating stagger is mid-flowfunction draw() {
particles.sort((a, b) => a.scale - b.scale) // z-sort by scale
ctx.clearRect(0, 0, cw, ch)
particles.forEach((p) => {
ctx.translate(cw / 2, ch / 2)
ctx.rotate(p.rotate)
ctx.drawImage(p.img, p.x, p.y, p.img.width * p.scale, p.img.height * p.scale)
ctx.resetTransform()
})
}canvas.addEventListener('pointerup', () => {
gsap.to(tl, {
timeScale: tl.isActive() ? 0 : 1, // smooth ease to pause/play
})
})window.addEventListener('resize', () => {
cw = canvas.width = innerWidth
ch = canvas.height = innerHeight
radius = Math.max(cw, ch)
tl.invalidate() // recalculates functional from-values on next render
})invalidate() forces GSAP to re-evaluate function-based values (the trig calculations) using updated dimensions.
| Tip | Why |
|---|---|
Use gsap.ticker.add(fn) for render loops | Syncs with GSAP's internal rAF — one frame budget, no double-paints |
| Minimize state changes | Batch translate/rotate calls; call resetTransform() once per particle |
| Sort sparingly | particles.sort() every frame is O(n log n) — skip if z-order is fixed |
Use will-change: transform on the <canvas> | Promotes to GPU layer, reduces compositing cost |
Prefer ctx.resetTransform() over save/restore | Faster — avoids stack push/pop overhead |
| Pre-render to offscreen canvas | For static sprites, draw once to OffscreenCanvas, then drawImage from it |
references/canvas-patterns.md — Full implementations: particle orbit system, canvas morphs (MorphSVG rendered to canvas)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.