diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 733024c..b0a1c49 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1 +1 @@ -Please see [The Coding Train Code of Conduct](https://github.com/CodingTrain/Code-of-Conduct) +Please see [The Coding Train Code of Conduct](https://github.com/CodingTrain/Code-of-Conduct) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a099036..8a445d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1 @@ -TBD +TBD diff --git a/LICENSE b/LICENSE index 02bc06e..651136a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2018 Daniel Shiffman - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2018 Daniel Shiffman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index dbfbd4c..d6ee0e3 100644 --- a/README.md +++ b/README.md @@ -1,167 +1,167 @@ -# Logo -Repo for implementation of Logo in JavaScript p5 - -
Supported commands -

- -> The description(s) and/or example(s) of the commands were taken (and corrected for this interpreter) from the [Berkeley Logo Reference Manual](https://people.eecs.berkeley.edu/~bh/v2ch14/manual.html) under MIT license. - -### Forward -Moves the turtle forward, in the direction that it's facing, by the specified distance (measured in turtle steps). -* `fd units` - -### Backward -Moves the turtle backward, i.e., exactly opposite to the direction that it's facing, by the specified distance. (The heading of the turtle does not change.) -* `bd units` - -### Right -Turns the turtle clockwise by the specified angle, measured by default in degrees (1/360 of a circle). -* `rt angle` - -### Left -Turns the turtle counterclockwise by the specified angle, measured by default in degrees (1/360 of a circle). -* `lt angle` - -### Penup -Sets the pen's position to UP, without changing its mode. -* `pu` - -### Pendown -Sets the pen's position to DOWN, without changing its mode. -* `pd` - -### Pensize -Sets the thickness of the pen with the given the value. Note: If it's given a negative value, by default will be thickness of 1. -* `pensize thicknessValue` - -### Set X Y -Moves the turtle to an absolute position in the graphics window. The two inputs are numbers, the X and Y coordinates. -* `setxy coordinateX coordinateY` - -### Set X -Moves the turtle horizontally from its old position to a new absolute horizontal coordinate. The input is the new X coordinate. -* `setx coordinateX` - -### Set Y -Moves the turtle vertically from its old position to a new absolute vertical coordinate. The input is the new Y coordinate. -* `sety coordinateY` - -### Home -Moves the turtle to the center of the screen. -* `home` - -### Radians -Changes the angle values to be used as radians -* `radians` - -### Degrees -Changes the angle values to be used as degrees -* `degrees` - -### Repeat - uns the following instruction list repeatedly, num times. Can be nested. -* `repeat num [instruction list]` - -### Color -Sets the pen color given the hexadecimal value in format `#FFF / #FFFFFF`. -* `color hexadecimalValue` - -### Color RGB -Sets the pen color given the RGB value. -* `colorrgb [red green blue]` - -### Author -Prints the given in the developer console -* `author [author website twitter]` - -

-
- -## Useful links -* [Live Web Demo](https://codingtrain.github.io/Logo/) -* [Coding Challenge Part 1](https://thecodingtrain.com/CodingChallenges/121.1-logo-interpreter.html) and -[Coding Challenge Part 2](https://thecodingtrain.com/CodingChallenges/121.2-logo-interpreter.html) -* [Coding Challenge Part 1 on Youtube](https://www.youtu.be/i-k04yzfMpw) and -[Coding Challenge Part 2 on Youtube](https://www.youtu.be/aOqEm101fms) -* [Logo Wikipedia Page](https://en.wikipedia.org/wiki/Logo_(programming_language)) -* [More Information about Logo and some Examples](http://cs.brown.edu/courses/bridge/1997/Resources/LogoTutorial.html) - -## Getting Started - -Getting started just takes 3 simple steps, each one is a command in the terminal. - -1. Clone this GitHub repository. -2. Navigate inside the repository directory on your machine. -3. Host the directory with a local web server. - -There are a few easy ways to achieve step number 3 depending on what you already have installed: - -
Node Package Manager (NPM) -

- -> If you have NPM installed you can use the [live-server](https://www.npmjs.com/package/live-server) NPM package. -> The neat thing about live-server is that it automatically refreshes the web page every time you change a file. -> -> If you don't have NPM installed you can download and install it [here](https://www.npmjs.com/get-npm). -> -> Once you have NPM, install live-server globally with `npm install --global live-server`. -> -> Then run the following commands in your terminal. -> ``` -> git clone https://github.com/CodingTrain/Logo.git -> cd Logo -> live-server --port=8080 . -> ``` -> Don't forget the dot at the end of the command on MacOS! -> -> Finally, you can open [http://localhost:8080](http://localhost:8080) in your browser and you're away! -> -> Note that when you close the terminal window, the web server will stop as well. - -

-
- -
- -
Python 2.x -

- -> You can use the [SimpleHTTPServer](https://docs.python.org/2/library/simplehttpserver.html) python module. -> -> If you don't have Python 2 installed you can download and install it [here](https://www.python.org/downloads/). -> -> Then run the following commands in your terminal. -> ``` -> git clone https://github.com/CodingTrain/Logo.git -> cd Logo -> python -m SimpleHTTPServer 8080 -> ``` -> Finally, you can open [http://localhost:8080](http://localhost:8080) in your browser and you're away! -> -> Note that when you close the terminal window, the web server will stop as well. - -

-
- -
- -
Python 3.x -

- -> You can use the [http.server](https://docs.python.org/3/library/http.server.html) python module. -> -> If you don't have Python 3 installed you can download and install it [here](https://www.python.org/downloads/). -> -> -> Then run the following commands in your terminal. -> ``` -> git clone https://github.com/CodingTrain/Logo.git -> cd Logo -> python3 -m http.server 8080 -> ``` -> Finally, you can open [http://localhost:8080](http://localhost:8080) in your browser and you're away! -> -> Note that when you close the terminal window, the web server will stop as well. - -

-
+# Logo +Repo for implementation of Logo in JavaScript p5 + +
Supported commands +

+ +> The description(s) and/or example(s) of the commands were taken (and corrected for this interpreter) from the [Berkeley Logo Reference Manual](https://people.eecs.berkeley.edu/~bh/v2ch14/manual.html) under MIT license. + +### Forward +Moves the turtle forward, in the direction that it's facing, by the specified distance (measured in turtle steps). +* `fd units` + +### Backward +Moves the turtle backward, i.e., exactly opposite to the direction that it's facing, by the specified distance. (The heading of the turtle does not change.) +* `bd units` + +### Right +Turns the turtle clockwise by the specified angle, measured by default in degrees (1/360 of a circle). +* `rt angle` + +### Left +Turns the turtle counterclockwise by the specified angle, measured by default in degrees (1/360 of a circle). +* `lt angle` + +### Penup +Sets the pen's position to UP, without changing its mode. +* `pu` + +### Pendown +Sets the pen's position to DOWN, without changing its mode. +* `pd` + +### Pensize +Sets the thickness of the pen with the given the value. Note: If it's given a negative value, by default will be thickness of 1. +* `pensize thicknessValue` + +### Set X Y +Moves the turtle to an absolute position in the graphics window. The two inputs are numbers, the X and Y coordinates. +* `setxy coordinateX coordinateY` + +### Set X +Moves the turtle horizontally from its old position to a new absolute horizontal coordinate. The input is the new X coordinate. +* `setx coordinateX` + +### Set Y +Moves the turtle vertically from its old position to a new absolute vertical coordinate. The input is the new Y coordinate. +* `sety coordinateY` + +### Home +Moves the turtle to the center of the screen. +* `home` + +### Radians +Changes the angle values to be used as radians +* `radians` + +### Degrees +Changes the angle values to be used as degrees +* `degrees` + +### Repeat + uns the following instruction list repeatedly, num times. Can be nested. +* `repeat num [instruction list]` + +### Color +Sets the pen color given the hexadecimal value in format `#FFF / #FFFFFF`. +* `color hexadecimalValue` + +### Color RGB +Sets the pen color given the RGB value. +* `colorrgb [red green blue]` + +### Author +Prints the given in the developer console +* `author [author website twitter]` + +

+
+ +## Useful links +* [Live Web Demo](https://codingtrain.github.io/Logo/) +* [Coding Challenge Part 1](https://thecodingtrain.com/CodingChallenges/121.1-logo-interpreter.html) and +[Coding Challenge Part 2](https://thecodingtrain.com/CodingChallenges/121.2-logo-interpreter.html) +* [Coding Challenge Part 1 on Youtube](https://www.youtu.be/i-k04yzfMpw) and +[Coding Challenge Part 2 on Youtube](https://www.youtu.be/aOqEm101fms) +* [Logo Wikipedia Page](https://en.wikipedia.org/wiki/Logo_(programming_language)) +* [More Information about Logo and some Examples](http://cs.brown.edu/courses/bridge/1997/Resources/LogoTutorial.html) + +## Getting Started + +Getting started just takes 3 simple steps, each one is a command in the terminal. + +1. Clone this GitHub repository. +2. Navigate inside the repository directory on your machine. +3. Host the directory with a local web server. + +There are a few easy ways to achieve step number 3 depending on what you already have installed: + +
Node Package Manager (NPM) +

+ +> If you have NPM installed you can use the [live-server](https://www.npmjs.com/package/live-server) NPM package. +> The neat thing about live-server is that it automatically refreshes the web page every time you change a file. +> +> If you don't have NPM installed you can download and install it [here](https://www.npmjs.com/get-npm). +> +> Once you have NPM, install live-server globally with `npm install --global live-server`. +> +> Then run the following commands in your terminal. +> ``` +> git clone https://github.com/CodingTrain/Logo.git +> cd Logo +> live-server --port=8080 . +> ``` +> Don't forget the dot at the end of the command on MacOS! +> +> Finally, you can open [http://localhost:8080](http://localhost:8080) in your browser and you're away! +> +> Note that when you close the terminal window, the web server will stop as well. + +

+
+ +
+ +
Python 2.x +

+ +> You can use the [SimpleHTTPServer](https://docs.python.org/2/library/simplehttpserver.html) python module. +> +> If you don't have Python 2 installed you can download and install it [here](https://www.python.org/downloads/). +> +> Then run the following commands in your terminal. +> ``` +> git clone https://github.com/CodingTrain/Logo.git +> cd Logo +> python -m SimpleHTTPServer 8080 +> ``` +> Finally, you can open [http://localhost:8080](http://localhost:8080) in your browser and you're away! +> +> Note that when you close the terminal window, the web server will stop as well. + +

+
+ +
+ +
Python 3.x +

+ +> You can use the [http.server](https://docs.python.org/3/library/http.server.html) python module. +> +> If you don't have Python 3 installed you can download and install it [here](https://www.python.org/downloads/). +> +> +> Then run the following commands in your terminal. +> ``` +> git clone https://github.com/CodingTrain/Logo.git +> cd Logo +> python3 -m http.server 8080 +> ``` +> Finally, you can open [http://localhost:8080](http://localhost:8080) in your browser and you're away! +> +> Note that when you close the terminal window, the web server will stop as well. + +

+
diff --git a/animation.js b/animation.js index 4783c39..0f6b3f8 100644 --- a/animation.js +++ b/animation.js @@ -1,99 +1,99 @@ -let Animation; - -// Use an anonymous closure to keep the AnimationManager private -(() => { - - // From here: gist.github.com/gre/1650294 - const easingFunctions = { - LINEAR : t => t, - EASEINQUAD : t => t ** 2, - EASEOUTQUAD : t => t * (2 - t), - EASEINOUTQUAD : t => t < .5 ? 2 * t ** 2 : -1 + (4 - 2 * t) * t, - EASEINCUBIC : t => t ** 3, - EASEOUTCUBIC : t => --t ** 3 + 1, - EASEINOUTCUBIC : t => t < .5 ? 4 * t ** 3 : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1, - EASEINQUART : t => t ** 4, - EASEOUTQUART : t => 1 - --t ** 4, - EASEINOUTQUART : t => t < .5 ? 8 * t ** 4 : 1 - 8 * --t ** 4, - EASEINQUINT : t => t ** 5, - EASEOUTQUINT : t => 1 + --t ** 5, - EASEINOUTQUINT : t => t < .5 ? 16 * t ** 5 : 1 + 16 * --t ** 5 - } - - function lerp(start, end, ratio) { - return start + (end - start) * ratio; - } - - class AnimationManager { - - constructor() { - this.activeAnimations = []; - this.update = this.update.bind(this); // Make sure 'this' is correct when invoked by requestAnimationFrame - this.update(); - } - - update() { - for (const anim of this.activeAnimations) - anim.update(); - this.activeAnimations = this.activeAnimations.filter(anim => !anim.complete); - requestAnimationFrame(this.update); - } - - registerAnimation(anim) { - this.activeAnimations.push(anim); - } - - } - - const animationManager = new AnimationManager(); - - Animation = class { - - constructor(startingValues, endingValues, duration, updateCallback, finishedCallback, easing = 'EASE_IN_OUT_QUART') { - - /* - * Normalize the easing function name such that all of the following are valid: - * "EASEINOUTCUBIC" - * "EASE_IN_OUT_CUBIC" - * "ease in out cubic" - * Case insensitive, optionally allowing spaces or underscores - */ - easing = easing.toUpperCase().replace(/[^A-Z]/g, ''); - - this.easingFunction = easingFunctions[easing]; - this.duration = duration; - this.startingValues = startingValues; - this.endingValues = endingValues; - this.values = Object.assign({}, this.startingValues); - this.startTime = performance.now(); - this.complete = false; - this.updateCallback = updateCallback; - this.finishedCallback = finishedCallback; - - // Register this animation so that it gets updated periodically - animationManager.registerAnimation(this); - } - - update() { - if (this.complete) return; - const now = performance.now(); - const elapsed = now - this.startTime; - let completionRatio = elapsed / this.duration; - this.complete = completionRatio >= 1; - completionRatio = Math.min(1, this.easingFunction(completionRatio)); - for (const val in this.values) - this.values[val] = lerp(this.startingValues[val], this.endingValues[val], completionRatio); - this.updateCallback(this.values); - if (this.complete) - this.finishedCallback(this.endingValues); - } - - skip() { - this.complete = true; - this.updateCallback(this.endingValues); - this.finishedCallback(this.endingValues); - } - - } - +let Animation; + +// Use an anonymous closure to keep the AnimationManager private +(() => { + + // From here: gist.github.com/gre/1650294 + const easingFunctions = { + LINEAR : t => t, + EASEINQUAD : t => t ** 2, + EASEOUTQUAD : t => t * (2 - t), + EASEINOUTQUAD : t => t < .5 ? 2 * t ** 2 : -1 + (4 - 2 * t) * t, + EASEINCUBIC : t => t ** 3, + EASEOUTCUBIC : t => --t ** 3 + 1, + EASEINOUTCUBIC : t => t < .5 ? 4 * t ** 3 : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1, + EASEINQUART : t => t ** 4, + EASEOUTQUART : t => 1 - --t ** 4, + EASEINOUTQUART : t => t < .5 ? 8 * t ** 4 : 1 - 8 * --t ** 4, + EASEINQUINT : t => t ** 5, + EASEOUTQUINT : t => 1 + --t ** 5, + EASEINOUTQUINT : t => t < .5 ? 16 * t ** 5 : 1 + 16 * --t ** 5 + } + + function lerp(start, end, ratio) { + return start + (end - start) * ratio; + } + + class AnimationManager { + + constructor() { + this.activeAnimations = []; + this.update = this.update.bind(this); // Make sure 'this' is correct when invoked by requestAnimationFrame + this.update(); + } + + update() { + for (const anim of this.activeAnimations) + anim.update(); + this.activeAnimations = this.activeAnimations.filter(anim => !anim.complete); + requestAnimationFrame(this.update); + } + + registerAnimation(anim) { + this.activeAnimations.push(anim); + } + + } + + const animationManager = new AnimationManager(); + + Animation = class { + + constructor(startingValues, endingValues, duration, updateCallback, finishedCallback, easing = 'EASE_IN_OUT_QUART') { + + /* + * Normalize the easing function name such that all of the following are valid: + * "EASEINOUTCUBIC" + * "EASE_IN_OUT_CUBIC" + * "ease in out cubic" + * Case insensitive, optionally allowing spaces or underscores + */ + easing = easing.toUpperCase().replace(/[^A-Z]/g, ''); + + this.easingFunction = easingFunctions[easing]; + this.duration = duration; + this.startingValues = startingValues; + this.endingValues = endingValues; + this.values = Object.assign({}, this.startingValues); + this.startTime = performance.now(); + this.complete = false; + this.updateCallback = updateCallback; + this.finishedCallback = finishedCallback; + + // Register this animation so that it gets updated periodically + animationManager.registerAnimation(this); + } + + update() { + if (this.complete) return; + const now = performance.now(); + const elapsed = now - this.startTime; + let completionRatio = elapsed / this.duration; + this.complete = completionRatio >= 1; + completionRatio = Math.min(1, this.easingFunction(completionRatio)); + for (const val in this.values) + this.values[val] = lerp(this.startingValues[val], this.endingValues[val], completionRatio); + this.updateCallback(this.values); + if (this.complete) + this.finishedCallback(this.endingValues); + } + + skip() { + this.complete = true; + this.updateCallback(this.endingValues); + this.finishedCallback(this.endingValues); + } + + } + })(); \ No newline at end of file diff --git a/bounding_box.js b/bounding_box.js index a992e4b..20ceb18 100644 --- a/bounding_box.js +++ b/bounding_box.js @@ -1,36 +1,36 @@ -class BoundingBox { - - constructor() { - this.reset(); - } - - reset() { - // By default it's positioned with top-left corner at [0,0] with a width and height of 1. - // The x and y values are always the center - this.left = this.top = 0; - this.right = this.bottom = 1; - this.width = this.height = 1; - this.x = this.y = .5; - } - - move(x, y) { - this.left += x; - this.right += x; - this.x += x; - this.top += y; - this.bottom += y; - this.y += y; - } - - includePoint(x, y) { - this.left = Math.min(this.left, x); - this.right = Math.max(this.right, x); - this.top = Math.min(this.top, y); - this.bottom = Math.max(this.bottom, y); - this.width = this.right - this.left; - this.height = this.bottom - this.top; - this.x = this.left + this.width * .5; - this.y = this.top + this.height * .5; - } - +class BoundingBox { + + constructor() { + this.reset(); + } + + reset() { + // By default it's positioned with top-left corner at [0,0] with a width and height of 1. + // The x and y values are always the center + this.left = this.top = 0; + this.right = this.bottom = 1; + this.width = this.height = 1; + this.x = this.y = .5; + } + + move(x, y) { + this.left += x; + this.right += x; + this.x += x; + this.top += y; + this.bottom += y; + this.y += y; + } + + includePoint(x, y) { + this.left = Math.min(this.left, x); + this.right = Math.max(this.right, x); + this.top = Math.min(this.top, y); + this.bottom = Math.max(this.bottom, y); + this.width = this.right - this.left; + this.height = this.bottom - this.top; + this.x = this.left + this.width * .5; + this.y = this.top + this.height * .5; + } + } \ No newline at end of file diff --git a/command.js b/command.js index e23f696..760f086 100644 --- a/command.js +++ b/command.js @@ -1,220 +1,231 @@ -/** - * This are the types a Command Argument can be: - * - STR: String, as its parsed it will returned. - * - INT: Integer, the parsed value will be casted into base10 Integer. - * - FLOAT: Float, the parsed value will be casted into a float js object. - * - COMMANDS: List of commands, the parsed value will be parse into Command - * Executors that will be executed. - * - PARAMETERS: Parameters, it almost doesnt make sense but is here to - * demonstrate the power of this way of parsing the content. - */ -const ARGUMENT_TYPES = { - STR: "STR", - INT: "INT", - FLOAT: "FLOAT", - COMMANDS: "COMMANDS", - EXPRESSION: "EXPRESSION", - PARAMETERS: "PARAMETERS" // Example -}; - -/** - * Argument a command can accept. The most important of this class is the - * type property. - * - * @class CommandArg - */ -class CommandArg { - - /** - * Creates an instance of CommandArg. - * @param {String} name Name of the argument - * @param {ARGUMENT_TYPES} type Type of the argument. Its important because - * this will define how the token is parsed. - * @param {*} validator Function used to check if the argument is valid - * @memberof CommandArg - */ - constructor(name, type, validator = undefined) { - this.name = name; - this.type = type; - if (validator === undefined) { - switch (type) { - case ARGUMENT_TYPES.INT: - this.validator = (str) => { - return /^\d+$/.test(str); - } - break; - case ARGUMENT_TYPES.FLOAT: - this.validator = (str) => { - return /^[-+]?[0-9]*\.?[0-9]*$/.test(str); - } - break; - case ARGUMENT_TYPES.EXPRESSION: - this.validator = (str) => { - let res = /^[-+]?([0-9]+\.?[0-9]?|[0-9]*\.?[0-9]+)(\s[+-/*]{1}\s[-+]?([0-9]+\.?[0-9]?|[0-9]*\.?[0-9]+))*$/.test(str); - return res; - } - break; - case ARGUMENT_TYPES.COMMANDS: - this.validator = (str,offset)=>{ - let p = new Parser(str,null,offset); - - p.parse() - return true; - } - break; - } - } else - this.validator = validator; - } -} - -/** - * Command that can be executed. - * - * @class Command - */ -class Command { - - /** - * Creates an instance of Command. - * @param {String} name Name of the command, againts this name the tokens will be - * matched. - * @param {[CommandArg]} args An array of CommandArg that this command needs in - * order to work. - * @param {*} func The JS function that will be executed when this command needs - * to be executed. - * @memberof Command - */ - constructor(name, args, func) { - this.name = name; - this.argsTemplate = args; - this.func = func; - } -} - -/** - * The command executor purpose is to hold the command to execute and the parsed - * arguments that has been read from the code. - */ -class CommandExecutor { - - /** - * Creates an instance of CommandExecutor. - * @param {Command} command The command you will want to execute. - * @param {[String]} values An array of string tokens, this array needs - * to be the same length of the arguments the commands can accept. - * These values will be casted to the argument type it corresponds to. - * @param {Function} callback Function to execute after the command is executed. - * @memberof CommandExecutor - */ - constructor(command, values, callback) { - this.callback = callback; - this.command = command; - this.values = []; - - for (let i = 0; i < values.length; i++) { - let value = values[i]; - let argTemplate = this.command.argsTemplate[i].type; - - switch (argTemplate) { - case ARGUMENT_TYPES.STR: - this.values.push(value); - break; - case ARGUMENT_TYPES.INT: - this.values.push(parseInt(value)); - break; - case ARGUMENT_TYPES.FLOAT: - this.values.push(parseFloat(value)); - break; - case ARGUMENT_TYPES.COMMANDS: - this.values.push( - new Parser(value, this.callback).parse() - ); - break; - case ARGUMENT_TYPES.EXPRESSION: - this.values.push(this.parseExpression(value).eval()); - break; - case ARGUMENT_TYPES.PARAMETERS: // Example - this.values.push(value.split(" ")); - break; - default: - console.log("Unknown argType: ", argTemplate); - break; - } - } - } - parseExpression(ExpressionString) { - let p = new Parser(ExpressionString,null); - let token = p.nextToken(); - let next = p.nextToken(); - let e; - let right; - if (next == '/' || next == '*' || next == '+' || next == '-') { - right = this.parseExpression(p.getArgs()); - e = new Expression(next,new Expression('$',token), right); - } else - return new Expression('$', token); - if (right.lvl() > e.lvl()) { - let new_left = new Expression(next, new Expression('$',token), right.left); - e = new Expression(right.type, new_left, right.right); - } - return e; - } - /** - * Executes the command with the values given at the creation of the - * instance. - * - * @memberof CommandExecutor - */ - execute(repcount) { - this.command.func.apply(this, this.values); - if (this.callback) { - this.callback(); - } - } -} - -/** - * It stores all the commands available. - * - * @class CommandLookUp - */ -class CommandLookUp { - - /** - * Creates an instance of CommandLookUp. - * @memberokUp - */ - constructor() { - this.commands = []; - } - - /** - * Adding a new command to the list. - * - * @param {Command} command New command to add to the list. - * @memberof CommandLookUp - */ - add(command) { - this.commands.push(command); - } - - /** - * Return a command that matches the name. - * - * @param {String} name The name of the command you want back. - * @returns The command that matches the name, or null if it can't find it. - * @memberof CommandLookUp - */ - get(name) { - let item = null; - let index = 0; - while (!item && index < this.commands.length) { - if (this.commands[index].name === name) item = this.commands[index]; - - index++; - } - - return item; - } -} +/** + * This are the types a Command Argument can be: + * - STR: String, as its parsed it will returned. + * - INT: Integer, the parsed value will be casted into base10 Integer. + * - FLOAT: Float, the parsed value will be casted into a float js object. + * - COMMANDS: List of commands, the parsed value will be parse into Command + * Executors that will be executed. + * - PARAMETERS: Parameters, it almost doesnt make sense but is here to + * demonstrate the power of this way of parsing the content. + */ +const ARGUMENT_TYPES = { + STR: "STR", + INT: "INT", + FLOAT: "FLOAT", + COMMANDS: "COMMANDS", + EXPRESSION: "EXPRESSION", + PARAMETERS: "PARAMETERS", // Example + COLOR: "COLOR" +}; + +/** + * Argument a command can accept. The most important of this class is the + * type property. + * + * @class CommandArg + */ +class CommandArg { + + /** + * Creates an instance of CommandArg. + * @param {String} name Name of the argument + * @param {ARGUMENT_TYPES} type Type of the argument. Its important because + * this will define how the token is parsed. + * @param {*} validator Function used to check if the argument is valid + * @memberof CommandArg + */ + constructor(name, type, validator = undefined) { + this.name = name; + this.type = type; + if (validator === undefined) { + switch (type) { + case ARGUMENT_TYPES.INT: + this.validator = (str) => { + return /^\d+$/.test(str); + } + break; + case ARGUMENT_TYPES.FLOAT: + this.validator = (str) => { + return /^[-+]?[0-9]*\.?[0-9]*$/.test(str); + } + break; + case ARGUMENT_TYPES.EXPRESSION: + this.validator = (str) => { + let res = /^[-+]?([0-9]+\.?[0-9]?|[0-9]*\.?[0-9]+)(\s[+-/*]{1}\s[-+]?([0-9]+\.?[0-9]?|[0-9]*\.?[0-9]+))*$/.test(str); + return res; + } + break; + case ARGUMENT_TYPES.STR: + this.validator = (str) => { + let res = /^[\s\S]+/.test(str); + print(res) + return res; + } + break; + case ARGUMENT_TYPES.COMMANDS: + this.validator = (str,offset)=>{ + let p = new Parser(str,null,offset); + + p.parse() + return true; + } + break; + } + } else + this.validator = validator; + } +} + +/** + * Command that can be executed. + * + * @class Command + */ +class Command { + + /** + * Creates an instance of Command. + * @param {String} name Name of the command, againts this name the tokens will be + * matched. + * @param {[CommandArg]} args An array of CommandArg that this command needs in + * order to work. + * @param {*} func The JS function that will be executed when this command needs + * to be executed. + * @memberof Command + */ + constructor(name, args, func) { + this.name = name; + this.argsTemplate = args; + this.func = func; + } +} + +/** + * The command executor purpose is to hold the command to execute and the parsed + * arguments that has been read from the code. + */ +class CommandExecutor { + + /** + * Creates an instance of CommandExecutor. + * @param {Command} command The command you will want to execute. + * @param {[String]} values An array of string tokens, this array needs + * to be the same length of the arguments the commands can accept. + * These values will be casted to the argument type it corresponds to. + * @param {Function} callback Function to execute after the command is executed. + * @memberof CommandExecutor + */ + constructor(command, values, callback) { + this.callback = callback; + this.command = command; + this.values = []; + + for (let i = 0; i < values.length; i++) { + let value = values[i]; + let argTemplate = this.command.argsTemplate[i].type; + + switch (argTemplate) { + case ARGUMENT_TYPES.STR: + this.values.push(value.substring(1, value.length - 1)); + break; + case ARGUMENT_TYPES.COLOR: + this.values.push(value); + break; + case ARGUMENT_TYPES.INT: + this.values.push(parseInt(value)); + break; + case ARGUMENT_TYPES.FLOAT: + this.values.push(parseFloat(value)); + break; + case ARGUMENT_TYPES.COMMANDS: + this.values.push( + new Parser(value, this.callback).parse() + ); + break; + case ARGUMENT_TYPES.EXPRESSION: + this.values.push(this.parseExpression(value).eval()); + break; + case ARGUMENT_TYPES.PARAMETERS: // Example + this.values.push(value.split(" ")); + break; + default: + console.log("Unknown argType: ", argTemplate); + break; + } + } + } + parseExpression(ExpressionString) { + let p = new Parser(ExpressionString,null); + let token = p.nextToken(); + let next = p.nextToken(); + let e; + let right; + if (next == '/' || next == '*' || next == '+' || next == '-') { + right = this.parseExpression(p.getArgs()); + e = new Expression(next,new Expression('$',token), right); + } else + return new Expression('$', token); + if (right.lvl() > e.lvl()) { + let new_left = new Expression(next, new Expression('$',token), right.left); + e = new Expression(right.type, new_left, right.right); + } + return e; + } + /** + * Executes the command with the values given at the creation of the + * instance. + * + * @memberof CommandExecutor + */ + execute(repcount) { + this.command.func.apply(this, this.values); + if (this.callback) { + this.callback(); + } + } +} + +/** + * It stores all the commands available. + * + * @class CommandLookUp + */ +class CommandLookUp { + + /** + * Creates an instance of CommandLookUp. + * @memberokUp + */ + constructor() { + this.commands = []; + } + + /** + * Adding a new command to the list. + * + * @param {Command} command New command to add to the list. + * @memberof CommandLookUp + */ + add(command) { + this.commands.push(command); + } + + /** + * Return a command that matches the name. + * + * @param {String} name The name of the command you want back. + * @returns The command that matches the name, or null if it can't find it. + * @memberof CommandLookUp + */ + get(name) { + let item = null; + let index = 0; + while (!item && index < this.commands.length) { + if (this.commands[index].name === name) item = this.commands[index]; + + index++; + } + + return item; + } +} diff --git a/commandList.js b/commandList.js index 16fec75..ff26e1d 100644 --- a/commandList.js +++ b/commandList.js @@ -1,190 +1,219 @@ -/** - * This instance of the CommandLookUp class will be the global variable - * that will store all the commands available. - */ -const commandLookUp = new CommandLookUp(); - -/** - * To add a new command, just need the name, the arguments, - * and then the function to execute. - * Note: When adding a new command, update the list of supported commands on the readme file - */ -commandLookUp.add( - new Command("fd", [new CommandArg("value", ARGUMENT_TYPES.EXPRESSION)], value => { - turtle.forward(value); - }) -); - -commandLookUp.add( - new Command("bd", [new CommandArg("value", ARGUMENT_TYPES.EXPRESSION)], value => { - turtle.forward(-value); - }) -); - -commandLookUp.add( - new Command("rt", [new CommandArg("value", ARGUMENT_TYPES.EXPRESSION)], value => { - turtle.right(value); - }) -); - -commandLookUp.add( - new Command("lt", [new CommandArg("value", ARGUMENT_TYPES.EXPRESSION)], value => { - turtle.right(-value); - }) -); - -commandLookUp.add( - new Command("pu", [], () => { - turtle.pen = false; - }) -); - -commandLookUp.add( - new Command("pd", [], () => { - turtle.pen = true; - }) -); - -commandLookUp.add( - new Command( - "pensize", - [new CommandArg("size", ARGUMENT_TYPES.EXPRESSION)], - size => { - turtle.strokeWeight = size; - } - ) -); - -commandLookUp.add( - new Command( - "setxy", - [ - new CommandArg("x", ARGUMENT_TYPES.EXPRESSION), - new CommandArg("y", ARGUMENT_TYPES.EXPRESSION) - ], - (x, y) => { - turtle.x = x; - turtle.y = y; - } - ) -); - -commandLookUp.add( - new Command("setx", [new CommandArg("x", ARGUMENT_TYPES.EXPRESSION)], x => { - turtle.x = x; - }) -); - -commandLookUp.add( - new Command("sety", [new CommandArg("y", ARGUMENT_TYPES.EXPRESSION)], y => { - turtle.y = y; - }) -); - -commandLookUp.add( - new Command("home", [], () => { - turtle["home"](); - }) -); - -commandLookUp.add( - new Command("radians", [], () => { - angleMode(RADIANS); - }) -); - -commandLookUp.add( - new Command("degrees", [], () => { - angleMode(DEGREES); - }) -); - -commandLookUp.add( - new Command( - "repeat", - [ - new CommandArg("lengthLoop", ARGUMENT_TYPES.INT), - new CommandArg("commands", ARGUMENT_TYPES.COMMANDS) - ], - function(lengthLoop, commands) { - for (let i = 0; i < lengthLoop; i++) { - for (let cmd of commands) { - cmd.execute(i + 1); - } - } - } - ) -); - -/** - * Color, added as example. Given a value, it set the stroke. - */ -commandLookUp.add( - new Command("color", [new CommandArg("color", ARGUMENT_TYPES.STR,(str)=>{ - return /^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/.test(str); - })], color => { - // sanity sake let you use hex without the need for # - if (color[0] != "#") { - color = "#" + color; - } - - turtle.strokeColor = color; - }) -); - -/* - * Not apart of logo this allows us to use a RGB instead of HEX - * Though not standard in logo this just gives us a slightly more fine grain color - */ -commandLookUp.add( - new Command( - "colorrgb", - [new CommandArg("params", ARGUMENT_TYPES.PARAMETERS)], - params => { - let [r, g, b] = params; - r = parseInt(r); - g = parseInt(g); - b = parseInt(b); - - if (r > 255) { - r = 255; - } - if (r < 0) { - r = 0; - } - - if (g > 255) { - g = 255; - } - if (g < 0) { - g = 0; - } - - if (b > 255) { - b = 255; - } - if (r < 0) { - b = 0; - } - - turtle.strokeColor = color(r, g, b); - } - ) -); - -/** - * Added as example of taking [...] as not only commands - * but strings you can later process. - * This command expects 3 args separated by spaces. - */ -commandLookUp.add( - new Command( - "author", - [new CommandArg("params", ARGUMENT_TYPES.PARAMETERS)], - params => { - const [author, website, twitter] = params; - console.log("This repository has been created by:"); - console.log(`${author} (@${twitter}) - ${website}`); - } - ) -); +/** + * This instance of the CommandLookUp class will be the global variable + * that will store all the commands available. + */ +const commandLookUp = new CommandLookUp(); + +/** + * To add a new command, just need the name, the arguments, + * and then the function to execute. + * Note: When adding a new command, update the list of supported commands on the readme file + */ + +commandLookUp.add( + new Command("label", [new CommandArg("displayText", ARGUMENT_TYPES.PARAMETERS)], params => { + textAlign(CENTER); + let realText = ""; + for (let param of params) { + if (param.charAt(0) == '"') { + realText += param.substring(1); + } else { + realText += param; + } + realText += " "; + } + turtle.label(realText); + }) +); + +commandLookUp.add( + new Command("cs", [], () => { + background(bgcolor); + }) +); + +commandLookUp.add( + new Command("setfontsize", [new CommandArg("size", ARGUMENT_TYPES.INT)], displayText => { + textSize(displayText); + }) +); + +commandLookUp.add( + new Command("fd", [new CommandArg("value", ARGUMENT_TYPES.EXPRESSION)], value => { + turtle.forward(value); + }) +); + +commandLookUp.add( + new Command("bd", [new CommandArg("value", ARGUMENT_TYPES.EXPRESSION)], value => { + turtle.forward(-value); + }) +); + +commandLookUp.add( + new Command("rt", [new CommandArg("value", ARGUMENT_TYPES.EXPRESSION)], value => { + turtle.right(value); + }) +); + +commandLookUp.add( + new Command("lt", [new CommandArg("value", ARGUMENT_TYPES.EXPRESSION)], value => { + turtle.right(-value); + }) +); + +commandLookUp.add( + new Command("pu", [], () => { + turtle.pen = false; + }) +); + +commandLookUp.add( + new Command("pd", [], () => { + turtle.pen = true; + }) +); + +commandLookUp.add( + new Command( + "pensize", + [new CommandArg("size", ARGUMENT_TYPES.EXPRESSION)], + size => { + turtle.strokeWeight = size; + } + ) +); + +commandLookUp.add( + new Command( + "setxy", + [ + new CommandArg("x", ARGUMENT_TYPES.EXPRESSION), + new CommandArg("y", ARGUMENT_TYPES.EXPRESSION) + ], + (x, y) => { + turtle.x = x; + turtle.y = y; + } + ) +); + +commandLookUp.add( + new Command("setx", [new CommandArg("x", ARGUMENT_TYPES.EXPRESSION)], x => { + turtle.x = x; + }) +); + +commandLookUp.add( + new Command("sety", [new CommandArg("y", ARGUMENT_TYPES.EXPRESSION)], y => { + turtle.y = y; + }) +); + +commandLookUp.add( + new Command("home", [], () => { + turtle["home"](); + }) +); + +commandLookUp.add( + new Command("radians", [], () => { + angleMode(RADIANS); + }) +); + +commandLookUp.add( + new Command("degrees", [], () => { + angleMode(DEGREES); + }) +); + +commandLookUp.add( + new Command( + "repeat", + [ + new CommandArg("lengthLoop", ARGUMENT_TYPES.INT), + new CommandArg("commands", ARGUMENT_TYPES.COMMANDS) + ], + function(lengthLoop, commands) { + for (let i = 0; i < lengthLoop; i++) { + for (let cmd of commands) { + cmd.execute(i + 1); + } + } + } + ) +); + +/** + * Color, added as example. Given a value, it set the stroke. + */ +commandLookUp.add( + new Command("color", [new CommandArg("color", ARGUMENT_TYPES.COLOR,(str)=>{ + return /^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/.test(str); + })], color => { + // sanity sake let you use hex without the need for # + if (color[0] != "#") { + color = "#" + color; + } + + turtle.strokeColor = color; + }) +); + +/* + * Not apart of logo this allows us to use a RGB instead of HEX + * Though not standard in logo this just gives us a slightly more fine grain color + */ +commandLookUp.add( + new Command( + "colorrgb", + [new CommandArg("params", ARGUMENT_TYPES.PARAMETERS)], + params => { + let [r, g, b] = params; + r = parseInt(r); + g = parseInt(g); + b = parseInt(b); + + if (r > 255) { + r = 255; + } + if (r < 0) { + r = 0; + } + + if (g > 255) { + g = 255; + } + if (g < 0) { + g = 0; + } + + if (b > 255) { + b = 255; + } + if (r < 0) { + b = 0; + } + + turtle.strokeColor = color(r, g, b); + } + ) +); + +/** + * Added as example of taking [...] as not only commands + * but strings you can later process. + * This command expects 3 args separated by spaces. + */ +commandLookUp.add( + new Command( + "author", + [new CommandArg("params", ARGUMENT_TYPES.PARAMETERS)], + params => { + const [author, website, twitter] = params; + console.log("This repository has been created by:"); + console.log(`${author} (@${twitter}) - ${website}`); + } + ) +); diff --git a/expression.js b/expression.js index e4d2e12..7e3222e 100644 --- a/expression.js +++ b/expression.js @@ -1,31 +1,31 @@ -class Expression { - constructor(type, left, right=undefined) { - this.type = type; - this.left = left; - this.right = right; - } - - eval() { - if(this.type == '$') { - return parseFloat(this.left); - } else if(this.type == '/') { - return this.left.eval() / this.right.eval(); - } else if(this.type == '*') { - return this.left.eval() * this.right.eval(); - } else if(this.type == '-') { - return this.left.eval() - this.right.eval(); - } else if(this.type == '+') { - return this.left.eval() + this.right.eval(); - } - return 0; - } - - lvl() { - if(this.type == '+') return 4; - if(this.type == '-') return 3; - if(this.type == '*') return 2; - if(this.type == '/') return 1; - return 0; - } - -} +class Expression { + constructor(type, left, right=undefined) { + this.type = type; + this.left = left; + this.right = right; + } + + eval() { + if(this.type == '$') { + return parseFloat(this.left); + } else if(this.type == '/') { + return this.left.eval() / this.right.eval(); + } else if(this.type == '*') { + return this.left.eval() * this.right.eval(); + } else if(this.type == '-') { + return this.left.eval() - this.right.eval(); + } else if(this.type == '+') { + return this.left.eval() + this.right.eval(); + } + return 0; + } + + lvl() { + if(this.type == '+') return 4; + if(this.type == '-') return 3; + if(this.type == '*') return 2; + if(this.type == '/') return 1; + return 0; + } + +} diff --git a/index.html b/index.html index 26959f7..59d6034 100644 --- a/index.html +++ b/index.html @@ -1,57 +1,57 @@ - - - - - - - - Logo - - - - - - - - - - - - - - - - - - - -
- - -
-
-
-
-
- - - - Icons made by Turtle from www.flaticon.com is licensed by CC 3.0 BY - - - - - - + + + + + + + + Logo + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+ + + + Icons made by Turtle from www.flaticon.com is licensed by CC 3.0 BY + + + + + + diff --git a/parser.js b/parser.js index 811130d..23753ff 100644 --- a/parser.js +++ b/parser.js @@ -1,139 +1,145 @@ - -class Parser { - - /** - * Creates an instance of Parser. - * @param {String} text The text to parse - * @param {Function} afterCmdCallback Function to execute after the commands are executed - * @memberof Parser - */ - constructor(text, afterCmdCallback,offset = 0) { - if (!text) text = ''; - - this.text = text.trimRight(); - this.index = 0; - this.afterCmdCallback = afterCmdCallback - this.offset = offset; - } - - /** - * Private method - * - * @returns Boolean If the index has surpased the length of the text or not. - * @memberof Parser - */ - remainingTokens() { - return this.index < this.text.length; - } - - /** - * Private method - * - * @returns String The next token after the actual index. - * @memberof Parser - */ - nextToken() { - let regWhitespace = /\s/; - - while (regWhitespace.test(this.text.charAt(this.index)) && this.remainingTokens()) this.index++; - - let firstChar = this.text.charAt(this.index); - - let token = ''; - let isTokenList = false; - let depth = 0; - - - if (firstChar === '[') { - this.index++; - depth++; - isTokenList = true; - } - - let actualChar = this.text.charAt(this.index); - - while (((regWhitespace.test(actualChar) && isTokenList) || !regWhitespace.test(actualChar)) && this.remainingTokens()) { - this.index++; - - if (isTokenList) { - if (actualChar === '[') depth++; - else if (actualChar === ']') depth--; - - if (actualChar === ']' && depth === 0) return token; - } - - token += actualChar; - actualChar = this.text.charAt(this.index); - } - - return token; - } - - getArgs() { - let ret = this.nextToken(); - let temp = this.index; - let next = this.nextToken(); - if (next == '/' || next == '*' || next == '+' || next == '-') { - return ret + ' ' + next + ' ' + this.getArgs(); - } - else { - this.index = temp; - return ret; - } - } - - /** - * Public method - * - * @returns [CommandExecutor] Parsed text converted into CommandExecutors - * ready to be executed. - * @memberof Parser - */ - parse() { - let cmdsExecutors = []; - while (this.remainingTokens()) { - let cmdStrat = this.index; - let token = this.nextToken(); - let cmd = commandLookUp.get(token); - let args = []; - if (cmd) { - for (let i = 0; i < cmd.argsTemplate.length; i++) { - let startIndex = this.index; - let arg = cmd.argsTemplate[i]; - let theArgToken = this.getArgs(); - let endIndex = this.index + 1; - if (arg.validator !== undefined) { - let valid; - if(arg.type == ARGUMENT_TYPES.COMMANDS) - { - while(this.text.length>startIndex && this.text[startIndex++]!='['); - console.log({arg:theArgToken,offset:startIndex}); - valid = arg.validator(theArgToken,startIndex+this.offset) - }else - valid = arg.validator(theArgToken); - if (!valid) { - console.error(`Argument number ${i} (${theArgToken}) is invalid for command ${token} parser offset ${this.offset}`); - throw { - startIndex: startIndex+this.offset, - endIndex: endIndex+this.offset - } - } - args.push(theArgToken); - } - else { - args.push(theArgToken); - } - - } - cmdsExecutors.push(new CommandExecutor(cmd, args, this.afterCmdCallback)); - }else { - let endIndex = this.index + 1; - throw { - startIndex: cmdStrat+this.offset, - endIndex: endIndex+this.offset - } - } - } - return cmdsExecutors; - } -} + +class Parser { + + /** + * Creates an instance of Parser. + * @param {String} text The text to parse + * @param {Function} afterCmdCallback Function to execute after the commands are executed + * @memberof Parser + */ + constructor(text, afterCmdCallback,offset = 0) { + if (!text) text = ''; + + this.text = text.trimRight(); + this.index = 0; + this.afterCmdCallback = afterCmdCallback + this.offset = offset; + } + + /** + * Private method + * + * @returns Boolean If the index has surpased the length of the text or not. + * @memberof Parser + */ + remainingTokens() { + return this.index < this.text.length; + } + + /** + * Private method + * + * @returns String The next token after the actual index. + * @memberof Parser + */ + nextToken() { + let regWhitespace = /\s/; + + while (regWhitespace.test(this.text.charAt(this.index)) && this.remainingTokens()) this.index++; + + let firstChar = this.text.charAt(this.index); + + let token = ''; + let isTokenList = false; + let depth = 0; + + + if (firstChar === '[') { + this.index++; + depth++; + isTokenList = true; + } + + let actualChar = this.text.charAt(this.index); + + while (((regWhitespace.test(actualChar) && isTokenList) || !regWhitespace.test(actualChar)) && this.remainingTokens()) { + this.index++; + + if (isTokenList) { + if (actualChar === '[') depth++; + else if (actualChar === ']') depth--; + + if (actualChar === ']' && depth === 0) return token; + } + + token += actualChar; + actualChar = this.text.charAt(this.index); + } + + return token; + } + + getArgs() { + let ret = this.nextToken(); + for (let i = 1; i < ret.length; i++) { + let char = ret.charAt(i); + if (char === '/' || char === '*' || char === '-' || char === '+') { + return ret.substring(0, i) + ' ' + char + ' ' + ret.substring(i + 1); + } + } + let temp = this.index; + let next = this.nextToken(); + if (next == '/' || next == '*' || next == '+' || next == '-') { + return ret + ' ' + next + ' ' + this.getArgs(); + } + else { + this.index = temp; + return ret; + } + } + + /** + * Public method + * + * @returns [CommandExecutor] Parsed text converted into CommandExecutors + * ready to be executed. + * @memberof Parser + */ + parse() { + let cmdsExecutors = []; + while (this.remainingTokens()) { + let cmdStrat = this.index; + let token = this.nextToken(); + let cmd = commandLookUp.get(token); + let args = []; + if (cmd) { + for (let i = 0; i < cmd.argsTemplate.length; i++) { + let startIndex = this.index; + let arg = cmd.argsTemplate[i]; + let theArgToken = this.getArgs(); + let endIndex = this.index + 1; + if (arg.validator !== undefined) { + let valid; + if(arg.type == ARGUMENT_TYPES.COMMANDS) + { + while(this.text.length>startIndex && this.text[startIndex++]!='['); + console.log({arg:theArgToken,offset:startIndex}); + valid = arg.validator(theArgToken,startIndex+this.offset) + }else + valid = arg.validator(theArgToken); + if (!valid) { + console.error(`Argument number ${i} (${theArgToken}) is invalid for command ${token} parser offset ${this.offset}`); + throw { + startIndex: startIndex+this.offset, + endIndex: endIndex+this.offset + } + } + args.push(theArgToken); + } + else { + args.push(theArgToken); + } + + } + cmdsExecutors.push(new CommandExecutor(cmd, args, this.afterCmdCallback)); + }else { + let endIndex = this.index + 1; + throw { + startIndex: cmdStrat+this.offset, + endIndex: endIndex+this.offset + } + } + } + return cmdsExecutors; + } +} diff --git a/sketch.js b/sketch.js index 4e2255f..c24c1b5 100644 --- a/sketch.js +++ b/sketch.js @@ -1,361 +1,379 @@ -// Coding Challenge 121: Logo -// https://youtu.be/i-k04yzfMpw - -let canvas; -let editor; -let editor_bg; -let turtle; -let recentreBtn; -let bgcolorBtn; - -let draggingCanvas = false; -let dragStartMousePos = new p5.Vector(); -let dragStartCanvasOffset = new p5.Vector(); -let allCases; -let bgcolor = "#6040e6"; - -let resizingEditor = false; - -// Used for scaling drawings to fit the canvas -let canvasScrollX = 0; -let canvasScrollY = 0; -let canvasScaleX = 1; -let canvasScaleY = 1; -let drawingBounds = new BoundingBox(); -let drawingBoundsFirstPoint = true; -let centerAnimation = null; - -let primary_keywords = ['fd', 'bd', 'rt', 'lt']; -let secondary_keywords = ['color', 'colorrgb', 'pensize', 'repeat', 'pu', 'pd']; - -function syntaxHighlight(code) { - let tokens = code.split(/(\[\s*\d+\s+\d+\s+\d+\s*\])|(\[)|(\])|(\s+)/).filter(x => (x !== undefined && x != '')); - let newHTML = ''; - - for (i of tokens) { - if (primary_keywords.includes(i)) { // #f48771 - newHTML += `${i}`; - } - - else if (secondary_keywords.includes(i)) { - newHTML += `${i}`; - } - - else if (['[', ']'].includes(i)) { - newHTML += `${i}`; - } - - else if (/#[a-zA-z0-9]/.test(i) || /\[\s*\d+\s+\d+\s+\d+\s*\]/.test(i)) { - newHTML += `${i}`; - } - - else if (parseInt(i) !== NaN) { - newHTML += `${i}`; - } - - else { - newHTML += `${i}`; - } - } - - return newHTML; -} - -function showError(start,end) -{ - let text = editor.value(); - start = (start > 0) ? start + 1 : start; - end = (start < text.length) ? end - 1 : end; - - let beforeErr= text.substring(0,start); - let afterErr = text.substring(end); - let err = text.substring(start,end); - let bg_text = `${syntaxHighlight(beforeErr)}${err}${syntaxHighlight(afterErr)}`; - editor_bg.html(bg_text); -} - -/* - * Returns a function that waits for `delay` milliseconds and if it had not been called again, invokes `func` - * If it _is_ called again within `delay` milliseconds, `func` is *not* invoked (yet) and the timer is reset. - */ -function debounce(func, delay) { - let timeout; - return () => { - clearTimeout(timeout); - timeout = setTimeout(() => { - func(); - timeout = null; - }, delay); - } -} - -function canvasResized() { - scaleToFitBoundingBox(drawingBounds, true); -} - -const debouncedCanvasResized = debounce(canvasResized, 300); - -function preload() { - loadJSON("./assets/tests.json", createTestDataView); -} - -function getCanvasSize() { - const bounds = select("#logo-canvas").elt.getBoundingClientRect(); - return { width: bounds.width, height: bounds.height }; -} - -function updateEditorOverlayMargin() { - const editorBounds = select('#editor-container').elt.getBoundingClientRect(); - editor_bg.elt.style.right = `${editorBounds.width - editor.elt.scrollWidth - 3}px`; -} - -function windowResized() { - const canvasSize = getCanvasSize(); - resizeCanvas(canvasSize.width, canvasSize.height); - updateResizeHandlePosition(); - debouncedCanvasResized(); - goTurtle(); - updateEditorOverlayMargin(); -} - -function setup() { - const canvasSize = getCanvasSize(); - canvas = createCanvas(canvasSize.width, canvasSize.height); - div = document.querySelector("#logo-canvas"); - - div.appendChild(canvas.elt); - - // Use a setTimeout with length 0 to call windowResized immediately after the DOM has updated (since we are dynamically injecting a canvas element) - setTimeout(() => { - windowResized(); - scaleToFitBoundingBox(drawingBounds); - }, 0); - - angleMode(DEGREES); - background(bgcolor); - - canvas.mousePressed(function () { - if (mouseIsPressed) { - draggingCanvas = true; - dragStartMousePos = new p5.Vector(mouseX, mouseY); - dragStartCanvasOffset = new p5.Vector(canvasScrollX, canvasScrollY); - } - }); - - canvas.mouseMoved(function () { - if (mouseIsPressed && draggingCanvas) { - canvasScrollX = dragStartCanvasOffset.x + dragStartMousePos.x - mouseX; - canvasScrollY = dragStartCanvasOffset.y + dragStartMousePos.y - mouseY; - goTurtle(); - } - }); - - canvas.mouseReleased(function () { - draggingCanvas = false; - }); - - recentreBtn = document.querySelector("#recentre"); - bgcolorBtn = document.querySelector("#bgcolor"); - - recentreBtn.onclick = function () { - scaleToFitBoundingBox(drawingBounds, true); - } - - bgcolorBtn.onclick = function () { - let r = floor(random(0, 255)); - let g = floor(random(0, 255)); - let b = floor(random(0, 255)); - - let hexR = `${r <= 15 ? '0' : ''}${r.toString(16)}`; - let hexG = `${g <= 15 ? '0' : ''}${g.toString(16)}`; - let hexB = `${b <= 15 ? '0' : ''}${b.toString(16)}`; - - let col = `#${hexR}${hexG}${hexB}`; - bgcolor = col; - goTurtle(); - } - - editor = select("#code"); - setDefaultDrawing(); - editor.input(() => { - updateEditorContent(); - goTurtle(); - }); - editor_bg = select("#code_bg"); - scaleToFitBoundingBox(drawingBounds); // This also redraws (it has to in order to measure the size of the drawing) - updateEditorContent(); - - editor.elt.addEventListener('scroll', ev => { - select('#code_bg').elt.scrollTop = editor.elt.scrollTop; - }, { passive: true }); // The 'passive: true' parameter increases performance when scrolling by making it impossible to cancel the scroll events -} - -function scaleToFitBoundingBox(boundingBox, animate = false) { - // If it's already animating, wait for it to finish - if (centerAnimation !== null) return; - - // Run this once first so we can measure the size of the drawing - goTurtle(); - - // 15% padding around the drawing in the canvas - let drawingPadding = Math.min(width, height) * 0.15; - let scale = Math.min((width - drawingPadding) / (boundingBox.width), (height - drawingPadding) / (boundingBox.height)); - - centerAnimation = new Animation( - { // Starting values - scaleX: canvasScaleX, - scaleY: canvasScaleY, - scrollX: canvasScrollX, - scrollY: canvasScrollY - }, - { // Ending values - scaleX: scale, - scaleY: scale, - scrollX: (drawingBounds.x * scale - width * .5), - scrollY: (drawingBounds.y * scale - height * .5) - }, - 500, // Duration - values => { // Update callback - canvasScaleX = values.scaleX; - canvasScaleY = values.scaleY; - canvasScrollX = values.scrollX; - canvasScrollY = values.scrollY; - goTurtle(); - }, - () => { // Finished callback - centerAnimation = null; - }, - 'EASE_IN_OUT_QUART' // Easing function - ); - - if (!animate) - centerAnimation.skip(); -} - -function afterCommandExecuted() { - if (turtle.pen) { - if (drawingBoundsFirstPoint) { - drawingBounds.reset(); - drawingBounds.move(turtle.x, turtle.y); - drawingBoundsFirstPoint = false; - } else { - drawingBounds.includePoint(turtle.x, turtle.y); - drawingBounds.includePoint(turtle.prevX, turtle.prevY); - } - } -} - -function goTurtle() { - turtle = new Turtle(0, 0, 0); - drawingBoundsFirstPoint = true; - background(bgcolor); - - push(); - translate(-canvasScrollX, -canvasScrollY); - scale(canvasScaleX, canvasScaleY); - - push(); - turtle.reset(); - let code = editor.value(); - let parser = new Parser(code, afterCommandExecuted); - try { - let commands = parser.parse(); - for (let cmd of commands) { - cmd.execute(); - } - } catch (err) { - showError(err.startIndex,err.endIndex); - } - - pop(); - - pop(); -} - -/** - * Writes the Logo code for the default drawing to the textarea - * Called on page load - * Also called when selecting the default item from the #testdata dropdown - */ -function setDefaultDrawing() { - editor.value("pu lt 90 fd 100 lt 90 fd 250 rt 90 rt 90 pd fd 500 rt 90 fd 150 rt 90 fd 500 rt 90 fd 150"); -} - -function createTestDataView(cases) { - let selector = select("#testdata"); - - selector.option("Logo Default", -1); - - for (i = 0; i < cases.length; i++) { - selector.option(cases[i].name, i); - } - - // because why not do it here - selector.changed(function () { - let val = parseInt(selector.value()); - if (val < 0) { - // Use the default drawing - setDefaultDrawing(); - } else { - // Use a drawing from tests.json - editor.value(cases[val].code); - } - - // Reset default parameters for turtle - turtle.strokeColor = 255; - turtle.dir = 0; - turtle.x = 0; - turtle.y = 0; - - // Reset default parameters for camera - canvasScrollX = 0; - canvasScrollY = 0; - canvasScaleX = 1; - canvasScaleY = 1; - - // Move and scale the drawing to fit on-screen - scaleToFitBoundingBox(drawingBounds); - - updateEditorOverlayMargin(); - updateEditorContent(); - }); -} - -function updateResizeHandlePosition() { - const resizeHandle = select('#resize-handle').elt; - const editorBounds = select('#editor-container').elt.getBoundingClientRect(); - resizeHandle.style.top = `${editorBounds.top - 8}px`; - resizeHandle.style.width = `${editorBounds.width}px`; -} - -function updateEditorSize(mouseY) { - const resizeHandleBounds = select('#resize-handle').elt.getBoundingClientRect(); - const editorBounds = select('#editor-container').elt.getBoundingClientRect(); - select('#editor-container').elt.style.height = `${editorBounds.bottom - mouseY - 8}px`; - windowResized(); - updateResizeHandlePosition(); -} - -function updateEditorContent() { - editor_bg.html(syntaxHighlight(editor.value())); -} - -function resizeHandleMouseDown(ev) { - resizingEditor = true; - ev.preventDefault(); - return false; -} - -function windowMouseMove(ev) { - if (resizingEditor) - updateEditorSize(ev.clientY); -} - -function windowMouseUp() { - resizingEditor = false; - updateResizeHandlePosition(); -} - -document.getElementById('resize-handle').addEventListener('mousedown', resizeHandleMouseDown); -document.addEventListener('mousemove', windowMouseMove); -document.addEventListener('mouseup', windowMouseUp); +// Coding Challenge 121: Logo +// https://youtu.be/i-k04yzfMpw + +let canvas; +let editor; +let editor_bg; +let turtle; +let recentreBtn; +let bgcolorBtn; + +let draggingCanvas = false; +let dragStartMousePos = new p5.Vector(); +let dragStartCanvasOffset = new p5.Vector(); +let allCases; +let bgcolor = "#6040e6"; + +let resizingEditor = false; + +// Used for scaling drawings to fit the canvas +let canvasScrollX = 0; +let canvasScrollY = 0; +let canvasScaleX = 1; +let canvasScaleY = 1; +let drawingBounds = new BoundingBox(); +let drawingBoundsFirstPoint = true; +let centerAnimation = null; + +let primary_keywords = ['fd', 'bd', 'rt', 'lt']; +let secondary_keywords = ['color', 'colorrgb', 'pensize', 'repeat', 'pu', 'pd']; + +function syntaxHighlight(code) { + let tokens = code.split(/(\[\s*\d+\s+\d+\s+\d+\s*\])|(\[)|(\])|(\s+)/).filter(x => (x !== undefined && x != '')); + let newHTML = ''; + + for (i of tokens) { + if (primary_keywords.includes(i)) { // #f48771 + newHTML += `${i}`; + } + + else if (secondary_keywords.includes(i)) { + newHTML += `${i}`; + } + + else if (['[', ']'].includes(i)) { + newHTML += `${i}`; + } + + else if (/#[a-zA-z0-9]/.test(i) || /\[\s*\d+\s+\d+\s+\d+\s*\]/.test(i)) { + newHTML += `${i}`; + } + + else if (parseInt(i) !== NaN) { + newHTML += `${i}`; + } + + else { + newHTML += `${i}`; + } + } + + return newHTML; +} + +function showError(start,end) +{ + let text = editor.value(); + start = (start > 0) ? start + 1 : start; + end = (start < text.length) ? end - 1 : end; + + let beforeErr= text.substring(0,start); + let afterErr = text.substring(end); + let err = text.substring(start,end); + let bg_text = `${syntaxHighlight(beforeErr)}${err}${syntaxHighlight(afterErr)}`; + editor_bg.html(bg_text); +} + +/* + * Returns a function that waits for `delay` milliseconds and if it had not been called again, invokes `func` + * If it _is_ called again within `delay` milliseconds, `func` is *not* invoked (yet) and the timer is reset. + */ +function debounce(func, delay) { + let timeout; + return () => { + clearTimeout(timeout); + timeout = setTimeout(() => { + func(); + timeout = null; + }, delay); + } +} + +function canvasResized() { + scaleToFitBoundingBox(drawingBounds, true); +} + +const debouncedCanvasResized = debounce(canvasResized, 300); + +function preload() { + loadJSON("./assets/tests.json", createTestDataView); +} + +function getCanvasSize() { + const bounds = select("#logo-canvas").elt.getBoundingClientRect(); + return { width: bounds.width, height: bounds.height }; +} + +function updateEditorOverlayMargin() { + const editorBounds = select('#editor-container').elt.getBoundingClientRect(); + editor_bg.elt.style.right = `${editorBounds.width - editor.elt.scrollWidth - 3}px`; +} + +function windowResized() { + const canvasSize = getCanvasSize(); + resizeCanvas(canvasSize.width, canvasSize.height); + updateResizeHandlePosition(); + debouncedCanvasResized(); + goTurtle(); + updateEditorOverlayMargin(); +} + +function setup() { + const canvasSize = getCanvasSize(); + canvas = createCanvas(canvasSize.width, canvasSize.height); + div = document.querySelector("#logo-canvas"); + + div.appendChild(canvas.elt); + + // Use a setTimeout with length 0 to call windowResized immediately after the DOM has updated (since we are dynamically injecting a canvas element) + setTimeout(() => { + windowResized(); + scaleToFitBoundingBox(drawingBounds); + }, 0); + + angleMode(DEGREES); + background(bgcolor); + + canvas.mousePressed(function () { + if (mouseIsPressed) { + draggingCanvas = true; + dragStartMousePos = new p5.Vector(mouseX, mouseY); + dragStartCanvasOffset = new p5.Vector(canvasScrollX, canvasScrollY); + } + }); + + canvas.mouseMoved(function () { + if (mouseIsPressed && draggingCanvas) { + canvasScrollX = dragStartCanvasOffset.x + dragStartMousePos.x - mouseX; + canvasScrollY = dragStartCanvasOffset.y + dragStartMousePos.y - mouseY; + goTurtle(); + } + }); + + canvas.mouseReleased(function () { + draggingCanvas = false; + }); + + recentreBtn = document.querySelector("#recentre"); + bgcolorBtn = document.querySelector("#bgcolor"); + + recentreBtn.onclick = function () { + scaleToFitBoundingBox(drawingBounds, true); + } + + bgcolorBtn.onclick = function () { + let r = floor(random(0, 255)); + let g = floor(random(0, 255)); + let b = floor(random(0, 255)); + + let hexR = `${r <= 15 ? '0' : ''}${r.toString(16)}`; + let hexG = `${g <= 15 ? '0' : ''}${g.toString(16)}`; + let hexB = `${b <= 15 ? '0' : ''}${b.toString(16)}`; + + let col = `#${hexR}${hexG}${hexB}`; + bgcolor = col; + goTurtle(); + } + + editor = select("#code"); + + setDefaultDrawing(); + + let editorValue = localStorage.getItem("logo"); + if (editorValue) { + editor.value(editorValue); + } + editor.input(() => { + updateEditorContent(); + goTurtle(); + }); + editor_bg = select("#code_bg"); + scaleToFitBoundingBox(drawingBounds); // This also redraws (it has to in order to measure the size of the drawing) + updateEditorContent(); + + editor.elt.addEventListener('scroll', ev => { + select('#code_bg').elt.scrollTop = editor.elt.scrollTop; + }, { passive: true }); // The 'passive: true' parameter increases performance when scrolling by making it impossible to cancel the scroll events +} + +function scaleToFitBoundingBox(boundingBox, animate = false) { + // If it's already animating, wait for it to finish + if (centerAnimation !== null) return; + + // Run this once first so we can measure the size of the drawing + goTurtle(); + + // 15% padding around the drawing in the canvas + let drawingPadding = Math.min(width, height) * 0.15; + let scale = Math.min((width - drawingPadding) / (boundingBox.width), (height - drawingPadding) / (boundingBox.height)); + + centerAnimation = new Animation( + { // Starting values + scaleX: canvasScaleX, + scaleY: canvasScaleY, + scrollX: canvasScrollX, + scrollY: canvasScrollY + }, + { // Ending values + scaleX: scale, + scaleY: scale, + scrollX: (drawingBounds.x * scale - width * .5), + scrollY: (drawingBounds.y * scale - height * .5) + }, + 500, // Duration + values => { // Update callback + canvasScaleX = values.scaleX; + canvasScaleY = values.scaleY; + canvasScrollX = values.scrollX; + canvasScrollY = values.scrollY; + goTurtle(); + }, + () => { // Finished callback + centerAnimation = null; + }, + 'EASE_IN_OUT_QUART' // Easing function + ); + + if (!animate) + centerAnimation.skip(); +} + +function afterCommandExecuted() { + if (turtle.pen) { + if (drawingBoundsFirstPoint) { + drawingBounds.reset(); + drawingBounds.move(turtle.x, turtle.y); + drawingBoundsFirstPoint = false; + } else { + drawingBounds.includePoint(turtle.x, turtle.y); + drawingBounds.includePoint(turtle.prevX, turtle.prevY); + } + } +} + +function goTurtle() { + turtle = new Turtle(0, 0, 0); + drawingBoundsFirstPoint = true; + background(bgcolor); + + push(); + translate(-canvasScrollX, -canvasScrollY); + scale(canvasScaleX, canvasScaleY); + + push(); + turtle.reset(); + let code = editor.value(); + let parser = new Parser(code, afterCommandExecuted); + try { + let commands = parser.parse(); + for (let cmd of commands) { + cmd.execute(); + } + } catch (err) { + showError(err.startIndex,err.endIndex); + } + + pop(); + + pop(); +} + +/** + * Writes the Logo code for the default drawing to the textarea + * Called on page load + * Also called when selecting the default item from the #testdata dropdown + */ +function setDefaultDrawing() { + editor.value("pu lt 90 fd 100 lt 90 fd 250 rt 90 rt 90 pd fd 500 rt 90 fd 150 rt 90 fd 500 rt 90 fd 150"); +} + +function createTestDataView(cases) { + let selector = select("#testdata"); + + selector.option("Logo Default", -1); + + for (i = 0; i < cases.length; i++) { + selector.option(cases[i].name, i); + } + + // because why not do it here + selector.changed(function () { + let val = parseInt(selector.value()); + if (val < 0) { + // Use the default drawing + setDefaultDrawing(); + } else { + // Use a drawing from tests.json + editor.value(cases[val].code); + } + + // Reset default parameters for turtle + turtle.strokeColor = 255; + turtle.dir = 0; + turtle.x = 0; + turtle.y = 0; + + // Reset default parameters for camera + canvasScrollX = 0; + canvasScrollY = 0; + canvasScaleX = 1; + canvasScaleY = 1; + + // Move and scale the drawing to fit on-screen + scaleToFitBoundingBox(drawingBounds); + + updateEditorOverlayMargin(); + updateEditorContent(); + }); +} + +function updateResizeHandlePosition() { + const resizeHandle = select('#resize-handle').elt; + const editorBounds = select('#editor-container').elt.getBoundingClientRect(); + resizeHandle.style.top = `${editorBounds.top - 8}px`; + resizeHandle.style.width = `${editorBounds.width}px`; +} + +function updateEditorSize(mouseY) { + const resizeHandleBounds = select('#resize-handle').elt.getBoundingClientRect(); + const editorBounds = select('#editor-container').elt.getBoundingClientRect(); + select('#editor-container').elt.style.height = `${editorBounds.bottom - mouseY - 8}px`; + windowResized(); + updateResizeHandlePosition(); +} + +function updateEditorContent() { + editor_bg.html(syntaxHighlight(editor.value())); +} + +function resizeHandleMouseDown(ev) { + resizingEditor = true; + ev.preventDefault(); + return false; +} + +function windowMouseMove(ev) { + if (resizingEditor) + updateEditorSize(ev.clientY); +} + +function windowMouseUp() { + resizingEditor = false; + updateResizeHandlePosition(); +} + +function keyPressed(e) { + if (e.key == 'd' && e.ctrlKey) { + saveCanvas('logo', 'png'); + e.preventDefault(); + } else if (e.key == 's' && e.ctrlKey) { + // save the code to local storage + localStorage.setItem('logo', editor.value()); + + e.preventDefault(); + } +} + +document.getElementById('resize-handle').addEventListener('mousedown', resizeHandleMouseDown); +document.addEventListener('mousemove', windowMouseMove); +document.addEventListener('mouseup', windowMouseUp); diff --git a/style.css b/style.css index fafa0f1..c17e1f9 100644 --- a/style.css +++ b/style.css @@ -1,204 +1,204 @@ -body { - padding: 0; - margin: 0; - background: rgba(0, 0, 0, 0.7); - overflow: hidden; - width: Calc(100vw - 1em); - height: 100vh; - display: grid; - grid-gap: 0.5em; - grid-template-columns: 100vw; - grid-template-rows: 4em 3.5fr auto 1.5em; - grid-template-areas: 'banner' - 'canvas' - 'editor' - 'footer'; -} - -#banner { - grid-area: banner; - background: #F5F5F5; - height: 20px; - padding: 10px 20px 35px 20px; - font-weight: 800; - display: grid; - grid-template-columns: 1.5fr 18fr 3.5fr 1fr 1.4fr; - font-size: 20px; - font-family: 'Montserrat', 'Open Sans'; -} - -#LOGO { - padding-top: 10px; - padding-left: 10px; -} - -#banner select { - margin-right: 10px; -} - -span[tooltip]::before { - content: ''; - opacity: 0; -} - -span[tooltip]:hover::before { - content: attr(tooltip); - opacity: 1; - position: absolute; - top: 60px; - right: 2%; - background: rgba(0, 0, 0, .7); - color: #ffffff; - font-size: 15px; - padding: 5px 10px; - border-radius: 10px; - - transition: opacity 1ms ease-in-out; -} - -.ico_btn_dark img { - border-radius: 100px; - transition: all .2s ease-in; -} - -.ico_btn_dark img:hover { - background: #00000013; -} - -.ico_btn_dark img:active { - background: #00000038; -} - -select { - border: 2px solid #ad9bf8; - border-radius: 10px; - outline: none; -} - -a { - color: #ad9bf8; - text-decoration: none; -} - -.editor-container { - grid-area: editor; - position: relative; - overflow: hidden; - margin: 0 10px 0 9px; - width: 99%; - width: Calc(100% - 18px); - min-height: 6em; - outline: none; - border-radius: 10px; - box-shadow: 0px 5px 8px 1px rgba(0, 0, 0, .3); - background-color:#1f2223; -} - -.textarea-container { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - overflow: hidden; -} - -#code { - width: Calc(100% - 34px); - height: Calc(100% - 11px); - font-size: 20px; - font-family: 'Courier New', Courier, monospace; - padding: 5px 18px 0 10px; - outline: none; - border-radius: 10px; - border: 3px solid rgba(83, 81, 81, 0.6); - white-space: pre-wrap; - word-wrap: break-word; - color: #ffffff; - background: transparent; - - transition: border 250ms ease-in; -} - -#code_bg { - position: absolute; - top: 3px; - left: 3px; - right: 3px; - bottom: 0; - overflow: hidden; - font-family: 'Courier New', Courier, monospace; - resize: none; - white-space: pre-wrap; - word-wrap: break-word; - font-size: 20px; - padding: 5px 18px 0 10px; - color: transparent; - background-color:#1f2223; -} - -mark { - color: #ffffff; - background-color: rgba(255, 0, 0, .6); - border-radius: 5px; -} - -textarea#code { - resize: none; - color: transparent; - caret-color: #528bff; /* Browser Compatibility Warning */ -} - -#code:focus { - border: 3px solid rgba(11, 44, 230, 0.5); -} - -#logo-canvas { - grid-area: canvas; - margin: 0 0.5em; - cursor: grab; - overflow: hidden; - border-radius: 10px; - box-shadow: 0px 5px 8px 1px rgba(0, 0, 0, .3); -} - -#logo-canvas:active { - cursor: grabbing; -} - -.copyright-footer { - grid-area: footer; - color: white; - font-family: 'Montserrat'; - padding: 0 0.5em; -} - -.resize-handle { - position: fixed; - left: 0; - right: 0; - width: 300px; - height: 16px; - margin: 0 auto; - padding: 0; - background-color: transparent; - cursor: row-resize; - z-index: 2; -} - -.resize-handle div { - position: relative; - top: 50%; - width: 90%; - margin: 0 auto; - height: 0; - background-color: rgb(55, 55, 55); - background-color: #f5f5f5; - box-shadow: 0px 2px 16px rgba(0, 0, 0, .3); - transition: all 150ms ease-in-out; -} - -.resize-handle:hover div { - top: 0; - height: 16px; -} +body { + padding: 0; + margin: 0; + background: rgba(0, 0, 0, 0.7); + overflow: hidden; + width: Calc(100vw - 1em); + height: 100vh; + display: grid; + grid-gap: 0.5em; + grid-template-columns: 100vw; + grid-template-rows: 4em 3.5fr auto 1.5em; + grid-template-areas: 'banner' + 'canvas' + 'editor' + 'footer'; +} + +#banner { + grid-area: banner; + background: #F5F5F5; + height: 20px; + padding: 10px 20px 35px 20px; + font-weight: 800; + display: grid; + grid-template-columns: 1.5fr 18fr 3.5fr 1fr 1.4fr; + font-size: 20px; + font-family: 'Montserrat', 'Open Sans'; +} + +#LOGO { + padding-top: 10px; + padding-left: 10px; +} + +#banner select { + margin-right: 10px; +} + +span[tooltip]::before { + content: ''; + opacity: 0; +} + +span[tooltip]:hover::before { + content: attr(tooltip); + opacity: 1; + position: absolute; + top: 60px; + right: 2%; + background: rgba(0, 0, 0, .7); + color: #ffffff; + font-size: 15px; + padding: 5px 10px; + border-radius: 10px; + + transition: opacity 1ms ease-in-out; +} + +.ico_btn_dark img { + border-radius: 100px; + transition: all .2s ease-in; +} + +.ico_btn_dark img:hover { + background: #00000013; +} + +.ico_btn_dark img:active { + background: #00000038; +} + +select { + border: 2px solid #ad9bf8; + border-radius: 10px; + outline: none; +} + +a { + color: #ad9bf8; + text-decoration: none; +} + +.editor-container { + grid-area: editor; + position: relative; + overflow: hidden; + margin: 0 10px 0 9px; + width: 99%; + width: Calc(100% - 18px); + min-height: 6em; + outline: none; + border-radius: 10px; + box-shadow: 0px 5px 8px 1px rgba(0, 0, 0, .3); + background-color:#1f2223; +} + +.textarea-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; +} + +#code { + width: Calc(100% - 34px); + height: Calc(100% - 11px); + font-size: 20px; + font-family: 'Courier New', Courier, monospace; + padding: 5px 18px 0 10px; + outline: none; + border-radius: 10px; + border: 3px solid rgba(83, 81, 81, 0.6); + white-space: pre-wrap; + word-wrap: break-word; + color: #ffffff; + background: transparent; + + transition: border 250ms ease-in; +} + +#code_bg { + position: absolute; + top: 3px; + left: 3px; + right: 3px; + bottom: 0; + overflow: hidden; + font-family: 'Courier New', Courier, monospace; + resize: none; + white-space: pre-wrap; + word-wrap: break-word; + font-size: 20px; + padding: 5px 18px 0 10px; + color: transparent; + background-color:#1f2223; +} + +mark { + color: #ffffff; + background-color: rgba(255, 0, 0, .6); + border-radius: 5px; +} + +textarea#code { + resize: none; + color: transparent; + caret-color: #528bff; /* Browser Compatibility Warning */ +} + +#code:focus { + border: 3px solid rgba(11, 44, 230, 0.5); +} + +#logo-canvas { + grid-area: canvas; + margin: 0 0.5em; + cursor: grab; + overflow: hidden; + border-radius: 10px; + box-shadow: 0px 5px 8px 1px rgba(0, 0, 0, .3); +} + +#logo-canvas:active { + cursor: grabbing; +} + +.copyright-footer { + grid-area: footer; + color: white; + font-family: 'Montserrat'; + padding: 0 0.5em; +} + +.resize-handle { + position: fixed; + left: 0; + right: 0; + width: 300px; + height: 16px; + margin: 0 auto; + padding: 0; + background-color: transparent; + cursor: row-resize; + z-index: 2; +} + +.resize-handle div { + position: relative; + top: 50%; + width: 90%; + margin: 0 auto; + height: 0; + background-color: rgb(55, 55, 55); + background-color: #f5f5f5; + box-shadow: 0px 2px 16px rgba(0, 0, 0, .3); + transition: all 150ms ease-in-out; +} + +.resize-handle:hover div { + top: 0; + height: 16px; +} diff --git a/turtle.js b/turtle.js index dfbc84a..d7cd856 100644 --- a/turtle.js +++ b/turtle.js @@ -1,44 +1,52 @@ -class Turtle { - constructor(x, y, angle) { - this.x = x; - this.y = y; - this.homeX = x; - this.homeY = y; - this.prevX = x; - this.prevY = y; - - this.dir = angle; - this.strokeColor = 255; - this.strokeWeight = 1; - } - - reset() { - this.pen = true; - } - - forward(amt) { - // Move the turtle - this.x += cos(this.dir) * amt; - this.y += sin(this.dir) * amt; - - // If the pen is down we should draw a line from the previous position to the new position - if (this.pen) { - stroke(this.strokeColor); - strokeWeight(this.strokeWeight); - line(this.prevX, this.prevY, this.x, this.y); - } - - // The current position will become the next previous position - this.prevX = this.x; - this.prevY = this.y; - } - - right(angle) { - this.dir += angle; - } - - home() { - this.x = this.homeX; - this.y = this.homeY; - } -} +class Turtle { + constructor(x, y, angle) { + this.x = x; + this.y = y; + this.homeX = x; + this.homeY = y; + this.prevX = x; + this.prevY = y; + + this.dir = angle; + this.strokeColor = 255; + this.strokeWeight = 1; + } + + reset() { + this.pen = true; + } + + forward(amt) { + // Move the turtle + this.x += cos(this.dir) * amt; + this.y += sin(this.dir) * amt; + + // If the pen is down we should draw a line from the previous position to the new position + if (this.pen) { + stroke(this.strokeColor); + strokeWeight(this.strokeWeight); + line(this.prevX, this.prevY, this.x, this.y); + } + + // The current position will become the next previous position + this.prevX = this.x; + this.prevY = this.y; + } + + right(angle) { + this.dir += angle; + } + + home() { + this.x = this.homeX; + this.y = this.homeY; + } + + label(displayText) { + push(); + translate(this.x, this.y); + rotate(this.dir); + text(displayText, 0, 0); + pop(); + } +}