Basic selection working

This commit is contained in:
Nathan Kunicki 2017-11-26 17:54:52 +00:00
parent 1419290e7a
commit eb6f502d96
8 changed files with 568 additions and 21 deletions

View File

@ -1,13 +1,30 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<title>BittMapp</title>
<link rel="stylesheet" href="./static/css/reset.css" />
<link rel="stylesheet" href="./static/css/styles.css" />
<script src="./static/js/serialisers.js"></script>
<script src="./static/js/bmptools.js"></script>
<script src="./static/js/bittmappeditor.js"></script>
<script src="./static/js/main.js"></script>
<link rel="stylesheet" href="./static/css/styles.css" />
</head>
<body>
<header>
<h2>Modes</h2>
<ul id="modes">
<li><a href="#" id="pencil_mode_button">Pencil Mode</a></li>
<li><a href="#" id="eraser_mode_button">Eraser Mode</a></li>
<li><a href="#" id="selection_mode_button">Selection Mode</a></li>
</ul>
<h2>File Options</h2>
<ul id="files">
<li><input type="text" id="open_file_pixelwidth_input" value="32" /> <input type="text" id="open_file_pixelheight_input" value="32" /> <a href="#" id="open_file_button">Open File</a></li>
<li><input type="text" id="filename_input" /> <a href="#" id="save_file_button">Save File</a></li>
</ul>
</header>
<section id="main">
<h3>Editor</h3>
<canvas id="editor"></canvas>
</section>
</body>

View File

@ -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();
}
}
}
}
}
}

151
src/bmptools.ts Normal file
View File

@ -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;
}
}

View File

@ -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]);

21
src/serialisers.ts Normal file
View File

@ -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);
}
}

48
static/css/reset.css Normal file
View File

@ -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;
}

View File

@ -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;
}

View File

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