commit:a84018384cbb7b120731ab1499b4ca44f323bc49
author:Chip Black
committer:Chip Black
date:Sun Oct 18 18:13:36 2015 -0500
parents:
Initial commit
diff --git a/.gitignore b/.gitignore
line changes: +2/-0
index 0000000..1eae0cf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+dist/
+node_modules/

diff --git a/Makefile b/Makefile
line changes: +10/-0
index 0000000..d1b4ee0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,10 @@
+all: dist/index.html dist/app.js dist/style.css
+
+dist/index.html: src/index.html
+	cp $< $@
+
+dist/style.css: src/style.css
+	cp $< $@
+
+dist/app.js: src/*.js
+	node_modules/.bin/webpack

diff --git a/package.json b/package.json
line changes: +11/-0
index 0000000..be1b33b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,11 @@
+{
+  "name": "Niven",
+  "description": "Professor Niven, 4-bit image editor for the IBM PC",
+  "version": "0.1.0",
+  "devDependencies": {
+    "babel-core": "^5.8.25",
+    "babel-loader": "^5.3.2",
+    "style-loader": "^0.12.4",
+    "webpack": "^1.12.2"
+  }
+}

diff --git a/src/Controller.js b/src/Controller.js
line changes: +5/-0
index 0000000..e369d3f
--- /dev/null
+++ b/src/Controller.js
@@ -0,0 +1,5 @@
+export default class Controller {
+    constructor(appState) {
+        this.state = appState;
+    }
+}

diff --git a/src/Fill.js b/src/Fill.js
line changes: +45/-0
index 0000000..6dff74f
--- /dev/null
+++ b/src/Fill.js
@@ -0,0 +1,45 @@
+import Controller from './Controller';
+
+export default class Fill extends Controller {
+    constructor(appState) {
+        super(appState);
+    }
+
+    // Flood fill is actually a lot harder than it looks
+    fill(x, y, targetColor) {
+        let left = x, right = x;
+        while (this.state.pixelEditor.getPixel(left, y) == targetColor) {
+            left--;
+        }
+        left++;
+        while (this.state.pixelEditor.getPixel(right, y) == targetColor) {
+            right++;
+        }
+        right--;
+
+        this.state.pixelEditor.setHLine(left, y, right - left + 1);
+        //this.state.pixelEditor.redraw(left, y, right - left + 1, 1);
+        for (let i = left; i <= right; i++) {
+            if (this.state.pixelEditor.getPixel(i, y - 1) == targetColor) {
+                this.fill(i, y - 1, targetColor);
+            }
+            if (this.state.pixelEditor.getPixel(i, y + 1) == targetColor) {
+                this.fill(i, y + 1, targetColor);
+            }
+        }
+    }
+
+    down(x, y) {
+        let targetColor = this.state.pixelEditor.getPixel(x, y);
+        if (targetColor == this.state.palette.color) {
+            // Filling a color on top of itself is a no-op
+            return;
+        }
+        this.fill(x, y, targetColor);
+        // TODO: Calculate a bounding box as we draw and only redraw that
+        this.state.pixelEditor.redraw();
+        this.state.pixelEditor.refresh();
+    }
+    move(x, y) { }
+    up(x, y) { }
+}

diff --git a/src/Line.js b/src/Line.js
line changes: +26/-0
index 0000000..c655c78
--- /dev/null
+++ b/src/Line.js
@@ -0,0 +1,26 @@
+import Controller from './Controller';
+
+export default class Line extends Controller {
+    constructor(appState) {
+        super(appState);
+        this.begin = null;
+    }
+    down(x, y) {
+        this.begin = {x, y};
+        this.state.pixelEditor.pushState();
+        this.state.pixelEditor.drawPixel(x, y);
+    }
+    move(x, y) {
+        if (this.begin) {
+            this.state.pixelEditor.resetState();
+            this.state.pixelEditor.setLine(this.begin.x, this.begin.y, x, y);
+            this.state.pixelEditor.refresh();
+            this.state.pixelEditor.redraw();
+        }
+    }
+    up(x, y) {
+        this.state.pixelEditor.popState();
+        this.state.pixelEditor.drawLine(this.begin.x, this.begin.y, x, y);
+        this.begin = null;
+    }
+}

diff --git a/src/Palette.js b/src/Palette.js
line changes: +86/-0
index 0000000..670faf2
--- /dev/null
+++ b/src/Palette.js
@@ -0,0 +1,86 @@
+import Controller from './Controller.js';
+
+var palettes = [
+    [   // Palette 0 - VGA
+        [0x00, 0x00, 0x00], // black
+        [0x00, 0x00, 0xAA], // blue
+        [0x00, 0xAA, 0x00], // green
+        [0x00, 0xAA, 0xAA], // cyan
+        [0xAA, 0x00, 0x00], // red
+        [0xAA, 0x00, 0xAA], // magenta
+        [0xAA, 0x55, 0x00], // brown
+        [0xAA, 0xAA, 0xAA], // light gray
+        [0x55, 0x55, 0x55], // dark gray
+        [0x55, 0x55, 0xFF], // bright blue
+        [0x55, 0xFF, 0x55], // bright green
+        [0x55, 0xFF, 0xFF], // bright cyan
+        [0xFF, 0x55, 0x55], // bright red
+        [0xFF, 0x55, 0xFF], // bright magenta
+        [0xFF, 0xFF, 0x55], // bright yellow
+        [0xFF, 0xFF, 0xFF], // white
+    ],
+    [   // Palette 1 - Perceptual RGB
+        [0x1F, 0x1F, 0x1F],
+        [0x5D, 0x5D, 0x5D],
+        [0x8C, 0x8C, 0x8C],
+        [0xD9, 0xD9, 0xD9],
+        [0x62, 0x00, 0x00],
+        [0x80, 0x00, 0x00],
+        [0xB0, 0x00, 0x00],
+        [0xDE, 0x00, 0x00],
+        [0x00, 0x54, 0x00],
+        [0x00, 0x73, 0x00],
+        [0x00, 0xA6, 0x00],
+        [0x00, 0xD4, 0x00],
+        [0x00, 0x00, 0x8C],
+        [0x00, 0x00, 0xB6],
+        [0x00, 0x00, 0xDC],
+        [0x28, 0x28, 0xFF],
+    ],
+]
+
+export default class Palette extends Controller {
+    constructor(appState) {
+        super(appState);
+        this.node = document.getElementById('palette');
+        this.swatches = [];
+        for (let i = 0; i < this.node.children.length; i++) {
+            let c = this.node.children[i];
+            if (c.className == 'swatch') {
+                let n = this.swatches.length;
+                c.addEventListener('mousedown', event => this.selectColor(n))
+                this.swatches.push(c);
+            }
+        }
+
+        this.selectPalette(0);
+        this.selectColor(0);
+    }
+
+    selectPalette(n) {
+        this.palette = n;
+        let p = palettes[n];
+        for (i = 0; i < 16; i++) {
+            this.swatches[i].style.backgroundColor = 'rgb(' + p[i][0] + ',' + p[i][1] + ',' + p[i][2] + ')';
+        }
+    }
+    nextPalette() {
+        this.selectPalette((this.palette + 1) % palettes.length);
+    }
+    prevPalette() {
+        let newPalette = this.palette - 1;
+        if (newPalette < 0)
+            newPalette = palettes.length - 1;
+        this.selectPalette(newPalette);
+    }
+    selectColor(n) {
+        if (this.color != undefined)
+            this.swatches[this.color].className = 'swatch';
+        this.color = n;
+        this.swatches[this.color].className = 'swatch selected';
+    }
+    getCSSColor(i = this.color) {
+        let c = palettes[this.palette][i];
+        return 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')';
+    }
+}

diff --git a/src/Pen.js b/src/Pen.js
line changes: +21/-0
index 0000000..9a3bb96
--- /dev/null
+++ b/src/Pen.js
@@ -0,0 +1,21 @@
+import Controller from './Controller';
+
+export default class Pen extends Controller {
+    constructor(appState) {
+        super(appState);
+        this.penIsDown = false;
+    }
+
+    down(x, y) {
+        this.penIsDown = true;
+        this.state.pixelEditor.drawPixel(x, y);
+    }
+    move(x, y) {
+        if (this.penIsDown) {
+            this.state.pixelEditor.drawPixel(x, y);
+        }
+    }
+    up(x, y) {
+        this.penIsDown = false;
+    }
+}

diff --git a/src/PixelEditor.js b/src/PixelEditor.js
line changes: +244/-0
index 0000000..d433d8e
--- /dev/null
+++ b/src/PixelEditor.js
@@ -0,0 +1,244 @@
+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.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);
+    }
+    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);
+        this.state.toolbox.getTool().move(x, y);
+    }
+    penUp(event) {
+        let { x, y } = this.getPixelCoords(event);
+        this.state.toolbox.getTool().up(x, y);
+    }
+}

diff --git a/src/Rect.js b/src/Rect.js
line changes: +44/-0
index 0000000..ef69b37
--- /dev/null
+++ b/src/Rect.js
@@ -0,0 +1,44 @@
+import Controller from './Controller';
+
+export default class Rect extends Controller {
+    constructor(appState) {
+        super(appState);
+        this.begin = null;
+    }
+    down(x, y) {
+        this.begin = {x, y};
+        this.state.pixelEditor.pushState();
+        this.state.pixelEditor.drawPixel(x, y);
+    }
+    move(x, y) {
+        if (this.begin) {
+            const x0 = x > this.begin.x ? this.begin.x : x;
+            const y0 = y > this.begin.y ? this.begin.y : y;
+            const w = Math.abs(x - this.begin.x) + 1;
+            const h = Math.abs(y - this.begin.y) + 1;
+
+            this.state.pixelEditor.resetState();
+            this.state.pixelEditor.setHLine(x0, this.begin.y, w);
+            this.state.pixelEditor.setHLine(x0, y, w);
+            this.state.pixelEditor.setVLine(this.begin.x, y0 + 1, h - 2);
+            this.state.pixelEditor.setVLine(x, y0 + 1, h - 2);
+            this.state.pixelEditor.refresh();
+            this.state.pixelEditor.redraw();
+        }
+    }
+    up(x, y) {
+        const x0 = x > this.begin.x ? this.begin.x : x;
+        const y0 = y > this.begin.y ? this.begin.y : y;
+        const w = Math.abs(x - this.begin.x) + 1;
+        const h = Math.abs(y - this.begin.y) + 1;
+
+        this.state.pixelEditor.popState();
+        this.state.pixelEditor.setHLine(x0, this.begin.y, w);
+        this.state.pixelEditor.setHLine(x0, y, w);
+        this.state.pixelEditor.setVLine(this.begin.x, y0 + 1, h - 2);
+        this.state.pixelEditor.setVLine(x, y0 + 1, h - 2);
+        this.state.pixelEditor.refresh();
+        this.state.pixelEditor.redraw();
+        this.begin = null;
+    }
+}

diff --git a/src/Snow.js b/src/Snow.js
line changes: +24/-0
index 0000000..f5e1595
--- /dev/null
+++ b/src/Snow.js
@@ -0,0 +1,24 @@
+import Controller from './Controller';
+
+export default class Snow extends Controller {
+    constructor(appState) {
+        super(appState);
+        this.penIsDown = false;
+    }
+    randomColor() {
+        return Math.floor(Math.random() * 16);
+    }
+
+    down(x, y) {
+        this.penIsDown = true;
+        this.state.pixelEditor.drawPixel(x, y, this.randomColor());
+    }
+    move(x, y) {
+        if (this.penIsDown) {
+            this.state.pixelEditor.drawPixel(x, y, this.randomColor());
+        }
+    }
+    up(x, y) {
+        this.penIsDown = false;
+    }
+}

diff --git a/src/Toolbox.js b/src/Toolbox.js
line changes: +63/-0
index 0000000..1df0597
--- /dev/null
+++ b/src/Toolbox.js
@@ -0,0 +1,63 @@
+import Controller from './Controller';
+import Pen from './Pen';
+import Fill from './Fill';
+import Line from './Line';
+import Rect from './Rect';
+import Snow from './Snow';
+
+export const TOOL_PEN = 0;
+export const TOOL_FILL = 1;
+export const TOOL_LINE = 2;
+export const TOOL_RECT = 3;
+export const TOOL_SNOW = 4;
+export const TOOL_COUNT = 5;
+
+// And also insert them into the window object for use in the UI
+window.TOOL_PEN = TOOL_PEN;
+window.TOOL_FILL = TOOL_FILL;
+window.TOOL_LINE = TOOL_LINE;
+window.TOOL_RECT = TOOL_RECT;
+window.TOOL_SNOW = TOOL_SNOW;
+
+export default class Toolbox extends Controller {
+    constructor(appState) {
+        super(appState);
+        
+        this.tools = [];
+        this.elems = [];
+
+        this.tools[TOOL_PEN] = new Pen(appState);
+        this.elems[TOOL_PEN] = document.getElementById('pen');
+
+        this.tools[TOOL_FILL] = new Fill(appState);
+        this.elems[TOOL_FILL] = document.getElementById('fill');
+
+        this.tools[TOOL_LINE] = new Line(appState);
+        this.elems[TOOL_LINE] = document.getElementById('line');
+
+        this.tools[TOOL_RECT] = new Rect(appState);
+        this.elems[TOOL_RECT] = document.getElementById('rect');
+
+        this.tools[TOOL_SNOW] = new Snow(appState);
+        this.elems[TOOL_SNOW] = document.getElementById('snow');
+
+        this.selectTool(TOOL_PEN);
+    }
+
+    selectTool(t) {
+        if (t < 0 || t >= TOOL_COUNT) {
+            throw Error("Invalid tool: " + t);
+        }
+        if (this.currentTool != undefined) {
+            this.elems[this.currentTool].className = null;
+        }
+        this.currentTool = t;
+        this.elems[this.currentTool].className = 'active';
+    }
+    getTool(t = this.currentTool) {
+        if (t < 0 || t >= TOOL_COUNT) {
+            throw Error("Invalid tool: " + t);
+        }
+        return this.tools[t];
+    }
+}

diff --git a/src/index.html b/src/index.html
line changes: +70/-0
index 0000000..c1e4e4b
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<link rel="stylesheet" href="style.css">
+<script type="text/javascript" src="app.js"></script>
+
+<div class="app">
+  <div class="editor">
+    <div class="tools1">
+      <div class="drawing-tools">
+        <button id="pen" onclick="selectTool(TOOL_PEN)">Pen</button>
+        <button id="fill" onclick="selectTool(TOOL_FILL)">Fill</button>
+        <button id="line" onclick="selectTool(TOOL_LINE)">Line</button>
+        <button id="rect" onclick="selectTool(TOOL_RECT)">Rect</button>
+        <button id="snow" onclick="selectTool(TOOL_SNOW)">Snow</button>
+      </div>
+      <div class="spacer"></div>
+      <div class="global-actions">
+        <button onclick="save()">Save</button>
+        <button>Load</button>
+        <button onclick="toggleGrid()" id="button-grid" class="active">Grid</button>
+        <div id="size-flyout" class="size-flyout">
+          <button onclick="selectSize(8)">8x8</button>
+          <button onclick="selectSize(16)">16x16</button>
+          <button onclick="selectSize(24)">24x24</button>
+          <button onclick="selectSize(32)">32x32</button>
+          <button onclick="selectSize(48)">48x48</button>
+          <button onclick="selectSize(64)">64x64</button>
+          <button onclick="selectSize(96)">96x96</button>
+          <button onclick="selectSize(128)">128x128</button>
+        </div>
+        <button onclick="showSizes()" id="button-size">Size</button>
+      </div>
+      <div class="preview" id="preview"></div>
+    </div>
+    <div class="viewport">
+      <canvas id="pixelEditor"></canvas>
+    </div>
+    <div class="tools2">
+      <div class="palette-switch">
+        <button onclick="prevPalette()">&larr;</button>
+        <button onclick="nextPalette()">&rarr;</button>
+      </div>
+      <div class="palette" id="palette">
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+        <div class="swatch"></div>
+      </div>
+      <div class="transparent-selector">
+        <button id="button-no-transparency" class="active">No Trans</button>
+        <button id="button-transparency">Trans</button>
+      </div>
+    </div>
+  </div>
+  <div class="debug">
+  </div>
+</div>
+
+<iframe id="saver"></iframe>

diff --git a/src/main.js b/src/main.js
line changes: +56/-0
index 0000000..8e85f23
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,56 @@
+import PixelEditor from './PixelEditor';
+import Palette from './Palette';
+import Toolbox from './Toolbox';
+import { TOOL_PEN, TOOL_FILL, TOOL_LINE } from './Toolbox';
+
+var appState = {};
+
+window.save = function save() {
+    let saver = document.getElementById('saver');
+    appState.pixelEditor.getFile().then(file => {
+        let uri = URL.createObjectURL(file);
+        saver.src = uri;
+        URL.revokeObjectURL(uri);
+    }).catch(e => {
+        if (e.message == "Object doesn't support property or method 'toBlob'" || e.message.search(/toBlob is not a function/)) {
+            alert("Sorry, save isn't supported in this browser");
+        } else {
+            alert("unknown error");
+            console.log(e);
+        }
+    });
+}
+
+window.nextPalette = function() {
+    appState.palette.nextPalette();
+    appState.pixelEditor.paletteChanged();
+}
+
+window.prevPalette = function() {
+    appState.palette.nextPalette();
+    appState.pixelEditor.paletteChanged();
+}
+
+window.toggleGrid = function() {
+    appState.pixelEditor.toggleGrid();
+}
+
+window.showSizes = function() {
+    document.getElementById('size-flyout').className = 'size-flyout active';
+}
+
+window.selectSize = function(n) {
+    appState.pixelEditor.setSize(n, n);
+    appState.pixelEditor.refresh();
+    document.getElementById('size-flyout').className = 'size-flyout';
+}
+
+window.selectTool = function(t) {
+    appState.toolbox.selectTool(t);
+}
+
+window.addEventListener('load', event => {
+    appState.palette = new Palette(appState);
+    appState.toolbox = new Toolbox(appState);
+    appState.pixelEditor = new PixelEditor(appState);
+});

diff --git a/src/style.css b/src/style.css
line changes: +151/-0
index 0000000..b82c467
--- /dev/null
+++ b/src/style.css
@@ -0,0 +1,151 @@
+body {
+	background-color: black;
+	color: white;
+}
+
+button {
+	border: 1px solid white;
+	background-color: black;
+	color: white;
+	display: block;
+	margin: 2px;
+}
+
+button.active {
+	background-color: white;
+	color: black;
+}
+
+.spacer {
+	flex: auto;
+}
+
+.app {
+	display: flex;
+	flex-flow: column;
+	position: absolute;
+	left: 0;
+	top: 0;
+	width: 100%;
+	height: 100%;
+}
+
+.global-actions {
+	display: flex;
+	flex-flow: row wrap;
+	flex: none;
+	width: 104px;
+}
+
+.global-actions button {
+	width: 48px;
+	height: 48px;
+}
+
+.size-flyout {
+	position: absolute;
+	display: none;
+}
+
+.size-flyout.active {
+	display: flex;
+	flex-flow: row;
+}
+
+.editor {
+	display: flex;
+	flex-flow: row;
+	flex: auto;
+}
+
+.tools1 {
+	display: flex;
+	flex-flow: column;
+}
+
+.drawing-tools {
+	display: flex;
+	flex-flow: row wrap;
+	width: 104px;
+}
+
+.drawing-tools button {
+	width: 48px;
+	height: 48px;
+}
+
+.preview {
+	display: flex;
+	justify-content: center;
+}
+
+.preview canvas {
+	margin: 8px;
+}
+
+.viewport {
+	flex: auto;
+	display: flex;
+	justify-content: center;
+}
+
+.viewport canvas#pixelEditor {
+	flex: 0 0 0;
+	margin: 4px;
+	border: 1px solid white;
+}
+
+.tools2 {
+	display: flex;
+	flex-flow: column;
+}
+
+.palette-switch {
+	display: flex;
+	flex-flow: row;
+}
+
+.palette-switch button {
+	width: 48px;
+	height: 48px;
+}
+
+.palette {
+	display: flex;
+	flex-flow: column wrap;
+	width: 104px;
+	flex: auto;
+}
+
+.palette .swatch {
+	width: 50%;
+	height: 12.5%;
+}
+
+.palette .swatch.selected {
+	border: 4px solid white;
+	width: calc(50% - 8px);
+	height: calc(12.5% - 8px);
+}
+
+.transparent-selector {
+	display: flex;
+	flex-flow: row;
+	width: 104px;
+}
+
+.transparent-selector button {
+	width: 48px;
+	height: 48px;
+}
+
+.debug {
+	display: none;
+	flex: none;
+}
+
+#saver {
+	width: 0;
+	height: 0;
+	display: none;
+}

diff --git a/webpack.config.js b/webpack.config.js
line changes: +20/-0
index 0000000..0045db1
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,20 @@
+module.exports = {
+    entry: "./src/main",
+    output: {
+        path: __dirname + '/dist',
+        filename: "app.js"
+    },
+    module: {
+        loaders: [
+            {
+                test: /\.js$/,
+                exclude: /node_modules/,
+                loader: "babel-loader",
+                query: {
+                    blacklist: "strict"
+                }
+            }
+        ]
+    },
+    devtool: 'source-map'
+};