├── .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 | 
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 | 
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 | 
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 | 
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 | 
84 |
85 | ### Sketching style
86 |
87 | 
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 | 
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 |  
109 |
110 | ## Examples
111 |
112 | 
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 |
--------------------------------------------------------------------------------