supports vectors with functions, methods, properties
This commit is contained in:
commit
4076440ff3
10 changed files with 969 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
20
README.md
Normal file
20
README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
**MatRepl**
|
||||||
|
* is a Matrix
|
||||||
|
* and a repl: Read–Eval–Print Loop, where Print is doing operations on vectors and matrices in a graphic environment
|
||||||
|
|
||||||
|
<img src="screenshot1.png"></img>
|
||||||
|
|
||||||
|
The repl has the following syntax (It's work in progress, new capabilities will be added)
|
||||||
|
* simple arithmetic expressions:
|
||||||
|
** add, subtract, divide, multiply
|
||||||
|
** variable declaration eg: a= ...
|
||||||
|
** vector(x0,y0,x,y) adds a vector
|
||||||
|
** remove(x) removes bindings (when it's an object (eg vector), removes it from the matrix)
|
||||||
|
** method calls:
|
||||||
|
*** a = vector(0,0,12,1)
|
||||||
|
*** a.type()
|
||||||
|
*** > vector
|
||||||
|
** property lookup
|
||||||
|
*** a.x
|
||||||
|
*** 12
|
||||||
|
|
||||||
BIN
screenshot-1.png
Normal file
BIN
screenshot-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
81
src/app.css
Normal file
81
src/app.css
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
body {
|
||||||
|
font: 13px Arial, sans-serif;
|
||||||
|
background: black;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
fill: none;
|
||||||
|
stroke: #4682b4;
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-grid {
|
||||||
|
fill: none;
|
||||||
|
stroke: gray;
|
||||||
|
stroke-width: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#console {
|
||||||
|
padding: 5px;
|
||||||
|
color: greenyellow;
|
||||||
|
background: black;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 40%;
|
||||||
|
height: 10em;
|
||||||
|
border: 2px solid darkgray;
|
||||||
|
border-radius: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.axis {
|
||||||
|
stroke-width: 1.5;
|
||||||
|
stroke: lightpink;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vector {
|
||||||
|
stroke-width: 2.5;
|
||||||
|
stroke: yellow;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prompt{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#command_input{
|
||||||
|
border: none transparent;
|
||||||
|
background: black;
|
||||||
|
color: greenyellow;
|
||||||
|
outline: none;
|
||||||
|
width: 39vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
#command_history{
|
||||||
|
font-size: 12px;
|
||||||
|
color: greenyellow;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1.5em;
|
||||||
|
max-height: 9em;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: visible;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
183
src/console.js
Normal file
183
src/console.js
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
/**
|
||||||
|
* handles user input from the console div
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
const state = {};
|
||||||
|
const command_input_element = document.getElementById('command_input');
|
||||||
|
const command_history_element = document.getElementById('command_history');
|
||||||
|
const bottom = document.getElementById('bottom');
|
||||||
|
command_input_element.value = '';
|
||||||
|
let command_history = [];
|
||||||
|
let command_history_index = 0;
|
||||||
|
|
||||||
|
command_input_element.onkeyup = function handle_key_input(event) {
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
if (command_history_index > -1) {
|
||||||
|
command_input_element.value = command_history[command_history_index];
|
||||||
|
if (command_history_index > 0) {
|
||||||
|
command_history_index -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
if (command_history_index < command_history.length - 1) {
|
||||||
|
command_history_index += 1;
|
||||||
|
command_input_element.value = command_history[command_history_index];
|
||||||
|
} else {
|
||||||
|
command_input_element.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
let command = command_input_element.value;
|
||||||
|
command_history_element.innerText += command + "\n";
|
||||||
|
command_input_element.value = '';
|
||||||
|
command_history_index = command_history.length;
|
||||||
|
let tokens = scan(command);
|
||||||
|
let statement = parse(tokens);
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = visit_expression(statement);
|
||||||
|
if (result.description) {
|
||||||
|
result = result.description;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
result = e.message;
|
||||||
|
}
|
||||||
|
command_history_element.innerText += result + "\n";
|
||||||
|
command_history.push(command);
|
||||||
|
command_history_element.scrollTo(0, command_history_element.scrollHeight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let visit_expression = function (expr) {
|
||||||
|
switch (expr.type) {
|
||||||
|
case 'declaration':
|
||||||
|
let value = visit_expression(expr.initializer);
|
||||||
|
let existing_value = state[expr.var_name.value];
|
||||||
|
if (existing_value) {
|
||||||
|
if (existing_value.type === 'vector') {
|
||||||
|
remove_vector(existing_value.object); // remove from screen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value.binding = expr.var_name.value;
|
||||||
|
state[expr.var_name.value] = value;
|
||||||
|
let description = state[expr.var_name.value].description;
|
||||||
|
if (!description) {
|
||||||
|
description = state[expr.var_name.value]; //questionable. use toString instead of message?
|
||||||
|
}
|
||||||
|
return {description: expr.var_name.value + ':' + description};
|
||||||
|
case 'group':
|
||||||
|
return visit_expression(expr.expression);
|
||||||
|
case 'unary':
|
||||||
|
let right_operand = visit_expression(expr.right);
|
||||||
|
if (expr.operator === token_types.MINUS) {
|
||||||
|
return -right_operand;
|
||||||
|
} else if (expr.operator === token_types.NOT) {
|
||||||
|
return !right_operand;
|
||||||
|
} else {
|
||||||
|
throw {message: 'illegal unary operator'};
|
||||||
|
}
|
||||||
|
case 'binary':
|
||||||
|
let left = visit_expression(expr.left);
|
||||||
|
let right = visit_expression(expr.right);
|
||||||
|
switch (expr.operator) {
|
||||||
|
case token_types.MINUS:
|
||||||
|
return left - right;
|
||||||
|
case token_types.PLUS:
|
||||||
|
return addition(left, right);
|
||||||
|
case token_types.STAR:
|
||||||
|
return multiplication(left, right);
|
||||||
|
case token_types.SLASH:
|
||||||
|
return left / right;
|
||||||
|
case token_types.DOT:
|
||||||
|
return method_call(left, expr.right);
|
||||||
|
}
|
||||||
|
throw {message: 'illegal binary operator'}
|
||||||
|
case 'identifier': {
|
||||||
|
if (state[expr.name]) {
|
||||||
|
return state[expr.name];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'literal':
|
||||||
|
return expr.value;
|
||||||
|
case 'call':
|
||||||
|
return call(expr.name, expr.arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const call = function (function_name, argument_exprs) {
|
||||||
|
let arguments = [];
|
||||||
|
for (let i = 0; i < argument_exprs.length; i++) {
|
||||||
|
arguments.push(visit_expression(argument_exprs[i]));
|
||||||
|
}
|
||||||
|
if (functions[function_name]) {
|
||||||
|
return functions[function_name](arguments);
|
||||||
|
} else {
|
||||||
|
let arg_list = '';
|
||||||
|
for (let i = 0; i < argument_exprs.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
arg_list += ',';
|
||||||
|
}
|
||||||
|
arg_list += argument_exprs[i].value_type;
|
||||||
|
}
|
||||||
|
return 'unimplemented: ' + function_name + '(' + arg_list + ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const method_call = function (object_wrapper, method_or_property) {
|
||||||
|
if (object_wrapper) {
|
||||||
|
if (method_or_property.type === 'call') { // method
|
||||||
|
if (typeof object_wrapper.object[method_or_property.name] !== 'function') {
|
||||||
|
throw {message: `method ${method_or_property.name} not found on ${object_wrapper.type}`};
|
||||||
|
}
|
||||||
|
return object_wrapper.object[method_or_property.name].apply(object_wrapper, method_or_property.arguments);
|
||||||
|
|
||||||
|
} else { // property
|
||||||
|
if (!object_wrapper.object.hasOwnProperty(method_or_property.name)){
|
||||||
|
throw {message: `property ${method_or_property.name} not found on ${object_wrapper.type}`};
|
||||||
|
}
|
||||||
|
return object_wrapper.object[method_or_property.name];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw {message: `not found: ${object_wrapper}`};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const functions = {
|
||||||
|
help: () => help(),
|
||||||
|
vector: (args) => add_vector({x0: args[0], y0: args[1], x: args[2], y: args[3]}),
|
||||||
|
remove: (args) => {
|
||||||
|
if (args[0].hasOwnProperty('binding')) {
|
||||||
|
delete this.state[args[0].binding];
|
||||||
|
return remove_vector(args[0].object); // by binding value
|
||||||
|
} else {
|
||||||
|
return remove_vector(args[0]); // by index (@...)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const help = function () {
|
||||||
|
return {message: 'vector(x0, y0, x, y): draws a vector from x0,y0 to x,y'}
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiplication = function (left, right) {
|
||||||
|
if (left.object && left.type === 'vector' && !right.object) {
|
||||||
|
return left.object.multiply(right);
|
||||||
|
}
|
||||||
|
if (right.object && right.type === 'vector' && !left.object) {
|
||||||
|
return right.object.multiply(left);
|
||||||
|
}
|
||||||
|
return left * right;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addition = function (left, right) {
|
||||||
|
if (left.object && left.type === 'vector' && right.object && right.type === 'vector') {
|
||||||
|
return left.object.add(right.object);
|
||||||
|
}
|
||||||
|
return left + right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)();
|
||||||
20
src/index.html
Normal file
20
src/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Interactive Linear Algebra</title>
|
||||||
|
<link rel="stylesheet" href="app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="console">
|
||||||
|
<div id="command_history"><div id="bottom"></div></div>
|
||||||
|
<label id="prompt">>
|
||||||
|
<input id="command_input" type="text" autofocus/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<script type="application/ecmascript" src="index.js"></script>
|
||||||
|
<script type="application/ecmascript" src="console.js"></script>
|
||||||
|
<script type="application/ecmascript" src="scanner.js"></script>
|
||||||
|
<script type="application/ecmascript" src="parser.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
297
src/index.js
Normal file
297
src/index.js
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
let add_vector,
|
||||||
|
remove_vector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry. draws the matrix
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
const SVG_NS = 'http://www.w3.org/2000/svg'; // program needs these to create svg elements
|
||||||
|
let grid_size = 100; // this is the nr of pixels for the basis vector (1,0) (0,1)
|
||||||
|
let half_grid_size = grid_size >> 1; // used to position the grid lines
|
||||||
|
let vectors = []; // collection of added vectors
|
||||||
|
let moving_vector; // user can move vector arrows. when moving, this refers to the arrow
|
||||||
|
let width = window.innerWidth, height = window.innerHeight;
|
||||||
|
let origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size,
|
||||||
|
origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size;
|
||||||
|
/**
|
||||||
|
* Creates an svg element
|
||||||
|
* @param element_type path,g, etc
|
||||||
|
* @returns SVG element
|
||||||
|
*/
|
||||||
|
const create = function (element_type) {
|
||||||
|
return document.createElementNS(SVG_NS, element_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates the d attribute string
|
||||||
|
* @param x0 start_x
|
||||||
|
* @param y0 start_y
|
||||||
|
* @param x1 end_x
|
||||||
|
* @param y1 end y
|
||||||
|
* @returns {string} to put in an SVG path
|
||||||
|
*/
|
||||||
|
const calculate_d = function (x0, y0, x1, y1) {
|
||||||
|
return "M" + x0 + " " + y0 + " L" + x1 + " " + y1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates a SVG line (path)
|
||||||
|
* @param x0 start_x
|
||||||
|
* @param y0 start_y
|
||||||
|
* @param x1 end_x
|
||||||
|
* @param y1 end_y
|
||||||
|
* @param css_class the css class to make up the element
|
||||||
|
* @returns an SVG path element
|
||||||
|
*/
|
||||||
|
const create_line = function (x0, y0, x1, y1, css_class) {
|
||||||
|
let path = create('path');
|
||||||
|
path.setAttribute('d', calculate_d(x0, y0, x1, y1));
|
||||||
|
path.setAttribute('class', css_class);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates the arrow path element
|
||||||
|
* @param id attribute
|
||||||
|
* @param x0 start_x
|
||||||
|
* @param y0 start_y
|
||||||
|
* @param x1 end_x
|
||||||
|
* @param y1 end_y
|
||||||
|
* @param css_class class attribute
|
||||||
|
* @returns {SVGPathElement}
|
||||||
|
*/
|
||||||
|
const arrow = function (id, x0, y0, x1, y1, css_class) {
|
||||||
|
let path = create('path');
|
||||||
|
|
||||||
|
path.setAttribute('d', calculate_d(x0, y0, x1, y1));
|
||||||
|
path.id = id;
|
||||||
|
path.setAttribute('class', css_class);
|
||||||
|
path.setAttribute('marker-end', 'url(#arrow)');
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the background grid of the space
|
||||||
|
* @param css_class class for the lines that are 'multiples of the basis vector'
|
||||||
|
* @param bg_css_class class for in between lines
|
||||||
|
* @returns {SVGGElement}
|
||||||
|
*/
|
||||||
|
const create_grid = function (css_class, bg_css_class) {
|
||||||
|
const group = create('g');
|
||||||
|
group.setAttribute('id', 'grid');
|
||||||
|
const horizontal = create('g');
|
||||||
|
horizontal.setAttribute('id', 'horizontal');
|
||||||
|
for (let y = 0; y < height; y += grid_size) {
|
||||||
|
horizontal.appendChild(create_line(0, y + half_grid_size, width, y + half_grid_size, css_class));
|
||||||
|
horizontal.appendChild(create_line(0, y, width, y, bg_css_class));
|
||||||
|
}
|
||||||
|
group.appendChild(horizontal);
|
||||||
|
const vertical = create('g');
|
||||||
|
vertical.setAttribute('id', 'vertical');
|
||||||
|
for (let x = 0; x < width; x += grid_size) {
|
||||||
|
vertical.appendChild(create_line(x + half_grid_size, 0, x + half_grid_size, height, css_class));
|
||||||
|
vertical.appendChild(create_line(x, 0, x, height, bg_css_class));
|
||||||
|
}
|
||||||
|
group.appendChild(vertical);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* removes child from element by id if found
|
||||||
|
* @param element
|
||||||
|
* @param child_id id to remove
|
||||||
|
*/
|
||||||
|
const remove_child = function (element, child_id) {
|
||||||
|
let node = element.firstChild;
|
||||||
|
while (node && child_id !== node.id) {
|
||||||
|
node = node.nextSibling;
|
||||||
|
}
|
||||||
|
if (node) {
|
||||||
|
element.removeChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* removes the grid from the DOM and adds an updated one.
|
||||||
|
*/
|
||||||
|
const redraw_grid = function () {
|
||||||
|
remove_child(svg, "grid");
|
||||||
|
svg.appendChild(create_grid('grid', 'bg-grid'));
|
||||||
|
svg.appendChild(create_axes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a vector to the set.
|
||||||
|
* @param vector
|
||||||
|
*/
|
||||||
|
add_vector = function (vector) {
|
||||||
|
vector.id = vectors.length;
|
||||||
|
vectors.push(vector);
|
||||||
|
redraw();
|
||||||
|
vector.add = (other) => add_vector({
|
||||||
|
x0: vector.x0 + other.x0,
|
||||||
|
y0: vector.x0 + other.x0,
|
||||||
|
x: vector.x + other.x,
|
||||||
|
y: vector.y + other.y
|
||||||
|
});
|
||||||
|
vector.multiply = (scalar) => add_vector({
|
||||||
|
x0: vector.x0 * scalar,
|
||||||
|
y0: vector.y0 * scalar,
|
||||||
|
x: vector.x * scalar,
|
||||||
|
y: vector.y * scalar
|
||||||
|
});
|
||||||
|
vector.is_vector = true;
|
||||||
|
vector.type = () => 'vector';
|
||||||
|
return { //object_wrapper
|
||||||
|
type: 'vector',
|
||||||
|
object: vector,
|
||||||
|
description: `vector@${vector.id}{x0:${vector.x0},y0:${vector.y0} x:${vector.x},y:${vector.y}}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_vector = function (vector_or_index) {
|
||||||
|
let index;
|
||||||
|
if (vector_or_index.is_vector) {
|
||||||
|
for (let i = 0; i < vectors.length; i++) {
|
||||||
|
if (vectors[i].id === vector_or_index.id) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index = vector_or_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vectors[index]) {
|
||||||
|
throw {message: `vector@${index} not found`};
|
||||||
|
}
|
||||||
|
|
||||||
|
vectors.splice(index, 1);
|
||||||
|
redraw();
|
||||||
|
return {description: `vector@${index} removed`};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The moving operation. Called by onmousemove on the svg ('canvas')
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
const move = function (event) {
|
||||||
|
if (moving_vector) {
|
||||||
|
let current_x = event.clientX;
|
||||||
|
let current_y = event.clientY;
|
||||||
|
vectors[moving_vector.id].x = (current_x - origin_x) / grid_size;
|
||||||
|
vectors[moving_vector.id].y = (origin_y - current_y) / grid_size;
|
||||||
|
moving_vector.setAttribute('d', calculate_d(origin_x, origin_y, current_x, current_y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws all the vectors.
|
||||||
|
*
|
||||||
|
* vector {
|
||||||
|
* x0,y0 origin
|
||||||
|
* x,y coordinates
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const draw_vectors = function () {
|
||||||
|
const vector_group = create("g");
|
||||||
|
vector_group.id = 'vectors';
|
||||||
|
|
||||||
|
for (let i = 0; i < vectors.length; i++) {
|
||||||
|
let vector_arrow = arrow(vectors[i].id,
|
||||||
|
origin_x + vectors[i].x0 * grid_size,
|
||||||
|
origin_y - vectors[i].y0 * grid_size,
|
||||||
|
origin_x + vectors[i].x * grid_size,
|
||||||
|
origin_y - vectors[i].y * grid_size,
|
||||||
|
'vector');
|
||||||
|
vector_arrow.onmousedown = function start_moving_vector(event) {
|
||||||
|
moving_vector = event.target;
|
||||||
|
};
|
||||||
|
vector_group.appendChild(vector_arrow);
|
||||||
|
}
|
||||||
|
svg.appendChild(vector_group);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all vectors in the svg and calls draw_vectors to draw updated versions.
|
||||||
|
*/
|
||||||
|
const redraw_vectors = function () {
|
||||||
|
remove_child(svg, 'vectors');
|
||||||
|
draw_vectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (re)draws all
|
||||||
|
*/
|
||||||
|
const redraw = function () {
|
||||||
|
redraw_grid();
|
||||||
|
redraw_vectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
const create_axes = function () {
|
||||||
|
let axes_group = create('g');
|
||||||
|
let x = create_line(0, origin_y, width, origin_y, 'axis');
|
||||||
|
x.id = 'x-axis';
|
||||||
|
axes_group.appendChild(x);
|
||||||
|
let y = create_line(origin_x, 0, origin_x, height, 'axis');
|
||||||
|
y.id = 'y-axis';
|
||||||
|
axes_group.appendChild(y);
|
||||||
|
return axes_group;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setup the arrow head for the vector
|
||||||
|
* @returns {SVGDefsElement}
|
||||||
|
*/
|
||||||
|
function create_defs() {
|
||||||
|
let defs = create('defs');
|
||||||
|
let marker = create('marker');
|
||||||
|
marker.id = 'arrow';
|
||||||
|
marker.setAttribute('orient', 'auto');
|
||||||
|
marker.setAttribute('viewBox', '0 0 10 10');
|
||||||
|
marker.setAttribute('markerWidth', '3');
|
||||||
|
marker.setAttribute('markerHeight', '4');
|
||||||
|
marker.setAttribute('markerUnits', 'strokeWidth');
|
||||||
|
marker.setAttribute('refX', '6');
|
||||||
|
marker.setAttribute('refY', '5');
|
||||||
|
let polyline = create('polyline');
|
||||||
|
polyline.setAttribute('points', '0,0 10,5 0,10 1,5');
|
||||||
|
polyline.setAttribute('fill', 'yellow');
|
||||||
|
marker.appendChild(polyline);
|
||||||
|
defs.appendChild(marker);
|
||||||
|
return defs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the SVG
|
||||||
|
* @returns {SVGElement}
|
||||||
|
*/
|
||||||
|
const create_svg = function () {
|
||||||
|
let svg = create('svg');
|
||||||
|
|
||||||
|
svg.onmousemove = move;
|
||||||
|
svg.onmouseup = function stop_moving_vector() {
|
||||||
|
moving_vector = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
let defs = create_defs();
|
||||||
|
svg.appendChild(defs);
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.onresize = function recalculate_window_dimensions() {
|
||||||
|
width = window.innerWidth;
|
||||||
|
height = window.innerHeight;
|
||||||
|
origin_x = Math.floor((width / grid_size) / 2) * grid_size + half_grid_size;
|
||||||
|
origin_y = Math.floor((height / grid_size) / 2) * grid_size + half_grid_size;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = create_svg();
|
||||||
|
document.body.appendChild(svg);
|
||||||
|
|
||||||
|
svg.appendChild(create_grid('grid', 'bg-grid'));
|
||||||
|
svg.appendChild(create_axes());
|
||||||
|
})
|
||||||
|
();
|
||||||
185
src/parser.js
Normal file
185
src/parser.js
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
const parse = function (tokens) {
|
||||||
|
let token_index = 0;
|
||||||
|
|
||||||
|
return statement();
|
||||||
|
|
||||||
|
function statement() {
|
||||||
|
if (check(token_types.IDENTIFIER, token_index) && check(token_types.EQUALS, token_index + 1)) {
|
||||||
|
let var_name = current_token();
|
||||||
|
advance();
|
||||||
|
advance();
|
||||||
|
return {type: 'declaration', var_name: var_name, initializer: expression()};
|
||||||
|
} else {
|
||||||
|
return expression();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expression() {
|
||||||
|
return equality();
|
||||||
|
}
|
||||||
|
|
||||||
|
function equality() {
|
||||||
|
let expr = comparison()
|
||||||
|
|
||||||
|
while (match([token_types.EQUALS_EQUALS, token_types.NOT_EQUALS])) {
|
||||||
|
let operator = previous_token();
|
||||||
|
let right = unary();
|
||||||
|
expr = {type: 'binary', left: expr, operator: operator, right: right};
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparison() {
|
||||||
|
let expr = addition();
|
||||||
|
|
||||||
|
while (match([token_types.LESS, token_types.LESS_OR_EQUAL, token_types.GREATER, token_types.GREATER_OR_EQUAL])) {
|
||||||
|
let operator = previous_token();
|
||||||
|
let right = addition();
|
||||||
|
expr = {type: 'binary', left: expr, operator: operator, right: right};
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addition() {
|
||||||
|
let expr = multiplication();
|
||||||
|
|
||||||
|
while (match([token_types.MINUS, token_types.PLUS])) {
|
||||||
|
let operator = previous_token();
|
||||||
|
let right = multiplication();
|
||||||
|
expr = {type: 'binary', left: expr, operator: operator, right: right};
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function multiplication() {
|
||||||
|
let expr = unary();
|
||||||
|
|
||||||
|
while (match([token_types.SLASH, token_types.STAR, token_types.DOT])) {
|
||||||
|
let operator = previous_token();
|
||||||
|
let right = unary();
|
||||||
|
expr = {type: 'binary', left: expr, operator: operator, right: right};
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unary() {
|
||||||
|
if (match([token_types.NOT, token_types.MINUS])) {
|
||||||
|
let operator = previous_token();
|
||||||
|
let right = unary();
|
||||||
|
return {type: 'unary', operator: operator, right: right};
|
||||||
|
} else {
|
||||||
|
return call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function call() {
|
||||||
|
let expr = primary();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (match([token_types.LEFT_PAREN])) {
|
||||||
|
expr = finish_call(expr.name);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish_call(callee) {
|
||||||
|
let arguments = [];
|
||||||
|
if (!check(token_types.RIGHT_PAREN, token_index)) {
|
||||||
|
do {
|
||||||
|
arguments.push(expression());
|
||||||
|
} while (match([token_types.COMMA]));
|
||||||
|
}
|
||||||
|
if (!match([token_types.RIGHT_PAREN])) {
|
||||||
|
throw {message: "Expect ')' after arguments."};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {type: 'call', name: callee, arguments: arguments};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function primary() {
|
||||||
|
if (match([token_types.NUMERIC, token_types.STRING])) {
|
||||||
|
return {type: 'literal', value: previous_token().value, value_type: previous_token().type};
|
||||||
|
} else if (match([token_types.LEFT_PAREN])) {
|
||||||
|
let expr = expression();
|
||||||
|
if (expr && match([token_types.RIGHT_PAREN])) {
|
||||||
|
return {type: 'group', expression: expr};
|
||||||
|
} else {
|
||||||
|
throw {message: 'expected expression or )'};
|
||||||
|
}
|
||||||
|
} else if (check(token_types.IDENTIFIER, token_index)) {
|
||||||
|
let identifier = {type: 'identifier', name: current_token().value};
|
||||||
|
advance();
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* matches token against array of tokens to check for equality (matching type)
|
||||||
|
* @param tokens_to_match array of tokens
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function match(tokens_to_match) {
|
||||||
|
for (let i = 0; i < tokens_to_match.length; i++) {
|
||||||
|
if (are_same(tokens_to_match[i], current_token())) {
|
||||||
|
advance()
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if token at position index matches the given
|
||||||
|
* @param token_to_check expected token type
|
||||||
|
* @param index of token to check
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function check(token_to_check, index) {
|
||||||
|
let token = tokens[index];
|
||||||
|
if (!token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return are_same(token_to_check, token);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if 2 tokens have same type
|
||||||
|
* @param token_1
|
||||||
|
* @param token_2
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function are_same(token_1, token_2) {
|
||||||
|
if (is_at_end()) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return token_1.type === token_2.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_at_end() {
|
||||||
|
return token_index >= tokens.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function advance() {
|
||||||
|
token_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function previous_token() {
|
||||||
|
return tokens[token_index - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function current_token() {
|
||||||
|
return tokens[token_index];
|
||||||
|
}
|
||||||
|
}
|
||||||
181
src/scanner.js
Normal file
181
src/scanner.js
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
/**
|
||||||
|
* Creates an array of tokens from a line of input.
|
||||||
|
*
|
||||||
|
* @param command: string
|
||||||
|
* @returns {token_type[]}
|
||||||
|
*/
|
||||||
|
const scan = function(command) {
|
||||||
|
let current_index = 0, // current index of char to look at in the command string
|
||||||
|
word_start_index = 0, // marker for start of a literal or identifier
|
||||||
|
tokens = [];
|
||||||
|
|
||||||
|
while (!is_at_end()) {
|
||||||
|
word_start_index = current_index;
|
||||||
|
let token = scan_token();
|
||||||
|
if (token) { // undefined mostly means whitespace
|
||||||
|
tokens.push(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
|
||||||
|
function scan_token() {
|
||||||
|
let next_char = advance();
|
||||||
|
switch (next_char) {
|
||||||
|
case '(':
|
||||||
|
return token_types.LEFT_PAREN;
|
||||||
|
case ')':
|
||||||
|
return token_types.RIGHT_PAREN;
|
||||||
|
case '[':
|
||||||
|
return token_types.LEFT_BRACKET;
|
||||||
|
case ']':
|
||||||
|
return token_types.RIGHT_BRACKET;
|
||||||
|
case ',':
|
||||||
|
return token_types.COMMA;
|
||||||
|
case '.':
|
||||||
|
return token_types.DOT;
|
||||||
|
case '-':
|
||||||
|
return token_types.MINUS;
|
||||||
|
case '+':
|
||||||
|
return token_types.PLUS;
|
||||||
|
case '*':
|
||||||
|
return token_types.STAR;
|
||||||
|
case '/':
|
||||||
|
return token_types.SLASH;
|
||||||
|
case '>':
|
||||||
|
if (expect('=')) {
|
||||||
|
return token_types.GREATER_OR_EQUAL;
|
||||||
|
} else {
|
||||||
|
return token_types.GREATER;
|
||||||
|
}
|
||||||
|
case '<':
|
||||||
|
if (expect('=')) {
|
||||||
|
return token_types.LESS_OR_EQUAL;
|
||||||
|
} else {
|
||||||
|
return token_types.LESS;
|
||||||
|
}
|
||||||
|
case '!':
|
||||||
|
if (expect('=')) {
|
||||||
|
return token_types.NOT_EQUALS;
|
||||||
|
} else {
|
||||||
|
return token_types.NOT;
|
||||||
|
}
|
||||||
|
case '=':
|
||||||
|
if (expect('=')) {
|
||||||
|
return token_types.EQUALS_EQUALS;
|
||||||
|
} else {
|
||||||
|
return token_types.EQUALS;
|
||||||
|
}
|
||||||
|
case '\'':
|
||||||
|
return string('\'');
|
||||||
|
case '\"':
|
||||||
|
return string('\"');
|
||||||
|
}
|
||||||
|
if (is_digit(next_char)) {
|
||||||
|
let token = Object.assign({}, token_types.NUMERIC);
|
||||||
|
token.value = parse_number();
|
||||||
|
return token;
|
||||||
|
} else {
|
||||||
|
if (is_alpha_or_underscore(next_char)) {
|
||||||
|
let token = Object.assign({}, token_types.IDENTIFIER);
|
||||||
|
token.value = parse_identifier();
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expect(expected_char) {
|
||||||
|
if (is_at_end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (current_char() === expected_char) {
|
||||||
|
advance();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function advance() {
|
||||||
|
if (current_index < command.length) {
|
||||||
|
current_index += 1;
|
||||||
|
}
|
||||||
|
return command[current_index - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_at_end() {
|
||||||
|
return current_index >= command.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function current_char() {
|
||||||
|
return command[current_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_digit(char) {
|
||||||
|
return char >= '0' && char <= '9';
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_part_of_number(char) {
|
||||||
|
return is_digit(char) || char === '.'; // no scientific notation for now
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_number() {
|
||||||
|
while (is_part_of_number(current_char())) {
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
let number_string = command.substring(word_start_index, current_index);
|
||||||
|
return Number.parseFloat(number_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_alpha_or_underscore(char) {
|
||||||
|
return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char === '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_alphanumeric_or_underscore(char) {
|
||||||
|
return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || is_digit(char) || char === '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse_identifier() {
|
||||||
|
while (is_alphanumeric_or_underscore(current_char())) {
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
return command.substring(word_start_index, current_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function string(quote) { // as of yet strings may not unclude escaped quotes that are also the start/end quote
|
||||||
|
while (current_char() !== quote && !is_at_end()) {
|
||||||
|
advance();
|
||||||
|
}
|
||||||
|
if (is_at_end() && current_char() !== quote) {
|
||||||
|
throw {message: 'unterminated string'}
|
||||||
|
} else {
|
||||||
|
let string_token = Object.assign({}, token_types.STRING);
|
||||||
|
string_token.value = command.substring(word_start_index + 1, current_index);
|
||||||
|
advance();
|
||||||
|
return string_token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const token_types = {
|
||||||
|
LEFT_PAREN: {type: 'left_paren'},
|
||||||
|
RIGHT_PAREN: {type: 'right_paren'},
|
||||||
|
LEFT_BRACKET: {type: 'left_bracket'},
|
||||||
|
RIGHT_BRACKET: {type: 'right_bracket'},
|
||||||
|
COMMA: {type: 'comma'},
|
||||||
|
DOT: {type: 'dot'},
|
||||||
|
MINUS: {type: 'minus'},
|
||||||
|
PLUS: {type: 'plus'},
|
||||||
|
STAR: {type: 'star'},
|
||||||
|
SLASH: {type: 'slash'},
|
||||||
|
EQUALS: {type: 'equals'},
|
||||||
|
EQUALS_EQUALS: {type: 'equals_equals'},
|
||||||
|
NOT_EQUALS: {type: 'not_equals'},
|
||||||
|
NOT: {type: 'not'},
|
||||||
|
GREATER: {type: 'greater'},
|
||||||
|
GREATER_OR_EQUAL: {type: 'greater_or_equal'},
|
||||||
|
LESS: {type: 'less'},
|
||||||
|
LESS_OR_EQUAL: {type: 'less_or_equal'},
|
||||||
|
NUMERIC: {type: 'number', value: undefined},
|
||||||
|
IDENTIFIER: {type: 'identifier', value: undefined},
|
||||||
|
STRING: {type: 'string', value: undefined}
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue