├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── canvas.ts ├── core.ts ├── fillers │ ├── dashed-filler.ts │ ├── dot-filler.ts │ ├── filler-interface.ts │ ├── filler.ts │ ├── hachure-filler.ts │ ├── hatch-filler.ts │ ├── scan-line-hachure.ts │ ├── zigzag-filler.ts │ └── zigzag-line-filler.ts ├── generator.ts ├── geometry.ts ├── math.ts ├── renderer.ts ├── rough.ts └── svg.ts ├── tsconfig.json └── visual-tests ├── canvas ├── arc.html ├── arc2.html ├── curve-seed.html ├── curve.html ├── curve2.html ├── curve3.html ├── curve4.html ├── dashed │ ├── arc.html │ ├── curve.html │ ├── ellipse.html │ ├── line.html │ ├── linearpath.html │ ├── path-with-transform.html │ ├── path.html │ ├── polygon.html │ └── rectangle.html ├── ellipse.html ├── ellipse2.html ├── ellipse3.html ├── line.html ├── linearpath.html ├── map.html ├── path-with-transform.html ├── path.html ├── path2.html ├── path3.html ├── path4.html ├── path5.html ├── path6.html ├── path7.html ├── poly-seed.html ├── polygon.html ├── polygon2.html ├── rectangle.html ├── singlestroke │ ├── arc.html │ ├── curve.html │ ├── ellipse.html │ ├── line.html │ ├── path.html │ ├── polygon.html │ └── rectangle.html └── us.json └── svg ├── dashed ├── ellipse.html ├── line.html ├── polygon.html └── rectangle.html ├── ellipse.html ├── line.html ├── polygon.html ├── rectangle.html └── rectangle2.html /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/eslint-recommended", 5 | "plugin:@typescript-eslint/recommended" 6 | ], 7 | "plugins": [ 8 | "@typescript-eslint" 9 | ], 10 | "rules": { 11 | "arrow-parens": [ 12 | "error", 13 | "always" 14 | ], 15 | "prefer-const": "error", 16 | "no-eval": "error", 17 | "no-trailing-spaces": "error", 18 | "no-var": "error", 19 | "quotes": [ 20 | "error", 21 | "single", 22 | { 23 | "allowTemplateLiterals": true 24 | } 25 | ], 26 | "semi": "error", 27 | "comma-dangle": [ 28 | "error", 29 | "always-multiline" 30 | ], 31 | "eqeqeq": "error", 32 | "no-useless-escape": "off", 33 | "@typescript-eslint/indent": [ 34 | "error", 35 | 2 36 | ], 37 | "@typescript-eslint/no-inferrable-types": "off", 38 | "@typescript-eslint/no-unused-vars": "error" 39 | } 40 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: pshihn 2 | open_collective: rough 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | node_modules 4 | z 5 | bin 6 | dist 7 | bundled 8 | bug.html -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | z 4 | node_modules 5 | src 6 | tslint.json 7 | rollup.config.js 8 | .gitignore 9 | visual-tests -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | # [4.5.0] - 2021-05-09 6 | * Better algorithm for nested and intersecting paths https://github.com/rough-stuff/rough/issues/183 7 | * Improved zigzag fill for concave shapes and nested paths. 8 | * Fixed "dots" fill when roughness was <1 Itw as creatings weird shapes https://github.com/rough-stuff/rough/issues/193 9 | * Configure precision when rendering to canvas as well as SVG using `fixedDecimalPlaceDigits` property. 10 | * Solid fill was broken for Arcs if arc angle was > 180 degrees 11 | * Remove notch from ellipses when roughness = 0 12 | 13 | 14 | # [4.4.0] - 2021-05-09 15 | 16 | * Added `preserveVertices` option when drawing shapes. Especially useful in paths. When rendering a shape, the vertices or the end points of the shape are not randomized if this is set to TRUE. This allows connected segments to always be connected. 17 | 18 | ## [4.3.0] - 2020-05-11 19 | 20 | * Added options to draw dashed lines - *strokeLineDash, strokeLineDashOffset, fillLineDash, fillLineDashOffset* 21 | * Added option to disable double stroking effect - *disableMultiStroke, disableMultiStrokeFill*. 22 | * Bug fixes to solid fill in SVG which was not obeying evenodd rules by default 23 | 24 | ## [4.1.0] - 2020-01-13 25 | 26 | * Added ability to **fill** non-svg curves 27 | 28 | ## [4.0.0] - 2020-01-13 29 | 30 | * Add optional seeding for randomness to ensure shapes generated with same arguments result in same vectors 31 | * Implemented a new algorithm for hachure generation based on scanlines. Smaller in code size, and about 20% faster 32 | * Algorithm update - adjust shape randomness and curve-step-counts based on the size of the shape 33 | * Removed async/worker builds - can be achieved in the app level, so no need to be in the lib 34 | * Support no-stroke sketching. `stroke: "none"` will not generate outline vectors anymore 35 | * Removed `sunburst` fill style - it had a lot of corner cases where it did not work, and not very popular. 36 | 37 | ## [3.1.0] - 2019-03-14 38 | 39 | * Added three new fill styles: **sunburst**, **dashed**, and **zigzag-line** 40 | * Added three new properties in *Options* to support these fill styles: 41 | * **dashOffset** - length of dashes in dashed fill 42 | * **dashGap** - length of gap between dashes in dashed fill 43 | * **zigzagOffset** - width of zigzag triangle when using zigzag-lines fill 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Preet Shihn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rough.js 2 | 3 | Rough.js is a small (\<9 kB) graphics library that lets you draw in a _sketchy_, _hand-drawn-like_, style. 4 | The library defines primitives to draw lines, curves, arcs, polygons, circles, and ellipses. It also supports drawing [SVG paths](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths). 5 | 6 | Rough.js works with both [Canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) and [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG). 7 | 8 | ![Rough.js sample](https://roughjs.com/images/cap_demo.png) 9 | 10 | [@RoughLib](https://twitter.com/RoughLib) on Twitter. 11 | 12 | ## Install 13 | 14 | from npm: 15 | 16 | ``` 17 | npm install --save roughjs 18 | ``` 19 | 20 | Or get the latest using unpkg: https://unpkg.com/roughjs@latest/bundled/rough.js 21 | 22 | 23 | If you are looking for bundled version in different formats, the npm package will have these in the following locations: 24 | 25 | CommonJS: `roughjs/bundled/rough.cjs.js` 26 | 27 | ESM: `roughjs/bundled/rough.esm.js` 28 | 29 | Browser IIFE: `roughjs/bundled/rough.js` 30 | 31 | 32 | ## Usage 33 | 34 | ![Rough.js rectangle](https://roughjs.com/images/m1.png) 35 | 36 | ```js 37 | const rc = rough.canvas(document.getElementById('canvas')); 38 | rc.rectangle(10, 10, 200, 200); // x, y, width, height 39 | ``` 40 | 41 | or SVG 42 | 43 | ```js 44 | const rc = rough.svg(svg); 45 | let node = rc.rectangle(10, 10, 200, 200); // x, y, width, height 46 | svg.appendChild(node); 47 | ``` 48 | 49 | ### Lines and Ellipses 50 | 51 | ![Rough.js rectangle](https://roughjs.com/images/m2.png) 52 | 53 | ```js 54 | rc.circle(80, 120, 50); // centerX, centerY, diameter 55 | rc.ellipse(300, 100, 150, 80); // centerX, centerY, width, height 56 | rc.line(80, 120, 300, 100); // x1, y1, x2, y2 57 | ``` 58 | 59 | ### Filling 60 | 61 | ![Rough.js rectangle](https://roughjs.com/images/m3.png) 62 | 63 | ```js 64 | rc.circle(50, 50, 80, { fill: 'red' }); // fill with red hachure 65 | rc.rectangle(120, 15, 80, 80, { fill: 'red' }); 66 | rc.circle(50, 150, 80, { 67 | fill: "rgb(10,150,10)", 68 | fillWeight: 3 // thicker lines for hachure 69 | }); 70 | rc.rectangle(220, 15, 80, 80, { 71 | fill: 'red', 72 | hachureAngle: 60, // angle of hachure, 73 | hachureGap: 8 74 | }); 75 | rc.rectangle(120, 105, 80, 80, { 76 | fill: 'rgba(255,0,200,0.2)', 77 | fillStyle: 'solid' // solid fill 78 | }); 79 | ``` 80 | 81 | Fill styles can be: **hachure**(default), **solid**, **zigzag**, **cross-hatch**, **dots**, **dashed**, or **zigzag-line** 82 | 83 | ![Rough.js fill examples](https://roughjs.com/images/m14.png) 84 | 85 | ### Sketching style 86 | 87 | ![Rough.js rectangle](https://roughjs.com/images/m4.png) 88 | 89 | ```js 90 | rc.rectangle(15, 15, 80, 80, { roughness: 0.5, fill: 'red' }); 91 | rc.rectangle(120, 15, 80, 80, { roughness: 2.8, fill: 'blue' }); 92 | rc.rectangle(220, 15, 80, 80, { bowing: 6, stroke: 'green', strokeWidth: 3 }); 93 | ``` 94 | 95 | ### SVG Paths 96 | 97 | ![Rough.js paths](https://roughjs.com/images/m5.png) 98 | 99 | ```js 100 | rc.path('M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z', { fill: 'green' }); 101 | rc.path('M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z', { fill: 'purple' }); 102 | rc.path('M80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z', { fill: 'red' }); 103 | rc.path('M230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z', { fill: 'blue' }); 104 | ``` 105 | 106 | SVG Path with simplification: 107 | 108 | ![Rough.js texas map](https://roughjs.com/images/m9.png) ![Rough.js texas map](https://roughjs.com/images/m10.png) 109 | 110 | ## Examples 111 | 112 | ![Rough.js US map](https://roughjs.com/images/m6.png) 113 | 114 | [View examples here](https://github.com/pshihn/rough/wiki/Examples) 115 | 116 | ## API & Documentation 117 | 118 | [Full Rough.js API](https://github.com/pshihn/rough/wiki) 119 | 120 | ## Credits 121 | 122 | Some of the core algorithms were adapted from [handy](https://www.gicentre.net/software/#/handy/) processing lib. 123 | 124 | Algorithm to convert SVG arcs to Canvas [described here](https://www.w3.org/TR/SVG/implnote.html) was adapted from [Mozilla codebase](https://hg.mozilla.org/mozilla-central/file/17156fbebbc8/content/svg/content/src/nsSVGPathDataParser.cpp#l887) 125 | 126 | ## Contributors 127 | 128 | ### Financial Contributors 129 | 130 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/rough/contribute)] 131 | 132 | #### Individuals 133 | 134 | 135 | 136 | #### Organizations 137 | 138 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/rough/contribute)] 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | ## License 155 | [MIT License](https://github.com/pshihn/rough/blob/master/LICENSE) (c) [Preet Shihn](https://twitter.com/preetster) 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roughjs", 3 | "version": "4.6.6", 4 | "description": "Create graphics using HTML Canvas or SVG with a hand-drawn, sketchy, appearance.", 5 | "main": "bundled/rough.cjs.js", 6 | "module": "bundled/rough.esm.js", 7 | "types": "bin/rough.d.ts", 8 | "scripts": { 9 | "build": "rm -rf bin && tsc && rollup -c", 10 | "lint": "eslint --ext ts src", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/pshihn/rough.git" 16 | }, 17 | "keywords": [ 18 | "canvas", 19 | "svg", 20 | "graphics", 21 | "sketchy", 22 | "hand drawn", 23 | "hand-drawn" 24 | ], 25 | "author": "Preet Shihn ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/pshihn/rough/issues" 29 | }, 30 | "homepage": "https://roughjs.com", 31 | "devDependencies": { 32 | "@rollup/plugin-node-resolve": "^13.0.6", 33 | "@rollup/plugin-typescript": "^8.3.0", 34 | "@typescript-eslint/eslint-plugin": "^4.33.0", 35 | "@typescript-eslint/parser": "^4.33.0", 36 | "eslint": "^7.32.0", 37 | "rollup": "^2.61.0", 38 | "rollup-plugin-terser": "^7.0.2", 39 | "tslib": "^2.3.1", 40 | "typescript": "^4.5.3" 41 | }, 42 | "dependencies": { 43 | "hachure-fill": "^0.5.2", 44 | "path-data-parser": "^0.1.0", 45 | "points-on-curve": "^0.2.0", 46 | "points-on-path": "^0.2.1" 47 | } 48 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import { terser } from "rollup-plugin-terser"; 3 | import typescript from '@rollup/plugin-typescript'; 4 | 5 | const input = 'bin/rough.js'; 6 | 7 | export default [ 8 | { 9 | input, 10 | output: { 11 | file: 'bundled/rough.js', 12 | format: 'iife', 13 | name: 'rough' 14 | }, 15 | plugins: [nodeResolve(), terser({ 16 | output: { 17 | comments: false 18 | } 19 | })] 20 | }, 21 | { 22 | input, 23 | output: { 24 | file: 'bundled/rough.esm.js', 25 | format: 'esm' 26 | }, 27 | plugins: [nodeResolve(), terser({ 28 | output: { 29 | comments: false 30 | } 31 | })] 32 | }, 33 | { 34 | input: 'src/rough.ts', 35 | output: { 36 | file: 'bundled/rough.cjs.js', 37 | format: 'cjs' 38 | }, 39 | plugins: [nodeResolve(), typescript({ target: "es5", importHelpers: true }), terser({ 40 | output: { 41 | comments: false 42 | } 43 | })] 44 | } 45 | ]; -------------------------------------------------------------------------------- /src/canvas.ts: -------------------------------------------------------------------------------- 1 | import { Config, Options, ResolvedOptions, Drawable, OpSet } from './core'; 2 | import { RoughGenerator } from './generator'; 3 | import { Point } from './geometry'; 4 | 5 | export class RoughCanvas { 6 | private gen: RoughGenerator; 7 | private canvas: HTMLCanvasElement; 8 | private ctx: CanvasRenderingContext2D; 9 | 10 | constructor(canvas: HTMLCanvasElement, config?: Config) { 11 | this.canvas = canvas; 12 | this.ctx = this.canvas.getContext('2d')!; 13 | this.gen = new RoughGenerator(config); 14 | } 15 | 16 | draw(drawable: Drawable): void { 17 | const sets = drawable.sets || []; 18 | const o = drawable.options || this.getDefaultOptions(); 19 | const ctx = this.ctx; 20 | const precision = drawable.options.fixedDecimalPlaceDigits; 21 | for (const drawing of sets) { 22 | switch (drawing.type) { 23 | case 'path': 24 | ctx.save(); 25 | ctx.strokeStyle = o.stroke === 'none' ? 'transparent' : o.stroke; 26 | ctx.lineWidth = o.strokeWidth; 27 | if (o.strokeLineDash) { 28 | ctx.setLineDash(o.strokeLineDash); 29 | } 30 | if (o.strokeLineDashOffset) { 31 | ctx.lineDashOffset = o.strokeLineDashOffset; 32 | } 33 | this._drawToContext(ctx, drawing, precision); 34 | ctx.restore(); 35 | break; 36 | case 'fillPath': { 37 | ctx.save(); 38 | ctx.fillStyle = o.fill || ''; 39 | const fillRule: CanvasFillRule = (drawable.shape === 'curve' || drawable.shape === 'polygon' || drawable.shape === 'path') ? 'evenodd' : 'nonzero'; 40 | this._drawToContext(ctx, drawing, precision, fillRule); 41 | ctx.restore(); 42 | break; 43 | } 44 | case 'fillSketch': 45 | this.fillSketch(ctx, drawing, o); 46 | break; 47 | } 48 | } 49 | } 50 | 51 | private fillSketch(ctx: CanvasRenderingContext2D, drawing: OpSet, o: ResolvedOptions) { 52 | let fweight = o.fillWeight; 53 | if (fweight < 0) { 54 | fweight = o.strokeWidth / 2; 55 | } 56 | ctx.save(); 57 | if (o.fillLineDash) { 58 | ctx.setLineDash(o.fillLineDash); 59 | } 60 | if (o.fillLineDashOffset) { 61 | ctx.lineDashOffset = o.fillLineDashOffset; 62 | } 63 | ctx.strokeStyle = o.fill || ''; 64 | ctx.lineWidth = fweight; 65 | this._drawToContext(ctx, drawing, o.fixedDecimalPlaceDigits); 66 | ctx.restore(); 67 | } 68 | 69 | private _drawToContext(ctx: CanvasRenderingContext2D, drawing: OpSet, fixedDecimals?: number, rule: CanvasFillRule = 'nonzero') { 70 | ctx.beginPath(); 71 | for (const item of drawing.ops) { 72 | const data = ((typeof fixedDecimals === 'number') && fixedDecimals >= 0) ? (item.data.map((d) => +d.toFixed(fixedDecimals))) : item.data; 73 | switch (item.op) { 74 | case 'move': 75 | ctx.moveTo(data[0], data[1]); 76 | break; 77 | case 'bcurveTo': 78 | ctx.bezierCurveTo(data[0], data[1], data[2], data[3], data[4], data[5]); 79 | break; 80 | case 'lineTo': 81 | ctx.lineTo(data[0], data[1]); 82 | break; 83 | } 84 | } 85 | if (drawing.type === 'fillPath') { 86 | ctx.fill(rule); 87 | } else { 88 | ctx.stroke(); 89 | } 90 | } 91 | 92 | get generator(): RoughGenerator { 93 | return this.gen; 94 | } 95 | 96 | getDefaultOptions(): ResolvedOptions { 97 | return this.gen.defaultOptions; 98 | } 99 | 100 | line(x1: number, y1: number, x2: number, y2: number, options?: Options): Drawable { 101 | const d = this.gen.line(x1, y1, x2, y2, options); 102 | this.draw(d); 103 | return d; 104 | } 105 | 106 | rectangle(x: number, y: number, width: number, height: number, options?: Options): Drawable { 107 | const d = this.gen.rectangle(x, y, width, height, options); 108 | this.draw(d); 109 | return d; 110 | } 111 | 112 | ellipse(x: number, y: number, width: number, height: number, options?: Options): Drawable { 113 | const d = this.gen.ellipse(x, y, width, height, options); 114 | this.draw(d); 115 | return d; 116 | } 117 | 118 | circle(x: number, y: number, diameter: number, options?: Options): Drawable { 119 | const d = this.gen.circle(x, y, diameter, options); 120 | this.draw(d); 121 | return d; 122 | } 123 | 124 | linearPath(points: Point[], options?: Options): Drawable { 125 | const d = this.gen.linearPath(points, options); 126 | this.draw(d); 127 | return d; 128 | } 129 | 130 | polygon(points: Point[], options?: Options): Drawable { 131 | const d = this.gen.polygon(points, options); 132 | this.draw(d); 133 | return d; 134 | } 135 | 136 | arc(x: number, y: number, width: number, height: number, start: number, stop: number, closed: boolean = false, options?: Options): Drawable { 137 | const d = this.gen.arc(x, y, width, height, start, stop, closed, options); 138 | this.draw(d); 139 | return d; 140 | } 141 | 142 | curve(points: Point[] | Point[][], options?: Options): Drawable { 143 | const d = this.gen.curve(points, options); 144 | this.draw(d); 145 | return d; 146 | } 147 | 148 | path(d: string, options?: Options): Drawable { 149 | const drawing = this.gen.path(d, options); 150 | this.draw(drawing); 151 | return drawing; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import { Point } from './geometry'; 2 | import { Random } from './math'; 3 | 4 | export const SVGNS = 'http://www.w3.org/2000/svg'; 5 | 6 | export interface Config { 7 | options?: Options; 8 | } 9 | 10 | export interface DrawingSurface { 11 | width: number | SVGAnimatedLength; 12 | height: number | SVGAnimatedLength; 13 | } 14 | 15 | export interface Options { 16 | maxRandomnessOffset?: number; 17 | roughness?: number; 18 | bowing?: number; 19 | stroke?: string; 20 | strokeWidth?: number; 21 | curveFitting?: number; 22 | curveTightness?: number; 23 | curveStepCount?: number; 24 | fill?: string; 25 | fillStyle?: string; 26 | fillWeight?: number; 27 | hachureAngle?: number; 28 | hachureGap?: number; 29 | simplification?: number; 30 | dashOffset?: number; 31 | dashGap?: number; 32 | zigzagOffset?: number; 33 | seed?: number; 34 | strokeLineDash?: number[]; 35 | strokeLineDashOffset?: number; 36 | fillLineDash?: number[]; 37 | fillLineDashOffset?: number; 38 | disableMultiStroke?: boolean; 39 | disableMultiStrokeFill?: boolean; 40 | preserveVertices?: boolean; 41 | fixedDecimalPlaceDigits?: number; 42 | fillShapeRoughnessGain?: number; 43 | } 44 | 45 | export interface ResolvedOptions extends Options { 46 | maxRandomnessOffset: number; 47 | roughness: number; 48 | bowing: number; 49 | stroke: string; 50 | strokeWidth: number; 51 | curveFitting: number; 52 | curveTightness: number; 53 | curveStepCount: number; 54 | fillStyle: string; 55 | fillWeight: number; 56 | hachureAngle: number; 57 | hachureGap: number; 58 | dashOffset: number; 59 | dashGap: number; 60 | zigzagOffset: number; 61 | seed: number; 62 | randomizer?: Random; 63 | disableMultiStroke: boolean; 64 | disableMultiStrokeFill: boolean; 65 | preserveVertices: boolean; 66 | fillShapeRoughnessGain: number; 67 | } 68 | 69 | export declare type OpType = 'move' | 'bcurveTo' | 'lineTo'; 70 | export declare type OpSetType = 'path' | 'fillPath' | 'fillSketch'; 71 | 72 | export interface Op { 73 | op: OpType; 74 | data: number[]; 75 | } 76 | 77 | export interface OpSet { 78 | type: OpSetType; 79 | ops: Op[]; 80 | size?: Point; 81 | path?: string; 82 | } 83 | 84 | export interface Drawable { 85 | shape: string; 86 | options: ResolvedOptions; 87 | sets: OpSet[]; 88 | } 89 | 90 | export interface PathInfo { 91 | d: string; 92 | stroke: string; 93 | strokeWidth: number; 94 | fill?: string; 95 | } -------------------------------------------------------------------------------- /src/fillers/dashed-filler.ts: -------------------------------------------------------------------------------- 1 | import { PatternFiller, RenderHelper } from './filler-interface'; 2 | import { ResolvedOptions, OpSet, Op } from '../core'; 3 | import { Point, Line, lineLength } from '../geometry'; 4 | import { polygonHachureLines } from './scan-line-hachure'; 5 | 6 | export class DashedFiller implements PatternFiller { 7 | private helper: RenderHelper; 8 | 9 | constructor(helper: RenderHelper) { 10 | this.helper = helper; 11 | } 12 | 13 | fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet { 14 | const lines = polygonHachureLines(polygonList, o); 15 | return { type: 'fillSketch', ops: this.dashedLine(lines, o) }; 16 | } 17 | 18 | private dashedLine(lines: Line[], o: ResolvedOptions): Op[] { 19 | const offset = o.dashOffset < 0 ? (o.hachureGap < 0 ? (o.strokeWidth * 4) : o.hachureGap) : o.dashOffset; 20 | const gap = o.dashGap < 0 ? (o.hachureGap < 0 ? (o.strokeWidth * 4) : o.hachureGap) : o.dashGap; 21 | const ops: Op[] = []; 22 | lines.forEach((line) => { 23 | const length = lineLength(line); 24 | const count = Math.floor(length / (offset + gap)); 25 | const startOffset = (length + gap - (count * (offset + gap))) / 2; 26 | let p1 = line[0]; 27 | let p2 = line[1]; 28 | if (p1[0] > p2[0]) { 29 | p1 = line[1]; 30 | p2 = line[0]; 31 | } 32 | const alpha = Math.atan((p2[1] - p1[1]) / (p2[0] - p1[0])); 33 | for (let i = 0; i < count; i++) { 34 | const lstart = i * (offset + gap); 35 | const lend = lstart + offset; 36 | const start: Point = [p1[0] + (lstart * Math.cos(alpha)) + (startOffset * Math.cos(alpha)), p1[1] + lstart * Math.sin(alpha) + (startOffset * Math.sin(alpha))]; 37 | const end: Point = [p1[0] + (lend * Math.cos(alpha)) + (startOffset * Math.cos(alpha)), p1[1] + (lend * Math.sin(alpha)) + (startOffset * Math.sin(alpha))]; 38 | ops.push(...this.helper.doubleLineOps(start[0], start[1], end[0], end[1], o)); 39 | } 40 | }); 41 | return ops; 42 | } 43 | } -------------------------------------------------------------------------------- /src/fillers/dot-filler.ts: -------------------------------------------------------------------------------- 1 | import { PatternFiller, RenderHelper } from './filler-interface'; 2 | import { ResolvedOptions, OpSet, Op } from '../core'; 3 | import { Point, Line, lineLength } from '../geometry'; 4 | import { polygonHachureLines } from './scan-line-hachure'; 5 | 6 | export class DotFiller implements PatternFiller { 7 | private helper: RenderHelper; 8 | 9 | constructor(helper: RenderHelper) { 10 | this.helper = helper; 11 | } 12 | 13 | fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet { 14 | o = Object.assign({}, o, { hachureAngle: 0 }); 15 | const lines = polygonHachureLines(polygonList, o); 16 | return this.dotsOnLines(lines, o); 17 | } 18 | 19 | private dotsOnLines(lines: Line[], o: ResolvedOptions): OpSet { 20 | const ops: Op[] = []; 21 | let gap = o.hachureGap; 22 | if (gap < 0) { 23 | gap = o.strokeWidth * 4; 24 | } 25 | gap = Math.max(gap, 0.1); 26 | let fweight = o.fillWeight; 27 | if (fweight < 0) { 28 | fweight = o.strokeWidth / 2; 29 | } 30 | const ro = gap / 4; 31 | for (const line of lines) { 32 | const length = lineLength(line); 33 | const dl = length / gap; 34 | const count = Math.ceil(dl) - 1; 35 | const offset = length - (count * gap); 36 | const x = ((line[0][0] + line[1][0]) / 2) - (gap / 4); 37 | const minY = Math.min(line[0][1], line[1][1]); 38 | 39 | for (let i = 0; i < count; i++) { 40 | const y = minY + offset + (i * gap); 41 | const cx = (x - ro) + Math.random() * 2 * ro; 42 | const cy = (y - ro) + Math.random() * 2 * ro; 43 | const el = this.helper.ellipse(cx, cy, fweight, fweight, o); 44 | ops.push(...el.ops); 45 | } 46 | } 47 | return { type: 'fillSketch', ops }; 48 | } 49 | } -------------------------------------------------------------------------------- /src/fillers/filler-interface.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedOptions, OpSet, Op } from '../core'; 2 | import { Point } from '../geometry'; 3 | 4 | export interface PatternFiller { 5 | fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet; 6 | } 7 | 8 | export interface RenderHelper { 9 | randOffset(x: number, o: ResolvedOptions): number; 10 | randOffsetWithRange(min: number, max: number, o: ResolvedOptions): number; 11 | ellipse(x: number, y: number, width: number, height: number, o: ResolvedOptions): OpSet; 12 | doubleLineOps(x1: number, y1: number, x2: number, y2: number, o: ResolvedOptions): Op[]; 13 | } -------------------------------------------------------------------------------- /src/fillers/filler.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedOptions } from '../core'; 2 | import { PatternFiller, RenderHelper } from './filler-interface'; 3 | import { HachureFiller } from './hachure-filler'; 4 | import { ZigZagFiller } from './zigzag-filler'; 5 | import { HatchFiller } from './hatch-filler'; 6 | import { DotFiller } from './dot-filler'; 7 | import { DashedFiller } from './dashed-filler'; 8 | import { ZigZagLineFiller } from './zigzag-line-filler'; 9 | 10 | const fillers: { [name: string]: PatternFiller } = {}; 11 | 12 | export function getFiller(o: ResolvedOptions, helper: RenderHelper): PatternFiller { 13 | let fillerName = o.fillStyle || 'hachure'; 14 | if (!fillers[fillerName]) { 15 | switch (fillerName) { 16 | case 'zigzag': 17 | if (!fillers[fillerName]) { 18 | fillers[fillerName] = new ZigZagFiller(helper); 19 | } 20 | break; 21 | case 'cross-hatch': 22 | if (!fillers[fillerName]) { 23 | fillers[fillerName] = new HatchFiller(helper); 24 | } 25 | break; 26 | case 'dots': 27 | if (!fillers[fillerName]) { 28 | fillers[fillerName] = new DotFiller(helper); 29 | } 30 | break; 31 | case 'dashed': 32 | if (!fillers[fillerName]) { 33 | fillers[fillerName] = new DashedFiller(helper); 34 | } 35 | break; 36 | case 'zigzag-line': 37 | if (!fillers[fillerName]) { 38 | fillers[fillerName] = new ZigZagLineFiller(helper); 39 | } 40 | break; 41 | case 'hachure': 42 | default: 43 | fillerName = 'hachure'; 44 | if (!fillers[fillerName]) { 45 | fillers[fillerName] = new HachureFiller(helper); 46 | } 47 | break; 48 | } 49 | } 50 | return fillers[fillerName]; 51 | } -------------------------------------------------------------------------------- /src/fillers/hachure-filler.ts: -------------------------------------------------------------------------------- 1 | import { PatternFiller, RenderHelper } from './filler-interface'; 2 | import { ResolvedOptions, OpSet, Op } from '../core'; 3 | import { Point, Line } from '../geometry'; 4 | import { polygonHachureLines } from './scan-line-hachure'; 5 | 6 | export class HachureFiller implements PatternFiller { 7 | private helper: RenderHelper; 8 | 9 | constructor(helper: RenderHelper) { 10 | this.helper = helper; 11 | } 12 | 13 | fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet { 14 | return this._fillPolygons(polygonList, o); 15 | } 16 | 17 | protected _fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet { 18 | const lines = polygonHachureLines(polygonList, o); 19 | const ops = this.renderLines(lines, o); 20 | return { type: 'fillSketch', ops }; 21 | } 22 | 23 | protected renderLines(lines: Line[], o: ResolvedOptions): Op[] { 24 | const ops: Op[] = []; 25 | for (const line of lines) { 26 | ops.push(...this.helper.doubleLineOps(line[0][0], line[0][1], line[1][0], line[1][1], o)); 27 | } 28 | return ops; 29 | } 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/fillers/hatch-filler.ts: -------------------------------------------------------------------------------- 1 | import { HachureFiller } from './hachure-filler'; 2 | import { ResolvedOptions, OpSet } from '../core'; 3 | import { Point } from '../geometry'; 4 | 5 | export class HatchFiller extends HachureFiller { 6 | fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet { 7 | const set = this._fillPolygons(polygonList, o); 8 | const o2 = Object.assign({}, o, { hachureAngle: o.hachureAngle + 90 }); 9 | const set2 = this._fillPolygons(polygonList, o2); 10 | set.ops = set.ops.concat(set2.ops); 11 | return set; 12 | } 13 | } -------------------------------------------------------------------------------- /src/fillers/scan-line-hachure.ts: -------------------------------------------------------------------------------- 1 | import { hachureLines } from 'hachure-fill'; 2 | import { Point, Line } from '../geometry'; 3 | import { ResolvedOptions } from '../core'; 4 | 5 | export function polygonHachureLines(polygonList: Point[][], o: ResolvedOptions): Line[] { 6 | const angle = o.hachureAngle + 90; 7 | let gap = o.hachureGap; 8 | if (gap < 0) { 9 | gap = o.strokeWidth * 4; 10 | } 11 | gap = Math.round(Math.max(gap, 0.1)); 12 | let skipOffset = 1; 13 | if (o.roughness >= 1) { 14 | if ((o.randomizer?.next() || Math.random()) > 0.7) { 15 | skipOffset = gap; 16 | } 17 | } 18 | return hachureLines(polygonList, gap, angle, skipOffset || 1); 19 | } -------------------------------------------------------------------------------- /src/fillers/zigzag-filler.ts: -------------------------------------------------------------------------------- 1 | import { HachureFiller } from './hachure-filler'; 2 | import { polygonHachureLines } from './scan-line-hachure'; 3 | import { ResolvedOptions, OpSet } from '../core'; 4 | import { Point, Line, lineLength } from '../geometry'; 5 | 6 | export class ZigZagFiller extends HachureFiller { 7 | fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet { 8 | let gap = o.hachureGap; 9 | if (gap < 0) { 10 | gap = o.strokeWidth * 4; 11 | } 12 | gap = Math.max(gap, 0.1); 13 | const o2 = Object.assign({}, o, { hachureGap: gap }); 14 | const lines = polygonHachureLines(polygonList, o2); 15 | const zigZagAngle = (Math.PI / 180) * o.hachureAngle; 16 | const zigzagLines: Line[] = []; 17 | const dgx = gap * 0.5 * Math.cos(zigZagAngle); 18 | const dgy = gap * 0.5 * Math.sin(zigZagAngle); 19 | for (const [p1, p2] of lines) { 20 | if (lineLength([p1, p2])) { 21 | zigzagLines.push([ 22 | [p1[0] - dgx, p1[1] + dgy], 23 | [...p2], 24 | ], [ 25 | [p1[0] + dgx, p1[1] - dgy], 26 | [...p2], 27 | ]); 28 | } 29 | } 30 | const ops = this.renderLines(zigzagLines, o); 31 | return { type: 'fillSketch', ops }; 32 | } 33 | } -------------------------------------------------------------------------------- /src/fillers/zigzag-line-filler.ts: -------------------------------------------------------------------------------- 1 | import { PatternFiller, RenderHelper } from './filler-interface'; 2 | import { ResolvedOptions, OpSet, Op } from '../core'; 3 | import { Point, Line, lineLength } from '../geometry'; 4 | import { polygonHachureLines } from './scan-line-hachure'; 5 | 6 | export class ZigZagLineFiller implements PatternFiller { 7 | private helper: RenderHelper; 8 | 9 | constructor(helper: RenderHelper) { 10 | this.helper = helper; 11 | } 12 | 13 | fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet { 14 | const gap = o.hachureGap < 0 ? (o.strokeWidth * 4) : o.hachureGap; 15 | const zo = o.zigzagOffset < 0 ? gap : o.zigzagOffset; 16 | o = Object.assign({}, o, { hachureGap: gap + zo }); 17 | const lines = polygonHachureLines(polygonList, o); 18 | return { type: 'fillSketch', ops: this.zigzagLines(lines, zo, o) }; 19 | } 20 | 21 | private zigzagLines(lines: Line[], zo: number, o: ResolvedOptions): Op[] { 22 | const ops: Op[] = []; 23 | lines.forEach((line) => { 24 | const length = lineLength(line); 25 | const count = Math.round(length / (2 * zo)); 26 | let p1 = line[0]; 27 | let p2 = line[1]; 28 | if (p1[0] > p2[0]) { 29 | p1 = line[1]; 30 | p2 = line[0]; 31 | } 32 | const alpha = Math.atan((p2[1] - p1[1]) / (p2[0] - p1[0])); 33 | for (let i = 0; i < count; i++) { 34 | const lstart = i * 2 * zo; 35 | const lend = (i + 1) * 2 * zo; 36 | const dz = Math.sqrt(2 * Math.pow(zo, 2)); 37 | const start: Point = [p1[0] + (lstart * Math.cos(alpha)), p1[1] + lstart * Math.sin(alpha)]; 38 | const end: Point = [p1[0] + (lend * Math.cos(alpha)), p1[1] + (lend * Math.sin(alpha))]; 39 | const middle: Point = [start[0] + dz * Math.cos(alpha + Math.PI / 4), start[1] + dz * Math.sin(alpha + Math.PI / 4)]; 40 | ops.push( 41 | ...this.helper.doubleLineOps(start[0], start[1], middle[0], middle[1], o), 42 | ...this.helper.doubleLineOps(middle[0], middle[1], end[0], end[1], o) 43 | ); 44 | } 45 | }); 46 | return ops; 47 | } 48 | } -------------------------------------------------------------------------------- /src/generator.ts: -------------------------------------------------------------------------------- 1 | import { Config, Options, Drawable, OpSet, Op, ResolvedOptions, PathInfo } from './core.js'; 2 | import { Point } from './geometry.js'; 3 | import { line, solidFillPolygon, patternFillPolygons, rectangle, ellipseWithParams, generateEllipseParams, linearPath, arc, patternFillArc, curve, svgPath } from './renderer.js'; 4 | import { randomSeed } from './math.js'; 5 | import { curveToBezier } from 'points-on-curve/lib/curve-to-bezier.js'; 6 | import { pointsOnBezierCurves } from 'points-on-curve'; 7 | import { pointsOnPath } from 'points-on-path'; 8 | 9 | const NOS = 'none'; 10 | 11 | export class RoughGenerator { 12 | private config: Config; 13 | 14 | defaultOptions: ResolvedOptions = { 15 | maxRandomnessOffset: 2, 16 | roughness: 1, 17 | bowing: 1, 18 | stroke: '#000', 19 | strokeWidth: 1, 20 | curveTightness: 0, 21 | curveFitting: 0.95, 22 | curveStepCount: 9, 23 | fillStyle: 'hachure', 24 | fillWeight: -1, 25 | hachureAngle: -41, 26 | hachureGap: -1, 27 | dashOffset: -1, 28 | dashGap: -1, 29 | zigzagOffset: -1, 30 | seed: 0, 31 | disableMultiStroke: false, 32 | disableMultiStrokeFill: false, 33 | preserveVertices: false, 34 | fillShapeRoughnessGain: 0.8, 35 | }; 36 | 37 | constructor(config?: Config) { 38 | this.config = config || {}; 39 | if (this.config.options) { 40 | this.defaultOptions = this._o(this.config.options); 41 | } 42 | } 43 | 44 | static newSeed(): number { 45 | return randomSeed(); 46 | } 47 | 48 | private _o(options?: Options): ResolvedOptions { 49 | return options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions; 50 | } 51 | 52 | private _d(shape: string, sets: OpSet[], options: ResolvedOptions): Drawable { 53 | return { shape, sets: sets || [], options: options || this.defaultOptions }; 54 | } 55 | 56 | line(x1: number, y1: number, x2: number, y2: number, options?: Options): Drawable { 57 | const o = this._o(options); 58 | return this._d('line', [line(x1, y1, x2, y2, o)], o); 59 | } 60 | 61 | rectangle(x: number, y: number, width: number, height: number, options?: Options): Drawable { 62 | const o = this._o(options); 63 | const paths = []; 64 | const outline = rectangle(x, y, width, height, o); 65 | if (o.fill) { 66 | const points: Point[] = [[x, y], [x + width, y], [x + width, y + height], [x, y + height]]; 67 | if (o.fillStyle === 'solid') { 68 | paths.push(solidFillPolygon([points], o)); 69 | } else { 70 | paths.push(patternFillPolygons([points], o)); 71 | } 72 | } 73 | if (o.stroke !== NOS) { 74 | paths.push(outline); 75 | } 76 | return this._d('rectangle', paths, o); 77 | } 78 | 79 | ellipse(x: number, y: number, width: number, height: number, options?: Options): Drawable { 80 | const o = this._o(options); 81 | const paths: OpSet[] = []; 82 | const ellipseParams = generateEllipseParams(width, height, o); 83 | const ellipseResponse = ellipseWithParams(x, y, o, ellipseParams); 84 | if (o.fill) { 85 | if (o.fillStyle === 'solid') { 86 | const shape = ellipseWithParams(x, y, o, ellipseParams).opset; 87 | shape.type = 'fillPath'; 88 | paths.push(shape); 89 | } else { 90 | paths.push(patternFillPolygons([ellipseResponse.estimatedPoints], o)); 91 | } 92 | } 93 | if (o.stroke !== NOS) { 94 | paths.push(ellipseResponse.opset); 95 | } 96 | return this._d('ellipse', paths, o); 97 | } 98 | 99 | circle(x: number, y: number, diameter: number, options?: Options): Drawable { 100 | const ret = this.ellipse(x, y, diameter, diameter, options); 101 | ret.shape = 'circle'; 102 | return ret; 103 | } 104 | 105 | linearPath(points: Point[], options?: Options): Drawable { 106 | const o = this._o(options); 107 | return this._d('linearPath', [linearPath(points, false, o)], o); 108 | } 109 | 110 | arc(x: number, y: number, width: number, height: number, start: number, stop: number, closed: boolean = false, options?: Options): Drawable { 111 | const o = this._o(options); 112 | const paths = []; 113 | const outline = arc(x, y, width, height, start, stop, closed, true, o); 114 | if (closed && o.fill) { 115 | if (o.fillStyle === 'solid') { 116 | const fillOptions: ResolvedOptions = { ...o }; 117 | fillOptions.disableMultiStroke = true; 118 | const shape = arc(x, y, width, height, start, stop, true, false, fillOptions); 119 | shape.type = 'fillPath'; 120 | paths.push(shape); 121 | } else { 122 | paths.push(patternFillArc(x, y, width, height, start, stop, o)); 123 | } 124 | } 125 | if (o.stroke !== NOS) { 126 | paths.push(outline); 127 | } 128 | return this._d('arc', paths, o); 129 | } 130 | 131 | curve(points: Point[] | Point[][], options?: Options): Drawable { 132 | const o = this._o(options); 133 | const paths: OpSet[] = []; 134 | const outline = curve(points, o); 135 | if (o.fill && o.fill !== NOS) { 136 | if (o.fillStyle === 'solid') { 137 | const fillShape = curve(points, { ...o, disableMultiStroke: true, roughness: o.roughness ? (o.roughness + o.fillShapeRoughnessGain) : 0 }); 138 | paths.push({ 139 | type: 'fillPath', 140 | ops: this._mergedShape(fillShape.ops), 141 | }); 142 | } else { 143 | const polyPoints: Point[] = []; 144 | const inputPoints = points; 145 | if (inputPoints.length) { 146 | const p1 = inputPoints[0]; 147 | const pointsList = (typeof p1[0] === 'number') ? [inputPoints as Point[]] : inputPoints as Point[][]; 148 | for (const points of pointsList) { 149 | if (points.length < 3) { 150 | polyPoints.push(...points); 151 | } else if (points.length === 3) { 152 | polyPoints.push(...pointsOnBezierCurves(curveToBezier([ 153 | points[0], 154 | points[0], 155 | points[1], 156 | points[2], 157 | ]), 10, (1 + o.roughness) / 2)); 158 | } else { 159 | polyPoints.push(...pointsOnBezierCurves(curveToBezier(points), 10, (1 + o.roughness) / 2)); 160 | } 161 | } 162 | } 163 | if (polyPoints.length) { 164 | paths.push(patternFillPolygons([polyPoints], o)); 165 | } 166 | } 167 | } 168 | if (o.stroke !== NOS) { 169 | paths.push(outline); 170 | } 171 | return this._d('curve', paths, o); 172 | } 173 | 174 | polygon(points: Point[], options?: Options): Drawable { 175 | const o = this._o(options); 176 | const paths: OpSet[] = []; 177 | const outline = linearPath(points, true, o); 178 | if (o.fill) { 179 | if (o.fillStyle === 'solid') { 180 | paths.push(solidFillPolygon([points], o)); 181 | } else { 182 | paths.push(patternFillPolygons([points], o)); 183 | } 184 | } 185 | if (o.stroke !== NOS) { 186 | paths.push(outline); 187 | } 188 | return this._d('polygon', paths, o); 189 | } 190 | 191 | path(d: string, options?: Options): Drawable { 192 | const o = this._o(options); 193 | const paths: OpSet[] = []; 194 | if (!d) { 195 | return this._d('path', paths, o); 196 | } 197 | d = (d || '').replace(/\n/g, ' ').replace(/(-\s)/g, '-').replace('/(\s\s)/g', ' '); 198 | 199 | const hasFill = o.fill && o.fill !== 'transparent' && o.fill !== NOS; 200 | const hasStroke = o.stroke !== NOS; 201 | const simplified = !!(o.simplification && (o.simplification < 1)); 202 | const distance = simplified ? (4 - 4 * (o.simplification || 1)) : ((1 + o.roughness) / 2); 203 | const sets = pointsOnPath(d, 1, distance); 204 | const shape = svgPath(d, o); 205 | 206 | if (hasFill) { 207 | if (o.fillStyle === 'solid') { 208 | if (sets.length === 1) { 209 | const fillShape = svgPath(d, { ...o, disableMultiStroke: true, roughness: o.roughness ? (o.roughness + o.fillShapeRoughnessGain) : 0 }); 210 | paths.push({ 211 | type: 'fillPath', 212 | ops: this._mergedShape(fillShape.ops), 213 | }); 214 | } else { 215 | paths.push(solidFillPolygon(sets, o)); 216 | } 217 | } else { 218 | paths.push(patternFillPolygons(sets, o)); 219 | } 220 | } 221 | if (hasStroke) { 222 | if (simplified) { 223 | sets.forEach((set) => { 224 | paths.push(linearPath(set, false, o)); 225 | }); 226 | } else { 227 | paths.push(shape); 228 | } 229 | } 230 | 231 | return this._d('path', paths, o); 232 | } 233 | 234 | opsToPath(drawing: OpSet, fixedDecimals?: number): string { 235 | let path = ''; 236 | for (const item of drawing.ops) { 237 | const data = ((typeof fixedDecimals === 'number') && fixedDecimals >= 0) ? (item.data.map((d) => +d.toFixed(fixedDecimals))) : item.data; 238 | switch (item.op) { 239 | case 'move': 240 | path += `M${data[0]} ${data[1]} `; 241 | break; 242 | case 'bcurveTo': 243 | path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `; 244 | break; 245 | case 'lineTo': 246 | path += `L${data[0]} ${data[1]} `; 247 | break; 248 | } 249 | } 250 | return path.trim(); 251 | } 252 | 253 | toPaths(drawable: Drawable): PathInfo[] { 254 | const sets = drawable.sets || []; 255 | const o = drawable.options || this.defaultOptions; 256 | const paths: PathInfo[] = []; 257 | for (const drawing of sets) { 258 | let path: PathInfo | null = null; 259 | switch (drawing.type) { 260 | case 'path': 261 | path = { 262 | d: this.opsToPath(drawing), 263 | stroke: o.stroke, 264 | strokeWidth: o.strokeWidth, 265 | fill: NOS, 266 | }; 267 | break; 268 | case 'fillPath': 269 | path = { 270 | d: this.opsToPath(drawing), 271 | stroke: NOS, 272 | strokeWidth: 0, 273 | fill: o.fill || NOS, 274 | }; 275 | break; 276 | case 'fillSketch': 277 | path = this.fillSketch(drawing, o); 278 | break; 279 | } 280 | if (path) { 281 | paths.push(path); 282 | } 283 | } 284 | return paths; 285 | } 286 | 287 | private fillSketch(drawing: OpSet, o: ResolvedOptions): PathInfo { 288 | let fweight = o.fillWeight; 289 | if (fweight < 0) { 290 | fweight = o.strokeWidth / 2; 291 | } 292 | return { 293 | d: this.opsToPath(drawing), 294 | stroke: o.fill || NOS, 295 | strokeWidth: fweight, 296 | fill: NOS, 297 | }; 298 | } 299 | 300 | private _mergedShape(input: Op[]): Op[] { 301 | return input.filter((d, i) => { 302 | if (i === 0) { 303 | return true; 304 | } 305 | if (d.op === 'move') { 306 | return false; 307 | } 308 | return true; 309 | }); 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/geometry.ts: -------------------------------------------------------------------------------- 1 | export type Point = [number, number]; 2 | export type Line = [Point, Point]; 3 | 4 | export interface Rectangle { 5 | x: number; 6 | y: number; 7 | width: number; 8 | height: number; 9 | } 10 | 11 | export function lineLength(line: Line): number { 12 | const p1 = line[0]; 13 | const p2 = line[1]; 14 | return Math.sqrt(Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2)); 15 | } -------------------------------------------------------------------------------- /src/math.ts: -------------------------------------------------------------------------------- 1 | export function randomSeed(): number { 2 | return Math.floor(Math.random() * 2 ** 31); 3 | } 4 | 5 | export class Random { 6 | private seed: number; 7 | 8 | constructor(seed: number) { 9 | this.seed = seed; 10 | } 11 | 12 | next(): number { 13 | if (this.seed) { 14 | return ((2 ** 31 - 1) & (this.seed = Math.imul(48271, this.seed))) / 2 ** 31; 15 | } else { 16 | return Math.random(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedOptions, Op, OpSet } from './core.js'; 2 | import { Point } from './geometry.js'; 3 | import { getFiller } from './fillers/filler.js'; 4 | import { RenderHelper } from './fillers/filler-interface.js'; 5 | import { Random } from './math.js'; 6 | import { parsePath, normalize, absolutize } from 'path-data-parser'; 7 | 8 | interface EllipseParams { 9 | rx: number; 10 | ry: number; 11 | increment: number; 12 | } 13 | 14 | const helper: RenderHelper = { 15 | randOffset, 16 | randOffsetWithRange, 17 | ellipse, 18 | doubleLineOps: doubleLineFillOps, 19 | }; 20 | 21 | export function line(x1: number, y1: number, x2: number, y2: number, o: ResolvedOptions): OpSet { 22 | return { type: 'path', ops: _doubleLine(x1, y1, x2, y2, o) }; 23 | } 24 | 25 | export function linearPath(points: Point[], close: boolean, o: ResolvedOptions): OpSet { 26 | const len = (points || []).length; 27 | if (len > 2) { 28 | const ops: Op[] = []; 29 | for (let i = 0; i < (len - 1); i++) { 30 | ops.push(..._doubleLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1], o)); 31 | } 32 | if (close) { 33 | ops.push(..._doubleLine(points[len - 1][0], points[len - 1][1], points[0][0], points[0][1], o)); 34 | } 35 | return { type: 'path', ops }; 36 | } else if (len === 2) { 37 | return line(points[0][0], points[0][1], points[1][0], points[1][1], o); 38 | } 39 | return { type: 'path', ops: [] }; 40 | } 41 | 42 | export function polygon(points: Point[], o: ResolvedOptions): OpSet { 43 | return linearPath(points, true, o); 44 | } 45 | 46 | export function rectangle(x: number, y: number, width: number, height: number, o: ResolvedOptions): OpSet { 47 | const points: Point[] = [ 48 | [x, y], 49 | [x + width, y], 50 | [x + width, y + height], 51 | [x, y + height], 52 | ]; 53 | return polygon(points, o); 54 | } 55 | 56 | export function curve(inputPoints: Point[] | Point[][], o: ResolvedOptions): OpSet { 57 | if (inputPoints.length) { 58 | const p1 = inputPoints[0]; 59 | const pointsList = (typeof p1[0] === 'number') ? [inputPoints as Point[]] : inputPoints as Point[][]; 60 | 61 | const o1 = _curveWithOffset(pointsList[0], 1 * (1 + o.roughness * 0.2), o); 62 | const o2 = o.disableMultiStroke ? [] : _curveWithOffset(pointsList[0], 1.5 * (1 + o.roughness * 0.22), cloneOptionsAlterSeed(o)); 63 | 64 | for (let i = 1; i < pointsList.length; i++) { 65 | const points = pointsList[i]; 66 | if (points.length) { 67 | const underlay = _curveWithOffset(points, 1 * (1 + o.roughness * 0.2), o); 68 | const overlay = o.disableMultiStroke ? [] : _curveWithOffset(points, 1.5 * (1 + o.roughness * 0.22), cloneOptionsAlterSeed(o)); 69 | for (const item of underlay) { 70 | if (item.op !== 'move') { 71 | o1.push(item); 72 | } 73 | } 74 | for (const item of overlay) { 75 | if (item.op !== 'move') { 76 | o2.push(item); 77 | } 78 | } 79 | } 80 | } 81 | 82 | return { type: 'path', ops: o1.concat(o2) }; 83 | } 84 | return { type: 'path', ops: [] }; 85 | } 86 | 87 | export interface EllipseResult { 88 | opset: OpSet; 89 | estimatedPoints: Point[]; 90 | } 91 | 92 | export function ellipse(x: number, y: number, width: number, height: number, o: ResolvedOptions): OpSet { 93 | const params = generateEllipseParams(width, height, o); 94 | return ellipseWithParams(x, y, o, params).opset; 95 | } 96 | 97 | export function generateEllipseParams(width: number, height: number, o: ResolvedOptions): EllipseParams { 98 | const psq = Math.sqrt(Math.PI * 2 * Math.sqrt((Math.pow(width / 2, 2) + Math.pow(height / 2, 2)) / 2)); 99 | const stepCount = Math.ceil(Math.max(o.curveStepCount, (o.curveStepCount / Math.sqrt(200)) * psq)); 100 | const increment = (Math.PI * 2) / stepCount; 101 | let rx = Math.abs(width / 2); 102 | let ry = Math.abs(height / 2); 103 | const curveFitRandomness = 1 - o.curveFitting; 104 | rx += _offsetOpt(rx * curveFitRandomness, o); 105 | ry += _offsetOpt(ry * curveFitRandomness, o); 106 | return { increment, rx, ry }; 107 | } 108 | 109 | export function ellipseWithParams(x: number, y: number, o: ResolvedOptions, ellipseParams: EllipseParams): EllipseResult { 110 | const [ap1, cp1] = _computeEllipsePoints(ellipseParams.increment, x, y, ellipseParams.rx, ellipseParams.ry, 1, ellipseParams.increment * _offset(0.1, _offset(0.4, 1, o), o), o); 111 | let o1 = _curve(ap1, null, o); 112 | if ((!o.disableMultiStroke) && (o.roughness !== 0)) { 113 | const [ap2] = _computeEllipsePoints(ellipseParams.increment, x, y, ellipseParams.rx, ellipseParams.ry, 1.5, 0, o); 114 | const o2 = _curve(ap2, null, o); 115 | o1 = o1.concat(o2); 116 | } 117 | return { 118 | estimatedPoints: cp1, 119 | opset: { type: 'path', ops: o1 }, 120 | }; 121 | } 122 | 123 | export function arc(x: number, y: number, width: number, height: number, start: number, stop: number, closed: boolean, roughClosure: boolean, o: ResolvedOptions): OpSet { 124 | const cx = x; 125 | const cy = y; 126 | let rx = Math.abs(width / 2); 127 | let ry = Math.abs(height / 2); 128 | rx += _offsetOpt(rx * 0.01, o); 129 | ry += _offsetOpt(ry * 0.01, o); 130 | let strt = start; 131 | let stp = stop; 132 | while (strt < 0) { 133 | strt += Math.PI * 2; 134 | stp += Math.PI * 2; 135 | } 136 | if ((stp - strt) > (Math.PI * 2)) { 137 | strt = 0; 138 | stp = Math.PI * 2; 139 | } 140 | const ellipseInc = (Math.PI * 2) / o.curveStepCount; 141 | const arcInc = Math.min(ellipseInc / 2, (stp - strt) / 2); 142 | const ops = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1, o); 143 | if (!o.disableMultiStroke) { 144 | const o2 = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1.5, o); 145 | ops.push(...o2); 146 | } 147 | if (closed) { 148 | if (roughClosure) { 149 | ops.push( 150 | ..._doubleLine(cx, cy, cx + rx * Math.cos(strt), cy + ry * Math.sin(strt), o), 151 | ..._doubleLine(cx, cy, cx + rx * Math.cos(stp), cy + ry * Math.sin(stp), o) 152 | ); 153 | } else { 154 | ops.push( 155 | { op: 'lineTo', data: [cx, cy] }, 156 | { op: 'lineTo', data: [cx + rx * Math.cos(strt), cy + ry * Math.sin(strt)] } 157 | ); 158 | } 159 | } 160 | return { type: 'path', ops }; 161 | } 162 | 163 | export function svgPath(path: string, o: ResolvedOptions): OpSet { 164 | const segments = normalize(absolutize(parsePath(path))); 165 | const ops: Op[] = []; 166 | let first: Point = [0, 0]; 167 | let current: Point = [0, 0]; 168 | for (const { key, data } of segments) { 169 | switch (key) { 170 | case 'M': { 171 | current = [data[0], data[1]]; 172 | first = [data[0], data[1]]; 173 | break; 174 | } 175 | case 'L': 176 | ops.push(..._doubleLine(current[0], current[1], data[0], data[1], o)); 177 | current = [data[0], data[1]]; 178 | break; 179 | case 'C': { 180 | const [x1, y1, x2, y2, x, y] = data; 181 | ops.push(..._bezierTo(x1, y1, x2, y2, x, y, current, o)); 182 | current = [x, y]; 183 | break; 184 | } 185 | case 'Z': 186 | ops.push(..._doubleLine(current[0], current[1], first[0], first[1], o)); 187 | current = [first[0], first[1]]; 188 | break; 189 | } 190 | } 191 | return { type: 'path', ops }; 192 | } 193 | 194 | // Fills 195 | 196 | export function solidFillPolygon(polygonList: Point[][], o: ResolvedOptions): OpSet { 197 | const ops: Op[] = []; 198 | for (const points of polygonList) { 199 | if (points.length) { 200 | const offset = o.maxRandomnessOffset || 0; 201 | const len = points.length; 202 | if (len > 2) { 203 | ops.push({ op: 'move', data: [points[0][0] + _offsetOpt(offset, o), points[0][1] + _offsetOpt(offset, o)] }); 204 | for (let i = 1; i < len; i++) { 205 | ops.push({ op: 'lineTo', data: [points[i][0] + _offsetOpt(offset, o), points[i][1] + _offsetOpt(offset, o)] }); 206 | } 207 | } 208 | } 209 | } 210 | return { type: 'fillPath', ops }; 211 | } 212 | 213 | export function patternFillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet { 214 | return getFiller(o, helper).fillPolygons(polygonList, o); 215 | } 216 | 217 | export function patternFillArc(x: number, y: number, width: number, height: number, start: number, stop: number, o: ResolvedOptions): OpSet { 218 | const cx = x; 219 | const cy = y; 220 | let rx = Math.abs(width / 2); 221 | let ry = Math.abs(height / 2); 222 | rx += _offsetOpt(rx * 0.01, o); 223 | ry += _offsetOpt(ry * 0.01, o); 224 | let strt = start; 225 | let stp = stop; 226 | while (strt < 0) { 227 | strt += Math.PI * 2; 228 | stp += Math.PI * 2; 229 | } 230 | if ((stp - strt) > (Math.PI * 2)) { 231 | strt = 0; 232 | stp = Math.PI * 2; 233 | } 234 | const increment = (stp - strt) / o.curveStepCount; 235 | const points: Point[] = []; 236 | for (let angle = strt; angle <= stp; angle = angle + increment) { 237 | points.push([cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)]); 238 | } 239 | points.push([cx + rx * Math.cos(stp), cy + ry * Math.sin(stp)]); 240 | points.push([cx, cy]); 241 | return patternFillPolygons([points], o); 242 | } 243 | 244 | export function randOffset(x: number, o: ResolvedOptions): number { 245 | return _offsetOpt(x, o); 246 | } 247 | 248 | export function randOffsetWithRange(min: number, max: number, o: ResolvedOptions): number { 249 | return _offset(min, max, o); 250 | } 251 | 252 | export function doubleLineFillOps(x1: number, y1: number, x2: number, y2: number, o: ResolvedOptions): Op[] { 253 | return _doubleLine(x1, y1, x2, y2, o, true); 254 | } 255 | 256 | // Private helpers 257 | 258 | function cloneOptionsAlterSeed(ops: ResolvedOptions): ResolvedOptions { 259 | const result: ResolvedOptions = { ...ops }; 260 | result.randomizer = undefined; 261 | if (ops.seed) { 262 | result.seed = ops.seed + 1; 263 | } 264 | return result; 265 | } 266 | 267 | function random(ops: ResolvedOptions): number { 268 | if (!ops.randomizer) { 269 | ops.randomizer = new Random(ops.seed || 0); 270 | } 271 | return ops.randomizer.next(); 272 | } 273 | 274 | function _offset(min: number, max: number, ops: ResolvedOptions, roughnessGain = 1): number { 275 | return ops.roughness * roughnessGain * ((random(ops) * (max - min)) + min); 276 | } 277 | 278 | function _offsetOpt(x: number, ops: ResolvedOptions, roughnessGain = 1): number { 279 | return _offset(-x, x, ops, roughnessGain); 280 | } 281 | 282 | function _doubleLine(x1: number, y1: number, x2: number, y2: number, o: ResolvedOptions, filling = false): Op[] { 283 | const singleStroke = filling ? o.disableMultiStrokeFill : o.disableMultiStroke; 284 | const o1 = _line(x1, y1, x2, y2, o, true, false); 285 | if (singleStroke) { 286 | return o1; 287 | } 288 | const o2 = _line(x1, y1, x2, y2, o, true, true); 289 | return o1.concat(o2); 290 | } 291 | 292 | function _line(x1: number, y1: number, x2: number, y2: number, o: ResolvedOptions, move: boolean, overlay: boolean): Op[] { 293 | const lengthSq = Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2); 294 | const length = Math.sqrt(lengthSq); 295 | let roughnessGain = 1; 296 | if (length < 200) { 297 | roughnessGain = 1; 298 | } else if (length > 500) { 299 | roughnessGain = 0.4; 300 | } else { 301 | roughnessGain = (-0.0016668) * length + 1.233334; 302 | } 303 | 304 | let offset = o.maxRandomnessOffset || 0; 305 | if ((offset * offset * 100) > lengthSq) { 306 | offset = length / 10; 307 | } 308 | const halfOffset = offset / 2; 309 | const divergePoint = 0.2 + random(o) * 0.2; 310 | let midDispX = o.bowing * o.maxRandomnessOffset * (y2 - y1) / 200; 311 | let midDispY = o.bowing * o.maxRandomnessOffset * (x1 - x2) / 200; 312 | midDispX = _offsetOpt(midDispX, o, roughnessGain); 313 | midDispY = _offsetOpt(midDispY, o, roughnessGain); 314 | const ops: Op[] = []; 315 | const randomHalf = () => _offsetOpt(halfOffset, o, roughnessGain); 316 | const randomFull = () => _offsetOpt(offset, o, roughnessGain); 317 | const preserveVertices = o.preserveVertices; 318 | if (move) { 319 | if (overlay) { 320 | ops.push({ 321 | op: 'move', data: [ 322 | x1 + (preserveVertices ? 0 : randomHalf()), 323 | y1 + (preserveVertices ? 0 : randomHalf()), 324 | ], 325 | }); 326 | } else { 327 | ops.push({ 328 | op: 'move', data: [ 329 | x1 + (preserveVertices ? 0 : _offsetOpt(offset, o, roughnessGain)), 330 | y1 + (preserveVertices ? 0 : _offsetOpt(offset, o, roughnessGain)), 331 | ], 332 | }); 333 | } 334 | } 335 | if (overlay) { 336 | ops.push({ 337 | op: 'bcurveTo', 338 | data: [ 339 | midDispX + x1 + (x2 - x1) * divergePoint + randomHalf(), 340 | midDispY + y1 + (y2 - y1) * divergePoint + randomHalf(), 341 | midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomHalf(), 342 | midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomHalf(), 343 | x2 + (preserveVertices ? 0 : randomHalf()), 344 | y2 + (preserveVertices ? 0 : randomHalf()), 345 | ], 346 | }); 347 | } else { 348 | ops.push({ 349 | op: 'bcurveTo', 350 | data: [ 351 | midDispX + x1 + (x2 - x1) * divergePoint + randomFull(), 352 | midDispY + y1 + (y2 - y1) * divergePoint + randomFull(), 353 | midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomFull(), 354 | midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomFull(), 355 | x2 + (preserveVertices ? 0 : randomFull()), 356 | y2 + (preserveVertices ? 0 : randomFull()), 357 | ], 358 | }); 359 | } 360 | return ops; 361 | } 362 | 363 | function _curveWithOffset(points: Point[], offset: number, o: ResolvedOptions): Op[] { 364 | if (!points.length) { 365 | return []; 366 | } 367 | const ps: Point[] = []; 368 | ps.push([ 369 | points[0][0] + _offsetOpt(offset, o), 370 | points[0][1] + _offsetOpt(offset, o), 371 | ]); 372 | ps.push([ 373 | points[0][0] + _offsetOpt(offset, o), 374 | points[0][1] + _offsetOpt(offset, o), 375 | ]); 376 | for (let i = 1; i < points.length; i++) { 377 | ps.push([ 378 | points[i][0] + _offsetOpt(offset, o), 379 | points[i][1] + _offsetOpt(offset, o), 380 | ]); 381 | if (i === (points.length - 1)) { 382 | ps.push([ 383 | points[i][0] + _offsetOpt(offset, o), 384 | points[i][1] + _offsetOpt(offset, o), 385 | ]); 386 | } 387 | } 388 | return _curve(ps, null, o); 389 | } 390 | 391 | function _curve(points: Point[], closePoint: Point | null, o: ResolvedOptions): Op[] { 392 | const len = points.length; 393 | const ops: Op[] = []; 394 | if (len > 3) { 395 | const b = []; 396 | const s = 1 - o.curveTightness; 397 | ops.push({ op: 'move', data: [points[1][0], points[1][1]] }); 398 | for (let i = 1; (i + 2) < len; i++) { 399 | const cachedVertArray = points[i]; 400 | b[0] = [cachedVertArray[0], cachedVertArray[1]]; 401 | b[1] = [cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6, cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6]; 402 | b[2] = [points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6, points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6]; 403 | b[3] = [points[i + 1][0], points[i + 1][1]]; 404 | ops.push({ op: 'bcurveTo', data: [b[1][0], b[1][1], b[2][0], b[2][1], b[3][0], b[3][1]] }); 405 | } 406 | if (closePoint && closePoint.length === 2) { 407 | const ro = o.maxRandomnessOffset; 408 | ops.push({ op: 'lineTo', data: [closePoint[0] + _offsetOpt(ro, o), closePoint[1] + _offsetOpt(ro, o)] }); 409 | } 410 | } else if (len === 3) { 411 | ops.push({ op: 'move', data: [points[1][0], points[1][1]] }); 412 | ops.push({ 413 | op: 'bcurveTo', 414 | data: [ 415 | points[1][0], points[1][1], 416 | points[2][0], points[2][1], 417 | points[2][0], points[2][1], 418 | ], 419 | }); 420 | } else if (len === 2) { 421 | ops.push(..._line(points[0][0], points[0][1], points[1][0], points[1][1], o, true, true)); 422 | } 423 | return ops; 424 | } 425 | 426 | function _computeEllipsePoints(increment: number, cx: number, cy: number, rx: number, ry: number, offset: number, overlap: number, o: ResolvedOptions): Point[][] { 427 | const coreOnly = o.roughness === 0; 428 | const corePoints: Point[] = []; 429 | const allPoints: Point[] = []; 430 | 431 | if (coreOnly) { 432 | increment = increment / 4; 433 | allPoints.push([ 434 | cx + rx * Math.cos(-increment), 435 | cy + ry * Math.sin(-increment), 436 | ]); 437 | for (let angle = 0; angle <= Math.PI * 2; angle = angle + increment) { 438 | const p: Point = [ 439 | cx + rx * Math.cos(angle), 440 | cy + ry * Math.sin(angle), 441 | ]; 442 | corePoints.push(p); 443 | allPoints.push(p); 444 | } 445 | allPoints.push([ 446 | cx + rx * Math.cos(0), 447 | cy + ry * Math.sin(0), 448 | ]); 449 | allPoints.push([ 450 | cx + rx * Math.cos(increment), 451 | cy + ry * Math.sin(increment), 452 | ]); 453 | } else { 454 | const radOffset = _offsetOpt(0.5, o) - (Math.PI / 2); 455 | allPoints.push([ 456 | _offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment), 457 | _offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment), 458 | ]); 459 | const endAngle = Math.PI * 2 + radOffset - 0.01; 460 | for (let angle = radOffset; angle < endAngle; angle = angle + increment) { 461 | const p: Point = [ 462 | _offsetOpt(offset, o) + cx + rx * Math.cos(angle), 463 | _offsetOpt(offset, o) + cy + ry * Math.sin(angle), 464 | ]; 465 | corePoints.push(p); 466 | allPoints.push(p); 467 | } 468 | allPoints.push([ 469 | _offsetOpt(offset, o) + cx + rx * Math.cos(radOffset + Math.PI * 2 + overlap * 0.5), 470 | _offsetOpt(offset, o) + cy + ry * Math.sin(radOffset + Math.PI * 2 + overlap * 0.5), 471 | ]); 472 | allPoints.push([ 473 | _offsetOpt(offset, o) + cx + 0.98 * rx * Math.cos(radOffset + overlap), 474 | _offsetOpt(offset, o) + cy + 0.98 * ry * Math.sin(radOffset + overlap), 475 | ]); 476 | allPoints.push([ 477 | _offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset + overlap * 0.5), 478 | _offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset + overlap * 0.5), 479 | ]); 480 | } 481 | 482 | 483 | return [allPoints, corePoints]; 484 | } 485 | 486 | function _arc(increment: number, cx: number, cy: number, rx: number, ry: number, strt: number, stp: number, offset: number, o: ResolvedOptions) { 487 | const radOffset = strt + _offsetOpt(0.1, o); 488 | const points: Point[] = []; 489 | points.push([ 490 | _offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment), 491 | _offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment), 492 | ]); 493 | for (let angle = radOffset; angle <= stp; angle = angle + increment) { 494 | points.push([ 495 | _offsetOpt(offset, o) + cx + rx * Math.cos(angle), 496 | _offsetOpt(offset, o) + cy + ry * Math.sin(angle), 497 | ]); 498 | } 499 | points.push([ 500 | cx + rx * Math.cos(stp), 501 | cy + ry * Math.sin(stp), 502 | ]); 503 | points.push([ 504 | cx + rx * Math.cos(stp), 505 | cy + ry * Math.sin(stp), 506 | ]); 507 | return _curve(points, null, o); 508 | } 509 | 510 | function _bezierTo(x1: number, y1: number, x2: number, y2: number, x: number, y: number, current: Point, o: ResolvedOptions): Op[] { 511 | const ops: Op[] = []; 512 | const ros = [o.maxRandomnessOffset || 1, (o.maxRandomnessOffset || 1) + 0.3]; 513 | let f: Point = [0, 0]; 514 | const iterations = o.disableMultiStroke ? 1 : 2; 515 | const preserveVertices = o.preserveVertices; 516 | for (let i = 0; i < iterations; i++) { 517 | if (i === 0) { 518 | ops.push({ op: 'move', data: [current[0], current[1]] }); 519 | } else { 520 | ops.push({ op: 'move', data: [current[0] + (preserveVertices ? 0 : _offsetOpt(ros[0], o)), current[1] + (preserveVertices ? 0 : _offsetOpt(ros[0], o))] }); 521 | } 522 | f = preserveVertices ? [x, y] : [x + _offsetOpt(ros[i], o), y + _offsetOpt(ros[i], o)]; 523 | ops.push({ 524 | op: 'bcurveTo', 525 | data: [ 526 | x1 + _offsetOpt(ros[i], o), y1 + _offsetOpt(ros[i], o), 527 | x2 + _offsetOpt(ros[i], o), y2 + _offsetOpt(ros[i], o), 528 | f[0], f[1], 529 | ], 530 | }); 531 | } 532 | return ops; 533 | } 534 | -------------------------------------------------------------------------------- /src/rough.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './core'; 2 | import { RoughCanvas } from './canvas'; 3 | import { RoughGenerator } from './generator'; 4 | import { RoughSVG } from './svg'; 5 | 6 | export default { 7 | canvas(canvas: HTMLCanvasElement, config?: Config): RoughCanvas { 8 | return new RoughCanvas(canvas, config); 9 | }, 10 | 11 | svg(svg: SVGSVGElement, config?: Config): RoughSVG { 12 | return new RoughSVG(svg, config); 13 | }, 14 | 15 | generator(config?: Config): RoughGenerator { 16 | return new RoughGenerator(config); 17 | }, 18 | 19 | newSeed(): number { 20 | return RoughGenerator.newSeed(); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/svg.ts: -------------------------------------------------------------------------------- 1 | import { Config, Options, OpSet, ResolvedOptions, Drawable, SVGNS } from './core'; 2 | import { RoughGenerator } from './generator'; 3 | import { Point } from './geometry'; 4 | 5 | export class RoughSVG { 6 | private gen: RoughGenerator; 7 | private svg: SVGSVGElement; 8 | 9 | constructor(svg: SVGSVGElement, config?: Config) { 10 | this.svg = svg; 11 | this.gen = new RoughGenerator(config); 12 | } 13 | 14 | draw(drawable: Drawable): SVGGElement { 15 | const sets = drawable.sets || []; 16 | const o = drawable.options || this.getDefaultOptions(); 17 | const doc = this.svg.ownerDocument || window.document; 18 | const g = doc.createElementNS(SVGNS, 'g'); 19 | const precision = drawable.options.fixedDecimalPlaceDigits; 20 | for (const drawing of sets) { 21 | let path = null; 22 | switch (drawing.type) { 23 | case 'path': { 24 | path = doc.createElementNS(SVGNS, 'path'); 25 | path.setAttribute('d', this.opsToPath(drawing, precision)); 26 | path.setAttribute('stroke', o.stroke); 27 | path.setAttribute('stroke-width', o.strokeWidth + ''); 28 | path.setAttribute('fill', 'none'); 29 | if (o.strokeLineDash) { 30 | path.setAttribute('stroke-dasharray', o.strokeLineDash.join(' ').trim()); 31 | } 32 | if (o.strokeLineDashOffset) { 33 | path.setAttribute('stroke-dashoffset', `${o.strokeLineDashOffset}`); 34 | } 35 | break; 36 | } 37 | case 'fillPath': { 38 | path = doc.createElementNS(SVGNS, 'path'); 39 | path.setAttribute('d', this.opsToPath(drawing, precision)); 40 | path.setAttribute('stroke', 'none'); 41 | path.setAttribute('stroke-width', '0'); 42 | path.setAttribute('fill', o.fill || ''); 43 | if (drawable.shape === 'curve' || drawable.shape === 'polygon') { 44 | path.setAttribute('fill-rule', 'evenodd'); 45 | } 46 | break; 47 | } 48 | case 'fillSketch': { 49 | path = this.fillSketch(doc, drawing, o); 50 | break; 51 | } 52 | } 53 | if (path) { 54 | g.appendChild(path); 55 | } 56 | } 57 | return g; 58 | } 59 | 60 | private fillSketch(doc: Document, drawing: OpSet, o: ResolvedOptions): SVGPathElement { 61 | let fweight = o.fillWeight; 62 | if (fweight < 0) { 63 | fweight = o.strokeWidth / 2; 64 | } 65 | const path = doc.createElementNS(SVGNS, 'path'); 66 | path.setAttribute('d', this.opsToPath(drawing, o.fixedDecimalPlaceDigits)); 67 | path.setAttribute('stroke', o.fill || ''); 68 | path.setAttribute('stroke-width', fweight + ''); 69 | path.setAttribute('fill', 'none'); 70 | if (o.fillLineDash) { 71 | path.setAttribute('stroke-dasharray', o.fillLineDash.join(' ').trim()); 72 | } 73 | if (o.fillLineDashOffset) { 74 | path.setAttribute('stroke-dashoffset', `${o.fillLineDashOffset}`); 75 | } 76 | return path; 77 | } 78 | 79 | get generator(): RoughGenerator { 80 | return this.gen; 81 | } 82 | 83 | getDefaultOptions(): ResolvedOptions { 84 | return this.gen.defaultOptions; 85 | } 86 | 87 | opsToPath(drawing: OpSet, fixedDecimalPlaceDigits?: number): string { 88 | return this.gen.opsToPath(drawing, fixedDecimalPlaceDigits); 89 | } 90 | 91 | line(x1: number, y1: number, x2: number, y2: number, options?: Options): SVGGElement { 92 | const d = this.gen.line(x1, y1, x2, y2, options); 93 | return this.draw(d); 94 | } 95 | 96 | rectangle(x: number, y: number, width: number, height: number, options?: Options): SVGGElement { 97 | const d = this.gen.rectangle(x, y, width, height, options); 98 | return this.draw(d); 99 | } 100 | 101 | ellipse(x: number, y: number, width: number, height: number, options?: Options): SVGGElement { 102 | const d = this.gen.ellipse(x, y, width, height, options); 103 | return this.draw(d); 104 | } 105 | 106 | circle(x: number, y: number, diameter: number, options?: Options): SVGGElement { 107 | const d = this.gen.circle(x, y, diameter, options); 108 | return this.draw(d); 109 | } 110 | 111 | linearPath(points: Point[], options?: Options): SVGGElement { 112 | const d = this.gen.linearPath(points, options); 113 | return this.draw(d); 114 | } 115 | 116 | polygon(points: Point[], options?: Options): SVGGElement { 117 | const d = this.gen.polygon(points, options); 118 | return this.draw(d); 119 | } 120 | 121 | arc(x: number, y: number, width: number, height: number, start: number, stop: number, closed: boolean = false, options?: Options): SVGGElement { 122 | const d = this.gen.arc(x, y, width, height, start, stop, closed, options); 123 | return this.draw(d); 124 | } 125 | 126 | curve(points: Point[] | Point[][], options?: Options): SVGGElement { 127 | const d = this.gen.curve(points, options); 128 | return this.draw(d); 129 | } 130 | 131 | path(d: string, options?: Options): SVGGElement { 132 | const drawing = this.gen.path(d, options); 133 | return this.draw(drawing); 134 | } 135 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2017", 8 | "dom" 9 | ], 10 | "declaration": true, 11 | "outDir": "./bin", 12 | "baseUrl": ".", 13 | "strict": true, 14 | "strictNullChecks": true, 15 | "noImplicitAny": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": [ 22 | "src/**/*.ts" 23 | ] 24 | } -------------------------------------------------------------------------------- /visual-tests/canvas/arc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Arc 8 | 9 | 10 | 11 | 12 | 13 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /visual-tests/canvas/arc2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Arc 8 | 9 | 10 | 11 | 12 | 13 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /visual-tests/canvas/curve-seed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /visual-tests/canvas/curve.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /visual-tests/canvas/curve2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /visual-tests/canvas/curve3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /visual-tests/canvas/curve4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /visual-tests/canvas/dashed/arc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Arc 8 | 9 | 10 | 11 | 12 | 13 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /visual-tests/canvas/dashed/curve.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /visual-tests/canvas/dashed/ellipse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Ellipse 8 | 9 | 10 | 11 | 12 | 13 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /visual-tests/canvas/dashed/line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Line 8 | 9 | 10 | 11 | 12 | 13 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /visual-tests/canvas/dashed/linearpath.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS linear path 8 | 9 | 10 | 11 | 12 | 13 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /visual-tests/canvas/dashed/path-with-transform.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /visual-tests/canvas/dashed/path.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /visual-tests/canvas/dashed/polygon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Polygon 8 | 9 | 10 | 11 | 12 | 13 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /visual-tests/canvas/dashed/rectangle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Rectangle 8 | 9 | 10 | 11 | 12 | 13 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /visual-tests/canvas/ellipse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Ellipse 8 | 9 | 10 | 11 | 12 | 13 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /visual-tests/canvas/ellipse2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Ellipse 8 | 9 | 10 | 11 | 12 | 13 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /visual-tests/canvas/ellipse3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Ellipse 8 | 9 | 10 | 11 | 12 | 13 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /visual-tests/canvas/line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Line 8 | 9 | 10 | 11 | 12 | 13 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /visual-tests/canvas/linearpath.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS linear path 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /visual-tests/canvas/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Map 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /visual-tests/canvas/path-with-transform.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /visual-tests/canvas/path.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /visual-tests/canvas/path2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /visual-tests/canvas/path3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /visual-tests/canvas/path4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /visual-tests/canvas/path5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Path 8 | 9 | 10 | 11 | 12 | 13 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /visual-tests/canvas/path6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /visual-tests/canvas/path7.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /visual-tests/canvas/poly-seed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /visual-tests/canvas/polygon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Polygon 8 | 9 | 10 | 11 | 12 | 13 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /visual-tests/canvas/polygon2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Polygon 8 | 9 | 10 | 11 | 12 | 13 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /visual-tests/canvas/rectangle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Rectangle 8 | 9 | 10 | 11 | 12 | 13 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /visual-tests/canvas/singlestroke/arc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Arc 8 | 9 | 10 | 11 | 12 | 13 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /visual-tests/canvas/singlestroke/curve.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /visual-tests/canvas/singlestroke/ellipse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Ellipse 8 | 9 | 10 | 11 | 12 | 13 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /visual-tests/canvas/singlestroke/line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Line 8 | 9 | 10 | 11 | 12 | 13 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /visual-tests/canvas/singlestroke/path.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Curve 8 | 9 | 10 | 11 | 12 | 13 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /visual-tests/canvas/singlestroke/polygon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Polygon 8 | 9 | 10 | 11 | 12 | 13 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /visual-tests/canvas/singlestroke/rectangle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Rectangle 8 | 9 | 10 | 11 | 12 | 13 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /visual-tests/svg/dashed/ellipse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Ellipse 8 | 9 | 10 | 11 | 12 | 13 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /visual-tests/svg/dashed/line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Line 8 | 9 | 10 | 11 | 12 | 13 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /visual-tests/svg/dashed/polygon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Polygon 8 | 9 | 10 | 11 | 12 | 13 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /visual-tests/svg/dashed/rectangle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Rectangle 8 | 9 | 10 | 11 | 12 | 13 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /visual-tests/svg/ellipse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Ellipse 8 | 9 | 10 | 11 | 12 | 13 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /visual-tests/svg/line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Line 8 | 9 | 10 | 11 | 12 | 13 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /visual-tests/svg/polygon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Polygon 8 | 9 | 10 | 11 | 12 | 13 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /visual-tests/svg/rectangle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Rectangle 8 | 9 | 10 | 11 | 12 | 13 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /visual-tests/svg/rectangle2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RoughJS Rectangle 8 | 9 | 10 | 11 | 12 | 13 | 24 | 25 | 26 | --------------------------------------------------------------------------------