You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
260 lines
8.7 KiB
JavaScript
260 lines
8.7 KiB
JavaScript
4 years ago
|
import { line, solidFillPolygon, patternFillPolygon, rectangle, ellipseWithParams, generateEllipseParams, linearPath, arc, patternFillArc, curve, svgPath } from './renderer.js';
|
||
|
import { randomSeed } from './math.js';
|
||
|
import { curveToBezier } from 'points-on-curve/lib/curve-to-bezier.js';
|
||
|
import { pointsOnBezierCurves } from 'points-on-curve';
|
||
|
import { pointsOnPath } from 'points-on-path';
|
||
|
const NOS = 'none';
|
||
|
export class RoughGenerator {
|
||
|
constructor(config) {
|
||
|
this.defaultOptions = {
|
||
|
maxRandomnessOffset: 2,
|
||
|
roughness: 1,
|
||
|
bowing: 1,
|
||
|
stroke: '#000',
|
||
|
strokeWidth: 1,
|
||
|
curveTightness: 0,
|
||
|
curveFitting: 0.95,
|
||
|
curveStepCount: 9,
|
||
|
fillStyle: 'hachure',
|
||
|
fillWeight: -1,
|
||
|
hachureAngle: -41,
|
||
|
hachureGap: -1,
|
||
|
dashOffset: -1,
|
||
|
dashGap: -1,
|
||
|
zigzagOffset: -1,
|
||
|
seed: 0,
|
||
|
combineNestedSvgPaths: false,
|
||
|
disableMultiStroke: false,
|
||
|
disableMultiStrokeFill: false
|
||
|
};
|
||
|
this.config = config || {};
|
||
|
if (this.config.options) {
|
||
|
this.defaultOptions = this._o(this.config.options);
|
||
|
}
|
||
|
}
|
||
|
static newSeed() {
|
||
|
return randomSeed();
|
||
|
}
|
||
|
_o(options) {
|
||
|
return options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions;
|
||
|
}
|
||
|
_d(shape, sets, options) {
|
||
|
return { shape, sets: sets || [], options: options || this.defaultOptions };
|
||
|
}
|
||
|
line(x1, y1, x2, y2, options) {
|
||
|
const o = this._o(options);
|
||
|
return this._d('line', [line(x1, y1, x2, y2, o)], o);
|
||
|
}
|
||
|
rectangle(x, y, width, height, options) {
|
||
|
const o = this._o(options);
|
||
|
const paths = [];
|
||
|
const outline = rectangle(x, y, width, height, o);
|
||
|
if (o.fill) {
|
||
|
const points = [[x, y], [x + width, y], [x + width, y + height], [x, y + height]];
|
||
|
if (o.fillStyle === 'solid') {
|
||
|
paths.push(solidFillPolygon(points, o));
|
||
|
}
|
||
|
else {
|
||
|
paths.push(patternFillPolygon(points, o));
|
||
|
}
|
||
|
}
|
||
|
if (o.stroke !== NOS) {
|
||
|
paths.push(outline);
|
||
|
}
|
||
|
return this._d('rectangle', paths, o);
|
||
|
}
|
||
|
ellipse(x, y, width, height, options) {
|
||
|
const o = this._o(options);
|
||
|
const paths = [];
|
||
|
const ellipseParams = generateEllipseParams(width, height, o);
|
||
|
const ellipseResponse = ellipseWithParams(x, y, o, ellipseParams);
|
||
|
if (o.fill) {
|
||
|
if (o.fillStyle === 'solid') {
|
||
|
const shape = ellipseWithParams(x, y, o, ellipseParams).opset;
|
||
|
shape.type = 'fillPath';
|
||
|
paths.push(shape);
|
||
|
}
|
||
|
else {
|
||
|
paths.push(patternFillPolygon(ellipseResponse.estimatedPoints, o));
|
||
|
}
|
||
|
}
|
||
|
if (o.stroke !== NOS) {
|
||
|
paths.push(ellipseResponse.opset);
|
||
|
}
|
||
|
return this._d('ellipse', paths, o);
|
||
|
}
|
||
|
circle(x, y, diameter, options) {
|
||
|
const ret = this.ellipse(x, y, diameter, diameter, options);
|
||
|
ret.shape = 'circle';
|
||
|
return ret;
|
||
|
}
|
||
|
linearPath(points, options) {
|
||
|
const o = this._o(options);
|
||
|
return this._d('linearPath', [linearPath(points, false, o)], o);
|
||
|
}
|
||
|
arc(x, y, width, height, start, stop, closed = false, options) {
|
||
|
const o = this._o(options);
|
||
|
const paths = [];
|
||
|
const outline = arc(x, y, width, height, start, stop, closed, true, o);
|
||
|
if (closed && o.fill) {
|
||
|
if (o.fillStyle === 'solid') {
|
||
|
const shape = arc(x, y, width, height, start, stop, true, false, o);
|
||
|
shape.type = 'fillPath';
|
||
|
paths.push(shape);
|
||
|
}
|
||
|
else {
|
||
|
paths.push(patternFillArc(x, y, width, height, start, stop, o));
|
||
|
}
|
||
|
}
|
||
|
if (o.stroke !== NOS) {
|
||
|
paths.push(outline);
|
||
|
}
|
||
|
return this._d('arc', paths, o);
|
||
|
}
|
||
|
curve(points, options) {
|
||
|
const o = this._o(options);
|
||
|
const paths = [];
|
||
|
const outline = curve(points, o);
|
||
|
if (o.fill && o.fill !== NOS && points.length >= 3) {
|
||
|
const bcurve = curveToBezier(points);
|
||
|
const polyPoints = pointsOnBezierCurves(bcurve, 10, (1 + o.roughness) / 2);
|
||
|
if (o.fillStyle === 'solid') {
|
||
|
paths.push(solidFillPolygon(polyPoints, o));
|
||
|
}
|
||
|
else {
|
||
|
paths.push(patternFillPolygon(polyPoints, o));
|
||
|
}
|
||
|
}
|
||
|
if (o.stroke !== NOS) {
|
||
|
paths.push(outline);
|
||
|
}
|
||
|
return this._d('curve', paths, o);
|
||
|
}
|
||
|
polygon(points, options) {
|
||
|
const o = this._o(options);
|
||
|
const paths = [];
|
||
|
const outline = linearPath(points, true, o);
|
||
|
if (o.fill) {
|
||
|
if (o.fillStyle === 'solid') {
|
||
|
paths.push(solidFillPolygon(points, o));
|
||
|
}
|
||
|
else {
|
||
|
paths.push(patternFillPolygon(points, o));
|
||
|
}
|
||
|
}
|
||
|
if (o.stroke !== NOS) {
|
||
|
paths.push(outline);
|
||
|
}
|
||
|
return this._d('polygon', paths, o);
|
||
|
}
|
||
|
path(d, options) {
|
||
|
const o = this._o(options);
|
||
|
const paths = [];
|
||
|
if (!d) {
|
||
|
return this._d('path', paths, o);
|
||
|
}
|
||
|
d = (d || '').replace(/\n/g, ' ').replace(/(-\s)/g, '-').replace('/(\s\s)/g', ' ');
|
||
|
const hasFill = o.fill && o.fill !== 'transparent' && o.fill !== NOS;
|
||
|
const hasStroke = o.stroke !== NOS;
|
||
|
const simplified = !!(o.simplification && (o.simplification < 1));
|
||
|
const distance = simplified ? (4 - 4 * (o.simplification)) : ((1 + o.roughness) / 2);
|
||
|
const sets = pointsOnPath(d, 1, distance);
|
||
|
if (hasFill) {
|
||
|
if (o.combineNestedSvgPaths) {
|
||
|
const combined = [];
|
||
|
sets.forEach((set) => combined.push(...set));
|
||
|
if (o.fillStyle === 'solid') {
|
||
|
paths.push(solidFillPolygon(combined, o));
|
||
|
}
|
||
|
else {
|
||
|
paths.push(patternFillPolygon(combined, o));
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
sets.forEach((polyPoints) => {
|
||
|
if (o.fillStyle === 'solid') {
|
||
|
paths.push(solidFillPolygon(polyPoints, o));
|
||
|
}
|
||
|
else {
|
||
|
paths.push(patternFillPolygon(polyPoints, o));
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
if (hasStroke) {
|
||
|
if (simplified) {
|
||
|
sets.forEach((set) => {
|
||
|
paths.push(linearPath(set, false, o));
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
paths.push(svgPath(d, o));
|
||
|
}
|
||
|
}
|
||
|
return this._d('path', paths, o);
|
||
|
}
|
||
|
opsToPath(drawing) {
|
||
|
let path = '';
|
||
|
for (const item of drawing.ops) {
|
||
|
const data = item.data;
|
||
|
switch (item.op) {
|
||
|
case 'move':
|
||
|
path += `M${data[0]} ${data[1]} `;
|
||
|
break;
|
||
|
case 'bcurveTo':
|
||
|
path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `;
|
||
|
break;
|
||
|
case 'lineTo':
|
||
|
path += `L${data[0]} ${data[1]} `;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
return path.trim();
|
||
|
}
|
||
|
toPaths(drawable) {
|
||
|
const sets = drawable.sets || [];
|
||
|
const o = drawable.options || this.defaultOptions;
|
||
|
const paths = [];
|
||
|
for (const drawing of sets) {
|
||
|
let path = null;
|
||
|
switch (drawing.type) {
|
||
|
case 'path':
|
||
|
path = {
|
||
|
d: this.opsToPath(drawing),
|
||
|
stroke: o.stroke,
|
||
|
strokeWidth: o.strokeWidth,
|
||
|
fill: NOS
|
||
|
};
|
||
|
break;
|
||
|
case 'fillPath':
|
||
|
path = {
|
||
|
d: this.opsToPath(drawing),
|
||
|
stroke: NOS,
|
||
|
strokeWidth: 0,
|
||
|
fill: o.fill || NOS
|
||
|
};
|
||
|
break;
|
||
|
case 'fillSketch':
|
||
|
path = this.fillSketch(drawing, o);
|
||
|
break;
|
||
|
}
|
||
|
if (path) {
|
||
|
paths.push(path);
|
||
|
}
|
||
|
}
|
||
|
return paths;
|
||
|
}
|
||
|
fillSketch(drawing, o) {
|
||
|
let fweight = o.fillWeight;
|
||
|
if (fweight < 0) {
|
||
|
fweight = o.strokeWidth / 2;
|
||
|
}
|
||
|
return {
|
||
|
d: this.opsToPath(drawing),
|
||
|
stroke: o.fill || NOS,
|
||
|
strokeWidth: fweight,
|
||
|
fill: NOS
|
||
|
};
|
||
|
}
|
||
|
}
|