diff --git a/ppfun-bridge/src/pixelplanet/canvases.js b/ppfun-bridge/src/pixelplanet/canvases.js index ee914ca..9124585 100644 --- a/ppfun-bridge/src/pixelplanet/canvases.js +++ b/ppfun-bridge/src/pixelplanet/canvases.js @@ -21,7 +21,8 @@ async function fetchCanvasData() { const canvas = data.canvases[id]; canvas.id = id; 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); } console.log('Successfully fetched canvas data from pixelplanet'); diff --git a/ppfun-bridge/src/pixelplanet/constants.js b/ppfun-bridge/src/pixelplanet/constants.js index 013a115..1191b4e 100644 --- a/ppfun-bridge/src/pixelplanet/constants.js +++ b/ppfun-bridge/src/pixelplanet/constants.js @@ -3,3 +3,4 @@ export const HEIGHT = 600; export const MAX_SCALE = 40; // 52 in log2 export const TILE_SIZE = 256; export const TILE_ZOOM_LEVEL = 2; +export const BACKGROUND_CLR_RGB = [ 196, 196, 196 ]; diff --git a/ppfun-bridge/src/pixelplanet/loadChunk.js b/ppfun-bridge/src/pixelplanet/loadChunk.js index ba4ffaa..a597df9 100644 --- a/ppfun-bridge/src/pixelplanet/loadChunk.js +++ b/ppfun-bridge/src/pixelplanet/loadChunk.js @@ -1,12 +1,5 @@ import fetch from 'node-fetch'; -import nodeCanvas from 'canvas'; -const { - createCanvas, - loadImage, - createImageData, -} = nodeCanvas; - import { TILE_SIZE, } from './constants.js'; @@ -25,39 +18,42 @@ async function fetchBaseChunk( const arrayBuffer = await response.arrayBuffer(); if (arrayBuffer.byteLength) { const chunkArray = new Uint8Array(arrayBuffer); - const abgr = palette.buffer2ABGR(chunkArray); - const imageData = createImageData( - new Uint8ClampedArray(abgr.buffer), - TILE_SIZE, - TILE_SIZE, - ); - const ctx = tile.getContext('2d'); - ctx.putImageData(imageData, 0, 0); + return palette.buffer2RGB(chunkArray); } } + throw new Error(`Chunk faulty or not found`); } -async function fetchTile(canvasId, zoom, cx, cy, tile) { - const url = `https://pixelplanet.fun/tiles/${canvasId}/${zoom}/${cx}/${cy}.png`; +function fetchTile(canvasId, zoom, cx, cy, tile) { + const url = `https://pixelplanet.fun/tiles/${canvasId}/${zoom}/${cx}/${cy}.webp`; console.log(`Fetching ${url}`); - const image = await loadImage(url); - const ctx = tile.getContext('2d'); - ctx.drawImage(image, 0, 0); + return sharp(url).removeAlpha().raw().toBuffer(); } +/** + * 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 ( canvas, zoom, cx, cy, ) { - const tile = createCanvas(TILE_SIZE, TILE_SIZE); - const canvasId = canvas.id; - if (canvas.maxTiledZoom === zoom) { - await fetchBaseChunk(canvasId, canvas.palette, zoom, cx, cy, tile); - } else { - await fetchTile(canvasId, zoom, cx, cy, tile); + try { + if (canvas.maxTiledZoom === zoom) { + return await fetchBaseChunk(canvasId, canvas.palette, 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; } diff --git a/ppfun-bridge/src/pixelplanet/renderCanvas.js b/ppfun-bridge/src/pixelplanet/renderCanvas.js index 76889a6..2dc7a4c 100644 --- a/ppfun-bridge/src/pixelplanet/renderCanvas.js +++ b/ppfun-bridge/src/pixelplanet/renderCanvas.js @@ -1,14 +1,13 @@ -import nodeCanvas from 'canvas'; -const { createCanvas } = nodeCanvas; +import sharp from 'sharp'; import getChunk from './loadChunk.js'; - import { WIDTH, HEIGHT, MAX_SCALE, TILE_SIZE, TILE_ZOOM_LEVEL, + BACKGROUND_CLR_RGB, } from './constants.js'; function coordToChunk(z, canvasSize, tiledScale) { @@ -19,85 +18,170 @@ function chunkToCoord(z, canvasSize, tiledScale) { return Math.round(z * TILE_SIZE / tiledScale - canvasSize / 2); } -async function drawChunk( - ctx, xOff, yOff, - canvas, tiledZoom, xc, yc, +async function fillRect( + buffer, width, height, + x, y, + w, h, + r, g, b, ) { - try { - const chunk = await getChunk(canvas, tiledZoom, xc, yc); - ctx.drawImage(chunk, xOff,yOff); - } catch(e) { - console.log(`Chunk ${xc} / ${yc} - ${tiledZoom} is empty`); + if (x < 0) { + w += x; + x = 0; + } + 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( canvas, x, y, z, + width = WIDTH, + height = HEIGHT, ) { - const can = createCanvas(WIDTH, HEIGHT); - const ctx = can.getContext('2d'); const canvasSize = canvas.size; let scale = 2 ** (z / 10); scale = Math.max(scale, TILE_SIZE / canvas.size); scale = Math.min(scale, MAX_SCALE); + // 2 + 2 is 4 minus 1 that's 3, Quick Mafs! let tiledScale = (scale > 0.5) ? 0 - : Math.round(Math.log2(scale) / 2); + : Math.round(Math.log2(scale) / Math.log2(TILE_ZOOM_LEVEL)); tiledScale = TILE_ZOOM_LEVEL ** tiledScale; - const tiledZoom = canvas.maxTiledZoom + Math.log2(tiledScale) / 2; - - const relScale = scale / tiledScale; - ctx.scale(relScale, relScale); - ctx.fillStyle = canvas.palette.colors[0]; - ctx.imageSmoothingEnabled = false; - ctx.patternQuality = "nearest"; - ctx.antialias = 'none'; - ctx.fillRect(0, 0, WIDTH, HEIGHT); - - const tlX = Math.floor(x - WIDTH / 2 / scale); - const tlY = Math.floor(y - HEIGHT / 2 / scale); - const brX = Math.floor(x - 1 + WIDTH / 2 / scale); - const brY = Math.floor(y - 1 + HEIGHT / 2 / scale); - + const tiledZoom = canvas.maxTiledZoom + + Math.log2(tiledScale) / Math.log2(TILE_ZOOM_LEVEL); + const idRelScale = scale / tiledScale; + // split relative scale into vertical and horizontal scale that rounds to full integer + const unscaledWidth = Math.round(width / idRelScale); + const unscaledHeight = Math.round(height / idRelScale); + const relScaleW = width / unscaledWidth; + const relScaleH = height / unscaledHeight; + // canvas coordinates of corners + const tlX = Math.floor(x - unscaledWidth / 2 / relScaleW); + const tlY = Math.floor(y - unscaledHeight / 2 / relScaleH); + const brX = Math.floor(x - 1 + unscaledWidth / 2 / relScaleW); + const brY = Math.floor(y - 1 + unscaledHeight / 2 / relScaleH); + // chunk coordinates of chunks in corners const tlCX = coordToChunk(tlX, canvasSize, tiledScale); const tlCY = coordToChunk(tlY, canvasSize, tiledScale); const brCX = coordToChunk(brX, canvasSize, tiledScale); const brCY = coordToChunk(brY, canvasSize, tiledScale); - 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 = []; for (let xc = tlCX; xc <= brCX; xc += 1) { for (let yc = tlCY; yc <= brCY; yc += 1) { - const xOff = Math.round((chunkToCoord(xc, canvasSize, tiledScale) - tlX) * tiledScale); - const yOff = Math.round((chunkToCoord(yc, canvasSize, tiledScale) - tlY) * tiledScale); - + const xOff = chunkToCoord(xc, canvasSize, tiledScale) - tlX; + const yOff = chunkToCoord(yc, canvasSize, tiledScale) - tlY; if (xc < 0 || xc >= chunkMax || yc < 0 || yc >= chunkMax) { - ctx.clearRect(xOff, yOff, TILE_SIZE, TILE_SIZE); - continue; + // out of canvas bounds + promises.push( + fillRect( + pixelBuffer, unscaledWidth, unscaledHeight, + xOff, yOff, TILE_SIZE, TILE_SIZE, + ...BACKGROUND_CLR_RGB, + ), + ); } - promises.push( - drawChunk(ctx, xOff, yOff, canvas, tiledZoom, xc, yc), + drawChunk( + pixelBuffer, unscaledWidth, unscaledHeight, + xOff, yOff, + canvas, tiledZoom, xc, yc, + ), ); } } 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 { - image: imageBuffer, + image: imageBuffer.buffer, name: `ppfun-snap-${canvas.title}_${x}_${y}_${z}.png`, - type: memetype, - w: can.width, - h: can.height, + type: 'image/png', + w: width, + h: height, size: imageBuffer.size, } }