switch from node-canvas to sharp
This commit is contained in:
parent
b6163fbb10
commit
e2badec793
|
@ -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');
|
||||||
|
|
|
@ -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 ];
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user