-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
368 lines (284 loc) · 11.9 KB
/
app.js
File metadata and controls
368 lines (284 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
'use strict';
/* TODO:
- Add option to reverse number
- Make clipboard success notification
*/
// Color and thickness of the grid's lines
const gridColor = 'grey';
const gridThickness = 2;
let rectangleActiveColor = 'white';
let rectangleInactiveColor = 'black';
// The number of rectangles in the grid
const numRectanglesWide = 106;
const numRectanglesHigh = 17;
// The total size of the grid lines
const totalVerticalLineSize = numRectanglesWide * gridThickness;
const totalHorizontalLineSize = numRectanglesHigh * gridThickness;
(function() {
// This array will hold the states of each rectangle
const grid = [];
// The canvas DOM element and the context to use for drawing
const canvas = document.getElementById('canvas');
// If this function exists, the canvas can be used for drawing on
if (!canvas.getContext) {
console.log('This browser does not support the canvas element.');
return;
}
// The canvas's context used for drawing
const ctx = canvas.getContext('2d');
// Let the canvas fill the width of the column,
// but set the height so it's not too tall
canvas.width = canvas.parentElement.offsetWidth;
canvas.height = .3 * window.innerHeight;
// The room for the rectangles is what is left over after the lines are drawn
const rectangleWidth = (canvas.width - totalVerticalLineSize) / numRectanglesWide;
const rectangleHeight = (canvas.height - totalHorizontalLineSize) / numRectanglesHigh;
// Prevent the right-click menu from popping up
canvas.oncontextmenu = e => e.preventDefault();
// Draw the grid lines and initializes the array holding the state of the grid
setupGrid();
// Draws inactive rectangles on the grid
clearGrid();
// Sets up IO functions like reading a K value from a textarea, clicking to
// turn on/off rectangles, etc
setupIO();
// Helper function that returns value limited to the range of min-max
function clampValueBetween(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// Clears the canvas from (x, y) to (x + rectangleWidth, y + rectangleHeight)
// and fills a rectangle there with the specified color
function drawRectangle(x, y, color) {
ctx.clearRect(x, y, rectangleWidth, rectangleHeight);
ctx.fillStyle = color;
ctx.fillRect(x, y, rectangleWidth, rectangleHeight);
}
// Returns coord's of the bottom left corner of the rectangle at (column, row)
function rectangleToCoordinate(column, row) {
/*
* Since the canvas element begins its coordinates in the top left corner and we count
* rectangles starting from the bottom right, we need to some conversion.
* We need 0 to be the max y coord, and the max y coord to be 0.
*
* In order to convert, we figure out the maximum possible value for the
* y coordinate and subtract our y-coordinate value from that.
*
* We also have to take into account the top and left grid line which takes
* up half the space of the normal grid lines.
*/
// The max amount of vertical space rectangles and grid lines could take up
// -1 because the coordinate of the rectangles begin at their top left
const maxRectSpace = (numRectanglesHigh - 1) * rectangleHeight;
const maxGridSpace = numRectanglesHigh * gridThickness;
const maxy = maxRectSpace + maxGridSpace;
// The actual amount of space, up to the column specified
const rectSpaceHorizontal = column * rectangleWidth;
const gridSpaceHorizontal = column * gridThickness + gridThickness / 2;
// The actual amount of space, up to the row specified
const rectSpaceVertical = row * rectangleHeight;
const gridSpaceVertical = row * gridThickness + gridThickness / 2;
const x = rectSpaceHorizontal + gridSpaceHorizontal;
const y = maxy - (rectSpaceVertical + gridSpaceVertical);
return { x, y };
}
// Returns the column and row of the rectangle intersected by (x, y)
function coordinateToRectangle(x, y){
// The space a single rectangle and border occupies
const unitWidth = rectangleWidth + gridThickness;
const unitHeight = rectangleHeight + gridThickness;
// We need to take into account the extra border at the top/left sides
// of the canvas, before performing the calculation
const extraBorder = gridThickness / 2;
// The grid counts rectangles starting at the bottom, but coordinates start
// at the top, so the row needs to be inverted (0 -> max, max -> 0, etc.)
const maxRow = numRectanglesHigh - 1;
let column = Math.ceil(x / unitWidth - extraBorder);
let row = maxRow - Math.ceil(y / unitHeight - extraBorder);
column = clampValueBetween(column, 0, numRectanglesWide - 1);
row = clampValueBetween(row, 0, numRectanglesHigh - 1);
return {
x: column,
y: row
};
}
// Turns on/off the rectangle at the given column and row
function setAndPlotRectangle(column, row, newState) {
grid[column][row] = newState;
plotRectangle(column, row);
}
// Flips the state of and plots the rect at (column, row)
function toggleRectangle(column, row) {
setAndPlotRectangle(column, row, !grid[column][row]);
}
// Creates the array to hold the "cell's" states and draws the grid's lines
function setupGrid() {
// Initialize the grid with all false values, i.e. the inactive state
for (let i = 0; i < numRectanglesWide; i++) {
grid.push([]);
for (let j = 0; j < numRectanglesHigh; j++) {
grid[i].push(false);
}
}
ctx.strokeStyle = gridColor;
ctx.lineWidth = gridThickness;
const drawLine = (x1, y1, x2, y2) => {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
};
// Since we draw the lines on the left and top sides of each rectangle,
// we need to use <= instead of < so that the last border is drawn
// Draws vertical grid lines
for (let i = 0; i <= numRectanglesWide; i++) {
const x = i * rectangleWidth + i * gridThickness;
drawLine(x, 0, x, canvas.height);
}
// Draws horizontal grid lines
for (let i = 0; i <= numRectanglesHigh; i++) {
const y = i * rectangleHeight + i * gridThickness;
drawLine(0, y, canvas.width, y);
}
}
// Calls setAndPlotRectangle on every rectangle to set them as inactive
function clearGrid() {
for (let i = 0; i < numRectanglesWide; i++) {
for (let j = 0; j < numRectanglesHigh; j++) {
setAndPlotRectangle(i, j, false);
}
}
}
function plotRectangle(column, row) {
const {x, y} = rectangleToCoordinate(column, row);
let color = grid[column][row] ? rectangleActiveColor : rectangleInactiveColor;
drawRectangle(x, y, color);
}
// Plots rectangles based on the states in the grid array
function plotGrid() {
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[i].length; j++) {
plotRectangle(i, j);
}
}
}
function setGridFromBinary(binaryString) {
clearGrid();
for (let i = 0; i < binaryString.length; i++) {
let digit = binaryString.charAt(binaryString.length - i - 1);
const row = i % 17;
const column = Math.floor(i / 17);
// If the digit is a 1, set the rectangle to true and to false otherwise
grid[column][row] = digit === '1';
}
}
// Sets up IO functions like reading a K value from the input textarea,
// mouse actions to turn on/off rectangles, showing errors, etc.
function setupIO() {
// The DOM elements interacted with
const readInputButton = document.getElementById('readInputButton');
const getOutputButton = document.getElementById('getOutputButton');
const outputTextarea = document.getElementById('outputArea');
const inputTextarea = document.getElementById('inputArea');
const cleanInputButton = document.getElementById('cleanInputButton');
const errorMessage = document.getElementById('errorMessage');
// Needed to handle large numbers
const sn = SchemeNumber;
// Shows the error alert with the given message
const showError = message => {
errorMessage.innerHTML = 'Error: ' + message;
errorMessage.style.visibility = 'visible';
};
// Hides the error alert
const hideErrorMessage = () => errorMessage.style.visibility = 'hidden';
// Used to clean user's input
const cleanString = string => string.replace(/\D+/g, '');
// Uses the bounding rectangle of the canvas to determine the location
// of the mouse relative to the upper left corner of the canvas
const getMouseCoordinate = evt => {
const canvasRectangle = canvas.getBoundingClientRect();
return {
x: evt.clientX - canvasRectangle.left,
y: evt.clientY - canvasRectangle.top
};
};
// Instantiates clipboard.js so we can copy text
const clipboard = new Clipboard('.btn-clipboard');
// Used to determine if the user is drawing when they move the mouse
let leftButtonDown = false;
let rightButtonDown = false;
// If button === 0, sets leftButtonDown to isDown,
// else if button === 2 sets rightButton to isDown
const setButtonDown = (button, isDown) => {
if (button === 0) {
leftButtonDown = isDown;
} else if (button === 2) {
rightButtonDown = isDown;
}
};
// When a mouse button is clicked, set the state of that button
document.addEventListener('mousedown', evt => setButtonDown(evt.button, true));
document.addEventListener('mouseup', evt => setButtonDown(evt.button, false));
// When the clean input button is pressed, remove any non-numerics from it
cleanInputButton.addEventListener('click', () => {
inputTextarea.value = cleanString(inputTextarea.value);
});
// When a button is pressed or the mouse moves, determine if any
// rectangles should be updated
const handleMouseAction = (evt, isClick = false) => {
const {x: mousex, y: mousey} = getMouseCoordinate(evt);
const {x: rectx, y: recty} = coordinateToRectangle(mousex, mousey);
if (rectx < 0 || recty < 0 ||
rectx > numRectanglesWide || recty > numRectanglesHigh) {
return;
}
if (leftButtonDown) {
setAndPlotRectangle(rectx, recty, true);
} else if (rightButtonDown) {
setAndPlotRectangle(rectx, recty, false);
} else if (isClick) {
toggleRectangle(rectx, recty);
}
};
canvas.addEventListener('mousedown', evt => handleMouseAction(evt, true));
canvas.addEventListener('mousemove', handleMouseAction);
getOutputButton.addEventListener('click', () => {
let plotString = '#b';
for (let i = numRectanglesWide - 1; i >= 0; i--) {
for (let j = numRectanglesHigh - 1; j >= 0; j--) {
plotString += grid[i][j] ? '1' : '0';
}
}
// Multiply result by 17
plotString = sn.fn['*'](plotString, sn('17'));
// Convert to string before plugging in textarea
outputTextarea.value = sn.fn['number->string'](plotString);
});
readInputButton.addEventListener('click', () => {
hideErrorMessage();
// Remove any non-numeric characters
const inputString = inputTextarea.value.replace(/\D+/g, '');
let inputNumber = sn.fn['string->number'](inputString);
if (!inputNumber) {
showError('Not a valid number, maybe use the input clean up button?');
return;
}
// K must be divisible by 17
let remainder = sn.fn['mod'](inputNumber, sn('17'));
// If K is not divisible by 17 show an error
if (!sn.fn['='](remainder, sn('0'))) {
showError('K is not divisible by 17');
return;
}
// Divide the input by 17
inputNumber = sn.fn['/'](inputNumber, sn('17'));
// Convert the input into binary
let binaryNumber = sn.fn['number->string'](inputNumber, sn('2'));
// Use the new binary number to set the state of each rectangle
setGridFromBinary(binaryNumber);
// Plot the state of each rectangle
plotGrid();
});
// To plot the formula that is plugged into the field by default
readInputButton.click();
}
})();