pixelate / index.html
n-Arno's picture
Update index.html
21cca8e verified
<!doctype html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide.js"></script>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
<style>
body {
grid-template-columns: none !important;
}
.editor {
display: flex;
width: 100vw;
height: 100vh;
}
.toolbar {
width: 500px;
}
.toolbar-item {
padding: 15px;
display: inline-block;
}
.tool-label {
display: block;
margin-bottom: 5px;
font-size: 14px;
font-weight: bold;
}
.tool-input {
width: 100%;
}
.image-area {
flex-grow: 1;
padding: 20px;
}
#canvas {
max-width: 100%;
max-height: 100%;
}
</style>
</head>
<body>
<div class="editor">
<div class="toolbar">
<div class="toolbar-item">
<input type="file" id="imageFileInput">
</div>
<div class="toolbar-item">
<label class="tool-label" for="scale">Scale (4)</label>
<input class="tool-input" type="range" id="scale" min="2" max="16" value="4" step="2" oninput="this.previousElementSibling.innerHTML = 'Scale (' + this.value + ')'">
</div>
<div class="toolbar-item">
<label class="tool-label" for="colors">Colors (16)</label>
<input class="tool-input" type="range" id="colors" min="4" max="256" value="16" step="2" oninput="this.previousElementSibling.innerHTML = 'Colors (' + this.value + ')'">
</div>
<br />
<div class="toolbar-item">
<label for="quant">Quantization method:</label><!-- https://pillow.readthedocs.io/en/stable/_modules/PIL/Image.html#Quantize -->
<select name="quant" id="quant">
<option value="3">LibImageQuant</option>
<option value="0">MedianCut</option>
<option value="2">FastOctree</option>
<option value="1">MaxCoverage</option>
</select>
</div>
<div class="toolbar-item">
<input type="checkbox" id="dither" name="dither" checked />
<label for="dither">Enable Dithering</label>
</div>
<div class="toolbar-item">
<input type="checkbox" id="kmeans" name="kmeans" />
<label for="kmeans">Enable K-Means</label>
</div>
<div class="toolbar-item">
<input type="checkbox" id="rgb555" name="rgb555" checked />
<label for="rgb555">Apply RGB555</label>
</div>
<div class="toolbar-item">
<input type="checkbox" id="snescrop" name="snescrop" />
<label for="snescrop">Crop to SNES size</label>
</div>
<div class="toolbar-item">
<input type="checkbox" id="rescale" name="rescale" checked />
<label for="rescale">Rescale post-processing</label>
</div>
<div class="toolbar-item">
<button onclick="pixelize()">Run</button>
</div>
<div class="toolbar-item">
<button id="reloadBtn" onclick="loadImage()" disabled>Reload original</button>
</div>
<div class="toolbar-item">
<textarea id="consoleOutput" style="width: 465px;" rows="20" disabled></textarea>
</div>
</div>
<div class="image-area">
<canvas id="canvas" height="500" width="500"></canvas>
</div>
</div>
</body>
<script>
const fileInput = document.querySelector("#imageFileInput");
const canvas = document.querySelector("#canvas");
const canvasCtx = canvas.getContext("2d");
const consoleOutput = document.getElementById("consoleOutput");
let image = null;
function loadImage() {
canvas.width = image.width;
canvas.height = image.height;
canvasCtx.drawImage(image, 0, 0);
}
fileInput.addEventListener("change", () => {
document.getElementById("reloadBtn").disabled = false;
image = new Image();
image.addEventListener("load", () => {
loadImage();
});
image.src = URL.createObjectURL(fileInput.files[0]);
});
function addToOutput(s) {
consoleOutput.value += s + "\n";
}
async function main() {
let pyodide = await loadPyodide();
await pyodide.loadPackage("numpy");
await pyodide.loadPackage("./pillow-10.2.0-cp312-cp312-pyodide_2024_0_wasm32.whl");
return pyodide;
}
let pyodideReadyPromise = main();
async function pixelize() {
if (!image) return;
consoleOutput.value = "";
let pyodide = await pyodideReadyPromise;
try {
pyodide.runPython(`
import numpy as np
from PIL import Image
from js import canvas, scale, colors, quant, dither, kmeans, rgb555, snescrop, rescale, addToOutput, ImageData
from pyodide.ffi import create_proxy
# Access to canvas content
canvasCtx = canvas.getContext("2d")
# SNES helpers
toRGB555 = lambda v: v >> 3 << 3
def apply(arr, fn):
vfn = np.vectorize(fn,otypes=[arr.dtype])
return vfn(arr.flatten()).reshape(arr.shape)
def snes_crop(image):
width, height = image.size
new_width, new_height = 256, 224
left = max([0, (width - new_width)/2])
top = max([0, (height - new_height)/2])
right = min([width, (width + new_width)/2])
bottom = min([height, (height + new_height)/2])
return image.crop((left, top, right, bottom))
addToOutput("Starting...")
# Read canvas ImageData as PIL.Image
im = Image.fromarray(np.array(canvasCtx.getImageData(0,0,canvas.width,canvas.height,{"pixelFormat":"rgba-unorm8"}).data.to_py()).reshape(canvas.height,canvas.width,4),mode='RGBA')
w, h = im.width, im.height
addToOutput(f"Got image of size ({w},{h})")
# Stupid pixel by resize
s = int(scale.value)
w, h = int(w/s), int(h/s)
im = im.resize((w,h))
addToOutput(f"Resized by scale {s} to ({w},{h})")
# Reduce or quantize colors
dither_method = Image.Dither.FLOYDSTEINBERG if dither.checked else Image.Dither.NONE
kmeans_colors = int(colors.value) if kmeans.checked else 0
im = im.convert('RGB').quantize(colors=int(colors.value), method=int(quant.value), dither=dither_method, kmeans=kmeans_colors).convert('RGBA')
addToOutput(f"Quantized to {colors.value} colors")
# Apply RGB555
if rgb555.checked:
im = Image.fromarray(apply(np.asarray(im.convert('RGB'),dtype='uint8'),toRGB555),'RGB').convert('RGBA')
addToOutput(f"Applied RGB555")
# Apply SNES crop
if snescrop.checked:
im = snes_crop(im)
w, h = im.width, im.height
addToOutput(f"Cropped to ({w},{h})")
# Rescale post-processing
if rescale.checked:
im = im.resize((w*s,h*s))
w, h = im.width, im.height
addToOutput(f"Rescaled to ({w},{h})")
# Convert back to ImageData
im = np.asarray(im,dtype='uint8').tobytes()
pixels_proxy = create_proxy(im)
pixels_buf = pixels_proxy.getBuffer("u8clamped")
img_data = ImageData.new(pixels_buf.data, w, h)
canvas.width = w
canvas.height = h
canvasCtx.putImageData(img_data, 0, 0)
pixels_proxy.destroy()
pixels_buf.release()
addToOutput("Done!")
`);
} catch (err) {
addToOutput(err);
}
}
</script>
</html>