Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active May 12, 2020 17:16
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mattdesl/f6ec92f776dce771cd07c7d2c7087e8b to your computer and use it in GitHub Desktop.
Save mattdesl/f6ec92f776dce771cd07c7d2c7087e8b to your computer and use it in GitHub Desktop.

high res tiled rendering with canvas-sketch

Copy these files in the gist to a new folder. Generate a package.json:

cd folder-with-code
npm init -y

Then install deps:

npm install puppeteer dateformat fast-png convert-length ora --save-dev

Start a canvas-sketch server on one of the two artworks:

canvas-sketch sketch3d.js

Then run:

node save.js

notes

Regular sketches should work but you'll need to add the window.export part after launching canvas sketch.

You can use scaleToView to develop in-browser with regular canvas-sketch. The tiled exporter will ignore this and render full size.

const puppeteer = require("puppeteer");
const dateformat = require("dateformat");
const fastPng = require("fast-png");
const fs = require("fs");
const path = require("path");
const convertLength = require("convert-length");
const ora = require("ora");
const bootup = /*js*/ `
window.exporter = async (manager) => {
manager.props.exporting = true;
manager.resize();
const {
units,
dimensions,
pixelsPerInch,
pixelRatio = 1
} = manager.props;
let width = await window.exporter_convertLength(dimensions[0], units, 'px', {
pixelsPerInch,
roundPixel: true
});
let height = await window.exporter_convertLength(dimensions[1], units, 'px', {
pixelsPerInch,
roundPixel: true
});
width *= pixelRatio;
height *= pixelRatio;
console.log([
'',
' Input Size:',
' ' + dimensions[0] + 'x' + dimensions[1] + ' ' + units,
' @' + pixelRatio + 'x',
' ' + pixelsPerInch + ' DPI',
'',
' Outpt Size:',
' ' + width + 'x' + height + ' px',
''
].join('\\n'))
const tileSize = Math.min(width, height, 1024);
const offCanvas = document.createElement('canvas');
offCanvas.width = offCanvas.height = tileSize;
const offContext = offCanvas.getContext("2d");
function draw (context) {
manager.props.exporting = true;
manager.render();
}
await tileRenderer();
async function tileRenderer() {
await window.tileRenderStart(width, height, manager.settings);
const xTiles = Math.ceil(width / tileSize);
const yTiles = Math.ceil(height / tileSize);
const tiles = [];
for (let y = 0; y < yTiles; y++) {
for (let x = 0; x < xTiles; x++) {
const xPos = x * tileSize;
const yPos = y * tileSize;
const curWidth = Math.min(width - xPos, tileSize);
const curHeight = Math.min(height - yPos, tileSize);
tiles.push({
xPos,
yPos,
curWidth,
curHeight
});
}
}
let offset = 0;
await tiles.reduce(async (p, tile, i, list) => {
await p;
manager.props.region = {
x: tile.xPos,
y: tile.yPos,
width: tile.curWidth,
height: tile.curHeight
};
let dataURL;
if (manager.props.gl) {
draw();
dataURL = manager.props.canvas.toDataURL('image/png');
} else {
offContext.save();
offContext.clearRect(0, 0, tileSize, tileSize);
offContext.translate(-tile.xPos, -tile.yPos);
offContext.scale((width / manager.props.width) / manager.props.scaleX, (height / manager.props.height) / manager.props.scaleY);
manager.props.context = offContext;
draw();
dataURL = offCanvas.toDataURL('image/png');
offContext.restore();
}
const formOpt = {
index: i,
total: list.length,
data: dataURL,
x: tile.xPos,
y: tile.yPos,
width: tile.curWidth,
height: tile.curHeight
};
await window.fetch('/exporter/blit', {
method: 'POST',
cache: 'no-cache',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
...formOpt
})
})
return new Promise(resolve => setTimeout(resolve));
}, Promise.resolve());
await window.tileRenderEnd();
}
}
`;
async function startExport(opt) {
const dir = opt.dir;
const args = puppeteer
.defaultArgs()
.filter(
arg =>
arg !== "--disable-gpu" && arg !== "about:blank" && arg !== "--headless"
);
const additionalArgs = `-–enable-gpu-rasterization
--force-gpu-rasterization
--enable-native-gpu-memory-buffers
--enable-oop-rasterization
--ignore-gpu-blacklist
--use-skia-deferred-display-list
--enable-surfaces-for-videos
-–enable-zero-copy
--enable-fast-unload`.split("\n");
args.push(...additionalArgs);
args.push("--headless");
// args.push("--disable-gpu");
args.push("--canvas-msaa-sample-count=4");
// args.push("--use-gl=desktop");
args.push("about:blank");
// console.log(`args: ${args.join("\n")}`);
const browser = await puppeteer.launch({
ignoreDefaultArgs: true,
args
});
const page = (await browser.pages())[0];
// const page = await browser.newPage();
page.on("console", consoleObj => console.log(consoleObj.text()));
page.on("error", err => {
console.log("error happened at the page: ", err.message);
});
page.on("pageerror", pageerr => {
console.log("pageerror occurred: ", pageerr.message);
});
function toArrayBuffer(buf) {
var ab = new ArrayBuffer(buf.length);
var view = new Uint8Array(ab);
for (var i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
return ab;
}
let buffer;
let bufferSize;
let filename;
await page.setRequestInterception(true);
let spinner;
let rendering = true;
const queue = [];
let interval = setInterval(async () => {
if (queue.length === 0 && !rendering) {
clearInterval(interval);
return;
}
if (queue.length === 0) {
return;
}
const next = queue.shift();
if (next == null) {
spinner.text = "Encoding PNG...";
spinner.render();
await new Promise(resolve => setTimeout(resolve, 1));
const buf = fastPng.encode({
...bufferSize,
data: buffer
});
spinner.text = "Writing file...";
spinner.render();
await new Promise(resolve => setTimeout(resolve, 1));
fs.writeFile(filename, buf, async err => {
if (err) throw err;
spinner.succeed("Finished writing: " + filename);
await browser.close();
});
} else {
const image = JSON.parse(next);
const curBuf = Buffer.from(
image.data.slice("data:image/png;base64,".length),
"base64"
);
const decoded = fastPng.decode(curBuf);
spinner.text = `Drawing Tile ${image.index + 1} / ${image.total}`;
// console.log("Blitting", image.index + 1, image.total);
blit(
decoded.data,
image.x,
image.y,
decoded.width,
decoded.height,
buffer,
bufferSize.width,
bufferSize.height
);
}
}, 0);
page.on("request", async request => {
if (request.url().endsWith("/exporter/blit")) {
// none of these work (it prints either undefined or empty value)
queue.push(request.postData());
request.respond({
status: 200
});
} else {
request.continue();
}
});
await page.exposeFunction(
"exporter_convertLength",
(value, fromUnit, toUnit, opt) => {
return convertLength(value, fromUnit, toUnit, opt);
}
);
await page.exposeFunction(
"tileRenderStart",
async (width, height, opt = {}) => {
spinner = ora("Loading...").start();
buffer = new Uint8ClampedArray(width * height * 4);
bufferSize = { width, height };
filename =
[opt.prefix || "", getTimeStamp(), opt.suffix || ""].join("-") + ".png";
}
);
await page.exposeFunction(
"tileRenderBlit",
async (data, x, y, tileWidth, tileHeight, width, height) => {
blit(
new Uint8ClampedArray(data),
x,
y,
tileWidth,
tileHeight,
buffer,
bufferSize.width,
bufferSize.height
);
}
);
await page.exposeFunction("tileRenderEnd", async () => {
rendering = false;
queue.push(null);
});
await page.evaluateOnNewDocument(bootup);
try {
await page.goto("http://localhost:9966/", {
waitUntil: "load"
});
} catch (err) {
console.error(err);
console.error(
`Error going to route ${combinedUrl} in the directory ${path.relative(
process.cwd(),
dir
)}`
);
}
}
function getTimeStamp() {
const dateFormatStr = `yyyy.mm.dd-HH.MM.ss`;
return dateformat(new Date(), dateFormatStr);
}
function blit(
tileBuffer,
tileX,
tileY,
tileWidth,
tileHeight,
atlasBuffer,
atlasWidth,
atlasHeight
) {
// for each row in the tile buffer, blit that row
for (let row = 0; row < tileHeight; row++) {
const atlasY = tileY + row;
const atlasX = tileX;
// if (atlasY < 0 || atlasX < 0) continue;
if (atlasY >= atlasHeight || atlasX >= atlasWidth) break;
const atlasXPixels = Math.min(tileWidth, atlasWidth - atlasX);
const tileStartIndex = row * tileWidth;
const tileEndIndex = tileStartIndex + atlasXPixels;
const rowPixels = tileBuffer.subarray(tileStartIndex * 4, tileEndIndex * 4);
const atlasStartIndex = atlasX + atlasY * atlasWidth;
atlasBuffer.set(rowPixels, atlasStartIndex * 4);
}
}
(async () => {
await startExport({
dir: process.cwd()
});
})();
const canvasSketch = require("canvas-sketch");
const Random = require("canvas-sketch-util/random");
const risoColors = require("riso-colors");
const paperColors = require("paper-colors");
Random.setSeed("1234");
const settings = {
suffix: Random.getSeed(),
dimensions: "A0",
units: "in",
pixelsPerInch: 300,
scaleToView: true
};
const sketch = ({ width, height }) => {
const backgroundColor = Random.pick(paperColors).hex;
const circles = [];
for (let i = 0; i < 500; i++) {
const [u, v] = Random.insideCircle(width);
const x = u + width / 2;
const y = v + height / 2;
const radius = Math.abs(Random.gaussian()) * width * 0.05;
const color = Random.pick(risoColors).hex;
circles.push({
x,
y,
radius,
color
});
}
return ({ context }) => {
context.fillStyle = backgroundColor;
context.fillRect(0, 0, width, height);
for (let i = 0; i < circles.length; i++) {
const { x, y, radius, color } = circles[i];
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2);
context.fillStyle = color;
context.fill();
}
};
};
(async () => {
const manager = await canvasSketch(sketch, settings);
if (window.exporter) window.exporter(manager);
})();
// Ensure ThreeJS is in global scope for the 'examples/'
global.THREE = require("three");
// Include any additional ThreeJS examples below
require("three/examples/js/controls/OrbitControls");
const canvasSketch = require("canvas-sketch");
const Random = require("canvas-sketch-util/random");
const risoColors = require("riso-colors");
const paperColors = require("paper-colors");
const packSpheres = require("pack-spheres");
Random.setSeed("1234");
const settings = {
// Make the loop animated
suffix: Random.getSeed(),
animate: false,
dimensions: "A0",
scaleToView: true,
pixelsPerInch: 300,
// Get a WebGL canvas rather than 2D
context: "webgl"
};
const sketch = ({ context }) => {
// Create a renderer
const renderer = new THREE.WebGLRenderer({
canvas: context.canvas
});
// WebGL background color
renderer.setClearColor(Random.pick(paperColors).hex, 1);
// Setup a camera
const camera = new THREE.PerspectiveCamera(50, 1, 0.01, 100);
camera.position.set(0, 0, -4);
camera.lookAt(new THREE.Vector3());
// Setup camera controller
const controls = new THREE.OrbitControls(camera, context.canvas);
// Setup your scene
const scene = new THREE.Scene();
// Setup a geometry
const geometry = new THREE.SphereGeometry(1, 32, 16);
const backgroundColor = Random.pick(paperColors).hex;
packSpheres({
sample() {
return Random.insideSphere();
},
random: Random.value
}).forEach(sphere => {
const color = Random.pick(risoColors).hex;
const material = new THREE.MeshBasicMaterial({
color
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.fromArray(sphere.position);
mesh.scale.setScalar(sphere.radius);
scene.add(mesh);
});
const light = new THREE.PointLight("white", 1);
light.position.set(0, 0, -5);
scene.add(light);
// draw each frame
return {
// Handle resize events here
resize({ pixelRatio, viewportWidth, viewportHeight }) {
renderer.setPixelRatio(pixelRatio);
renderer.setSize(viewportWidth, viewportHeight, false);
camera.aspect = viewportWidth / viewportHeight;
camera.updateProjectionMatrix();
},
// Update & render your scene here
render({ time, region, pixelRatio, viewportWidth, viewportHeight }) {
if (region) {
renderer.setPixelRatio(1);
renderer.setSize(region.width, region.height, false);
camera.aspect = region.width / region.height;
camera.setViewOffset(
viewportWidth,
viewportHeight,
region.x,
region.y,
region.width,
region.height
);
camera.updateProjectionMatrix();
}
controls.update();
renderer.render(scene, camera);
},
// Dispose of events & renderer for cleaner hot-reloading
unload() {
controls.dispose();
renderer.dispose();
}
};
};
(async () => {
const manager = await canvasSketch(sketch, settings);
if (window.exporter) window.exporter(manager);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment