How to turning svg shapes into canvas draw calls for html5 games
- Step 1Author sprites as flat path shapes — Build each sprite in your vector tool as
<path>geometry with solid fills — avoid gradients, filters, and live text. Outline (flatten) any primitives like<circle>or<rect>to paths, because the exporter only translates<path>elements. - Step 2Export a draw function per sprite — Run each sprite SVG through the Canvas Exporter. You get
export function draw<SpriteName>(ctx)in<name>-canvas.js. The name comes from the file stem (ship.svg→drawShip). - Step 3Pull the modules into your asset layer — Import the generated functions in your loader:
import { drawShip, drawAsteroid } from './sprites/index.js'. Because they're standard ESM exports, no special pipeline is needed beyond your normal bundler. - Step 4Draw in the loop with a per-entity transform — The function takes only
ctx, so set up the entity's transform first:ctx.save(); ctx.translate(e.x, e.y); ctx.rotate(e.angle); ctx.scale(e.scale, e.scale); drawShip(ctx); ctx.restore();. Do this for each entity every frame after clearing the canvas. - Step 5Re-use the geometry for collision — For point-in-shape tests, re-run the same path calls (without filling) and call
ctx.isPointInPath(px, py). Apply the same transform first so the test matches what's drawn. For fast broad-phase, keep a bounding box per entity and only do the precise test on overlap. - Step 6Hand-finish what the exporter can't cover — Add
ctx.lineWidth/ctx.lineCapbefore stroked calls (stroke width isn't carried over), replace any// arc approximated as linewith a realctx.arc()where the curve matters, and draw HUD text with your ownctx.fillText()since<text>isn't converted.
Game asset → what the exporter gives you
Plan your art pipeline around what actually converts. 'You add' is the small amount of glue code the exporter doesn't write.
| Asset type | Exporter output | You add |
|---|---|---|
| Flat path sprite (ship, asteroid) | Full draw<Name>(ctx) with fill/stroke | Per-frame ctx.translate/rotate/scale |
| Sprite with rounded arc edges | Arcs become straight lineTo (flagged) | Replace flagged lines with ctx.arc if it matters |
Sprite using <circle>/<rect> | Skipped (not a <path>) | Outline to paths before export |
| HUD / score text | Not converted (<text> ignored) | Your own ctx.fillText calls each frame |
| Gradient-shaded sprite | No gradient; solid fill attr only | ctx.createLinearGradient by hand, or flatten art |
| Stroked outline with width | Stroke colour only | Set ctx.lineWidth/lineCap/lineJoin |
Drawing one sprite many times per frame
The exported function is stateless apart from the ctx.save()/restore() it wraps itself in. Reuse it freely; the per-entity transform lives at the call site.
| Need | Pattern |
|---|---|
| Position a sprite | ctx.translate(x, y) before the call |
| Rotate a sprite | ctx.rotate(radians) (rotates around the current origin — translate to pivot first) |
| Scale / zoom | ctx.scale(s, s) before the call |
| Isolate transforms | Wrap each entity in ctx.save() … ctx.restore() |
| Tint differently per instance | Set ctx.globalAlpha / composite mode before calling (path colours are baked from attributes) |
| Crisp on Retina | Size canvas to w*dpr × h*dpr, ctx.scale(dpr, dpr) once at setup |
Cookbook
Game-loop snippets that wrap the exported draw functions. The functions themselves come straight from the Canvas Exporter.
Render a moving, rotating ship sprite
The exported drawShip(ctx) draws at the SVG's local coordinates. You place and orient it each frame by transforming the context around the call.
import { drawShip } from './sprites/ship-canvas.js';
function render(ctx, ship) {
ctx.save();
ctx.translate(ship.x, ship.y);
ctx.rotate(ship.angle); // radians
ctx.scale(ship.scale, ship.scale);
drawShip(ctx); // draws at local (0,0)-based path coords
ctx.restore();
}
// in the loop:
ctx.clearRect(0, 0, W, H);
for (const e of entities) render(ctx, e);Spawn 200 asteroids from one exported function
One drawAsteroid(ctx) reused for the whole field — vary position/rotation/scale per instance. This is the immediate-mode efficiency you moved to Canvas for.
import { drawAsteroid } from './sprites/asteroid-canvas.js';
function renderField(ctx, rocks) {
for (const r of rocks) {
ctx.save();
ctx.translate(r.x, r.y);
ctx.rotate(r.spin);
ctx.scale(r.size, r.size);
drawAsteroid(ctx);
ctx.restore();
}
}
// 200 rocks → 200 transform+draw blocks, no DOM, no per-shape nodesPoint-in-sprite collision via isPointInPath
Reuse the exported geometry for hit-testing. Apply the same transform, re-issue the path (you can copy the path calls into a hitbox function), then test the cursor or projectile point.
function shipContains(ctx, ship, px, py) {
ctx.save();
ctx.translate(ship.x, ship.y);
ctx.rotate(ship.angle);
ctx.scale(ship.scale, ship.scale);
drawShipPath(ctx); // beginPath + the path commands, NO fill
const hit = ctx.isPointInPath(px, py);
ctx.restore();
return hit;
}
// drawShipPath = the exported body minus the fill()/stroke() linesHUD text the exporter won't make for you
<text> in your SVG is ignored, so draw score/labels yourself. This keeps text crisp and controllable per frame.
function drawHud(ctx, score) {
ctx.save();
ctx.font = '16px monospace';
ctx.fillStyle = '#e5e7eb';
ctx.fillText('SCORE ' + score, 12, 24);
ctx.restore();
}
// Alternative: outline the text to paths first (svg-font-to-path),
// then export those paths — but live ctx.fillText is simpler for HUD.Retina-crisp setup once at boot
Canvas is a bitmap, so high-DPR displays need an explicit scale or sprites look soft. Do this once when sizing the canvas.
const dpr = window.devicePixelRatio || 1; canvas.width = W * dpr; canvas.height = H * dpr; canvas.style.width = W + 'px'; canvas.style.height = H + 'px'; ctx.scale(dpr, dpr); // now your draw* calls render crisply // re-run on resize / DPR change
Edge cases and what actually happens
Expecting the draw function to take x, y, rotation
Context-onlydraw<Name>(ctx) accepts only the context. There are no position/rotation/scale parameters — you wrap the call in ctx.translate/ctx.rotate/ctx.scale (inside save/restore) per entity each frame. Plan your render code around transforming the context, not passing transform args.
Sprite has rounded/curved corners drawn with arcs
Degraded to lineElliptical arcs (A) export as straight lineTo segments with a flag comment, so round corners go faceted. For game art, either convert arcs to cubic Béziers in your editor before export, or hand-replace the flagged line with a ctx.arc()/ctx.ellipse().
Sprite built from `<circle>`/`<rect>`/`<polygon>`
SkippedOnly <path> geometry is translated. A coin drawn as a <circle> or a crate as a <rect> produces nothing in the draw body. Outline these to paths in your art tool first — or, for simple shapes, just draw them directly with ctx.arc/ctx.rect and skip the exporter for those.
Gradient-shaded sprite
Not convertedGradients aren't translated; only a solid fill/stroke attribute survives. A fill="url(#metal)" becomes an unusable ctx.fillStyle = 'url(#metal)'. Flatten the gradient to a flat colour for the converted version, or build the ctx.createLinearGradient yourself.
Outline thickness is part of the art
Width ignoredstroke-width isn't read — strokes default to 1px in Canvas. If a sprite's outline weight matters (it usually does in games), set ctx.lineWidth (and lineJoin/lineCap) before invoking the draw function, or before its stroke section.
Collision needs the exact filled area, not just an outline
Re-issue the pathctx.isPointInPath works against the current path, so collision needs the path commands re-issued (a beginPath + the moveTo/lineTo/curveTo calls) under the same transform. Keep a path-only variant of each sprite (the exported body without fill()/stroke()), or use ctx.isPointInPath(path2d, x, y) with a Path2D you build once.
Rotation pivots around the wrong point
Pivot setupctx.rotate spins around the current transform origin. If your sprite's path coordinates start at the top-left, rotation looks off-centre. Translate to the desired pivot (often the sprite's centre) before rotating, then offset the draw, or author the SVG centred on its origin.
Performance dips with thousands of complex sprites
Consider WebGLCanvas 2D with exported draw calls is great for hundreds–low-thousands of sprites, but it's CPU-bound. For bullet-hell counts or huge particle fields, a GPU renderer (WebGL / PixiJS) outperforms re-issuing path calls each frame. The exporter targets Canvas 2D only.
Frequently asked questions
Should I use SVG or Canvas for a 2D HTML5 game?
Canvas for the gameplay rendering — many moving objects at 60fps make SVG's DOM overhead impractical. Keep static, interactive UI (menus, settings) as DOM/SVG if you like the styling and events. Author your art as SVG paths, then convert to Canvas draw calls for the in-game render.
How do I position and rotate an exported sprite?
Transform the context around the call: ctx.save(); ctx.translate(x, y); ctx.rotate(angle); ctx.scale(s, s); drawSprite(ctx); ctx.restore();. The function itself takes only ctx and draws at the SVG's local coordinates, so all placement is done via the context transform each frame.
Can I use the SVG path data for collision detection?
Yes. Re-issue the path commands (a beginPath plus the moveTo/lineTo/curveTo calls — the exported body without the fill/stroke) under the same transform, then call ctx.isPointInPath(px, py). For speed, do a bounding-box check first and only run the precise test on overlap.
What about SVG text in my game UI?
The exporter does not convert <text> to ctx.fillText() — text elements are ignored. Draw HUD/score text yourself with ctx.fillText, which is simplest and keeps it crisp. If you need the lettering as vector shapes, outline it to paths first (e.g. with svg-font-to-path) and export those paths.
Do gradients on my sprites carry over?
No. Only a solid fill/stroke attribute is exported; gradients aren't translated and a fill="url(#…)" won't render. Either flatten the gradient to a flat colour before exporting, or add ctx.createLinearGradient/createRadialGradient calls in the generated function yourself.
Are arcs (rounded shapes) accurate after export?
Not by default — A arcs are approximated as straight lines, with a comment flagging each one. For accurate curves, convert arcs to cubic Béziers in your vector editor before exporting, or replace the flagged lineTo with a real ctx.arc()/ctx.ellipse().
How do I draw the same sprite hundreds of times efficiently?
Call the one exported function in a loop, varying the transform per instance. Each call is plain ctx.* drawing with no DOM, which is exactly the immediate-mode advantage. For very high counts (thousands at 60fps) consider a Path2D you build once, or move to WebGL.
Does stroke width survive the conversion?
No — only the stroke colour is set via ctx.strokeStyle. stroke-width, stroke-linecap, and stroke-linejoin aren't read, so strokes use Canvas defaults. Set ctx.lineWidth and friends before the draw call (or before its stroke portion) to match your art.
Will my sprites look sharp on high-DPI screens?
Only if you set up DPR scaling. Vectors are resolution-independent, but Canvas is a bitmap: size the backing store to width*devicePixelRatio and call ctx.scale(dpr, dpr) once at setup (and on resize), then your exported draw calls render crisply.
Can I bundle all my sprite functions into one import?
Yes. Each export is standard ESM, so create a barrel module that re-exports them (export * from './ship-canvas.js') and import the whole set from one path. Watch for name collisions if two SVGs share a file stem — both would export draw<SameName>.
Is the conversion done on a server?
No — it runs in your browser via the DOM parser; the SVG never leaves your machine (the result panel shows 0 bytes uploaded). For build-time automation, the local @jadapps/runner does the work on your machine; JAD's hosted API never accepts uploads.
Do I need a paid plan to use this for game art?
Yes — the Canvas Exporter is a Developer-tier tool. On Free/Pro you'll see an upgrade overlay. If you're not ready for Canvas and just want lighter SVG sprites, svg-pro-minifier and svg-path-simplifier are available on lower tiers (the simplifier only thins pure-polyline paths and skips curved paths).
Privacy first
Every JAD SVG tool runs entirely in your browser using the DOM API and Canvas. Your SVG files never leave your device — verified by zero outbound network requests during processing.