switch from node-canvas to sharp

This commit is contained in:
HF 2023-05-30 14:50:29 +02:00
parent b6163fbb10
commit e2badec793
4 changed files with 154 additions and 72 deletions

View File

@ -21,7 +21,8 @@ async function fetchCanvasData() {
const canvas = data.canvases[id]; const canvas = data.canvases[id];
canvas.id = id; canvas.id = id;
canvas.palette = new Palette(canvas.colors); canvas.palette = new Palette(canvas.colors);
canvas.maxTiledZoom = Math.log2(canvas.size / TILE_SIZE) / TILE_ZOOM_LEVEL * 2; canvas.maxTiledZoom = Math.log2(canvas.size / TILE_SIZE)
/ Math.log2(TILE_ZOOM_LEVEL);
canvases.set(canvas.ident, canvas); canvases.set(canvas.ident, canvas);
} }
console.log('Successfully fetched canvas data from pixelplanet'); console.log('Successfully fetched canvas data from pixelplanet');

View File

@ -3,3 +3,4 @@ export const HEIGHT = 600;
export const MAX_SCALE = 40; // 52 in log2 export const MAX_SCALE = 40; // 52 in log2
export const TILE_SIZE = 256; export const TILE_SIZE = 256;
export const TILE_ZOOM_LEVEL = 2; export const TILE_ZOOM_LEVEL = 2;
export const BACKGROUND_CLR_RGB = [ 196, 196, 196 ];

View File

@ -1,12 +1,5 @@
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import nodeCanvas from 'canvas';
const {
createCanvas,
loadImage,
createImageData,
} = nodeCanvas;
import { import {
TILE_SIZE, TILE_SIZE,
} from './constants.js'; } from './constants.js';
@ -25,39 +18,42 @@ async function fetchBaseChunk(
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer();
if (arrayBuffer.byteLength) { if (arrayBuffer.byteLength) {
const chunkArray = new Uint8Array(arrayBuffer); const chunkArray = new Uint8Array(arrayBuffer);
const abgr = palette.buffer2ABGR(chunkArray); return palette.buffer2RGB(chunkArray);
const imageData = createImageData(
new Uint8ClampedArray(abgr.buffer),
TILE_SIZE,
TILE_SIZE,
);
const ctx = tile.getContext('2d');
ctx.putImageData(imageData, 0, 0);
} }
} }
throw new Error(`Chunk faulty or not found`);
} }
async function fetchTile(canvasId, zoom, cx, cy, tile) { function fetchTile(canvasId, zoom, cx, cy, tile) {
const url = `https://pixelplanet.fun/tiles/${canvasId}/${zoom}/${cx}/${cy}.png`; const url = `https://pixelplanet.fun/tiles/${canvasId}/${zoom}/${cx}/${cy}.webp`;
console.log(`Fetching ${url}`); console.log(`Fetching ${url}`);
const image = await loadImage(url); return sharp(url).removeAlpha().raw().toBuffer();
const ctx = tile.getContext('2d');
ctx.drawImage(image, 0, 0);
} }
/**
* fetch a tile
* @param canvas canvas object
* @param zoom tile zoom level
* @param cx cavnas coord x
* @param cy cavnas coord y
* @return RGB Buffer of chunk (NOT padded)
*/
export default async function ( export default async function (
canvas, canvas,
zoom, zoom,
cx, cx,
cy, cy,
) { ) {
const tile = createCanvas(TILE_SIZE, TILE_SIZE);
const canvasId = canvas.id; const canvasId = canvas.id;
if (canvas.maxTiledZoom === zoom) { try {
await fetchBaseChunk(canvasId, canvas.palette, zoom, cx, cy, tile); if (canvas.maxTiledZoom === zoom) {
} else { return await fetchBaseChunk(canvasId, canvas.palette, zoom, cx, cy, tile);
await fetchTile(canvasId, zoom, cx, cy, tile); } else {
return await fetchTile(canvasId, zoom, cx, cy, tile);
}
} catch () {
console.log(`Chunk ${cx} / ${cy} - ${zoom} is empty of faulty`);
return Buffer.allocUnsafe(0);
} }
return tile; return tile;
} }

View File

@ -1,14 +1,13 @@
import nodeCanvas from 'canvas'; import sharp from 'sharp';
const { createCanvas } = nodeCanvas;
import getChunk from './loadChunk.js'; import getChunk from './loadChunk.js';
import { import {
WIDTH, WIDTH,
HEIGHT, HEIGHT,
MAX_SCALE, MAX_SCALE,
TILE_SIZE, TILE_SIZE,
TILE_ZOOM_LEVEL, TILE_ZOOM_LEVEL,
BACKGROUND_CLR_RGB,
} from './constants.js'; } from './constants.js';
function coordToChunk(z, canvasSize, tiledScale) { function coordToChunk(z, canvasSize, tiledScale) {
@ -19,85 +18,170 @@ function chunkToCoord(z, canvasSize, tiledScale) {
return Math.round(z * TILE_SIZE / tiledScale - canvasSize / 2); return Math.round(z * TILE_SIZE / tiledScale - canvasSize / 2);
} }
async function drawChunk( async function fillRect(
ctx, xOff, yOff, buffer, width, height,
canvas, tiledZoom, xc, yc, x, y,
w, h,
r, g, b,
) { ) {
try { if (x < 0) {
const chunk = await getChunk(canvas, tiledZoom, xc, yc); w += x;
ctx.drawImage(chunk, xOff,yOff); x = 0;
} catch(e) { }
console.log(`Chunk ${xc} / ${yc} - ${tiledZoom} is empty`); if (y < 0) {
h += y;
y = 0;
}
if (w > width) {
w = width;
}
if (h > height) {
h = height;
}
if (w < 0 || h < 0) {
// out of image bounds
return;
}
const rowMax = r + h;
for (let row = y; row < rowMax; row += 1) {
let pos = (row * width + x) * 3;
const max = pos + w * 3;
while (pos < max) {
buffer[pos++] = r;
buffer[pos++] = g;
buffer[pos++] = b;
}
} }
} }
async function drawChunk(
buffer, width, height,
xOff, yOff,
canvas, tiledZoom, xc, yc,
) {
const chunkBuffer = await getChunk(canvas, tiledZoom, xc, yc);
const [ emptyR, emptyG, emptyB ] = canvas.palette.rgb;
let row = Math.max(-yOff, 0);
const rowMax = Math.min(TILE_SIZE - yOff + height, TILE_SIZE);
const colMin = Math.max(-xOff, 0);
const colMax = Math.min(TILE_SIZE - xOff + width, TILE_SIZE);
if (colMax < 0 || rowMax < 0) {
// out of image bounds
return;
}
const cutWidth = colMax - colMin;
for (; row < rowMax; row += 1) {
let pos = (row * TILE_SIZE + colMin) * 3;
const max = pos + cutWidth * 3;
let ib = ((row + yOff) * width + colMin + xOff) * 3;
while (pos < max) {
if (pos < chunkBuffer.length) {
buffer[ib++] = chunkBuffer[pos++];
buffer[ib++] = chunkBuffer[pos++];
buffer[ib++] = chunkBuffer[pos++];
} else {
buffer[ib++] = emptyR;
buffer[ib++] = emptyG;
buffer[ib++] = emptyB;
}
}
}
}
/**
* Draw canvas section from given coordinates
* @param canvas canvas Data
* @param x x coordinates of center of wanted image
* @param y y coordinates of center of wanted image
* @param z wanted zoomlevel in log2 * 10
* @param width width of image (optional)
* @param height height of image (optional)
*/
export default async function renderCanvas( export default async function renderCanvas(
canvas, canvas,
x, x,
y, y,
z, z,
width = WIDTH,
height = HEIGHT,
) { ) {
const can = createCanvas(WIDTH, HEIGHT);
const ctx = can.getContext('2d');
const canvasSize = canvas.size; const canvasSize = canvas.size;
let scale = 2 ** (z / 10); let scale = 2 ** (z / 10);
scale = Math.max(scale, TILE_SIZE / canvas.size); scale = Math.max(scale, TILE_SIZE / canvas.size);
scale = Math.min(scale, MAX_SCALE); scale = Math.min(scale, MAX_SCALE);
// 2 + 2 is 4 minus 1 that's 3, Quick Mafs!
let tiledScale = (scale > 0.5) let tiledScale = (scale > 0.5)
? 0 ? 0
: Math.round(Math.log2(scale) / 2); : Math.round(Math.log2(scale) / Math.log2(TILE_ZOOM_LEVEL));
tiledScale = TILE_ZOOM_LEVEL ** tiledScale; tiledScale = TILE_ZOOM_LEVEL ** tiledScale;
const tiledZoom = canvas.maxTiledZoom + Math.log2(tiledScale) / 2; const tiledZoom = canvas.maxTiledZoom
+ Math.log2(tiledScale) / Math.log2(TILE_ZOOM_LEVEL);
const relScale = scale / tiledScale; const idRelScale = scale / tiledScale;
ctx.scale(relScale, relScale); // split relative scale into vertical and horizontal scale that rounds to full integer
ctx.fillStyle = canvas.palette.colors[0]; const unscaledWidth = Math.round(width / idRelScale);
ctx.imageSmoothingEnabled = false; const unscaledHeight = Math.round(height / idRelScale);
ctx.patternQuality = "nearest"; const relScaleW = width / unscaledWidth;
ctx.antialias = 'none'; const relScaleH = height / unscaledHeight;
ctx.fillRect(0, 0, WIDTH, HEIGHT); // canvas coordinates of corners
const tlX = Math.floor(x - unscaledWidth / 2 / relScaleW);
const tlX = Math.floor(x - WIDTH / 2 / scale); const tlY = Math.floor(y - unscaledHeight / 2 / relScaleH);
const tlY = Math.floor(y - HEIGHT / 2 / scale); const brX = Math.floor(x - 1 + unscaledWidth / 2 / relScaleW);
const brX = Math.floor(x - 1 + WIDTH / 2 / scale); const brY = Math.floor(y - 1 + unscaledHeight / 2 / relScaleH);
const brY = Math.floor(y - 1 + HEIGHT / 2 / scale); // chunk coordinates of chunks in corners
const tlCX = coordToChunk(tlX, canvasSize, tiledScale); const tlCX = coordToChunk(tlX, canvasSize, tiledScale);
const tlCY = coordToChunk(tlY, canvasSize, tiledScale); const tlCY = coordToChunk(tlY, canvasSize, tiledScale);
const brCX = coordToChunk(brX, canvasSize, tiledScale); const brCX = coordToChunk(brX, canvasSize, tiledScale);
const brCY = coordToChunk(brY, canvasSize, tiledScale); const brCY = coordToChunk(brY, canvasSize, tiledScale);
console.log(`Load chunks from ${tlCX} / ${tlCY} to ${brCX} / ${brCY}`); console.log(`Load chunks from ${tlCX} / ${tlCY} to ${brCX} / ${brCY}`);
const chunkMax = canvasSize / TILE_SIZE * tiledScale;
// create RGB buffer for unscalled pixels and load chunks into it
const pixelBuffer = Buffer.allocUnsafe(unscaledWidth * unscaledHeight * 3);
const chunkMax = canvas.size / TILE_SIZE * tiledScale;
const promises = []; const promises = [];
for (let xc = tlCX; xc <= brCX; xc += 1) { for (let xc = tlCX; xc <= brCX; xc += 1) {
for (let yc = tlCY; yc <= brCY; yc += 1) { for (let yc = tlCY; yc <= brCY; yc += 1) {
const xOff = Math.round((chunkToCoord(xc, canvasSize, tiledScale) - tlX) * tiledScale); const xOff = chunkToCoord(xc, canvasSize, tiledScale) - tlX;
const yOff = Math.round((chunkToCoord(yc, canvasSize, tiledScale) - tlY) * tiledScale); const yOff = chunkToCoord(yc, canvasSize, tiledScale) - tlY;
if (xc < 0 || xc >= chunkMax || yc < 0 || yc >= chunkMax) { if (xc < 0 || xc >= chunkMax || yc < 0 || yc >= chunkMax) {
ctx.clearRect(xOff, yOff, TILE_SIZE, TILE_SIZE); // out of canvas bounds
continue; promises.push(
fillRect(
pixelBuffer, unscaledWidth, unscaledHeight,
xOff, yOff, TILE_SIZE, TILE_SIZE,
...BACKGROUND_CLR_RGB,
),
);
} }
promises.push( promises.push(
drawChunk(ctx, xOff, yOff, canvas, tiledZoom, xc, yc), drawChunk(
pixelBuffer, unscaledWidth, unscaledHeight,
xOff, yOff,
canvas, tiledZoom, xc, yc,
),
); );
} }
} }
await Promise.all(promises); await Promise.all(promises);
// scale and convert to png
const imageBuffer = await sharp(pixelBuffer, {
raw: {
width: unscaledWidth,
height: unscaledHeight,
channels: 3,
},
})
.resize({ width, height })
.png()
.toBuffer();
const memetype = 'image/png';
const imageBuffer = can.toBuffer(memetype);
return { return {
image: imageBuffer, image: imageBuffer.buffer,
name: `ppfun-snap-${canvas.title}_${x}_${y}_${z}.png`, name: `ppfun-snap-${canvas.title}_${x}_${y}_${z}.png`,
type: memetype, type: 'image/png',
w: can.width, w: width,
h: can.height, h: height,
size: imageBuffer.size, size: imageBuffer.size,
} }
} }