├── .github └── workflows │ └── publish.yml ├── .gitignore ├── dist ├── assets │ ├── env-DjWubmgd.js │ ├── main-_G4yc6cG.js │ └── main-rlcMladr.css └── index.html ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── readme.md ├── sand-pattern.mjs ├── sand_table_pattern_maker.jpg ├── scripts └── create-new.js ├── src ├── css │ ├── main.css │ └── sliders.css ├── index.html └── js │ ├── env.js │ ├── gCode.js │ ├── patterns │ ├── Circle.js │ ├── Coordinates.js │ ├── Cross.js │ ├── Curvature.js │ ├── Cycloid.js │ ├── Diameters.js │ ├── Draw.js │ ├── Egg.js │ ├── Farris.js │ ├── FermatSpiral.js │ ├── Fibonacci.js │ ├── FibonacciLollipops.js │ ├── Frame.js │ ├── Gcode.js │ ├── Gravity.js │ ├── Heart.js │ ├── Lindenmayer.js │ ├── Lissajous.js │ ├── LogarithmicSpiral.js │ ├── Parametric.js │ ├── Rectangle.js │ ├── Rhodonea.js │ ├── ShapeMorph.js │ ├── ShapeSpin.js │ ├── SpinMorph.js │ ├── Spiral.js │ ├── SpiralZigZag.js │ ├── Spokes.js │ ├── Star.js │ ├── Sunset.js │ ├── Superellipse.js │ ├── Text.js │ ├── ThetaRhoInput.js │ ├── WigglySpiral.js │ ├── ZigZag.js │ ├── index.js │ └── utils │ │ └── Utilities.js │ ├── sand_table_pattern_maker.js │ └── thetaRho.js └── vite.config.js /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 17 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Checkout source 28 | uses: actions/checkout@v4 29 | 30 | - name: Setup Node.js 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: '22' # Use your desired Node version 34 | 35 | - name: Install dependencies 36 | run: npm install 37 | 38 | - name: Build project 39 | run: npm run build 40 | 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v3 43 | with: 44 | path: ./dist 45 | 46 | deploy: 47 | needs: build 48 | runs-on: ubuntu-latest 49 | environment: 50 | name: github-pages 51 | url: ${{ steps.deployment.outputs.page_url }} 52 | 53 | steps: 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules -------------------------------------------------------------------------------- /dist/assets/env-DjWubmgd.js: -------------------------------------------------------------------------------- 1 | var e={app:{version:"0.1.0",framerate:10},canvas:{width:648,height:648},table:{format:"cartesian",units:"mm",x:{min:0,max:472},y:{min:0,max:380}},motor:{speed:4e3},ball:{diameter:19},gcode:{command:"G0"},recalculate_pattern:!0,mouse:{pressed:!1,x:null,y:null}};export{e}; 2 | -------------------------------------------------------------------------------- /dist/assets/main-rlcMladr.css: -------------------------------------------------------------------------------- 1 | body{padding:0;margin:0;font-family:Dosis,sans-serif;background-color:#444;font-size:16px}header{margin:0;padding:.5em 0;text-align:center;background-color:#202020}header h1{margin:0;padding:0;font-size:1em;color:#fff}h2{font-size:1em;margin-top:1.5em;color:#f0ffb4}p.small{font-size:.75em}footer{color:gray;text-align:center;background-color:#202020;padding:.5em 0}footer p{margin:0;padding:0}footer a,footer a:link,footer a:visited,footer a:hover,footer a:active{color:#8c8c8c;text-decoration:underline}#sketch-holder{position:relative;width:648px;margin:0 auto}.container{display:grid;grid-template-columns:50% 50%;color:#444}.box{background-color:#444;color:#fff;padding:20px}.box:nth-of-type(2){z-index:2;background:#0000004d}#canvas-holder{min-width:600px}#pattern-selector{background-color:#000;padding:.25em .5em;margin:.25em 0}#pattern-selector label{display:inline-block;width:7em}table tr td:nth-of-type(1){width:7em}button{background-color:#4caf50;border:none;border-radius:.5em;color:#fff;padding:15px 32px;text-align:center;text-decoration:none;display:inline-block;font-size:16px;cursor:pointer}.pattern-control{background-color:#202020;padding:.25em .5em;margin:.25em 0}.pattern-control label{display:inline-block;width:7em}.pattern-control input[type=range]{width:20em}.pattern-control input[type=text]{width:40em} 2 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sand Pattern Maker 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Sand Pattern Maker

14 | 15 |
16 |
17 |
18 | 19 |
20 |

Plotter Configuration

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
Format
X Range
Y Range
Motor Speed
Ball Size
43 |
44 | 45 |
46 |

Pattern Input

47 |
48 |
49 |
50 | 51 |
52 |

Pattern Output

53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
Instructions
Distance
Time
67 |
68 | 69 |
70 |

 

71 |
72 |

Download Pattern image and G-code instructions at once. Chrome Required.

73 |
74 | 75 |
76 |
77 | 78 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | 4 | /** @type {import('eslint').Linter.Config[]} */ 5 | export default [ 6 | { files: ["src/**/*.js"], languageOptions: { 7 | sourceType: "module", 8 | globals: globals.browser 9 | } 10 | }, 11 | { 12 | languageOptions: { 13 | globals: globals.node 14 | } 15 | }, 16 | pluginJs.configs.recommended, 17 | { 18 | rules: { 19 | // indent: ['error', 2], 20 | } 21 | } 22 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sand-table-pattern-maker", 3 | "version": "0.3.0", 4 | "description": "A web application for creating patterns to draw in a sand table using (G-code and THR files for Sisyphus tables)", 5 | "author": "Mark Roland", 6 | "license": "", 7 | "engines": { 8 | "node": ">=22.x", 9 | "npm": ">=10.x" 10 | }, 11 | "scripts": { 12 | "dev": "vite", 13 | "build": "vite build", 14 | "preview": "vite preview", 15 | "lint": "eslint ./src/js" 16 | }, 17 | "bin": { 18 | "sand-pattern": "./sand-pattern.mjs" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.25.1", 22 | "eslint": "^9.25.1", 23 | "globals": "^16.0.0", 24 | "vite": "^6.3.2" 25 | }, 26 | "dependencies": { 27 | "@markroland/path-helper": "^1.37.0", 28 | "p5": "^2.0.0", 29 | "minimist": "^1.2.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Sand Table Pattern Maker 2 | 3 | ![Sand Table](sand_table_pattern_maker.jpg) 4 | 5 | [Launch Pattern Maker](https://markroland.github.io/sand-table-pattern-maker/) 6 | 7 | This is part of my [Sand Table Build](https://markroland.com/portfolio/sand-table) 8 | 9 | Built with [p5.js](https://p5js.org) 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install 15 | npm run dev 16 | ``` 17 | 18 | ## Run using NPM 19 | 20 | ``` 21 | node sand-pattern.mjs --pattern=circle > circle.json 22 | ``` 23 | 24 | Or using the `bin` command: 25 | 26 | ``` 27 | sand-pattern --pattern=circle > circle.json 28 | ``` 29 | 30 | ## Controls 31 | 32 | - Press "o" to toggle an overlay of the pattern settings in the canvas 33 | - Press "c" to toggle the live coordinates and plotter mechanism view 34 | - Press "d" to download a heightmap PNG of the pattern. 35 | - Press "p" to play/pause playback of pattern. 36 | 37 | ## How to Build a New Pattern 38 | 39 | ``` 40 | node ./scripts/create-new.js NewPatternName 41 | ``` 42 | 43 | Pattern classes are made up of at least 4 methods: 44 | 45 | - **constructor** - The class constructor defines a few aspect of the class including: 46 | - `this.key` - Used for identifying the class's properties in the main sketch file 47 | - `this.name` - Used for referencing the pattern in the UI. 48 | - `this.config` - This is an object that defines the pattern's input configuration options. 49 | - `this.path` - Initializes the pattern's output path coordinates 50 | - **draw** - This class is called by the main sketch to draw the pattern. It reads and 51 | updates the UI input values and sends the input to the class's `calc` method. 52 | - **calc** - This is where the algorithm for the pattern is implemented. Using the selected 53 | inputs, the method returns the coordinates for the complete path. 54 | 55 | Once you've completed your design, submit a Pull Request and if it works, I'll merge it in. Thanks in advance! 56 | 57 | ## Patterns 58 | 59 | - XY Coordinates 60 | - Circle 61 | - Cross 62 | - Cycloid ([Epicycloid](https://en.wikipedia.org/wiki/Epicycloid), [Hypocycloid](https://en.wikipedia.org/wiki/Hypocycloid), [Hypotrochoid](https://en.wikipedia.org/wiki/Hypotrochoid)) 63 | - Curvature 64 | - Diameters 65 | - Free Draw 66 | - Easter Eggs ([Reference](https://math.stackexchange.com/questions/3375853/parametric-equations-for-a-true-egg-shape)) 67 | - Farris Curve ([Reference](http://www.sineofthetimes.org/the-art-of-parametric-equations-2/)) 68 | - Fermat's Spiral ([Reference](https://en.wikipedia.org/wiki/Fermat%27s_spiral)) 69 | - Fibonacci 70 | - Fibonacci Lollipops 71 | - Frames (Border Patterns) 72 | - G-Code 73 | - Gravity 74 | - Heart ([Reference](http://mathworld.wolfram.com/HeartCurve.html)) 75 | - Space Filling Curves \[[1](https://p5js.org/examples/simulate-l-systems.html)\] \[[2](https://en.wikipedia.org/wiki/Space-filling_curve)\] \[[3](https://fedimser.github.io/l-systems.html)\] 76 | - Lissajous Curve ([Lissajous Curves](https://en.wikipedia.org/wiki/Lissajous_curve)) 77 | - Parametric ([Butterfly Curve](https://en.wikipedia.org/wiki/Butterfly_curve_(transcendental))) 78 | - Rectangle 79 | - Rhodonea (Rose) Curve ([Rose Curve](https://en.wikipedia.org/wiki/Rose_(mathematics))) 80 | - Shape Morph 81 | - Shape Spin 82 | - Spiral 83 | - Spiral (Logarithmic) ([Reference](https://en.wikipedia.org/wiki/Logarithmic_spiral)) 84 | - Spokes 85 | - Star 86 | - Superellipse \[[1](https://en.wikipedia.org/wiki/Superellipse)\] \[[2](https://mathworld.wolfram.com/Superellipse.html)\] \[[3](https://thecodingtrain.com/CodingChallenges/019-superellipse.html)\] 87 | - Text 88 | - Theta Rho Coordinates 89 | - Wiggly Spiral 90 | - Zig Zag 91 | 92 | ## License 93 | 94 | [![Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](https://i.creativecommons.org/l/by-nd/2.0/88x31.png)](https://creativecommons.org/licenses/by-nc-sa/4.0/) 95 | 96 | This work is licensed under a [Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/) License. 97 | 98 | This work makes use of [p5.js](https://p5js.org), which carries a [GNU Lesser General Public License](https://p5js.org/copyright.html). 99 | -------------------------------------------------------------------------------- /sand-pattern.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Pattern Generator 4 | 5 | import minimist from 'minimist'; 6 | 7 | import env from './src/js/env.js'; 8 | import Circle from './src/js/patterns/Circle.js'; 9 | 10 | // Parse arguments 11 | const argv = minimist(process.argv.slice(2)); 12 | 13 | let Pattern; 14 | 15 | switch(argv.pattern) { 16 | case 'circle': 17 | const MyCircle = new Circle(env); 18 | Pattern = MyCircle.calc(0, 0, 100, 0); 19 | console.log(Pattern); 20 | break; 21 | default: 22 | console.log("Please use the `pattern` argument to set the shape. One of: circle") 23 | process.exit(1); 24 | } 25 | 26 | // console.log(Pattern.path()); 27 | -------------------------------------------------------------------------------- /sand_table_pattern_maker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markroland/sand-table-pattern-maker/996f63ddc1d6e10e840d4867a29671696f58e77b/sand_table_pattern_maker.jpg -------------------------------------------------------------------------------- /scripts/create-new.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const newFileName = process.argv[2]; 4 | if (!newFileName) { 5 | console.error('Please provide a file name as the first argument.'); 6 | process.exit(1); 7 | } 8 | const srcPath = path.join(__dirname, '../src/js/patterns/SpiralZigZag.js'); 9 | const destPath = path.join(__dirname, `../src/js/patterns/${newFileName}.js`); 10 | 11 | fs.copyFile(srcPath, destPath, (err) => { 12 | 13 | if (err) { 14 | console.error('Error copying file:', err); 15 | process.exit(1); 16 | } 17 | 18 | // Use SpiralZigZag as a template 19 | let content = fs.readFileSync(destPath, 'utf8'); 20 | content = content.replace('class SpiralZigZag', `class ${newFileName}`); 21 | content = content.replace('export default SpiralZigZag', `export default ${newFileName}`); 22 | fs.writeFileSync(destPath, content, 'utf8'); 23 | 24 | // Update index.js programmatically using the newFileName variable 25 | const indexPath = path.join(__dirname, '../src/js/patterns/index.js'); 26 | let indexContent = fs.readFileSync(indexPath, 'utf8'); 27 | 28 | // Update the import block (excluding env import) 29 | const importBlockRegex = /((?:import (?!env).+from\s+['"].+['"];[\r\n])+)/; 30 | let importBlockMatch = indexContent.match(importBlockRegex); 31 | if (importBlockMatch) { 32 | let importLines = importBlockMatch[1].split('\n').filter(line => line.trim() !== ''); 33 | importLines.push(`import ${newFileName} from './${newFileName}.js';`); 34 | importLines.sort((a, b) => { 35 | const nameA = a.match(/import\s+(\S+)\s+/)[1].toLowerCase(); 36 | const nameB = b.match(/import\s+(\S+)\s+/)[1].toLowerCase(); 37 | return nameA.localeCompare(nameB); 38 | }); 39 | const newImportBlock = importLines.join('\n') + '\n'; 40 | indexContent = indexContent.replace(importBlockRegex, newImportBlock); 41 | } 42 | 43 | // Update the Patterns object block 44 | const patternBlockRegex = /(const Patterns = {\s*)([\s\S]*?)(\s*}\s*)/m; 45 | let patternBlockMatch = indexContent.match(patternBlockRegex); 46 | if(patternBlockMatch) { 47 | const before = patternBlockMatch[1]; 48 | let patternLines = patternBlockMatch[2].split('\n').filter(line => line.trim() !== ''); 49 | const after = patternBlockMatch[3]; 50 | patternLines.push(` "${newFileName.toLowerCase()}": new ${newFileName}(env),`); 51 | patternLines.sort((a, b) => { 52 | const keyA = a.match(/"([^"]+)"/)[1].toLowerCase(); 53 | const keyB = b.match(/"([^"]+)"/)[1].toLowerCase(); 54 | return keyA.localeCompare(keyB); 55 | }); 56 | const newPatternBlock = before + patternLines.join('\n') + after; 57 | indexContent = indexContent.replace(patternBlockRegex, newPatternBlock); 58 | } 59 | 60 | fs.writeFileSync(indexPath, indexContent, 'utf8'); 61 | }); -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | font-family: 'Dosis', sans-serif; 5 | background-color: rgb(68,68,68); 6 | font-size: 16px; 7 | } 8 | 9 | header { 10 | margin: 0; 11 | padding: 0.5em 0; 12 | text-align: center; 13 | background-color: rgb(32,32,32); 14 | } 15 | 16 | header h1 { 17 | margin: 0; 18 | padding: 0; 19 | font-size: 1em; 20 | color: rgb(255, 255, 255); 21 | } 22 | 23 | h2 { 24 | font-size: 1em; 25 | margin-top: 1.5em; 26 | color: rgb(240, 255, 180); 27 | } 28 | 29 | p.small { 30 | font-size: 0.75em; 31 | } 32 | 33 | footer { 34 | color: rgb(128, 128, 128); 35 | text-align: center; 36 | background-color: rgb(32,32,32); 37 | padding: 0.5em 0; 38 | } 39 | 40 | footer p{ 41 | margin: 0; 42 | padding: 0; 43 | } 44 | 45 | footer a, 46 | footer a:link, 47 | footer a:visited, 48 | footer a:hover, 49 | footer a:active 50 | { 51 | color: rgb(140, 140, 140); 52 | text-decoration: underline; 53 | } 54 | 55 | #sketch-holder { 56 | position: relative; 57 | width: 648px; 58 | margin: 0 auto; 59 | } 60 | 61 | .container { 62 | display: grid; 63 | grid-template-columns: 50% 50%; 64 | /*grid-gap: 10px;*/ 65 | color: rgb(68,68,68); 66 | } 67 | .box { 68 | background-color: rgb(68,68,68); 69 | color: rgb(255,255,255); 70 | padding: 20px; 71 | } 72 | 73 | .box:nth-of-type(2) { 74 | z-index: 2; 75 | background: rgba(0, 0, 0, 0.3); 76 | } 77 | 78 | #canvas-holder { 79 | min-width: 600px; 80 | } 81 | 82 | #pattern-selector { 83 | background-color: rgb(0, 0, 0); 84 | padding: 0.25em 0.5em; 85 | margin: 0.25em 0; 86 | } 87 | 88 | #pattern-selector label { 89 | display: inline-block; 90 | width: 7em; 91 | } 92 | 93 | table tr td:nth-of-type(1) { 94 | width: 7em; 95 | } 96 | 97 | button { 98 | background-color: #4CAF50; 99 | border: none; 100 | border-radius: 0.5em; 101 | color: white; 102 | padding: 15px 32px; 103 | text-align: center; 104 | text-decoration: none; 105 | display: inline-block; 106 | font-size: 16px; 107 | cursor: pointer; 108 | } 109 | 110 | .pattern-control { 111 | background-color: rgb(32, 32, 32); 112 | padding: 0.25em 0.5em; 113 | margin: 0.25em 0; 114 | } 115 | 116 | .pattern-control label { 117 | display: inline-block; 118 | width: 7em; 119 | } 120 | .pattern-control input[type="range"] { 121 | width: 20em; 122 | } 123 | 124 | .pattern-control input[type="text"] { 125 | width: 40em; 126 | } -------------------------------------------------------------------------------- /src/css/sliders.css: -------------------------------------------------------------------------------- 1 | /* https://www.w3schools.com/howto/howto_js_rangeslider.asp */ 2 | 3 | .slidecontainer { 4 | width: 100%; /* Width of the outside container */ 5 | } 6 | 7 | .slider { 8 | -webkit-appearance: none; 9 | width: 100%; 10 | height: 5px; 11 | border-radius: 5px; 12 | background: #d3d3d3; 13 | outline: none; 14 | opacity: 0.7; 15 | -webkit-transition: .2s; 16 | transition: opacity .2s; 17 | } 18 | 19 | .slider::-webkit-slider-thumb { 20 | -webkit-appearance: none; 21 | appearance: none; 22 | width: 25px; 23 | height: 25px; 24 | border-radius: 50%; 25 | background: #4CAF50; 26 | cursor: pointer; 27 | } 28 | 29 | .slider::-moz-range-thumb { 30 | width: 25px; 31 | height: 15px; 32 | border-radius: 50%; 33 | background: #4CAF50; 34 | cursor: pointer; 35 | } 36 | 37 | /* Mouse-over effects */ 38 | .slider:hover { 39 | opacity: 1; /* Fully shown on mouse-over */ 40 | } 41 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sand Pattern Maker 6 | 7 | 8 | 9 | 10 |

Sand Pattern Maker

11 | 12 |
13 |
14 |
15 | 16 |
17 |

Plotter Configuration

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
Format
X Range
Y Range
Motor Speed
Ball Size
40 |
41 | 42 |
43 |

Pattern Input

44 |
45 |
46 |
47 | 48 |
49 |

Pattern Output

50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 |
Playback
Instructions
Distance
Time
68 |
69 | 70 |
71 |

 

72 |
73 |

Download Pattern image and G-code instructions at once. Chrome Required.

74 |
75 | 76 |
77 |
78 | 79 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/js/env.js: -------------------------------------------------------------------------------- 1 | var env = { 2 | "app": { 3 | "version": "0.1.0", 4 | "framerate": 10 5 | }, 6 | "canvas": { 7 | "width": 648, 8 | "height": 648 9 | }, 10 | "table" : { 11 | "format": "cartesian", 12 | "units": "mm", 13 | "x": { 14 | "min": 0.0, 15 | "max": 472.0 16 | }, 17 | "y": { 18 | "min": 0.0, 19 | "max": 380.0 20 | } 21 | }, 22 | "motor": { 23 | "speed": 4000.0 24 | }, 25 | "ball": { 26 | "diameter": 10.0 27 | }, 28 | "gcode": { 29 | "command": "G0" 30 | }, 31 | "recalculate_pattern": false, 32 | "mouse": { 33 | "pressed": false, 34 | "x": null, 35 | "y": null 36 | } 37 | }; 38 | 39 | export default env; -------------------------------------------------------------------------------- /src/js/gCode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine G-Code headers, footer and path 3 | */ 4 | function createGcode(env, pattern, path, gCommand = "G0") { 5 | 6 | // Get G-Code header 7 | var gcode = gCodeHeader(env, pattern); 8 | 9 | // Compose start command(s) 10 | gcode = gcode.concat(gCodeStart()); 11 | 12 | // Compose G-code path 13 | gcode = gcode.concat("; G-code path"); 14 | for (let i = 0; i < path.length; i++) { 15 | gcode.push(gCommand + " X" + (path[i][0] + env.table.x.max/2).toFixed(2) + " Y" + (path[i][1] + env.table.y.max/2,0,2).toFixed(2)); 16 | } 17 | 18 | // Compose end command(s) 19 | gcode = gcode.concat(gCodeFinish()); 20 | 21 | return gcode; 22 | } 23 | 24 | /** 25 | * Header Content for the G-code file 26 | */ 27 | function gCodeHeader(env, pattern) { 28 | 29 | var header = [ 30 | "; Created using https://markroland.github.io/sand-table-pattern-maker/", 31 | ";", 32 | "; " + (new Date().getMonth() + 1) + "/" + new Date().getDate() + "/" + new Date().getFullYear() + " " + new Date().getHours() + ":" + new Date().getMinutes() + ":" + new Date().getSeconds(), 33 | ";", 34 | "; Machine Specifications:", 35 | ";", 36 | "; Units: " + env.table.units, 37 | ";", 38 | "; Min X: " + env.table.x.min, 39 | "; Max X: " + env.table.x.max, 40 | "; Min Y: " + env.table.y.min, 41 | "; Max Y: " + env.table.y.max, 42 | "; Motor Speed (" + env.table.units + "/min): " + env.motor.speed, 43 | ";", 44 | "; Pattern Specifications:", 45 | ";", 46 | "; Name: " + pattern.name, 47 | ";", 48 | "; Parameters:" 49 | ]; 50 | 51 | // Add pattern parameters 52 | const entries = Object.entries(pattern.config) 53 | for (const [param, content] of entries) { 54 | header = header.concat([`; - ${param}: ${content}`]); 55 | } 56 | 57 | header = header.concat([ 58 | ";", 59 | "; URL: " + window.location, 60 | ";", 61 | // "; Pattern Distance (" + env.table.units + "): " + distance.toFixed(1), 62 | // "; Pattern Draw Time (minutes): " + (distance / motor_speed).toFixed(1), 63 | "" 64 | ]); 65 | 66 | return header; 67 | } 68 | 69 | /** 70 | * Startup commands for the G-code file 71 | */ 72 | function gCodeStart() { 73 | return [ 74 | "", 75 | "; Custom G-code to execute before the start of the sketch", 76 | "", 77 | // Insert your code here 78 | ]; 79 | } 80 | 81 | /** 82 | * Finishing commands for the G-code file 83 | */ 84 | function gCodeFinish() { 85 | return [ 86 | "", 87 | "; Custom G-code to execute after the end of the sketch", 88 | "", 89 | // Insert your code here 90 | ]; 91 | } 92 | 93 | export default createGcode; -------------------------------------------------------------------------------- /src/js/patterns/Circle.js: -------------------------------------------------------------------------------- 1 | class Circle { 2 | 3 | constructor(env) { 4 | 5 | this.key = "circle"; 6 | 7 | this.name = "Circle"; 8 | 9 | const max_r = Math.min((env.table.x.max - env.table.x.min), (env.table.y.max - env.table.y.min))/2; 10 | 11 | this.config = { 12 | "radius": { 13 | "name": "Radius (r)", 14 | "value": null, 15 | "input": { 16 | "type": "createSlider", 17 | "params" : [ 18 | 1, 19 | 0.5 * Math.min(env.table.x.max, env.table.y.max), 20 | max_r/2, 21 | 1 22 | ], 23 | "class": "slider", 24 | "displayValue": true 25 | } 26 | }, 27 | "angle": { 28 | "name": "Start Angle (𝜃)", 29 | "value": null, 30 | "input": { 31 | "type": "createSlider", 32 | "params" : [ 33 | 0, 34 | 360, 35 | 0, 36 | 1 37 | ], 38 | "class": "slider", 39 | "displayValue": true 40 | } 41 | }, 42 | "reverse": { 43 | "name": "Reverse", 44 | "value": null, 45 | "input": { 46 | "type": "createCheckbox", 47 | "attributes" : [{ 48 | "type" : "checkbox", 49 | "checked" : null 50 | }], 51 | "params": [0, 1, 0], 52 | "displayValue": false 53 | } 54 | } 55 | }; 56 | 57 | this.path = []; 58 | } 59 | 60 | draw() { 61 | 62 | // Update object 63 | this.config.radius.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 64 | this.config.angle.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 65 | 66 | // Display selected values 67 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.radius.value; 68 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.angle.value + '°'; 69 | 70 | // Calculate path for Circle at center 71 | //* 72 | let path = this.calc( 73 | 0, 74 | 0, 75 | this.config.radius.value, 76 | (this.config.angle.value / 360) * (2 * Math.PI) 77 | ); 78 | //*/ 79 | 80 | // Calculate path for Circle not at center 81 | /* 82 | var start_x = 0.25 * max_x; 83 | var start_y = 0.25 * max_y; 84 | let path = this.calc( 85 | start_x, 86 | start_y, 87 | this.config.radius, 88 | atan(start_y/start_x) + PI 89 | ); 90 | //*/ 91 | 92 | // Update object 93 | this.path = path; 94 | 95 | return path; 96 | } 97 | 98 | /** 99 | * Calculate coordinates for a circle 100 | * 101 | * @param float start_x Starting X position (in G-code coordinates) 102 | * @param float start_y Starting Y position (in G-code coordinates) 103 | * @param float start_r Starting radius, where 0 is [x,y] 104 | * @param float start_theta Starting theta angle, between 0 and (2 * Math.PI). 105 | * 0-degrees corresponds to the positive X direction and rotates counter clockwise 106 | * (i.e. PI/2 is the positive y direction) 107 | * @param int rotation_direction Set 1 to move counterclockwise, -1 to move clockwise 108 | * 109 | * 110 | **/ 111 | calc(start_x, start_y, radius, start_theta, rotation_direction = 1) { 112 | 113 | // Set initial values 114 | var x; 115 | var y; 116 | var theta = start_theta; 117 | 118 | // Initialize return value - the path array 119 | // This stores the x,y coordinates for each step 120 | var path = new Array(); 121 | 122 | // Iteration counter. 123 | var step = 0; 124 | 125 | // The number of "sides" to the circle. 126 | // A larger number makes the circle more smooth 127 | // let max_r = Math.min((max_x - min_x), (max_y - min_y))/2; 128 | // let sides = 30 + (radius/max_r) * 30; 129 | let sides = 60; 130 | 131 | // Loop through one revolution 132 | while (theta < start_theta + (2 * Math.PI)) { 133 | 134 | // Rotational Angle (steps per rotation in the denominator) 135 | theta = rotation_direction * (start_theta + (step/sides) * (2 * Math.PI)); 136 | 137 | // Convert polar position to rectangular coordinates 138 | x = start_x + (radius * Math.cos(theta)); 139 | y = start_y + (radius * Math.sin(theta)); 140 | 141 | // Add coordinates to shape array 142 | path.push([x,y]); 143 | 144 | // Increment iteration counter 145 | step++; 146 | } 147 | 148 | return path; 149 | } 150 | } 151 | 152 | export default Circle; -------------------------------------------------------------------------------- /src/js/patterns/Coordinates.js: -------------------------------------------------------------------------------- 1 | class Coordinates { 2 | 3 | constructor() { 4 | 5 | this.key = "coordinates"; 6 | 7 | this.name = "XY Coordinates"; 8 | 9 | this.config = { 10 | "coordinates": { 11 | "name": "Coordinates", 12 | "value": null, 13 | "input": { 14 | "type": "createTextarea", 15 | "attributes" : { 16 | "rows": 11, 17 | "cols": 12, 18 | }, 19 | "value" : "100,0\n0,100\n-100,0\n0,-100\n100,0", 20 | "params" : [] 21 | } 22 | }, 23 | "reverse": { 24 | "name": "Reverse", 25 | "value": null, 26 | "input": { 27 | "type": "createCheckbox", 28 | "attributes" : [{ 29 | "type" : "checkbox", 30 | "checked" : null 31 | }], 32 | "params": [0, 1, 0], 33 | "displayValue": false 34 | } 35 | } 36 | }; 37 | 38 | this.path = []; 39 | } 40 | 41 | draw() { 42 | 43 | // Update object 44 | this.config.coordinates.value = document.querySelector('#pattern-controls > div:nth-child(1) > textarea').value; 45 | 46 | // Calculate path for Circle at center 47 | let path = this.calc( 48 | this.config.coordinates.value 49 | ); 50 | 51 | // Update object 52 | this.path = path; 53 | 54 | return path; 55 | } 56 | 57 | /** 58 | * Calculate coordinates 59 | **/ 60 | calc(data) { 61 | 62 | // Set initial values 63 | let x; 64 | let y; 65 | 66 | // Initialize return value - the path array 67 | // This stores the x,y coordinates for each step 68 | let path = new Array(); 69 | 70 | // Split string by line 71 | let lines = data.split("\n"); 72 | 73 | // Loop through lines and split by comma 74 | lines.forEach(function(element) { 75 | 76 | let coordinates = element.split(","); 77 | x = parseFloat(coordinates[0]); 78 | y = parseFloat(coordinates[1]); 79 | 80 | // Add coordinates to shape array 81 | path.push([x,y]); 82 | }) 83 | 84 | return path; 85 | } 86 | } 87 | 88 | export default Coordinates; -------------------------------------------------------------------------------- /src/js/patterns/Cross.js: -------------------------------------------------------------------------------- 1 | class Cross { 2 | 3 | constructor(env) { 4 | 5 | this.key = "cross"; 6 | 7 | this.name = "Cross"; 8 | 9 | this.env = env; 10 | 11 | this.config = { 12 | "width": { 13 | "name": "Width", 14 | "value": null, 15 | "input": { 16 | "type": "createSlider", 17 | "params" : [ 18 | 1, 19 | (env.table.x.max - env.table.x.min), 20 | 0.3 * (env.table.x.max - env.table.x.min), 21 | 1 22 | ], 23 | "class": "slider", 24 | "displayValue": true 25 | } 26 | }, 27 | "height": { 28 | "name": "Height", 29 | "value": null, 30 | "input": { 31 | "type": "createSlider", 32 | "params" : [ 33 | 1, 34 | (env.table.x.max - env.table.x.min), 35 | (env.table.x.max - env.table.x.min) / 2, 36 | 1 37 | ], 38 | "class": "slider", 39 | "displayValue": true 40 | } 41 | }, 42 | "intersect": { 43 | "name": "Intersect Height", 44 | "value": null, 45 | "input": { 46 | "type": "createSlider", 47 | "params" : [ 48 | 0, 49 | 100, 50 | 75, 51 | 1 52 | ], 53 | "class": "slider", 54 | "displayValue": true 55 | } 56 | }, 57 | "starburst": { 58 | "name": "Starburst", 59 | "value": null, 60 | "input": { 61 | "type": "createCheckbox", 62 | "attributes" : [{ 63 | "type" : "checkbox", 64 | "checked" : true 65 | }], 66 | "params": [0, 1, 1], 67 | "displayValue": false 68 | } 69 | } 70 | }; 71 | 72 | this.path = []; 73 | } 74 | 75 | draw() { 76 | 77 | // Update object 78 | this.config.width.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 79 | this.config.height.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 80 | this.config.intersect.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 81 | 82 | this.config.starburst.value = false; 83 | if (document.querySelector('#pattern-controls > div:nth-child(4) > input[type=checkbox]').checked) { 84 | this.config.starburst.value = true; 85 | } 86 | 87 | // Display selected values 88 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.width.value + " " + this.env.table.units; 89 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.height.value + " " + this.env.table.units; 90 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.intersect.value + "%"; 91 | 92 | // Calculate path for Circle at center 93 | let path = this.calc( 94 | this.config.width.value, 95 | this.config.height.value, 96 | this.config.intersect.value/100, 97 | this.config.starburst.value 98 | ); 99 | 100 | // Update object 101 | this.path = path; 102 | 103 | return path; 104 | } 105 | 106 | /** 107 | * Calculate coordinates for the shape 108 | * 109 | * @param integer cross_width Cross Width 110 | * @param integer cross_height Cross Height 111 | * @param float cross_intersect Height of horizontal bar as percentage of the height 112 | * 113 | * @return Array Path 114 | **/ 115 | calc(cross_width, cross_height, cross_intersect, starburst) { 116 | 117 | // Initialize return value - the path array 118 | // This stores the x,y coordinates for each step 119 | var path = new Array(); 120 | 121 | let y_center = (cross_intersect * cross_height) - (cross_height/2); 122 | 123 | let increment = this.env.ball.diameter/2; 124 | let rotations = 3; 125 | var offset; 126 | 127 | if (starburst) { 128 | path.push([0, y_center]); 129 | let rx; 130 | let ry; 131 | for (let i = 0; i < 16; i++){ 132 | 133 | // Calculate ellipse radius 134 | rx = (cross_width / 2) + ((rotations + 3) * increment); 135 | ry = (cross_height / 2) + ((rotations + 3) * increment) - y_center; 136 | 137 | // Extend rays of the bottom half 138 | if (i/16 > 0.5) { 139 | ry = (cross_height / 2) + ((rotations + 3) * increment) + y_center; 140 | } 141 | 142 | // 45-degree rays 143 | if ((i + 2) % 4 == 0) { 144 | rx = 0.9 * rx; 145 | ry = 0.9 * ry; 146 | } 147 | 148 | // Odd Rays 149 | if ((i + 1) % 2 == 0) { 150 | rx = 0.8 * rx; 151 | ry = 0.8 * ry; 152 | } 153 | 154 | path.push([ 155 | rx * Math.cos(i/16 * (2 * Math.PI)), 156 | ry * Math.sin(i/16 * (2 * Math.PI)) + y_center 157 | ]); 158 | path.push([0, y_center]); 159 | } 160 | } 161 | 162 | // Cross path 163 | var cross_path = new Array(); 164 | for (let i = 0; i < rotations; i++) { 165 | offset = i * increment; 166 | cross_path = cross_path.concat([ 167 | [0 - offset, y_center - offset], 168 | [-cross_width/2 - offset, y_center - offset], 169 | [-cross_width/2 - offset, y_center + offset], 170 | [0 - offset, y_center + offset], 171 | [0 - offset, cross_height/2 + offset], 172 | [0 + offset, cross_height/2 + offset], 173 | [0 + offset, y_center + offset], 174 | [cross_width/2 + offset, y_center + offset], 175 | [cross_width/2 + offset, y_center - offset], 176 | [0 + offset, y_center - offset], 177 | [0 + offset, -cross_height/2 - offset], 178 | [0 - offset, -cross_height/2 - offset], 179 | [0 - offset, y_center - offset] 180 | ]); 181 | } 182 | 183 | // Reverse path 184 | // This may be able to be accomplished more elegantly, 185 | // but Array reverse was not working well for multi-dimensional arrays 186 | var reverse_path = new Array(); 187 | for (let i=0; i < cross_path.length; i++) { 188 | reverse_path.push(cross_path[(cross_path.length-1) - i]); 189 | } 190 | path = path.concat(reverse_path); 191 | 192 | // Return to center 193 | // path.push([0, y_center]); 194 | 195 | return path; 196 | } 197 | } 198 | 199 | export default Cross; -------------------------------------------------------------------------------- /src/js/patterns/Curvature.js: -------------------------------------------------------------------------------- 1 | import PathHelper from '@markroland/path-helper' 2 | 3 | class Curvature { 4 | 5 | constructor(env) { 6 | this.key = "curvature"; 7 | this.name = "Curvature"; 8 | this.env = env; 9 | 10 | this.config = { 11 | "radius": { 12 | "name": "Start Radius", 13 | "value": null, 14 | "input": { 15 | "type": "createSlider", 16 | "params" : [ 17 | 0, 18 | 1, 19 | 0.05, 20 | 0.01 21 | ], 22 | "class": "slider", 23 | "displayValue": true 24 | } 25 | }, 26 | "iterations": { 27 | "name": "Iterations", 28 | "value": null, 29 | "input": { 30 | "type": "createSlider", 31 | "params" : [ 32 | 2, 33 | 100, 34 | 40, 35 | 2 36 | ], 37 | "class": "slider", 38 | "displayValue": true 39 | } 40 | }, 41 | }; 42 | 43 | this.path = []; 44 | } 45 | 46 | draw() { 47 | 48 | const PathHelp = new PathHelper(); 49 | 50 | // Update object 51 | this.config.radius.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 52 | this.config.iterations.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 53 | 54 | // Display selected values 55 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.radius.value; 56 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.iterations.value; 57 | 58 | let path = []; 59 | 60 | // Set maximum radius based on table size 61 | const max_r = 0.5 * Math.min( 62 | (this.env.table.x.max - this.env.table.x.min), 63 | (this.env.table.y.max - this.env.table.y.min) 64 | ); 65 | 66 | // Crop Circle 67 | const cropShape = PathHelp.polygon(60, max_r); 68 | 69 | // This needs to be even to mirror properly 70 | const i_max = this.config.iterations.value; 71 | 72 | // const starting_radius = this.config.radius.value * max_r; 73 | const starting_radius = this.config.radius.value * max_r; 74 | const ending_radius = 40.0 * max_r; 75 | for (let i = 0; i < i_max; i++) { 76 | 77 | // The radius should increase. It can be eased using different methods 78 | const radius = starting_radius + this.#easeInQuart(i/i_max) * (ending_radius - starting_radius); 79 | 80 | // The y position should start out near the top and the bottom of the circle/arc 81 | // should step down by one increment of (i) as a fraction of the maximum radius, 82 | // ensuring equal spacing between each arc 83 | const y = max_r + 1.0 * radius - (max_r * (i / i_max)); 84 | 85 | const sides = Math.round((2 * Math.PI * radius) / (0.5 * this.env.ball.diameter)); 86 | let arc = PathHelp.arc( 87 | [0, 0], 88 | radius, 89 | 2 * Math.PI, 90 | 0.5 * Math.PI, 91 | sides 92 | ); 93 | 94 | if (i % 2) { 95 | arc.reverse(); 96 | } 97 | 98 | arc = PathHelp.translatePath(arc, [0, y]); 99 | 100 | let cropped = PathHelp.cropToShape([arc], cropShape); 101 | 102 | if (cropped.length) { 103 | path = path.concat(cropped[0]); 104 | } 105 | } 106 | 107 | const horizontal_line = [ 108 | [-max_r, 0], 109 | [0, 0] 110 | ]; 111 | if (path[path.length - 1][0] > 0) { 112 | horizontal_line.reverse(); 113 | } 114 | path = path.concat(horizontal_line); 115 | 116 | // Duplicate and flip 117 | let flip = PathHelp.deepCopy(path); 118 | flip.reverse(); 119 | flip = PathHelp.rotatePath(flip, Math.PI); 120 | flip.shift(); 121 | path = path.concat(flip); 122 | 123 | // Define arc from home position to start of path 124 | const arc_from_home = PathHelp.arc( 125 | [0, 0], 126 | max_r, 127 | 0.5 * Math.PI, 128 | 0, 129 | 12 130 | ); 131 | 132 | // Define arc from end of path to home position 133 | const arc_to_home = PathHelp.arc( 134 | [0, 0], 135 | max_r, 136 | 0.5 * Math.PI, 137 | 1.5 * Math.PI, 138 | 12 139 | ); 140 | 141 | // Redefine path with start/ending paths 142 | path = arc_from_home.concat(path, arc_to_home); 143 | 144 | // path = PathHelp.simplify(path, 0.5 * this.env.ball.diameter); 145 | 146 | // Simplify decimal precision 147 | path = path.map(function(point) { 148 | return point.map(function(number) { 149 | return parseFloat(number.toFixed(2)); 150 | }); 151 | }); 152 | 153 | return path; 154 | } 155 | 156 | #easeOutCubic(x) { 157 | return 1 - Math.pow(1 - x, 3); 158 | } 159 | 160 | #easeInQuart(x) { 161 | return x * x * x * x; 162 | } 163 | 164 | #easeInCubic(x) { 165 | return x * x * x; 166 | } 167 | 168 | #easeInQuad(x) { 169 | return x * x; 170 | } 171 | 172 | #easeInCirc(x) { 173 | return 1 - Math.sqrt(1 - Math.pow(x, 2)); 174 | } 175 | 176 | #easeInExpo(x) { 177 | return x === 0 ? 0 : Math.pow(2, 10 * x - 10); 178 | } 179 | } 180 | 181 | export default Curvature; -------------------------------------------------------------------------------- /src/js/patterns/Cycloid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cycloid 3 | */ 4 | class Cycloid { 5 | 6 | constructor(env) { 7 | 8 | this.key = "cycloid"; 9 | 10 | this.name = "Cycloid"; 11 | 12 | this.config = { 13 | "radius_a": { 14 | "name": "Fixed Radius (A)", 15 | "value": null, 16 | "input": { 17 | "type": "createSlider", 18 | "params" : [ 19 | 1, 20 | Math.floor(0.5 * Math.min((env.table.x.max, env.table.y.max))), 21 | 30, 22 | 1 23 | ], 24 | "class": "slider", 25 | "displayValue": true 26 | } 27 | }, 28 | "radius_b": { 29 | "name": "Fixed Radius (B)", 30 | "value": null, 31 | "input": { 32 | "type": "createSlider", 33 | "params" : [ 34 | -Math.floor(0.5 * Math.min(env.table.x.max, env.table.y.max)), 35 | -1, 36 | -Math.floor(0.25 * Math.min(env.table.x.max, env.table.y.max)), 37 | 1 38 | ], 39 | "class": "slider", 40 | "displayValue": true 41 | } 42 | }, 43 | "arm_length": { 44 | "name": "Arm Length", 45 | "value": null, 46 | "input": { 47 | "type": "createSlider", 48 | "params" : [ 49 | 1, 50 | 0.5 * Math.min(env.table.x.max, env.table.y.max), 51 | Math.floor(0.25 * Math.min(env.table.x.max, env.table.y.max)), 52 | 1 53 | ], 54 | "class": "slider", 55 | "displayValue": true 56 | } 57 | }, 58 | "reverse": { 59 | "name": "Reverse", 60 | "value": null, 61 | "input": { 62 | "type": "createCheckbox", 63 | "attributes" : [{ 64 | "type" : "checkbox", 65 | "checked" : null 66 | }], 67 | "params": [0, 1, 0], 68 | "displayValue": false 69 | } 70 | } 71 | }; 72 | 73 | this.path = []; 74 | } 75 | 76 | draw() { 77 | 78 | // Update object 79 | this.config.radius_a.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 80 | this.config.radius_b.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 81 | this.config.arm_length.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 82 | 83 | // Display selected values 84 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.radius_a.value; 85 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.radius_b.value; 86 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.arm_length.value; 87 | 88 | // Calculate the path 89 | let path = this.calc( 90 | this.config.radius_a.value, 91 | this.config.radius_b.value, 92 | this.config.arm_length.value 93 | ); 94 | 95 | // Update object 96 | this.path = path; 97 | 98 | return path; 99 | } 100 | 101 | /** 102 | * Calculate coordinates for a Cycloid 103 | * 104 | * http://xahlee.info/SpecialPlaneCurves_dir/EpiHypocycloid_dir/epiHypocycloid.html 105 | **/ 106 | calc(radius_a, radius_b, arm_length) 107 | { 108 | // Set initial values 109 | var x; 110 | var y; 111 | var theta = 0; 112 | 113 | // Initialize shape path array 114 | // This stores the x,y coordinates for each step 115 | var path = new Array(); 116 | 117 | // Set the step multiplication factor. A value of 1 will increase theta 118 | // by 1-degree. A value of 10 will result in theta increasing by 119 | // 10-degrees for each drawing loop. A larger number results in fewer 120 | // instructions (and a faster drawing), but at lower curve resolution. 121 | // A small number has the best resolution, but results in a large instruction 122 | // set and slower draw times. 10 seems to be a good balance. 123 | var step_scale = 10; 124 | 125 | // Calculate the period of the Cycloid 126 | // It is 2-Pi times the result of the rolling circle's radius divided by the 127 | // Greatest Common Divisor of the two circle radii. 128 | // https://www.reddit.com/r/math/comments/27nz3l/how_do_i_calculate_the_periodicity_of_a/ 129 | var cycloid_period = Math.abs(radius_b / this.greatest_common_divisor(parseInt(radius_a), parseInt(radius_b))) * (2 * Math.PI); 130 | 131 | // Continue as long as the design stays within bounds of the plotter 132 | const i_max = cycloid_period / (step_scale * (Math.PI / 180)) 133 | for (let i = 0; i <= i_max; i++) { 134 | 135 | // Calculate theta offset for the step 136 | theta = this.#radians(step_scale * i); 137 | 138 | // Cycloid parametric equations 139 | x = (radius_a + radius_b) * Math.cos(theta) + arm_length * Math.cos(((radius_a + radius_b)/radius_b) * theta); 140 | y = (radius_a + radius_b) * Math.sin(theta) + arm_length * Math.sin(((radius_a + radius_b)/radius_b) * theta); 141 | 142 | // Add coordinates to shape array 143 | path.push([x,y]); 144 | 145 | // Increment iteration counter 146 | i++; 147 | } 148 | 149 | return path; 150 | } 151 | 152 | #radians(degrees) { 153 | return degrees * (Math.PI / 180); 154 | } 155 | 156 | /** 157 | * Calculate the Greatest Common Divisor (or Highest Common Factor) of 2 numbers 158 | * 159 | * https://en.wikipedia.org/wiki/Greatest_common_divisor 160 | * https://www.geeksforgeeks.org/c-program-find-gcd-hcf-two-numbers/ 161 | */ 162 | greatest_common_divisor(a, b) { 163 | if (b == 0) { 164 | return a; 165 | } 166 | return this.greatest_common_divisor(b, a % b); 167 | } 168 | } 169 | 170 | export default Cycloid; -------------------------------------------------------------------------------- /src/js/patterns/Diameters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Diameters 3 | */ 4 | class Diameters { 5 | 6 | constructor(env) { 7 | 8 | this.key = "diameters"; 9 | 10 | this.name = "Diameters"; 11 | 12 | this.env = env; 13 | 14 | this.config = { 15 | "spokes": { 16 | "name": "Spokes", 17 | "value": null, 18 | "input": { 19 | "type": "createSlider", 20 | "params" : [ 21 | 2, 22 | 60, 23 | 12, 24 | 2 25 | ], 26 | "class": "slider", 27 | "displayValue": true 28 | } 29 | }, 30 | "waves": { 31 | "name": "Waves", 32 | "value": null, 33 | "input": { 34 | "type": "createSlider", 35 | "params" : [ 36 | 1, 37 | 30, 38 | 4, 39 | 1 40 | ], 41 | "class": "slider", 42 | "displayValue": true 43 | } 44 | }, 45 | "amplitude": { 46 | "name": "Amplitude", 47 | "value": null, 48 | "input": { 49 | "type": "createSlider", 50 | "params" : [ 51 | 0, 52 | 60, 53 | 20, 54 | 1 55 | ], 56 | "class": "slider", 57 | "displayValue": true 58 | } 59 | } 60 | }; 61 | 62 | this.path = []; 63 | } 64 | 65 | draw() { 66 | 67 | // Update object 68 | this.config.spokes.value = document.querySelector('#pattern-controls > div:nth-child(1) > input').value; 69 | this.config.waves.value = document.querySelector('#pattern-controls > div:nth-child(2) > input').value; 70 | this.config.amplitude.value = document.querySelector('#pattern-controls > div:nth-child(3) > input').value; 71 | 72 | // Display selected values 73 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.spokes.value; 74 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.waves.value; 75 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.amplitude.value; 76 | 77 | // Calculate the path 78 | let path = this.calc( 79 | parseInt(this.config.spokes.value), 80 | parseInt(this.config.waves.value), 81 | parseInt(this.config.amplitude.value) 82 | ); 83 | 84 | // Update object 85 | this.path = path; 86 | 87 | return path; 88 | } 89 | 90 | /** 91 | * Diameters that cross the circle 92 | **/ 93 | calc(num_spokes, num_waves, wave_amplitude) 94 | { 95 | 96 | // Set initial values 97 | var x; 98 | var y; 99 | var theta = 0; 100 | 101 | const max_x = this.env.table.x.max; 102 | const max_y = this.env.table.y.max; 103 | 104 | // Initialize shape path array 105 | // This stores the x,y coordinates for each step 106 | var path = new Array(); 107 | 108 | // Iteration counter. 109 | var step = 0; 110 | 111 | // Change in theta per step 112 | var theta_per_step = (2*Math.PI) / num_spokes; 113 | 114 | // Sub-steps 115 | var sub_steps = 20 * num_waves; 116 | 117 | // Set direction of travel for "x" 118 | var direction = 1; 119 | 120 | // Loop through 360 degrees 121 | while (theta < (2*Math.PI)) { 122 | 123 | // Calculate new theta 124 | theta = step * theta_per_step; 125 | 126 | for (var j = 0; j <= sub_steps; j++) { 127 | 128 | // Sine Wave 129 | x = direction * (Math.min(max_x, max_y)/2) * ((j - (sub_steps/2))/(sub_steps/2)); 130 | y = wave_amplitude * Math.sin((j/sub_steps) * num_waves * (2*Math.PI)); 131 | 132 | // Rotate [x,y] coordinates around [0,0] by angle theta, and then append to path 133 | path.push( 134 | this.rotationMatrix(x, y, theta) 135 | ); 136 | } 137 | 138 | // Increment iteration counter 139 | step++; 140 | 141 | // Alternate the direction each step, going from +x to -x 142 | direction = direction * -1; 143 | } 144 | 145 | return path; 146 | } 147 | 148 | /** 149 | * Rotate points x and y by angle theta about center point (0,0) 150 | * https://en.wikipedia.org/wiki/Rotation_matrix 151 | **/ 152 | rotationMatrix(x, y, theta) { 153 | return [ 154 | x * Math.cos(theta) - y * Math.sin(theta), 155 | x * Math.sin(theta) + y * Math.cos(theta) 156 | ]; 157 | } 158 | } 159 | 160 | export default Diameters; -------------------------------------------------------------------------------- /src/js/patterns/Draw.js: -------------------------------------------------------------------------------- 1 | class Draw { 2 | 3 | constructor(env) { 4 | 5 | this.key = "draw"; 6 | 7 | this.name = "Free Draw"; 8 | 9 | this.env = env; 10 | 11 | this.config = { 12 | }; 13 | 14 | this.path = [[0,0]]; 15 | } 16 | 17 | 18 | draw() { 19 | 20 | const min_x = this.env.table.x.min; 21 | const max_x = this.env.table.x.max; 22 | const min_y = this.env.table.y.min; 23 | const max_y = this.env.table.y.max; 24 | const mouseX = this.env.mouse.x; 25 | const mouseY = this.env.mouse.y; 26 | 27 | let x; 28 | let y; 29 | let plotter_x, plotter_y; 30 | 31 | let path = []; 32 | 33 | if (this.env.mouse.pressed) { 34 | 35 | // Translate Canvas coordinates to table coordinates 36 | x = mouseX - this.env.canvas.width/2; 37 | y = -(mouseY - this.env.canvas.height/2); 38 | 39 | // Translate "centered" coordinates to plotter position 40 | plotter_x = x + ((max_x - min_x)/2); 41 | plotter_y = y + ((max_y - min_y)/2); 42 | 43 | // Add to path if within plotter bounds 44 | if (plotter_x > min_x && plotter_x < max_x && plotter_y > min_y && plotter_y < max_y) { 45 | 46 | // On first press replace [0,0] initial value with x,y 47 | if (path.length == 1 && path[0][0] == 0 && path[0][1] == 0) { 48 | this.path = [[x,y]]; 49 | } else { 50 | this.path.push([x,y]); 51 | } 52 | } 53 | } 54 | 55 | return this.path; 56 | } 57 | } 58 | 59 | export default Draw; -------------------------------------------------------------------------------- /src/js/patterns/Egg.js: -------------------------------------------------------------------------------- 1 | class Egg { 2 | 3 | constructor(env) { 4 | 5 | this.key = "egg"; 6 | 7 | this.name = "Easter Eggs"; 8 | 9 | let max_r = Math.min((env.table.x.max - env.table.x.min), (env.table.y.max - env.table.y.min))/2; 10 | 11 | this.config = { 12 | "radius": { 13 | "name": "Size", 14 | "value": null, 15 | "input": { 16 | "type": "createSlider", 17 | "params" : [ 18 | 80, 19 | 0.5 * Math.min(env.table.x.max, env.table.y.max), 20 | max_r/2, 21 | 1 22 | ], 23 | "class": "slider", 24 | "displayValue": true 25 | } 26 | }, 27 | "reverse": { 28 | "name": "Reverse", 29 | "value": null, 30 | "input": { 31 | "type": "createCheckbox", 32 | "attributes" : [{ 33 | "type" : "checkbox", 34 | "checked" : null 35 | }], 36 | "params": [0, 1, 0], 37 | "displayValue": false 38 | } 39 | } 40 | }; 41 | 42 | this.path = []; 43 | } 44 | 45 | draw() { 46 | 47 | // Update object 48 | this.config.radius.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 49 | 50 | // Display selected values 51 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.radius.value; 52 | 53 | // Calculate path for Circle at center 54 | let path = this.calc( 55 | this.config.radius.value 56 | ); 57 | 58 | // Update object 59 | this.path = path; 60 | 61 | return path; 62 | } 63 | 64 | /** 65 | * Egg 66 | **/ 67 | calc(radius) { 68 | 69 | // Set initial values 70 | var theta; 71 | let egg_path = new Array(); 72 | let egg_1; 73 | let egg_2; 74 | 75 | // Initialize return value - the path array 76 | // This stores the x,y coordinates for each step 77 | var path = new Array(); 78 | var point = new Array(); 79 | 80 | // The number of "sides" to the shape. 81 | // A larger number makes the shape more smooth 82 | let sides = 60; 83 | 84 | // Calculate standard Egg shape 85 | for (var i = 0; i <= sides; i++) { 86 | 87 | // Rotational Angle 88 | theta = (i/sides) * (2 * Math.PI) - Math.PI; 89 | 90 | // Get coordinates for shape 91 | point = this.parametricEgg(theta); 92 | 93 | // Add coordinates to shape array 94 | egg_path.push([ 95 | radius * point[0], 96 | radius * point[1] 97 | ]); 98 | } 99 | 100 | // Move Standard Egg to Center 101 | let path_center = this.getPathCenter(egg_path); 102 | egg_path = this.translatePath(egg_path, [-path_center[0], -path_center[1]]); 103 | 104 | // Rotate 90 degrees clockwise so it's "up" 105 | egg_path = this.rotatePath(egg_path, -0.5 * Math.PI); 106 | 107 | // Use the Egg path to create an Egg filled with a spiral 108 | let spiral_path = this.spiralizePath(egg_path, 10, 60); 109 | egg_1 = egg_path.concat(spiral_path.reverse()); 110 | egg_1 = this.rotatePath(egg_1, Math.PI/10); 111 | egg_1 = this.translatePath(egg_1, [-30, 0]) 112 | 113 | // Use the Egg path to create an Egg filled with stripes 114 | let stripe_path = this.stripePath(egg_path); 115 | egg_2 = stripe_path.reverse(); 116 | egg_2 = egg_2.concat(egg_path); 117 | egg_2 = this.scalePath(egg_2, 0.8); 118 | egg_2 = this.translatePath(egg_2, [30, 0]); 119 | egg_2 = this.rotatePath(egg_2, -Math.PI/8); 120 | 121 | // Join Egg 1 with Egg 2 122 | path = egg_1.concat(egg_2) 123 | 124 | return path; 125 | } 126 | 127 | /** 128 | * Parametric Equation for an Egg shape 129 | * 130 | * https://math.stackexchange.com/questions/3375853/parametric-equations-for-a-true-egg-shape 131 | */ 132 | parametricEgg(theta) { 133 | 134 | let x,y; 135 | let k = 1.00; 136 | let b = 2.02 137 | 138 | x = (1 / (2 * Math.sqrt(1 + Math.pow(k,2)))) 139 | * ( 140 | (((Math.pow(k,2) - 1)/k)*b) 141 | + ( 142 | (((Math.pow(k,2) + 1)/k)*b) 143 | * 144 | Math.sqrt(Math.pow(b,2) - 4 * k * Math.cos(theta)) 145 | ) 146 | ); 147 | 148 | y = (2 * Math.sin(theta)) 149 | / ( 150 | b + Math.sqrt(Math.pow(b,2) - 4 * k * Math.cos(theta)) 151 | ); 152 | 153 | // This is my customization because the equations as translated 154 | // from the source web site didn't look quite right. 155 | x = 0.4 * x; 156 | 157 | return [x, y]; 158 | } 159 | 160 | /** 161 | * Return column of a multidimensional array 162 | **/ 163 | arrayColumn(arr, n) { 164 | return arr.map(a => a[n]); 165 | } 166 | 167 | /** 168 | * Get the center point of the path 169 | **/ 170 | getPathCenter(path) { 171 | 172 | // Get X and Y coordinates as an 1-dimensional array 173 | let x_coordinates = this.arrayColumn(path, 0); 174 | let y_coordinates = this.arrayColumn(path, 1); 175 | let min_x = Math.min(...x_coordinates) 176 | let min_y = Math.min(...y_coordinates); 177 | 178 | return [ 179 | min_x + (Math.max(...x_coordinates) - min_x) / 2, 180 | min_y + (Math.max(...y_coordinates) - min_y) / 2 181 | ]; 182 | } 183 | 184 | /** 185 | * Fill Path with Stripes 186 | * This doesn't do what I want, which is horizontal, level stripes, 187 | * but it's fine for now. 188 | **/ 189 | stripePath(path) { 190 | let new_path = new Array(); 191 | let index; 192 | for (var i = 0; i < path.length; i++) { 193 | if (i % 4 == 0) { 194 | index = i; 195 | } else if (i % 4 == 1) { 196 | index = i; 197 | } else if (i % 4 == 2) { 198 | index = path.length - i; 199 | } else if (i % 4 == 3) { 200 | index = path.length - i; 201 | } 202 | new_path.push(path[index]); 203 | } 204 | return new_path; 205 | } 206 | 207 | /** 208 | * Spiralize Path 209 | **/ 210 | spiralizePath(path, revolutions) { 211 | let point = new Array(); 212 | let new_path = new Array(); 213 | let i_max = path.length * revolutions; 214 | // let path_center = this.getPathCenter(path); 215 | let spiral_center = [0, -50] 216 | for (var i = 0; i < i_max; i++) { 217 | point = [ 218 | path[i % path.length][0], 219 | path[i % path.length][1] 220 | ] 221 | new_path.push([ 222 | -((point[0] * (i/i_max)) + (-spiral_center[0] * i/i_max))+spiral_center[0], 223 | ((point[1] * (i/i_max)) + (-spiral_center[1] * i/i_max))+spiral_center[1], 224 | ]); 225 | } 226 | return new_path; 227 | } 228 | 229 | /** 230 | * Scale Path 231 | * path A path array of [x,y] coordinates 232 | * scale A value from 0 to 1 233 | **/ 234 | scalePath(path, scale) { 235 | return path.map(function(a){ 236 | return [ 237 | a[0] * scale, 238 | a[1] * scale 239 | ]; 240 | }); 241 | } 242 | 243 | /** 244 | * Translate a path 245 | **/ 246 | translatePath(path, delta) { 247 | return path.map(function(a){ 248 | return [ 249 | a[0] + delta[0], 250 | a[1] + delta[1] 251 | ]; 252 | }); 253 | } 254 | 255 | /** 256 | * Rotate points x and y by angle theta about center point (0,0) 257 | * https://en.wikipedia.org/wiki/Rotation_matrix 258 | **/ 259 | rotatePath(path, theta) { 260 | return path.map(function(a){ 261 | return [ 262 | a[0] * Math.cos(theta) - a[1] * Math.sin(theta), 263 | a[0] * Math.sin(theta) + a[1] * Math.cos(theta) 264 | ] 265 | }); 266 | } 267 | } 268 | 269 | export default Egg; -------------------------------------------------------------------------------- /src/js/patterns/Farris.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Farris Curve 3 | * http://www.quantamagazine.org/how-to-create-art-with-mathematics-20151008 4 | * http://www.sineofthetimes.org/the-art-of-parametric-equations-2/ 5 | */ 6 | class Farris { 7 | 8 | constructor(env) { 9 | 10 | this.key = "farris"; 11 | 12 | this.name = "Farris Curve"; 13 | 14 | this.env = env; 15 | 16 | this.path_sampling_optimization = 2; 17 | 18 | // Define the parametric equations using text inputs 19 | this.config = { 20 | "A": { 21 | "name": "A Coefficient", 22 | "value": null, 23 | "input": { 24 | "type": "createSlider", 25 | "params" : [ 26 | 0, 27 | 20, 28 | 1, 29 | 1 30 | ], 31 | "class": "slider", 32 | "displayValue": true 33 | } 34 | }, 35 | "B": { 36 | "name": "B Coefficient", 37 | "value": null, 38 | "input": { 39 | "type": "createSlider", 40 | "params" : [ 41 | 0, 42 | 20, 43 | 6, 44 | 1 45 | ], 46 | "class": "slider", 47 | "displayValue": true 48 | } 49 | }, 50 | "C": { 51 | "name": "C Coefficient", 52 | "value": null, 53 | "input": { 54 | "type": "createSlider", 55 | "params" : [ 56 | 0, 57 | 20, 58 | 14, 59 | 1 60 | ], 61 | "class": "slider", 62 | "displayValue": true 63 | } 64 | }, 65 | "scale": { 66 | "name": "Scale", 67 | "value": null, 68 | "input": { 69 | "type": "createSlider", 70 | "params" : [ 71 | 0, 72 | 100, 73 | 25, 74 | 1 75 | ], 76 | "class": "slider", 77 | "displayValue": true 78 | } 79 | }, 80 | "rotation": { 81 | "name": "Rotation", 82 | "value": null, 83 | "input": { 84 | "type": "createSlider", 85 | "params" : [ 86 | -180, 87 | 180, 88 | 0, 89 | 1 90 | ], 91 | "class": "slider", 92 | "displayValue": true 93 | } 94 | } 95 | }; 96 | 97 | this.path = []; 98 | } 99 | 100 | draw() { 101 | 102 | const min_x = this.env.table.x.min; 103 | const max_x = this.env.table.x.max; 104 | const min_y = this.env.table.y.min; 105 | const max_y = this.env.table.y.max; 106 | 107 | // Update object 108 | this.config.A.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 109 | this.config.B.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 110 | this.config.C.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 111 | this.config.scale.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 112 | this.config.rotation.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(5) > input').value); 113 | 114 | // Display selected value(s) 115 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.A.value.toFixed(0); 116 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.B.value.toFixed(0); 117 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.C.value.toFixed(0); 118 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.scale.value.toFixed(0) + "%"; 119 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(5) > span').innerHTML = this.config.rotation.value.toFixed(0) + "°"; 120 | 121 | let path = this.calc( 122 | Math.min((max_x - min_x), (max_y - min_y)), 123 | this.config.A.value, 124 | this.config.B.value, 125 | this.config.C.value, 126 | this.config.scale.value, 127 | this.config.rotation.value 128 | ); 129 | 130 | // Update object 131 | this.path = path; 132 | 133 | return path; 134 | } 135 | 136 | /** 137 | * Calculate coordinates for the shape 138 | * 139 | * @return Array Path 140 | **/ 141 | calc(radius, A, B, C, scale, rotation) { 142 | 143 | // Set initial values 144 | var x; 145 | var y; 146 | var theta = 0.0; 147 | 148 | // Initialize return value - the path array 149 | // This stores the x,y coordinates for each step 150 | var path = new Array(); 151 | 152 | // Iteration counter. 153 | var step = 0; 154 | 155 | // Set period of full rotation 156 | let period = 2 * Math.PI; 157 | 158 | // Set the steps per revolution. Oversample and small distances can be optimized out afterward 159 | let steps_per_revolution = 1000; 160 | 161 | // Loop through one revolution 162 | while (theta < period) { 163 | 164 | // Rotational Angle (steps per rotation in the denominator) 165 | theta = (step/steps_per_revolution) * period 166 | 167 | // Run the parametric equations 168 | x = (scale/100) * radius * (Math.cos(A*theta) + Math.cos(B*theta)/2 + Math.sin(C*theta)/3); 169 | y = (scale/100) * radius * (Math.sin(A*theta) + Math.sin(B*theta)/2 + Math.cos(C*theta)/3); 170 | 171 | // Add coordinates to shape array 172 | path.push([x,y]); 173 | 174 | // Increment iteration counter 175 | step++; 176 | } 177 | 178 | // Rotate 179 | // Every pattern is "rotated" by 12.5 degrees at Theta=0. 180 | // I'm applying a base rotation so the pattern starts on the X-axis 181 | rotation = rotation - Math.atan2(1/3, 1.5) * 180 / Math.PI; 182 | path = path.map(function(element) { 183 | return this.rotationMatrix(element[0], element[1], rotation * (Math.PI/180)) 184 | }, this); 185 | 186 | return path; 187 | } 188 | 189 | /** 190 | * Rotate points x and y by angle theta about center point (0,0) 191 | * https://en.wikipedia.org/wiki/Rotation_matrix 192 | **/ 193 | rotationMatrix(x, y, theta) { 194 | return [ 195 | x * Math.cos(theta) - y * Math.sin(theta), 196 | x * Math.sin(theta) + y * Math.cos(theta) 197 | ]; 198 | } 199 | } 200 | 201 | export default Farris; -------------------------------------------------------------------------------- /src/js/patterns/FermatSpiral.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fermat's Spiral 3 | * 4 | * https://en.wikipedia.org/wiki/Fermat%27s_spiral 5 | */ 6 | class FermatSpiral { 7 | 8 | constructor(env) { 9 | 10 | this.key = "fermatspiral"; 11 | 12 | this.name = "Fermat's Spiral"; 13 | 14 | this.env = env; 15 | 16 | this.config = { 17 | "revolutions": { 18 | "name": "Revolutions", 19 | "value": null, 20 | "input": { 21 | "type": "createSlider", 22 | "params" : [ 23 | 1, 24 | 10, 25 | 3, 26 | 1 27 | ], 28 | "class": "slider", 29 | "displayValue": true 30 | } 31 | }, 32 | "return": { 33 | "name": "Return Home", 34 | "value": null, 35 | "input": { 36 | "type": "createCheckbox", 37 | "attributes" : [{ 38 | "type" : "checkbox", 39 | "checked" : null 40 | }], 41 | "params": [0, 1, 0], 42 | "displayValue": false 43 | } 44 | }, 45 | "reverse": { 46 | "name": "Reverse", 47 | "value": null, 48 | "input": { 49 | "type": "createCheckbox", 50 | "attributes" : [{ 51 | "type" : "checkbox", 52 | "checked" : null 53 | }], 54 | "params": [0, 1, 0], 55 | "displayValue": false 56 | } 57 | } 58 | }; 59 | 60 | this.path = []; 61 | } 62 | 63 | 64 | /** 65 | * Draw path - Use class's "calc" method to convert inputs to a draw path 66 | */ 67 | draw() { 68 | 69 | // Read in selected value(s) 70 | this.config.revolutions.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 71 | 72 | // Display selected value(s) 73 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.revolutions.value; 74 | 75 | // Return to home 76 | this.config.return.value = false; 77 | if (document.querySelector('#pattern-controls > div:nth-child(2) > input[type=checkbox]').checked) { 78 | this.config.return.value = true; 79 | } 80 | 81 | // Calculate path 82 | let path = this.calc( 83 | this.config.revolutions.value, 84 | this.config.return.value 85 | ); 86 | 87 | // Update object 88 | this.path = path; 89 | 90 | return path; 91 | } 92 | 93 | /** 94 | * Calculate coordinates for the shape 95 | * 96 | * @param integer Revolutions 97 | * 98 | * @return Array Path 99 | **/ 100 | calc(revolutions, return_to_home) { 101 | 102 | /* 103 | Recommended settings: 104 | 105 | revolutions: 3 106 | a: 10 107 | pow_n: 1.0 108 | 109 | */ 110 | 111 | const min_x = this.env.table.x.min; 112 | const max_x = this.env.table.x.max; 113 | const min_y = this.env.table.y.min; 114 | const max_y = this.env.table.y.max; 115 | 116 | // Set initial values 117 | var x; 118 | var y; 119 | 120 | // Initialize return value - the path array 121 | // This stores the x,y coordinates for each step 122 | var path = new Array(); 123 | 124 | // Controls "tightness" of spiral. 1.0 is a good value 125 | const pow_n = 1.0; 126 | 127 | // Radius of spiral 128 | let a = 30 / revolutions; 129 | 130 | // The number of "sides" to the circle. 131 | const steps_per_revolution = 60; 132 | 133 | // Loop through one revolution 134 | const t_min = revolutions * 0; 135 | const t_max = revolutions * (2 * Math.PI); 136 | const t_step = (t_max - t_min) / (revolutions * steps_per_revolution); 137 | 138 | // Negative Radius 139 | for (var t = t_max; t >= t_min; t -= t_step) { 140 | 141 | // Run the parametric equations 142 | x = a * Math.pow(t, pow_n) * Math.cos(t); 143 | y = a * Math.pow(t, pow_n) * Math.sin(t); 144 | 145 | // Add coordinates to shape array 146 | path.push([x,y]); 147 | } 148 | 149 | // Positive Radius 150 | for (let t = t_min; t <= t_max + t_step; t += t_step) { 151 | 152 | // Run the parametric equations 153 | x = -a * Math.pow(t, pow_n) * Math.cos(t); 154 | y = -a * Math.pow(t, pow_n) * Math.sin(t); 155 | 156 | // Add coordinates to shape array 157 | path.push([x,y]); 158 | } 159 | 160 | if (return_to_home) { 161 | var i_max = 24; 162 | let r = Math.min(max_x - min_x, max_y - min_y) / 2; 163 | for (var i = 1; i <= i_max; i++) { 164 | path.push([ 165 | r * Math.cos(i/i_max * Math.PI + Math.PI), 166 | r * Math.sin(i/i_max * Math.PI + Math.PI), 167 | ]); 168 | } 169 | path.push([r, 0]); 170 | } 171 | 172 | return path; 173 | } 174 | } 175 | 176 | export default FermatSpiral; -------------------------------------------------------------------------------- /src/js/patterns/Fibonacci.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fibonacci 3 | */ 4 | class Fibonacci { 5 | 6 | constructor(env) { 7 | 8 | this.key = "fibonacci"; 9 | 10 | this.name = "Fibonacci"; 11 | 12 | this.env = env; 13 | 14 | this.config = { 15 | "decay": { 16 | "name": "Decay Factor", 17 | "value": null, 18 | "input": { 19 | "type": "createSlider", 20 | "params" : [ 21 | -0.020, 22 | -0.001, 23 | -0.004, 24 | 0.0001 25 | ], 26 | "class": "slider", 27 | "displayValue": true 28 | } 29 | }, 30 | "rtc": { 31 | "name": "Return to Center", 32 | "value": null, 33 | "input": { 34 | "type": "createCheckbox", 35 | "attributes" : [{ 36 | "type" : "checkbox", 37 | "checked" : null 38 | }], 39 | "params": [0, 1, 0], 40 | "displayValue": false 41 | } 42 | }, 43 | "reverse": { 44 | "name": "Reverse", 45 | "value": null, 46 | "input": { 47 | "type": "createCheckbox", 48 | "attributes" : [{ 49 | "type" : "checkbox", 50 | "checked" : null 51 | }], 52 | "params": [0, 1, 0], 53 | "displayValue": false 54 | } 55 | } 56 | }; 57 | 58 | this.path = []; 59 | } 60 | 61 | draw() { 62 | 63 | // Update object 64 | this.config.decay.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 65 | this.config.rtc.value = false; 66 | if (document.querySelector('#pattern-controls > div:nth-child(2) > input[type=checkbox]').checked) { 67 | this.config.rtc.value = true; 68 | } 69 | 70 | // Display selected values 71 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.decay.value.toFixed(4); 72 | 73 | // Calculate the path 74 | let path = this.calc( 75 | this.config.decay.value, 76 | this.config.rtc.value 77 | ); 78 | 79 | // Update object 80 | this.path = path; 81 | 82 | return path; 83 | } 84 | 85 | /* 86 | * Draw Fibonacci Spiral Spokes 87 | * 88 | * Type: Radial 89 | **/ 90 | calc(radius_shrink_factor, return_to_center) 91 | { 92 | 93 | const min_x = this.env.table.x.min; 94 | const max_x = this.env.table.x.max; 95 | const min_y = this.env.table.y.min; 96 | const max_y = this.env.table.y.max; 97 | const ball_size = this.env.ball.diameter; 98 | 99 | var path = new Array(); 100 | var r_max = Math.min(max_x - min_x, max_y - min_y) / 2; 101 | var r; 102 | var theta; 103 | var x, y; 104 | 105 | // Calculate the number of iterations required to decay 106 | // to a minimum value; 107 | var r_min = ball_size / 2; 108 | var i_max = Math.log(r_min/r_max) / radius_shrink_factor; 109 | 110 | // Loop through iterations 111 | for (var i = 0; i < i_max; i++) { 112 | 113 | // Increment theta by golden ratio each iteration 114 | // https://en.wikipedia.org/wiki/Golden_angle 115 | theta = i * Math.PI * (3.0 - Math.sqrt(5)); 116 | 117 | // Set the radius 118 | r = r_max * Math.exp(radius_shrink_factor * i) 119 | 120 | // Convert to cartesian 121 | x = r * Math.cos(theta); 122 | y = r * Math.sin(theta); 123 | 124 | // Add point to path 125 | path.push([x,y]); 126 | 127 | // Go back to center 128 | if (return_to_center) { 129 | path.push([0,0]); 130 | } 131 | } 132 | 133 | // End in center 134 | if (!return_to_center) { 135 | path.push([0,0]); 136 | } 137 | 138 | return path; 139 | } 140 | } 141 | 142 | export default Fibonacci; -------------------------------------------------------------------------------- /src/js/patterns/FibonacciLollipops.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fibonacci Lollipops 3 | */ 4 | class FibonacciLollipops { 5 | 6 | constructor(env) { 7 | 8 | this.key = "fibonaccilollipops"; 9 | 10 | this.name = "Fibonacci Lollipops"; 11 | 12 | this.env = env; 13 | 14 | this.config = { 15 | "lollipopradius": { 16 | "name": "Lollipop Radius", 17 | "value": null, 18 | "input": { 19 | "type": "createSlider", 20 | "params" : [ 21 | 10, 22 | 60, 23 | 30, 24 | 1 25 | ], 26 | "class": "slider", 27 | "displayValue": true 28 | } 29 | }, 30 | "lollipopsides": { 31 | "name": "Lollipop Sides", 32 | "value": null, 33 | "input": { 34 | "type": "createSlider", 35 | "params" : [ 36 | 3, 37 | 12, 38 | 6, 39 | 1 40 | ], 41 | "class": "slider", 42 | "displayValue": true 43 | } 44 | }, 45 | "lollipopturns": { 46 | "name": "Lollipop Turns", 47 | "value": null, 48 | "input": { 49 | "type": "createSlider", 50 | "params" : [ 51 | 1.5, 52 | 7.5, 53 | 3.5, 54 | 1 55 | ], 56 | "class": "slider", 57 | "displayValue": true 58 | } 59 | }, 60 | "spiral_factor": { 61 | "name": "Shrink Factor", 62 | "value": null, 63 | "input": { 64 | "type": "createSlider", 65 | "params" : [ 66 | -0.020, 67 | -0.003, 68 | -0.010, 69 | 0.0001 70 | ], 71 | "class": "slider", 72 | "displayValue": true 73 | } 74 | } 75 | }; 76 | 77 | this.path = []; 78 | } 79 | 80 | 81 | /** 82 | * Draw path - Use class's "calc" method to convert inputs to a draw path 83 | */ 84 | draw() { 85 | 86 | // Read in selected value(s) 87 | this.config.lollipopradius.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 88 | this.config.lollipopsides.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 89 | this.config.lollipopturns.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 90 | this.config.spiral_factor.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 91 | 92 | // Display selected value(s) 93 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.lollipopradius.value; 94 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.lollipopsides.value; 95 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.lollipopturns.value; 96 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.spiral_factor.value.toFixed(4); 97 | 98 | 99 | // Calculate path 100 | let path = this.calc( 101 | this.config.lollipopradius.value, 102 | this.config.lollipopsides.value, 103 | this.config.lollipopturns.value, 104 | this.config.spiral_factor.value 105 | ); 106 | 107 | // Update object 108 | this.path = path; 109 | 110 | return path; 111 | } 112 | 113 | /** 114 | * Calculate coordinates for the shape 115 | * 116 | * @param integer Revolutions 117 | * 118 | * @return Array Path 119 | **/ 120 | calc(spiral_r_max, spiral_sides, spiral_revolutions, radius_shrink_factor) { 121 | 122 | const min_x = this.env.table.x.min; 123 | const max_x = this.env.table.x.max; 124 | const min_y = this.env.table.y.min; 125 | const max_y = this.env.table.y.max; 126 | const ball_size = this.env.ball.diameter; 127 | 128 | // Initialize shape path array 129 | // This stores the x,y coordinates for each step 130 | var path = new Array(); 131 | var r_max = Math.min(max_x-min_x, max_y-min_y) / 2; 132 | var r; 133 | var theta; 134 | 135 | // Calculate the number of iterations required to decay 136 | // to a minimum value; 137 | var r_min = ball_size / 2; 138 | var i_max = Math.log(r_min/r_max) / radius_shrink_factor; 139 | 140 | // "Lollipop" Spiral 141 | var sub_path = new Array(); 142 | var x1, y1; 143 | var spiral_r, spiral_theta; 144 | 145 | // Loop through iterations 146 | for (var i = 0; i < i_max; i++) { 147 | 148 | // Increment theta by golden ratio each iteration 149 | // https://en.wikipedia.org/wiki/Golden_angle 150 | theta = i * Math.PI * (3.0 - Math.sqrt(5)); 151 | 152 | // Set the radius of the Fibonacci "petal" (lollipop height) 153 | // Decrease the radius a bit each cycle 154 | r = (r_max - spiral_r_max) * Math.exp(radius_shrink_factor * i); 155 | 156 | // Lollipop Spiral 157 | sub_path = []; 158 | for (var k = 0; k <= spiral_revolutions * spiral_sides; k++) { 159 | spiral_theta = (k/spiral_sides) * (2 * Math.PI); 160 | spiral_r = (spiral_r_max * (1 - 0.5 * (i/i_max))) * (k/(spiral_revolutions * spiral_sides)); 161 | x1 = spiral_r * Math.cos(spiral_theta); 162 | y1 = spiral_r * Math.sin(spiral_theta); 163 | sub_path.push([x1,y1]); 164 | } 165 | 166 | // Translate circle to end of lollipop 167 | sub_path = this.translate_path(sub_path, r, 0); 168 | 169 | path = path.concat(this.rotationMatrix(sub_path, theta)); 170 | 171 | // Return to center; 172 | path.push([0.0, 0.0]); 173 | } 174 | 175 | return path; 176 | } 177 | 178 | /** 179 | * Translate a path 180 | **/ 181 | translate_path(path, x_delta, y_delta) { 182 | return path.map(function(a){ 183 | return [ 184 | a[0] + x_delta, 185 | a[1] + y_delta 186 | ]; 187 | }); 188 | } 189 | 190 | /** 191 | * Rotate points x and y by angle theta about center point (0,0) 192 | * https://en.wikipedia.org/wiki/Rotation_matrix 193 | **/ 194 | rotationMatrix(path, theta) { 195 | return path.map(function(a){ 196 | return [ 197 | a[0] * Math.cos(theta) - a[1] * Math.sin(theta), 198 | a[0] * Math.sin(theta) + a[1] * Math.cos(theta) 199 | ] 200 | }); 201 | } 202 | 203 | } 204 | 205 | export default FibonacciLollipops; -------------------------------------------------------------------------------- /src/js/patterns/Frame.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Frame 3 | */ 4 | class Frame { 5 | 6 | constructor(env) { 7 | 8 | this.key = "frame"; 9 | 10 | this.name = "Frames (Border Patterns)"; 11 | 12 | this.env = env; 13 | 14 | this.config = { 15 | "num_spiral": { 16 | "name": "Spirals", 17 | "value": null, 18 | "input": { 19 | "type": "createSlider", 20 | "params" : [ 21 | 2, 22 | 12, 23 | 4, 24 | 1 25 | ], 26 | "class": "slider", 27 | "displayValue": true 28 | } 29 | }, 30 | "a": { 31 | "name": "a", 32 | "value": null, 33 | "input": { 34 | "type": "createSlider", 35 | "params" : [ 36 | -1.0, 37 | 1.0, 38 | 0.5, 39 | 0.01 40 | ], 41 | "class": "slider", 42 | "displayValue": true 43 | } 44 | }, 45 | "b": { 46 | "name": "b", 47 | "value": null, 48 | "input": { 49 | "type": "createSlider", 50 | "params" : [ 51 | -1.0, 52 | 0.0, 53 | -0.35, 54 | 0.01 55 | ], 56 | "class": "slider", 57 | "displayValue": true 58 | } 59 | }, 60 | "revolutions": { 61 | "name": "Revolutions", 62 | "value": null, 63 | "input": { 64 | "type": "createSlider", 65 | "params" : [ 66 | 0.1, 67 | 4, 68 | 2, 69 | 0.1 70 | ], 71 | "class": "slider", 72 | "displayValue": true 73 | } 74 | }, 75 | "rotate": { 76 | "name": "Rotate", 77 | "value": null, 78 | "input": { 79 | "type": "createSlider", 80 | "params": [ 81 | 0, 82 | 360, 83 | 0, 84 | 1 85 | ], 86 | "class": "slider", 87 | "displayValue": true 88 | } 89 | } 90 | }; 91 | 92 | this.path = []; 93 | } 94 | 95 | /** 96 | * Draw path - Use class's "calc" method to convert inputs to a draw path 97 | */ 98 | draw() { 99 | 100 | // Read in selected value(s) 101 | this.config.num_spiral.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 102 | this.config.a.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 103 | this.config.b.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 104 | this.config.revolutions.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 105 | this.config.rotate.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(5) > input').value); 106 | 107 | // Display selected value(s) 108 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.num_spiral.value; 109 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.a.value.toFixed(2) + " * rmax"; 110 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.b.value.toFixed(2); 111 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.revolutions.value.toFixed(1); 112 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(5) > span').innerHTML = this.config.rotate.value + "°"; 113 | 114 | // Calculate path 115 | let path = this.calc( 116 | this.config.num_spiral.value, 117 | this.config.a.value, 118 | this.config.b.value, 119 | this.config.revolutions.value, 120 | 60 121 | ); 122 | 123 | // Rotate path around the center of drawing area 124 | if (this.config.rotate.value > 0) { 125 | path = this.rotationMatrix(path, this.config.rotate.value * (Math.PI/180)); 126 | } 127 | 128 | // Update object 129 | this.path = path; 130 | 131 | return path; 132 | } 133 | 134 | /** 135 | * Calculate coordinates for the shape 136 | * 137 | * @param integer Revolutions 138 | * 139 | * @return Array Path 140 | **/ 141 | calc(num_spirals, a, b, revolutions, sides = 60) { 142 | 143 | const max_x = this.env.table.x.max; 144 | const max_y = this.env.table.y.max; 145 | 146 | let radius = Math.min(max_x/2, max_y/2); 147 | 148 | // Initialize return value - the path array 149 | // This stores the x,y coordinates for each step 150 | let spiral = this.LogSpiral(a, b, revolutions, sides) 151 | 152 | var translate_x = -spiral[0][0]; 153 | spiral = this.translate_path(spiral, translate_x, 0) 154 | spiral = this.rotationMatrix(spiral, -0.100 * Math.PI) 155 | spiral = this.translate_path(spiral, radius, 0) 156 | 157 | var master_path = new Array(); 158 | 159 | for (var j = 0; j <= sides; j++) { 160 | 161 | // Go clockwise 162 | master_path.push([ 163 | radius * Math.cos((j/sides) * 2 * Math.PI), 164 | -radius * Math.sin((j/sides) * 2 * Math.PI) 165 | ]) 166 | 167 | // Draw spiral 168 | // Note: This will re-draw the first/last spiral 169 | // I'm okay with this because it ends the pattern more nicely 170 | if (j % (sides/num_spirals) < 1) { 171 | master_path = master_path.concat( 172 | this.rotationMatrix(spiral, -(j/sides) * 2 * Math.PI) 173 | ); 174 | } 175 | } 176 | 177 | return master_path; 178 | } 179 | 180 | /** 181 | * Translate a path 182 | **/ 183 | translate_path(path, x_delta, y_delta) { 184 | return path.map(function(a){ 185 | return [ 186 | a[0] + x_delta, 187 | a[1] + y_delta 188 | ]; 189 | }); 190 | } 191 | 192 | /** 193 | * Rotate points x and y by angle theta about center point (0,0) 194 | * https://en.wikipedia.org/wiki/Rotation_matrix 195 | **/ 196 | rotationMatrix(path, theta) { 197 | return path.map(function(a){ 198 | return [ 199 | a[0] * Math.cos(theta) - a[1] * Math.sin(theta), 200 | a[0] * Math.sin(theta) + a[1] * Math.cos(theta) 201 | ] 202 | }); 203 | } 204 | 205 | /** 206 | * Calculate coordinates for the shape 207 | * 208 | * @param integer Revolutions 209 | * 210 | * @return Array Path 211 | **/ 212 | LogSpiral(a, b, revolutions, sides) { 213 | 214 | const min_x = this.env.table.x.min; 215 | const max_x = this.env.table.x.max; 216 | const min_y = this.env.table.y.min; 217 | const max_y = this.env.table.y.max; 218 | 219 | // Set initial values 220 | var x; 221 | var y; 222 | 223 | // Calculate the maximum radius 224 | var max_r = Math.min(max_x - min_x, max_y - min_y) / 2; 225 | 226 | // Initialize shape path array 227 | // This stores the x,y coordinates for each step 228 | var path = new Array(); 229 | 230 | // Set max rotations. Go over by 1 "side" so that endpoint completes a full rotation 231 | let theta_max = (revolutions + 1/sides) * (2 * Math.PI); 232 | 233 | // Set the change per increment 234 | let delta_theta = (2 * Math.PI) / sides; 235 | 236 | // Loop through each angle segment 237 | for (let theta = 0; theta < theta_max; theta += delta_theta) { 238 | 239 | // Convert polar position to rectangular coordinates 240 | x = max_r * a * Math.exp(b * theta) * Math.cos(theta); 241 | y = max_r * a * Math.exp(b * theta) * Math.sin(theta); 242 | 243 | // Add coordinates to shape array 244 | path.push([x,y]); 245 | } 246 | 247 | // Reverse path 248 | // This may be able to be accomplished more elegantly, 249 | // but Array reverse was not working well for multi-dimensional arrays 250 | var reverse_path = new Array(); 251 | for (var i=0; i < path.length; i++) { 252 | reverse_path.push(path[(path.length-1) - i]); 253 | } 254 | path = path.concat(reverse_path); 255 | 256 | return path; 257 | } 258 | 259 | } 260 | 261 | export default Frame; -------------------------------------------------------------------------------- /src/js/patterns/Gcode.js: -------------------------------------------------------------------------------- 1 | class Gcode { 2 | 3 | constructor(env) { 4 | 5 | this.key = "gcode"; 6 | 7 | this.name = "G-Code"; 8 | 9 | this.env = env; 10 | 11 | this.config = { 12 | "gcode": { 13 | "name": "G-Code", 14 | "value": null, 15 | "input": { 16 | "type": "createTextarea", 17 | "attributes" : { 18 | "rows": 11, 19 | "cols": 22, 20 | }, 21 | "value" : "G0 X336.00 Y190.00" + "\n" 22 | + "G0 X236.00 Y290.00" + "\n" 23 | + "G0 X136.00 Y190.00" + "\n" 24 | + "G0 X236.00 Y90.00" + "\n" 25 | + "G0 X336.00 Y190.00", 26 | "params" : [] 27 | } 28 | }, 29 | "reverse": { 30 | "name": "Reverse", 31 | "value": null, 32 | "input": { 33 | "type": "createCheckbox", 34 | "attributes" : [{ 35 | "type" : "checkbox", 36 | "checked" : null 37 | }], 38 | "params": [0, 1, 0], 39 | "displayValue": false 40 | } 41 | } 42 | }; 43 | 44 | this.path = []; 45 | } 46 | 47 | draw() { 48 | 49 | // Update object 50 | this.config.gcode.value = document.querySelector('#pattern-controls > div:nth-child(1) > textarea').value; 51 | 52 | // Calculate path for Circle at center 53 | let path = this.calc( 54 | this.config.gcode.value 55 | ); 56 | 57 | // Update object 58 | this.path = path; 59 | 60 | return path; 61 | } 62 | 63 | /** 64 | * Calculate coordinates 65 | **/ 66 | calc(data) { 67 | 68 | const min_x = this.env.table.x.min; 69 | const max_x = this.env.table.x.max; 70 | const min_y = this.env.table.y.min; 71 | const max_y = this.env.table.y.max; 72 | 73 | // Set initial values 74 | let x; 75 | let y; 76 | 77 | // Initialize return value - the path array 78 | // This stores the x,y coordinates for each step 79 | let path = new Array(); 80 | 81 | // Split string by line 82 | let lines = data.split("\n"); 83 | 84 | // Loop through lines and split by comma 85 | lines.forEach(function(element) { 86 | 87 | // Parse G-Code instructions 88 | // This is a first pass. There's room for improvement here. 89 | let coordinates = element.match(/^G[0,1]\s[X,Y]([0-9\.]+)\s?[X,Y]([0-9\.]+)/) 90 | 91 | if (coordinates) { 92 | x = parseFloat(coordinates[1]); 93 | y = parseFloat(coordinates[2]); 94 | 95 | // Translate to center 96 | x -= (max_x - min_x)/2; 97 | y -= (max_y - min_y)/2; 98 | 99 | // Add coordinates to shape array 100 | path.push([x,y]); 101 | } 102 | }) 103 | 104 | return path; 105 | } 106 | } 107 | 108 | export default Gcode; -------------------------------------------------------------------------------- /src/js/patterns/Gravity.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gravity Pattern 3 | * Inspired by Daniel Shiffman's "Nature of Code" 4 | */ 5 | class Gravity { 6 | 7 | constructor(env) { 8 | 9 | this.key = "gravity"; 10 | 11 | this.name = "Gravity"; 12 | 13 | this.env = env; 14 | 15 | this.config = { 16 | "steps": { 17 | "name": "Iteration Steps", 18 | "value": null, 19 | "input": { 20 | "type": "createSlider", 21 | "params" : [ 22 | 1000, 23 | 10000, 24 | 2000, 25 | 100 26 | ], 27 | "class": "slider", 28 | "displayValue": true 29 | } 30 | }, 31 | "A1m": { 32 | "name": "Attractor Mass", 33 | "value": null, 34 | "input": { 35 | "type": "createSlider", 36 | "params" : [ 37 | 0.1, 38 | 100, 39 | 50, 40 | 0.0001 41 | ], 42 | "class": "slider", 43 | "displayValue": true 44 | } 45 | }, 46 | "xp0": { 47 | "name": "X0 Position", 48 | "value": null, 49 | "input": { 50 | "type": "createSlider", 51 | "params" : [ 52 | -(env.table.x.max - env.table.x.min) / 2, 53 | (env.table.x.max - env.table.x.min) / 2, 54 | 0, 55 | 1.0 56 | ], 57 | "class": "slider", 58 | "displayValue": true 59 | } 60 | }, 61 | "yp0": { 62 | "name": "Y0 Position", 63 | "value": null, 64 | "input": { 65 | "type": "createSlider", 66 | "params" : [ 67 | -(env.table.y.max - env.table.y.min) / 2, 68 | (env.table.y.max - env.table.y.min) / 2, 69 | 0, 70 | 0.1 71 | ], 72 | "class": "slider", 73 | "displayValue": true 74 | } 75 | }, 76 | "xv0": { 77 | "name": "X0 Velocity", 78 | "value": null, 79 | "input": { 80 | "type": "createSlider", 81 | "params" : [ 82 | -20, 83 | 20, 84 | 5, 85 | 0.01 86 | ], 87 | "class": "slider", 88 | "displayValue": true 89 | } 90 | }, 91 | "yv0": { 92 | "name": "Y0 Velocity", 93 | "value": null, 94 | "input": { 95 | "type": "createSlider", 96 | "params" : [ 97 | -20, 98 | 20, 99 | 5, 100 | 0.01 101 | ], 102 | "class": "slider", 103 | "displayValue": true 104 | } 105 | } 106 | }; 107 | 108 | this.path = []; 109 | 110 | this.movers; 111 | this.attractors = []; 112 | 113 | } 114 | 115 | draw() { 116 | 117 | // Update object 118 | this.config.steps.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 119 | this.config.A1m.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 120 | this.config.xp0.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 121 | this.config.yp0.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 122 | this.config.xv0.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(5) > input').value); 123 | this.config.yv0.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(6) > input').value); 124 | 125 | // Display selected values 126 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.steps.value; 127 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.A1m.value; 128 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.xp0.value; 129 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.yp0.value; 130 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(5) > span').innerHTML = this.config.xv0.value; 131 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(6) > span').innerHTML = this.config.yv0.value; 132 | 133 | // Define the Mover object - this represents the ball 134 | // The "mass" on this appears to have no impact on the path 135 | this.mover = new Mover( 136 | 10, 137 | this.config.xp0.value, 138 | this.config.yp0.value, 139 | this.config.xv0.value, 140 | this.config.yv0.value 141 | ); 142 | 143 | // Define the Attractor objects that the Mover object is attracted toward 144 | this.attractors[0] = new Attractor(1, this.config.A1m.value, -100, 0); 145 | this.attractors[1] = new Attractor(1, this.config.A1m.value, 100, 0); 146 | 147 | // Calculate path 148 | let path = this.calc( 149 | this.config.steps.value 150 | ); 151 | 152 | return path; 153 | } 154 | 155 | /** 156 | * Calculate coordinates 157 | **/ 158 | calc(steps) { 159 | 160 | let path = new Array(); 161 | let point = new Array(); 162 | let force = new p5.Vector(0, 0); 163 | // let forces = new Array(); 164 | // let attractor_force = new p5.Vector(0, 0); 165 | let force1; 166 | let force2; 167 | 168 | // Loop through an arbitrary number of time iterations 169 | for (var i = 0; i < steps; i++) { 170 | 171 | // Sum the forces of the Attractors 172 | // This didn't work 173 | /* 174 | for (var j = 0; j < this.attractors.length; j++) { 175 | attractor_force = this.attractors[j].calculateAttraction(this.mover, 5, 20); 176 | force = p5.Vector.add(force, attractor_force); 177 | } 178 | //*/ 179 | force1 = this.attractors[0].calculateAttraction(this.mover, 5, 20); 180 | force2 = this.attractors[1].calculateAttraction(this.mover, 5, 20); 181 | force = p5.Vector.add(force1, force2); 182 | 183 | // Apply the force to the Mover 184 | this.mover.applyForce(force); 185 | 186 | // Update the position, velocity and acceleration of the mover 187 | this.mover.update(); 188 | 189 | // Save the Mover position to a Path position 190 | point = [ 191 | this.mover.position.x, 192 | this.mover.position.y 193 | ]; 194 | 195 | // Add the position to the Path if it is within the bounds of the table 196 | if ((point[0] > -(this.env.table.x.max - this.env.table.x.min)/2 && point[0] < (this.env.table.x.max - this.env.table.x.min)/2) 197 | && (point[1] > -(this.env.table.y.max - this.env.table.y.min)/2 && point[1] < (this.env.table.y.max - this.env.table.y.min)/2)) { 198 | path.push(point); 199 | } 200 | } 201 | 202 | return path; 203 | } 204 | } 205 | 206 | /* 207 | * Attractor Class 208 | * Credit to Daniel Shiffman (Nature of Code) 209 | * Reference Sketch: https://editor.p5js.org/jcponce/sketches/OTt5ZZqT9 210 | */ 211 | class Attractor { 212 | 213 | constructor(G, m, x, y) { 214 | this.position = new p5.Vector(x,y); 215 | this.mass = m; 216 | this.G = G; 217 | } 218 | 219 | calculateAttraction(mover, min_distance = 5, max_distance = 20) { 220 | 221 | // Calculate direction of force 222 | let force = p5.Vector.sub(this.position, mover.position); 223 | 224 | // Distance between objects 225 | let distance = force.mag(); 226 | 227 | // Limiting the distance to eliminate "extreme" results for very close or very far objects 228 | distance = this.#constrain(distance, min_distance, max_distance); 229 | 230 | // Normalize vector (distance doesn't matter here, we just want this vector for direction) 231 | force.normalize(); 232 | 233 | // Calculate gravitional force magnitude 234 | let strength = (this.G * this.mass * mover.mass) / (distance * distance); 235 | 236 | // Get force vector --> magnitude * direction 237 | force.mult(strength); 238 | 239 | return force; 240 | } 241 | 242 | #constrain(value, min, max) { 243 | if (value < min) { 244 | return min; 245 | } else if (value > max) { 246 | return max; 247 | } else { 248 | return value; 249 | } 250 | } 251 | } 252 | 253 | /* 254 | * Mover Class 255 | * Credit to Daniel Shiffman (Nature of Code) 256 | * Reference Sketch: https://editor.p5js.org/jcponce/sketches/OTt5ZZqT9 257 | */ 258 | class Mover { 259 | 260 | constructor(m, x, y, vx, vy) { 261 | this.mass = m; 262 | this.position = new p5.Vector(x, y); 263 | this.velocity = new p5.Vector(vx, vy); 264 | this.acceleration = new p5.Vector(0, 0); 265 | } 266 | 267 | applyForce(force) { 268 | let acceleration = p5.Vector.div(force, this.mass); 269 | this.acceleration.add(acceleration); 270 | } 271 | 272 | update() { 273 | this.velocity.add(this.acceleration); 274 | this.position.add(this.velocity); 275 | this.acceleration.mult(0); 276 | } 277 | } 278 | 279 | export default Gravity; -------------------------------------------------------------------------------- /src/js/patterns/Heart.js: -------------------------------------------------------------------------------- 1 | // Heart Curve 2 | // http://mathworld.wolfram.com/HeartCurve.html 3 | class Heart { 4 | 5 | constructor() { 6 | 7 | this.key = "heart"; 8 | 9 | this.name = "Heart"; 10 | 11 | // Define the parametric equations using text inputs 12 | this.config = { 13 | "a": { 14 | "name": "X cof. a", 15 | "value": null, 16 | "input": { 17 | "type": "createSlider", 18 | "params" : [ 19 | 1, 20 | 20, 21 | 16, 22 | 1 23 | ], 24 | "class": "slider", 25 | "displayValue": true 26 | } 27 | }, 28 | "b": { 29 | "name": "Y cof. b", 30 | "value": null, 31 | "input": { 32 | "type": "createSlider", 33 | "params" : [ 34 | 1, 35 | 20, 36 | 13, 37 | 1 38 | ], 39 | "class": "slider", 40 | "displayValue": true 41 | } 42 | }, 43 | "c": { 44 | "name": "Y cof. c", 45 | "value": null, 46 | "input": { 47 | "type": "createSlider", 48 | "params" : [ 49 | 1, 50 | 20, 51 | 5, 52 | 1 53 | ], 54 | "class": "slider", 55 | "displayValue": true 56 | } 57 | }, 58 | "d": { 59 | "name": "Y cof. d", 60 | "value": null, 61 | "input": { 62 | "type": "createSlider", 63 | "params" : [ 64 | 1, 65 | 20, 66 | 2, 67 | 1 68 | ], 69 | "class": "slider", 70 | "displayValue": true 71 | } 72 | }, 73 | "e": { 74 | "name": "Y cof. e", 75 | "value": null, 76 | "input": { 77 | "type": "createSlider", 78 | "params" : [ 79 | 1, 80 | 20, 81 | 1, 82 | 1 83 | ], 84 | "class": "slider", 85 | "displayValue": true 86 | } 87 | }, 88 | "scale": { 89 | "name": "scale", 90 | "value": null, 91 | "input": { 92 | "type": "createSlider", 93 | "params" : [ 94 | 1, 95 | 20, 96 | 10, 97 | 0.2 98 | ], 99 | "class": "slider", 100 | "displayValue": true 101 | } 102 | }, 103 | "shrink": { 104 | "name": "shrink", 105 | "value": null, 106 | "input": { 107 | "type": "createSlider", 108 | "params" : [ 109 | 0.0002, 110 | 0.0020, 111 | 0.0003, 112 | 0.0001 113 | ], 114 | "class": "slider", 115 | "displayValue": true 116 | } 117 | }, 118 | "twist": { 119 | "name": "Twist", 120 | "value": null, 121 | "input": { 122 | "type": "createSlider", 123 | "params" : [ 124 | -1, 125 | 1, 126 | 0, 127 | 0.01 128 | ], 129 | "class": "slider", 130 | "displayValue": true 131 | } 132 | }, 133 | "reverse": { 134 | "name": "Reverse", 135 | "value": null, 136 | "input": { 137 | "type": "createCheckbox", 138 | "attributes" : [{ 139 | "type" : "checkbox", 140 | "checked" : null 141 | }], 142 | "params": [0, 1, 0], 143 | "displayValue": false 144 | } 145 | } 146 | }; 147 | 148 | this.path = []; 149 | } 150 | 151 | draw() { 152 | 153 | // Update object 154 | 155 | // Read in selected value(s) 156 | this.config.a.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 157 | this.config.b.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 158 | this.config.c.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 159 | this.config.d.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 160 | this.config.e.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(5) > input').value); 161 | this.config.scale.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(6) > input').value); 162 | this.config.shrink.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(7) > input').value); 163 | this.config.twist.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(8) > input').value); 164 | 165 | // Display selected value(s) 166 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.a.value; 167 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.b.value; 168 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.c.value; 169 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.d.value; 170 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(5) > span').innerHTML = this.config.e.value; 171 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(6) > span').innerHTML = this.config.scale.value.toFixed(1); 172 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(7) > span').innerHTML = this.config.shrink.value.toFixed(4); 173 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(8) > span').innerHTML = this.config.twist.value.toFixed(2); 174 | 175 | let path = this.calc( 176 | this.config.a.value, 177 | this.config.b.value, 178 | this.config.c.value, 179 | this.config.d.value, 180 | this.config.e.value, 181 | this.config.scale.value, 182 | this.config.shrink.value, 183 | this.config.twist.value 184 | ); 185 | 186 | // Update object 187 | this.path = path; 188 | 189 | return path; 190 | } 191 | 192 | /** 193 | * Calculate coordinates for the shape 194 | * 195 | * @return Array Path 196 | **/ 197 | calc(a,b,c,d,e,scale,shrink,twist) { 198 | 199 | // Set initial values 200 | var x; 201 | var y; 202 | var r = 1.0; 203 | var t = 0.0; 204 | 205 | // Initialize return value - the path array 206 | // This stores the x,y coordinates for each step 207 | var path = new Array(); 208 | 209 | // Iteration counter. 210 | var step = 0; 211 | 212 | // The number of "sides" to the circle. 213 | let steps_per_revolution = 80; 214 | 215 | // Loop through one revolution 216 | // while (t < 100 * (2 * Math.PI)) { 217 | while (r > 0) { 218 | 219 | // Rotational Angle (steps per rotation in the denominator) 220 | t = (step/steps_per_revolution) * (2 * Math.PI); 221 | 222 | // Run the parametric equations 223 | x = r * scale * (a * Math.pow(Math.sin(t), 3)); 224 | y = r * scale * (b * Math.cos(t) - c * Math.cos(2 * t) - d * Math.cos(3 * t) - e * Math.cos(4 * t)); 225 | 226 | // Add coordinates to shape array 227 | path.push(this.rotationMatrix(x, y, twist * t/steps_per_revolution)); 228 | 229 | r -= shrink; 230 | 231 | // Increment iteration counter 232 | step++; 233 | } 234 | 235 | return path; 236 | } 237 | 238 | /** 239 | * Rotate points x and y by angle theta about center point (0,0) 240 | * https://en.wikipedia.org/wiki/Rotation_matrix 241 | **/ 242 | rotationMatrix(x, y, theta) { 243 | return [ 244 | x * Math.cos(theta) - y * Math.sin(theta), 245 | x * Math.sin(theta) + y * Math.cos(theta) 246 | ]; 247 | } 248 | 249 | } 250 | 251 | export default Heart; -------------------------------------------------------------------------------- /src/js/patterns/Lindenmayer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Space Filling Curves 3 | * 4 | * Modified from https://p5js.org/examples/simulate-l-systems.html on 6/15/2019 5 | * 6 | * https://en.wikipedia.org/wiki/Space-filling_curve 7 | * https://fedimser.github.io/l-systems.html 8 | * 9 | */ 10 | class Lindenmayer { 11 | 12 | constructor(env) { 13 | 14 | this.key = "lindenmayer"; 15 | 16 | this.name = "Space Filling Curves"; 17 | 18 | this.env = env; 19 | 20 | // Hilbert Curve 21 | // https://en.wikipedia.org/wiki/Hilbert_curve#Representation_as_Lindenmayer_system 22 | this.hilbert_curve = { 23 | "l_system": { 24 | "axiom": "A", 25 | "rules": [ 26 | ["A", "-BF+AFA+FB-"], 27 | ["B", "+AF-BFB-FA+"] 28 | ] 29 | }, 30 | "draw": { 31 | "length": env.ball.diameter, 32 | "angle": 90 33 | } 34 | } 35 | 36 | // Gosper Curve 37 | // https://en.wikipedia.org/wiki/Gosper_curve 38 | // "F"-based rules found at https://gist.github.com/nitaku/6521802 39 | this.gosper_curve = { 40 | "l_system": { 41 | "axiom": "A", 42 | "rules": [ 43 | ["A", "A+BF++BF-FA--FAFA-BF+"], 44 | ["B", "-FA+BFBF++BF+FA--FA-B"] 45 | ] 46 | }, 47 | "draw": { 48 | "length": env.ball.diameter, 49 | "angle": 60 50 | } 51 | } 52 | 53 | // Sierpinkski Arrowhead 54 | this.sierpinski_arrowhead = { 55 | "l_system": { 56 | "axiom": "AF", 57 | "rules": [ 58 | ["A", "BF+AF+B"], 59 | ["B", "AF-BF-A"] 60 | ] 61 | }, 62 | "draw": { 63 | "length": env.ball.diameter, 64 | "angle": 60 65 | } 66 | } 67 | 68 | // Set the curve to draw 69 | this.curve = this.sierpinski_arrowhead; 70 | 71 | this.config = { 72 | "curve": { 73 | "name": "Curve", 74 | "value": null, 75 | "input": { 76 | "type": "createSelect", 77 | "options": { 78 | "hilbert_curve": "Hilbert Curve", 79 | "gosper_curve": "Gosper Curve", 80 | "sierpinski_arrowhead": "Sierpinski Arrowhead" 81 | } 82 | } 83 | }, 84 | "iterations": { 85 | "name": "Iterations", 86 | "value": null, 87 | "input": { 88 | "type": "createSlider", 89 | "params": [ 90 | 1, 91 | 7, 92 | 3, 93 | 1 94 | ], 95 | "class": "slider", 96 | "displayValue": true 97 | } 98 | }, 99 | "length": { 100 | "name": "Line Length", 101 | "value": null, 102 | "input": { 103 | "type": "createSlider", 104 | "params": [ 105 | 1, 106 | 50, 107 | 10, 108 | 0.1 109 | ], 110 | "class": "slider", 111 | "displayValue": true 112 | } 113 | }, 114 | "rotate": { 115 | "name": "Rotate", 116 | "value": null, 117 | "input": { 118 | "type": "createSlider", 119 | "params": [ 120 | 0, 121 | 360, 122 | 0, 123 | 1 124 | ], 125 | "class": "slider", 126 | "displayValue": true 127 | } 128 | }, 129 | "reverse": { 130 | "name": "Reverse", 131 | "value": null, 132 | "input": { 133 | "type": "createCheckbox", 134 | "attributes" : [{ 135 | "type" : "checkbox", 136 | "checked" : null 137 | }], 138 | "params": [0, 1, 0], 139 | "displayValue": false 140 | } 141 | } 142 | } 143 | 144 | this.path = []; 145 | } 146 | 147 | 148 | /** 149 | * Draw path - Use class's "calc" method to convert inputs to a draw path 150 | */ 151 | draw() { 152 | 153 | // Read in selected value(s) 154 | 155 | var curve_type = document.querySelector('#pattern-controls > div:nth-child(1) > select').value; 156 | this.config.curve.value = curve_type; 157 | this.curve = this[curve_type]; 158 | 159 | this.config.iterations.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 160 | this.curve.iterations = parseInt(this.config.iterations.value); 161 | this.config.length.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 162 | this.config.rotate.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 163 | 164 | // Display selected value(s) 165 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.iterations.value; 166 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.length.value.toFixed(1) + " " + this.env.table.units; 167 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.rotate.value + "°"; 168 | 169 | let lindenmayer_string = this.curve.l_system.axiom; 170 | for (let i = 0; i < this.curve.iterations; i++) { 171 | lindenmayer_string = this.compose_lindenmayer_string(lindenmayer_string); 172 | } 173 | 174 | // Log Lindemayer System string 175 | // console.log(lindenmayer_string); 176 | 177 | // Calculate path 178 | let path = this.calc(lindenmayer_string, this.config.length.value); 179 | 180 | // Log Path coordinates 181 | // console.log(path); 182 | 183 | // Update object 184 | this.path = path; 185 | 186 | return path; 187 | } 188 | 189 | /** 190 | * Calculate coordinates for the shape 191 | * 192 | * @param integer Revolutions 193 | * 194 | * @return Array Path 195 | **/ 196 | calc(lindenmayer_string, segment_length) { 197 | 198 | // Initialize shape path array 199 | // This stores the x,y coordinates for each step 200 | var path = new Array(); 201 | 202 | var pos = 0; 203 | 204 | let x = 0; 205 | let y = 0; 206 | 207 | var current_angle = 0; 208 | 209 | while (pos < lindenmayer_string.length-1) { 210 | 211 | if (lindenmayer_string[pos] == 'F') { 212 | 213 | // Draw forward 214 | 215 | // polar to cartesian based on step and current_angle: 216 | let x1 = x + segment_length * Math.cos(this.#radians(current_angle)); 217 | let y1 = y + segment_length * Math.sin(this.#radians(current_angle)); 218 | 219 | path.push([x1, y1]); 220 | 221 | // update the turtle's position: 222 | x = x1; 223 | y = y1; 224 | 225 | } else if (lindenmayer_string[pos] == '+') { 226 | 227 | // Turn left 228 | current_angle += this.curve.draw.angle; 229 | 230 | } else if (lindenmayer_string[pos] == '-') { 231 | 232 | // Turn right 233 | current_angle -= this.curve.draw.angle; 234 | } 235 | 236 | pos++; 237 | } 238 | 239 | // Translate path to center of drawing area 240 | path = this.translate_to_center(path); 241 | 242 | // Rotate path around the center of drawing area 243 | if (this.config.rotate.value > 0) { 244 | path = this.rotate_around_center(path); 245 | path = this.translate_to_center(path); 246 | } 247 | 248 | return path; 249 | } 250 | 251 | compose_lindenmayer_string(s) { 252 | 253 | // Start a blank output string 254 | let outputstring = ''; 255 | 256 | // Iterate through the Lindenmayer rules looking for symbol matches 257 | for (let i = 0; i < s.length; i++) { 258 | 259 | let ismatch = 0; 260 | 261 | for (let j = 0; j < this.curve.l_system.rules.length; j++) { 262 | 263 | if (s[i] == this.curve.l_system.rules[j][0]) { 264 | 265 | // Write substitution 266 | outputstring += this.curve.l_system.rules[j][1]; 267 | 268 | // We have a match, so don't copy over symbol 269 | ismatch = 1; 270 | 271 | break; 272 | } 273 | } 274 | 275 | // If nothing matches, just copy the symbol over. 276 | if (ismatch == 0) { 277 | outputstring+= s[i]; 278 | } 279 | } 280 | 281 | return outputstring; 282 | } 283 | 284 | translate_to_center(path) { 285 | 286 | // Define function to extract column from multidimensional array 287 | const arrayColumn = (arr, n) => arr.map(a => a[n]); 288 | 289 | // Get X and Y coordinates as an 1-dimensional array 290 | var x_coordinates = arrayColumn(path, 0); 291 | var x_min = Math.min(...x_coordinates); 292 | var x_max = Math.max(...x_coordinates); 293 | 294 | var y_coordinates = arrayColumn(path, 1); 295 | var y_min = Math.min(...y_coordinates); 296 | var y_max = Math.max(...y_coordinates); 297 | 298 | return path.map(function(a){ 299 | return [ 300 | (a[0] - x_min - ((x_max - x_min)/2)), 301 | (a[1] - y_min - ((y_max - y_min)/2)), 302 | ]; 303 | }); 304 | } 305 | 306 | rotate_around_center(path) { 307 | var theta = this.#radians(this.config.rotate.value); 308 | return path.map(function(a){ 309 | var x = a[0]; 310 | var y = a[1]; 311 | return [ 312 | x * Math.cos(theta) - y * Math.sin(theta), 313 | x * Math.sin(theta) + y * Math.cos(theta) 314 | ]; 315 | }); 316 | } 317 | 318 | #radians(degrees) { 319 | return degrees * (Math.PI / 180); 320 | } 321 | } 322 | 323 | export default Lindenmayer; -------------------------------------------------------------------------------- /src/js/patterns/Lissajous.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Lissajous Curve 3 | * https://en.wikipedia.org/wiki/Lissajous_curve 4 | */ 5 | class Lissajous { 6 | 7 | constructor(env) { 8 | 9 | this.key = "lissajous"; 10 | 11 | this.name = "Lissajous Curve"; 12 | 13 | this.path_sampling_optimization = 2; 14 | 15 | // Define the parametric equations using text inputs 16 | this.config = { 17 | "A": { 18 | "name": "X Amplitude", 19 | "value": null, 20 | "input": { 21 | "type": "createSlider", 22 | "params" : [ 23 | 0, 24 | (env.table.x.max - env.table.x.min)/2, 25 | (env.table.x.max - env.table.x.min)/2, 26 | 1 27 | ], 28 | "class": "slider", 29 | "displayValue": true 30 | } 31 | }, 32 | "a1": { 33 | "name": "X Frequency (a)", 34 | "value": null, 35 | "input": { 36 | "type": "createSlider", 37 | "params" : [ 38 | 1, 39 | 100, 40 | 8, 41 | 1 42 | ], 43 | "class": "slider", 44 | "displayValue": true 45 | } 46 | }, 47 | "B": { 48 | "name": "Y Amplitude", 49 | "value": null, 50 | "input": { 51 | "type": "createSlider", 52 | "params" : [ 53 | 0, 54 | (env.table.y.max - env.table.y.min)/2, 55 | (env.table.y.max - env.table.y.min)/2, 56 | 1 57 | ], 58 | "class": "slider", 59 | "displayValue": true 60 | } 61 | }, 62 | "b1": { 63 | "name": "Y Frequency (b)", 64 | "value": null, 65 | "input": { 66 | "type": "createSlider", 67 | "params" : [ 68 | 1, 69 | 100, 70 | 9, 71 | 1 72 | ], 73 | "class": "slider", 74 | "displayValue": true 75 | } 76 | }, 77 | "phase": { 78 | "name": "Phase Offset", 79 | "value": null, 80 | "input": { 81 | "type": "createSlider", 82 | "params" : [ 83 | -Math.PI, 84 | Math.PI, 85 | 0, 86 | Math.PI / 32 87 | ], 88 | "class": "slider", 89 | "displayValue": true 90 | } 91 | }, 92 | "rotation": { 93 | "name": "Rotation", 94 | "value": null, 95 | "input": { 96 | "type": "createSlider", 97 | "params" : [ 98 | -180, 99 | 180, 100 | 0, 101 | 1 102 | ], 103 | "class": "slider", 104 | "displayValue": true 105 | } 106 | } 107 | }; 108 | 109 | this.path = []; 110 | } 111 | 112 | draw() { 113 | 114 | // Update object 115 | this.config.A.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 116 | this.config.a1.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 117 | this.config.B.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 118 | this.config.b1.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 119 | this.config.phase.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(5) > input').value); 120 | this.config.rotation.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(6) > input').value); 121 | 122 | // Display selected value(s) 123 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.A.value; 124 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.a1.value; 125 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.B.value; 126 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.b1.value; 127 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(5) > span').innerHTML = Math.round(parseFloat(this.config.phase.value) * (180/Math.PI), 2) + "°"; 128 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(6) > span').innerHTML = this.config.rotation.value + "°"; 129 | 130 | let path = this.calc( 131 | this.config.A.value, 132 | this.config.a1.value, 133 | this.config.B.value, 134 | this.config.b1.value, 135 | this.config.phase.value, 136 | this.config.rotation.value 137 | ); 138 | 139 | // Update object 140 | this.path = path; 141 | 142 | return path; 143 | } 144 | 145 | /** 146 | * Calculate coordinates for the shape 147 | * 148 | * @return Array Path 149 | **/ 150 | calc(A, a1, B, b1, phase, rotation) { 151 | 152 | // Set initial values 153 | var x; 154 | var y; 155 | var theta = 0.0; 156 | 157 | // Initialize return value - the path array 158 | // This stores the x,y coordinates for each step 159 | var path = new Array(); 160 | 161 | // Iteration counter. 162 | var step = 0; 163 | 164 | // Calculate the full period of the Lissajous curve 165 | // https://stackoverflow.com/questions/9620324/how-to-calculate-the-period-of-a-lissajous-curve 166 | let lissajous_period = 2 * Math.PI / this.greatest_common_divisor(a1, b1); 167 | 168 | // Set the steps per revolution. Oversample and small distances can be optimized out afterward 169 | let steps_per_revolution = 5000; 170 | 171 | // Loop through one revolution 172 | while (theta < lissajous_period) { 173 | 174 | // Rotational Angle (steps per rotation in the denominator) 175 | theta = (step/steps_per_revolution) * lissajous_period; 176 | 177 | // Run the parametric equations 178 | x = A * Math.cos(a1*theta + phase); 179 | y = B * Math.cos(b1*theta); 180 | 181 | // Add coordinates to shape array 182 | path.push([x,y]); 183 | 184 | // Increment iteration counter 185 | step++; 186 | } 187 | 188 | // Rotate 189 | if (rotation != 0) { 190 | path = path.map(function(element) { 191 | return this.rotationMatrix(element[0], element[1], rotation * (Math.PI/180)) 192 | }, this); 193 | } 194 | 195 | return path; 196 | } 197 | 198 | /** 199 | * Calculate the Greatest Common Divisor (or Highest Common Factor) of 2 numbers 200 | * 201 | * https://en.wikipedia.org/wiki/Greatest_common_divisor 202 | * https://www.geeksforgeeks.org/c-program-find-gcd-hcf-two-numbers/ 203 | */ 204 | greatest_common_divisor(a, b) { 205 | if (b == 0) { 206 | return a; 207 | } 208 | return this.greatest_common_divisor(b, a % b); 209 | } 210 | 211 | /** 212 | * Rotate points x and y by angle theta about center point (0,0) 213 | * https://en.wikipedia.org/wiki/Rotation_matrix 214 | **/ 215 | rotationMatrix(x, y, theta) { 216 | return [ 217 | x * Math.cos(theta) - y * Math.sin(theta), 218 | x * Math.sin(theta) + y * Math.cos(theta) 219 | ]; 220 | } 221 | } 222 | 223 | export default Lissajous; -------------------------------------------------------------------------------- /src/js/patterns/LogarithmicSpiral.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Logarithmic Spiral 3 | * https://en.wikipedia.org/wiki/Logarithmic_spiral 4 | * https://mathworld.wolfram.com/LogarithmicSpiral.html 5 | */ 6 | class LogarithmicSpiral { 7 | 8 | constructor(env) { 9 | 10 | this.key = "logspiral"; 11 | 12 | this.name = "Spiral (Logarithmic)"; 13 | 14 | this.env = env; 15 | 16 | this.config = { 17 | "a": { 18 | "name": "a", 19 | "value": null, 20 | "input": { 21 | "type": "createSlider", 22 | "params" : [ 23 | -1.0, 24 | 1.0, 25 | 1.0, 26 | 0.1 27 | ], 28 | "class": "slider", 29 | "displayValue": true 30 | } 31 | }, 32 | "b": { 33 | "name": "b", 34 | "value": null, 35 | "input": { 36 | "type": "createSlider", 37 | "params" : [ 38 | -1.0, 39 | 0.0, 40 | -0.25, 41 | 0.01 42 | ], 43 | "class": "slider", 44 | "displayValue": true 45 | } 46 | }, 47 | "revolutions": { 48 | "name": "Revolutions", 49 | "value": null, 50 | "input": { 51 | "type": "createSlider", 52 | "params" : [ 53 | 1, 54 | 60, 55 | 4, 56 | 1 57 | ], 58 | "class": "slider", 59 | "displayValue": true 60 | } 61 | }, 62 | "rotate": { 63 | "name": "Rotate", 64 | "value": null, 65 | "input": { 66 | "type": "createSlider", 67 | "params": [ 68 | 0, 69 | 360, 70 | 0, 71 | 1 72 | ], 73 | "class": "slider", 74 | "displayValue": true 75 | } 76 | }, 77 | "reverse": { 78 | "name": "Reverse", 79 | "value": null, 80 | "input": { 81 | "type": "createCheckbox", 82 | "attributes" : [{ 83 | "type" : "checkbox", 84 | "checked" : null 85 | }], 86 | "params": [0, 1, 0], 87 | "displayValue": false 88 | } 89 | } 90 | }; 91 | 92 | this.path = []; 93 | } 94 | 95 | 96 | /** 97 | * Draw path - Use class's "calc" method to convert inputs to a draw path 98 | */ 99 | draw() { 100 | 101 | // Read in selected value(s) 102 | this.config.a.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 103 | this.config.b.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 104 | this.config.revolutions.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 105 | this.config.rotate.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 106 | 107 | // Display selected value(s) 108 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.a.value.toFixed(2) + " * rmax"; 109 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.b.value.toFixed(2); 110 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.revolutions.value; 111 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.rotate.value + "°"; 112 | 113 | // Calculate path 114 | let path = this.calc( 115 | this.config.a.value, 116 | this.config.b.value, 117 | this.config.revolutions.value, 118 | 60 119 | ); 120 | 121 | // Update object 122 | this.path = path; 123 | 124 | // Rotate path around the center of drawing area 125 | if (this.config.rotate.value > 0) { 126 | path = this.rotate_around_center(path); 127 | } 128 | 129 | return path; 130 | } 131 | 132 | /** 133 | * Calculate coordinates for the shape 134 | * 135 | * @param integer Revolutions 136 | * 137 | * @return Array Path 138 | **/ 139 | calc(a, b, revolutions, sides) { 140 | 141 | const min_x = this.env.table.x.min; 142 | const max_x = this.env.table.x.max; 143 | const min_y = this.env.table.y.min; 144 | const max_y = this.env.table.y.max; 145 | 146 | // Set initial values 147 | var x; 148 | var y; 149 | 150 | // Calculate the maximum radius 151 | var max_r = Math.min(max_x - min_x, max_y - min_y) / 2; 152 | 153 | // Initialize shape path array 154 | // This stores the x,y coordinates for each step 155 | var path = new Array(); 156 | 157 | // Set max rotations. Go over by 1 "side" so that endpoint completes a full rotation 158 | let theta_max = (revolutions + 1/sides) * (2 * Math.PI); 159 | 160 | // Set the change per increment 161 | let delta_theta = (2 * Math.PI) / sides; 162 | 163 | // Loop through each angle segment 164 | for (let theta = 0; theta < theta_max; theta += delta_theta) { 165 | 166 | // Convert polar position to rectangular coordinates 167 | x = max_r * a * Math.exp(b * theta) * Math.cos(theta); 168 | y = max_r * a * Math.exp(b * theta) * Math.sin(theta); 169 | 170 | // Add coordinates to shape array 171 | path.push([x,y]); 172 | } 173 | 174 | return path; 175 | } 176 | 177 | rotate_around_center(path) { 178 | let theta = this.config.rotate.value * (Math.PI/180); 179 | return path.map(function(a){ 180 | var x = a[0]; 181 | var y = a[1]; 182 | return [ 183 | x * Math.cos(theta) - y * Math.sin(theta), 184 | x * Math.sin(theta) + y * Math.cos(theta) 185 | ]; 186 | }); 187 | } 188 | } 189 | 190 | export default LogarithmicSpiral; -------------------------------------------------------------------------------- /src/js/patterns/Parametric.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://en.wikipedia.org/wiki/Parametric_equation 3 | */ 4 | class Parametric { 5 | 6 | constructor() { 7 | 8 | this.key = "parametric"; 9 | 10 | this.name = "Parametric"; 11 | 12 | // Butterfly Curve 13 | // https://en.wikipedia.org/wiki/Butterfly_curve_(transcendental) 14 | let butterfly = { 15 | "x": "40 * Math.sin(t) * (Math.pow(Math.E, Math.cos(t)) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5))", 16 | "y": "40 * Math.cos(t) * (Math.pow(Math.E, Math.cos(t)) - 2 * Math.cos(4*t) - Math.pow(Math.sin(t/12), 5))" 17 | }; 18 | 19 | // Define the parametric equations using text inputs 20 | this.config = { 21 | "x": { 22 | "name": "X", 23 | "value": null, 24 | "input": { 25 | "type": "createInput", 26 | "params" : [ 27 | butterfly.x 28 | ] 29 | } 30 | }, 31 | "y": { 32 | "name": "Y", 33 | "value": null, 34 | "input": { 35 | "type": "createInput", 36 | "params" : [ 37 | butterfly.y 38 | ] 39 | } 40 | } 41 | }; 42 | 43 | this.path = []; 44 | } 45 | 46 | draw() { 47 | 48 | // Update object 49 | this.config.x.value = document.querySelector('#pattern-controls > div:nth-child(1) > input').value; 50 | this.config.y.value = document.querySelector('#pattern-controls > div:nth-child(2) > input').value; 51 | 52 | let path = this.calc( 53 | this.config.x.value, 54 | this.config.y.value 55 | ); 56 | 57 | // Update object 58 | this.path = path; 59 | 60 | return path; 61 | } 62 | 63 | /** 64 | * Calculate coordinates for the shape 65 | * 66 | * @return Array Path 67 | **/ 68 | calc(x_equation, y_equation) { 69 | 70 | // Set initial values 71 | var x; 72 | var y; 73 | var t = 0.0; 74 | 75 | // Initialize return value - the path array 76 | // This stores the x,y coordinates for each step 77 | var path = new Array(); 78 | 79 | // Iteration counter. 80 | var step = 0; 81 | 82 | // The number of "sides" to the circle. 83 | let steps_per_revolution = 120; 84 | 85 | // Loop through one revolution 86 | while (t < (2 * Math.PI)) { 87 | 88 | // Rotational Angle (steps per rotation in the denominator) 89 | t = (step/steps_per_revolution) * (2 * Math.PI); 90 | 91 | // Run the parametric equations 92 | x = eval(x_equation); 93 | y = eval(y_equation); 94 | 95 | // Add coordinates to shape array 96 | path.push([x,y]); 97 | 98 | // Increment iteration counter 99 | step++; 100 | } 101 | 102 | return path; 103 | } 104 | } 105 | 106 | export default Parametric; -------------------------------------------------------------------------------- /src/js/patterns/Rectangle.js: -------------------------------------------------------------------------------- 1 | class Rectangle { 2 | 3 | constructor(env) { 4 | 5 | this.key = "rectangle"; 6 | 7 | this.name = "Rectangle"; 8 | 9 | this.config = { 10 | "width": { 11 | "name": "Width", 12 | "value": null, 13 | "input": { 14 | "type": "createSlider", 15 | "params" : [ 16 | 1, 17 | (env.table.x.max - env.table.x.min), 18 | (env.table.x.max - env.table.x.min) / 2, 19 | 1 20 | ], 21 | "class": "slider", 22 | "displayValue": true 23 | } 24 | }, 25 | "height": { 26 | "name": "Height", 27 | "value": null, 28 | "input": { 29 | "type": "createSlider", 30 | "params" : [ 31 | 1, 32 | (env.table.y.max - env.table.y.min), 33 | (env.table.y.max - env.table.y.min) / 2, 34 | 1 35 | ], 36 | "class": "slider", 37 | "displayValue": true 38 | } 39 | }, 40 | "reverse": { 41 | "name": "Reverse", 42 | "value": 0, 43 | "input": { 44 | "type": "createCheckbox", 45 | "attributes" : [{ 46 | "type" : "checkbox", 47 | "checked" : null 48 | }], 49 | "params": [0, 1, 0], 50 | "displayValue": false 51 | } 52 | } 53 | }; 54 | 55 | this.path = []; 56 | } 57 | 58 | draw() { 59 | 60 | // Update object 61 | this.config.width.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 62 | this.config.height.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 63 | 64 | // Display selected values 65 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.width.value; 66 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.height.value; 67 | 68 | // Calculate path for Circle at center 69 | let path = this.calc( 70 | this.config.width.value, 71 | this.config.height.value 72 | ); 73 | 74 | // Update object 75 | this.path = path; 76 | 77 | return path; 78 | } 79 | 80 | /** 81 | * Calculate coordinates for the shape 82 | * 83 | * @param float width Rectangle Width 84 | * @param float height Rectangle Height 85 | * 86 | * @return Array Path 87 | **/ 88 | calc(width, height) { 89 | return [ 90 | [- width/2, - height/2], 91 | [+ width/2, - height/2], 92 | [+ width/2, + height/2], 93 | [- width/2, + height/2], 94 | [- width/2, - height/2], 95 | ]; 96 | } 97 | } 98 | 99 | export default Rectangle; -------------------------------------------------------------------------------- /src/js/patterns/Rhodonea.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Rhodonea Curve 3 | * https://en.wikipedia.org/wiki/Rose_(mathematics) 4 | */ 5 | class Rhodonea { 6 | 7 | constructor(env) { 8 | 9 | this.key = "rhodonea"; 10 | 11 | this.name = "Rhodonea (Rose) Curve"; 12 | 13 | // this.path_sampling_optimization = 1; 14 | 15 | let max_r = Math.min((env.table.x.max - env.table.x.min), (env.table.y.max - env.table.y.min))/2; 16 | 17 | // Define the parametric equations using text inputs 18 | this.config = { 19 | "amplitude": { 20 | "name": "Amplitude", 21 | "value": null, 22 | "input": { 23 | "type": "createSlider", 24 | "params" : [ 25 | 0, 26 | max_r, 27 | max_r, 28 | 1 29 | ], 30 | "class": "slider", 31 | "displayValue": true 32 | } 33 | }, 34 | "petals": { 35 | "name": "Petal Value (k)", 36 | "value": null, 37 | "input": { 38 | "type": "createSlider", 39 | "params" : [ 40 | 0.5, 41 | 20, 42 | 5, 43 | 0.5 44 | ], 45 | "class": "slider", 46 | "displayValue": true 47 | } 48 | } 49 | }; 50 | 51 | this.path = []; 52 | } 53 | 54 | draw() { 55 | 56 | // Update object 57 | this.config.amplitude.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 58 | this.config.petals.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 59 | 60 | // Display selected value(s) 61 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.amplitude.value; 62 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.petals.value; 63 | 64 | let path = this.calc( 65 | this.config.amplitude.value, 66 | this.config.petals.value 67 | ); 68 | 69 | // Update object 70 | this.path = path; 71 | 72 | return path; 73 | } 74 | 75 | /** 76 | * Calculate coordinates for the shape 77 | * 78 | * @return Array Path 79 | **/ 80 | calc(amplitude, petals) { 81 | 82 | // Set initial values 83 | var x; 84 | var y; 85 | var theta = 0.0; 86 | 87 | // Initialize return value - the path array 88 | // This stores the x,y coordinates for each step 89 | var path = new Array(); 90 | 91 | // Iteration counter. 92 | var step = 0; 93 | 94 | // Set period of full rotation 95 | let period = 2 * Math.PI; 96 | 97 | // Set the steps per revolution. Oversample and small distances can be optimized out afterward 98 | let steps_per_revolution = 500; 99 | 100 | // Loop through one revolution 101 | while (theta < period) { 102 | 103 | // Rotational Angle (steps per rotation in the denominator) 104 | theta = (step/steps_per_revolution) * period; 105 | 106 | // Run the parametric equations 107 | x = amplitude * Math.sin(petals*theta) * Math.cos(theta); 108 | y = amplitude * Math.sin(petals*theta) * Math.sin(theta); 109 | 110 | // Add coordinates to shape array 111 | path.push([x,y]); 112 | 113 | // Increment iteration counter 114 | step++; 115 | } 116 | 117 | return path; 118 | } 119 | } 120 | 121 | export default Rhodonea; -------------------------------------------------------------------------------- /src/js/patterns/ShapeMorph.js: -------------------------------------------------------------------------------- 1 | class ShapeMorph { 2 | 3 | constructor(env) { 4 | 5 | this.key = "shapemorph"; 6 | 7 | this.name = "Shape Morph"; 8 | 9 | // Define the shape to spin 10 | 11 | // 60 works well across many divisors (shape sides) 12 | this.sides = 60; 13 | 14 | let radius = Math.min(env.table.x.max/2, env.table.y.max/2); 15 | 16 | // Define shapes 17 | this.circle = this.circleShape(this.sides, radius); 18 | this.heart = this.heartShape(this.sides, radius); 19 | this.star = this.starShape(this.sides, radius); 20 | this.square = this.squareShape(this.sides, radius); 21 | 22 | // Define the parametric equations using text inputs 23 | this.config = { 24 | "startShape": { 25 | "name": "Inside Shape", 26 | "value": "square", 27 | "input": { 28 | "type": "createSelect", 29 | "options": { 30 | "circle": "Circle", 31 | "heart": "Heart", 32 | "star": "Star", 33 | "square": "Square" 34 | } 35 | } 36 | }, 37 | "endShape": { 38 | "name": "Outside Shape", 39 | "value": "circle", 40 | "input": { 41 | "type": "createSelect", 42 | "options": { 43 | "circle": "Circle", 44 | "heart": "Heart", 45 | "star": "Star", 46 | "square": "Square" 47 | } 48 | } 49 | }, 50 | "revolutions": { 51 | "name": "Revolutions", 52 | "value": null, 53 | "input": { 54 | "type": "createSlider", 55 | "params" : [ 56 | 1, 57 | 40, 58 | 10, 59 | 1 60 | ], 61 | "class": "slider", 62 | "displayValue": true 63 | } 64 | }, 65 | "twist": { 66 | "name": "Twist", 67 | "value": null, 68 | "input": { 69 | "type": "createSlider", 70 | "params" : [ 71 | -1.0, 72 | 1.0, 73 | 0.0, 74 | 0.01 75 | ], 76 | "class": "slider", 77 | "displayValue": true 78 | } 79 | }, 80 | "completion": { 81 | "name": "Completion", 82 | "value": null, 83 | "input": { 84 | "type": "createSlider", 85 | "params" : [ 86 | 0.1, 87 | 1.0, 88 | 0.5, 89 | 0.1 90 | ], 91 | "class": "slider", 92 | "displayValue": true 93 | } 94 | }, 95 | "reverse": { 96 | "name": "Reverse", 97 | "value": null, 98 | "input": { 99 | "type": "createCheckbox", 100 | "attributes" : [{ 101 | "type" : "checkbox", 102 | "checked" : null 103 | }], 104 | "params": [0, 1, 0], 105 | "displayValue": false 106 | } 107 | } 108 | }; 109 | 110 | this.path = []; 111 | } 112 | 113 | draw() { 114 | 115 | // Update object 116 | 117 | this.config.startShape.value = document.querySelector('#pattern-controls > div:nth-child(1) > select').value; 118 | this.config.endShape.value = document.querySelector('#pattern-controls > div:nth-child(2) > select').value; 119 | 120 | this.config.revolutions.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 121 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.revolutions.value; 122 | 123 | this.config.twist.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 124 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.twist.value; 125 | 126 | this.config.completion.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(5) > input').value); 127 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(5) > span').innerHTML = (100 * this.config.completion.value) + "%"; 128 | 129 | let path = this.calc( 130 | this[this.config.startShape.value], 131 | this[this.config.endShape.value], 132 | this.sides, 133 | this.config.revolutions.value, 134 | this.config.completion.value, 135 | this.config.twist.value 136 | ); 137 | 138 | // Update object 139 | this.path = path; 140 | 141 | return path; 142 | } 143 | 144 | /** 145 | * Calculate coordinates for the shape 146 | * 147 | * @return Array Path 148 | **/ 149 | calc(base_shape, end_shape, steps_per_revolution, revolutions, completion, twist) { 150 | 151 | // Set initial values 152 | var x; 153 | var y; 154 | let current_revolution; 155 | 156 | // Initialize return value - the path array 157 | // This stores the x,y coordinates for each step 158 | var path = new Array(); 159 | 160 | // Iteration counter. 161 | var step = 0; 162 | 163 | let lerp_amount; 164 | 165 | // Theta values 166 | const max_t = revolutions * (2 * Math.PI); 167 | const min_t = (1.0 - completion) * max_t; 168 | let t = min_t; 169 | 170 | // Loop through revolutions 171 | while (t < max_t) { 172 | 173 | // Rotational Angle (steps per rotation in the denominator) 174 | t = (step/steps_per_revolution) * (2 * Math.PI) + min_t; 175 | 176 | // Calculate current rotation 177 | current_revolution = Math.floor(t/(2 * Math.PI)); 178 | 179 | // Calculate x,y coordinates 180 | // TODO: http://www.gizma.com/easing/#quint2 181 | lerp_amount = (current_revolution/revolutions); 182 | x = (t/max_t) * this.#lerp(base_shape[step % steps_per_revolution][0], end_shape[step % steps_per_revolution][0], lerp_amount); 183 | y = (t/max_t) * this.#lerp(base_shape[step % steps_per_revolution][1], end_shape[step % steps_per_revolution][1], lerp_amount); 184 | 185 | // Rotate [x,y] coordinates around [0,0] by angle theta, and then append to path 186 | path.push(this.rotationMatrix(x, y, twist * t/steps_per_revolution)); 187 | 188 | path.push([x,y]); 189 | 190 | // Increment iteration counter 191 | step++; 192 | } 193 | 194 | // Remove the last element of the path. 195 | /* 196 | I'm not sure why, but without this the last element is anchored at a point we don't want. 197 | There's probably a more elegant way to handle this in the loop above, but for now 198 | I'm going with this. 199 | */ 200 | path.pop(); 201 | 202 | return path; 203 | } 204 | 205 | /** 206 | * Rotate points x and y by angle theta about center point (0,0) 207 | * https://en.wikipedia.org/wiki/Rotation_matrix 208 | **/ 209 | rotationMatrix(x, y, theta) { 210 | return [ 211 | x * Math.cos(theta) - y * Math.sin(theta), 212 | x * Math.sin(theta) + y * Math.cos(theta) 213 | ]; 214 | } 215 | 216 | circleShape(array_size, radius) { 217 | 218 | let shape = []; 219 | 220 | for (var i = 0; i <= (array_size+0); i++) { 221 | shape.push([ 222 | radius * Math.cos((i/array_size) * 2 * Math.PI), 223 | -radius * Math.sin((i/array_size) * 2 * Math.PI) 224 | ]); 225 | } 226 | 227 | return shape; 228 | } 229 | 230 | heartShape(array_size, radius = 10) { 231 | 232 | let shape = []; 233 | 234 | let x, y, theta; 235 | 236 | for (var i = 0; i <= (array_size+0); i++) { 237 | theta = ((i/array_size) * (2 * Math.PI)) + 0.5*Math.PI; 238 | x = radius * (16 * Math.pow(Math.sin(theta), 3)); 239 | y = radius * (13 * Math.cos(theta) - 5 * Math.cos(2 * theta) - 2 * Math.cos(3 * theta) - Math.cos(4 * theta)); 240 | shape.push([x,y]); 241 | } 242 | 243 | return shape; 244 | } 245 | 246 | starShape(array_size, radius) { 247 | 248 | let shape = []; 249 | 250 | let x, y, theta, i_radius; 251 | 252 | var star_points = 5; 253 | var points_per_side = (array_size / star_points); 254 | var minimum_radius = 0.4 * radius; 255 | 256 | for (var i = 0; i <= array_size; i++) { 257 | theta = (i/array_size) * (2 * Math.PI); 258 | 259 | if (i % points_per_side == 0) { 260 | // Star point (maximum) 261 | i_radius = radius; 262 | // } else if ((i + 6) % (array_size / star_points) == 0) { 263 | } else if (i % (points_per_side/2) == 0) { 264 | // Star inner minimum 265 | i_radius = minimum_radius; 266 | } else if (i % points_per_side < (points_per_side/2)) { 267 | // Descend from point to anti-point 268 | i_radius = radius - (((i % (points_per_side/2)) / (points_per_side/2)) * (radius - minimum_radius)); 269 | } else { 270 | // Ascend from anti-point to point 271 | i_radius = (minimum_radius) + (((i % (points_per_side/2)) / (points_per_side/2)) * (radius - (minimum_radius))); 272 | } 273 | 274 | x = i_radius * Math.cos(theta); 275 | y = -i_radius * Math.sin(theta); 276 | 277 | shape.push([x,y]); 278 | } 279 | 280 | return shape; 281 | } 282 | 283 | // TODO: Re-write this to go counter-clockwise 284 | squareShape(array_size, radius) { 285 | 286 | // Construct array for Starting Shape (Square) 287 | // Middle right side to bottom right corner 288 | 289 | let i_max = array_size/8; 290 | 291 | let shape = []; 292 | 293 | for (var i = 0; i < i_max; i++) { 294 | shape.push([ 295 | radius, 296 | -((i/i_max) * radius), 297 | ]); 298 | } 299 | 300 | // bottom right corner to bottom left corner 301 | i_max = array_size/4; 302 | for (let i = 0; i < i_max; i++) { 303 | shape.push([ 304 | radius - ((i/i_max) * (2 * radius)), 305 | -radius 306 | ]); 307 | } 308 | 309 | // bottom left corner to top left corner 310 | i_max = array_size/4; 311 | for (let i = 0; i < i_max; i++) { 312 | shape.push([ 313 | -radius, 314 | -radius + ((i/i_max) * (2 * radius)), 315 | ]); 316 | } 317 | 318 | // top left corner to top right corner 319 | i_max = array_size/4; 320 | for (let i = 0; i < i_max; i++) { 321 | shape.push([ 322 | -radius + ((i/i_max) * (2 * radius)), 323 | radius 324 | ]); 325 | } 326 | 327 | // top right corner to middle right side 328 | i_max = array_size/8; 329 | for (let i = 0; i < i_max; i++) { 330 | shape.push([ 331 | radius, 332 | radius - ((i/i_max) * radius), 333 | ]); 334 | } 335 | 336 | shape.push([radius,0]); 337 | 338 | return shape; 339 | } 340 | 341 | #lerp(start, end, amount) { 342 | return start + (end - start) * amount; 343 | } 344 | } 345 | 346 | export default ShapeMorph; -------------------------------------------------------------------------------- /src/js/patterns/ShapeSpin.js: -------------------------------------------------------------------------------- 1 | class ShapeSpin { 2 | 3 | constructor() { 4 | 5 | this.key = "shapespin"; 6 | 7 | this.name = "Shape Spin"; 8 | 9 | // Define the shape to spin 10 | 11 | // Shape 1 12 | //* 13 | this.base_shape = [ 14 | [-160, 0], 15 | [80, -80], 16 | [160, 0], 17 | [80, 80], 18 | [-160, 0] 19 | ]; 20 | //*/ 21 | 22 | // Square 23 | /* 24 | this.base_shape = [ 25 | [30, 30], 26 | [60, 30], 27 | [60, 60], 28 | [30, 60], 29 | [30, 30] 30 | ]; 31 | //*/ 32 | 33 | // Ellipse 34 | /* 35 | var shape_sides = 32; 36 | this.base_shape = []; 37 | for (var j = 0; j <= (shape_sides+0); j++) { 38 | this.base_shape.push([ 39 | 0.3 * Math.min(max_x/2, max_y/2) + 0.7 * Math.min(max_x/2, max_y/2) * Math.cos((j/shape_sides) * 2 * Math.PI), 40 | 0.3 * Math.min(max_x/2, max_y/2) * Math.sin((j/shape_sides) * 2 * Math.PI) 41 | ]); 42 | } 43 | //*/ 44 | 45 | // Define the parametric equations using text inputs 46 | this.config = { 47 | "steps": { 48 | "name": "Steps", 49 | "value": null, 50 | "input": { 51 | "type": "createSlider", 52 | "params" : [ 53 | 1, 54 | 120, 55 | 30, 56 | 1 57 | ], 58 | "class": "slider", 59 | "displayValue": true 60 | } 61 | }, 62 | }; 63 | 64 | this.path = []; 65 | } 66 | 67 | draw() { 68 | 69 | // Update object 70 | this.config.steps.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 71 | 72 | // Display selected values 73 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.steps.value; 74 | 75 | let path = this.calc( 76 | this.base_shape, 77 | this.config.steps.value 78 | ); 79 | 80 | // Update object 81 | this.path = path; 82 | 83 | return path; 84 | } 85 | 86 | /** 87 | * Calculate coordinates for the shape 88 | * 89 | * @return Array Path 90 | **/ 91 | calc(base_shape, steps_per_revolution) { 92 | 93 | // Set initial values 94 | var t = 0.0; 95 | 96 | // Initialize return value - the path array 97 | // This stores the x,y coordinates for each step 98 | var path = new Array(); 99 | 100 | // Iteration counter. 101 | var step = 0; 102 | 103 | const max_t = (2 * Math.PI); 104 | 105 | // Loop through one revolution 106 | while (t < max_t) { 107 | 108 | // Rotational Angle (steps per rotation in the denominator) 109 | t = (step/steps_per_revolution) * (2 * Math.PI); 110 | 111 | // Loop through base shape 112 | base_shape.forEach(function(element) { 113 | 114 | // Rotate [x,y] coordinates around [0,0] by angle theta, and then append to path 115 | path.push( 116 | this.rotationMatrix( 117 | // this.translate(element[0], 100 - (2 * 20 * (t/max_t))), 118 | // this.translate(element[0], Math.min(max_x/2, max_y/2) - 50), 119 | element[0], 120 | element[1], 121 | t 122 | ) 123 | ); 124 | }, this); 125 | 126 | // Increment iteration counter 127 | step++; 128 | } 129 | 130 | return path; 131 | } 132 | 133 | /** 134 | * Rotate points x and y by angle theta about center point (0,0) 135 | * https://en.wikipedia.org/wiki/Rotation_matrix 136 | **/ 137 | rotationMatrix(x, y, theta) { 138 | return [ 139 | x * Math.cos(theta) - y * Math.sin(theta), 140 | x * Math.sin(theta) + y * Math.cos(theta) 141 | ]; 142 | } 143 | 144 | /** 145 | * Translate points 146 | **/ 147 | translate(pos, delta) { 148 | return pos + delta; 149 | } 150 | } 151 | 152 | export default ShapeSpin; -------------------------------------------------------------------------------- /src/js/patterns/SpinMorph.js: -------------------------------------------------------------------------------- 1 | import PathHelper from '@markroland/path-helper' 2 | import * as Utilities from './utils/Utilities.js'; 3 | 4 | class SpinMorph { 5 | 6 | constructor(env) { 7 | this.key = this.constructor.name.toLowerCase(); 8 | this.name = 'Spin Morph'; 9 | this.env = env; 10 | 11 | this.max_r = 0.5 * Math.min( 12 | (env.table.x.max - env.table.x.min), 13 | (env.table.y.max - env.table.y.min) 14 | ); 15 | 16 | this.config = { 17 | "radius": { 18 | "name": "Radius (r)", 19 | "value": null, 20 | "input": { 21 | "type": "createSlider", 22 | "params" : [ 23 | 1, 24 | 0.5 * Math.min(env.table.x.max, env.table.y.max), 25 | 0.5 * this.max_r, 26 | 1 27 | ], 28 | "class": "slider", 29 | "displayValue": true 30 | } 31 | }, 32 | "angle": { 33 | "name": "Start Angle (𝜃)", 34 | "value": null, 35 | "input": { 36 | "type": "createSlider", 37 | "params" : [ 38 | 0, 39 | 360, 40 | 0, 41 | 1 42 | ], 43 | "class": "slider", 44 | "displayValue": true 45 | } 46 | }, 47 | "reverse": { 48 | "name": "Reverse", 49 | "value": null, 50 | "input": { 51 | "type": "createCheckbox", 52 | "attributes" : [{ 53 | "type" : "checkbox", 54 | "checked" : null 55 | }], 56 | "params": [0, 1, 0], 57 | "displayValue": false 58 | } 59 | } 60 | }; 61 | 62 | this.path = []; 63 | } 64 | 65 | draw() { 66 | 67 | // Update object 68 | this.config.radius.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 69 | this.config.angle.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 70 | 71 | // Display selected values 72 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.radius.value; 73 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.angle.value + '°'; 74 | 75 | // Calculate path for Circle at center 76 | if (!this.path.length) { 77 | this.path = this.constructPath(); 78 | } 79 | 80 | return this.path; 81 | } 82 | 83 | /** 84 | * Calculate coordinates for a circle 85 | **/ 86 | constructPath() { 87 | 88 | // Start path at far right 89 | let path = []; 90 | 91 | const PathHelp = new PathHelper(); 92 | 93 | 94 | 95 | let square = PathHelp.polygon(4, 0.5 * this.max_r); 96 | square = PathHelp.translatePath(square, [0.5 * this.max_r, 0]); 97 | 98 | let circle = PathHelp.polygon(60, 0.5 * this.max_r); 99 | circle = PathHelp.translatePath(circle, [0.5 * this.max_r, 0]); 100 | 101 | const i_max = 90; 102 | 103 | for (let i = 0; i < i_max; i++) { 104 | let square_i = PathHelp.deepCopy(square); 105 | square_i = PathHelp.rotatePath(square_i, (2 * Math.PI) * (i / i_max)); 106 | path = path.concat(square_i); 107 | } 108 | 109 | for (let i = 0; i < i_max; i++) { 110 | let square_i = PathHelp.deepCopy(square); 111 | square_i = PathHelp.rotatePath(square_i, (2 * Math.PI) * (i / i_max)); 112 | square_i = PathHelp.scalePath(square_i, 1 - i / i_max); 113 | 114 | let circle_i = PathHelp.deepCopy(circle); 115 | circle_i = PathHelp.rotatePath(circle_i, (2 * Math.PI) * (i / i_max)); 116 | circle_i = PathHelp.scalePath(circle_i, 1 - i / i_max); 117 | 118 | let morph = PathHelp.morph(square_i, circle_i, i / i_max); 119 | path = path.concat(morph); 120 | } 121 | // path = path.concat(square); 122 | 123 | // --- 124 | 125 | path = PathHelp.simplify(path, 0.33 * this.env.ball.diameter); 126 | 127 | path = PathHelp.reflectPath(path, "y"); 128 | 129 | return path; 130 | } 131 | } 132 | 133 | export default SpinMorph; -------------------------------------------------------------------------------- /src/js/patterns/Spiral.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spiral 3 | */ 4 | class Spiral { 5 | 6 | constructor(env) { 7 | 8 | this.key = "spiral"; 9 | 10 | this.name = "Spiral"; 11 | 12 | this.env = env; 13 | 14 | this.config = { 15 | "sides": { 16 | "name": "Sides", 17 | "value": null, 18 | "input": { 19 | "type": "createSlider", 20 | "params" : [ 21 | 3, 22 | 60, 23 | 12, 24 | 1 25 | ], 26 | "class": "slider", 27 | "displayValue": true 28 | } 29 | }, 30 | "revolutions": { 31 | "name": "Revolutions", 32 | "value": null, 33 | "input": { 34 | "type": "createSlider", 35 | "params" : [ 36 | 1, 37 | 60, 38 | 20, 39 | 1 40 | ], 41 | "class": "slider", 42 | "displayValue": true 43 | } 44 | }, 45 | "start_r": { 46 | "name": "Start Radius", 47 | "value": null, 48 | "input": { 49 | "type": "createSlider", 50 | "params" : [ 51 | 0, 52 | 1.00, 53 | 0, 54 | 0.01 55 | ], 56 | "class": "slider", 57 | "displayValue": true 58 | } 59 | }, 60 | "start_theta": { 61 | "name": "Start Theta", 62 | "value": null, 63 | "input": { 64 | "type": "createSlider", 65 | "params" : [ 66 | 0, 67 | 360, 68 | 0, 69 | 1 70 | ], 71 | "class": "slider", 72 | "displayValue": true 73 | } 74 | }, 75 | "twist": { 76 | "name": "Twist", 77 | "value": null, 78 | "input": { 79 | "type": "createSlider", 80 | "params" : [ 81 | -1, 82 | 1, 83 | 0, 84 | 0.01 85 | ], 86 | "class": "slider", 87 | "displayValue": true 88 | } 89 | }, 90 | "noise": { 91 | "name": "Noise", 92 | "value": null, 93 | "input": { 94 | "type": "createSlider", 95 | "params" : [ 96 | 0, 97 | 50, 98 | 0, 99 | 1 100 | ], 101 | "class": "slider", 102 | "displayValue": true 103 | } 104 | }, 105 | "reverse": { 106 | "name": "Reverse", 107 | "value": null, 108 | "input": { 109 | "type": "createCheckbox", 110 | "attributes" : [{ 111 | "type" : "checkbox", 112 | "checked" : null 113 | }], 114 | "params": [0, 1, 0], 115 | "displayValue": false 116 | } 117 | } 118 | }; 119 | 120 | this.path = []; 121 | } 122 | 123 | 124 | /** 125 | * Draw path - Use class's "calc" method to convert inputs to a draw path 126 | */ 127 | draw() { 128 | 129 | // Read in selected value(s) 130 | this.config.sides.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 131 | this.config.revolutions.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 132 | this.config.start_r.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 133 | this.config.start_theta.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 134 | this.config.twist.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(5) > input').value); 135 | this.config.noise.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(6) > input').value); 136 | 137 | // Display selected value(s) 138 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.sides.value; 139 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.revolutions.value; 140 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.start_r.value.toFixed(2); 141 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.start_theta.value + "°"; 142 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(5) > span').innerHTML = this.config.twist.value; 143 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(6) > span').innerHTML = this.config.noise.value; 144 | 145 | // Calculate path 146 | let path = this.calc( 147 | this.config.start_r.value, 148 | this.config.start_theta.value, 149 | this.config.revolutions.value, 150 | this.config.sides.value, 151 | this.config.twist.value, 152 | this.config.noise.value 153 | ); 154 | 155 | // Update object 156 | this.path = path; 157 | 158 | return path; 159 | } 160 | 161 | /** 162 | * Calculate coordinates for the shape 163 | * 164 | * @param integer Revolutions 165 | * 166 | * @return Array Path 167 | **/ 168 | calc(start_r, start_theta, revolutions, sides, twist, noise) { 169 | 170 | // Set initial values 171 | var x; 172 | var y; 173 | var r; 174 | var theta; 175 | var max_r = Math.min(this.env.table.x.max - this.env.table.x.min, this.env.table.y.max - this.env.table.y.min) / 2; 176 | var start_x = start_r * max_r * Math.cos(start_theta * (Math.PI/180)); 177 | var start_y = start_r * max_r * Math.sin(start_theta * (Math.PI/180)); 178 | 179 | // Initialize shape path array 180 | var path = new Array(); 181 | 182 | // Loop through revolutions 183 | var i_max = sides * revolutions; 184 | var theta_max = (2 * Math.PI) * revolutions; 185 | var theta_twist; 186 | for (var i = 0; i <= i_max; i++) { 187 | 188 | // Rotational Angle 189 | theta_twist = ((i_max - i) / i_max) * twist * (2 * Math.PI); 190 | theta = (i/i_max) * theta_max - theta_twist; 191 | 192 | // Increment radius 193 | r = max_r * (i/i_max); 194 | 195 | // Add noise, except to the beginning and end points 196 | if (noise > 0 && i > 0 && i < i_max) { 197 | r -= noise * Math.random(); 198 | } 199 | 200 | // Convert polar position to rectangular coordinates 201 | x = r * Math.cos(theta); 202 | y = r * Math.sin(theta); 203 | 204 | // Move the focus point of the spiral 205 | x += (-start_x * (i/i_max)) + start_x; 206 | y += (-start_y * (i/i_max)) + start_y; 207 | 208 | // Add coordinates to shape array 209 | path.push([x,y]); 210 | } 211 | 212 | return path; 213 | } 214 | } 215 | 216 | export default Spiral; -------------------------------------------------------------------------------- /src/js/patterns/SpiralZigZag.js: -------------------------------------------------------------------------------- 1 | import PathHelper from '@markroland/path-helper' 2 | import * as Utilities from './utils/Utilities.js'; 3 | 4 | class SpiralZigZag { 5 | 6 | constructor(env) { 7 | this.key = this.constructor.name.toLowerCase(); 8 | this.name = this.constructor.name; 9 | this.env = env; 10 | 11 | this.max_r = 0.5 * Math.min( 12 | (env.table.x.max - env.table.x.min), 13 | (env.table.y.max - env.table.y.min) 14 | ); 15 | 16 | this.config = { 17 | "radius": { 18 | "name": "Radius (r)", 19 | "value": null, 20 | "input": { 21 | "type": "createSlider", 22 | "params" : [ 23 | 1, 24 | 0.5 * Math.min(env.table.x.max, env.table.y.max), 25 | 0.5 * this.max_r, 26 | 1 27 | ], 28 | "class": "slider", 29 | "displayValue": true 30 | } 31 | }, 32 | "angle": { 33 | "name": "Start Angle (𝜃)", 34 | "value": null, 35 | "input": { 36 | "type": "createSlider", 37 | "params" : [ 38 | 0, 39 | 360, 40 | 0, 41 | 1 42 | ], 43 | "class": "slider", 44 | "displayValue": true 45 | } 46 | }, 47 | "reverse": { 48 | "name": "Reverse", 49 | "value": null, 50 | "input": { 51 | "type": "createCheckbox", 52 | "attributes" : [{ 53 | "type" : "checkbox", 54 | "checked" : null 55 | }], 56 | "params": [0, 1, 0], 57 | "displayValue": false 58 | } 59 | } 60 | }; 61 | 62 | this.path = []; 63 | } 64 | 65 | draw() { 66 | 67 | // Update object 68 | this.config.radius.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 69 | this.config.angle.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 70 | 71 | // Display selected values 72 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.radius.value; 73 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.angle.value + '°'; 74 | 75 | // Calculate path for Circle at center 76 | if (!this.path.length) { 77 | this.path = this.constructPath(); 78 | } 79 | 80 | return this.path; 81 | } 82 | 83 | /** 84 | * Calculate coordinates for a circle 85 | **/ 86 | constructPath() { 87 | 88 | // Start path at far right 89 | let path = [ 90 | ]; 91 | 92 | const PathHelp = new PathHelper(); 93 | 94 | // path = PathHelp.polygon(120, this.config.radius.value); 95 | const revolutions = 9; 96 | let spiral = Utilities.spiral( 97 | this.max_r - this.env.ball.diameter, 98 | this.max_r - revolutions * 2 * this.env.ball.diameter, 99 | revolutions, 100 | 120 101 | ); 102 | 103 | path = Utilities.zigZag(spiral, this.env.ball.diameter); 104 | 105 | path.shift(); 106 | 107 | path = [[this.max_r, 0]].concat(path); 108 | 109 | // path = spiral; 110 | 111 | // path = path.concat( 112 | // // console.log( 113 | // Utilities.arcBetweenPoints( 114 | // path[path.length - 1][0], 115 | // path[path.length - 1][1], 116 | // 0, 117 | // 0.5 * this.env.table.y.max, 118 | // this.env.ball.diameter 119 | // ) 120 | // ) 121 | 122 | 123 | // --- 124 | 125 | path = PathHelp.reflectPath(path, "y"); 126 | 127 | return path; 128 | } 129 | } 130 | 131 | export default SpiralZigZag; -------------------------------------------------------------------------------- /src/js/patterns/Spokes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spokes 3 | */ 4 | class Spokes { 5 | 6 | constructor(env) { 7 | 8 | this.key = "spokes"; 9 | 10 | this.name = "Spokes"; 11 | 12 | this.env = env; 13 | 14 | this.config = { 15 | "spokes": { 16 | "name": "Spokes", 17 | "value": null, 18 | "input": { 19 | "type": "createSlider", 20 | "params" : [ 21 | 1, 22 | 120, 23 | 60, 24 | 1 25 | ], 26 | "class": "slider", 27 | "displayValue": true 28 | } 29 | }, 30 | "waves": { 31 | "name": "Waves", 32 | "value": null, 33 | "input": { 34 | "type": "createSlider", 35 | "params" : [ 36 | 0.5, 37 | 10, 38 | 4, 39 | 0.5 40 | ], 41 | "class": "slider", 42 | "displayValue": true 43 | } 44 | }, 45 | "amplitude": { 46 | "name": "Amplitude", 47 | "value": null, 48 | "input": { 49 | "type": "createSlider", 50 | "params" : [ 51 | 0, 52 | 60, 53 | 10, 54 | 1 55 | ], 56 | "class": "slider", 57 | "displayValue": true 58 | } 59 | }, 60 | "reverse": { 61 | "name": "Reverse", 62 | "value": null, 63 | "input": { 64 | "type": "createCheckbox", 65 | "attributes" : [{ 66 | "type" : "checkbox", 67 | "checked" : null 68 | }], 69 | "params": [0, 1, 0], 70 | "displayValue": false 71 | } 72 | } 73 | }; 74 | 75 | this.path = []; 76 | } 77 | 78 | draw() { 79 | 80 | // Update object 81 | this.config.spokes.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 82 | this.config.waves.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 83 | this.config.amplitude.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 84 | 85 | // Display selected values 86 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.spokes.value; 87 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.waves.value.toFixed(1); 88 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.amplitude.value; 89 | 90 | // Calculate the path 91 | let path = this.calc( 92 | this.config.spokes.value, 93 | this.config.waves.value, 94 | this.config.amplitude.value 95 | ); 96 | 97 | // Update object 98 | this.path = path; 99 | 100 | return path; 101 | } 102 | 103 | /** 104 | * Diameters that cross the circle 105 | **/ 106 | calc(num_spokes, num_waves, wave_amplitude) 107 | { 108 | 109 | const max_x = this.env.table.x.max; 110 | const max_y = this.env.table.y.max; 111 | 112 | // Set initial values 113 | var x; 114 | var y; 115 | var theta = 0; 116 | 117 | // Initialize shape path array 118 | // This stores the x,y coordinates for each step 119 | var path = new Array(); 120 | 121 | // Iteration counter. 122 | var step = 0; 123 | 124 | // Change in theta per step 125 | var theta_per_step = (2 * Math.PI) / num_spokes; 126 | 127 | // Sub-steps 128 | var sub_steps = 20 * num_waves; 129 | 130 | // Set direction of travel for "x" 131 | var direction = 1; 132 | 133 | // Loop through 360 degrees 134 | while (theta < (2 * Math.PI)) { 135 | 136 | // Calculate new theta 137 | theta = step * theta_per_step; 138 | 139 | for (var j = 0; j <= sub_steps; j++) { 140 | 141 | // Sine Wave 142 | if (direction > 0) { 143 | x = direction * (Math.min(max_x, max_y)/2) * (j/sub_steps); 144 | } else { 145 | x = -direction * (Math.min(max_x, max_y)/2) * ((sub_steps - j)/sub_steps); 146 | } 147 | y = direction * wave_amplitude * Math.sin((j/sub_steps) * num_waves * (2 * Math.PI)); 148 | 149 | // Rotate [x,y] coordinates around [0,0] by angle theta, and then append to path 150 | path.push( 151 | this.rotationMatrix(x, y, theta) 152 | ); 153 | } 154 | 155 | // Increment iteration counter 156 | step++; 157 | 158 | // Alternate the direction each step, going from +x to -x 159 | direction = direction * -1; 160 | } 161 | 162 | return path; 163 | } 164 | 165 | /** 166 | * Rotate points x and y by angle theta about center point (0,0) 167 | * https://en.wikipedia.org/wiki/Rotation_matrix 168 | **/ 169 | rotationMatrix(x, y, theta) { 170 | return [ 171 | x * Math.cos(theta) - y * Math.sin(theta), 172 | x * Math.sin(theta) + y * Math.cos(theta) 173 | ]; 174 | } 175 | } 176 | 177 | export default Spokes; -------------------------------------------------------------------------------- /src/js/patterns/Star.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Star 3 | */ 4 | class Star { 5 | 6 | constructor(env) { 7 | 8 | this.key = "star"; 9 | 10 | this.name = "Star"; 11 | 12 | this.env = env; 13 | 14 | this.config = { 15 | "points": { 16 | "name": "Points", 17 | "value": null, 18 | "input": { 19 | "type": "createSlider", 20 | "params" : [ 21 | 2, 22 | 12, 23 | 5, 24 | 1 25 | ], 26 | "class": "slider", 27 | "displayValue": true 28 | } 29 | }, 30 | "pointiness": { 31 | "name": "Pointiness", 32 | "value": null, 33 | "input": { 34 | "type": "createSlider", 35 | "params" : [ 36 | 0, 37 | 1, 38 | 0.5, 39 | 0.01 40 | ], 41 | "class": "slider", 42 | "displayValue": true 43 | } 44 | }, 45 | "revolutions": { 46 | "name": "Revolutions", 47 | "value": null, 48 | "input": { 49 | "type": "createSlider", 50 | "params" : [ 51 | 1, 52 | 60, 53 | 20, 54 | 1 55 | ], 56 | "class": "slider", 57 | "displayValue": true 58 | } 59 | }, 60 | "twist": { 61 | "name": "Twist", 62 | "value": null, 63 | "input": { 64 | "type": "createSlider", 65 | "params" : [ 66 | -1, 67 | 1, 68 | 0, 69 | 0.01 70 | ], 71 | "class": "slider", 72 | "displayValue": true 73 | } 74 | }, 75 | "reverse": { 76 | "name": "Reverse", 77 | "value": null, 78 | "input": { 79 | "type": "createCheckbox", 80 | "attributes" : [{ 81 | "type" : "checkbox", 82 | "checked" : null 83 | }], 84 | "params": [0, 1, 0], 85 | "displayValue": false 86 | } 87 | } 88 | }; 89 | 90 | this.path = []; 91 | } 92 | 93 | 94 | /** 95 | * Draw path - Use class's "calc" method to convert inputs to a draw path 96 | */ 97 | draw() { 98 | 99 | // Read in selected value(s) 100 | this.config.points.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 101 | this.config.pointiness.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 102 | this.config.revolutions.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 103 | this.config.twist.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 104 | 105 | // Display selected value(s) 106 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.points.value; 107 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.pointiness.value.toFixed(2); 108 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.revolutions.value; 109 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.twist.value.toFixed(2) + " Revs"; 110 | 111 | // Calculate path 112 | let path = this.calc( 113 | 0, 114 | 0, 115 | 0, 116 | 0, 117 | this.config.revolutions.value, 118 | this.config.points.value, 119 | this.config.pointiness.value, 120 | this.config.twist.value 121 | ); 122 | 123 | // Update object 124 | this.path = path; 125 | 126 | return path; 127 | } 128 | 129 | /** 130 | * Calculate coordinates for the shape 131 | * 132 | * @param integer Revolutions 133 | * 134 | * @return Array Path 135 | **/ 136 | calc(start_x, start_y, start_r, start_theta, revolutions, points, pointiness, twist) { 137 | 138 | const min_x = this.env.table.x.min; 139 | const max_x = this.env.table.x.max; 140 | const min_y = this.env.table.y.min; 141 | const max_y = this.env.table.y.max; 142 | 143 | // Set initial values 144 | var x; 145 | var y; 146 | var r = start_r; 147 | var theta = start_theta; 148 | 149 | // Initialize shape path array 150 | // This stores the x,y coordinates for each step 151 | var path = new Array(); 152 | 153 | // Calculate the maximum radius 154 | var max_r = Math.min(max_x - min_x, max_y - min_y) / 2; 155 | 156 | // Loop through revolutions 157 | var sides = 2 * points; 158 | var i_max = sides * revolutions; 159 | var theta_max = (2 * Math.PI) * revolutions; 160 | var theta_twist; 161 | for (var i = 0; i <= i_max; i++) { 162 | 163 | // Rotational Angle 164 | theta_twist = ((i_max - i) / i_max) * twist * (2 * Math.PI); 165 | theta = (i/i_max) * theta_max - theta_twist; 166 | 167 | // Increment radius 168 | r = start_r + (1 - ((i % 2) * pointiness)) * (max_r * (i/i_max)); 169 | 170 | // Convert polar position to rectangular coordinates 171 | x = start_x + (r * Math.cos(theta)); 172 | y = start_y + (r * Math.sin(theta)); 173 | 174 | // Add coordinates to shape array 175 | path.push([x,y]); 176 | } 177 | 178 | return path; 179 | } 180 | } 181 | 182 | export default Star; -------------------------------------------------------------------------------- /src/js/patterns/Sunset.js: -------------------------------------------------------------------------------- 1 | class Sunset { 2 | 3 | constructor(env) { 4 | 5 | this.key = "sunset"; 6 | 7 | this.name = "Sunset"; 8 | 9 | this.env = env; 10 | 11 | this.config = {}; 12 | 13 | this.path = []; 14 | } 15 | 16 | draw() { 17 | 18 | const max_r = 0.5 * Math.min( 19 | (this.env.table.x.max - this.env.table.x.min), 20 | (this.env.table.y.max - this.env.table.y.min) 21 | ); 22 | 23 | const center = { 24 | x: (this.env.table.x.max + this.env.table.x.min) / 2, 25 | y: (this.env.table.y.max + this.env.table.y.min) / 2 26 | } 27 | 28 | let path = [max_r, 0] 29 | 30 | // Move from required Sisyphus starting point at rho-theta [1,0] to top 31 | let i_max = 12; 32 | for (let i = 0; i <= i_max; i++) { 33 | path.push([ 34 | max_r * Math.cos(i/i_max * (0.5 * Math.PI)), 35 | max_r * Math.sin(i/i_max * (0.5 * Math.PI)) 36 | ]); 37 | } 38 | 39 | // Draw horizontal lines 40 | let j_max = 20; 41 | for (let j = 1; j <= j_max; j++) { 42 | const direction = (j % 2 == 0) ? 1 : -1; 43 | // const y = max_r - j * (1 * this.env.ball.diameter); 44 | const y = max_r * Math.exp(-(j/j_max) * 1.0); 45 | 46 | const x_start = direction * Math.sqrt(Math.pow(max_r, 2) - Math.pow(y, 2)); 47 | const x_end = direction * -Math.sqrt(Math.pow(max_r, 2) - Math.pow(y, 2)); 48 | path.push( 49 | [x_start, y], 50 | [x_end, y] 51 | ); 52 | } 53 | 54 | // Save y value of horizon position 55 | const y_horizon = path[path.length - 1][1]; 56 | 57 | // Go to center 58 | path.push([0, path[path.length - 1][1]]); 59 | 60 | // Spiral out 61 | let last = path[path.length - 1]; 62 | for (let i = 1; i <= 5; i++) { 63 | let j_max = 24; 64 | for (let j = 0; j <= j_max; j++) { 65 | const theta = (j/j_max) * (2 * Math.PI) - 0.5 * Math.PI; 66 | const r = max_r * (i/j_max); 67 | path.push([ 68 | last[0] + r * Math.cos(theta), 69 | last[1] + r * Math.sin(theta) 70 | ]); 71 | } 72 | } 73 | 74 | // Go to Horizon 75 | path.push([ 76 | -Math.sqrt(Math.pow(max_r, 2) - Math.pow(y_horizon, 2)), 77 | y_horizon 78 | ]); 79 | 80 | // Draw horizontal lines 81 | j_max = 70; 82 | let y = y_horizon; 83 | let j = 0; 84 | while (y > -(0.5 * (this.env.table.y.max - this.env.table.y.min))) { 85 | 86 | const direction = (j % 2 === 0) ? 1 : -1; 87 | 88 | // Calculate spacing using exponential interpolation 89 | const t = j / (j_max - 1); // Normalized step (0 to 1) 90 | const spacing = 0.33 * this.env.ball.diameter * Math.pow(2 / 0.33, t); // Exponential growth 91 | 92 | const x_start = direction * Math.sqrt(Math.pow(max_r, 2) - Math.pow(y, 2)); 93 | 94 | const x_end = direction * -Math.sqrt(Math.pow(max_r, 2) - Math.pow(y, 2)); 95 | path.push( 96 | [x_start, y], 97 | [x_end, y] 98 | ); 99 | 100 | // Update y position 101 | y -= spacing; 102 | j++; 103 | } 104 | 105 | // Return to Sisyphus "Home" [1, 0] 106 | //* 107 | path = path.concat(this.#arcToHome( 108 | path[path.length - 1][0], 109 | path[path.length - 1][1], 110 | max_r, 111 | -1 112 | )); 113 | //*/ 114 | 115 | // Update object 116 | this.path = path; 117 | 118 | return path; 119 | } 120 | 121 | #easeOutCirc(x) { 122 | return Math.sqrt(1 - Math.pow(x - 1, 2)); 123 | } 124 | 125 | /** 126 | * 127 | * @todo Use direction to go the other way 128 | * @param {number} x 129 | * @param {number} y 130 | * @param {number} radius 131 | * @param {number} direction 132 | * @param {number} steps 133 | * @returns 134 | */ 135 | #arcToHome(x, y, radius, direction, steps = 30) { 136 | let path = []; 137 | const i_max = steps; 138 | let theta = Math.atan2(y, x); 139 | if (theta < 0) { 140 | theta += 2 * Math.PI; 141 | } 142 | for (let i = 0; i <= i_max; i++) { 143 | let angle = theta - (i/i_max) * theta; 144 | if (direction < 0) { 145 | angle = theta + (i/i_max) * (Math.PI * 2 - theta); 146 | } 147 | 148 | // let angle = theta - (i/i_max) * (Math.PI * 2 - theta); 149 | // if (direction > 0) { 150 | // // TODO: Fix this 151 | // angle = (i/i_max) * theta; 152 | // } 153 | 154 | 155 | path.push([ 156 | radius * Math.cos(angle), 157 | radius * Math.sin(angle) 158 | ]); 159 | } 160 | return path; 161 | } 162 | 163 | } 164 | 165 | export default Sunset; -------------------------------------------------------------------------------- /src/js/patterns/Superellipse.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Super Ellipse 3 | * 4 | * https://en.wikipedia.org/wiki/Superellipse 5 | * https://mathworld.wolfram.com/Superellipse.html 6 | * https://thecodingtrain.com/CodingChallenges/019-superellipse.html 7 | * 8 | */ 9 | class Superellipse { 10 | 11 | constructor(env) { 12 | 13 | this.key = "superellipse"; 14 | 15 | this.name = "Superellipse"; 16 | 17 | let max_r = Math.min((env.table.x.max - env.table.x.min), (env.table.y.max - env.table.y.min))/2; 18 | 19 | this.config = { 20 | "width": { 21 | "name": "Width", 22 | "value": null, 23 | "input": { 24 | "type": "createSlider", 25 | "params" : [ 26 | 0, 27 | 0.5 * (env.table.x.max - env.table.x.min), 28 | max_r, 29 | 1 30 | ], 31 | "class": "slider", 32 | "displayValue": true 33 | } 34 | }, 35 | "height": { 36 | "name": "Height", 37 | "value": null, 38 | "input": { 39 | "type": "createSlider", 40 | "params" : [ 41 | 0, 42 | 0.5 * (env.table.y.max - env.table.y.min), 43 | max_r, 44 | 1 45 | ], 46 | "class": "slider", 47 | "displayValue": true 48 | } 49 | }, 50 | "n": { 51 | "name": "n-value", 52 | "value": null, 53 | "input": { 54 | "type": "createSlider", 55 | "params" : [ 56 | 0.1, 57 | 10, 58 | 1.5, 59 | 0.1 60 | ], 61 | "class": "slider", 62 | "displayValue": true 63 | } 64 | }, 65 | "spiralize": { 66 | "name": "Spiralize", 67 | "value": null, 68 | "input": { 69 | "type": "createCheckbox", 70 | "attributes" : [{ 71 | "type" : "checkbox", 72 | "checked" : true 73 | }], 74 | "params": [0, 1, 1], 75 | "displayValue": false 76 | } 77 | }, 78 | "reverse": { 79 | "name": "Reverse", 80 | "value": null, 81 | "input": { 82 | "type": "createCheckbox", 83 | "attributes" : [{ 84 | "type" : "checkbox", 85 | "checked" : null 86 | }], 87 | "params": [0, 1, 0], 88 | "displayValue": false 89 | } 90 | } 91 | }; 92 | 93 | this.path = []; 94 | } 95 | 96 | draw() { 97 | 98 | // Update object 99 | this.config.width.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 100 | this.config.height.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 101 | this.config.n.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 102 | this.config.spiralize.value = false; 103 | if (document.querySelector('#pattern-controls > div:nth-child(4) > input[type=checkbox]').checked) { 104 | this.config.spiralize.value = true; 105 | } 106 | 107 | // Display selected values 108 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.width.value; 109 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.height.value; 110 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.n.value.toFixed(1); 111 | 112 | // Calculate path 113 | let path = this.calc( 114 | this.config.width.value, 115 | this.config.height.value, 116 | this.config.n.value, 117 | this.config.spiralize.value 118 | ); 119 | 120 | // Update object 121 | this.path = path; 122 | 123 | return path; 124 | } 125 | 126 | /** 127 | * Calculate coordinates for a Superellipse 128 | * 129 | * @param float width 130 | * @param float height 131 | * @param float n 132 | * @param float spiralize 133 | * 134 | **/ 135 | calc(width, height, n, spiralize) { 136 | 137 | // Set initial values 138 | var x; 139 | var y; 140 | var a = width; 141 | var b = height; 142 | 143 | // Initialize return value - the path array 144 | // This stores the x,y coordinates for each step 145 | var path = new Array(); 146 | 147 | // The number of "sides" 148 | // A larger number makes the shape more smooth 149 | let sides = 60; 150 | 151 | if (spiralize) { 152 | a = 0; 153 | b = 0; 154 | var n_base = n; 155 | var max_loops = 30; 156 | for (var loop = 0; loop < max_loops; loop++) { 157 | var a_base = (loop / max_loops) * width; 158 | var b_base = (loop / max_loops) * height; 159 | n = (loop / max_loops) * n_base + 0.1; 160 | for (var theta = 0; theta <= 2 * Math.PI; theta += (2 * Math.PI) / sides) { 161 | a = a_base + (theta/(2 * Math.PI)) * (width/max_loops) 162 | b = b_base + (theta/(2 * Math.PI)) * (height/max_loops) 163 | x = Math.pow(Math.abs(Math.cos(theta)), (2/n)) * a * this.sgn(Math.cos(theta)); 164 | y = Math.pow(Math.abs(Math.sin(theta)), (2/n)) * b * this.sgn(Math.sin(theta)); 165 | path.push([x,y]); 166 | } 167 | } 168 | } else { 169 | 170 | // Loop through one revolution 171 | for (let theta = 0; theta <= 2 * Math.PI; theta += (2 * Math.PI) / sides) { 172 | x = Math.pow(Math.abs(Math.cos(theta)), (2/n)) * a * this.sgn(Math.cos(theta)); 173 | y = Math.pow(Math.abs(Math.sin(theta)), (2/n)) * b * this.sgn(Math.sin(theta)); 174 | path.push([x,y]); 175 | } 176 | } 177 | 178 | return path; 179 | } 180 | 181 | /** 182 | * Return +1 if the number is positive, -1 if the number is negative, and 0 if zero 183 | */ 184 | sgn(val) { 185 | if (val == 0) { 186 | return 0; 187 | } 188 | return val / Math.abs(val); 189 | } 190 | } 191 | 192 | export default Superellipse; -------------------------------------------------------------------------------- /src/js/patterns/ThetaRhoInput.js: -------------------------------------------------------------------------------- 1 | class ThetaRhoInput { 2 | 3 | constructor(env) { 4 | 5 | this.key = "thr"; 6 | 7 | this.name = "Theta Rho Coordinates"; 8 | 9 | this.env = env; 10 | 11 | let spiral_test = "# Start at Center\n0 0\n\n" 12 | + "# Spiral out 4 revolutions\n" 13 | + "25.13272 1\n\n" 14 | + "# Do one full circle at maximum radius\n" 15 | + "31.4159 1\n\n" 16 | + "# do 40 revs in\n" 17 | + "251.32.72 0\n" 18 | 19 | let square_test = "0 0.5\n" 20 | + "0.7854 0.7071\n" 21 | + "1.5708 0.5\n" 22 | + "2.3562 0.7071\n" 23 | + "3.1416 0.5\n" 24 | + "3.9270 0.7071\n" 25 | + "4.7124 0.5\n" 26 | + "5.4978 0.7071\n" 27 | + "6.2832 0.5\n"; 28 | 29 | this.config = { 30 | "thr": { 31 | "name": "Theta Rho", 32 | "value": null, 33 | "input": { 34 | "type": "createTextarea", 35 | "attributes" : { 36 | "rows": 11, 37 | "cols": 30, 38 | }, 39 | "value" : square_test, 40 | "params" : [] 41 | } 42 | } 43 | }; 44 | 45 | this.path = []; 46 | } 47 | 48 | draw() { 49 | 50 | // Update object 51 | this.config.thr.value = document.querySelector('#pattern-controls > div:nth-child(1) > textarea').value; 52 | 53 | // Calculate path for Circle at center 54 | let path = this.calc( 55 | this.config.thr.value 56 | ); 57 | 58 | // Update object 59 | this.path = path; 60 | 61 | return path; 62 | } 63 | 64 | /** 65 | * Calculate coordinates 66 | **/ 67 | calc(data) { 68 | 69 | const min_x = this.env.table.x.min; 70 | const max_x = this.env.table.x.max; 71 | const min_y = this.env.table.y.min; 72 | const max_y = this.env.table.y.max; 73 | 74 | // Calculate the maximum radius of the machine based on its dimensions 75 | let max_r = Math.min((max_x - min_x), (max_y - min_y))/2; 76 | 77 | let thr_commands = new Array(); 78 | 79 | // Initialize return value - the path array 80 | // This stores the x,y coordinates for each step 81 | let path = new Array(); 82 | 83 | // Split string by line 84 | let lines = data.split("\n"); 85 | 86 | // Loop through lines and extract theta-rho instructions 87 | lines.forEach(function(element) { 88 | let coordinates = element.match(/^([\+\-0-9\.]+)\s+([\+\-0-9\.]+)\s*$/) 89 | if (coordinates) { 90 | thr_commands.push([ 91 | parseFloat(coordinates[1]), 92 | parseFloat(coordinates[2]) 93 | ]); 94 | } 95 | }); 96 | 97 | // Convert starting point from Theta-Rho to XY 98 | path.push([ 99 | max_r * thr_commands[0][1] * Math.cos(thr_commands[0][0]), 100 | max_r * thr_commands[0][1] * Math.sin(thr_commands[0][0]) 101 | ]); 102 | 103 | // Loop through Theta-Rho commands 104 | let num_thr_commands = thr_commands.length 105 | let substeps_per_revolution = 60; 106 | let num_substeps; 107 | for (var i = 1; i < num_thr_commands; i++) { 108 | 109 | // Interpolate steps. This is necessary to emulate how a Sisbot will plot the coordinates 110 | let theta_delta = thr_commands[i][0] - thr_commands[i-1][0]; 111 | let radius_delta = thr_commands[i][1] - thr_commands[i-1][1]; 112 | num_substeps = Math.abs((theta_delta * (180/Math.PI)/360) * substeps_per_revolution); 113 | for (var j = 0; j < num_substeps; j++) { 114 | path.push([ 115 | max_r * (thr_commands[i-1][1] + (j/num_substeps) * radius_delta) * Math.cos(thr_commands[i-1][0] + (j/num_substeps) * theta_delta), 116 | max_r * (thr_commands[i-1][1] + (j/num_substeps) * radius_delta) * Math.sin(thr_commands[i-1][0] + (j/num_substeps) * theta_delta) 117 | ]); 118 | } 119 | } 120 | 121 | // Convert starting point from Theta-Rho to XY 122 | path.push([ 123 | max_r * thr_commands[num_thr_commands-1][1] * Math.cos(thr_commands[num_thr_commands-1][0]), 124 | max_r * thr_commands[num_thr_commands-1][1] * Math.sin(thr_commands[num_thr_commands-1][0]) 125 | ]); 126 | 127 | // Rotate the path by a quarter revolution 128 | path = this.rotatePath(path, Math.PI/2); 129 | 130 | // Flip on the X axis 131 | path = this.scalePath(path, [-1, 1]); 132 | 133 | return path; 134 | } 135 | 136 | /** 137 | * Scale Path 138 | * path A path array of [x,y] coordinates 139 | * scale A value from 0 to 1 140 | **/ 141 | scalePath(path, scale) { 142 | return path.map(function(a){ 143 | return [ 144 | a[0] * scale[0], 145 | a[1] * scale[1] 146 | ]; 147 | }); 148 | } 149 | 150 | /** 151 | * Rotate points x and y by angle theta about center point (0,0) 152 | * https://en.wikipedia.org/wiki/Rotation_matrix 153 | **/ 154 | rotatePath(path, theta) { 155 | return path.map(function(a){ 156 | return [ 157 | a[0] * Math.cos(theta) - a[1] * Math.sin(theta), 158 | a[0] * Math.sin(theta) + a[1] * Math.cos(theta) 159 | ] 160 | }); 161 | } 162 | } 163 | 164 | export default ThetaRhoInput; -------------------------------------------------------------------------------- /src/js/patterns/WigglySpiral.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wiggly Spiral 3 | */ 4 | class WigglySpiral { 5 | 6 | constructor(env) { 7 | 8 | this.key = "wigglyspiral"; 9 | 10 | this.name = "Wiggly Spiral"; 11 | 12 | this.env = env; 13 | 14 | this.config = { 15 | "offset": { 16 | "name": "Offset", 17 | "value": null, 18 | "input": { 19 | "type": "createSlider", 20 | "params" : [ 21 | 2, 22 | 40, 23 | 20, 24 | 1 25 | ], 26 | "class": "slider", 27 | "displayValue": true 28 | } 29 | }, 30 | "amplitude": { 31 | "name": "Amplitude", 32 | "value": null, 33 | "input": { 34 | "type": "createSlider", 35 | "params" : [ 36 | 1, 37 | 10, 38 | 5, 39 | 0.1 40 | ], 41 | "class": "slider", 42 | "displayValue": true 43 | } 44 | }, 45 | "wiggles": { 46 | "name": "Wiggles/Rev", 47 | "value": null, 48 | "input": { 49 | "type": "createSlider", 50 | "params" : [ 51 | 0, 52 | 40, 53 | 20, 54 | 0.1 55 | ], 56 | "class": "slider", 57 | "displayValue": true 58 | } 59 | } 60 | }; 61 | 62 | this.path = []; 63 | } 64 | 65 | 66 | /** 67 | * Draw path - Use class's "calc" method to convert inputs to a draw path 68 | */ 69 | draw() { 70 | 71 | const max_y = this.env.table.y.max; 72 | 73 | // Read in selected value(s) 74 | this.config.offset.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(1) > input').value); 75 | this.config.amplitude.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 76 | this.config.wiggles.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 77 | 78 | // Display selected value(s) 79 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(1) > span').innerHTML = this.config.offset.value + " " + this.env.table.units; 80 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = this.config.amplitude.value.toFixed(1) + " " + this.env.table.units; 81 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.wiggles.value; 82 | 83 | // Calculate path 84 | let path = this.calc( 85 | 0, 86 | 0, 87 | 0.9 * (max_y/2), 88 | 0, 89 | this.config.offset.value, 90 | this.config.amplitude.value, 91 | this.config.wiggles.value 92 | ); 93 | 94 | // Update object 95 | this.path = path; 96 | 97 | return path; 98 | } 99 | 100 | /** 101 | * Calculate coordinates for the shape 102 | * 103 | * @param integer Revolutions 104 | * 105 | * @return Array Path 106 | **/ 107 | calc(start_x, start_y, start_r, start_theta, distance_between_turns, wiggle_amplitude, wiggle_frequency) { 108 | 109 | // Set initial values 110 | var x; 111 | var y; 112 | var r = start_r; 113 | var theta = start_theta; 114 | 115 | // Initialize shape path array 116 | // This stores the x,y coordinates for each step 117 | var path = new Array(); 118 | 119 | // Iteration counter. 120 | var step = 0; 121 | 122 | // Increase the denominator to get finer resolution (more instructions/longer time to plot) 123 | var theta_per_step = 1/300; 124 | 125 | // Continue as long as the design stays within bounds of the plotter 126 | // This isn't quite right yet. I need to look into the coordinate translations 127 | // while (r < max_r && x > width/2-max_x/2 && x < width/2+max_x/2 && y > height/2-max_y/2 && y < height/2-max_y/2) { 128 | while (r > 0) { 129 | // while (theta < 100 * (2 * Math.PI)) { 130 | 131 | // Rotational Angle (steps per rotation in the denominator) 132 | theta = step * theta_per_step * (2 * Math.PI); 133 | 134 | // Decrement the radius by a set amount per rotation 135 | // Every full rotation the radius is reduced by the offset (distance_between_turns) 136 | r = start_r - distance_between_turns * (step * theta_per_step); 137 | 138 | // Optional: Decay Frequency and Amplitude 139 | // wiggle_frequency = 0.9999 * wiggle_frequency; 140 | // wiggle_amplitude = 0.99999 * wiggle_amplitude; 141 | 142 | // Add a wiggle with a constant amplitude 143 | // Subtract from radius so that drawing area will not be exceeded 144 | r = r - wiggle_amplitude * Math.sin(wiggle_frequency * theta); 145 | 146 | // Convert polar position to rectangular coordinates 147 | x = start_x + (r * Math.cos(theta)); 148 | y = start_y + (r * Math.sin(theta)); 149 | 150 | // Add coordinates to shape array 151 | path.push([x,y]); 152 | 153 | // Increment iteration counter 154 | step++; 155 | } 156 | 157 | return path; 158 | } 159 | } 160 | 161 | export default WigglySpiral; -------------------------------------------------------------------------------- /src/js/patterns/ZigZag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Zig Zag 3 | */ 4 | class ZigZag { 5 | 6 | constructor(env) { 7 | 8 | this.key = "zigzag"; 9 | 10 | this.name = "Zig Zag"; 11 | 12 | this.env = env; 13 | 14 | this.config = { 15 | "bound": { 16 | "name": "Bounding Shape", 17 | "value": null, 18 | "input": { 19 | "type": "createSelect", 20 | "options": { 21 | "rectangle": "Max Rectangle", 22 | "circle": "Max Circle" 23 | } 24 | } 25 | }, 26 | "spacing": { 27 | "name": "Spacing", 28 | "value": null, 29 | "input": { 30 | "type": "createSlider", 31 | "params" : [ 32 | -((env.table.y.max - env.table.y.min) / 2), 33 | -1, 34 | -0.5 * ((env.table.y.max - env.table.y.min) / 2), 35 | 1 36 | ], 37 | "class": "slider", 38 | "displayValue": true 39 | } 40 | }, 41 | "margin": { 42 | "name": "Margin", 43 | "value": null, 44 | "input": { 45 | "type": "createSlider", 46 | "params" : [ 47 | 0, 48 | Math.min(env.table.x.max - env.table.x.min, env.table.y.max - env.table.y.min)/10, 49 | 0, 50 | 1 51 | ], 52 | "class": "slider", 53 | "displayValue": true 54 | } 55 | }, 56 | "rotation": { 57 | "name": "Rotation", 58 | "value": null, 59 | "input": { 60 | "type": "createSlider", 61 | "params" : [ 62 | -180, 63 | 180, 64 | 0, 65 | 1 66 | ], 67 | "class": "slider", 68 | "displayValue": true 69 | } 70 | }, 71 | "border": { 72 | "name": "Border", 73 | "value": null, 74 | "input": { 75 | "type": "createCheckbox", 76 | "attributes" : [{ 77 | "type" : "checkbox", 78 | "checked" : true 79 | }], 80 | "params": [0, 1, 0], 81 | "displayValue": false 82 | } 83 | }, 84 | "reverse": { 85 | "name": "Reverse", 86 | "value": null, 87 | "input": { 88 | "type": "createCheckbox", 89 | "attributes" : [{ 90 | "type" : "checkbox", 91 | "checked" : null 92 | }], 93 | "params": [0, 1, 0], 94 | "displayValue": false 95 | } 96 | } 97 | }; 98 | 99 | this.path = []; 100 | } 101 | 102 | 103 | /** 104 | * Draw path - Use class's "calc" method to convert inputs to a draw path 105 | */ 106 | draw() { 107 | 108 | const min_y = this.env.table.y.min; 109 | const max_y = this.env.table.y.max; 110 | 111 | // Read in selected value(s) 112 | this.config.bound.value = document.querySelector('#pattern-controls > div:nth-child(1) > select').value; 113 | this.config.spacing.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(2) > input').value); 114 | this.config.margin.value = parseInt(document.querySelector('#pattern-controls > div:nth-child(3) > input').value); 115 | this.config.rotation.value = parseFloat(document.querySelector('#pattern-controls > div:nth-child(4) > input').value); 116 | this.config.border.value = 0 117 | if (document.querySelector('#pattern-controls > div:nth-child(5) > input[type=checkbox]').checked) { 118 | this.config.border.value = 1 119 | } 120 | 121 | // Display selected value(s) 122 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(2) > span').innerHTML = ((max_y - min_y)/(-this.config.spacing.value)).toFixed(1) + " " + this.env.table.units; 123 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(3) > span').innerHTML = this.config.margin.value + " mm"; 124 | document.querySelector('#pattern-controls > div.pattern-control:nth-child(4) > span').innerHTML = this.config.rotation.value + "°"; 125 | 126 | // Calculate path 127 | let path = this.calc( 128 | (max_y - min_y) / (-this.config.spacing.value), 129 | this.config.margin.value, 130 | 0.0, 131 | this.config.border.value, 132 | this.config.bound.value 133 | ); 134 | 135 | // Rotate 136 | path = path.map(function(element) { 137 | return this.rotationMatrix(element[0], element[1], this.config.rotation.value * (Math.PI/180)) 138 | }, this); 139 | 140 | // Update object 141 | this.path = path; 142 | 143 | return path; 144 | } 145 | 146 | /** 147 | * Calculate coordinates for the shape 148 | * 149 | * @param float Spacing 150 | * @param float Margin 151 | * @param float Angle 152 | * @param integer Border 153 | * 154 | * @return Array Path 155 | **/ 156 | calc(spacing, margin, angle, border, bounding_shape) { 157 | 158 | const min_x = this.env.table.x.min; 159 | const max_x = this.env.table.x.max; 160 | const min_y = this.env.table.y.min; 161 | const max_y = this.env.table.y.max; 162 | 163 | // Set initial values 164 | var start_x = -((max_x/2) - margin); 165 | var start_y = -((max_y/2) - margin); 166 | var x = start_x; 167 | var y = start_y; 168 | 169 | // Initialize shape path array 170 | // This stores the x,y coordinates for each step 171 | var path = new Array(); 172 | 173 | // Iteration counter. 174 | var step = 0; 175 | 176 | // Continue as long as the design stays within bounds of the plotter 177 | while ((y + spacing) <= ((max_y/2) - margin)) { 178 | 179 | if (bounding_shape == "circle") { 180 | 181 | if (step % 4 == 0) { 182 | // Move Right 183 | x = Math.sqrt(Math.pow(190, 2) - (Math.pow(y, 2))) 184 | } else if (step % 4 == 1) { 185 | // Move Up 186 | y = y + spacing; 187 | x = Math.sqrt(Math.pow(190, 2) - (Math.pow(y, 2))) 188 | } else if (step % 4 == 2) { 189 | // Move Left 190 | x = -Math.sqrt(Math.pow(190, 2) - (Math.pow(y, 2))) 191 | } else if (step % 4 == 3) { 192 | // Move Up 193 | y = y + spacing; 194 | x = -Math.sqrt(Math.pow(190, 2) - (Math.pow(y, 2))) 195 | } 196 | 197 | } else { 198 | 199 | // Rectangular 200 | if (step % 4 == 0) { 201 | // Move Right 202 | x = (max_x - min_x)/2 - margin; 203 | } else if (step % 4 == 2) { 204 | // Move Left 205 | x = -(max_x - min_x)/2 + margin; 206 | } else { 207 | // Move Up 208 | y = y + spacing; 209 | } 210 | } 211 | 212 | // Add coordinates to shape array 213 | path.push([x,y]); 214 | 215 | // Increment iteration counter 216 | step++; 217 | } 218 | 219 | if (border) { 220 | 221 | if (bounding_shape == "circle") { 222 | 223 | // Loop through one revolution 224 | for (var theta = 0.0; theta < 2 * Math.PI; theta += ((2 * Math.PI)/60)) { 225 | path.push([ 226 | 0.5 * (max_y - min_y) * Math.cos(theta + (0.5 * Math.PI)), 227 | 0.5 * (max_y - min_y) * Math.sin(theta + (0.5 * Math.PI)) 228 | ]); 229 | } 230 | 231 | } else { 232 | 233 | // Ends on left (min) side 234 | if (x < 0) { 235 | path.push([(max_x/2 - margin), y]); 236 | path.push([(max_x/2 - margin), start_y]); 237 | path.push([x, start_y]); 238 | path.push([x, y]); 239 | } else { 240 | path.push([+((max_x-min_x)/2 - margin), start_y]); 241 | path.push([-((max_x-min_x)/2 - margin), start_y]); 242 | path.push([-((max_x-min_x)/2 - margin), y]); 243 | path.push([x, y]); 244 | } 245 | } 246 | 247 | } 248 | 249 | return path; 250 | } 251 | 252 | /** 253 | * Rotate points x and y by angle theta about center point (0,0) 254 | * https://en.wikipedia.org/wiki/Rotation_matrix 255 | **/ 256 | rotationMatrix(x, y, theta) { 257 | return [ 258 | x * Math.cos(theta) - y * Math.sin(theta), 259 | x * Math.sin(theta) + y * Math.cos(theta) 260 | ]; 261 | } 262 | } 263 | 264 | export default ZigZag; -------------------------------------------------------------------------------- /src/js/patterns/index.js: -------------------------------------------------------------------------------- 1 | import env from './../env.js'; 2 | 3 | import Circle from './Circle.js'; 4 | import Coordinates from './Coordinates.js'; 5 | import Cross from './Cross.js'; 6 | import Curvature from './Curvature.js'; 7 | import Cycloid from './Cycloid.js'; 8 | import Diameters from './Diameters.js'; 9 | import Draw from './Draw.js'; 10 | import Egg from './Egg.js'; 11 | import Farris from './Farris.js'; 12 | import FermatSpiral from './FermatSpiral.js'; 13 | import Fibonacci from './Fibonacci.js'; 14 | import FibonacciLollipops from './FibonacciLollipops.js'; 15 | import Frame from './Frame.js'; 16 | import Gcode from './Gcode.js'; 17 | import Gravity from './Gravity.js'; 18 | import Heart from './Heart.js'; 19 | import Lindenmayer from './Lindenmayer.js'; 20 | import Lissajous from './Lissajous.js'; 21 | import LogarithmicSpiral from './LogarithmicSpiral.js'; 22 | import Parametric from './Parametric.js'; 23 | import Rectangle from './Rectangle.js'; 24 | import Rhodonea from './Rhodonea.js'; 25 | import ShapeMorph from './ShapeMorph.js'; 26 | import ShapeSpin from './ShapeSpin.js'; 27 | import SpinMorph from './SpinMorph.js'; 28 | import Spiral from './Spiral.js'; 29 | import SpiralZigZag from './SpiralZigZag.js'; 30 | import Spokes from './Spokes.js'; 31 | import Star from './Star.js'; 32 | import Sunset from './Sunset.js'; 33 | import Superellipse from './Superellipse.js'; 34 | import Text from './Text.js'; 35 | import ThetaRhoInput from './ThetaRhoInput.js'; 36 | import WigglySpiral from './WigglySpiral.js'; 37 | import ZigZag from './ZigZag.js'; 38 | 39 | const Patterns = { 40 | "circle": new Circle(env), 41 | "coordinates": new Coordinates(), 42 | "cross": new Cross(env), 43 | "curvature": new Curvature(env), 44 | "cycloid": new Cycloid(env), 45 | "diameters": new Diameters(env), 46 | "draw": new Draw(env), 47 | "egg": new Egg(env), 48 | "farris": new Farris(env), 49 | "fermatspiral": new FermatSpiral(env), 50 | "fibonacci": new Fibonacci(env), 51 | "fibonaccilollipops": new FibonacciLollipops(env), 52 | "frame": new Frame(env), 53 | "gcode": new Gcode(env), 54 | "gravity": new Gravity(env), 55 | "heart": new Heart(), 56 | "lindenmayer": new Lindenmayer(env), 57 | "lissajous": new Lissajous(env), 58 | "logspiral": new LogarithmicSpiral(env), 59 | "parametric": new Parametric(), 60 | "rectangle": new Rectangle(env), 61 | "rhodonea": new Rhodonea(env), 62 | "shapemorph": new ShapeMorph(env), 63 | "shapespin": new ShapeSpin(), 64 | "spinmorph": new SpinMorph(env), 65 | "spiral": new Spiral(env), 66 | "spiralzigzag": new SpiralZigZag(env), 67 | "spokes": new Spokes(env), 68 | "star": new Star(env), 69 | "sunset": new Sunset(env), 70 | "superellipse": new Superellipse(env), 71 | "text": new Text(env), 72 | "thr": new ThetaRhoInput(env), 73 | "wigglyspiral": new WigglySpiral(env), 74 | "zigzag": new ZigZag(env) 75 | } 76 | 77 | export default Patterns; -------------------------------------------------------------------------------- /src/js/patterns/utils/Utilities.js: -------------------------------------------------------------------------------- 1 | import PathHelper from '@markroland/path-helper' 2 | 3 | export function spiral(r1, r2, n, sides) { 4 | 5 | let path = []; 6 | 7 | const PathHelp = new PathHelper(); 8 | 9 | const stepSize = (2 * Math.PI * r1) / sides; // Approximate step size based on initial radius 10 | let theta = 0; 11 | let r = r1; 12 | 13 | while (r >= r2) { 14 | path.push([ 15 | r * Math.cos(theta), 16 | r * Math.sin(theta), 17 | ]); 18 | 19 | theta += stepSize / r; // Increment angle based on step size 20 | r = r1 + (theta / (2 * Math.PI * n)) * (r2 - r1); // Increment radius proportionally 21 | } 22 | 23 | // path = PathHelp.simplify(path, stepSize); 24 | 25 | // const theta1 = 0; 26 | // const theta2 = n * 2 * Math.PI; 27 | // const i_max = n * sides; 28 | // for (let i = 0; i <= i_max; i++) { 29 | // const r = r1 + (i/i_max) * (r2 - r1); 30 | // const theta = theta1 + (i/i_max) * (theta2 - theta1); 31 | 32 | // path.push([ 33 | // r * Math.cos(theta), 34 | // r * Math.sin(theta), 35 | // ]); 36 | // } 37 | 38 | // path = PathHelp.simplify(path, step_size); 39 | 40 | return path; 41 | } 42 | 43 | export function zigZag(path, offset) { 44 | 45 | const PathHelp = new PathHelper(); 46 | 47 | let innerPath = PathHelp.offsetPath(path, -offset); 48 | 49 | // Need this for closed shapes: 50 | // innerPath = PathHelp.shiftPath(innerPath, 2); 51 | // innerPath.push(innerPath[0]); 52 | // innerPath.splice(1, 1); 53 | 54 | let outerPath = PathHelp.offsetPath(path, offset); 55 | 56 | // Need this for closed shapes: 57 | // outerPath = PathHelp.shiftPath(outerPath, 2); 58 | // outerPath.push(outerPath[0]); 59 | // outerPath.splice(1, 1); 60 | 61 | // ZigZag 62 | let newPath = []; 63 | for (let i = 0; i < path.length; i++) { 64 | if (i % 2 === 0) { 65 | newPath.push(innerPath[i]); 66 | newPath.push(outerPath[i]); 67 | } else { 68 | newPath.push(outerPath[i]); 69 | newPath.push(innerPath[i]); 70 | } 71 | } 72 | path = newPath; 73 | 74 | return newPath; 75 | } 76 | 77 | /** 78 | * This may not do quite what I want it to do. 79 | */ 80 | export function arcBetweenPoints(x1, y1, x2, y2, step_size) { 81 | 82 | const PathHelp = new PathHelper(); 83 | 84 | let path = [[x1, y1]]; 85 | 86 | const r1 = Math.sqrt(x1 * x1 + y1 * y1); 87 | const theta1 = Math.atan2(y1, x1); 88 | 89 | const r2 = Math.sqrt(x2 * x2 + y2 * y2); 90 | const theta2 = Math.atan2(y2, x2); 91 | 92 | const i_max = 1000; 93 | for (let i = 0; i < i_max; i++) { 94 | 95 | const r = r1 + (i/i_max) * (r2 - r1); 96 | const theta = theta1 + (i/i_max) * (theta2 - theta1); 97 | 98 | path.push([ 99 | r * Math.cos(theta), 100 | r * Math.sin(theta), 101 | ]); 102 | } 103 | 104 | path = PathHelp.simplify(path, step_size); 105 | 106 | return path; 107 | } 108 | -------------------------------------------------------------------------------- /src/js/thetaRho.js: -------------------------------------------------------------------------------- 1 | /** Class representing a Theta-Rho Sisyphus track */ 2 | class thetaRho { 3 | 4 | constructor(env) { 5 | 6 | this.env = env; 7 | 8 | this.header = [ 9 | "# Created using https://markroland.github.io/sand-table-pattern-maker/", 10 | "# Version " + env.app.version, 11 | "#", 12 | "# Track Created: " + (new Date().getMonth() + 1) + "/" + new Date().getDate() + "/" + new Date().getFullYear() + " " + new Date().getHours() + ":" + new Date().getMinutes() + ":" + new Date().getSeconds(), 13 | "#", 14 | "# Written by Mark Roland", 15 | "" 16 | ]; 17 | 18 | this.preamble = [ 19 | "#", 20 | "# Custom Theta-Rho to execute before the start of the track", 21 | "#", 22 | // Insert your code here 23 | ]; 24 | 25 | this.postamble = [ 26 | "#", 27 | "# Custom Theta-Rho to execute after the end of the sketch", 28 | "#", 29 | // Insert your code here 30 | ]; 31 | } 32 | 33 | /** 34 | * Create the Theta-Rho track file used by Sisyphus tables 35 | * 36 | * https://sisyphus-industries.com/community/community-tracks/new-tool-for-creating-simple-algorithmic-tracks/#post-296 37 | * 38 | */ 39 | convert(path) { 40 | 41 | const min_x = this.env.table.x.min; 42 | const max_x = this.env.table.x.max; 43 | const min_y = this.env.table.y.min; 44 | const max_y = this.env.table.y.max; 45 | 46 | // Initialize variables 47 | var theta; 48 | var current_theta; 49 | var previous_theta; 50 | var delta_theta; 51 | 52 | var rho; 53 | 54 | // Rotate the path by a quarter revolution 55 | // Should rotation be negative? 56 | path = this.ThrRotatePath(path, Math.PI/2); 57 | 58 | // Flip on the X axis 59 | path = this.ThrScalePath(path, [-1, 1]); 60 | 61 | // Set the maximum radius supported by the plotter. 62 | // This is limited by the shortest axis 63 | var max_radius = Math.min((max_x - min_x),(max_y - min_y)) / 2; 64 | 65 | // Initialize thetaRho command string with the standard header 66 | var thetaRho = this.header; 67 | 68 | // Add standard start command(s) 69 | thetaRho = thetaRho.concat(this.preamble); 70 | thetaRho = thetaRho.concat(""); 71 | 72 | thetaRho = thetaRho.concat("# Track"); 73 | 74 | // Subdivide Path. 75 | // The coefficient here is arbitrary so long as 76 | path = this.subdividePath(path, 0.125 * this.env.ball.diameter); 77 | 78 | // The first point is initialized so that when looping 79 | // through successive points the Theta value can be compared 80 | // to the previous Theta position. 81 | previous_theta = this.calcTheta(path[0][0], path[0][1]); 82 | thetaRho.push( 83 | previous_theta.toFixed(4) 84 | + " " 85 | + this.calcRho(path[0][0], path[0][1], max_radius).toFixed(4) 86 | ); 87 | 88 | // Loop through the rest of the path coordinates 89 | for (let i = 1; i < path.length; i++) { 90 | 91 | // Calculate current Theta from 0 to 2-Pi 92 | current_theta = this.calcTheta(path[i][0], path[i][1]) 93 | 94 | // Add on the full rotations previously completed 95 | current_theta += Math.floor(previous_theta / (2 * Math.PI)) * (2 * Math.PI); 96 | 97 | // Compute the difference to the last point. 98 | delta_theta = current_theta - previous_theta; 99 | 100 | // Correct delta so that it takes the shortest path 101 | if (delta_theta < -Math.PI) { 102 | delta_theta += 2.0 * Math.PI; 103 | } else if (delta_theta > Math.PI) { 104 | delta_theta -= 2.0 * Math.PI; 105 | } 106 | 107 | // Add change in theta to previous theta position 108 | theta = previous_theta + delta_theta; 109 | 110 | // Save theta position as previous for next iteration 111 | previous_theta = theta; 112 | 113 | // Calculate Rho 114 | rho = this.calcRho( 115 | path[i][0], 116 | path[i][1], 117 | max_radius 118 | ); 119 | 120 | // Save coordinates to path 121 | thetaRho.push(theta.toFixed(4) + " " + rho.toFixed(4)); 122 | } 123 | 124 | // Add end command(s) 125 | thetaRho = thetaRho.concat(""); 126 | thetaRho = thetaRho.concat(this.postamble); 127 | 128 | return thetaRho; 129 | } 130 | 131 | /** 132 | * Scale Path 133 | * path An array of [x,y] coordinates 134 | * scale An array of [x,y] scale factors 135 | **/ 136 | ThrScalePath(path, scale) { 137 | return path.map(function(a){ 138 | return [ 139 | a[0] * scale[0], 140 | a[1] * scale[1] 141 | ]; 142 | }); 143 | } 144 | 145 | /** 146 | * Rotate points x and y by angle theta about center point (0,0) 147 | * https://en.wikipedia.org/wiki/Rotation_matrix 148 | **/ 149 | ThrRotatePath(path, theta) { 150 | return path.map(function(a){ 151 | return [ 152 | a[0] * Math.cos(theta) - a[1] * Math.sin(theta), 153 | a[0] * Math.sin(theta) + a[1] * Math.cos(theta) 154 | ] 155 | }); 156 | } 157 | 158 | /** 159 | * Subdivide a path 160 | * path A path array 161 | * max_segment_length Smaller values create create a more detailed path 162 | * Return Array 163 | **/ 164 | subdividePath(path, max_segment_length) { 165 | 166 | let new_path = new Array(); 167 | let delta_x, delta_y; 168 | 169 | // Loop through path coordinates 170 | let i_max = path.length - 1; 171 | for (let i = 0; i < i_max; i++) { 172 | 173 | // Calculate the distance between the current and next point 174 | delta_x = path[i+1][0] - path[i][0]; 175 | delta_y = path[i+1][1] - path[i][1]; 176 | let delta_distance = Math.sqrt( 177 | Math.pow(delta_x, 2) 178 | + 179 | Math.pow(delta_y, 2) 180 | ); 181 | 182 | // Calculate the number of steps by which to divide the distance 183 | let num_substeps = Math.ceil(delta_distance/max_segment_length); 184 | 185 | // Add sub-step coordinates for each subdivision 186 | for (var j = 0; j < num_substeps; j++) { 187 | new_path.push([ 188 | path[i][0] + delta_x * (j/num_substeps), 189 | path[i][1] + delta_y * (j/num_substeps) 190 | ]); 191 | } 192 | 193 | // Add last step 194 | if (i+1 == i_max) { 195 | new_path.push([ 196 | path[i][0] + delta_x, 197 | path[i][1] + delta_y 198 | ]); 199 | } 200 | } 201 | 202 | // Add last coordinate 203 | new_path.concat(path.slice(-1)); 204 | 205 | return new_path; 206 | } 207 | 208 | /** 209 | * Calculate the angle Theta ranging from 0 to two Pi 210 | * @param {number} x - The X Coordinate 211 | * @param {number} y - The Y Coordinate 212 | * Return {number} The polar angle, Theta, between [0,0] and [x,y] 213 | **/ 214 | calcTheta(x, y) { 215 | 216 | // Calculator theta in a range from +Pi to -Pi 217 | let theta = Math.atan2(y, x); 218 | 219 | // Convert -pi-to-0 to pi-to-2pi 220 | if (theta < 0) { 221 | theta = (2 * Math.PI) + theta; 222 | } 223 | return theta; 224 | } 225 | 226 | /** 227 | * Calculate the radius Rho and normalize to a value between 0 and 1 228 | * @param {number} x - The X Coordinate 229 | * @param {number} y - The Y Coordinate 230 | * @param {number} max_radius - The maximum radius of the table 231 | * Return {number} The radius Rho, normalized between 0 and 1 232 | **/ 233 | calcRho(x, y, max_radius) { 234 | return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) / max_radius; 235 | } 236 | } 237 | 238 | export default thetaRho; -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { resolve } from 'path'; 3 | 4 | export default defineConfig(({ mode }) => { 5 | const isLocalhost = mode === 'development'; 6 | return { 7 | base: isLocalhost ? '/' : '/sand-table-pattern-maker/', 8 | root: 'src', 9 | publicDir: '../public', 10 | build: { 11 | outDir: '../dist', 12 | emptyOutDir: true, 13 | rollupOptions: { 14 | input: { 15 | main: resolve(__dirname, 'src/index.html'), 16 | env: resolve(__dirname, 'src/js/env.js'), 17 | }, 18 | }, 19 | }, 20 | server: { 21 | open: true, 22 | }, 23 | optimizeDeps: { 24 | include: ['@markroland/path-helper'], 25 | }, 26 | }; 27 | }); --------------------------------------------------------------------------------