From eb6f502d96566024665b4e1e3d39c865e9defc85 Mon Sep 17 00:00:00 2001 From: Nathan Kunicki Date: Sun, 26 Nov 2017 17:54:52 +0000 Subject: [PATCH] Basic selection working --- index.html | 21 +++- src/bittmappeditor.ts | 236 +++++++++++++++++++++++++++++++++++++++--- src/bmptools.ts | 151 +++++++++++++++++++++++++++ src/main.ts | 73 ++++++++++++- src/serialisers.ts | 21 ++++ static/css/reset.css | 48 +++++++++ static/css/styles.css | 36 ++++++- tslint.json | 3 + 8 files changed, 568 insertions(+), 21 deletions(-) create mode 100644 src/bmptools.ts create mode 100644 src/serialisers.ts create mode 100644 static/css/reset.css 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

+ +
+

Editor

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": []