├── bin
├── test.js
├── babelify.js
├── client-transform.js
├── template.js
├── file-saver.js
├── index.js
└── dev.js
├── docs
└── screenshots
│ ├── paint-1.png
│ ├── circles-1.png
│ ├── circles-2.png
│ └── screenshot.png
├── .npmignore
├── .gitignore
├── lib
├── create-canvas
│ ├── browser.js
│ └── index.js
├── node-client.js
├── run-entry.js
├── browser-client.js
└── penplot.js
├── example
├── commonjs.js
├── swirling-circles.js
├── simple-circles.js
└── generative-paint.js
├── LICENSE.md
├── util
├── svg.js
├── random.js
└── geom.js
├── package.json
└── README.md
/bin/test.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/screenshots/paint-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/penplot/HEAD/docs/screenshots/paint-1.png
--------------------------------------------------------------------------------
/docs/screenshots/circles-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/penplot/HEAD/docs/screenshots/circles-1.png
--------------------------------------------------------------------------------
/docs/screenshots/circles-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/penplot/HEAD/docs/screenshots/circles-2.png
--------------------------------------------------------------------------------
/docs/screenshots/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mattdesl/penplot/HEAD/docs/screenshots/screenshot.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | bower_components
2 | node_modules
3 | *.log
4 | .DS_Store
5 | bundle.js
6 | test
7 | test.js
8 | demo/
9 | .npmignore
10 | LICENSE.md
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components
2 | node_modules
3 | *.log
4 | .DS_Store
5 | bundle.js
6 | example/plot/
7 | !example/plot/basic.js
8 | !example/plot/commonjs.js
9 | tmp
--------------------------------------------------------------------------------
/lib/create-canvas/browser.js:
--------------------------------------------------------------------------------
1 | export default function createCanvas (width = 300, height = 150) {
2 | const canvas = document.createElement('canvas');
3 | canvas.width = width;
4 | canvas.height = height;
5 | return canvas;
6 | };
--------------------------------------------------------------------------------
/lib/node-client.js:
--------------------------------------------------------------------------------
1 | import run from './run-entry';
2 |
3 | module.exports = function (name, file, opts) {
4 | let entry;
5 | try {
6 | entry = require(file);
7 | } catch (err) {
8 | console.error(err);
9 | console.error('Could not require file:', name);
10 | process.exit(1);
11 | }
12 | run(name, entry, opts);
13 | };
14 |
15 |
16 |
--------------------------------------------------------------------------------
/lib/create-canvas/index.js:
--------------------------------------------------------------------------------
1 | export default function createCanvas (width = 300, height = 150) {
2 | let canvas;
3 | try {
4 | const Canvas = require('canvas');
5 | return canvas = new Canvas(width, height);
6 | } catch (err) {
7 | console.error(err.message);
8 | console.error('Could not install "canvas" module for use with --node option.');
9 | process.exit(1);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/example/commonjs.js:
--------------------------------------------------------------------------------
1 | /*
2 | This is an example of how to format your module if
3 | you prefer to use CommonJS.
4 | */
5 |
6 | const { PaperSize, Orientation } = require('penplot');
7 |
8 | module.exports = function createPlot (context, dimensions) {
9 | const [ width, height ] = dimensions;
10 |
11 | return {
12 | draw,
13 | animate: false,
14 | clear: true
15 | };
16 |
17 | function draw (dt = 0) {
18 | context.fillRect(0, 0, width / 2, height / 2);
19 | }
20 | };
21 |
22 | module.exports.dimensions = PaperSize.LETTER;
23 | module.exports.orientation = Orientation.LANDSCAPE;
24 |
--------------------------------------------------------------------------------
/lib/run-entry.js:
--------------------------------------------------------------------------------
1 | import penplot from 'penplot';
2 |
3 | export default function run (name, result, options) {
4 | options = options || {};
5 | if (!result) throw new Error(`Module ${name} does not export anything!`);
6 | if (result.__esModule) {
7 | // assume ES2015
8 | if (!result.default) {
9 | throw new Error(`Malformed penplot function in ${name}\nES2015 modules must export a default function.`);
10 | }
11 | const opts = Object.assign({}, result, options);
12 | delete opts.default;
13 | delete opts.__esModule;
14 | penplot(result.default, opts);
15 | } else if (typeof result === 'function') {
16 | // assume CommonJS
17 | penplot(result, Object.assign({}, result, options));
18 | } else {
19 | throw new Error(`Malformed penplot function in ${name}\nModule must be ` +
20 | 'in ES2015 or CommonJS style and should return a function.');
21 | }
22 | }
--------------------------------------------------------------------------------
/bin/babelify.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | const alias = {
5 | penplot: require.resolve('../')
6 | };
7 |
8 | // Add in all the utils
9 | const utilFolder = path.resolve(__dirname, '../util');
10 | const files = fs.readdirSync(utilFolder)
11 | .filter(f => /\.js(on)?$/i.test(f))
12 | .map(file => {
13 | const ext = /\.json$/i.test(file) ? undefined : path.extname(file);
14 | const name = path.basename(file, ext);
15 | const filePath = path.resolve(utilFolder, file);
16 | return {
17 | name: `penplot/util/${name}`,
18 | filePath: filePath
19 | };
20 | });
21 |
22 | files.forEach(file => {
23 | alias[file.name] = file.filePath;
24 | });
25 |
26 | module.exports.getTransform = function () {
27 | return require('babelify').configure(module.exports.getOptions());
28 | };
29 |
30 | module.exports.getOptions = function () {
31 | return {
32 | presets: [ require.resolve('babel-preset-es2015') ],
33 | plugins: [
34 | [ require.resolve('babel-plugin-module-resolver'), {
35 | alias
36 | } ]
37 | ]
38 | };
39 | };
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2016 Matt DesLauriers
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
20 | OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/bin/client-transform.js:
--------------------------------------------------------------------------------
1 | const through = require('through2');
2 | const path = require('path');
3 | const fs = require('fs');
4 | const maxstache = require('maxstache');
5 |
6 | module.exports = function clientTransform (clientEntry, entries) {
7 | const cwd = process.cwd();
8 |
9 | return function (file) {
10 | if (path.resolve(file) === clientEntry) {
11 | return through(undefined, end);
12 | } else {
13 | return through();
14 | }
15 | }
16 |
17 | function end () {
18 | const src = maxstache(fs.readFileSync(clientEntry, 'utf8'), {
19 | entry: generateEntryCode()
20 | });
21 |
22 | this.push(src);
23 | this.push(null);
24 | }
25 |
26 | function generateEntryCode () {
27 | const values = entries.map(entry => {
28 | const fileName = path.basename(entry, path.extname(entry));
29 | const filePath = path.relative(cwd, entry);
30 | const name = path.join(path.dirname(filePath), fileName);
31 | const file = path.resolve(entry);
32 | return ` ${JSON.stringify(name)}: require(${JSON.stringify(file)})`;
33 | }).join(',\n');
34 |
35 | return `const entries = {\n${values}\n};`;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/util/svg.js:
--------------------------------------------------------------------------------
1 | import defined from 'defined';
2 |
3 | const TO_PX = 35.43307;
4 | const DEFAULT_SVG_LINE_WIDTH = 0.03;
5 |
6 | export function polylinesToSVG (polylines, opt = {}) {
7 | const dimensions = opt.dimensions;
8 | if (!dimensions) throw new TypeError('must specify dimensions currently');
9 | const decimalPlaces = 5;
10 |
11 | let commands = [];
12 | polylines.forEach(line => {
13 | line.forEach((point, j) => {
14 | const type = (j === 0) ? 'M' : 'L';
15 | const x = (TO_PX * point[0]).toFixed(decimalPlaces);
16 | const y = (TO_PX * point[1]).toFixed(decimalPlaces);
17 | commands.push(`${type} ${x} ${y}`);
18 | });
19 | });
20 |
21 | const svgPath = commands.join(' ');
22 | const viewWidth = (dimensions[0] * TO_PX).toFixed(decimalPlaces);
23 | const viewHeight = (dimensions[1] * TO_PX).toFixed(decimalPlaces);
24 | const fillStyle = opt.fillStyle || 'none';
25 | const strokeStyle = opt.strokeStyle || 'black';
26 | const lineWidth = defined(opt.lineWidth, DEFAULT_SVG_LINE_WIDTH);
27 |
28 | return `
29 |
31 | `;
37 | }
38 |
--------------------------------------------------------------------------------
/bin/template.js:
--------------------------------------------------------------------------------
1 | import { PaperSize, Orientation } from 'penplot';
2 | import { polylinesToSVG } from 'penplot/util/svg';
3 | import { clipPolylinesToBox } from 'penplot/util/geom';
4 |
5 | export const orientation = Orientation.LANDSCAPE;
6 | export const dimensions = PaperSize.LETTER;
7 |
8 | export default function createPlot (context, dimensions) {
9 | const [ width, height ] = dimensions;
10 | let lines = [];
11 |
12 | // Draw some circles expanding outward
13 | const steps = 5;
14 | const count = 20;
15 | const spacing = 1;
16 | const radius = 2;
17 | for (let j = 0; j < count; j++) {
18 | const r = radius + j * spacing;
19 | const circle = [];
20 | for (let i = 0; i < steps; i++) {
21 | const t = i / Math.max(1, steps - 1);
22 | const angle = Math.PI * 2 * t;
23 | circle.push([
24 | width / 2 + Math.cos(angle) * r,
25 | height / 2 + Math.sin(angle) * r
26 | ]);
27 | }
28 | lines.push(circle);
29 | }
30 |
31 | // Clip all the lines to a margin
32 | const margin = 1.5;
33 | const box = [ margin, margin, width - margin, height - margin ];
34 | lines = clipPolylinesToBox(lines, box);
35 |
36 | return {
37 | draw,
38 | print,
39 | background: 'white',
40 | animate: false,
41 | clear: true
42 | };
43 |
44 | function draw () {
45 | lines.forEach(points => {
46 | context.beginPath();
47 | points.forEach(p => context.lineTo(p[0], p[1]));
48 | context.stroke();
49 | });
50 | }
51 |
52 | function print () {
53 | return polylinesToSVG(lines, {
54 | dimensions
55 | });
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/example/swirling-circles.js:
--------------------------------------------------------------------------------
1 | /*
2 | This is an example of swirling circles clipped by a 1.5 cm margin.
3 | */
4 |
5 | import { PaperSize, Orientation } from 'penplot';
6 | import { polylinesToSVG } from 'penplot/util/svg';
7 | import { clipPolylinesToBox } from 'penplot/util/geom';
8 |
9 | export const orientation = Orientation.PORTRAIT;
10 | export const dimensions = PaperSize.LETTER;
11 |
12 | export default function createPlot (context, dimensions) {
13 | const [ width, height ] = dimensions;
14 |
15 | let lines = [];
16 |
17 | // Fill the array of lines
18 | const steps = 256;
19 | const circle = [];
20 | for (let i = 0; i < steps; i++) {
21 | const t = i / Math.max(1, steps - 1);
22 | const radius = 20 * t;
23 | const swirl = 30;
24 | const angle = Math.PI * 2 * t * swirl;
25 | const x = Math.cos(angle) * radius;
26 | const y = Math.sin(angle) * radius;
27 | const cx = width / 2;
28 | const cy = height / 2;
29 | circle.push([ x + cx, y + cy ]);
30 | }
31 | lines.push(circle);
32 |
33 | // Clip all the lines to a margin
34 | const margin = 1.5;
35 | const box = [ margin, margin, width - margin, height - margin ];
36 | lines = clipPolylinesToBox(lines, box);
37 |
38 | return {
39 | draw,
40 | print,
41 | background: 'white',
42 | animate: false,
43 | clear: true
44 | };
45 |
46 | function draw () {
47 | lines.forEach(points => {
48 | context.beginPath();
49 | points.forEach(p => context.lineTo(p[0], p[1]));
50 | context.stroke();
51 | });
52 | }
53 |
54 | function print () {
55 | return polylinesToSVG(lines, {
56 | dimensions
57 | });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/example/simple-circles.js:
--------------------------------------------------------------------------------
1 | /*
2 | This is a basic example of a plot that could be sent
3 | to AxiDraw V3 and similar pen plotters.
4 | */
5 |
6 | import newArray from 'new-array';
7 |
8 | import { PaperSize, Orientation } from 'penplot';
9 | import { randomFloat, setSeed } from 'penplot/util/random';
10 | import { polylinesToSVG } from 'penplot/util/svg';
11 |
12 | setSeed(2);
13 |
14 | export const orientation = Orientation.PORTRAIT;
15 | export const dimensions = PaperSize.LETTER;
16 |
17 | export default function createPlot (context, dimensions) {
18 | const [ width, height ] = dimensions;
19 |
20 | const lineCount = 20;
21 | const segments = 500;
22 | const radius = 2;
23 |
24 | const lines = newArray(lineCount).map((_, j) => {
25 | const angleOffset = randomFloat(-Math.PI * 2, Math.PI * 2);
26 | const angleScale = randomFloat(0.001, 1);
27 |
28 | return newArray(segments).map((_, i) => {
29 | const t = i / (segments - 1);
30 | const angle = (Math.PI * 2 * t + angleOffset) * angleScale;
31 | const x = Math.cos(angle);
32 | const y = Math.sin(angle);
33 | const offset = j * 0.2;
34 | const r = radius + offset;
35 | return [ x * r + width / 2, y * r + height / 2 ];
36 | });
37 | });
38 |
39 | return {
40 | draw,
41 | print,
42 | clear: true,
43 | background: 'white'
44 | };
45 |
46 | function draw () {
47 | lines.forEach(points => {
48 | context.beginPath();
49 | points.forEach(p => context.lineTo(p[0], p[1]));
50 | context.stroke();
51 | });
52 | }
53 |
54 | function print () {
55 | return polylinesToSVG(lines, {
56 | dimensions
57 | });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/browser-client.js:
--------------------------------------------------------------------------------
1 | /*
2 | Scaffolds a penplot canvas and render loop and handles
3 | the directory listing for multiple entries.
4 | */
5 |
6 | import 'babel-polyfill';
7 | import insertCSS from 'insert-css';
8 | import run from './run-entry';
9 |
10 | function list (names) {
11 | insertCSS(`
12 | body {
13 | margin: 0;
14 | }
15 | div {
16 | margin: 20px;
17 | columns: 3;
18 | }
19 | a, a:hover, a:visited, a:active {
20 | display: block;
21 | font: 14px 'Helvetica', sans-serif;
22 | text-decoration: none;
23 | color: #89bbed;
24 | padding-bottom: 5px;
25 | }
26 | a:hover {
27 | color: #518cc6;
28 | }
29 | `)
30 | const container = document.createElement('div');
31 | names.forEach(name => {
32 | const uri = encodeURI(name);
33 | const a = document.createElement('a');
34 | a.setAttribute('href', uri);
35 | a.textContent = name;
36 | container.appendChild(a);
37 | });
38 | document.body.appendChild(container);
39 | }
40 |
41 | /*
42 | The following gets generated by our browserify transform
43 | to fill in the desired penplot entries.
44 | */
45 | {{entry}}
46 |
47 | /*
48 | Runs the current entry or provides a directory listing if necessary.
49 | */
50 | const names = Object.keys(entries);
51 | if (names.length === 1) {
52 | const name = names[0];
53 | run(name, entries[name]);
54 | } else {
55 | const path = decodeURI(location.pathname)
56 | .replace(/[\/]+$/, '')
57 | .replace(/^[\/]+/, '') || '/';
58 | if (path === '/') {
59 | list(names);
60 | } else {
61 | if (path in entries) {
62 | run(path, entries[path]);
63 | } else {
64 | console.error('no file', path)
65 | console.log(entries)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/bin/file-saver.js:
--------------------------------------------------------------------------------
1 | const downloadsFolder = require('downloads-folder');
2 | const uuid = require('uuid/v1');
3 | const path = require('path');
4 | const fs = require('fs');
5 | const moment = require('moment');
6 | const chalk = require('chalk');
7 |
8 | module.exports = function createFileSaver (opt) {
9 | const cwd = opt.cwd || process.cwd();
10 | let printOutputFolder;
11 | let isDownloads = false;
12 | if (opt.output) {
13 | printOutputFolder = opt.output;
14 | } else {
15 | isDownloads = true;
16 | printOutputFolder = downloadsFolder();
17 | }
18 |
19 | return {
20 | getFile: getFile,
21 | printDisplayPath: printDisplayPath,
22 | getDisplayPath: getDisplayPath
23 | };
24 |
25 | function printDisplayPath (filePath) {
26 | const ext = path.extname(filePath).replace(/^\./, '').toUpperCase();
27 | console.log(chalk.cyan(`‣ Saved ${ext} print to:`), chalk.bold(getDisplayPath(filePath)));
28 | }
29 |
30 | function composeFile (name, extension, number) {
31 | return (number === 0 ? name : `${name} (${number})`) + `.${extension}`;
32 | }
33 |
34 | function getFile (extension, cb) {
35 | fs.readdir(printOutputFolder, (err, files) => {
36 | if (err) {
37 | console.error(chalk.yellow(`‣ WARN`), 'Could not read folder:', chalk.bold(printOutputFolder));
38 | console.error(err);
39 | cb(path.resolve(printOutputFolder, uuid() + `.${extension}`));
40 | } else {
41 | const type = extension === 'svg' ? 'Plot' : 'Render';
42 | const date = moment().format('YYYY-MM-DD [at] h.mm.ss A');
43 | let name = `${type} - ${date}`;
44 | let number = 0;
45 | while (true) {
46 | let test = composeFile(name, extension, number);
47 | if (files.indexOf(test) >= 0) {
48 | // file already exists
49 | number++;
50 | } else {
51 | break;
52 | }
53 | }
54 | const fileName = composeFile(name, extension, number);
55 | cb(path.resolve(printOutputFolder, fileName));
56 | }
57 | });
58 | }
59 |
60 | function getDisplayPath (filePath) {
61 | return isDownloads ? filePath : path.relative(cwd, filePath);
62 | }
63 | }
--------------------------------------------------------------------------------
/bin/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const args = process.argv.slice(2);
3 | const argv = require('budo/lib/parse-args')(args);
4 | const path = require('path');
5 | const fs = require('fs');
6 | const chalk = require('chalk');
7 | const mkdirp = require('mkdirp');
8 | const glob = require('globby');
9 |
10 | if (argv._.length === 0) {
11 | console.error(chalk.red('‣ ERROR'), 'You must specify a file path:')
12 | console.error(chalk.dim(' penplot myplot.js'));
13 | process.exit(1);
14 | }
15 |
16 | const promise = argv.write ? Promise.resolve([ argv._[0] ]) : glob(argv._);
17 | promise.then(paths => {
18 | if (paths.length > 1) {
19 | console.log(chalk.cyan(`‣ Bundling ${chalk.bold(paths.length)} entries...`));
20 | start(paths);
21 | } else {
22 | const entry = paths[0];
23 | // if the file doesn't exist, stub it out
24 | isFile(entry, exists => {
25 | if (exists) {
26 | if (argv.write) {
27 | console.error(chalk.yellow('‣ WARN'), 'Ignoring --write argument, file exists:', chalk.bold(entry));
28 | }
29 | start(entry);
30 | } else if (argv.write) {
31 | console.log(chalk.cyan(`‣ Writing plot file to:`), chalk.bold(entry));
32 | const dir = path.dirname(entry);
33 | mkdirp(dir, err => {
34 | if (err) throw err;
35 | const template = fs.readFileSync(path.resolve(__dirname, 'template.js'));
36 | fs.writeFile(entry, template, function (err) {
37 | if (err) throw err;
38 | start(entry);
39 | });
40 | });
41 | } else {
42 | fileError(entry);
43 | }
44 | });
45 | }
46 | }).catch(() => {
47 | fileError(argv._[0]);
48 | });
49 |
50 | function fileError (entry) {
51 | console.error(chalk.red('‣ ERROR'), 'File does not exist:', chalk.bold(entry));
52 | console.error('\nUse --write if you want to stub a new file, for e.g.');
53 | console.error(' penplot myplot.js --write');
54 | process.exit(1);
55 | }
56 |
57 | function start (entries) {
58 | entries = Array.isArray(entries) ? entries : [ entries ];
59 |
60 | if (process.env.NODE_ENV !== 'production') {
61 | require('./dev.js')(args, argv, entries);
62 | } else {
63 | throw new Error('Build output not yet supported.');
64 | }
65 | }
66 |
67 | function isFile (file, cb) {
68 | fs.stat(file, function (err, stat) {
69 | if (!err) {
70 | if (!stat.isFile()) throw new Error(`${file} is not a file!`);
71 | return cb(true);
72 | }
73 | if (err.code === 'ENOENT') {
74 | cb(false);
75 | } else {
76 | throw err;
77 | }
78 | })
79 | }
80 |
--------------------------------------------------------------------------------
/util/random.js:
--------------------------------------------------------------------------------
1 | // utility for random features
2 |
3 | import seedRandom from 'seed-random';
4 | import SimplexNoise from 'simplex-noise';
5 |
6 | const defaultRandom = () => Math.random();
7 | let currentRandom = defaultRandom;
8 | let currentSimplex = new SimplexNoise(currentRandom);
9 |
10 | export const random = () => currentRandom();
11 |
12 | export const setSeed = (seed, opt) => {
13 | if (typeof seed === 'number' || typeof seed === 'string') {
14 | currentRandom = seedRandom(seed, opt);
15 | } else {
16 | currentRandom = defaultRandom;
17 | }
18 | currentSimplex = new SimplexNoise(currentRandom);
19 | }
20 |
21 | export const noise2D = (x, y) => currentSimplex.noise2D(x, y);
22 | export const noise3D = (x, y, z) => currentSimplex.noise3D(x, y, z);
23 | export const noise4D = (x, y, z, w) => currentSimplex.noise4D(x, y, z, w);
24 |
25 | export const randomSign = () => random() > 0.5 ? 1 : -1;
26 |
27 | export const randomFloat = (min, max) => {
28 | if (max === undefined) {
29 | max = min;
30 | min = 0;
31 | }
32 |
33 | if (typeof min !== 'number' || typeof max !== 'number') {
34 | throw new TypeError('Expected all arguments to be numbers');
35 | }
36 |
37 | return random() * (max - min) + min;
38 | };
39 |
40 | export const shuffle = (arr) => {
41 | if (!Array.isArray(arr)) {
42 | throw new TypeError('Expected Array, got ' + typeof arr);
43 | }
44 |
45 | var rand;
46 | var tmp;
47 | var len = arr.length;
48 | var ret = arr.slice();
49 | while (len) {
50 | rand = Math.floor(random() * len--);
51 | tmp = ret[len];
52 | ret[len] = ret[rand];
53 | ret[rand] = tmp;
54 | }
55 | return ret;
56 | };
57 |
58 | export const randomInt = (min, max) => {
59 | if (max === undefined) {
60 | max = min;
61 | min = 0;
62 | }
63 |
64 | if (typeof min !== 'number' || typeof max !== 'number') {
65 | throw new TypeError('Expected all arguments to be numbers');
66 | }
67 |
68 | return Math.floor(randomFloat(min, max));
69 | };
70 |
71 | // uniform distribution in a 2D circle
72 | export const randomCircle = (out, scale = 1) => {
73 | var r = random() * 2.0 * Math.PI;
74 | out[0] = Math.cos(r) * scale;
75 | out[1] = Math.sin(r) * scale;
76 | return out;
77 | };
78 |
79 | // uniform distribution in a 3D sphere
80 | export const randomSphere = (out, scale = 1) => {
81 | var r = random() * 2.0 * Math.PI;
82 | var z = (random() * 2.0) - 1.0;
83 | var zScale = Math.sqrt(1.0 - z * z) * scale;
84 | out[0] = Math.cos(r) * zScale;
85 | out[1] = Math.sin(r) * zScale;
86 | out[2] = z * scale;
87 | return out;
88 | };
89 |
90 | // uniform distribution of quaternion rotations
91 | export const randomQuaternion = (out) => {
92 | const u1 = random();
93 | const u2 = random();
94 | const u3 = random();
95 |
96 | const sq1 = Math.sqrt(1 - u1);
97 | const sq2 = Math.sqrt(u1);
98 |
99 | const theta1 = Math.PI * 2 * u2;
100 | const theta2 = Math.PI * 2 * u3;
101 |
102 | const x = Math.sin(theta1) * sq1;
103 | const y = Math.cos(theta1) * sq1;
104 | const z = Math.sin(theta2) * sq2;
105 | const w = Math.cos(theta2) * sq2;
106 | out[0] = x;
107 | out[1] = y;
108 | out[2] = z;
109 | out[3] = w;
110 | return out;
111 | };
112 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "penplot",
3 | "version": "3.0.0",
4 | "description": "a lightweight tool for generative 2D line art",
5 | "main": "./lib/penplot.js",
6 | "license": "MIT",
7 | "author": {
8 | "name": "Matt DesLauriers",
9 | "email": "dave.des@gmail.com",
10 | "url": "https://github.com/mattdesl"
11 | },
12 | "browser": {
13 | "./lib/create-canvas": "./lib/create-canvas/browser.js"
14 | },
15 | "dependencies": {
16 | "acorn": "^4.0.11",
17 | "alpha-complex": "^1.0.0",
18 | "array-almost-equal": "^1.0.0",
19 | "ast-types": "^0.9.5",
20 | "babel-plugin-module-resolver": "^2.5.0",
21 | "babel-polyfill": "^6.23.0",
22 | "babel-preset-es2015": "^6.22.0",
23 | "babel-register": "^6.24.1",
24 | "babelify": "^7.3.0",
25 | "body": "^5.1.0",
26 | "body-parser": "^1.17.1",
27 | "bound-points": "^1.0.0",
28 | "budo": "^10.0.3",
29 | "bunny": "^1.0.1",
30 | "cdt2d": "^1.0.0",
31 | "chalk": "^1.1.3",
32 | "clamp": "^1.0.1",
33 | "color-style": "^1.0.0",
34 | "defined": "^1.0.0",
35 | "delaunay-triangulate": "^1.1.6",
36 | "dom-css": "^2.1.0",
37 | "downloads-folder": "^1.0.0",
38 | "envify": "^4.1.0",
39 | "escodegen": "^1.8.1",
40 | "gl-vec2": "^1.0.0",
41 | "gl-vec3": "^1.0.3",
42 | "glob": "^7.1.1",
43 | "globby": "^6.1.0",
44 | "insert-css": "^2.0.0",
45 | "installify": "^1.1.0",
46 | "is-buffer": "^1.1.4",
47 | "is-promise": "^2.1.0",
48 | "keycode": "^2.1.8",
49 | "lerp": "^1.0.3",
50 | "lineclip": "^1.1.5",
51 | "loud-rejection": "^1.6.0",
52 | "map-limit": "0.0.1",
53 | "mapify": "^0.1.0",
54 | "maxstache": "^1.0.7",
55 | "merge-vertices": "^1.0.1",
56 | "minimist": "^1.2.0",
57 | "mkdirp": "^0.5.1",
58 | "moment": "^2.17.1",
59 | "new-array": "^1.0.0",
60 | "object-assign": "^4.1.1",
61 | "parse-unit": "^1.0.1",
62 | "point-in-triangle": "^1.0.1",
63 | "primitive-cube": "^2.0.0",
64 | "quantize-vertices": "^1.0.2",
65 | "raf-loop": "^1.1.3",
66 | "recast": "^0.11.22",
67 | "remove-orphan-vertices": "^1.0.0",
68 | "rescale-vertices": "^1.0.0",
69 | "seed-random": "^2.2.0",
70 | "simplex-noise": "^2.3.0",
71 | "smoothstep": "^1.0.1",
72 | "split-polygon": "^1.0.0",
73 | "svg-3d-simplicial-complex": "^0.1.1",
74 | "through2": "^2.0.3",
75 | "triangle-centroid": "^1.0.0",
76 | "triangle-incenter": "^1.0.2",
77 | "unreachable-branch-transform": "^0.5.1",
78 | "uuid": "^3.0.1",
79 | "vectors": "^0.1.0",
80 | "vertices-bounding-box": "^1.0.0",
81 | "xhr": "^2.4.0"
82 | },
83 | "devDependencies": {
84 | "color-convert": "^1.9.0",
85 | "css-color-converter": "^1.1.0",
86 | "draw-triangles-2d": "^1.0.0",
87 | "icosphere": "^1.0.0",
88 | "nice-color-palettes": "^2.0.0",
89 | "perspective-camera": "^2.0.1"
90 | },
91 | "scripts": {
92 | "test": "node test.js"
93 | },
94 | "keywords": [
95 | "pen",
96 | "plot",
97 | "plotter",
98 | "plotterart",
99 | "ink",
100 | "svg",
101 | "2d",
102 | "makeblock",
103 | "xy",
104 | "axidraw",
105 | "axidrawv3",
106 | "evil",
107 | "mad",
108 | "scientist"
109 | ],
110 | "repository": {
111 | "type": "git",
112 | "url": "git://github.com/mattdesl/penplot.git"
113 | },
114 | "homepage": "https://github.com/mattdesl/penplot",
115 | "bugs": {
116 | "url": "https://github.com/mattdesl/penplot/issues"
117 | },
118 | "bin": {
119 | "penplot": "./bin/index.js"
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/bin/dev.js:
--------------------------------------------------------------------------------
1 | require('loud-rejection')();
2 |
3 | const assign = require('object-assign');
4 | const budo = require('budo');
5 | const bodyParser = require('body-parser');
6 | const path = require('path');
7 | const fs = require('fs');
8 | const createClientTransform = require('./client-transform');
9 | const createEnvify = require('envify/custom');
10 |
11 | const babelify = require('./babelify');
12 | const installify = require('installify');
13 | const unreachableBranch = require('unreachable-branch-transform');
14 | const createFileSaver = require('./file-saver');
15 |
16 | module.exports = dev;
17 | function dev (args, argv, entries) {
18 | const cwd = process.cwd();
19 | const fileSaver = createFileSaver({
20 | output: argv.output,
21 | cwd: cwd
22 | });
23 |
24 | const envVars = {};
25 | const isNode = argv.node;
26 | if (isNode) {
27 | process.env.IS_NODE = envVars.IS_NODE = '1';
28 | throw new Error('The --node option has been removed in penplot@3.x');
29 | }
30 |
31 | if (isNode) {
32 | require('babel-register')(babelify.getOptions());
33 | if (entries.length > 1 || entries.length === 0) {
34 | throw new Error('The --node option only supports a single entry right now.');
35 | }
36 |
37 | const isStdout = argv.stdout;
38 | const entryName = entries[0];
39 | const entryFile = path.resolve(cwd, entries[0]);
40 | const opts = {
41 | onComplete: (context) => {
42 | const runSave = stream => {
43 | context.canvas.pngStream().pipe(stream);
44 | };
45 |
46 | if (isStdout) {
47 | runSave(process.stdout);
48 | } else {
49 | fileSaver.getFile('png', (filePath) => {
50 | const outStream = fs.createWriteStream(filePath);
51 | outStream.on('close', () => {
52 | fileSaver.printDisplayPath(filePath);
53 | });
54 | runSave(outStream);
55 | });
56 | }
57 | }
58 | };
59 | require('../lib/node-client.js')(entryName, entryFile, opts);
60 | } else {
61 | // replace entry with our own client
62 | const clientEntry = path.resolve(__dirname, '../lib/browser-client.js');
63 | argv._ = [ clientEntry ];
64 |
65 | const generateClient = createClientTransform(clientEntry, entries);
66 | const transforms = [
67 | generateClient,
68 | babelify.getTransform(),
69 | createEnvify(envVars),
70 | unreachableBranch
71 | ];
72 |
73 | if (argv['auto-install']) {
74 | transforms.push([ installify, { save: true } ]);
75 | }
76 |
77 | const opts = assign({}, argv, {
78 | title: 'penplot',
79 | live: true,
80 | pushstate: true,
81 | base: '/',
82 | browserify: {
83 | transform: transforms
84 | },
85 | middleware: [
86 | bodyParser.json({
87 | limit: '1gb'
88 | }),
89 | middleware
90 | ],
91 | serve: 'bundle.js'
92 | });
93 |
94 | budo.cli(args, opts);
95 | }
96 |
97 | function middleware (req, res, next) {
98 | if (req.url === '/print') {
99 | fileSaver.getFile('svg', file => svg(file, req, res));
100 | } else if (req.url === '/save') {
101 | fileSaver.getFile('png', file => png(file, req, res));
102 | } else {
103 | next();
104 | }
105 | }
106 |
107 | function svg (filePath, req, res) {
108 | if (!req.body || !req.body.svg) {
109 | res.writeHead(400, 'missing SVG in print()');
110 | res.end();
111 | }
112 | fs.writeFile(filePath, req.body.svg, function (err) {
113 | if (err) {
114 | console.error(err);
115 | res.writeHead(400, err.message);
116 | return res.end();
117 | }
118 | fileSaver.printDisplayPath(filePath);
119 | res.writeHead(200, 'ok');
120 | res.end();
121 | });
122 | }
123 |
124 | function png (filePath, req, res) {
125 | if (!req.body || !req.body.data) {
126 | res.writeHead(400, 'missing base64 data for save function');
127 | res.end();
128 | }
129 | const data = Buffer.from(req.body.data, 'base64');
130 | fs.writeFile(filePath, data, function (err) {
131 | if (err) {
132 | console.error(err);
133 | res.writeHead(400, err.message);
134 | return res.end();
135 | }
136 | fileSaver.printDisplayPath(filePath);
137 | res.writeHead(200, 'ok');
138 | res.end();
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/example/generative-paint.js:
--------------------------------------------------------------------------------
1 | /*
2 | This is a more advanced example of a generative/algorithmic
3 | print created with `penplot` and Canvas2D. It mostly consists of
4 | concentric circles with different arc lengths and a lot of
5 | random offsets.
6 | */
7 |
8 | import newArray from 'new-array';
9 | import clamp from 'clamp';
10 | import { PaperSize, Orientation } from 'penplot';
11 | import * as RND from 'penplot/util/random';
12 | import allPalettes from 'nice-color-palettes/500';
13 | import colorConvert from 'color-convert';
14 | import fromCSS from 'css-color-converter';
15 |
16 | // often it's handy to mark down a nice seed so you can re-print it later
17 | // at a different size
18 | const seed = '42032';
19 |
20 | // other times you may want to idly browse through random seeds
21 | // const seed = String(Math.floor(Math.random() * 100000));
22 |
23 | // check the console for the seed number :)
24 | console.log('Seed:', seed);
25 |
26 | RND.setSeed(seed);
27 |
28 | const palettes = [
29 | [ 'hsl(0, 0%, 85%)', 'hsl(0, 0%, 95%)' ],
30 | RND.shuffle(RND.shuffle(allPalettes)[0]).slice(0, 3)
31 | ];
32 |
33 | export const orientation = Orientation.PORTRAIT;
34 | export const dimensions = PaperSize.SKETCHBOOK;
35 |
36 | export default function createPlot (context, dimensions) {
37 | const [ width, height ] = dimensions;
38 |
39 | const lineCount = 300;
40 | const segments = 2000;
41 |
42 | const corePalette = palettes[0];
43 | const altPalette = palettes[1];
44 | const allPoints = [];
45 |
46 | const lines = newArray(lineCount).map((_, j) => {
47 | const radius = 0;
48 | const angleOffset = RND.randomFloat(-Math.PI * 2, Math.PI * 2);
49 | const angleScale = RND.randomFloat(0.01, 0.01);
50 | const pal = (RND.randomFloat(1) > 0.75) ? altPalette : corePalette;
51 |
52 | const startColor = pal[RND.randomInt(pal.length)];
53 | const hsl = colorConvert.hex.hsl(fromCSS(startColor).toHexString().replace(/^#/, ''));
54 | const color = startColor;
55 |
56 | // only modify the color palettes
57 | const isHSLMod = pal === altPalette;
58 |
59 | return {
60 | lineWidth: RND.randomFloat(1) > 0.25 ? RND.randomFloat(0.01, 0.05) : RND.randomFloat(0.01, 4),
61 | color,
62 | hsl,
63 | isHSLMod,
64 | alpha: RND.randomFloat(0.15, 0.75),
65 | points: newArray(segments).map((_, i) => {
66 | const t = i / (segments - 1);
67 | const K = j / (lineCount - 1);
68 | const angleOff = RND.noise2D(K * 1, t * 200) * 0.1;
69 | const angle = (Math.PI * 2 * t) * angleScale + angleOff + angleOffset;
70 | const x = Math.cos(angle);
71 | const y = Math.sin(angle);
72 | const offset = j * (0.2 + RND.randomFloat(-1, RND.randomFloat(0, 5)) * 0.1) * 0.5;
73 | const r = radius + offset;
74 | const center = RND.randomCircle([], 0.01)
75 | const point = [ x * r + width / 2 + center[0], y * r + height / 2 + center[1] ];
76 | const f = 10;
77 | const amp = 0.005;
78 | point[0] += RND.noise2D(f * point[0], f * point[1], f * 1000) * amp;
79 | point[1] += RND.noise2D(f * point[0], f * point[1], f * -1000) * amp;
80 |
81 | const newColor = isHSLMod
82 | ? `#${colorConvert.hsl.hex(offsetLightness(hsl, RND.randomFloat(-1, 1) * 10))}`
83 | : startColor;
84 | allPoints.push(point);
85 | return {
86 | position: point,
87 | color: newColor
88 | };
89 | })
90 | };
91 | });
92 |
93 | return {
94 | draw,
95 | outputSize: '300 dpi', // render as print resolution instead of web
96 | background: corePalette[0]
97 | };
98 |
99 | function offsetLightness (hsl, l) {
100 | hsl = hsl.slice();
101 | hsl[2] += l;
102 | hsl[2] = clamp(hsl[2], 0, 100);
103 | return hsl;
104 | }
105 |
106 | function draw () {
107 | lines.forEach(line => {
108 | // render each line as small segments so we get overlapping
109 | // opacities
110 | for (let i = 0; i < line.points.length / 2; i++) {
111 | context.beginPath();
112 | context.globalAlpha = line.alpha;
113 | context.lineWidth = line.lineWidth;
114 | context.lineJoin = 'round';
115 | context.lineCap = 'square';
116 | context.strokeStyle = line.points[i * 2 + 0].color;
117 | const [ x1, y1 ] = line.points[i * 2 + 0].position;
118 | const [ x2, y2 ] = line.points[i * 2 + 1].position;
119 | context.lineTo(x1, y1);
120 | context.lineTo(x2, y2);
121 | context.stroke();
122 | }
123 | });
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # penplot
2 |
3 | [](http://github.com/badges/stability-badges)
4 |
5 | An experimental and highly opinionated development environment for generative and pen plotter art.
6 |
7 | Some features:
8 |
9 | - Zero configuration: just run a command and start writing `