diff --git a/index.html b/index.html
index 2c03651..0de74b5 100644
--- a/index.html
+++ b/index.html
@@ -1,13 +1,30 @@
-
+
BittMapp
+
+
+
+
-
+
+ Modes
+
+ File Options
+
+
diff --git a/src/bittmappeditor.ts b/src/bittmappeditor.ts
index dd82520..5bf81f3 100644
--- a/src/bittmappeditor.ts
+++ b/src/bittmappeditor.ts
@@ -13,6 +13,13 @@ enum MouseButton {
}
+enum Mode {
+ PENCIL = 0,
+ ERASER = 1,
+ SELECTION = 2
+}
+
+
class BittMappEditor {
@@ -33,7 +40,17 @@ class BittMappEditor {
private _mouseDown: boolean = false;
private _mouseButton: MouseButton;
+ private _selectionStartX: number = 0;
+ private _selectionStartY: number = 0;
+ private _selectionEndX: number = 0;
+ private _selectionEndY: number = 0;
+
+ private _editorMode: Mode = Mode.PENCIL;
+
+ private _downloadHelper: HTMLAnchorElement;
+
private _data: Uint8Array;
+ private _selection: Uint8Array;
constructor (options: IBittMappEditorConstructorOptions) {
@@ -85,6 +102,9 @@ class BittMappEditor {
this._context.scale(this._scale, this._scale);
+ this._data = new Uint8Array((this._width / 8) * this._height);
+ this._selection = new Uint8Array((this._width / 8) * this._height);
+
this.resize();
this.canvas.addEventListener("contextmenu", (event) => {
@@ -94,6 +114,12 @@ class BittMappEditor {
this.canvas.addEventListener("mousedown", (event) => {
this._mouseDown = true;
this._mouseButton = event.button as MouseButton;
+ this._selectionStartX = this._calculateXFromMouseCoords(event.offsetX);
+ this._selectionStartY = this._calculateYFromMouseCoords(event.offsetY);
+ this._selectionEndX = this._selectionStartX + 1;
+ this._selectionEndY = this._selectionStartY + 1;
+ // NK: Only wipe selection if Ctrl isn't pressed
+ this._selection = new Uint8Array((this._width / 8) * this._height);
this._handleMouseEvent(event, this._mouseButton);
});
@@ -107,31 +133,93 @@ class BittMappEditor {
this._mouseDown = false;
});
+ this._downloadHelper = document.createElement("a");
+ document.body.appendChild(this._downloadHelper);
+ (this._downloadHelper as any).style = "display: none";
+
+ }
+
+
+ public pencilMode () {
+ this._selection = new Uint8Array((this._width / 8) * this._height);
+ this._editorMode = Mode.PENCIL;
+ }
+
+
+ public eraserMode () {
+ this._selection = new Uint8Array((this._width / 8) * this._height);
+ this._editorMode = Mode.ERASER;
+ }
+
+
+ public selectionMode () {
+ this._editorMode = Mode.SELECTION;
}
public setPixel (x: number, y: number): void {
- const byte: number = ((y * (this._width / 8)) + Math.floor(x / 8));
- const mask: number = 1 << (x % 8);
+ const byte: number = this._calculateByteFromCoords(x, y);
+ // const byte: number = ((y * (this._width / 8)) + Math.floor(x / 8));
+ const mask: number = this._calculateByteMask(x);
this._data[byte] = this._data[byte] |= mask;
}
public unsetPixel (x: number, y: number): void {
- const byte: number = ((y * (this._width / 8)) + Math.floor(x / 8));
- const mask: number = 1 << (x % 8);
+ const byte: number = this._calculateByteFromCoords(x, y);
+ // const byte: number = ((y * (this._width / 8)) + Math.floor(x / 8));
+ const mask: number = this._calculateByteMask(x);
this._data[byte] = this._data[byte] &= ~mask;
}
+ public selectPixel (x: number, y: number): void {
+ const byte: number = this._calculateByteFromCoords(x, y);
+ // const byte: number = ((y * (this._width / 8)) + Math.floor(x / 8));
+ const mask: number = this._calculateByteMask(x);
+ this._selection[byte] = this._selection[byte] |= mask;
+ }
+
+
+ public deselectPixel (x: number, y: number): void {
+ const byte: number = this._calculateByteFromCoords(x, y);
+ // const byte: number = ((y * (this._width / 8)) + Math.floor(x / 8));
+ const mask: number = this._calculateByteMask(x);
+ this._selection[byte] = this._selection[byte] &= ~mask;
+ }
+
+
+ public deselectAll (): void {
+ this._selection = new Uint8Array((this._width / 8) * this._height);
+ }
+
+
public resize (width: number = this._width, height: number = this._height): void {
- this._data = new Uint8Array((width / 8) * height);
- this._pixelWidth = this.canvasWidth / this._width;
- this._pixelHeight = this.canvasHeight / this._height;
+ // TODO: Resize the data buffer
+ this._width = width;
+ this._height = height;
+ this._pixelWidth = this.canvasWidth / width;
+ this._pixelHeight = this.canvasHeight / height;
this._redraw();
}
+ public loadFromData (data: Uint8Array, width: number, height: number) {
+ this._data = data;
+ this.resize(width, height);
+ }
+
+
+ public saveToFile (filename: string) {
+ const blob: Blob = new Blob([this._data], {type: "octet/stream"});
+ const url: string = window.URL.createObjectURL(blob);
+ this._downloadHelper.href = url;
+ this._downloadHelper.download = filename;
+ this._downloadHelper.click();
+ window.URL.revokeObjectURL(url);
+ }
+
+
get height (): number {
return this._height;
}
@@ -152,15 +240,67 @@ class BittMappEditor {
}
+ private _calculateByteFromCoords (x: number, y: number): number {
+ return Math.floor(((y * this._width) + x) / 8);
+ }
+
+
+ private _calculateByteMask (x: number) {
+ return 1 << (x % 8);
+ }
+
+
+ private _calculateXFromMouseCoords (mouseX: number, round: any = Math.floor): number {
+ return round(mouseX / this._pixelWidth);
+ }
+
+
+ private _calculateYFromMouseCoords (mouseY: number, round: any = Math.floor): number {
+ return round(mouseY / this._pixelHeight);
+ }
+
+
private _handleMouseEvent (event: MouseEvent, button: number): void {
- const pixelX: number = Math.floor(event.offsetX / this._pixelWidth);
- const pixelY: number = Math.floor(event.offsetY / this._pixelHeight);
+ let mouseX: number = this._calculateXFromMouseCoords(event.offsetX);
+ let mouseY: number = this._calculateYFromMouseCoords(event.offsetY);
- if (button === MouseButton.LEFT) {
- this.setPixel(pixelX, pixelY);
- } else if (button === MouseButton.RIGHT) {
- this.unsetPixel(pixelX, pixelY);
+ switch (this._editorMode) {
+ case Mode.PENCIL:
+
+ if (button === MouseButton.LEFT) {
+ this.setPixel(mouseX, mouseY);
+ } else if (button === MouseButton.RIGHT) {
+ this.unsetPixel(mouseX, mouseY);
+ }
+
+ break;
+ case Mode.ERASER:
+
+ if (button === MouseButton.LEFT) {
+ this.unsetPixel(mouseX, mouseY);
+ } else if (button === MouseButton.RIGHT) {
+ this.setPixel(mouseX, mouseY);
+ }
+
+ break;
+ case Mode.SELECTION:
+
+ // Only do this if Ctrl isn't pressed
+ mouseX = this._calculateXFromMouseCoords(event.offsetX, Math.ceil);
+ mouseY = this._calculateYFromMouseCoords(event.offsetY, Math.ceil);
+
+ this._selection = new Uint8Array((this._width / 8) * this._height);
+ this._selectionEndX = mouseX;
+ this._selectionEndY = mouseY;
+
+ for (let x: number = this._selectionStartX; x < this._selectionEndX; x++) {
+ for (let y: number = this._selectionStartY; y < this._selectionEndY; y++) {
+ this.selectPixel(x, y);
+ }
+ }
+
+ break;
}
this._redraw();
@@ -168,9 +308,31 @@ class BittMappEditor {
}
+ private _isSelected (x: number, y: number): boolean {
+ if (x < 0 || y < 0) {
+ return false;
+ } else if (x > this._width - 1 || y > this._height - 1) {
+ return false;
+ }
+
+ const byte: number = this._calculateByteFromCoords(x, y);
+ const mask: number = this._calculateByteMask(x);
+
+ if ((this._selection[byte] & mask) >= 1) {
+ return true;
+ }
+
+ return false;
+
+ }
+
+
private _redraw (): void {
this._drawGrid();
this._drawPixels();
+ if (this._editorMode === Mode.SELECTION) {
+ this._drawSelection();
+ }
}
@@ -243,4 +405,52 @@ class BittMappEditor {
}
+ private _drawSelection (): void {
+
+ this._context.strokeStyle = "#FF0000";
+
+ for (let x: number = 0; x < this._width; x++) {
+ for (let y: number = 0; y < this._height; y++) {
+
+ const byte: number = this._calculateByteFromCoords(x, y);
+ const mask: number = this._calculateByteMask(x);
+
+ if (this._isSelected(x, y)) {
+
+ if (!this._isSelected(x - 1, y)) {
+ this._context.beginPath();
+ this._context.moveTo(x * this._pixelWidth, y * this._pixelHeight);
+ this._context.lineTo(x * this._pixelWidth, (y * this._pixelHeight) + this._pixelHeight);
+ this._context.stroke();
+ }
+
+ if (!this._isSelected(x, y - 1)) {
+ this._context.beginPath();
+ this._context.moveTo(x * this._pixelWidth, y * this._pixelHeight);
+ this._context.lineTo((x * this._pixelWidth) + this._pixelWidth, y * this._pixelHeight);
+ this._context.stroke();
+ }
+
+ if (!this._isSelected(x + 1, y)) {
+ this._context.beginPath();
+ this._context.moveTo((x * this._pixelWidth) + this._pixelWidth, y * this._pixelHeight);
+ this._context.lineTo((x * this._pixelWidth) + this._pixelWidth, (y * this._pixelHeight) + this._pixelHeight);
+ this._context.stroke();
+ }
+
+ if (!this._isSelected(x, y + 1)) {
+ this._context.beginPath();
+ this._context.moveTo(x * this._pixelWidth, (y * this._pixelHeight) + this._pixelHeight);
+ this._context.lineTo((x * this._pixelWidth) + this._pixelWidth, (y * this._pixelHeight) + this._pixelHeight);
+ this._context.stroke();
+ }
+
+ }
+
+ }
+ }
+
+ }
+
+
}
diff --git a/src/bmptools.ts b/src/bmptools.ts
new file mode 100644
index 0000000..547595f
--- /dev/null
+++ b/src/bmptools.ts
@@ -0,0 +1,151 @@
+interface IBMPHeaderData {
+ fileLength: number;
+ headerLength: number;
+ pixelDataOffset: number;
+ width: number;
+ height: number;
+ planes: number;
+ bitsPerPixel: number;
+ compressed: number;
+ imageSize: number;
+ numberColors: number;
+ importantColors: number;
+ bytesRead: number;
+}
+
+
+interface IBittMappData {
+
+ data: Uint8Array;
+ width: number;
+ height: number;
+
+}
+
+
+class BMPTools {
+
+
+ public static extractBitmap (inputData: Uint8Array): IBittMappData {
+
+ const headerData: IBMPHeaderData = BMPTools.extractHeaderData(inputData);
+ const pixelData: Uint8Array = inputData.slice(headerData.pixelDataOffset, headerData.fileLength);
+ const rowSize: number = (headerData.width - 1 - (headerData.width - 1) % 32 + 32) / 8;
+
+ // console.log(headerData);
+
+ const outputData: Uint8Array = new Uint8Array(Math.ceil((headerData.width * headerData.height) / 8));
+
+ if (headerData.height > 0) {
+ let bitCount: number = 0;
+ for (let y: number = headerData.height - 1; y >= 0; y--) {
+ const rowStart: number = rowSize * y;
+ for (let x: number = 0; x < headerData.width; x++) {
+ const inputByte: number = Math.floor(x / 8);
+ const outputByte: number = Math.floor((((headerData.height - 1 - y) * headerData.width) + x) / 8);
+ const inputMask: number = 1 << (x % 8);
+ const outputMask: number = 1 << (bitCount % 8);
+ const val: number = pixelData[rowStart + inputByte] & inputMask;
+ // console.log(val);
+ outputData[outputByte] = val + outputData[outputByte];
+ // console.log((rowStart + inputByte), inputMask, outputByte, outputMask);
+ bitCount++;
+ }
+ }
+ } /* else {
+ let bitCount: number = 0;
+ for (let y: number = 0; y < headerData.height; y++) {
+ const rowStart: number = rowSize * y;
+ console.log(pixelData[rowStart], pixelData[rowStart + 1], pixelData[rowStart + 2], pixelData[rowStart + 3]);
+ for (let x: number = 0; x < headerData.width; x++) {
+ const inputByte: number = Math.floor(x / 8);
+ const outputByte: number = Math.floor(((y * headerData.width) + x) / 8);
+ const inputMask: number = 1 << (x % 8);
+ const outputMask: number = 1 << (bitCount % 8);
+ let val: number = pixelData[rowStart + inputByte];
+ console.log(val);
+ //outputData[outputByte] = val |= outputMask;
+ console.log((rowStart + inputByte), inputMask, outputByte, outputMask);
+ bitCount++;
+ }
+ }
+ }*/
+
+ return {
+ data: outputData,
+ height: headerData.height,
+ width: headerData.width
+ };
+
+ }
+
+
+ public static extractHeaderData (inputData: Uint8Array): IBMPHeaderData {
+
+ const headerData = {} as any;
+
+ if (inputData.length < 54) {
+ throw new Error("Data not valid (Not long enough, must be at least 54 bytes)");
+ }
+
+ if (!(inputData[0] === 0x42 && inputData[1] === 0x4d)) {
+ throw new Error("Data not valid (No BM header)");
+ }
+
+ headerData.fileLength = Serialisers.readUint32(inputData, 2, true);
+ if (headerData.fileLength !== inputData.length) {
+ throw new Error("Data not valid (Length incorrect)");
+ }
+
+ headerData.pixelDataOffset = Serialisers.readUint32(inputData, 10, true);
+ headerData.headerLength = Serialisers.readUint32(inputData, 14, true);
+ headerData.width = Serialisers.readUint32(inputData, 18, true);
+ headerData.height = Serialisers.readInt32(inputData, 22, true);
+ headerData.planes = Serialisers.readUint16(inputData, 26, true);
+ if (headerData.planes !== 1) {
+ throw new Error("Data not valid (Only one plane supported)");
+ }
+
+ headerData.bitsPerPixel = Serialisers.readUint16(inputData, 28, true);
+ if (headerData.bitsPerPixel !== 1) {
+ throw new Error("Data not valid (Only monochrome BMP data supported)");
+ }
+
+ headerData.compressed = Serialisers.readUint32(inputData, 30, true);
+ if (headerData.compressed !== 0) {
+ throw new Error("Data not valid (Only uncompressed data supported)");
+ }
+
+ headerData.imageSize = Serialisers.readUint32(inputData, 34, true);
+ headerData.imageSize = Serialisers.readUint32(inputData, 34, true);
+ headerData.numberColors = Serialisers.readUint32(inputData, 46, true);
+ headerData.importantColors = Serialisers.readUint32(inputData, 50, true);
+
+ headerData.bytesRead = headerData.headerLength + 14;
+
+ return (headerData as IBMPHeaderData);
+
+ }
+
+
+ public static isPossiblyBMPFormat (inputData: Uint8Array): boolean {
+
+ if (inputData.length < 54) {
+ return false;
+ }
+
+ if (!(inputData[0] === 0x42 && inputData[1] === 0x4d)) {
+ return false;
+ }
+
+ const fileLen: number = Serialisers.readUint32(inputData, 2, true);
+ if (fileLen !== inputData.length) {
+ return false;
+ }
+
+ return true;
+
+ }
+
+
+}
diff --git a/src/main.ts b/src/main.ts
index 0063b8b..3966cc9 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -2,10 +2,79 @@ window.onload = (): void => {
const editor: BittMappEditor = new BittMappEditor({
canvas: document.getElementById("editor") as HTMLCanvasElement,
- canvasHeight: 640,
- canvasWidth: 640,
+ canvasHeight: 480,
+ canvasWidth: 480,
height: 32,
width: 32
});
+ (document.getElementById("pencil_mode_button") as HTMLAnchorElement).addEventListener("click", (event: MouseEvent) => {
+ event.preventDefault();
+ editor.pencilMode();
+ });
+
+ (document.getElementById("eraser_mode_button") as HTMLAnchorElement).addEventListener("click", (event: MouseEvent) => {
+ event.preventDefault();
+ editor.eraserMode();
+ });
+
+ (document.getElementById("selection_mode_button") as HTMLAnchorElement).addEventListener("click", (event: MouseEvent) => {
+ event.preventDefault();
+ editor.selectionMode();
+ });
+
+ (document.getElementById("save_file_button") as HTMLAnchorElement).addEventListener("click", (event: MouseEvent) => {
+ event.preventDefault();
+ const filename: string = (document.getElementById("filename_input") as HTMLInputElement).value;
+ editor.saveToFile(filename);
+ });
+
+ (document.getElementById("open_file_button") as HTMLAnchorElement).addEventListener("drop", (event: DragEvent) => {
+
+ event.preventDefault();
+
+ const dataTransfer: DataTransfer = event.dataTransfer;
+
+ if (dataTransfer.items.length > 1) {
+ alert("One file only!");
+ return;
+ }
+
+ if (dataTransfer.items) {
+ for (const item of (dataTransfer.items as any)) {
+
+ const file: File = item.getAsFile();
+ const fileReader: FileReader = new FileReader();
+
+ fileReader.addEventListener("loadend", () => {
+
+ const data: Uint8Array = new Uint8Array(fileReader.result);
+
+ if (BMPTools.isPossiblyBMPFormat(data)) {
+ const bittMappData: IBittMappData = BMPTools.extractBitmap(data);
+ const pixelWidth = bittMappData.width;
+ const pixelHeight = bittMappData.height;
+ editor.loadFromData(bittMappData.data, pixelWidth, pixelHeight);
+ } else {
+ const pixelWidth = parseInt((document.getElementById("open_file_pixelwidth_input") as HTMLInputElement).value, 10);
+ const pixelHeight = parseInt((document.getElementById("open_file_pixelheight_input") as HTMLInputElement).value, 10);
+ editor.loadFromData(data, pixelWidth, pixelHeight);
+ }
+
+ });
+
+ fileReader.readAsArrayBuffer(file);
+ }
+ }
+ });
+
+ (document.getElementById("open_file_button") as HTMLAnchorElement).addEventListener("dragover", (event: DragEvent) => {
+ event.preventDefault();
+ });
+
};
+
+
+
+
+// const testData: Uint8Array = new Uint8Array([66, 77, 190, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 40, 0, 0, 0, 32, 0, 0, 0, 32, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 128, 0, 0, 0, 116, 18, 0, 0, 116, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 15, 255, 255, 255, 247, 246, 142, 115, 183, 246, 181, 173, 183, 240, 141, 173, 183, 246, 181, 173, 183, 249, 142, 109, 23, 255, 255, 255, 247, 246, 141, 127, 247, 246, 189, 127, 247, 244, 138, 191, 247, 242, 186, 191, 247, 246, 138, 191, 247, 255, 255, 255, 247, 247, 139, 71, 247, 247, 187, 123, 247, 241, 184, 67, 247, 246, 187, 91, 247, 241, 188, 219, 247, 255, 255, 255, 247]);
diff --git a/src/serialisers.ts b/src/serialisers.ts
new file mode 100644
index 0000000..294a5e2
--- /dev/null
+++ b/src/serialisers.ts
@@ -0,0 +1,21 @@
+class Serialisers {
+
+
+ public static readInt32 (data: Uint8Array, start: number, littleEndian: boolean = false): number {
+ // console.log(new DataView(data.buffer.slice(start, start + 4)).getInt32(0));
+ return new DataView(data.buffer).getInt32(start, littleEndian);
+ }
+
+
+ public static readUint32 (data: Uint8Array, start: number, littleEndian: boolean = false): number {
+ // console.log(data.buffer.slice(start, start + 4));
+ // console.log(new DataView(data.buffer).getUint32(start, littleEndian));
+ return new DataView(data.buffer).getUint32(start, littleEndian);
+ }
+
+
+ public static readUint16 (data: Uint8Array, start: number, littleEndian: boolean = false): number {
+ return new DataView(data.buffer).getUint16(start, littleEndian);
+ }
+
+}
diff --git a/static/css/reset.css b/static/css/reset.css
new file mode 100644
index 0000000..af94440
--- /dev/null
+++ b/static/css/reset.css
@@ -0,0 +1,48 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
\ No newline at end of file
diff --git a/static/css/styles.css b/static/css/styles.css
index fae9d02..e041e1b 100644
--- a/static/css/styles.css
+++ b/static/css/styles.css
@@ -3,13 +3,41 @@ html, body {
}
body {
- background-color: #1E1E1E;
+ background-color: #2A2A2A;
}
+header {
+ background-color: #3E3F40;
+ height: 40px;
+}
+
+header h2, section h3 {
+ display: none;
+}
+
+header ul {
+ position: absolute;
+ top: 0;
+}
+
+header ul#files {
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+header ul li {
+ display: inline;
+}
+
+
+
section#main {
- position: relative;
- width: 100%;
- height: 100%;
+ position: absolute;
+ left: 0;
+ top: 50px;
+ right: 0;
+ bottom: 0;
line-height: 100%;
text-align: center;
}
diff --git a/tslint.json b/tslint.json
index b77e717..1379856 100644
--- a/tslint.json
+++ b/tslint.json
@@ -9,6 +9,9 @@
"space-before-function-paren": false,
"no-bitwise": false,
"trailing-comma": false,
+ "max-line-length": false,
+ "prefer-for-of": false,
+ "typedef": true,
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"]
},
"rulesDirectory": []