/src/PixelEditor.js
import Controller from './Controller';
export default class PixelEditor extends Controller {
constructor(appState) {
super(appState);
this.node = document.getElementById('pixelEditor');
this.node.addEventListener('mousedown', event => this.penDown(event));
this.node.addEventListener('mousemove', event => this.penMove(event));
this.node.addEventListener('mouseup', event => this.penUp(event));
this.lastMoveLocation = {x: null, y: null};
this.saveStack = [];
this.gridShowing = true;
this.setSize(16, 16);
this.refresh();
}
setSize(w, h) {
this.width = w;
this.height = h;
this.pixels = new Uint8Array(this.width * this.height);
this.node.width = w * 10;
this.node.height = h * 10;
this.ctx = this.node.getContext('2d');
this.ctx.imageSmoothingEnabled = false;
this.ctx.mozImageSmoothingEnabled = false;
this.ctx.webkitImageSmoothingEnabled = false;
this.ctx.msImageSmoothingEnabled = false;
this.grid = document.createElement('canvas');
this.grid.width = w * 10;
this.grid.height = h * 10;
let gridctx = this.grid.getContext('2d');
gridctx.strokeStyle = 'white';
for (let i = 0; i < this.width; i++) {
gridctx.beginPath();
gridctx.moveTo(i * 10, 0);
gridctx.lineTo(i * 10, h * 10);
gridctx.stroke();
gridctx.beginPath();
gridctx.moveTo(0, i * 10);
gridctx.lineTo(w * 10, i * 10);
gridctx.stroke();
}
let previewContainer = document.getElementById('preview');
if (this.preview) {
previewContainer.removeChild(this.preview);
}
this.preview = document.createElement('canvas');
this.preview.width = w;
this.preview.height = h;
this.pctx = this.preview.getContext('2d');
this.pctx.fillStyle = this.state.palette.getCSSColor(0);
this.pctx.fillRect(0, 0, w, h);
previewContainer.appendChild(this.preview);
}
loadFromImage(img) {
let validSize = false;
let sizes = [8, 16, 24, 32, 48, 64, 96, 128];
for (let i in sizes) {
if (img.width <= sizes[i]) {
this.setSize(sizes[i], sizes[i]);
validSize = true;
break;
}
}
if (!validSize) {
alert("Image too large. Must be less than 128x128");
return;
}
this.pctx.fillStyle = this.state.palette.getCSSColor(0);
this.pctx.fillRect(0, 0, this.width, this.height);
this.pctx.drawImage(img, 0, 0);
let imageData = this.pctx.getImageData(0, 0, this.width, this.height);
for (let i = 0; i < imageData.data.length; i += 4) {
let c = this.state.palette.findClosestColor(imageData.data.slice(i, i + 4));
this.pixels[i / 4] = c;
}
this.redraw();
this.refresh();
}
getFile() {
return new Promise((resolve, reject) => {
this.preview.toBlob(blob => {
let f = new File([blob], 'image.png', {type: 'application/octet-stream'});
resolve(f);
});
});
}
redraw(x, y, w, h) {
if (x == undefined) {
x = 0;
y = 0;
w = this.width;
h = this.height;
}
this.pctx.clearRect(x, y, w, h);
for (let iy = y; iy < y + h; iy++) {
for (let ix = x; ix < x + w; ix++) {
let c = this.pixels[iy * this.width + ix];
this.pctx.fillStyle = this.state.palette.getCSSColor(c);
this.pctx.fillRect(ix, iy, 1, 1);
}
}
}
refresh(x, y, w, h) {
if (x == undefined) {
x = 0;
y = 0;
w = this.width;
h = this.height;
}
this.ctx.drawImage(this.preview, x, y, w, h, x * 10, y * 10, w * 10, h * 10);
if (this.gridShowing) {
this.ctx.save();
this.ctx.globalCompositeOperation = 'exclusion';
this.ctx.drawImage(this.grid, x * 10, y * 10, w * 10, h * 10, x * 10, y * 10, w * 10, h * 10);
this.ctx.restore();
}
}
setPixel(x, y, v = this.state.palette.color) {
if (x < 0 || y < 0 || x >= this.width || y >= this.height)
return;
let addr = y * this.width + x;
this.pixels[addr] = v;
}
drawPixel(x, y, v = this.state.palette.color) {
this.setPixel(x, y, v);
this.redraw(x, y, 1, 1);
this.refresh(x, y, 1, 1);
}
setHLine(x, y, l, v = this.state.palette.color) {
if (x < 0 || y < 0 || x >= this.width || y >= this.height)
return;
let base = y * this.width + x;
let end = base + l;
if (end > this.pixels.length) {
end = this.pixels.length;
}
for (let i = base; i < end; i++) {
this.pixels[i] = v;
}
}
drawHLine(x, y, l, v = this.state.palette.color) {
this.setHLine(x, y, l, v);
this.redraw(x, y, l, 1);
this.refresh(x, y, l, 1);
}
setVLine(x, y, l, v = this.state.palette.color) {
if (x < 0 || y < 0 || x >= this.width || y >= this.height)
return;
let end = y + l;
if (end > this.height) {
end = this.height;
}
for (let i = y; i < end; i++) {
this.pixels[i * this.width + x] = v;
}
}
drawVLine(x, y, l, v = this.state.palette.color) {
this.setVLine(x, y, l, v);
this.redraw(x, y, l, 1);
this.refresh(x, y, l, 1);
}
setLine(x1, y1, x2, y2) {
if (x1 == undefined || x1 == undefined || x2 == undefined || y2 == undefined) {
throw Error("Undefined arguments to setLine");
}
if (x < 0 || y < 0 || x >= this.width || y >= this.height)
return;
// Props to Rosetta Code
// http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm
const dx = Math.abs(x2 - x1);
const dy = Math.abs(y2 - y1);
const sx = x1 < x2 ? 1 : -1;
const sy = y1 < y2 ? 1 : -1;
let [x, y] = [x1, y1];
let err = (dx > dy ? dx : -dy) / 2;
while (true) {
this.state.pixelEditor.setPixel(x, y);
if (x == x2 && y == y2)
break;
let e2 = err;
if (e2 > -dx) {
err -= dy;
x += sx;
}
if (e2 < dy) {
err += dx;
y += sy;
}
}
}
drawLine(x1, y1, x2, y2) {
const x = x1 < x2 ? x1 : x2;
const y = y1 < y2 ? y1 : y2;
const w = Math.abs(x2 - x1) + 1;
const h = Math.abs(y2 - y1) + 1;
this.setLine(x1, y1, x2, y2);
this.redraw(x, y, w, h);
this.refresh(x, y, w, h);
}
getPixel(x, y) {
if (x < 0 || y < 0 || x >= this.width || y >= this.height)
return -1;
let addr = y * this.width + x;
return this.pixels[addr];
}
paletteChanged() {
this.redraw();
this.refresh();
}
toggleGrid() {
this.gridShowing = !this.gridShowing;
this.refresh();
document.getElementById('button-grid').className = this.gridShowing ? 'active' : '';
}
pushState() {
this.saveStack.push({
pixels: new Uint8Array(this.pixels),
});
}
resetState() {
if (this.saveStack.length == 0) {
return;
}
let state = this.saveStack[this.saveStack.length - 1];
this.pixels = new Uint8Array(state.pixels);
}
popState() {
if (this.saveStack.length == 0) {
return;
}
let state = this.saveStack.pop();
this.pixels = state.pixels;
}
getPixelCoords(event) {
let rect = this.node.getBoundingClientRect();
let scale = rect.width / this.width;
return {
x: Math.floor(event.offsetX / scale),
y: Math.floor(event.offsetY / scale),
};
}
penDown(event) {
let { x, y } = this.getPixelCoords(event);
this.state.toolbox.getTool().down(x, y);
}
penMove(event) {
let { x, y } = this.getPixelCoords(event);
if (x != this.lastMoveLocation.x || y != this.lastMoveLocation.y) {
this.state.toolbox.getTool().move(x, y);
this.lastMoveLocation.x = x;
this.lastMoveLocation.y = y;
}
}
penUp(event) {
let { x, y } = this.getPixelCoords(event);
this.state.toolbox.getTool().up(x, y);
}
}