├── .DS_Store ├── .browserslist ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .tool-versions ├── README.md ├── babel.config.js ├── index.html ├── package.json ├── postcss.config.js ├── src ├── .DS_Store ├── components │ └── Drawing.js ├── contexts │ ├── ContextInterface.js │ ├── P5Context.js │ └── SVGContext.js ├── helpers │ ├── dom-helpers.js │ └── math.js ├── index.js └── styles │ └── index.scss ├── webpack.dev.js ├── yarn-error.log └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piratefsh/svg-js/150132f136bd6294c5a19f3569f0aba71054213c/.DS_Store -------------------------------------------------------------------------------- /.browserslist: -------------------------------------------------------------------------------- 1 | > 0.25% 2 | not dead -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 3 | "plugins": [ 4 | "prettier" 5 | ], 6 | "parserOptions": { "ecmaVersion": 10, "sourceType": "module" }, 7 | "rules": { 8 | "prettier/prettier": "error" 9 | }, 10 | "env": { 11 | "browser": true, 12 | "es6": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 10.16.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # p5 + SVG 2 | 3 | Simple setup to generate drawings using svg.js and p5.js 4 | 5 | ![screenshot of p5/js and SVG renderings of an abstract drawing](https://pbs.twimg.com/media/Dpa3KGJXgAETTzf.png:large) 6 | 7 | ## Install 8 | ``` 9 | yarn 10 | 11 | // or 12 | 13 | npm install 14 | ``` 15 | 16 | ## Start server 17 | ``` 18 | yarn start 19 | 20 | //or 21 | 22 | npm run start 23 | ``` 24 | 25 | and visit http://localhost:8080 26 | 27 | ## Usage 28 | 29 | Add code to `src/components/Drawing.js` to draw! Only supports `line` and `ellipse` APIs for now. -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "useBuiltIns": "entry", 9 | "corejs": 3 10 | } 11 | ] 12 | ] 13 | 14 | const plugins = ["@babel/plugin-transform-destructuring"] 15 | 16 | return { 17 | presets, 18 | plugins 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | svg-js 7 | 8 | 9 |

Hello world

10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "avg-js", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --config webpack.dev.js --mode development", 9 | "build": "webpack --config webpack.prod.js --mode production", 10 | "preview": "npm run build && http-server dist" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "" 15 | }, 16 | "keywords": [], 17 | "author": "Sher Minn Chong", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "" 21 | }, 22 | "homepage": "", 23 | "dependencies": { 24 | "core-js": "3", 25 | "mathjs": "^5.1.1", 26 | "normalize.css": "^8.0.0", 27 | "p5": "^0.7.2", 28 | "svg.js": "^2.7.1" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.6.4", 32 | "@babel/plugin-transform-destructuring": "^7.6.0", 33 | "@babel/preset-env": "^7.6.3", 34 | "ajv": "^6.3.0", 35 | "babel-loader": "^8.0.6", 36 | "clean-webpack-plugin": "^0.1.19", 37 | "css-loader": "^0.28.11", 38 | "cssnano": "^3.10.0", 39 | "eslint": "^6.5.1", 40 | "eslint-config-airbnb": "^18.0.1", 41 | "eslint-config-prettier": "^6.4.0", 42 | "eslint-plugin-import": "^2.18.2", 43 | "eslint-plugin-jsx-a11y": "^6.2.3", 44 | "eslint-plugin-prettier": "^3.1.1", 45 | "eslint-plugin-react": "^7.16.0", 46 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 47 | "favicons-webpack-plugin": "0.0.8", 48 | "file-loader": "^1.1.11", 49 | "html-webpack-plugin": "^3.1.0", 50 | "http-server": "^0.11.1", 51 | "node-sass": "^4.8.3", 52 | "optimize-css-assets-webpack-plugin": "^4.0.0", 53 | "postcss-loader": "^2.1.3", 54 | "prettier": "^1.18.2", 55 | "sass-loader": "^6.0.7", 56 | "source-map-loader": "^0.2.3", 57 | "style-loader": "^0.20.3", 58 | "url-loader": "^1.0.1", 59 | "webpack": "^4.2.0", 60 | "webpack-cli": "^3.3.9", 61 | "webpack-dev-server": "^3.8.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PostCSS is a tool for transforming styles with JS plugins. 3 | * These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more. 4 | * https://github.com/postcss/postcss 5 | */ 6 | module.exports = { 7 | plugins: [ 8 | /* 9 | * Adds vendor prefixes to css attributes 10 | * https://github.com/postcss/autoprefixer 11 | */ 12 | require('autoprefixer')({ 13 | /* It should add vendor prefixes for the last 2 versions of all browsers, meaning old prefixes such as 14 | * -webkit-border-radius: 5px; that the latest browsers support as border-radius won't be added. 15 | * https://github.com/ai/browserslist#queries 16 | */ 17 | browsers: 'last 2 versions' 18 | }) 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piratefsh/svg-js/150132f136bd6294c5a19f3569f0aba71054213c/src/.DS_Store -------------------------------------------------------------------------------- /src/components/Drawing.js: -------------------------------------------------------------------------------- 1 | export default class Drawing { 2 | constructor({ styles, ctx, width, height }) { 3 | // add defaults 4 | this.styles = { 5 | stroke: "black", 6 | strokeWidth: 1, 7 | fill: "rgba(0, 0, 0, 0.1)", 8 | ...styles 9 | }; 10 | 11 | // set drawing contesxt 12 | this.ctx = ctx; 13 | 14 | this.width = width; 15 | this.height = height; 16 | } 17 | 18 | draw() { 19 | const { ctx } = this; 20 | ctx.draw(() => { 21 | ctx.setStyles({ fill: "#eee" }); 22 | ctx.rect(this.width, this.height, 0, 0); 23 | 24 | ctx.setStyles(this.styles); 25 | 26 | ctx.startBezier(30, 70); 27 | ctx.bezierVertex(50, 50, 134, 122, 160, 170); 28 | ctx.bezierVertex(190, 120, 210, 210); 29 | ctx.endBezier(); 30 | 31 | ctx.startLine(); 32 | ctx.lineVertex(200, 0); 33 | ctx.lineVertex(100, 50); 34 | ctx.lineVertex(250, 100); 35 | ctx.lineVertex(100, 200); 36 | ctx.lineVertex(400, 400); 37 | ctx.endLine(); 38 | 39 | ctx.setStyles({ fill: "rgba(0,0,0,0)" }); 40 | ctx.thickLine(30, 100, 50, 200, 4); 41 | ctx.thickDot(60, 220, 20); 42 | }); 43 | } 44 | 45 | save() { 46 | this.ctx.save(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/contexts/ContextInterface.js: -------------------------------------------------------------------------------- 1 | import { translate, normalize, rotate, mult } from "../helpers/math"; 2 | 3 | export default class ContextInterface { 4 | constructor(parentNode, width, height) { 5 | this.instance = null; 6 | this.styles = {}; 7 | 8 | this.parentNode = parentNode; 9 | 10 | /* eslint-disable no-plusplus */ 11 | this.parentNode.id = `${ 12 | this.constructor.name 13 | }-${ContextInterface.COUNTER++}`; 14 | 15 | this.width = width; 16 | this.height = height; 17 | this.instantiate(parentNode); 18 | } 19 | 20 | /* eslint-disable class-methods-use-this */ 21 | instantiate(options) { 22 | return `instantiate ${options}`; 23 | } 24 | 25 | animate() { 26 | return "animate"; 27 | } 28 | 29 | line() { 30 | return "line"; 31 | } 32 | 33 | ellipse() { 34 | return "ellipse"; 35 | } 36 | 37 | point() { 38 | return "point"; 39 | } 40 | 41 | rect() { 42 | return "rect"; 43 | } 44 | 45 | crect() { 46 | // same as rect, but position passed in is used as center 47 | return "crect"; 48 | } 49 | 50 | arc() { 51 | return "arc"; 52 | } 53 | 54 | startLine() { 55 | return `startLine`; 56 | } 57 | 58 | endLine() { 59 | return `endLine`; 60 | } 61 | 62 | endBezier() { 63 | return `endBezier`; 64 | } 65 | 66 | bezierVertex(x, y) { 67 | return `bezierVertex ${x}, ${y}`; 68 | } 69 | 70 | saveFileName() { 71 | return `${this.constructor.name}-${new Date()}`; 72 | } 73 | 74 | /** 75 | Draw takes in a function that is run to draw on the context. 76 | Has to be a function due to weird way that p5 is instantiate 77 | */ 78 | draw(fn) { 79 | fn(); 80 | } 81 | 82 | getDOMElement() { 83 | return null; 84 | } 85 | 86 | thickLine(sx, sy, ex, ey, lineWidth = 1) { 87 | lineWidth = Math.trunc(lineWidth); 88 | const vec = normalize(rotate({ x: sx - ex, y: sy - ey }, Math.PI / 2)); 89 | for (let i = 0; i < lineWidth; i++) { 90 | const offset = mult(vec, i - Math.floor(lineWidth / 2)); 91 | const st = translate({ x: sx, y: sy }, offset); 92 | const en = translate({ x: ex, y: ey }, offset); 93 | this.line(st.x, st.y, en.x, en.y); 94 | } 95 | } 96 | 97 | thickDot(x, y, size = 1) { 98 | for (let i = 0; i < size; i++) { 99 | this.ellipse(i, i, x, y); 100 | } 101 | } 102 | } 103 | 104 | ContextInterface.COUNTER = 0; 105 | -------------------------------------------------------------------------------- /src/contexts/P5Context.js: -------------------------------------------------------------------------------- 1 | import P5 from "p5"; 2 | import ContextInterface from "./ContextInterface"; 3 | import { rotate } from "../helpers/math"; 4 | 5 | /* eslint-disable no-underscore-dangle */ 6 | 7 | export default class P5Context extends ContextInterface { 8 | constructor(...args) { 9 | super(...args); 10 | this._bezierPoints = null; 11 | this._loop = false; 12 | this._cache = {}; 13 | } 14 | 15 | p5Functions(p) { 16 | this.p5renderer = p; 17 | /* eslint-disable no-param-reassign */ 18 | p.draw = this.drawFn; 19 | p.setup = () => { 20 | p.createCanvas(this.width, this.height); 21 | if (!this._loop) { 22 | p.noLoop(); 23 | } 24 | }; 25 | /* eslint-enable no-param-reassign */ 26 | } 27 | 28 | draw(drawFn) { 29 | this.drawFn = drawFn; 30 | 31 | // instantiate on draw instead of in constructor 32 | // is is because p5 needs draw function on instantiation 33 | this.instance = new P5(this.p5Functions.bind(this), this.parentNode); 34 | } 35 | 36 | save() { 37 | this.p5renderer.saveCanvas(this.saveFileName()); 38 | } 39 | 40 | animate(flag) { 41 | this._loop = flag; 42 | } 43 | 44 | line(...args) { 45 | this.instance.line(...args); 46 | } 47 | 48 | ellipse(width, height, x, y) { 49 | this.instance.ellipse(x, y, width, height); 50 | } 51 | 52 | rect(width, height, x, y) { 53 | this.instance.rect(x, y, width, height); 54 | } 55 | 56 | crect(width, height, x, y) { 57 | this.instance.rect(x - width / 2, y - height / 2, width, height); 58 | } 59 | 60 | arc(x, y, radX, radY, start, stop) { 61 | this.instance.arc(x, y, radX * 2, radY * 2, start, stop); 62 | } 63 | 64 | point(x, y) { 65 | this.instance.point(x, y); 66 | } 67 | 68 | startLine() { 69 | const { instance } = this; 70 | instance.beginShape(instance.LINE); 71 | } 72 | 73 | endLine() { 74 | const { instance } = this; 75 | instance.endShape(); 76 | } 77 | 78 | lineVertex(x, y) { 79 | const { instance } = this; 80 | instance.vertex(x, y); 81 | } 82 | 83 | startBezier(x, y) { 84 | const { instance } = this; 85 | 86 | // throw error if starting new bezier before closing previous 87 | if (this._bezierPoints !== null) { 88 | throw Error( 89 | "startBezier: tried to start a new bezier curve before closing a previous one." 90 | ); 91 | } 92 | 93 | instance.beginShape(); 94 | instance.vertex(x, y); 95 | 96 | // keep track of points to calculate 97 | // continuous control points 98 | this._bezierPoints = [[x, y]]; 99 | } 100 | 101 | endBezier() { 102 | if (this._bezierPoints === null) { 103 | throw Error( 104 | "endBezier: tried to end a bezier curve before starting one." 105 | ); 106 | } 107 | 108 | this.instance.endShape(); 109 | this._bezierPoints = null; 110 | } 111 | 112 | bezierVertex(...args) { 113 | const { instance, _bezierPoints } = this; 114 | 115 | if (args.length === 6) { 116 | instance.bezierVertex(...args); 117 | } else if (args.length === 4) { 118 | const [c2x, c2y, x, y] = args; 119 | // calculate continuous control point 120 | const prev = _bezierPoints[_bezierPoints.length - 1]; 121 | 122 | // prev can either have 4 or 6 args, just want last 4 123 | if (prev.length >= 4) { 124 | const prevcx = prev[prev.length - 4]; 125 | const prevcy = prev[prev.length - 3]; 126 | const prevx = prev[prev.length - 2]; 127 | const prevy = prev[prev.length - 1]; 128 | // rotate normalized c2 by the end point by 180 deg 129 | const { x: cx, y: cy } = rotate( 130 | { 131 | x: prevcx - prevx, 132 | y: prevcy - prevy 133 | }, 134 | Math.PI 135 | ); 136 | instance.bezierVertex(cx + prevx, cy + prevy, c2x, c2y, x, y); 137 | } else { 138 | // if missing first control point, use start point 139 | instance.bezierVertex(...prev, ...args); 140 | } 141 | } else { 142 | throw Error("bezierVertex: expected 4 or 6 arguments"); 143 | } 144 | 145 | this._bezierPoints.push(args); 146 | } 147 | 148 | setStyles(styles) { 149 | const { stroke, strokeWidth, strokeWeight, fill } = styles; 150 | if (stroke) this.instance.stroke(this._fetchColor(stroke)); 151 | if (strokeWidth || strokeWeight) 152 | this.instance.strokeWeight(strokeWidth || strokeWeight); 153 | if (fill) this.instance.fill(this._fetchColor(fill)); 154 | } 155 | 156 | _fetchColor(color) { 157 | if (color) { 158 | if (color in this._cache) { 159 | return this._cache[color]; 160 | } 161 | this._cache[color] = this.instance.color(color); 162 | return this._cache[color]; 163 | } 164 | } 165 | 166 | getDOMElement() { 167 | return this.parentNode; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/contexts/SVGContext.js: -------------------------------------------------------------------------------- 1 | import SVG from "svg.js"; 2 | import { polarToCartesian } from "../helpers/math"; 3 | import ContextInterface from "./ContextInterface"; 4 | 5 | /* eslint-disable no-underscore-dangle */ 6 | 7 | const STYLE_NAME_MAP = { 8 | strokeWidth: "stroke-width" 9 | }; 10 | 11 | const QUAD_COLOR_REGEXP = /([.0-9]{1,4}%?)/g; 12 | const COLOR_WITH_ALPHA_REGEX = /(hsla|rgba)\(.*\)/; 13 | 14 | // turn (hsl|rgb)a colors into (hsl|rgb) and opacity 15 | function parseColor(key, val) { 16 | const str = `${val}`; 17 | const format = str.match(COLOR_WITH_ALPHA_REGEX); 18 | if (format) { 19 | const matches = str.match(QUAD_COLOR_REGEXP); 20 | if (matches) { 21 | const styles = {}; 22 | const [x, y, z, a] = matches; 23 | const formatWithoutAlpha = format[1].substring(0, 3); 24 | styles[key] = `${formatWithoutAlpha}(${x}, ${y}, ${z})`; 25 | styles[`${key}-opacity`] = a; 26 | return styles; 27 | } 28 | } 29 | return {}; 30 | } 31 | 32 | function parseStyle(key, val) { 33 | const styles = {}; 34 | if (key in STYLE_NAME_MAP) { 35 | styles[STYLE_NAME_MAP[key]] = val; 36 | } else { 37 | styles[key] = val; 38 | } 39 | 40 | return Object.assign(styles, parseColor(key, val)); 41 | } 42 | 43 | export default class SVGContext extends ContextInterface { 44 | instantiate(parentNode) { 45 | // make svg node 46 | this.instance = SVG(parentNode.id).size(this.width, this.height); 47 | this._bezierPoints = null; 48 | this._linePoints = null; 49 | } 50 | 51 | setStyles(s) { 52 | const normalizedStyles = Object.keys(s).reduce( 53 | (acc, key) => Object.assign(acc, parseStyle(key, s[key])), 54 | {} 55 | ); 56 | this.styles = Object.assign(this.styles, normalizedStyles); 57 | } 58 | 59 | line(x1, y1, x2, y2) { 60 | return this.instance 61 | .path(`M ${x1} ${y1} L ${x1} ${y1} ${x2} ${y2}`) 62 | .attr(this.styles); 63 | } 64 | 65 | ellipse(sizeX, sizeY, x, y) { 66 | return this.instance 67 | .ellipse(sizeX, sizeY) 68 | .cx(x) 69 | .cy(y) 70 | .attr(this.styles); 71 | } 72 | 73 | rect(sizeX, sizeY, x, y) { 74 | return this.instance 75 | .rect(sizeX, sizeY) 76 | .cx(x + sizeX / 2) 77 | .cy(y + sizeY / 2) 78 | .attr(this.styles); 79 | } 80 | 81 | crect(sizeX, sizeY, x, y) { 82 | return this.instance 83 | .rect(sizeX, sizeY) 84 | .cx(x) 85 | .cy(y) 86 | .attr(this.styles); 87 | } 88 | 89 | arc(x, y, radX, radY, startRadian, stopRadian) { 90 | const rotation = 0; 91 | const largeArcFlag = 0; 92 | const sweepFlag = 0; 93 | const start = polarToCartesian(radX, radY, stopRadian); 94 | const end = polarToCartesian(radX, radY, startRadian); 95 | return this.instance 96 | .path( 97 | `M ${start.x + x} ${start.y + 98 | y} A ${radX} ${radY} ${rotation} ${largeArcFlag} ${sweepFlag} ${end.x + 99 | x} ${end.y + y}` 100 | ) 101 | .attr(this.styles); 102 | } 103 | 104 | point(x, y) { 105 | return this.ellipse(0.5, 0.5, x, y); 106 | } 107 | 108 | startLine() { 109 | if (this._bezierPoints !== null) { 110 | throw Error( 111 | "startLine: tried to start a new line before closing a previous one." 112 | ); 113 | } 114 | 115 | this._linePoints = []; 116 | } 117 | 118 | endLine() { 119 | if (this._linePoints === null) { 120 | throw Error("endLine: tried to end a line before starting one."); 121 | } 122 | const [start, ...points] = this._linePoints; 123 | const { x: x1, y: y1 } = start; 124 | this.instance 125 | .path( 126 | `M ${x1} ${y1} L ${points 127 | .map(({ x, y, type }) => `${type} ${x} ${y}`) 128 | .join(" ")}` 129 | ) 130 | .attr(this.styles); 131 | this._linePoints = null; 132 | } 133 | 134 | lineVertex(x, y) { 135 | this._linePoints.push({ x, y, type: "" }); 136 | } 137 | 138 | lineMove(x, y) { 139 | this._linePoints.push({ x, y, type: "M" }); 140 | } 141 | 142 | startBezier(x, y) { 143 | // throw error if starting new bezier before closing previous 144 | if (this._bezierPoints !== null) { 145 | throw Error( 146 | "startBezier: tried to start a new bezier curve before closing a previous one." 147 | ); 148 | } 149 | 150 | this._bezierPoints = [[x, y]]; 151 | } 152 | 153 | endBezier() { 154 | if (this._bezierPoints === null) { 155 | throw Error( 156 | "endBezier: tried to end a bezier curve before starting one." 157 | ); 158 | } 159 | 160 | const curveStr = this._bezierPoints 161 | .map((p, i) => { 162 | if (i === 0) { 163 | const [x, y] = p; 164 | return `M ${x} ${y}`; 165 | } 166 | if (i === 1) { 167 | // if first control point is missing, use start point 168 | if (p.length !== 6) { 169 | const [x, y] = this._bezierPoints[i - 1]; 170 | return `C ${x} ${y} ${p.join(" ")}`; 171 | } 172 | return `C ${p.join(" ")}`; 173 | } 174 | return `S ${p.join(" ")}`; 175 | }) 176 | .join(" "); 177 | 178 | this._bezierPoints = null; // reset 179 | 180 | return this.instance.path(curveStr).attr(this.styles); 181 | } 182 | 183 | bezierVertex(...points) { 184 | const { _bezierPoints } = this; 185 | if (_bezierPoints) { 186 | if (points.length === 4 || points.length === 6) { 187 | _bezierPoints.push(points); 188 | } else { 189 | throw Error("bezierVertex: expected 4 or 6 arguments"); 190 | } 191 | } 192 | } 193 | 194 | /* eslint-disable class-methods-use-this */ 195 | draw(fn) { 196 | fn(); 197 | } 198 | 199 | getDOMElement() { 200 | return this.instance.node; 201 | } 202 | 203 | saveFileName() { 204 | return `${super.saveFileName()}.svg`; 205 | } 206 | 207 | save() { 208 | const svgEl = this.instance.node; 209 | const name = this.saveFileName(); 210 | svgEl.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 211 | const svgData = svgEl.outerHTML; 212 | const preface = '\r\n'; 213 | const svgBlob = new Blob([preface, svgData], { 214 | type: "image/svg+xml;charset=utf-8" 215 | }); 216 | const svgUrl = URL.createObjectURL(svgBlob); 217 | const downloadLink = document.createElement("a"); 218 | downloadLink.href = svgUrl; 219 | downloadLink.download = name; 220 | downloadLink.innerHTML = "download"; 221 | document.body.appendChild(downloadLink); 222 | downloadLink.click(); 223 | document.body.removeChild(downloadLink); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/helpers/dom-helpers.js: -------------------------------------------------------------------------------- 1 | const makeSaveButton = ({ label, fn, element }) => { 2 | let saveBtn; 3 | if (element) { 4 | saveBtn = element; 5 | } else { 6 | saveBtn = document.createElement("button"); 7 | saveBtn.innerHTML = label; 8 | document.body.appendChild(saveBtn); 9 | } 10 | saveBtn.addEventListener("click", fn); 11 | }; 12 | 13 | const makeContextContainer = (id = "drawing") => { 14 | const node = document.createElement("div"); 15 | node.id = id; 16 | document.body.appendChild(node); 17 | 18 | return node; 19 | }; 20 | 21 | export { makeSaveButton, makeContextContainer }; 22 | -------------------------------------------------------------------------------- /src/helpers/math.js: -------------------------------------------------------------------------------- 1 | const TWO_PI = Math.PI * 2; 2 | const THIRD_TWO_PI = TWO_PI / 3; 3 | 4 | function random(start, end) { 5 | return Math.random() * (end - start) + start; 6 | } 7 | 8 | function randomSelect(arr) { 9 | return arr[Math.floor(random(0, arr.length))]; 10 | } 11 | 12 | function radians(deg) { 13 | return (deg / 180) * Math.PI; 14 | } 15 | 16 | function polarToCartesian(rx, ry, rad) { 17 | return { 18 | x: rx * Math.cos(rad), 19 | y: ry * Math.sin(rad) 20 | }; 21 | } 22 | 23 | function translate({ x, y }, { x: xt, y: yt }) { 24 | return { 25 | x: x + xt, 26 | y: y + yt 27 | }; 28 | } 29 | 30 | function normalize({ x, y }) { 31 | const mag = dist({ x: 0, y: 0 }, { x, y }); 32 | return { x: x / mag, y: y / mag }; 33 | } 34 | 35 | function dist(a, b) { 36 | return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); 37 | } 38 | 39 | function rotate({ x, y }, theta = 0, origin) { 40 | if (origin) { 41 | return translate( 42 | rotate( 43 | translate( 44 | { x, y }, 45 | { 46 | x: -origin.x, 47 | y: -origin.y 48 | } 49 | ), 50 | theta 51 | ), 52 | origin 53 | ); 54 | } 55 | return { 56 | x: x * Math.cos(theta) - y * Math.sin(theta), 57 | y: y * Math.cos(theta) + x * Math.sin(theta) 58 | }; 59 | } 60 | 61 | function mult({ x, y }, n) { 62 | return { x: x * n, y: y * n }; 63 | } 64 | 65 | function radToChord(radius, theta = THIRD_TWO_PI) { 66 | return 2 * radius * Math.sin(theta / 2); 67 | } 68 | 69 | function chordToRad(len, theta = THIRD_TWO_PI) { 70 | return len / (2 * Math.sin(theta / 2)); 71 | } 72 | 73 | function equiTriangleHeight(len) { 74 | return Math.sqrt(len * len - Math.pow(len / 2, 2)); 75 | } 76 | export { 77 | radToChord, 78 | chordToRad, 79 | equiTriangleHeight, 80 | mult, 81 | normalize, 82 | dist, 83 | random, 84 | radians, 85 | polarToCartesian, 86 | rotate, 87 | translate, 88 | randomSelect 89 | }; 90 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "./styles/index.scss"; 2 | import { makeContextContainer, makeSaveButton } from "./helpers/dom-helpers"; 3 | import Drawing from "./components/Drawing"; 4 | import P5Context from "./contexts/P5Context"; 5 | import SVGContext from "./contexts/SVGContext"; 6 | 7 | const makeDrawing = ({ Context, width, height }) => { 8 | const parent = makeContextContainer(Context.NAME); 9 | const ctx = new Context(parent, width, height); 10 | const options = { 11 | ctx, 12 | width, 13 | height, 14 | styles: { 15 | strokeWidth: 4, 16 | stroke: "hsla(30, 40%, 50%, 0.6)" 17 | } 18 | }; 19 | const instance = new Drawing(options); 20 | instance.draw(); 21 | makeSaveButton({ 22 | label: `Save ${Context.name}`, 23 | fn: () => instance.save(), 24 | element: ctx.getDOMElement() 25 | }); 26 | }; 27 | const main = () => { 28 | const width = 300; 29 | const height = 300; 30 | 31 | makeDrawing({ Context: P5Context, width, height }); 32 | makeDrawing({ Context: SVGContext, width, height }); 33 | }; 34 | 35 | main(); 36 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | //nothing for now 2 | 3 | #my-drawing svg{ 4 | border: 1px solid #ddd; 5 | } 6 | 7 | div{ 8 | display: inline-block; 9 | border: 1px solid #ccc; 10 | } -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | devtool: 'eval-cheap-module-source-map', 7 | entry: './src/index.js', 8 | devServer: { 9 | port: 8080, 10 | contentBase: path.join(__dirname, "dist") 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | }, 19 | { 20 | test: /\.(scss|css)$/, 21 | use: [ 22 | { 23 | // creates style nodes from JS strings 24 | loader: "style-loader", 25 | options: { 26 | sourceMap: true 27 | } 28 | }, 29 | { 30 | // translates CSS into CommonJS 31 | loader: "css-loader", 32 | options: { 33 | sourceMap: true 34 | } 35 | }, 36 | { 37 | // compiles Sass to CSS 38 | loader: "sass-loader", 39 | options: { 40 | outputStyle: 'expanded', 41 | sourceMap: true, 42 | sourceMapContents: true 43 | } 44 | } 45 | // Please note we are not running postcss here 46 | ] 47 | } 48 | , 49 | { 50 | // Load all images as base64 encoding if they are smaller than 8192 bytes 51 | test: /\.(png|jpg|gif)$/, 52 | use: [ 53 | { 54 | loader: 'url-loader', 55 | options: { 56 | // On development we want to see where the file is coming from, hence we preserve the [path] 57 | name: '[path][name].[ext]?hash=[hash:20]', 58 | limit: 8192 59 | } 60 | } 61 | ] 62 | } 63 | ], 64 | }, 65 | plugins: [ 66 | new HtmlWebpackPlugin({ 67 | template: './index.html', 68 | inject: true 69 | }) 70 | ] 71 | }; 72 | --------------------------------------------------------------------------------