+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
+{
+ "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"
+ }
+}
+export default class Controller {
+ constructor(appState) {
+ this.state = appState;
+ }
+}
+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) { }
+}
+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;
+ }
+}
+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] + ')';
+ }
+}
+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;
+ }
+}
+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);
+ }
+}
+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;
+ }
+}
+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;
+ }
+}
+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];
+ }
+}
+<!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()">←</button>
+ <button onclick="nextPalette()">→</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>
+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);
+});
+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;
+}
+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'
+};