├── .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 | 
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 |
--------------------------------------------------------------------------------