Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active December 15, 2023 17:45
Show Gist options
  • Save mattdesl/83a1eb53bb703f5dd939d424ccab86b6 to your computer and use it in GitHub Desktop.
Save mattdesl/83a1eb53bb703f5dd939d424ccab86b6 to your computer and use it in GitHub Desktop.
png + svg export with canvas-sketch and context 2D

PNG + SVG export from canvas-sketch

The helper function canvas-to-svg.js wraps a given render function (or renderer object) so that you can use Canvas2D context methods as usual, but upon single frame export (with Cmd/Ctrl + S) it will produce both a PNG and SVG file.

This uses canvas2svg which is not a perfect solution, as the Canvas2D API was never designed to be translated to SVG. Its best to stick with simple shape and path operations.

Full instructions: first install the canvas-sketch CLI if you haven't already:

npm install canvas-sketch-cli -g

Then copy the sketch.js and canvas-to-svg.js into a new folder, say the folder is called mysketch. Install dependencies:

# move into it
cd mysketch

# make a new package.json
npm init -y

# install deps
npm install canvas-sketch canvas-sketch-util canvas2svg --save

Then run the sketch:

canvas-sketch sketch.js --open

Usage in Existing Sketches

It looks like this:

const canvasSketch = require('canvas-sketch');
const svg = require('./canvas-to-svg.js');

const settings = {
  // ... your settings ...
};

const sketch = () => {
  return svg(props => {
    const { context, width, height } = props;
    
    // ... draw your art ...
  });
};

canvasSketch(sketch, settings);
const canvasSketch = require("canvas-sketch");
const Canvas2SVG = require("canvas2svg");
const Color = require("canvas-sketch-util/color");
module.exports = canvasToSVG;
function canvasToSVG(sketch) {
const obj = typeof sketch === "function" ? {} : sketch;
const render = typeof sketch === "function" ? sketch : obj.render;
return {
...obj,
render(props) {
render(props);
if (props.exporting && !props.recording) {
return [props.canvas, serialize(render, props)];
}
}
};
}
function fix(node, name) {
if (!node.hasAttribute(name) || !node.getAttribute(name)) return;
const attr = node.getAttribute(name);
const parsed = Color.parse(attr);
if (parsed) node.setAttribute(name, parsed.hex);
}
function serialize(draw, props) {
const {
canvasWidth,
canvasHeight,
width,
height,
units,
scaleX,
scaleY
} = props;
const context = new Canvas2SVG(canvasWidth, canvasHeight);
draw({ ...props, context });
const svg = context.getSvg().cloneNode(true);
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
[...svg.querySelectorAll("*")].forEach(node => {
fix(node, "fill");
fix(node, "stroke");
});
svg.setAttribute("width", width + units);
svg.setAttribute("height", height + units);
return {
data: new XMLSerializer().serializeToString(svg),
extension: ".svg"
};
}
const canvasSketch = require("canvas-sketch");
const svg = require("./canvas-to-svg.js");
const settings = {
scaleToView: true,
dimensions: "A4",
units: "in",
animate: false,
time: 1.5,
duration: 5,
pixelsPerInch: 300
};
const sketch = async () => {
// Setup your font assets here
const fontName = "Suprapower-Heavy";
const fontFormat = ".otf";
const fontPath = "assets/fonts/";
try {
// We ensure the font is loaded before rendering,
// otherwise the first frame might not draw the correct font.
const fontUrl = `${fontPath}${fontName}${fontFormat}`;
const font = new window.FontFace(fontName, `url(${fontUrl})`);
await font.load();
document.fonts.add(font);
} catch (err) {
console.warn(`Font not loaded, will fall back to another sans-serif.`);
}
return svg(({ context, width, height, playhead }) => {
const margin = 0.5; // half inch margin
// fill paper white bg
context.fillStyle = "hsl(0, 0%, 100%)";
context.fillRect(0, 0, width, height);
// fill gray ink bg
context.fillStyle = "hsl(0, 0%, 90%)";
context.fillRect(margin, margin, width - margin * 2, height - margin * 2);
// draw some rect shape
context.save();
context.fillStyle = "tomato";
const size = width * 0.5;
context.translate(width / 2, height / 2);
context.rotate(playhead * Math.PI * 2);
context.translate(-size / 2, -size / 2);
context.fillRect(0, 0, size, size);
context.restore();
// draw some path shape
context.beginPath();
context.arc(
width / 2,
height / 2,
width / 4,
0,
Math.PI * 2 * playhead,
false
);
context.lineWidth = width * 0.1;
context.lineJoin = "round";
context.lineCap = "round";
context.strokeStyle = "rebeccapurple";
context.globalAlpha = 0.85;
context.stroke();
context.globalAlpha = 1;
// draw some text - which will be exported as <text>
// (i.e. no fonts embedded!)
context.font = `${width * 0.075}px "${fontName}", "Helvetica", sans-serif`;
context.textAlign = "center";
context.textBaseline = "middle";
context.fillStyle = "black";
context.fillText("SVG", width / 2, height / 2);
});
};
canvasSketch(sketch, settings);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment