import { createRenderer } from './renderer-factory.js'; const hasSelf = typeof self !== 'undefined'; export class RoughGenerator { constructor(config, surface) { this.defaultOptions = { maxRandomnessOffset: 2, roughness: 1, bowing: 1, stroke: '#000', strokeWidth: 1, curveTightness: 0, curveStepCount: 9, fill: null, fillStyle: 'hachure', fillWeight: -1, hachureAngle: -41, hachureGap: -1 }; this.config = config || {}; this.surface = surface; this.renderer = createRenderer(this.config); if (this.config.options) { this.defaultOptions = this._options(this.config.options); } } _options(options) { return options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions; } _drawable(shape, sets, options) { return { shape, sets: sets || [], options: options || this.defaultOptions }; } get lib() { return this.renderer; } getCanvasSize() { const val = (w) => { if (w && typeof w === 'object') { if (w.baseVal && w.baseVal.value) { return w.baseVal.value; } } return w || 100; }; if (this.surface) { return [val(this.surface.width), val(this.surface.height)]; } return [100, 100]; } computePolygonSize(points) { if (points.length) { let left = points[0][0]; let right = points[0][0]; let top = points[0][1]; let bottom = points[0][1]; for (let i = 1; i < points.length; i++) { left = Math.min(left, points[i][0]); right = Math.max(right, points[i][0]); top = Math.min(top, points[i][1]); bottom = Math.max(bottom, points[i][1]); } return [(right - left), (bottom - top)]; } return [0, 0]; } polygonPath(points) { let d = ''; if (points.length) { d = `M${points[0][0]},${points[0][1]}`; for (let i = 1; i < points.length; i++) { d = `${d} L${points[i][0]},${points[i][1]}`; } } return d; } computePathSize(d) { let size = [0, 0]; if (hasSelf && self.document) { try { const ns = 'http://www.w3.org/2000/svg'; const svg = self.document.createElementNS(ns, 'svg'); svg.setAttribute('width', '0'); svg.setAttribute('height', '0'); const pathNode = self.document.createElementNS(ns, 'path'); pathNode.setAttribute('d', d); svg.appendChild(pathNode); self.document.body.appendChild(svg); const bb = pathNode.getBBox(); if (bb) { size[0] = bb.width || 0; size[1] = bb.height || 0; } self.document.body.removeChild(svg); } catch (err) { } } const canvasSize = this.getCanvasSize(); if (!(size[0] * size[1])) { size = canvasSize; } size[0] = Math.min(size[0], canvasSize[0]); size[1] = Math.min(size[1], canvasSize[1]); return size; } line(x1, y1, x2, y2, options) { const o = this._options(options); return this._drawable('line', [this.lib.line(x1, y1, x2, y2, o)], o); } rectangle(x, y, width, height, options) { const o = this._options(options); const paths = []; if (o.fill) { const points = [[x, y], [x + width, y], [x + width, y + height], [x, y + height]]; if (o.fillStyle === 'solid') { paths.push(this.lib.solidFillPolygon(points, o)); } else { paths.push(this.lib.patternFillPolygon(points, o)); } } paths.push(this.lib.rectangle(x, y, width, height, o)); return this._drawable('rectangle', paths, o); } ellipse(x, y, width, height, options) { const o = this._options(options); const paths = []; if (o.fill) { if (o.fillStyle === 'solid') { const shape = this.lib.ellipse(x, y, width, height, o); shape.type = 'fillPath'; paths.push(shape); } else { paths.push(this.lib.patternFillEllipse(x, y, width, height, o)); } } paths.push(this.lib.ellipse(x, y, width, height, o)); return this._drawable('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._options(options); return this._drawable('linearPath', [this.lib.linearPath(points, false, o)], o); } arc(x, y, width, height, start, stop, closed = false, options) { const o = this._options(options); const paths = []; if (closed && o.fill) { if (o.fillStyle === 'solid') { const shape = this.lib.arc(x, y, width, height, start, stop, true, false, o); shape.type = 'fillPath'; paths.push(shape); } else { paths.push(this.lib.patternFillArc(x, y, width, height, start, stop, o)); } } paths.push(this.lib.arc(x, y, width, height, start, stop, closed, true, o)); return this._drawable('arc', paths, o); } curve(points, options) { const o = this._options(options); return this._drawable('curve', [this.lib.curve(points, o)], o); } polygon(points, options) { const o = this._options(options); const paths = []; if (o.fill) { if (o.fillStyle === 'solid') { paths.push(this.lib.solidFillPolygon(points, o)); } else { const size = this.computePolygonSize(points); const fillPoints = [ [0, 0], [size[0], 0], [size[0], size[1]], [0, size[1]] ]; const shape = this.lib.patternFillPolygon(fillPoints, o); shape.type = 'path2Dpattern'; shape.size = size; shape.path = this.polygonPath(points); paths.push(shape); } } paths.push(this.lib.linearPath(points, true, o)); return this._drawable('polygon', paths, o); } path(d, options) { const o = this._options(options); const paths = []; if (!d) { return this._drawable('path', paths, o); } if (o.fill) { if (o.fillStyle === 'solid') { const shape = { type: 'path2Dfill', path: d, ops: [] }; paths.push(shape); } else { const size = this.computePathSize(d); const points = [ [0, 0], [size[0], 0], [size[0], size[1]], [0, size[1]] ]; const shape = this.lib.patternFillPolygon(points, o); shape.type = 'path2Dpattern'; shape.size = size; shape.path = d; paths.push(shape); } } paths.push(this.lib.svgPath(d, o)); return this._drawable('path', paths, o); } 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: 'none' }; break; case 'fillPath': path = { d: this.opsToPath(drawing), stroke: 'none', strokeWidth: 0, fill: o.fill || 'none' }; break; case 'fillSketch': path = this.fillSketch(drawing, o); break; case 'path2Dfill': path = { d: drawing.path || '', stroke: 'none', strokeWidth: 0, fill: o.fill || 'none' }; break; case 'path2Dpattern': { const size = drawing.size; const pattern = { x: 0, y: 0, width: 1, height: 1, viewBox: `0 0 ${Math.round(size[0])} ${Math.round(size[1])}`, patternUnits: 'objectBoundingBox', path: this.fillSketch(drawing, o) }; path = { d: drawing.path, stroke: 'none', strokeWidth: 0, pattern: pattern }; 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 || 'none', strokeWidth: fweight, fill: 'none' }; } 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 'qcurveTo': path += `Q${data[0]} ${data[1]}, ${data[2]} ${data[3]} `; break; case 'lineTo': path += `L${data[0]} ${data[1]} `; break; } } return path.trim(); } }