Skip to content

Commit 1fc6555

Browse files
authored
Merge pull request #68 from stombeur/master
example that shows how to pen-plot arcs
2 parents 867cd18 + 1c2fd18 commit 1fc6555

File tree

2 files changed

+341
-0
lines changed

2 files changed

+341
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* A Canvas2D + SVG Pen Plotter example with circles missing a quarter segment
3+
*
4+
* @author Stephane Tombeur (https://github.com/stombeur)
5+
*/
6+
7+
const canvasSketch = require('canvas-sketch');
8+
const penplot = require('./util/penplotsvg');
9+
10+
// create an instance of SvgFile to store the svg lines and arcs
11+
const svgFile = new penplot.SvgFile();
12+
13+
const settings = {
14+
dimensions: 'A3',
15+
orientation: 'portrait',
16+
pixelsPerInch: 300,
17+
scaleToView: true,
18+
units: 'cm',
19+
};
20+
21+
const getRandomInt = (max, min = 0) => min + Math.floor(Math.random() * Math.floor(max));
22+
23+
const sketch = (context) => {
24+
let margin = 0.2;
25+
let radius = 1;
26+
let columns = 8;
27+
let rows = 14;
28+
29+
let drawingWidth = (columns * (radius * 2 + margin)) - margin;
30+
let drawingHeight = (rows * (radius * 2 + margin)) - margin;
31+
let marginLeft = (context.width - drawingWidth) / 2;
32+
let marginTop = (context.height - drawingHeight) / 2;
33+
34+
// randomize missing circle segments
35+
let o = [];
36+
for (let r = 0; r < rows; r++) {
37+
o[r] = [];
38+
for (let i = 0; i < columns; i++) {
39+
let angle = getRandomInt(4,0) * 90; // there are four segments of 90degrees in a circle
40+
o[r].push(angle);
41+
}
42+
}
43+
44+
return ({ context, width, height, units }) => {
45+
// draw an arc on the canvas and also add it to the svg file
46+
const drawArc = (cx, cy, radius, sAngle, eAngle) => {
47+
context.beginPath();
48+
context.arc(cx, cy, radius, (Math.PI / 180) * sAngle, (Math.PI / 180) * eAngle);
49+
context.stroke();
50+
51+
svgFile.addArc(cx, cy, radius, sAngle, eAngle);
52+
}
53+
54+
context.fillStyle = 'white';
55+
context.fillRect(0, 0, width, height);
56+
context.strokeStyle = 'black';
57+
context.lineWidth = 0.01;
58+
59+
let posX = marginLeft;
60+
let posY = marginTop;
61+
62+
let increments = 15; // nr of lines inside the circle
63+
let step = radius / increments;
64+
65+
for (let r = 0; r < rows; r++) {
66+
for (let c = 0; c < columns; c++) {
67+
for (let s = 0; s < (increments); s++) {
68+
// draw a 270degree arc, starting from a random 90degree segment
69+
drawArc(posX + radius, posY + radius, s * step, o[r][c], o[r][c] + 270);
70+
}
71+
posX = posX + (radius * 2) + margin;
72+
}
73+
posX = marginLeft;
74+
posY = posY + radius * 2 + margin;
75+
}
76+
77+
return [
78+
// Export PNG as first layer
79+
context.canvas,
80+
// Export SVG for pen plotter as second layer
81+
{
82+
data: svgFile.toSvg({
83+
width,
84+
height,
85+
units
86+
}),
87+
extension: '.svg',
88+
}
89+
];
90+
};
91+
};
92+
93+
canvasSketch(sketch, settings);

examples/util/penplotsvg.js

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
const defined = require('defined');
2+
const convertUnits = require('convert-units');
3+
var convert = require('convert-length');
4+
5+
// 96 DPI for SVG programs like Inkscape etc
6+
const TO_PX = 35.43307;
7+
var DEFAULT_PIXELS_PER_INCH = 90;
8+
var DEFAULT_PEN_THICKNESS = 0.03;
9+
var DEFAULT_PEN_THICKNESS_UNIT = 'cm';
10+
11+
function cm(value, unit) {
12+
return convertUnits(value)
13+
.from(unit)
14+
.to('cm');
15+
}
16+
17+
// create svg paths from polylines [[x,y], ...]
18+
function polyLinesToSvgPaths(polylines, opt = {}) {
19+
if (!opt.units || typeof opt.units !== 'string')
20+
throw new TypeError(
21+
'must specify { units } string as well as dimensions, such as: { units: "in" }'
22+
);
23+
const units = opt.units.toLowerCase();
24+
const decimalPlaces = 5;
25+
26+
let commands = [];
27+
polylines.forEach(line => {
28+
line.forEach((point, j) => {
29+
const type = j === 0 ? 'M' : 'L';
30+
const x = (TO_PX * cm(point[0], units)).toFixed(decimalPlaces);
31+
const y = (TO_PX * cm(point[1], units)).toFixed(decimalPlaces);
32+
commands.push(`${type} ${x} ${y}`);
33+
});
34+
});
35+
36+
return commands;
37+
}
38+
39+
// create svg paths from Arc objects
40+
function arcsToSvgPaths(arcs, opt = {}) {
41+
if (!opt.units || typeof opt.units !== 'string')
42+
throw new TypeError(
43+
'must specify { units } string as well as dimensions, such as: { units: "in" }'
44+
);
45+
const units = opt.units.toLowerCase();
46+
if (units === 'px')
47+
throw new Error(
48+
'px units are not yet supported by this function, your print should be defined in "cm" or "in"'
49+
);
50+
51+
let commands = [];
52+
arcs.forEach(input => {
53+
let arc = input.toSvgPixels(units);
54+
commands.push(
55+
`M${arc.startX} ${arc.startY} A${arc.radiusX},${arc.radiusY} ${
56+
arc.rotX
57+
} ${arc.largeArcFlag},${arc.sweepFlag} ${arc.endX},${arc.endY}`
58+
);
59+
});
60+
61+
return commands;
62+
}
63+
64+
// convert paths to an svg file
65+
// mostly formatting into svg-xml
66+
function pathsToSvgFile(paths, opt = {}) {
67+
opt = opt || {};
68+
69+
var width = opt.width;
70+
var height = opt.height;
71+
72+
var computeBounds =
73+
typeof width === 'undefined' || typeof height === 'undefined';
74+
if (computeBounds) {
75+
throw new Error('Must specify "width" and "height" options');
76+
}
77+
78+
var units = opt.units || 'px';
79+
80+
var convertOptions = {
81+
roundPixel: false,
82+
precision: defined(opt.precision, 5),
83+
pixelsPerInch: DEFAULT_PIXELS_PER_INCH
84+
};
85+
var svgPath = paths.join(' ');
86+
var viewWidth = convert(width, units, 'px', convertOptions).toString();
87+
var viewHeight = convert(height, units, 'px', convertOptions).toString();
88+
var fillStyle = opt.fillStyle || 'none';
89+
var strokeStyle = opt.strokeStyle || 'black';
90+
var lineWidth = opt.lineWidth;
91+
92+
// Choose a default line width based on a relatively fine-tip pen
93+
if (typeof lineWidth === 'undefined') {
94+
// Convert to user units
95+
lineWidth = convert(
96+
DEFAULT_PEN_THICKNESS,
97+
DEFAULT_PEN_THICKNESS_UNIT,
98+
units,
99+
convertOptions
100+
).toString();
101+
}
102+
103+
return [
104+
'<?xml version="1.0" standalone="no"?>',
105+
' <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ',
106+
' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
107+
' <svg width="' + width + units + '" height="' + height + units + '"',
108+
' xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 ' + viewWidth + ' ' + viewHeight + '">',
109+
' <g>',
110+
' <path d="' + svgPath + '" fill="' + fillStyle + '" stroke="' + strokeStyle + '" stroke-width="' + lineWidth + units + '" />',
111+
' </g>',
112+
'</svg>'
113+
].join('\n');
114+
}
115+
116+
// container class for the data needed to create an svg arc path
117+
class Arc {
118+
constructor() {
119+
this.startX = 0;
120+
this.startY = 0;
121+
this.endX = 0;
122+
this.endY = 0;
123+
this.radiusX = 0;
124+
this.radiusY = 0;
125+
this.rotX = 0;
126+
this.largeArcFlag = 0;
127+
this.sweepFlag = 1;
128+
}
129+
130+
// transform canvas pixels to svg
131+
toSvgPixels(units, decimalPlaces = 5) {
132+
let a = new Arc();
133+
a.startX = (TO_PX * cm(this.startX, units)).toFixed(decimalPlaces);
134+
a.startY = (TO_PX * cm(this.startY, units)).toFixed(decimalPlaces);
135+
a.endX = (TO_PX * cm(this.endX, units)).toFixed(decimalPlaces);
136+
a.endY = (TO_PX * cm(this.endY, units)).toFixed(decimalPlaces);
137+
138+
a.radiusX = (TO_PX * cm(this.radiusX, units)).toFixed(decimalPlaces);
139+
a.radiusY = (TO_PX * cm(this.radiusY, units)).toFixed(decimalPlaces);
140+
a.rotX = this.rotX;
141+
a.largeArcFlag = this.largeArcFlag;
142+
a.sweepFlag = this.sweepFlag;
143+
144+
return a;
145+
}
146+
}
147+
148+
// this class makes it easier to handle svg output
149+
// use in this order:
150+
// - new SvgFile()
151+
// - addLine or addArc or addCircle (repeat x times)
152+
// - toSvg(options)
153+
class SvgFile {
154+
constructor(options = {}) {
155+
this.lines = [];
156+
this.arcs = [];
157+
this.options = options;
158+
}
159+
160+
addLine(line) {
161+
this.lines.push(line);
162+
}
163+
164+
addCircle(cx, cy, radius) {
165+
this.arcs.push(...createCircle(cx, cy, radius));
166+
}
167+
168+
addArc(cx, cy, radius, sAngle, eAngle) {
169+
this.arcs.push(createArc(cx, cy, radius, sAngle, eAngle));
170+
}
171+
172+
toSvg(options = null){
173+
if (!options) { options = this.options; }
174+
let lineCommands = polyLinesToSvgPaths(this.lines, options);
175+
let arcCommands = arcsToSvgPaths(this.arcs, options);
176+
return pathsToSvgFile([...lineCommands, ...arcCommands], options);
177+
}
178+
}
179+
180+
// create a circle from 2 180degree arcs
181+
// (a single 360 degree arc cancels itself out in svg)
182+
function createCircle(cx, cy, radius) {
183+
let a1 = new Arc();
184+
a1.startX = cx + radius;
185+
a1.startY = cy;
186+
a1.endX = cx - radius;
187+
a1.endY = cy;
188+
a1.radiusX = radius;
189+
a1.radiusY = radius;
190+
191+
let a2 = new Arc();
192+
a2.startX = cx - radius;
193+
a2.startY = cy;
194+
a2.endX = cx + radius;
195+
a2.endY = cy;
196+
a2.radiusX = radius;
197+
a2.radiusY = radius;
198+
199+
return [a1, a2];
200+
}
201+
202+
// create an Arc object
203+
function createArc(cx, cy, radius, sAngle, eAngle) {
204+
let zeroX = cx + radius,
205+
zeroY = cy,
206+
start = rotate([zeroX, zeroY], [cx, cy], sAngle),
207+
end = rotate([zeroX, zeroY], [cx, cy], eAngle);
208+
209+
// zero = 3 o'clock like in canvas2D
210+
// to calculate the x,y for the start of the arc, we rotate [zero] around [cx,cy] for [sAngle] degrees
211+
// same for the end of the arc, but [eAngle] degrees
212+
213+
let a1 = new Arc();
214+
a1.radiusX = radius;
215+
a1.radiusY = radius;
216+
a1.startX = start[0];
217+
a1.startY = start[1];
218+
a1.endX = end[0];
219+
a1.endY = end[1];
220+
// if the arc spans >= 180 degrees, use the large-arc-flag
221+
a1.largeArcFlag = (eAngle - sAngle) >= 180 ? 1 : 0;
222+
223+
return a1;
224+
}
225+
226+
// rotate a [point] over [angle] degrees around [center]
227+
const rotate = (point, center, angle) => {
228+
if (angle === 0) return point;
229+
230+
let radians = (Math.PI / 180) * angle,
231+
x = point[0],
232+
y = point[1],
233+
cx = center[0],
234+
cy = center[1],
235+
cos = Math.cos(radians),
236+
sin = Math.sin(radians),
237+
nx = cos * (x - cx) - sin * (y - cy) + cx,
238+
ny = cos * (y - cy) + sin * (x - cx) + cy;
239+
return [nx, ny];
240+
};
241+
242+
module.exports.arcsToSvgPaths = arcsToSvgPaths;
243+
module.exports.polyLinesToSvgPaths = polyLinesToSvgPaths;
244+
module.exports.pathsToSvgFile = pathsToSvgFile;
245+
module.exports.Arc = Arc;
246+
module.exports.SvgFile = SvgFile;
247+
module.exports.createCircle = createCircle;
248+
module.exports.createArc = createArc;

0 commit comments

Comments
 (0)