trying mapping positions
parent
69a61088c4
commit
2559d948d3
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Preet Shihn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,72 @@
|
||||
# path-data-parser
|
||||
|
||||
I know there are a bunch of SVG path parsers out there, but here's one that I have been using for a few years now. It's small, tree-shakable and provides four simple functions that I have needed in several graphics projects.
|
||||
|
||||
## Install
|
||||
|
||||
From npm
|
||||
|
||||
```
|
||||
npm install --save path-data-parser
|
||||
```
|
||||
|
||||
The code is shipped as ES6 modules.
|
||||
|
||||
## Methods
|
||||
|
||||
The module exposes 4 methods
|
||||
|
||||
### pasrePath(d: string): Segment[]
|
||||
|
||||
This is the main method that parses the SVG path data. You pass in the path string and it returns an array of `Segments`. A segment has a `key` which is the commands, e.g. `M` or `h` or `C`; and `data` which is an array of numbers used by the command
|
||||
|
||||
```javascript
|
||||
Segment {
|
||||
key: string;
|
||||
data: number[];
|
||||
}
|
||||
```
|
||||
|
||||
example:
|
||||
|
||||
```javascript
|
||||
import { parsePath } from 'path-data-parser';
|
||||
const segments = parsePath('M10 10 h 80 v 80 h -80 C Z');
|
||||
```
|
||||
|
||||
### serialize(segments: Segment[]): string
|
||||
|
||||
This is essentially the opposite of the `parsePath` command. It outputs a path string from an array of `Segment` objects.
|
||||
|
||||
```javascript
|
||||
import { parsePath, serialize, absolutize } from 'path-data-parser';
|
||||
|
||||
const segments = parsePath('M10 10 h 80 v 80 h -80 Z');
|
||||
const absoluteSegments = absolutize(segments); // Turns relative commands to absolute
|
||||
const outputPath = serialize(absoluteSegments);
|
||||
console.log(outputPath); // M 10 10 H 90 V 90 H 10 Z
|
||||
```
|
||||
|
||||
### absolutize(segments: Segment[]): Segment[]
|
||||
|
||||
Translates relative commands to absolute commands. i.e. all commands that use relative positions (lower-case ones), turns into absolute position commands (upper-case ones).
|
||||
See the `serialize` example above.
|
||||
|
||||
### normalize(segments: Segment[]): Segment[]
|
||||
|
||||
Normalize takes a list of _absolute segments_ and outputs a list of segments with only four commands: `M, L, C, Z`. So every segment is described as move, line, or a bezier curve (cubic).
|
||||
|
||||
This is useful when translating SVG paths to non SVG mediums - Canvas, or some other graphics platform. Most such platforms will support lines and bezier curves. It also simplifies the cases to consider when modifying these segments.
|
||||
|
||||
```javascript
|
||||
import { parsePath, serialize, absolutize, normalize } from 'path-data-parser';
|
||||
|
||||
const segments = parsePath(' M 10 80 Q 52.5 10, 95 80 T 180 80');
|
||||
const absoluteSegments = absolutize(segments);
|
||||
const normalizedSegments = normalize(absoluteSegments);
|
||||
// M 10 80 C 38.33 33.33, 66.67 33.33, 95 80 C 123.33 126.67, 151.67 126.67, 180 80
|
||||
|
||||
```
|
||||
|
||||
## License
|
||||
[MIT License](https://github.com/pshihn/path-data-parser/blob/master/LICENSE)
|
@ -0,0 +1,2 @@
|
||||
import { Segment } from './parser.js';
|
||||
export declare function absolutize(segments: Segment[]): Segment[];
|
@ -0,0 +1,110 @@
|
||||
// Translate relative commands to absolute commands
|
||||
export function absolutize(segments) {
|
||||
let cx = 0, cy = 0;
|
||||
let subx = 0, suby = 0;
|
||||
const out = [];
|
||||
for (const { key, data } of segments) {
|
||||
switch (key) {
|
||||
case 'M':
|
||||
out.push({ key: 'M', data: [...data] });
|
||||
[cx, cy] = data;
|
||||
[subx, suby] = data;
|
||||
break;
|
||||
case 'm':
|
||||
cx += data[0];
|
||||
cy += data[1];
|
||||
out.push({ key: 'M', data: [cx, cy] });
|
||||
subx = cx;
|
||||
suby = cy;
|
||||
break;
|
||||
case 'L':
|
||||
out.push({ key: 'L', data: [...data] });
|
||||
[cx, cy] = data;
|
||||
break;
|
||||
case 'l':
|
||||
cx += data[0];
|
||||
cy += data[1];
|
||||
out.push({ key: 'L', data: [cx, cy] });
|
||||
break;
|
||||
case 'C':
|
||||
out.push({ key: 'C', data: [...data] });
|
||||
cx = data[4];
|
||||
cy = data[5];
|
||||
break;
|
||||
case 'c': {
|
||||
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
|
||||
out.push({ key: 'C', data: newdata });
|
||||
cx = newdata[4];
|
||||
cy = newdata[5];
|
||||
break;
|
||||
}
|
||||
case 'Q':
|
||||
out.push({ key: 'Q', data: [...data] });
|
||||
cx = data[2];
|
||||
cy = data[3];
|
||||
break;
|
||||
case 'q': {
|
||||
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
|
||||
out.push({ key: 'Q', data: newdata });
|
||||
cx = newdata[2];
|
||||
cy = newdata[3];
|
||||
break;
|
||||
}
|
||||
case 'A':
|
||||
out.push({ key: 'A', data: [...data] });
|
||||
cx = data[5];
|
||||
cy = data[6];
|
||||
break;
|
||||
case 'a':
|
||||
cx += data[5];
|
||||
cy += data[6];
|
||||
out.push({ key: 'A', data: [data[0], data[1], data[2], data[3], data[4], cx, cy] });
|
||||
break;
|
||||
case 'H':
|
||||
out.push({ key: 'H', data: [...data] });
|
||||
cx = data[0];
|
||||
break;
|
||||
case 'h':
|
||||
cx += data[0];
|
||||
out.push({ key: 'H', data: [cx] });
|
||||
break;
|
||||
case 'V':
|
||||
out.push({ key: 'V', data: [...data] });
|
||||
cy = data[0];
|
||||
break;
|
||||
case 'v':
|
||||
cy += data[0];
|
||||
out.push({ key: 'V', data: [cy] });
|
||||
break;
|
||||
case 'S':
|
||||
out.push({ key: 'S', data: [...data] });
|
||||
cx = data[2];
|
||||
cy = data[3];
|
||||
break;
|
||||
case 's': {
|
||||
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
|
||||
out.push({ key: 'S', data: newdata });
|
||||
cx = newdata[2];
|
||||
cy = newdata[3];
|
||||
break;
|
||||
}
|
||||
case 'T':
|
||||
out.push({ key: 'T', data: [...data] });
|
||||
cx = data[0];
|
||||
cy = data[1];
|
||||
break;
|
||||
case 't':
|
||||
cx += data[0];
|
||||
cy += data[1];
|
||||
out.push({ key: 'T', data: [cx, cy] });
|
||||
break;
|
||||
case 'Z':
|
||||
case 'z':
|
||||
out.push({ key: 'Z', data: [] });
|
||||
cx = subx;
|
||||
cy = suby;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export { parsePath, serialize } from './parser.js';
|
||||
export { absolutize } from './absolutize.js';
|
||||
export { normalize } from './normalize.js';
|
@ -0,0 +1,3 @@
|
||||
export { parsePath, serialize } from './parser.js';
|
||||
export { absolutize } from './absolutize.js';
|
||||
export { normalize } from './normalize.js';
|
@ -0,0 +1,2 @@
|
||||
import { Segment } from './parser.js';
|
||||
export declare function normalize(segments: Segment[]): Segment[];
|
@ -0,0 +1,219 @@
|
||||
// Normalize path to include only M, L, C, and Z commands
|
||||
export function normalize(segments) {
|
||||
const out = [];
|
||||
let lastType = '';
|
||||
let cx = 0, cy = 0;
|
||||
let subx = 0, suby = 0;
|
||||
let lcx = 0, lcy = 0;
|
||||
for (const { key, data } of segments) {
|
||||
switch (key) {
|
||||
case 'M':
|
||||
out.push({ key: 'M', data: [...data] });
|
||||
[cx, cy] = data;
|
||||
[subx, suby] = data;
|
||||
break;
|
||||
case 'C':
|
||||
out.push({ key: 'C', data: [...data] });
|
||||
cx = data[4];
|
||||
cy = data[5];
|
||||
lcx = data[2];
|
||||
lcy = data[3];
|
||||
break;
|
||||
case 'L':
|
||||
out.push({ key: 'L', data: [...data] });
|
||||
[cx, cy] = data;
|
||||
break;
|
||||
case 'H':
|
||||
cx = data[0];
|
||||
out.push({ key: 'L', data: [cx, cy] });
|
||||
break;
|
||||
case 'V':
|
||||
cy = data[0];
|
||||
out.push({ key: 'L', data: [cx, cy] });
|
||||
break;
|
||||
case 'S': {
|
||||
let cx1 = 0, cy1 = 0;
|
||||
if (lastType === 'C' || lastType === 'S') {
|
||||
cx1 = cx + (cx - lcx);
|
||||
cy1 = cy + (cy - lcy);
|
||||
}
|
||||
else {
|
||||
cx1 = cx;
|
||||
cy1 = cy;
|
||||
}
|
||||
out.push({ key: 'C', data: [cx1, cy1, ...data] });
|
||||
lcx = data[0];
|
||||
lcy = data[1];
|
||||
cx = data[2];
|
||||
cy = data[3];
|
||||
break;
|
||||
}
|
||||
case 'T': {
|
||||
const [x, y] = data;
|
||||
let x1 = 0, y1 = 0;
|
||||
if (lastType === 'Q' || lastType === 'T') {
|
||||
x1 = cx + (cx - lcx);
|
||||
y1 = cy + (cy - lcy);
|
||||
}
|
||||
else {
|
||||
x1 = cx;
|
||||
y1 = cy;
|
||||
}
|
||||
const cx1 = cx + 2 * (x1 - cx) / 3;
|
||||
const cy1 = cy + 2 * (y1 - cy) / 3;
|
||||
const cx2 = x + 2 * (x1 - x) / 3;
|
||||
const cy2 = y + 2 * (y1 - y) / 3;
|
||||
out.push({ key: 'C', data: [cx1, cy1, cx2, cy2, x, y] });
|
||||
lcx = x1;
|
||||
lcy = y1;
|
||||
cx = x;
|
||||
cy = y;
|
||||
break;
|
||||
}
|
||||
case 'Q': {
|
||||
const [x1, y1, x, y] = data;
|
||||
const cx1 = cx + 2 * (x1 - cx) / 3;
|
||||
const cy1 = cy + 2 * (y1 - cy) / 3;
|
||||
const cx2 = x + 2 * (x1 - x) / 3;
|
||||
const cy2 = y + 2 * (y1 - y) / 3;
|
||||
out.push({ key: 'C', data: [cx1, cy1, cx2, cy2, x, y] });
|
||||
lcx = x1;
|
||||
lcy = y1;
|
||||
cx = x;
|
||||
cy = y;
|
||||
break;
|
||||
}
|
||||
case 'A': {
|
||||
const r1 = Math.abs(data[0]);
|
||||
const r2 = Math.abs(data[1]);
|
||||
const angle = data[2];
|
||||
const largeArcFlag = data[3];
|
||||
const sweepFlag = data[4];
|
||||
const x = data[5];
|
||||
const y = data[6];
|
||||
if (r1 === 0 || r2 === 0) {
|
||||
out.push({ key: 'C', data: [cx, cy, x, y, x, y] });
|
||||
cx = x;
|
||||
cy = y;
|
||||
}
|
||||
else {
|
||||
if (cx !== x || cy !== y) {
|
||||
const curves = arcToCubicCurves(cx, cy, x, y, r1, r2, angle, largeArcFlag, sweepFlag);
|
||||
curves.forEach(function (curve) {
|
||||
out.push({ key: 'C', data: curve });
|
||||
});
|
||||
cx = x;
|
||||
cy = y;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Z':
|
||||
out.push({ key: 'Z', data: [] });
|
||||
cx = subx;
|
||||
cy = suby;
|
||||
break;
|
||||
}
|
||||
lastType = key;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function degToRad(degrees) {
|
||||
return (Math.PI * degrees) / 180;
|
||||
}
|
||||
function rotate(x, y, angleRad) {
|
||||
const X = x * Math.cos(angleRad) - y * Math.sin(angleRad);
|
||||
const Y = x * Math.sin(angleRad) + y * Math.cos(angleRad);
|
||||
return [X, Y];
|
||||
}
|
||||
function arcToCubicCurves(x1, y1, x2, y2, r1, r2, angle, largeArcFlag, sweepFlag, recursive) {
|
||||
const angleRad = degToRad(angle);
|
||||
let params = [];
|
||||
let f1 = 0, f2 = 0, cx = 0, cy = 0;
|
||||
if (recursive) {
|
||||
[f1, f2, cx, cy] = recursive;
|
||||
}
|
||||
else {
|
||||
[x1, y1] = rotate(x1, y1, -angleRad);
|
||||
[x2, y2] = rotate(x2, y2, -angleRad);
|
||||
const x = (x1 - x2) / 2;
|
||||
const y = (y1 - y2) / 2;
|
||||
let h = (x * x) / (r1 * r1) + (y * y) / (r2 * r2);
|
||||
if (h > 1) {
|
||||
h = Math.sqrt(h);
|
||||
r1 = h * r1;
|
||||
r2 = h * r2;
|
||||
}
|
||||
const sign = (largeArcFlag === sweepFlag) ? -1 : 1;
|
||||
const r1Pow = r1 * r1;
|
||||
const r2Pow = r2 * r2;
|
||||
const left = r1Pow * r2Pow - r1Pow * y * y - r2Pow * x * x;
|
||||
const right = r1Pow * y * y + r2Pow * x * x;
|
||||
const k = sign * Math.sqrt(Math.abs(left / right));
|
||||
cx = k * r1 * y / r2 + (x1 + x2) / 2;
|
||||
cy = k * -r2 * x / r1 + (y1 + y2) / 2;
|
||||
f1 = Math.asin(parseFloat(((y1 - cy) / r2).toFixed(9)));
|
||||
f2 = Math.asin(parseFloat(((y2 - cy) / r2).toFixed(9)));
|
||||
if (x1 < cx) {
|
||||
f1 = Math.PI - f1;
|
||||
}
|
||||
if (x2 < cx) {
|
||||
f2 = Math.PI - f2;
|
||||
}
|
||||
if (f1 < 0) {
|
||||
f1 = Math.PI * 2 + f1;
|
||||
}
|
||||
if (f2 < 0) {
|
||||
f2 = Math.PI * 2 + f2;
|
||||
}
|
||||
if (sweepFlag && f1 > f2) {
|
||||
f1 = f1 - Math.PI * 2;
|
||||
}
|
||||
if (!sweepFlag && f2 > f1) {
|
||||
f2 = f2 - Math.PI * 2;
|
||||
}
|
||||
}
|
||||
let df = f2 - f1;
|
||||
if (Math.abs(df) > (Math.PI * 120 / 180)) {
|
||||
const f2old = f2;
|
||||
const x2old = x2;
|
||||
const y2old = y2;
|
||||
if (sweepFlag && f2 > f1) {
|
||||
f2 = f1 + (Math.PI * 120 / 180) * (1);
|
||||
}
|
||||
else {
|
||||
f2 = f1 + (Math.PI * 120 / 180) * (-1);
|
||||
}
|
||||
x2 = cx + r1 * Math.cos(f2);
|
||||
y2 = cy + r2 * Math.sin(f2);
|
||||
params = arcToCubicCurves(x2, y2, x2old, y2old, r1, r2, angle, 0, sweepFlag, [f2, f2old, cx, cy]);
|
||||
}
|
||||
df = f2 - f1;
|
||||
const c1 = Math.cos(f1);
|
||||
const s1 = Math.sin(f1);
|
||||
const c2 = Math.cos(f2);
|
||||
const s2 = Math.sin(f2);
|
||||
const t = Math.tan(df / 4);
|
||||
const hx = 4 / 3 * r1 * t;
|
||||
const hy = 4 / 3 * r2 * t;
|
||||
const m1 = [x1, y1];
|
||||
const m2 = [x1 + hx * s1, y1 - hy * c1];
|
||||
const m3 = [x2 + hx * s2, y2 - hy * c2];
|
||||
const m4 = [x2, y2];
|
||||
m2[0] = 2 * m1[0] - m2[0];
|
||||
m2[1] = 2 * m1[1] - m2[1];
|
||||
if (recursive) {
|
||||
return [m2, m3, m4].concat(params);
|
||||
}
|
||||
else {
|
||||
params = [m2, m3, m4].concat(params);
|
||||
const curves = [];
|
||||
for (let i = 0; i < params.length; i += 3) {
|
||||
const r1 = rotate(params[i][0], params[i][1], angleRad);
|
||||
const r2 = rotate(params[i + 1][0], params[i + 1][1], angleRad);
|
||||
const r3 = rotate(params[i + 2][0], params[i + 2][1], angleRad);
|
||||
curves.push([r1[0], r1[1], r2[0], r2[1], r3[0], r3[1]]);
|
||||
}
|
||||
return curves;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export interface Segment {
|
||||
key: string;
|
||||
data: number[];
|
||||
}
|
||||
export declare function parsePath(d: string): Segment[];
|
||||
export declare function serialize(segments: Segment[]): string;
|
@ -0,0 +1,107 @@
|
||||
const COMMAND = 0;
|
||||
const NUMBER = 1;
|
||||
const EOD = 2;
|
||||
const PARAMS = { A: 7, a: 7, C: 6, c: 6, H: 1, h: 1, L: 2, l: 2, M: 2, m: 2, Q: 4, q: 4, S: 4, s: 4, T: 2, t: 2, V: 1, v: 1, Z: 0, z: 0 };
|
||||
function tokenize(d) {
|
||||
const tokens = new Array();
|
||||
while (d !== '') {
|
||||
if (d.match(/^([ \t\r\n,]+)/)) {
|
||||
d = d.substr(RegExp.$1.length);
|
||||
}
|
||||
else if (d.match(/^([aAcChHlLmMqQsStTvVzZ])/)) {
|
||||
tokens[tokens.length] = { type: COMMAND, text: RegExp.$1 };
|
||||
d = d.substr(RegExp.$1.length);
|
||||
}
|
||||
else if (d.match(/^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/)) {
|
||||
tokens[tokens.length] = { type: NUMBER, text: `${parseFloat(RegExp.$1)}` };
|
||||
d = d.substr(RegExp.$1.length);
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
tokens[tokens.length] = { type: EOD, text: '' };
|
||||
return tokens;
|
||||
}
|
||||
function isType(token, type) {
|
||||
return token.type === type;
|
||||
}
|
||||
export function parsePath(d) {
|
||||
const segments = [];
|
||||
const tokens = tokenize(d);
|
||||
let mode = 'BOD';
|
||||
let index = 0;
|
||||
let token = tokens[index];
|
||||
while (!isType(token, EOD)) {
|
||||
let paramsCount = 0;
|
||||
const params = [];
|
||||
if (mode === 'BOD') {
|
||||
if (token.text === 'M' || token.text === 'm') {
|
||||
index++;
|
||||
paramsCount = PARAMS[token.text];
|
||||
mode = token.text;
|
||||
}
|
||||
else {
|
||||
return parsePath('M0,0' + d);
|
||||
}
|
||||
}
|
||||
else if (isType(token, NUMBER)) {
|
||||
paramsCount = PARAMS[mode];
|
||||
}
|
||||
else {
|
||||
index++;
|
||||
paramsCount = PARAMS[token.text];
|
||||
mode = token.text;
|
||||
}
|
||||
if ((index + paramsCount) < tokens.length) {
|
||||
for (let i = index; i < index + paramsCount; i++) {
|
||||
const numbeToken = tokens[i];
|
||||
if (isType(numbeToken, NUMBER)) {
|
||||
params[params.length] = +numbeToken.text;
|
||||
}
|
||||
else {
|
||||
throw new Error('Param not a number: ' + mode + ',' + numbeToken.text);
|
||||
}
|
||||
}
|
||||
if (typeof PARAMS[mode] === 'number') {
|
||||
const segment = { key: mode, data: params };
|
||||
segments.push(segment);
|
||||
index += paramsCount;
|
||||
token = tokens[index];
|
||||
if (mode === 'M')
|
||||
mode = 'L';
|
||||
if (mode === 'm')
|
||||
mode = 'l';
|
||||
}
|
||||
else {
|
||||
throw new Error('Bad segment: ' + mode);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error('Path data ended short');
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
export function serialize(segments) {
|
||||
const tokens = [];
|
||||
for (const { key, data } of segments) {
|
||||
tokens.push(key);
|
||||
switch (key) {
|
||||
case 'C':
|
||||
case 'c':
|
||||
tokens.push(data[0], `${data[1]},`, data[2], `${data[3]},`, data[4], data[5]);
|
||||
break;
|
||||
case 'S':
|
||||
case 's':
|
||||
case 'Q':
|
||||
case 'q':
|
||||
tokens.push(data[0], `${data[1]},`, data[2], data[3]);
|
||||
break;
|
||||
default:
|
||||
tokens.push(...data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tokens.join(' ');
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
{
|
||||
"_from": "path-data-parser@^0.1.0",
|
||||
"_id": "path-data-parser@0.1.0",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
|
||||
"_location": "/path-data-parser",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "path-data-parser@^0.1.0",
|
||||
"name": "path-data-parser",
|
||||
"escapedName": "path-data-parser",
|
||||
"rawSpec": "^0.1.0",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^0.1.0"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/points-on-path",
|
||||
"/roughjs"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
|
||||
"_shasum": "8f5ba5cc70fc7becb3dcefaea08e2659aba60b8c",
|
||||
"_spec": "path-data-parser@^0.1.0",
|
||||
"_where": "/Users/1009738/PracticalVision/node_modules/roughjs",
|
||||
"author": {
|
||||
"name": "Preet Shihn"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/pshihn/path-data-parser/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"deprecated": false,
|
||||
"description": "Yet another SVG path parser. This one's tiny",
|
||||
"devDependencies": {
|
||||
"tslint": "^6.1.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"homepage": "https://github.com/pshihn/path-data-parser#readme",
|
||||
"keywords": [
|
||||
"svg path parser",
|
||||
"SVG",
|
||||
"path parser",
|
||||
"SVGPATH",
|
||||
"pathdata",
|
||||
"svg parser"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"module": "lib/index.js",
|
||||
"name": "path-data-parser",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pshihn/path-data-parser.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf lib && tsc",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"types": "lib/index.d.ts",
|
||||
"version": "0.1.0"
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
import { Segment } from './parser.js';
|
||||
|
||||
// Translate relative commands to absolute commands
|
||||
export function absolutize(segments: Segment[]): Segment[] {
|
||||
let cx = 0, cy = 0;
|
||||
let subx = 0, suby = 0;
|
||||
const out: Segment[] = [];
|
||||
for (const { key, data } of segments) {
|
||||
switch (key) {
|
||||
case 'M':
|
||||
out.push({ key: 'M', data: [...data] });
|
||||
[cx, cy] = data;
|
||||
[subx, suby] = data;
|
||||
break;
|
||||
case 'm':
|
||||
cx += data[0];
|
||||
cy += data[1];
|
||||
out.push({ key: 'M', data: [cx, cy] });
|
||||
subx = cx;
|
||||
suby = cy;
|
||||
break;
|
||||
case 'L':
|
||||
out.push({ key: 'L', data: [...data] });
|
||||
[cx, cy] = data;
|
||||
break;
|
||||
case 'l':
|
||||
cx += data[0];
|
||||
cy += data[1];
|
||||
out.push({ key: 'L', data: [cx, cy] });
|
||||
break;
|
||||
case 'C':
|
||||
out.push({ key: 'C', data: [...data] });
|
||||
cx = data[4];
|
||||
cy = data[5];
|
||||
break;
|
||||
case 'c': {
|
||||
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
|
||||
out.push({ key: 'C', data: newdata });
|
||||
cx = newdata[4];
|
||||
cy = newdata[5];
|
||||
break;
|
||||
}
|
||||
case 'Q':
|
||||
out.push({ key: 'Q', data: [...data] });
|
||||
cx = data[2];
|
||||
cy = data[3];
|
||||
break;
|
||||
case 'q': {
|
||||
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
|
||||
out.push({ key: 'Q', data: newdata });
|
||||
cx = newdata[2];
|
||||
cy = newdata[3];
|
||||
break;
|
||||
}
|
||||
case 'A':
|
||||
out.push({ key: 'A', data: [...data] });
|
||||
cx = data[5];
|
||||
cy = data[6];
|
||||
break;
|
||||
case 'a':
|
||||
cx += data[5];
|
||||
cy += data[6];
|
||||
out.push({ key: 'A', data: [data[0], data[1], data[2], data[3], data[4], cx, cy] });
|
||||
break;
|
||||
case 'H':
|
||||
out.push({ key: 'H', data: [...data] });
|
||||
cx = data[0];
|
||||
break;
|
||||
case 'h':
|
||||
cx += data[0];
|
||||
out.push({ key: 'H', data: [cx] });
|
||||
break;
|
||||
case 'V':
|
||||
out.push({ key: 'V', data: [...data] });
|
||||
cy = data[0];
|
||||
break;
|
||||
case 'v':
|
||||
cy += data[0];
|
||||
out.push({ key: 'V', data: [cy] });
|
||||
break;
|
||||
case 'S':
|
||||
out.push({ key: 'S', data: [...data] });
|
||||
cx = data[2];
|
||||
cy = data[3];
|
||||
break;
|
||||
case 's': {
|
||||
const newdata = data.map((d, i) => (i % 2) ? (d + cy) : (d + cx));
|
||||
out.push({ key: 'S', data: newdata });
|
||||
cx = newdata[2];
|
||||
cy = newdata[3];
|
||||
break;
|
||||
}
|
||||
case 'T':
|
||||
out.push({ key: 'T', data: [...data] });
|
||||
cx = data[0];
|
||||
cy = data[1];
|
||||
break;
|
||||
case 't':
|
||||
cx += data[0];
|
||||
cy += data[1];
|
||||
out.push({ key: 'T', data: [cx, cy] });
|
||||
break;
|
||||
case 'Z':
|
||||
case 'z':
|
||||
out.push({ key: 'Z', data: [] });
|
||||
cx = subx;
|
||||
cy = suby;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export { parsePath, serialize } from './parser.js';
|
||||
export { absolutize } from './absolutize.js';
|
||||
export { normalize } from './normalize.js';
|
@ -0,0 +1,242 @@
|
||||
import { Segment } from './parser.js';
|
||||
|
||||
// Normalize path to include only M, L, C, and Z commands
|
||||
export function normalize(segments: Segment[]): Segment[] {
|
||||
const out: Segment[] = [];
|
||||
|
||||
let lastType = '';
|
||||
let cx = 0, cy = 0;
|
||||
let subx = 0, suby = 0;
|
||||
let lcx = 0, lcy = 0;
|
||||
|
||||
for (const { key, data } of segments) {
|
||||
switch (key) {
|
||||
case 'M':
|
||||
out.push({ key: 'M', data: [...data] });
|
||||
[cx, cy] = data;
|
||||
[subx, suby] = data;
|
||||
break;
|
||||
case 'C':
|
||||
out.push({ key: 'C', data: [...data] });
|
||||
cx = data[4];
|
||||
cy = data[5];
|
||||
lcx = data[2];
|
||||
lcy = data[3];
|
||||
break;
|
||||
case 'L':
|
||||
out.push({ key: 'L', data: [...data] });
|
||||
[cx, cy] = data;
|
||||
break;
|
||||
case 'H':
|
||||
cx = data[0];
|
||||
out.push({ key: 'L', data: [cx, cy] });
|
||||
break;
|
||||
case 'V':
|
||||
cy = data[0];
|
||||
out.push({ key: 'L', data: [cx, cy] });
|
||||
break;
|
||||
case 'S': {
|
||||
let cx1 = 0, cy1 = 0;
|
||||
if (lastType === 'C' || lastType === 'S') {
|
||||
cx1 = cx + (cx - lcx);
|
||||
cy1 = cy + (cy - lcy);
|
||||
} else {
|
||||
cx1 = cx;
|
||||
cy1 = cy;
|
||||
}
|
||||
out.push({ key: 'C', data: [cx1, cy1, ...data] });
|
||||
lcx = data[0];
|
||||
lcy = data[1];
|
||||
cx = data[2];
|
||||
cy = data[3];
|
||||
break;
|
||||
}
|
||||
case 'T': {
|
||||
const [x, y] = data;
|
||||
let x1 = 0, y1 = 0;
|
||||
if (lastType === 'Q' || lastType === 'T') {
|
||||
x1 = cx + (cx - lcx);
|
||||
y1 = cy + (cy - lcy);
|
||||
} else {
|
||||
x1 = cx;
|
||||
y1 = cy;
|
||||
}
|
||||
const cx1 = cx + 2 * (x1 - cx) / 3;
|
||||
const cy1 = cy + 2 * (y1 - cy) / 3;
|
||||
const cx2 = x + 2 * (x1 - x) / 3;
|
||||
const cy2 = y + 2 * (y1 - y) / 3;
|
||||
out.push({ key: 'C', data: [cx1, cy1, cx2, cy2, x, y] });
|
||||
lcx = x1;
|
||||
lcy = y1;
|
||||
cx = x;
|
||||
cy = y;
|
||||
break;
|
||||
}
|
||||
case 'Q': {
|
||||
const [x1, y1, x, y] = data;
|
||||
const cx1 = cx + 2 * (x1 - cx) / 3;
|
||||
const cy1 = cy + 2 * (y1 - cy) / 3;
|
||||
const cx2 = x + 2 * (x1 - x) / 3;
|
||||
const cy2 = y + 2 * (y1 - y) / 3;
|
||||
out.push({ key: 'C', data: [cx1, cy1, cx2, cy2, x, y] });
|
||||
lcx = x1;
|
||||
lcy = y1;
|
||||
cx = x;
|
||||
cy = y;
|
||||
break;
|
||||
}
|
||||
case 'A': {
|
||||
const r1 = Math.abs(data[0]);
|
||||
const r2 = Math.abs(data[1]);
|
||||
const angle = data[2];
|
||||
const largeArcFlag = data[3];
|
||||
const sweepFlag = data[4];
|
||||
const x = data[5];
|
||||
const y = data[6];
|
||||
if (r1 === 0 || r2 === 0) {
|
||||
out.push({ key: 'C', data: [cx, cy, x, y, x, y] });
|
||||
cx = x;
|
||||
cy = y;
|
||||
} else {
|
||||
if (cx !== x || cy !== y) {
|
||||
const curves: number[][] = arcToCubicCurves(cx, cy, x, y, r1, r2, angle, largeArcFlag, sweepFlag);
|
||||
curves.forEach(function (curve) {
|
||||
out.push({ key: 'C', data: curve });
|
||||
});
|
||||
cx = x;
|
||||
cy = y;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Z':
|
||||
out.push({ key: 'Z', data: [] });
|
||||
cx = subx;
|
||||
cy = suby;
|
||||
break;
|
||||
}
|
||||
lastType = key;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function degToRad(degrees: number): number {
|
||||
return (Math.PI * degrees) / 180;
|
||||
}
|
||||
|
||||
function rotate(x: number, y: number, angleRad: number): [number, number] {
|
||||
const X = x * Math.cos(angleRad) - y * Math.sin(angleRad);
|
||||
const Y = x * Math.sin(angleRad) + y * Math.cos(angleRad);
|
||||
return [X, Y];
|
||||
}
|
||||
|
||||
function arcToCubicCurves(x1: number, y1: number, x2: number, y2: number, r1: number, r2: number, angle: number, largeArcFlag: number, sweepFlag: number, recursive?: number[]): number[][] {
|
||||
const angleRad = degToRad(angle);
|
||||
let params: number[][] = [];
|
||||
|
||||
let f1 = 0, f2 = 0, cx = 0, cy = 0;
|
||||
if (recursive) {
|
||||
[f1, f2, cx, cy] = recursive;
|
||||
} else {
|
||||
[x1, y1] = rotate(x1, y1, -angleRad);
|
||||
[x2, y2] = rotate(x2, y2, -angleRad);
|
||||
|
||||
const x = (x1 - x2) / 2;
|
||||
const y = (y1 - y2) / 2;
|
||||
let h = (x * x) / (r1 * r1) + (y * y) / (r2 * r2);
|
||||
if (h > 1) {
|
||||
h = Math.sqrt(h);
|
||||
r1 = h * r1;
|
||||
r2 = h * r2;
|
||||
}
|
||||
|
||||
const sign = (largeArcFlag === sweepFlag) ? -1 : 1;
|
||||
|
||||
const r1Pow = r1 * r1;
|
||||
const r2Pow = r2 * r2;
|
||||
|
||||
const left = r1Pow * r2Pow - r1Pow * y * y - r2Pow * x * x;
|
||||
const right = r1Pow * y * y + r2Pow * x * x;
|
||||
|
||||
const k = sign * Math.sqrt(Math.abs(left / right));
|
||||
|
||||
cx = k * r1 * y / r2 + (x1 + x2) / 2;
|
||||
cy = k * -r2 * x / r1 + (y1 + y2) / 2;
|
||||
|
||||
f1 = Math.asin(parseFloat(((y1 - cy) / r2).toFixed(9)));
|
||||
f2 = Math.asin(parseFloat(((y2 - cy) / r2).toFixed(9)));
|
||||
|
||||
if (x1 < cx) {
|
||||
f1 = Math.PI - f1;
|
||||
}
|
||||
if (x2 < cx) {
|
||||
f2 = Math.PI - f2;
|
||||
}
|
||||
|
||||
if (f1 < 0) {
|
||||
f1 = Math.PI * 2 + f1;
|
||||
}
|
||||
if (f2 < 0) {
|
||||
f2 = Math.PI * 2 + f2;
|
||||
}
|
||||
|
||||
if (sweepFlag && f1 > f2) {
|
||||
f1 = f1 - Math.PI * 2;
|
||||
}
|
||||
if (!sweepFlag && f2 > f1) {
|
||||
f2 = f2 - Math.PI * 2;
|
||||
}
|
||||
}
|
||||
|
||||
let df = f2 - f1;
|
||||
|
||||
if (Math.abs(df) > (Math.PI * 120 / 180)) {
|
||||
const f2old = f2;
|
||||
const x2old = x2;
|
||||
const y2old = y2;
|
||||
|
||||
if (sweepFlag && f2 > f1) {
|
||||
f2 = f1 + (Math.PI * 120 / 180) * (1);
|
||||
}
|
||||
else {
|
||||
f2 = f1 + (Math.PI * 120 / 180) * (-1);
|
||||
}
|
||||
|
||||
x2 = cx + r1 * Math.cos(f2);
|
||||
y2 = cy + r2 * Math.sin(f2);
|
||||
params = arcToCubicCurves(x2, y2, x2old, y2old, r1, r2, angle, 0, sweepFlag, [f2, f2old, cx, cy]);
|
||||
}
|
||||
|
||||
df = f2 - f1;
|
||||
|
||||
const c1 = Math.cos(f1);
|
||||
const s1 = Math.sin(f1);
|
||||
const c2 = Math.cos(f2);
|
||||
const s2 = Math.sin(f2);
|
||||
const t = Math.tan(df / 4);
|
||||
const hx = 4 / 3 * r1 * t;
|
||||
const hy = 4 / 3 * r2 * t;
|
||||
|
||||
const m1 = [x1, y1];
|
||||
const m2 = [x1 + hx * s1, y1 - hy * c1];
|
||||
const m3 = [x2 + hx * s2, y2 - hy * c2];
|
||||
const m4 = [x2, y2];
|
||||
|
||||
m2[0] = 2 * m1[0] - m2[0];
|
||||
m2[1] = 2 * m1[1] - m2[1];
|
||||
|
||||
if (recursive) {
|
||||
return [m2, m3, m4].concat(params);
|
||||
}
|
||||
else {
|
||||
params = [m2, m3, m4].concat(params);
|
||||
const curves = [];
|
||||
for (let i = 0; i < params.length; i += 3) {
|
||||
const r1 = rotate(params[i][0], params[i][1], angleRad);
|
||||
const r2 = rotate(params[i + 1][0], params[i + 1][1], angleRad);
|
||||
const r3 = rotate(params[i + 2][0], params[i + 2][1], angleRad);
|
||||
curves.push([r1[0], r1[1], r2[0], r2[1], r3[0], r3[1]]);
|
||||
}
|
||||
return curves;
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
const COMMAND = 0;
|
||||
const NUMBER = 1;
|
||||
const EOD = 2;
|
||||
type TokenType = 0 | 1 | 2;
|
||||
|
||||
interface PathToken {
|
||||
type: TokenType;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
key: string;
|
||||
data: number[];
|
||||
}
|
||||
|
||||
const PARAMS: { [key: string]: number } = { A: 7, a: 7, C: 6, c: 6, H: 1, h: 1, L: 2, l: 2, M: 2, m: 2, Q: 4, q: 4, S: 4, s: 4, T: 2, t: 2, V: 1, v: 1, Z: 0, z: 0 };
|
||||
|
||||
function tokenize(d: string): PathToken[] {
|
||||
const tokens: PathToken[] = new Array();
|
||||
while (d !== '') {
|
||||
if (d.match(/^([ \t\r\n,]+)/)) {
|
||||
d = d.substr(RegExp.$1.length);
|
||||
} else if (d.match(/^([aAcChHlLmMqQsStTvVzZ])/)) {
|
||||
tokens[tokens.length] = { type: COMMAND, text: RegExp.$1 };
|
||||
d = d.substr(RegExp.$1.length);
|
||||
} else if (d.match(/^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/)) {
|
||||
tokens[tokens.length] = { type: NUMBER, text: `${parseFloat(RegExp.$1)}` };
|
||||
d = d.substr(RegExp.$1.length);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
tokens[tokens.length] = { type: EOD, text: '' };
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function isType(token: PathToken, type: number) {
|
||||
return token.type === type;
|
||||
}
|
||||
|
||||
export function parsePath(d: string): Segment[] {
|
||||
const segments: Segment[] = [];
|
||||
const tokens = tokenize(d);
|
||||
let mode = 'BOD';
|
||||
let index = 0;
|
||||
let token = tokens[index];
|
||||
while (!isType(token, EOD)) {
|
||||
let paramsCount = 0;
|
||||
const params: number[] = [];
|
||||
if (mode === 'BOD') {
|
||||
if (token.text === 'M' || token.text === 'm') {
|
||||
index++;
|
||||
paramsCount = PARAMS[token.text];
|
||||
mode = token.text;
|
||||
} else {
|
||||
return parsePath('M0,0' + d);
|
||||
}
|
||||
} else if (isType(token, NUMBER)) {
|
||||
paramsCount = PARAMS[mode];
|
||||
} else {
|
||||
index++;
|
||||
paramsCount = PARAMS[token.text];
|
||||
mode = token.text;
|
||||
}
|
||||
if ((index + paramsCount) < tokens.length) {
|
||||
for (let i = index; i < index + paramsCount; i++) {
|
||||
const numbeToken = tokens[i];
|
||||
if (isType(numbeToken, NUMBER)) {
|
||||
params[params.length] = +numbeToken.text;
|
||||
}
|
||||
else {
|
||||
throw new Error('Param not a number: ' + mode + ',' + numbeToken.text);
|
||||
}
|
||||
}
|
||||
if (typeof PARAMS[mode] === 'number') {
|
||||
const segment: Segment = { key: mode, data: params };
|
||||
segments.push(segment);
|
||||
index += paramsCount;
|
||||
token = tokens[index];
|
||||
if (mode === 'M') mode = 'L';
|
||||
if (mode === 'm') mode = 'l';
|
||||
} else {
|
||||
throw new Error('Bad segment: ' + mode);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Path data ended short');
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function serialize(segments: Segment[]): string {
|
||||
const tokens: (string | number)[] = [];
|
||||
for (const { key, data } of segments) {
|
||||
tokens.push(key);
|
||||
switch (key) {
|
||||
case 'C':
|
||||
case 'c':
|
||||
tokens.push(data[0], `${data[1]},`, data[2], `${data[3]},`, data[4], data[5]);
|
||||
break;
|
||||
case 'S':
|
||||
case 's':
|
||||
case 'Q':
|
||||
case 'q':
|
||||
tokens.push(data[0], `${data[1]},`, data[2], data[3]);
|
||||
break;
|
||||
default:
|
||||
tokens.push(...data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tokens.join(' ');
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"es2017"
|
||||
],
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
{
|
||||
"rules": {
|
||||
"arrow-parens": true,
|
||||
"class-name": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces",
|
||||
2
|
||||
],
|
||||
"prefer-const": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": true,
|
||||
"no-internal-module": true,
|
||||
"no-trailing-whitespace": false,
|
||||
"no-var-keyword": true,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single",
|
||||
"avoid-escape"
|
||||
],
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"trailing-comma": [
|
||||
true,
|
||||
"multiline"
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"variable-name": [
|
||||
true,
|
||||
"ban-keywords"
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Preet Shihn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,2 @@
|
||||
import { Point } from './index.js';
|
||||
export declare function curveToBezier(pointsIn: Point[], curveTightness?: number): Point[];
|
@ -0,0 +1,35 @@
|
||||
function clone(p) {
|
||||
return [...p];
|
||||
}
|
||||
export function curveToBezier(pointsIn, curveTightness = 0) {
|
||||
const len = pointsIn.length;
|
||||
if (len < 3) {
|
||||
throw new Error('A curve must have at least three points.');
|
||||
}
|
||||
const out = [];
|
||||
if (len === 3) {
|
||||
out.push(clone(pointsIn[0]), clone(pointsIn[1]), clone(pointsIn[2]), clone(pointsIn[2]));
|
||||
}
|
||||
else {
|
||||
const points = [];
|
||||
points.push(pointsIn[0], pointsIn[0]);
|
||||
for (let i = 1; i < pointsIn.length; i++) {
|
||||
points.push(pointsIn[i]);
|
||||
if (i === (pointsIn.length - 1)) {
|
||||
points.push(pointsIn[i]);
|
||||
}
|
||||
}
|
||||
const b = [];
|
||||
const s = 1 - curveTightness;
|
||||
out.push(clone(points[0]));
|
||||
for (let i = 1; (i + 2) < points.length; i++) {
|
||||
const cachedVertArray = points[i];
|
||||
b[0] = [cachedVertArray[0], cachedVertArray[1]];
|
||||
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];
|
||||
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];
|
||||
b[3] = [points[i + 1][0], points[i + 1][1]];
|
||||
out.push(b[1], b[2], b[3]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export declare type Point = [number, number];
|
||||
export declare function simplify(points: Point[], distance: number): Point[];
|
||||
export declare function pointsOnBezierCurves(points: Point[], tolerance?: number, distance?: number): Point[];
|
@ -0,0 +1,59 @@
|
||||
{
|
||||
"_from": "points-on-curve@^0.2.0",
|
||||
"_id": "points-on-curve@0.2.0",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
|
||||
"_location": "/points-on-curve",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "points-on-curve@^0.2.0",
|
||||
"name": "points-on-curve",
|
||||
"escapedName": "points-on-curve",
|
||||
"rawSpec": "^0.2.0",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^0.2.0"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/points-on-path",
|
||||
"/roughjs"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
|
||||
"_shasum": "7dbb98c43791859434284761330fa893cb81b4d1",
|
||||
"_spec": "points-on-curve@^0.2.0",
|
||||
"_where": "/Users/1009738/PracticalVision/node_modules/roughjs",
|
||||
"author": {
|
||||
"name": "Preet Shihn"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/pshihn/bezier-points/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"deprecated": false,
|
||||
"description": "Estimate points on a bezier curve or a set of connexted bezier curves",
|
||||
"devDependencies": {
|
||||
"tslint": "^6.1.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"homepage": "https://github.com/pshihn/bezier-points#readme",
|
||||
"keywords": [
|
||||
"Bezier",
|
||||
"graphics"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"module": "lib/index.js",
|
||||
"name": "points-on-curve",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pshihn/bezier-points.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf lib && tsc",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"types": "lib/index.d.ts",
|
||||
"version": "0.2.0"
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { Point } from './index.js';
|
||||
|
||||
function clone(p: Point): Point {
|
||||
return [...p] as Point;
|
||||
}
|
||||
|
||||
export function curveToBezier(pointsIn: Point[], curveTightness = 0): Point[] {
|
||||
const len = pointsIn.length;
|
||||
if (len < 3) {
|
||||
throw new Error('A curve must have at least three points.');
|
||||
}
|
||||
const out: Point[] = [];
|
||||
if (len === 3) {
|
||||
out.push(
|
||||
clone(pointsIn[0]),
|
||||
clone(pointsIn[1]),
|
||||
clone(pointsIn[2]),
|
||||
clone(pointsIn[2])
|
||||
);
|
||||
} else {
|
||||
const points: Point[] = [];
|
||||
points.push(pointsIn[0], pointsIn[0]);
|
||||
for (let i = 1; i < pointsIn.length; i++) {
|
||||
points.push(pointsIn[i]);
|
||||
if (i === (pointsIn.length - 1)) {
|
||||
points.push(pointsIn[i]);
|
||||
}
|
||||
}
|
||||
const b: Point[] = [];
|
||||
const s = 1 - curveTightness;
|
||||
out.push(clone(points[0]));
|
||||
for (let i = 1; (i + 2) < points.length; i++) {
|
||||
const cachedVertArray = points[i];
|
||||
b[0] = [cachedVertArray[0], cachedVertArray[1]];
|
||||
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];
|
||||
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];
|
||||
b[3] = [points[i + 1][0], points[i + 1][1]];
|
||||
out.push(b[1], b[2], b[3]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"es2017"
|
||||
],
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
{
|
||||
"rules": {
|
||||
"arrow-parens": true,
|
||||
"class-name": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces",
|
||||
2
|
||||
],
|
||||
"prefer-const": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": true,
|
||||
"no-internal-module": true,
|
||||
"no-trailing-whitespace": false,
|
||||
"no-var-keyword": true,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single",
|
||||
"avoid-escape"
|
||||
],
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"trailing-comma": [
|
||||
true,
|
||||
"multiline"
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"variable-name": [
|
||||
true,
|
||||
"ban-keywords"
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Preet
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,55 @@
|
||||
# points-on-path
|
||||
|
||||
This package calculate the points on a SVG Path with a certain tolerance. It can also simplify the shape to use fewer points.
|
||||
This can really usefule when estimating lines/polygons for paths in WebGL or for Hit/Cosllision detections.
|
||||
|
||||
This package essentially combines packages [path-data-parser](https://github.com/pshihn/path-data-parser) and [points-on-curve](https://github.com/pshihn/bezier-points)
|
||||
|
||||
## Install
|
||||
|
||||
From npm
|
||||
|
||||
```
|
||||
npm install --save points-on-path
|
||||
```
|
||||
|
||||
The package is distributed as an ES6 module.
|
||||
|
||||
## Usage
|
||||
|
||||
### pointsOnPath(path: string, tolerance?: number, distance?: number): PathPoints
|
||||
|
||||
Pass in a SVG path string and get back a `PathPoints` object. A `PathPoints` gives you a list of points (each being a an array of 2 numbers `[x, y]`), and a flag telling you if the path is actually composed of multiple disconnected paths.
|
||||
|
||||
```javascript
|
||||
PathPoints {
|
||||
points: Point[];
|
||||
continuous: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Take this path for example:
|
||||
|
||||
![points on path](https://user-images.githubusercontent.com/833927/79054782-ba8d0300-7bfc-11ea-8f16-ed36001c56c9.png)
|
||||
|
||||
and estimate the points on the path
|
||||
|
||||
```javascript
|
||||
import { pointsOnPath } from 'points-on-path';
|
||||
|
||||
const points = pointsOnPath('M240,100c50,0,0,125,50,100s0,-125,50,-150s175,50,50,100s-175,50,-300,0s0,-125,50,-100s0,125,50,150s0,-100,50,-100');
|
||||
// plotPoints(points);
|
||||
```
|
||||
|
||||
![points on path](https://user-images.githubusercontent.com/833927/79054650-8d8c2080-7bfb-11ea-93cf-2c070dfe63c5.png)
|
||||
|
||||
The method also accepts two optional values `tolerance` and `distance`. These are described by [points-on-curve](https://github.com/pshihn/bezier-points); to estimate more tolerant and fewer points.
|
||||
|
||||
|
||||
![points on path](https://user-images.githubusercontent.com/833927/79054652-8e24b700-7bfb-11ea-8ff8-68dce51a3940.png)
|
||||
|
||||
![points on path](https://user-images.githubusercontent.com/833927/79054653-8ebd4d80-7bfb-11ea-8645-a5a0ed81cf84.png)
|
||||
|
||||
## License
|
||||
[MIT License](https://github.com/pshihn/points-on-path/blob/master/LICENSE)
|
||||
|
@ -0,0 +1,3 @@
|
||||
import { Point } from 'points-on-curve';
|
||||
export { Point } from 'points-on-curve';
|
||||
export declare function pointsOnPath(path: string, tolerance?: number, distance?: number): Point[][];
|
@ -0,0 +1,61 @@
|
||||
import { pointsOnBezierCurves, simplify } from 'points-on-curve';
|
||||
import { parsePath, absolutize, normalize } from 'path-data-parser';
|
||||
export function pointsOnPath(path, tolerance, distance) {
|
||||
const segments = parsePath(path);
|
||||
const normalized = normalize(absolutize(segments));
|
||||
const sets = [];
|
||||
let currentPoints = [];
|
||||
let start = [0, 0];
|
||||
let pendingCurve = [];
|
||||
const appendPendingCurve = () => {
|
||||
if (pendingCurve.length >= 4) {
|
||||
currentPoints.push(...pointsOnBezierCurves(pendingCurve, tolerance));
|
||||
}
|
||||
pendingCurve = [];
|
||||
};
|
||||
const appendPendingPoints = () => {
|
||||
appendPendingCurve();
|
||||
if (currentPoints.length) {
|
||||
sets.push(currentPoints);
|
||||
currentPoints = [];
|
||||
}
|
||||
};
|
||||
for (const { key, data } of normalized) {
|
||||
switch (key) {
|
||||
case 'M':
|
||||
appendPendingPoints();
|
||||
start = [data[0], data[1]];
|
||||
currentPoints.push(start);
|
||||
break;
|
||||
case 'L':
|
||||
appendPendingCurve();
|
||||
currentPoints.push([data[0], data[1]]);
|
||||
break;
|
||||
case 'C':
|
||||
if (!pendingCurve.length) {
|
||||
const lastPoint = currentPoints.length ? currentPoints[currentPoints.length - 1] : start;
|
||||
pendingCurve.push([lastPoint[0], lastPoint[1]]);
|
||||
}
|
||||
pendingCurve.push([data[0], data[1]]);
|
||||
pendingCurve.push([data[2], data[3]]);
|
||||
pendingCurve.push([data[4], data[5]]);
|
||||
break;
|
||||
case 'Z':
|
||||
appendPendingCurve();
|
||||
currentPoints.push([start[0], start[1]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
appendPendingPoints();
|
||||
if (!distance) {
|
||||
return sets;
|
||||
}
|
||||
const out = [];
|
||||
for (const set of sets) {
|
||||
const simplifiedSet = simplify(set, distance);
|
||||
if (simplifiedSet.length) {
|
||||
out.push(simplifiedSet);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
{
|
||||
"_from": "points-on-path@^0.2.1",
|
||||
"_id": "points-on-path@0.2.1",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
|
||||
"_location": "/points-on-path",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "points-on-path@^0.2.1",
|
||||
"name": "points-on-path",
|
||||
"escapedName": "points-on-path",
|
||||
"rawSpec": "^0.2.1",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^0.2.1"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/roughjs"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
|
||||
"_shasum": "553202b5424c53bed37135b318858eacff85dd52",
|
||||
"_spec": "points-on-path@^0.2.1",
|
||||
"_where": "/Users/1009738/PracticalVision/node_modules/roughjs",
|
||||
"author": {
|
||||
"name": "Preet Shihn"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/pshihn/points-on-path/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {
|
||||
"path-data-parser": "0.1.0",
|
||||
"points-on-curve": "0.2.0"
|
||||
},
|
||||
"deprecated": false,
|
||||
"description": "Estimate points on a SVG path",
|
||||
"devDependencies": {
|
||||
"tslint": "^6.1.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"homepage": "https://github.com/pshihn/points-on-path#readme",
|
||||
"keywords": [
|
||||
"SVG",
|
||||
"graphics"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"module": "lib/index.js",
|
||||
"name": "points-on-path",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pshihn/points-on-path.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf lib && tsc",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"types": "lib/index.d.ts",
|
||||
"version": "0.2.1"
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import { Point, pointsOnBezierCurves, simplify } from 'points-on-curve';
|
||||
import { parsePath, absolutize, normalize } from 'path-data-parser';
|
||||
|
||||
export { Point } from 'points-on-curve';
|
||||
|
||||
export function pointsOnPath(path: string, tolerance?: number, distance?: number): Point[][] {
|
||||
const segments = parsePath(path);
|
||||
const normalized = normalize(absolutize(segments));
|
||||
|
||||
const sets: Point[][] = [];
|
||||
let currentPoints: Point[] = [];
|
||||
let start: Point = [0, 0];
|
||||
let pendingCurve: Point[] = [];
|
||||
|
||||
const appendPendingCurve = () => {
|
||||
if (pendingCurve.length >= 4) {
|
||||
currentPoints.push(...pointsOnBezierCurves(pendingCurve, tolerance));
|
||||
}
|
||||
pendingCurve = [];
|
||||
};
|
||||
|
||||
const appendPendingPoints = () => {
|
||||
appendPendingCurve();
|
||||
if (currentPoints.length) {
|
||||
sets.push(currentPoints);
|
||||
currentPoints = [];
|
||||
}
|
||||
};
|
||||
|
||||
for (const { key, data } of normalized) {
|
||||
switch (key) {
|
||||
case 'M':
|
||||
appendPendingPoints();
|
||||
start = [data[0], data[1]];
|
||||
currentPoints.push(start);
|
||||
break;
|
||||
case 'L':
|
||||
appendPendingCurve();
|
||||
currentPoints.push([data[0], data[1]]);
|
||||
break;
|
||||
case 'C':
|
||||
if (!pendingCurve.length) {
|
||||
const lastPoint = currentPoints.length ? currentPoints[currentPoints.length - 1] : start;
|
||||
pendingCurve.push([lastPoint[0], lastPoint[1]]);
|
||||
}
|
||||
pendingCurve.push([data[0], data[1]]);
|
||||
pendingCurve.push([data[2], data[3]]);
|
||||
pendingCurve.push([data[4], data[5]]);
|
||||
break;
|
||||
case 'Z':
|
||||
appendPendingCurve();
|
||||
currentPoints.push([start[0], start[1]]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
appendPendingPoints();
|
||||
|
||||
if (!distance) {
|
||||
return sets;
|
||||
}
|
||||
|
||||
const out: Point[][] = [];
|
||||
for (const set of sets) {
|
||||
const simplifiedSet = simplify(set, distance);
|
||||
if (simplifiedSet.length) {
|
||||
out.push(simplifiedSet);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"es2017"
|
||||
],
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
{
|
||||
"rules": {
|
||||
"arrow-parens": true,
|
||||
"class-name": true,
|
||||
"indent": [
|
||||
true,
|
||||
"spaces",
|
||||
2
|
||||
],
|
||||
"prefer-const": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": true,
|
||||
"no-internal-module": true,
|
||||
"no-trailing-whitespace": false,
|
||||
"no-var-keyword": true,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"single",
|
||||
"avoid-escape"
|
||||
],
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"trailing-comma": [
|
||||
true,
|
||||
"multiline"
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"variable-name": [
|
||||
true,
|
||||
"ban-keywords"
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.3.0] - 2020-05-11
|
||||
|
||||
* Added options to draw dashed lines - *strokeLineDash, strokeLineDashOffset, fillLineDash, fillLineDashOffset*
|
||||
* Added option to disable double stroking effect - *disableMultiStroke, disableMultiStrokeFill*.
|
||||
* Bug fixes to solid fill in SVG which was not obeying evenodd rules by default
|
||||
|
||||
## [4.1.0] - 2020-01-13
|
||||
|
||||
* Added ability to **fill** non-svg curves
|
||||
|
||||
## [4.0.0] - 2020-01-13
|
||||
|
||||
* Add optional seeding for randomness to ensure shapes generated with same arguments result in same vectors
|
||||
* Implemented a new algorithm for hachure generation based on scanlines. Smaller in code size, and about 20% faster
|
||||
* Algorithm update - adjust shape randomness and curve-step-counts based on the size of the shape
|
||||
* Removed async/worker builds - can be achieved in the app level, so no need to be in the lib
|
||||
* Support no-stroke sketching. `stroke: "none"` will not generate outline vectors anymore
|
||||
* Removed `sunburst` fill style - it had a lot of corner cases where it did not work, and not very popular.
|
||||
|
||||
## [3.1.0] - 2019-03-14
|
||||
|
||||
* Added three new fill styles: **sunburst**, **dashed**, and **zigzag-line**
|
||||
* Added three new properties in *Options* to support these fill styles:
|
||||
* **dashOffset** - length of dashes in dashed fill
|
||||
* **dashGap** - length of gap between dashes in dashed fill
|
||||
* **zigzagOffset** - width of zigzag triangle when using zigzag-lines fill
|
||||
|
||||
|
||||
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Preet Shihn
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,127 @@
|
||||
# Rough.js
|
||||
|
||||
<b>Rough.js</b> is a small (\<9 kB) graphics library that lets you draw in a _sketchy_, _hand-drawn-like_, style.
|
||||
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).
|
||||
|
||||
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).
|
||||
|
||||
![Rough.js sample](https://roughjs.com/images/cap_demo.png)
|
||||
|
||||
[@RoughLib](https://twitter.com/RoughLib) on Twitter.
|
||||
|
||||
## Install
|
||||
|
||||
from npm:
|
||||
|
||||
```
|
||||
npm install --save roughjs
|
||||
```
|
||||
|
||||
Or get the latest using unpkg: https://unpkg.com/roughjs@latest/bundled/rough.js
|
||||
|
||||
|
||||
If you are looking for bundled version in different formats, the npm package will have these in the following locations:
|
||||
|
||||
CommonJS: `roughjs/bundled/rough.cjs.js`
|
||||
|
||||
ESM: `roughjs/bundled/rough.esm.js`
|
||||
|
||||
Browser IIFE: `roughjs/bundled/rough.js`
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
![Rough.js rectangle](https://roughjs.com/images/m1.png)
|
||||
|
||||
```js
|
||||
const rc = rough.canvas(document.getElementById('canvas'));
|
||||
rc.rectangle(10, 10, 200, 200); // x, y, width, height
|
||||
```
|
||||
|
||||
or SVG
|
||||
|
||||
```js
|
||||
const rc = rough.svg(svg);
|
||||
let node = rc.rectangle(10, 10, 200, 200); // x, y, width, height
|
||||
svg.appendChild(node);
|
||||
```
|
||||
|
||||
### Lines and Ellipses
|
||||
|
||||
![Rough.js rectangle](https://roughjs.com/images/m2.png)
|
||||
|
||||
```js
|
||||
rc.circle(80, 120, 50); // centerX, centerY, diameter
|
||||
rc.ellipse(300, 100, 150, 80); // centerX, centerY, width, height
|
||||
rc.line(80, 120, 300, 100); // x1, y1, x2, y2
|
||||
```
|
||||
|
||||
### Filling
|
||||
|
||||
![Rough.js rectangle](https://roughjs.com/images/m3.png)
|
||||
|
||||
```js
|
||||
rc.circle(50, 50, 80, { fill: 'red' }); // fill with red hachure
|
||||
rc.rectangle(120, 15, 80, 80, { fill: 'red' });
|
||||
rc.circle(50, 150, 80, {
|
||||
fill: "rgb(10,150,10)",
|
||||
fillWeight: 3 // thicker lines for hachure
|
||||
});
|
||||
rc.rectangle(220, 15, 80, 80, {
|
||||
fill: 'red',
|
||||
hachureAngle: 60, // angle of hachure,
|
||||
hachureGap: 8
|
||||
});
|
||||
rc.rectangle(120, 105, 80, 80, {
|
||||
fill: 'rgba(255,0,200,0.2)',
|
||||
fillStyle: 'solid' // solid fill
|
||||
});
|
||||
```
|
||||
|
||||
Fill styles can be: **hachure**(default), **solid**, **zigzag**, **cross-hatch**, **dots**, **dashed**, or **zigzag-line**
|
||||
|
||||
![Rough.js fill examples](https://roughjs.com/images/m14.png)
|
||||
|
||||
### Sketching style
|
||||
|
||||
![Rough.js rectangle](https://roughjs.com/images/m4.png)
|
||||
|
||||
```js
|
||||
rc.rectangle(15, 15, 80, 80, { roughness: 0.5, fill: 'red' });
|
||||
rc.rectangle(120, 15, 80, 80, { roughness: 2.8, fill: 'blue' });
|
||||
rc.rectangle(220, 15, 80, 80, { bowing: 6, stroke: 'green', strokeWidth: 3 });
|
||||
```
|
||||
|
||||
### SVG Paths
|
||||
|
||||
![Rough.js paths](https://roughjs.com/images/m5.png)
|
||||
|
||||
```js
|
||||
rc.path('M80 80 A 45 45, 0, 0, 0, 125 125 L 125 80 Z', { fill: 'green' });
|
||||
rc.path('M230 80 A 45 45, 0, 1, 0, 275 125 L 275 80 Z', { fill: 'purple' });
|
||||
rc.path('M80 230 A 45 45, 0, 0, 1, 125 275 L 125 230 Z', { fill: 'red' });
|
||||
rc.path('M230 230 A 45 45, 0, 1, 1, 275 275 L 275 230 Z', { fill: 'blue' });
|
||||
```
|
||||
|
||||
SVG Path with simplification:
|
||||
|
||||
![Rough.js texas map](https://roughjs.com/images/m9.png) ![Rough.js texas map](https://roughjs.com/images/m10.png)
|
||||
|
||||
## Examples
|
||||
|
||||
![Rough.js US map](https://roughjs.com/images/m6.png)
|
||||
|
||||
[View examples here](https://github.com/pshihn/rough/wiki/Examples)
|
||||
|
||||
## API & Documentation
|
||||
|
||||
[Full Rough.js API](https://github.com/pshihn/rough/wiki)
|
||||
|
||||
## Credits
|
||||
|
||||
Some of the core algorithms were adapted from [handy](https://www.gicentre.net/software/#/handy/) processing lib.
|
||||
|
||||
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)
|
||||
|
||||
## License
|
||||
[MIT License](https://github.com/pshihn/rough/blob/master/LICENSE) (c) [Preet Shihn](https://twitter.com/preetster)
|
@ -0,0 +1,23 @@
|
||||
import { Config, Options, ResolvedOptions, Drawable } from './core';
|
||||
import { RoughGenerator } from './generator';
|
||||
import { Point } from './geometry';
|
||||
export declare class RoughCanvas {
|
||||
private gen;
|
||||
private canvas;
|
||||
private ctx;
|
||||
constructor(canvas: HTMLCanvasElement, config?: Config);
|
||||
draw(drawable: Drawable): void;
|
||||
private fillSketch;
|
||||
private _drawToContext;
|
||||
get generator(): RoughGenerator;
|
||||
getDefaultOptions(): ResolvedOptions;
|
||||
line(x1: number, y1: number, x2: number, y2: number, options?: Options): Drawable;
|
||||
rectangle(x: number, y: number, width: number, height: number, options?: Options): Drawable;
|
||||
ellipse(x: number, y: number, width: number, height: number, options?: Options): Drawable;
|
||||
circle(x: number, y: number, diameter: number, options?: Options): Drawable;
|
||||
linearPath(points: Point[], options?: Options): Drawable;
|
||||
polygon(points: Point[], options?: Options): Drawable;
|
||||
arc(x: number, y: number, width: number, height: number, start: number, stop: number, closed?: boolean, options?: Options): Drawable;
|
||||
curve(points: Point[], options?: Options): Drawable;
|
||||
path(d: string, options?: Options): Drawable;
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
import { RoughGenerator } from './generator';
|
||||
export class RoughCanvas {
|
||||
constructor(canvas, config) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.gen = new RoughGenerator(config);
|
||||
}
|
||||
draw(drawable) {
|
||||
const sets = drawable.sets || [];
|
||||
const o = drawable.options || this.getDefaultOptions();
|
||||
const ctx = this.ctx;
|
||||
for (const drawing of sets) {
|
||||
switch (drawing.type) {
|
||||
case 'path':
|
||||
ctx.save();
|
||||
ctx.strokeStyle = o.stroke === 'none' ? 'transparent' : o.stroke;
|
||||
ctx.lineWidth = o.strokeWidth;
|
||||
if (o.strokeLineDash) {
|
||||
ctx.setLineDash(o.strokeLineDash);
|
||||
}
|
||||
if (o.strokeLineDashOffset) {
|
||||
ctx.lineDashOffset = o.strokeLineDashOffset;
|
||||
}
|
||||
this._drawToContext(ctx, drawing);
|
||||
ctx.restore();
|
||||
break;
|
||||
case 'fillPath':
|
||||
ctx.save();
|
||||
ctx.fillStyle = o.fill || '';
|
||||
const fillRule = (drawable.shape === 'curve' || drawable.shape === 'polygon') ? 'evenodd' : 'nonzero';
|
||||
this._drawToContext(ctx, drawing, fillRule);
|
||||
ctx.restore();
|
||||
break;
|
||||
case 'fillSketch':
|
||||
this.fillSketch(ctx, drawing, o);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
fillSketch(ctx, drawing, o) {
|
||||
let fweight = o.fillWeight;
|
||||
if (fweight < 0) {
|
||||
fweight = o.strokeWidth / 2;
|
||||
}
|
||||
ctx.save();
|
||||
if (o.fillLineDash) {
|
||||
ctx.setLineDash(o.fillLineDash);
|
||||
}
|
||||
if (o.fillLineDashOffset) {
|
||||
ctx.lineDashOffset = o.fillLineDashOffset;
|
||||
}
|
||||
ctx.strokeStyle = o.fill || '';
|
||||
ctx.lineWidth = fweight;
|
||||
this._drawToContext(ctx, drawing);
|
||||
ctx.restore();
|
||||
}
|
||||
_drawToContext(ctx, drawing, rule = 'nonzero') {
|
||||
ctx.beginPath();
|
||||
for (const item of drawing.ops) {
|
||||
const data = item.data;
|
||||
switch (item.op) {
|
||||
case 'move':
|
||||
ctx.moveTo(data[0], data[1]);
|
||||
break;
|
||||
case 'bcurveTo':
|
||||
ctx.bezierCurveTo(data[0], data[1], data[2], data[3], data[4], data[5]);
|
||||
break;
|
||||
case 'lineTo':
|
||||
ctx.lineTo(data[0], data[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (drawing.type === 'fillPath') {
|
||||
ctx.fill(rule);
|
||||
}
|
||||
else {
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
get generator() {
|
||||
return this.gen;
|
||||
}
|
||||
getDefaultOptions() {
|
||||
return this.gen.defaultOptions;
|
||||
}
|
||||
line(x1, y1, x2, y2, options) {
|
||||
const d = this.gen.line(x1, y1, x2, y2, options);
|
||||
this.draw(d);
|
||||
return d;
|
||||
}
|
||||
rectangle(x, y, width, height, options) {
|
||||
const d = this.gen.rectangle(x, y, width, height, options);
|
||||
this.draw(d);
|
||||
return d;
|
||||
}
|
||||
ellipse(x, y, width, height, options) {
|
||||
const d = this.gen.ellipse(x, y, width, height, options);
|
||||
this.draw(d);
|
||||
return d;
|
||||
}
|
||||
circle(x, y, diameter, options) {
|
||||
const d = this.gen.circle(x, y, diameter, options);
|
||||
this.draw(d);
|
||||
return d;
|
||||
}
|
||||
linearPath(points, options) {
|
||||
const d = this.gen.linearPath(points, options);
|
||||
this.draw(d);
|
||||
return d;
|
||||
}
|
||||
polygon(points, options) {
|
||||
const d = this.gen.polygon(points, options);
|
||||
this.draw(d);
|
||||
return d;
|
||||
}
|
||||
arc(x, y, width, height, start, stop, closed = false, options) {
|
||||
const d = this.gen.arc(x, y, width, height, start, stop, closed, options);
|
||||
this.draw(d);
|
||||
return d;
|
||||
}
|
||||
curve(points, options) {
|
||||
const d = this.gen.curve(points, options);
|
||||
this.draw(d);
|
||||
return d;
|
||||
}
|
||||
path(d, options) {
|
||||
const drawing = this.gen.path(d, options);
|
||||
this.draw(drawing);
|
||||
return drawing;
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
import { Point } from './geometry';
|
||||
import { Random } from './math';
|
||||
export declare const SVGNS = "http://www.w3.org/2000/svg";
|
||||
export interface Config {
|
||||
options?: Options;
|
||||
}
|
||||
export interface DrawingSurface {
|
||||
width: number | SVGAnimatedLength;
|
||||
height: number | SVGAnimatedLength;
|
||||
}
|
||||
export interface Options {
|
||||
maxRandomnessOffset?: number;
|
||||
roughness?: number;
|
||||
bowing?: number;
|
||||
stroke?: string;
|
||||
strokeWidth?: number;
|
||||
curveFitting?: number;
|
||||
curveTightness?: number;
|
||||
curveStepCount?: number;
|
||||
fill?: string;
|
||||
fillStyle?: string;
|
||||
fillWeight?: number;
|
||||
hachureAngle?: number;
|
||||
hachureGap?: number;
|
||||
simplification?: number;
|
||||
dashOffset?: number;
|
||||
dashGap?: number;
|
||||
zigzagOffset?: number;
|
||||
seed?: number;
|
||||
combineNestedSvgPaths?: boolean;
|
||||
strokeLineDash?: number[];
|
||||
strokeLineDashOffset?: number;
|
||||
fillLineDash?: number[];
|
||||
fillLineDashOffset?: number;
|
||||
disableMultiStroke?: boolean;
|
||||
disableMultiStrokeFill?: boolean;
|
||||
}
|
||||
export interface ResolvedOptions extends Options {
|
||||
maxRandomnessOffset: number;
|
||||
roughness: number;
|
||||
bowing: number;
|
||||
stroke: string;
|
||||
strokeWidth: number;
|
||||
curveFitting: number;
|
||||
curveTightness: number;
|
||||
curveStepCount: number;
|
||||
fillStyle: string;
|
||||
fillWeight: number;
|
||||
hachureAngle: number;
|
||||
hachureGap: number;
|
||||
dashOffset: number;
|
||||
dashGap: number;
|
||||
zigzagOffset: number;
|
||||
seed: number;
|
||||
combineNestedSvgPaths: boolean;
|
||||
randomizer?: Random;
|
||||
disableMultiStroke: boolean;
|
||||
disableMultiStrokeFill: boolean;
|
||||
}
|
||||
export declare type OpType = 'move' | 'bcurveTo' | 'lineTo';
|
||||
export declare type OpSetType = 'path' | 'fillPath' | 'fillSketch';
|
||||
export interface Op {
|
||||
op: OpType;
|
||||
data: number[];
|
||||
}
|
||||
export interface OpSet {
|
||||
type: OpSetType;
|
||||
ops: Op[];
|
||||
size?: Point;
|
||||
path?: string;
|
||||
}
|
||||
export interface Drawable {
|
||||
shape: string;
|
||||
options: ResolvedOptions;
|
||||
sets: OpSet[];
|
||||
}
|
||||
export interface PathInfo {
|
||||
d: string;
|
||||
stroke: string;
|
||||
strokeWidth: number;
|
||||
fill?: string;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export const SVGNS = 'http://www.w3.org/2000/svg';
|
@ -0,0 +1,9 @@
|
||||
import { PatternFiller, RenderHelper } from './filler-interface';
|
||||
import { ResolvedOptions, OpSet } from '../core';
|
||||
import { Point } from '../geometry';
|
||||
export declare class DashedFiller implements PatternFiller {
|
||||
private helper;
|
||||
constructor(helper: RenderHelper);
|
||||
fillPolygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
private dashedLine;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { lineLength } from '../geometry';
|
||||
import { polygonHachureLines } from './scan-line-hachure';
|
||||
export class DashedFiller {
|
||||
constructor(helper) {
|
||||
this.helper = helper;
|
||||
}
|
||||
fillPolygon(points, o) {
|
||||
const lines = polygonHachureLines(points, o);
|
||||
return { type: 'fillSketch', ops: this.dashedLine(lines, o) };
|
||||
}
|
||||
dashedLine(lines, o) {
|
||||
const offset = o.dashOffset < 0 ? (o.hachureGap < 0 ? (o.strokeWidth * 4) : o.hachureGap) : o.dashOffset;
|
||||
const gap = o.dashGap < 0 ? (o.hachureGap < 0 ? (o.strokeWidth * 4) : o.hachureGap) : o.dashGap;
|
||||
const ops = [];
|
||||
lines.forEach((line) => {
|
||||
const length = lineLength(line);
|
||||
const count = Math.floor(length / (offset + gap));
|
||||
const startOffset = (length + gap - (count * (offset + gap))) / 2;
|
||||
let p1 = line[0];
|
||||
let p2 = line[1];
|
||||
if (p1[0] > p2[0]) {
|
||||
p1 = line[1];
|
||||
p2 = line[0];
|
||||
}
|
||||
const alpha = Math.atan((p2[1] - p1[1]) / (p2[0] - p1[0]));
|
||||
for (let i = 0; i < count; i++) {
|
||||
const lstart = i * (offset + gap);
|
||||
const lend = lstart + offset;
|
||||
const start = [p1[0] + (lstart * Math.cos(alpha)) + (startOffset * Math.cos(alpha)), p1[1] + lstart * Math.sin(alpha) + (startOffset * Math.sin(alpha))];
|
||||
const end = [p1[0] + (lend * Math.cos(alpha)) + (startOffset * Math.cos(alpha)), p1[1] + (lend * Math.sin(alpha)) + (startOffset * Math.sin(alpha))];
|
||||
ops.push(...this.helper.doubleLineOps(start[0], start[1], end[0], end[1], o));
|
||||
}
|
||||
});
|
||||
return ops;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { PatternFiller, RenderHelper } from './filler-interface';
|
||||
import { ResolvedOptions, OpSet } from '../core';
|
||||
import { Point } from '../geometry';
|
||||
export declare class DotFiller implements PatternFiller {
|
||||
private helper;
|
||||
constructor(helper: RenderHelper);
|
||||
fillPolygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
private dotsOnLines;
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import { lineLength } from '../geometry';
|
||||
import { polygonHachureLines } from './scan-line-hachure';
|
||||
export class DotFiller {
|
||||
constructor(helper) {
|
||||
this.helper = helper;
|
||||
}
|
||||
fillPolygon(points, o) {
|
||||
o = Object.assign({}, o, { curveStepCount: 4, hachureAngle: 0, roughness: 1 });
|
||||
const lines = polygonHachureLines(points, o);
|
||||
return this.dotsOnLines(lines, o);
|
||||
}
|
||||
dotsOnLines(lines, o) {
|
||||
const ops = [];
|
||||
let gap = o.hachureGap;
|
||||
if (gap < 0) {
|
||||
gap = o.strokeWidth * 4;
|
||||
}
|
||||
gap = Math.max(gap, 0.1);
|
||||
let fweight = o.fillWeight;
|
||||
if (fweight < 0) {
|
||||
fweight = o.strokeWidth / 2;
|
||||
}
|
||||
const ro = gap / 4;
|
||||
for (const line of lines) {
|
||||
const length = lineLength(line);
|
||||
const dl = length / gap;
|
||||
const count = Math.ceil(dl) - 1;
|
||||
const offset = length - (count * gap);
|
||||
const x = ((line[0][0] + line[1][0]) / 2) - (gap / 4);
|
||||
const minY = Math.min(line[0][1], line[1][1]);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const y = minY + offset + (i * gap);
|
||||
const cx = this.helper.randOffsetWithRange(x - ro, x + ro, o);
|
||||
const cy = this.helper.randOffsetWithRange(y - ro, y + ro, o);
|
||||
const el = this.helper.ellipse(cx, cy, fweight, fweight, o);
|
||||
ops.push(...el.ops);
|
||||
}
|
||||
}
|
||||
return { type: 'fillSketch', ops };
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { ResolvedOptions, OpSet, Op } from '../core';
|
||||
import { Point } from '../geometry';
|
||||
export interface PatternFiller {
|
||||
fillPolygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
}
|
||||
export interface RenderHelper {
|
||||
randOffset(x: number, o: ResolvedOptions): number;
|
||||
randOffsetWithRange(min: number, max: number, o: ResolvedOptions): number;
|
||||
ellipse(x: number, y: number, width: number, height: number, o: ResolvedOptions): OpSet;
|
||||
doubleLineOps(x1: number, y1: number, x2: number, y2: number, o: ResolvedOptions): Op[];
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { ResolvedOptions } from '../core';
|
||||
import { PatternFiller, RenderHelper } from './filler-interface';
|
||||
export declare function getFiller(o: ResolvedOptions, helper: RenderHelper): PatternFiller;
|
@ -0,0 +1,47 @@
|
||||
import { HachureFiller } from './hachure-filler';
|
||||
import { ZigZagFiller } from './zigzag-filler';
|
||||
import { HatchFiller } from './hatch-filler';
|
||||
import { DotFiller } from './dot-filler';
|
||||
import { DashedFiller } from './dashed-filler';
|
||||
import { ZigZagLineFiller } from './zigzag-line-filler';
|
||||
const fillers = {};
|
||||
export function getFiller(o, helper) {
|
||||
let fillerName = o.fillStyle || 'hachure';
|
||||
if (!fillers[fillerName]) {
|
||||
switch (fillerName) {
|
||||
case 'zigzag':
|
||||
if (!fillers[fillerName]) {
|
||||
fillers[fillerName] = new ZigZagFiller(helper);
|
||||
}
|
||||
break;
|
||||
case 'cross-hatch':
|
||||
if (!fillers[fillerName]) {
|
||||
fillers[fillerName] = new HatchFiller(helper);
|
||||
}
|
||||
break;
|
||||
case 'dots':
|
||||
if (!fillers[fillerName]) {
|
||||
fillers[fillerName] = new DotFiller(helper);
|
||||
}
|
||||
break;
|
||||
case 'dashed':
|
||||
if (!fillers[fillerName]) {
|
||||
fillers[fillerName] = new DashedFiller(helper);
|
||||
}
|
||||
break;
|
||||
case 'zigzag-line':
|
||||
if (!fillers[fillerName]) {
|
||||
fillers[fillerName] = new ZigZagLineFiller(helper);
|
||||
}
|
||||
break;
|
||||
case 'hachure':
|
||||
default:
|
||||
fillerName = 'hachure';
|
||||
if (!fillers[fillerName]) {
|
||||
fillers[fillerName] = new HachureFiller(helper);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fillers[fillerName];
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { PatternFiller, RenderHelper } from './filler-interface';
|
||||
import { ResolvedOptions, OpSet } from '../core';
|
||||
import { Point } from '../geometry';
|
||||
export declare class HachureFiller implements PatternFiller {
|
||||
private helper;
|
||||
constructor(helper: RenderHelper);
|
||||
fillPolygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
protected _fillPolygon(points: Point[], o: ResolvedOptions, connectEnds?: boolean): OpSet;
|
||||
private renderLines;
|
||||
private connectingLines;
|
||||
private midPointInPolygon;
|
||||
private splitOnIntersections;
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
import { lineLength, lineIntersection, doIntersect, isPointInPolygon } from '../geometry';
|
||||
import { polygonHachureLines } from './scan-line-hachure';
|
||||
export class HachureFiller {
|
||||
constructor(helper) {
|
||||
this.helper = helper;
|
||||
}
|
||||
fillPolygon(points, o) {
|
||||
return this._fillPolygon(points, o);
|
||||
}
|
||||
_fillPolygon(points, o, connectEnds = false) {
|
||||
let lines = polygonHachureLines(points, o);
|
||||
if (connectEnds) {
|
||||
const connectingLines = this.connectingLines(points, lines);
|
||||
lines = lines.concat(connectingLines);
|
||||
}
|
||||
const ops = this.renderLines(lines, o);
|
||||
return { type: 'fillSketch', ops };
|
||||
}
|
||||
renderLines(lines, o) {
|
||||
const ops = [];
|
||||
for (const line of lines) {
|
||||
ops.push(...this.helper.doubleLineOps(line[0][0], line[0][1], line[1][0], line[1][1], o));
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
connectingLines(polygon, lines) {
|
||||
const result = [];
|
||||
if (lines.length > 1) {
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const prev = lines[i - 1];
|
||||
if (lineLength(prev) < 3) {
|
||||
continue;
|
||||
}
|
||||
const current = lines[i];
|
||||
const segment = [current[0], prev[1]];
|
||||
if (lineLength(segment) > 3) {
|
||||
const segSplits = this.splitOnIntersections(polygon, segment);
|
||||
result.push(...segSplits);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
midPointInPolygon(polygon, segment) {
|
||||
return isPointInPolygon(polygon, (segment[0][0] + segment[1][0]) / 2, (segment[0][1] + segment[1][1]) / 2);
|
||||
}
|
||||
splitOnIntersections(polygon, segment) {
|
||||
const error = Math.max(5, lineLength(segment) * 0.1);
|
||||
const intersections = [];
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
const p1 = polygon[i];
|
||||
const p2 = polygon[(i + 1) % polygon.length];
|
||||
if (doIntersect(p1, p2, ...segment)) {
|
||||
const ip = lineIntersection(p1, p2, segment[0], segment[1]);
|
||||
if (ip) {
|
||||
const d0 = lineLength([ip, segment[0]]);
|
||||
const d1 = lineLength([ip, segment[1]]);
|
||||
if (d0 > error && d1 > error) {
|
||||
intersections.push({
|
||||
point: ip,
|
||||
distance: d0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (intersections.length > 1) {
|
||||
const ips = intersections.sort((a, b) => a.distance - b.distance).map((d) => d.point);
|
||||
if (!isPointInPolygon(polygon, ...segment[0])) {
|
||||
ips.shift();
|
||||
}
|
||||
if (!isPointInPolygon(polygon, ...segment[1])) {
|
||||
ips.pop();
|
||||
}
|
||||
if (ips.length <= 1) {
|
||||
if (this.midPointInPolygon(polygon, segment)) {
|
||||
return [segment];
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const spoints = [segment[0], ...ips, segment[1]];
|
||||
const slines = [];
|
||||
for (let i = 0; i < (spoints.length - 1); i += 2) {
|
||||
const subSegment = [spoints[i], spoints[i + 1]];
|
||||
if (this.midPointInPolygon(polygon, subSegment)) {
|
||||
slines.push(subSegment);
|
||||
}
|
||||
}
|
||||
return slines;
|
||||
}
|
||||
else if (this.midPointInPolygon(polygon, segment)) {
|
||||
return [segment];
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { HachureFiller } from './hachure-filler';
|
||||
import { ResolvedOptions, OpSet } from '../core';
|
||||
import { Point } from '../geometry';
|
||||
export declare class HatchFiller extends HachureFiller {
|
||||
fillPolygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import { HachureFiller } from './hachure-filler';
|
||||
export class HatchFiller extends HachureFiller {
|
||||
fillPolygon(points, o) {
|
||||
const set = this._fillPolygon(points, o);
|
||||
const o2 = Object.assign({}, o, { hachureAngle: o.hachureAngle + 90 });
|
||||
const set2 = this._fillPolygon(points, o2);
|
||||
set.ops = set.ops.concat(set2.ops);
|
||||
return set;
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { Point, Line } from '../geometry';
|
||||
import { ResolvedOptions } from '../core';
|
||||
export declare function polygonHachureLines(points: Point[], o: ResolvedOptions): Line[];
|
@ -0,0 +1,114 @@
|
||||
import { rotatePoints, rotateLines } from '../geometry';
|
||||
export function polygonHachureLines(points, o) {
|
||||
const rotationCenter = [0, 0];
|
||||
const angle = Math.round(o.hachureAngle + 90);
|
||||
if (angle) {
|
||||
rotatePoints(points, rotationCenter, angle);
|
||||
}
|
||||
const lines = straightHachureLines(points, o);
|
||||
if (angle) {
|
||||
rotatePoints(points, rotationCenter, -angle);
|
||||
rotateLines(lines, rotationCenter, -angle);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
function straightHachureLines(points, o) {
|
||||
const vertices = [...points];
|
||||
if (vertices[0].join(',') !== vertices[vertices.length - 1].join(',')) {
|
||||
vertices.push([vertices[0][0], vertices[0][1]]);
|
||||
}
|
||||
const lines = [];
|
||||
if (vertices && vertices.length > 2) {
|
||||
let gap = o.hachureGap;
|
||||
if (gap < 0) {
|
||||
gap = o.strokeWidth * 4;
|
||||
}
|
||||
gap = Math.max(gap, 0.1);
|
||||
// Create sorted edges table
|
||||
const edges = [];
|
||||
for (let i = 0; i < vertices.length - 1; i++) {
|
||||
const p1 = vertices[i];
|
||||
const p2 = vertices[i + 1];
|
||||
if (p1[1] !== p2[1]) {
|
||||
const ymin = Math.min(p1[1], p2[1]);
|
||||
edges.push({
|
||||
ymin,
|
||||
ymax: Math.max(p1[1], p2[1]),
|
||||
x: ymin === p1[1] ? p1[0] : p2[0],
|
||||
islope: (p2[0] - p1[0]) / (p2[1] - p1[1])
|
||||
});
|
||||
}
|
||||
}
|
||||
edges.sort((e1, e2) => {
|
||||
if (e1.ymin < e2.ymin) {
|
||||
return -1;
|
||||
}
|
||||
if (e1.ymin > e2.ymin) {
|
||||
return 1;
|
||||
}
|
||||
if (e1.x < e2.x) {
|
||||
return -1;
|
||||
}
|
||||
if (e1.x > e2.x) {
|
||||
return 1;
|
||||
}
|
||||
if (e1.ymax === e2.ymax) {
|
||||
return 0;
|
||||
}
|
||||
return (e1.ymax - e2.ymax) / Math.abs((e1.ymax - e2.ymax));
|
||||
});
|
||||
if (!edges.length) {
|
||||
return lines;
|
||||
}
|
||||
// Start scanning
|
||||
let activeEdges = [];
|
||||
let y = edges[0].ymin;
|
||||
while (activeEdges.length || edges.length) {
|
||||
if (edges.length) {
|
||||
let ix = -1;
|
||||
for (let i = 0; i < edges.length; i++) {
|
||||
if (edges[i].ymin > y) {
|
||||
break;
|
||||
}
|
||||
ix = i;
|
||||
}
|
||||
const removed = edges.splice(0, ix + 1);
|
||||
removed.forEach((edge) => {
|
||||
activeEdges.push({ s: y, edge });
|
||||
});
|
||||
}
|
||||
activeEdges = activeEdges.filter((ae) => {
|
||||
if (ae.edge.ymax <= y) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
activeEdges.sort((ae1, ae2) => {
|
||||
if (ae1.edge.x === ae2.edge.x) {
|
||||
return 0;
|
||||
}
|
||||
return (ae1.edge.x - ae2.edge.x) / Math.abs((ae1.edge.x - ae2.edge.x));
|
||||
});
|
||||
// fill between the edges
|
||||
if (activeEdges.length > 1) {
|
||||
for (let i = 0; i < activeEdges.length; i = i + 2) {
|
||||
const nexti = i + 1;
|
||||
if (nexti >= activeEdges.length) {
|
||||
break;
|
||||
}
|
||||
const ce = activeEdges[i].edge;
|
||||
const ne = activeEdges[nexti].edge;
|
||||
lines.push([
|
||||
[Math.round(ce.x), y],
|
||||
[Math.round(ne.x), y]
|
||||
]);
|
||||
}
|
||||
}
|
||||
y += gap;
|
||||
activeEdges.forEach((ae) => {
|
||||
ae.edge.x = ae.edge.x + (gap * ae.edge.islope);
|
||||
});
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { HachureFiller } from './hachure-filler';
|
||||
import { ResolvedOptions, OpSet } from '../core';
|
||||
import { Point } from '../geometry';
|
||||
export declare class ZigZagFiller extends HachureFiller {
|
||||
fillPolygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { HachureFiller } from './hachure-filler';
|
||||
export class ZigZagFiller extends HachureFiller {
|
||||
fillPolygon(points, o) {
|
||||
return this._fillPolygon(points, o, true);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { PatternFiller, RenderHelper } from './filler-interface';
|
||||
import { ResolvedOptions, OpSet } from '../core';
|
||||
import { Point } from '../geometry';
|
||||
export declare class ZigZagLineFiller implements PatternFiller {
|
||||
private helper;
|
||||
constructor(helper: RenderHelper);
|
||||
fillPolygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
private zigzagLines;
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { lineLength } from '../geometry';
|
||||
import { polygonHachureLines } from './scan-line-hachure';
|
||||
export class ZigZagLineFiller {
|
||||
constructor(helper) {
|
||||
this.helper = helper;
|
||||
}
|
||||
fillPolygon(points, o) {
|
||||
const gap = o.hachureGap < 0 ? (o.strokeWidth * 4) : o.hachureGap;
|
||||
const zo = o.zigzagOffset < 0 ? gap : o.zigzagOffset;
|
||||
o = Object.assign({}, o, { hachureGap: gap + zo });
|
||||
const lines = polygonHachureLines(points, o);
|
||||
return { type: 'fillSketch', ops: this.zigzagLines(lines, zo, o) };
|
||||
}
|
||||
zigzagLines(lines, zo, o) {
|
||||
const ops = [];
|
||||
lines.forEach((line) => {
|
||||
const length = lineLength(line);
|
||||
const count = Math.round(length / (2 * zo));
|
||||
let p1 = line[0];
|
||||
let p2 = line[1];
|
||||
if (p1[0] > p2[0]) {
|
||||
p1 = line[1];
|
||||
p2 = line[0];
|
||||
}
|
||||
const alpha = Math.atan((p2[1] - p1[1]) / (p2[0] - p1[0]));
|
||||
for (let i = 0; i < count; i++) {
|
||||
const lstart = i * 2 * zo;
|
||||
const lend = (i + 1) * 2 * zo;
|
||||
const dz = Math.sqrt(2 * Math.pow(zo, 2));
|
||||
const start = [p1[0] + (lstart * Math.cos(alpha)), p1[1] + lstart * Math.sin(alpha)];
|
||||
const end = [p1[0] + (lend * Math.cos(alpha)), p1[1] + (lend * Math.sin(alpha))];
|
||||
const middle = [start[0] + dz * Math.cos(alpha + Math.PI / 4), start[1] + dz * Math.sin(alpha + Math.PI / 4)];
|
||||
ops.push(...this.helper.doubleLineOps(start[0], start[1], middle[0], middle[1], o), ...this.helper.doubleLineOps(middle[0], middle[1], end[0], end[1], o));
|
||||
}
|
||||
});
|
||||
return ops;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { Config, Options, Drawable, OpSet, ResolvedOptions, PathInfo } from './core.js';
|
||||
import { Point } from './geometry.js';
|
||||
export declare class RoughGenerator {
|
||||
private config;
|
||||
defaultOptions: ResolvedOptions;
|
||||
constructor(config?: Config);
|
||||
static newSeed(): number;
|
||||
private _o;
|
||||
private _d;
|
||||
line(x1: number, y1: number, x2: number, y2: number, options?: Options): Drawable;
|
||||
rectangle(x: number, y: number, width: number, height: number, options?: Options): Drawable;
|
||||
ellipse(x: number, y: number, width: number, height: number, options?: Options): Drawable;
|
||||
circle(x: number, y: number, diameter: number, options?: Options): Drawable;
|
||||
linearPath(points: Point[], options?: Options): Drawable;
|
||||
arc(x: number, y: number, width: number, height: number, start: number, stop: number, closed?: boolean, options?: Options): Drawable;
|
||||
curve(points: Point[], options?: Options): Drawable;
|
||||
polygon(points: Point[], options?: Options): Drawable;
|
||||
path(d: string, options?: Options): Drawable;
|
||||
opsToPath(drawing: OpSet): string;
|
||||
toPaths(drawable: Drawable): PathInfo[];
|
||||
private fillSketch;
|
||||
}
|
@ -0,0 +1,259 @@
|
||||
import { line, solidFillPolygon, patternFillPolygon, rectangle, ellipseWithParams, generateEllipseParams, linearPath, arc, patternFillArc, curve, svgPath } from './renderer.js';
|
||||
import { randomSeed } from './math.js';
|
||||
import { curveToBezier } from 'points-on-curve/lib/curve-to-bezier.js';
|
||||
import { pointsOnBezierCurves } from 'points-on-curve';
|
||||
import { pointsOnPath } from 'points-on-path';
|
||||
const NOS = 'none';
|
||||
export class RoughGenerator {
|
||||
constructor(config) {
|
||||
this.defaultOptions = {
|
||||
maxRandomnessOffset: 2,
|
||||
roughness: 1,
|
||||
bowing: 1,
|
||||
stroke: '#000',
|
||||
strokeWidth: 1,
|
||||
curveTightness: 0,
|
||||
curveFitting: 0.95,
|
||||
curveStepCount: 9,
|
||||
fillStyle: 'hachure',
|
||||
fillWeight: -1,
|
||||
hachureAngle: -41,
|
||||
hachureGap: -1,
|
||||
dashOffset: -1,
|
||||
dashGap: -1,
|
||||
zigzagOffset: -1,
|
||||
seed: 0,
|
||||
combineNestedSvgPaths: false,
|
||||
disableMultiStroke: false,
|
||||
disableMultiStrokeFill: false
|
||||
};
|
||||
this.config = config || {};
|
||||
if (this.config.options) {
|
||||
this.defaultOptions = this._o(this.config.options);
|
||||
}
|
||||
}
|
||||
static newSeed() {
|
||||
return randomSeed();
|
||||
}
|
||||
_o(options) {
|
||||
return options ? Object.assign({}, this.defaultOptions, options) : this.defaultOptions;
|
||||
}
|
||||
_d(shape, sets, options) {
|
||||
return { shape, sets: sets || [], options: options || this.defaultOptions };
|
||||
}
|
||||
line(x1, y1, x2, y2, options) {
|
||||
const o = this._o(options);
|
||||
return this._d('line', [line(x1, y1, x2, y2, o)], o);
|
||||
}
|
||||
rectangle(x, y, width, height, options) {
|
||||
const o = this._o(options);
|
||||
const paths = [];
|
||||
const outline = rectangle(x, y, width, height, o);
|
||||
if (o.fill) {
|
||||
const points = [[x, y], [x + width, y], [x + width, y + height], [x, y + height]];
|
||||
if (o.fillStyle === 'solid') {
|
||||
paths.push(solidFillPolygon(points, o));
|
||||
}
|
||||
else {
|
||||
paths.push(patternFillPolygon(points, o));
|
||||
}
|
||||
}
|
||||
if (o.stroke !== NOS) {
|
||||
paths.push(outline);
|
||||
}
|
||||
return this._d('rectangle', paths, o);
|
||||
}
|
||||
ellipse(x, y, width, height, options) {
|
||||
const o = this._o(options);
|
||||
const paths = [];
|
||||
const ellipseParams = generateEllipseParams(width, height, o);
|
||||
const ellipseResponse = ellipseWithParams(x, y, o, ellipseParams);
|
||||
if (o.fill) {
|
||||
if (o.fillStyle === 'solid') {
|
||||
const shape = ellipseWithParams(x, y, o, ellipseParams).opset;
|
||||
shape.type = 'fillPath';
|
||||
paths.push(shape);
|
||||
}
|
||||
else {
|
||||
paths.push(patternFillPolygon(ellipseResponse.estimatedPoints, o));
|
||||
}
|
||||
}
|
||||
if (o.stroke !== NOS) {
|
||||
paths.push(ellipseResponse.opset);
|
||||
}
|
||||
return this._d('ellipse', paths, o);
|
||||
}
|
||||
circle(x, y, diameter, options) {
|
||||
const ret = this.ellipse(x, y, diameter, diameter, options);
|
||||
ret.shape = 'circle';
|
||||
return ret;
|
||||
}
|
||||
linearPath(points, options) {
|
||||
const o = this._o(options);
|
||||
return this._d('linearPath', [linearPath(points, false, o)], o);
|
||||
}
|
||||
arc(x, y, width, height, start, stop, closed = false, options) {
|
||||
const o = this._o(options);
|
||||
const paths = [];
|
||||
const outline = arc(x, y, width, height, start, stop, closed, true, o);
|
||||
if (closed && o.fill) {
|
||||
if (o.fillStyle === 'solid') {
|
||||
const shape = arc(x, y, width, height, start, stop, true, false, o);
|
||||
shape.type = 'fillPath';
|
||||
paths.push(shape);
|
||||
}
|
||||
else {
|
||||
paths.push(patternFillArc(x, y, width, height, start, stop, o));
|
||||
}
|
||||
}
|
||||
if (o.stroke !== NOS) {
|
||||
paths.push(outline);
|
||||
}
|
||||
return this._d('arc', paths, o);
|
||||
}
|
||||
curve(points, options) {
|
||||
const o = this._o(options);
|
||||
const paths = [];
|
||||
const outline = curve(points, o);
|
||||
if (o.fill && o.fill !== NOS && points.length >= 3) {
|
||||
const bcurve = curveToBezier(points);
|
||||
const polyPoints = pointsOnBezierCurves(bcurve, 10, (1 + o.roughness) / 2);
|
||||
if (o.fillStyle === 'solid') {
|
||||
paths.push(solidFillPolygon(polyPoints, o));
|
||||
}
|
||||
else {
|
||||
paths.push(patternFillPolygon(polyPoints, o));
|
||||
}
|
||||
}
|
||||
if (o.stroke !== NOS) {
|
||||
paths.push(outline);
|
||||
}
|
||||
return this._d('curve', paths, o);
|
||||
}
|
||||
polygon(points, options) {
|
||||
const o = this._o(options);
|
||||
const paths = [];
|
||||
const outline = linearPath(points, true, o);
|
||||
if (o.fill) {
|
||||
if (o.fillStyle === 'solid') {
|
||||
paths.push(solidFillPolygon(points, o));
|
||||
}
|
||||
else {
|
||||
paths.push(patternFillPolygon(points, o));
|
||||
}
|
||||
}
|
||||
if (o.stroke !== NOS) {
|
||||
paths.push(outline);
|
||||
}
|
||||
return this._d('polygon', paths, o);
|
||||
}
|
||||
path(d, options) {
|
||||
const o = this._o(options);
|
||||
const paths = [];
|
||||
if (!d) {
|
||||
return this._d('path', paths, o);
|
||||
}
|
||||
d = (d || '').replace(/\n/g, ' ').replace(/(-\s)/g, '-').replace('/(\s\s)/g', ' ');
|
||||
const hasFill = o.fill && o.fill !== 'transparent' && o.fill !== NOS;
|
||||
const hasStroke = o.stroke !== NOS;
|
||||
const simplified = !!(o.simplification && (o.simplification < 1));
|
||||
const distance = simplified ? (4 - 4 * (o.simplification)) : ((1 + o.roughness) / 2);
|
||||
const sets = pointsOnPath(d, 1, distance);
|
||||
if (hasFill) {
|
||||
if (o.combineNestedSvgPaths) {
|
||||
const combined = [];
|
||||
sets.forEach((set) => combined.push(...set));
|
||||
if (o.fillStyle === 'solid') {
|
||||
paths.push(solidFillPolygon(combined, o));
|
||||
}
|
||||
else {
|
||||
paths.push(patternFillPolygon(combined, o));
|
||||
}
|
||||
}
|
||||
else {
|
||||
sets.forEach((polyPoints) => {
|
||||
if (o.fillStyle === 'solid') {
|
||||
paths.push(solidFillPolygon(polyPoints, o));
|
||||
}
|
||||
else {
|
||||
paths.push(patternFillPolygon(polyPoints, o));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (hasStroke) {
|
||||
if (simplified) {
|
||||
sets.forEach((set) => {
|
||||
paths.push(linearPath(set, false, o));
|
||||
});
|
||||
}
|
||||
else {
|
||||
paths.push(svgPath(d, o));
|
||||
}
|
||||
}
|
||||
return this._d('path', paths, o);
|
||||
}
|
||||
opsToPath(drawing) {
|
||||
let path = '';
|
||||
for (const item of drawing.ops) {
|
||||
const data = item.data;
|
||||
switch (item.op) {
|
||||
case 'move':
|
||||
path += `M${data[0]} ${data[1]} `;
|
||||
break;
|
||||
case 'bcurveTo':
|
||||
path += `C${data[0]} ${data[1]}, ${data[2]} ${data[3]}, ${data[4]} ${data[5]} `;
|
||||
break;
|
||||
case 'lineTo':
|
||||
path += `L${data[0]} ${data[1]} `;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return path.trim();
|
||||
}
|
||||
toPaths(drawable) {
|
||||
const sets = drawable.sets || [];
|
||||
const o = drawable.options || this.defaultOptions;
|
||||
const paths = [];
|
||||
for (const drawing of sets) {
|
||||
let path = null;
|
||||
switch (drawing.type) {
|
||||
case 'path':
|
||||
path = {
|
||||
d: this.opsToPath(drawing),
|
||||
stroke: o.stroke,
|
||||
strokeWidth: o.strokeWidth,
|
||||
fill: NOS
|
||||
};
|
||||
break;
|
||||
case 'fillPath':
|
||||
path = {
|
||||
d: this.opsToPath(drawing),
|
||||
stroke: NOS,
|
||||
strokeWidth: 0,
|
||||
fill: o.fill || NOS
|
||||
};
|
||||
break;
|
||||
case 'fillSketch':
|
||||
path = this.fillSketch(drawing, o);
|
||||
break;
|
||||
}
|
||||
if (path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
fillSketch(drawing, o) {
|
||||
let fweight = o.fillWeight;
|
||||
if (fweight < 0) {
|
||||
fweight = o.strokeWidth / 2;
|
||||
}
|
||||
return {
|
||||
d: this.opsToPath(drawing),
|
||||
stroke: o.fill || NOS,
|
||||
strokeWidth: fweight,
|
||||
fill: NOS
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
export declare type Point = [number, number];
|
||||
export declare type Line = [Point, Point];
|
||||
export interface Rectangle {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
export declare function rotatePoints(points: Point[], center: Point, degrees: number): void;
|
||||
export declare function rotateLines(lines: Line[], center: Point, degrees: number): void;
|
||||
export declare function lineLength(line: Line): number;
|
||||
export declare function lineIntersection(a: Point, b: Point, c: Point, d: Point): Point | null;
|
||||
export declare function isPointInPolygon(points: Point[], x: number, y: number): boolean;
|
||||
export declare function doIntersect(p1: Point, q1: Point, p2: Point, q2: Point): boolean;
|
@ -0,0 +1,100 @@
|
||||
export function rotatePoints(points, center, degrees) {
|
||||
if (points && points.length) {
|
||||
const [cx, cy] = center;
|
||||
const angle = (Math.PI / 180) * degrees;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
points.forEach((p) => {
|
||||
const [x, y] = p;
|
||||
p[0] = ((x - cx) * cos) - ((y - cy) * sin) + cx;
|
||||
p[1] = ((x - cx) * sin) + ((y - cy) * cos) + cy;
|
||||
});
|
||||
}
|
||||
}
|
||||
export function rotateLines(lines, center, degrees) {
|
||||
const points = [];
|
||||
lines.forEach((line) => points.push(...line));
|
||||
rotatePoints(points, center, degrees);
|
||||
}
|
||||
export function lineLength(line) {
|
||||
const p1 = line[0];
|
||||
const p2 = line[1];
|
||||
return Math.sqrt(Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2));
|
||||
}
|
||||
export function lineIntersection(a, b, c, d) {
|
||||
const a1 = b[1] - a[1];
|
||||
const b1 = a[0] - b[0];
|
||||
const c1 = a1 * (a[0]) + b1 * (a[1]);
|
||||
const a2 = d[1] - c[1];
|
||||
const b2 = c[0] - d[0];
|
||||
const c2 = a2 * (c[0]) + b2 * (c[1]);
|
||||
const determinant = a1 * b2 - a2 * b1;
|
||||
return determinant ? [(b2 * c1 - b1 * c2) / determinant, (a1 * c2 - a2 * c1) / determinant] : null;
|
||||
}
|
||||
export function isPointInPolygon(points, x, y) {
|
||||
const vertices = points.length;
|
||||
// There must be at least 3 vertices in polygon
|
||||
if (vertices < 3) {
|
||||
return false;
|
||||
}
|
||||
const extreme = [Number.MAX_SAFE_INTEGER, y];
|
||||
const p = [x, y];
|
||||
let count = 0;
|
||||
for (let i = 0; i < vertices; i++) {
|
||||
const current = points[i];
|
||||
const next = points[(i + 1) % vertices];
|
||||
if (doIntersect(current, next, p, extreme)) {
|
||||
if (orientation(current, p, next) === 0) {
|
||||
return onSegment(current, p, next);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
// true if count is off
|
||||
return count % 2 === 1;
|
||||
}
|
||||
// Check if q lies on the line segment pr
|
||||
function onSegment(p, q, r) {
|
||||
return (q[0] <= Math.max(p[0], r[0]) &&
|
||||
q[0] >= Math.min(p[0], r[0]) &&
|
||||
q[1] <= Math.max(p[1], r[1]) &&
|
||||
q[1] >= Math.min(p[1], r[1]));
|
||||
}
|
||||
// For the ordered points p, q, r, return
|
||||
// 0 if p, q, r are collinear
|
||||
// 1 if Clockwise
|
||||
// 2 if counterclickwise
|
||||
function orientation(p, q, r) {
|
||||
const val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]);
|
||||
if (val === 0) {
|
||||
return 0;
|
||||
}
|
||||
return val > 0 ? 1 : 2;
|
||||
}
|
||||
// Check is p1q1 intersects with p2q2
|
||||
export function doIntersect(p1, q1, p2, q2) {
|
||||
const o1 = orientation(p1, q1, p2);
|
||||
const o2 = orientation(p1, q1, q2);
|
||||
const o3 = orientation(p2, q2, p1);
|
||||
const o4 = orientation(p2, q2, q1);
|
||||
if (o1 !== o2 && o3 !== o4) {
|
||||
return true;
|
||||
}
|
||||
// p1, q1 and p2 are colinear and p2 lies on segment p1q1
|
||||
if (o1 === 0 && onSegment(p1, p2, q1)) {
|
||||
return true;
|
||||
}
|
||||
// p1, q1 and p2 are colinear and q2 lies on segment p1q1
|
||||
if (o2 === 0 && onSegment(p1, q2, q1)) {
|
||||
return true;
|
||||
}
|
||||
// p2, q2 and p1 are colinear and p1 lies on segment p2q2
|
||||
if (o3 === 0 && onSegment(p2, p1, q2)) {
|
||||
return true;
|
||||
}
|
||||
// p2, q2 and q1 are colinear and q1 lies on segment p2q2
|
||||
if (o4 === 0 && onSegment(p2, q1, q2)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export declare function randomSeed(): number;
|
||||
export declare class Random {
|
||||
private seed;
|
||||
constructor(seed: number);
|
||||
next(): number;
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
export function randomSeed() {
|
||||
return Math.floor(Math.random() * 2 ** 31);
|
||||
}
|
||||
export class Random {
|
||||
constructor(seed) {
|
||||
this.seed = seed;
|
||||
}
|
||||
next() {
|
||||
if (this.seed) {
|
||||
return ((2 ** 31 - 1) & (this.seed = Math.imul(48271, this.seed))) / 2 ** 31;
|
||||
}
|
||||
else {
|
||||
return Math.random();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { ResolvedOptions, Op, OpSet } from './core.js';
|
||||
import { Point } from './geometry.js';
|
||||
interface EllipseParams {
|
||||
rx: number;
|
||||
ry: number;
|
||||
increment: number;
|
||||
}
|
||||
export declare function line(x1: number, y1: number, x2: number, y2: number, o: ResolvedOptions): OpSet;
|
||||
export declare function linearPath(points: Point[], close: boolean, o: ResolvedOptions): OpSet;
|
||||
export declare function polygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
export declare function rectangle(x: number, y: number, width: number, height: number, o: ResolvedOptions): OpSet;
|
||||
export declare function curve(points: Point[], o: ResolvedOptions): OpSet;
|
||||
export interface EllipseResult {
|
||||
opset: OpSet;
|
||||
estimatedPoints: Point[];
|
||||
}
|
||||
export declare function ellipse(x: number, y: number, width: number, height: number, o: ResolvedOptions): OpSet;
|
||||
export declare function generateEllipseParams(width: number, height: number, o: ResolvedOptions): EllipseParams;
|
||||
export declare function ellipseWithParams(x: number, y: number, o: ResolvedOptions, ellipseParams: EllipseParams): EllipseResult;
|
||||
export declare function arc(x: number, y: number, width: number, height: number, start: number, stop: number, closed: boolean, roughClosure: boolean, o: ResolvedOptions): OpSet;
|
||||
export declare function svgPath(path: string, o: ResolvedOptions): OpSet;
|
||||
export declare function solidFillPolygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
export declare function patternFillPolygon(points: Point[], o: ResolvedOptions): OpSet;
|
||||
export declare function patternFillArc(x: number, y: number, width: number, height: number, start: number, stop: number, o: ResolvedOptions): OpSet;
|
||||
export declare function randOffset(x: number, o: ResolvedOptions): number;
|
||||
export declare function randOffsetWithRange(min: number, max: number, o: ResolvedOptions): number;
|
||||
export declare function doubleLineFillOps(x1: number, y1: number, x2: number, y2: number, o: ResolvedOptions): Op[];
|
||||
export {};
|
@ -0,0 +1,431 @@
|
||||
import { getFiller } from './fillers/filler.js';
|
||||
import { Random } from './math.js';
|
||||
import { parsePath, normalize, absolutize } from 'path-data-parser';
|
||||
const helper = {
|
||||
randOffset,
|
||||
randOffsetWithRange,
|
||||
ellipse,
|
||||
doubleLineOps: doubleLineFillOps
|
||||
};
|
||||
export function line(x1, y1, x2, y2, o) {
|
||||
return { type: 'path', ops: _doubleLine(x1, y1, x2, y2, o) };
|
||||
}
|
||||
export function linearPath(points, close, o) {
|
||||
const len = (points || []).length;
|
||||
if (len > 2) {
|
||||
const ops = [];
|
||||
for (let i = 0; i < (len - 1); i++) {
|
||||
ops.push(..._doubleLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1], o));
|
||||
}
|
||||
if (close) {
|
||||
ops.push(..._doubleLine(points[len - 1][0], points[len - 1][1], points[0][0], points[0][1], o));
|
||||
}
|
||||
return { type: 'path', ops };
|
||||
}
|
||||
else if (len === 2) {
|
||||
return line(points[0][0], points[0][1], points[1][0], points[1][1], o);
|
||||
}
|
||||
return { type: 'path', ops: [] };
|
||||
}
|
||||
export function polygon(points, o) {
|
||||
return linearPath(points, true, o);
|
||||
}
|
||||
export function rectangle(x, y, width, height, o) {
|
||||
const points = [
|
||||
[x, y],
|
||||
[x + width, y],
|
||||
[x + width, y + height],
|
||||
[x, y + height]
|
||||
];
|
||||
return polygon(points, o);
|
||||
}
|
||||
export function curve(points, o) {
|
||||
let o1 = _curveWithOffset(points, 1 * (1 + o.roughness * 0.2), o);
|
||||
if (!o.disableMultiStroke) {
|
||||
const o2 = _curveWithOffset(points, 1.5 * (1 + o.roughness * 0.22), cloneOptionsAlterSeed(o));
|
||||
o1 = o1.concat(o2);
|
||||
}
|
||||
return { type: 'path', ops: o1 };
|
||||
}
|
||||
export function ellipse(x, y, width, height, o) {
|
||||
const params = generateEllipseParams(width, height, o);
|
||||
return ellipseWithParams(x, y, o, params).opset;
|
||||
}
|
||||
export function generateEllipseParams(width, height, o) {
|
||||
const psq = Math.sqrt(Math.PI * 2 * Math.sqrt((Math.pow(width / 2, 2) + Math.pow(height / 2, 2)) / 2));
|
||||
const stepCount = Math.max(o.curveStepCount, (o.curveStepCount / Math.sqrt(200)) * psq);
|
||||
const increment = (Math.PI * 2) / stepCount;
|
||||
let rx = Math.abs(width / 2);
|
||||
let ry = Math.abs(height / 2);
|
||||
const curveFitRandomness = 1 - o.curveFitting;
|
||||
rx += _offsetOpt(rx * curveFitRandomness, o);
|
||||
ry += _offsetOpt(ry * curveFitRandomness, o);
|
||||
return { increment, rx, ry };
|
||||
}
|
||||
export function ellipseWithParams(x, y, o, ellipseParams) {
|
||||
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);
|
||||
let o1 = _curve(ap1, null, o);
|
||||
if (!o.disableMultiStroke) {
|
||||
const [ap2] = _computeEllipsePoints(ellipseParams.increment, x, y, ellipseParams.rx, ellipseParams.ry, 1.5, 0, o);
|
||||
const o2 = _curve(ap2, null, o);
|
||||
o1 = o1.concat(o2);
|
||||
}
|
||||
return {
|
||||
estimatedPoints: cp1,
|
||||
opset: { type: 'path', ops: o1 }
|
||||
};
|
||||
}
|
||||
export function arc(x, y, width, height, start, stop, closed, roughClosure, o) {
|
||||
const cx = x;
|
||||
const cy = y;
|
||||
let rx = Math.abs(width / 2);
|
||||
let ry = Math.abs(height / 2);
|
||||
rx += _offsetOpt(rx * 0.01, o);
|
||||
ry += _offsetOpt(ry * 0.01, o);
|
||||
let strt = start;
|
||||
let stp = stop;
|
||||
while (strt < 0) {
|
||||
strt += Math.PI * 2;
|
||||
stp += Math.PI * 2;
|
||||
}
|
||||
if ((stp - strt) > (Math.PI * 2)) {
|
||||
strt = 0;
|
||||
stp = Math.PI * 2;
|
||||
}
|
||||
const ellipseInc = (Math.PI * 2) / o.curveStepCount;
|
||||
const arcInc = Math.min(ellipseInc / 2, (stp - strt) / 2);
|
||||
const ops = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1, o);
|
||||
if (!o.disableMultiStroke) {
|
||||
const o2 = _arc(arcInc, cx, cy, rx, ry, strt, stp, 1.5, o);
|
||||
ops.push(...o2);
|
||||
}
|
||||
if (closed) {
|
||||
if (roughClosure) {
|
||||
ops.push(..._doubleLine(cx, cy, cx + rx * Math.cos(strt), cy + ry * Math.sin(strt), o), ..._doubleLine(cx, cy, cx + rx * Math.cos(stp), cy + ry * Math.sin(stp), o));
|
||||
}
|
||||
else {
|
||||
ops.push({ op: 'lineTo', data: [cx, cy] }, { op: 'lineTo', data: [cx + rx * Math.cos(strt), cy + ry * Math.sin(strt)] });
|
||||
}
|
||||
}
|
||||
return { type: 'path', ops };
|
||||
}
|
||||
export function svgPath(path, o) {
|
||||
const segments = normalize(absolutize(parsePath(path)));
|
||||
const ops = [];
|
||||
let first = [0, 0];
|
||||
let current = [0, 0];
|
||||
for (const { key, data } of segments) {
|
||||
switch (key) {
|
||||
case 'M': {
|
||||
const ro = 1 * (o.maxRandomnessOffset || 0);
|
||||
ops.push({ op: 'move', data: data.map((d) => d + _offsetOpt(ro, o)) });
|
||||
current = [data[0], data[1]];
|
||||
first = [data[0], data[1]];
|
||||
break;
|
||||
}
|
||||
case 'L':
|
||||
ops.push(..._doubleLine(current[0], current[1], data[0], data[1], o));
|
||||
current = [data[0], data[1]];
|
||||
break;
|
||||
case 'C': {
|
||||
const [x1, y1, x2, y2, x, y] = data;
|
||||
ops.push(..._bezierTo(x1, y1, x2, y2, x, y, current, o));
|
||||
current = [x, y];
|
||||
break;
|
||||
}
|
||||
case 'Z':
|
||||
ops.push(..._doubleLine(current[0], current[1], first[0], first[1], o));
|
||||
current = [first[0], first[1]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { type: 'path', ops };
|
||||
}
|
||||
// Fills
|
||||
export function solidFillPolygon(points, o) {
|
||||
const ops = [];
|
||||
if (points.length) {
|
||||
const offset = o.maxRandomnessOffset || 0;
|
||||
const len = points.length;
|
||||
if (len > 2) {
|
||||
ops.push({ op: 'move', data: [points[0][0] + _offsetOpt(offset, o), points[0][1] + _offsetOpt(offset, o)] });
|
||||
for (let i = 1; i < len; i++) {
|
||||
ops.push({ op: 'lineTo', data: [points[i][0] + _offsetOpt(offset, o), points[i][1] + _offsetOpt(offset, o)] });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { type: 'fillPath', ops };
|
||||
}
|
||||
export function patternFillPolygon(points, o) {
|
||||
return getFiller(o, helper).fillPolygon(points, o);
|
||||
}
|
||||
export function patternFillArc(x, y, width, height, start, stop, o) {
|
||||
const cx = x;
|
||||
const cy = y;
|
||||
let rx = Math.abs(width / 2);
|
||||
let ry = Math.abs(height / 2);
|
||||
rx += _offsetOpt(rx * 0.01, o);
|
||||
ry += _offsetOpt(ry * 0.01, o);
|
||||
let strt = start;
|
||||
let stp = stop;
|
||||
while (strt < 0) {
|
||||
strt += Math.PI * 2;
|
||||
stp += Math.PI * 2;
|
||||
}
|
||||
if ((stp - strt) > (Math.PI * 2)) {
|
||||
strt = 0;
|
||||
stp = Math.PI * 2;
|
||||
}
|
||||
const increment = (stp - strt) / o.curveStepCount;
|
||||
const points = [];
|
||||
for (let angle = strt; angle <= stp; angle = angle + increment) {
|
||||
points.push([cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)]);
|
||||
}
|
||||
points.push([cx + rx * Math.cos(stp), cy + ry * Math.sin(stp)]);
|
||||
points.push([cx, cy]);
|
||||
return patternFillPolygon(points, o);
|
||||
}
|
||||
export function randOffset(x, o) {
|
||||
return _offsetOpt(x, o);
|
||||
}
|
||||
export function randOffsetWithRange(min, max, o) {
|
||||
return _offset(min, max, o);
|
||||
}
|
||||
export function doubleLineFillOps(x1, y1, x2, y2, o) {
|
||||
return _doubleLine(x1, y1, x2, y2, o, true);
|
||||
}
|
||||
// Private helpers
|
||||
function cloneOptionsAlterSeed(ops) {
|
||||
const result = Object.assign({}, ops);
|
||||
result.randomizer = undefined;
|
||||
if (ops.seed) {
|
||||
result.seed = ops.seed + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function random(ops) {
|
||||
if (!ops.randomizer) {
|
||||
ops.randomizer = new Random(ops.seed || 0);
|
||||
}
|
||||
return ops.randomizer.next();
|
||||
}
|
||||
function _offset(min, max, ops, roughnessGain = 1) {
|
||||
return ops.roughness * roughnessGain * ((random(ops) * (max - min)) + min);
|
||||
}
|
||||
function _offsetOpt(x, ops, roughnessGain = 1) {
|
||||
return _offset(-x, x, ops, roughnessGain);
|
||||
}
|
||||
function _doubleLine(x1, y1, x2, y2, o, filling = false) {
|
||||
const singleStroke = filling ? o.disableMultiStrokeFill : o.disableMultiStroke;
|
||||
const o1 = _line(x1, y1, x2, y2, o, true, false);
|
||||
if (singleStroke) {
|
||||
return o1;
|
||||
}
|
||||
const o2 = _line(x1, y1, x2, y2, o, true, true);
|
||||
return o1.concat(o2);
|
||||
}
|
||||
function _line(x1, y1, x2, y2, o, move, overlay) {
|
||||
const lengthSq = Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2);
|
||||
const length = Math.sqrt(lengthSq);
|
||||
let roughnessGain = 1;
|
||||
if (length < 200) {
|
||||
roughnessGain = 1;
|
||||
}
|
||||
else if (length > 500) {
|
||||
roughnessGain = 0.4;
|
||||
}
|
||||
else {
|
||||
roughnessGain = (-0.0016668) * length + 1.233334;
|
||||
}
|
||||
let offset = o.maxRandomnessOffset || 0;
|
||||
if ((offset * offset * 100) > lengthSq) {
|
||||
offset = length / 10;
|
||||
}
|
||||
const halfOffset = offset / 2;
|
||||
const divergePoint = 0.2 + random(o) * 0.2;
|
||||
let midDispX = o.bowing * o.maxRandomnessOffset * (y2 - y1) / 200;
|
||||
let midDispY = o.bowing * o.maxRandomnessOffset * (x1 - x2) / 200;
|
||||
midDispX = _offsetOpt(midDispX, o, roughnessGain);
|
||||
midDispY = _offsetOpt(midDispY, o, roughnessGain);
|
||||
const ops = [];
|
||||
const randomHalf = () => _offsetOpt(halfOffset, o, roughnessGain);
|
||||
const randomFull = () => _offsetOpt(offset, o, roughnessGain);
|
||||
if (move) {
|
||||
if (overlay) {
|
||||
ops.push({
|
||||
op: 'move', data: [
|
||||
x1 + randomHalf(),
|
||||
y1 + randomHalf()
|
||||
]
|
||||
});
|
||||
}
|
||||
else {
|
||||
ops.push({
|
||||
op: 'move', data: [
|
||||
x1 + _offsetOpt(offset, o, roughnessGain),
|
||||
y1 + _offsetOpt(offset, o, roughnessGain)
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
if (overlay) {
|
||||
ops.push({
|
||||
op: 'bcurveTo', data: [
|
||||
midDispX + x1 + (x2 - x1) * divergePoint + randomHalf(),
|
||||
midDispY + y1 + (y2 - y1) * divergePoint + randomHalf(),
|
||||
midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomHalf(),
|
||||
midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomHalf(),
|
||||
x2 + randomHalf(),
|
||||
y2 + randomHalf()
|
||||
]
|
||||
});
|
||||
}
|
||||
else {
|
||||
ops.push({
|
||||
op: 'bcurveTo', data: [
|
||||
midDispX + x1 + (x2 - x1) * divergePoint + randomFull(),
|
||||
midDispY + y1 + (y2 - y1) * divergePoint + randomFull(),
|
||||
midDispX + x1 + 2 * (x2 - x1) * divergePoint + randomFull(),
|
||||
midDispY + y1 + 2 * (y2 - y1) * divergePoint + randomFull(),
|
||||
x2 + randomFull(),
|
||||
y2 + randomFull()
|
||||
]
|
||||
});
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
function _curveWithOffset(points, offset, o) {
|
||||
const ps = [];
|
||||
ps.push([
|
||||
points[0][0] + _offsetOpt(offset, o),
|
||||
points[0][1] + _offsetOpt(offset, o),
|
||||
]);
|
||||
ps.push([
|
||||
points[0][0] + _offsetOpt(offset, o),
|
||||
points[0][1] + _offsetOpt(offset, o),
|
||||
]);
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
ps.push([
|
||||
points[i][0] + _offsetOpt(offset, o),
|
||||
points[i][1] + _offsetOpt(offset, o),
|
||||
]);
|
||||
if (i === (points.length - 1)) {
|
||||
ps.push([
|
||||
points[i][0] + _offsetOpt(offset, o),
|
||||
points[i][1] + _offsetOpt(offset, o),
|
||||
]);
|
||||
}
|
||||
}
|
||||
return _curve(ps, null, o);
|
||||
}
|
||||
function _curve(points, closePoint, o) {
|
||||
const len = points.length;
|
||||
const ops = [];
|
||||
if (len > 3) {
|
||||
const b = [];
|
||||
const s = 1 - o.curveTightness;
|
||||
ops.push({ op: 'move', data: [points[1][0], points[1][1]] });
|
||||
for (let i = 1; (i + 2) < len; i++) {
|
||||
const cachedVertArray = points[i];
|
||||
b[0] = [cachedVertArray[0], cachedVertArray[1]];
|
||||
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];
|
||||
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];
|
||||
b[3] = [points[i + 1][0], points[i + 1][1]];
|
||||
ops.push({ op: 'bcurveTo', data: [b[1][0], b[1][1], b[2][0], b[2][1], b[3][0], b[3][1]] });
|
||||
}
|
||||
if (closePoint && closePoint.length === 2) {
|
||||
const ro = o.maxRandomnessOffset;
|
||||
ops.push({ op: 'lineTo', data: [closePoint[0] + _offsetOpt(ro, o), closePoint[1] + _offsetOpt(ro, o)] });
|
||||
}
|
||||
}
|
||||
else if (len === 3) {
|
||||
ops.push({ op: 'move', data: [points[1][0], points[1][1]] });
|
||||
ops.push({
|
||||
op: 'bcurveTo', data: [
|
||||
points[1][0], points[1][1],
|
||||
points[2][0], points[2][1],
|
||||
points[2][0], points[2][1]
|
||||
]
|
||||
});
|
||||
}
|
||||
else if (len === 2) {
|
||||
ops.push(..._doubleLine(points[0][0], points[0][1], points[1][0], points[1][1], o));
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
function _computeEllipsePoints(increment, cx, cy, rx, ry, offset, overlap, o) {
|
||||
const corePoints = [];
|
||||
const allPoints = [];
|
||||
const radOffset = _offsetOpt(0.5, o) - (Math.PI / 2);
|
||||
allPoints.push([
|
||||
_offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment),
|
||||
_offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment)
|
||||
]);
|
||||
for (let angle = radOffset; angle < (Math.PI * 2 + radOffset - 0.01); angle = angle + increment) {
|
||||
const p = [
|
||||
_offsetOpt(offset, o) + cx + rx * Math.cos(angle),
|
||||
_offsetOpt(offset, o) + cy + ry * Math.sin(angle)
|
||||
];
|
||||
corePoints.push(p);
|
||||
allPoints.push(p);
|
||||
}
|
||||
allPoints.push([
|
||||
_offsetOpt(offset, o) + cx + rx * Math.cos(radOffset + Math.PI * 2 + overlap * 0.5),
|
||||
_offsetOpt(offset, o) + cy + ry * Math.sin(radOffset + Math.PI * 2 + overlap * 0.5)
|
||||
]);
|
||||
allPoints.push([
|
||||
_offsetOpt(offset, o) + cx + 0.98 * rx * Math.cos(radOffset + overlap),
|
||||
_offsetOpt(offset, o) + cy + 0.98 * ry * Math.sin(radOffset + overlap)
|
||||
]);
|
||||
allPoints.push([
|
||||
_offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset + overlap * 0.5),
|
||||
_offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset + overlap * 0.5)
|
||||
]);
|
||||
return [allPoints, corePoints];
|
||||
}
|
||||
function _arc(increment, cx, cy, rx, ry, strt, stp, offset, o) {
|
||||
const radOffset = strt + _offsetOpt(0.1, o);
|
||||
const points = [];
|
||||
points.push([
|
||||
_offsetOpt(offset, o) + cx + 0.9 * rx * Math.cos(radOffset - increment),
|
||||
_offsetOpt(offset, o) + cy + 0.9 * ry * Math.sin(radOffset - increment)
|
||||
]);
|
||||
for (let angle = radOffset; angle <= stp; angle = angle + increment) {
|
||||
points.push([
|
||||
_offsetOpt(offset, o) + cx + rx * Math.cos(angle),
|
||||
_offsetOpt(offset, o) + cy + ry * Math.sin(angle)
|
||||
]);
|
||||
}
|
||||
points.push([
|
||||
cx + rx * Math.cos(stp),
|
||||
cy + ry * Math.sin(stp)
|
||||
]);
|
||||
points.push([
|
||||
cx + rx * Math.cos(stp),
|
||||
cy + ry * Math.sin(stp)
|
||||
]);
|
||||
return _curve(points, null, o);
|
||||
}
|
||||
function _bezierTo(x1, y1, x2, y2, x, y, current, o) {
|
||||
const ops = [];
|
||||
const ros = [o.maxRandomnessOffset || 1, (o.maxRandomnessOffset || 1) + 0.3];
|
||||
let f = [0, 0];
|
||||
const iterations = o.disableMultiStroke ? 1 : 2;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
if (i === 0) {
|
||||
ops.push({ op: 'move', data: [current[0], current[1]] });
|
||||
}
|
||||
else {
|
||||
ops.push({ op: 'move', data: [current[0] + _offsetOpt(ros[0], o), current[1] + _offsetOpt(ros[0], o)] });
|
||||
}
|
||||
f = [x + _offsetOpt(ros[i], o), y + _offsetOpt(ros[i], o)];
|
||||
ops.push({
|
||||
op: 'bcurveTo', data: [
|
||||
x1 + _offsetOpt(ros[i], o), y1 + _offsetOpt(ros[i], o),
|
||||
x2 + _offsetOpt(ros[i], o), y2 + _offsetOpt(ros[i], o),
|
||||
f[0], f[1]
|
||||
]
|
||||
});
|
||||
}
|
||||
return ops;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { Config } from './core';
|
||||
import { RoughCanvas } from './canvas';
|
||||
import { RoughGenerator } from './generator';
|
||||
import { RoughSVG } from './svg';
|
||||
declare const _default: {
|
||||
canvas(canvas: HTMLCanvasElement, config?: Config | undefined): RoughCanvas;
|
||||
svg(svg: SVGSVGElement, config?: Config | undefined): RoughSVG;
|
||||
generator(config?: Config | undefined): RoughGenerator;
|
||||
newSeed(): number;
|
||||
};
|
||||
export default _default;
|
@ -0,0 +1,17 @@
|
||||
import { RoughCanvas } from './canvas';
|
||||
import { RoughGenerator } from './generator';
|
||||
import { RoughSVG } from './svg';
|
||||
export default {
|
||||
canvas(canvas, config) {
|
||||
return new RoughCanvas(canvas, config);
|
||||
},
|
||||
svg(svg, config) {
|
||||
return new RoughSVG(svg, config);
|
||||
},
|
||||
generator(config) {
|
||||
return new RoughGenerator(config);
|
||||
},
|
||||
newSeed() {
|
||||
return RoughGenerator.newSeed();
|
||||
}
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
import { Config, Options, OpSet, ResolvedOptions, Drawable } from './core';
|
||||
import { RoughGenerator } from './generator';
|
||||
import { Point } from './geometry';
|
||||
export declare class RoughSVG {
|
||||
private gen;
|
||||
private svg;
|
||||
constructor(svg: SVGSVGElement, config?: Config);
|
||||
draw(drawable: Drawable): SVGGElement;
|
||||
private fillSketch;
|
||||
get generator(): RoughGenerator;
|
||||
getDefaultOptions(): ResolvedOptions;
|
||||
opsToPath(drawing: OpSet): string;
|
||||
line(x1: number, y1: number, x2: number, y2: number, options?: Options): SVGGElement;
|
||||
rectangle(x: number, y: number, width: number, height: number, options?: Options): SVGGElement;
|
||||
ellipse(x: number, y: number, width: number, height: number, options?: Options): SVGGElement;
|
||||
circle(x: number, y: number, diameter: number, options?: Options): SVGGElement;
|
||||
linearPath(points: Point[], options?: Options): SVGGElement;
|
||||
polygon(points: Point[], options?: Options): SVGGElement;
|
||||
arc(x: number, y: number, width: number, height: number, start: number, stop: number, closed?: boolean, options?: Options): SVGGElement;
|
||||
curve(points: Point[], options?: Options): SVGGElement;
|
||||
path(d: string, options?: Options): SVGGElement;
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
import { SVGNS } from './core';
|
||||
import { RoughGenerator } from './generator';
|
||||
export class RoughSVG {
|
||||
constructor(svg, config) {
|
||||
this.svg = svg;
|
||||
this.gen = new RoughGenerator(config);
|
||||
}
|
||||
draw(drawable) {
|
||||
const sets = drawable.sets || [];
|
||||
const o = drawable.options || this.getDefaultOptions();
|
||||
const doc = this.svg.ownerDocument || window.document;
|
||||
const g = doc.createElementNS(SVGNS, 'g');
|
||||
for (const drawing of sets) {
|
||||
let path = null;
|
||||
switch (drawing.type) {
|
||||
case 'path': {
|
||||
path = doc.createElementNS(SVGNS, 'path');
|
||||
path.setAttribute('d', this.opsToPath(drawing));
|
||||
path.setAttribute('stroke', o.stroke);
|
||||
path.setAttribute('stroke-width', o.strokeWidth + '');
|
||||
path.setAttribute('fill', 'none');
|
||||
if (o.strokeLineDash) {
|
||||
path.setAttribute('stroke-dasharray', o.strokeLineDash.join(' ').trim());
|
||||
}
|
||||
if (o.strokeLineDashOffset) {
|
||||
path.setAttribute('stroke-dashoffset', `${o.strokeLineDashOffset}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'fillPath': {
|
||||
path = doc.createElementNS(SVGNS, 'path');
|
||||
path.setAttribute('d', this.opsToPath(drawing));
|
||||
path.setAttribute('stroke', 'none');
|
||||
path.setAttribute('stroke-width', '0');
|
||||
path.setAttribute('fill', o.fill || '');
|
||||
if (drawable.shape === 'curve' || drawable.shape === 'polygon') {
|
||||
path.setAttribute('fill-rule', 'evenodd');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'fillSketch': {
|
||||
path = this.fillSketch(doc, drawing, o);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (path) {
|
||||
g.appendChild(path);
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
fillSketch(doc, drawing, o) {
|
||||
let fweight = o.fillWeight;
|
||||
if (fweight < 0) {
|
||||
fweight = o.strokeWidth / 2;
|
||||
}
|
||||
const path = doc.createElementNS(SVGNS, 'path');
|
||||
path.setAttribute('d', this.opsToPath(drawing));
|
||||
path.setAttribute('stroke', o.fill || '');
|
||||
path.setAttribute('stroke-width', fweight + '');
|
||||
path.setAttribute('fill', 'none');
|
||||
if (o.fillLineDash) {
|
||||
path.setAttribute('stroke-dasharray', o.fillLineDash.join(' ').trim());
|
||||
}
|
||||
if (o.fillLineDashOffset) {
|
||||
path.setAttribute('stroke-dashoffset', `${o.fillLineDashOffset}`);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
get generator() {
|
||||
return this.gen;
|
||||
}
|
||||
getDefaultOptions() {
|
||||
return this.gen.defaultOptions;
|
||||
}
|
||||
opsToPath(drawing) {
|
||||
return this.gen.opsToPath(drawing);
|
||||
}
|
||||
line(x1, y1, x2, y2, options) {
|
||||
const d = this.gen.line(x1, y1, x2, y2, options);
|
||||
return this.draw(d);
|
||||
}
|
||||
rectangle(x, y, width, height, options) {
|
||||
const d = this.gen.rectangle(x, y, width, height, options);
|
||||
return this.draw(d);
|
||||
}
|
||||
ellipse(x, y, width, height, options) {
|
||||
const d = this.gen.ellipse(x, y, width, height, options);
|
||||
return this.draw(d);
|
||||
}
|
||||
circle(x, y, diameter, options) {
|
||||
const d = this.gen.circle(x, y, diameter, options);
|
||||
return this.draw(d);
|
||||
}
|
||||
linearPath(points, options) {
|
||||
const d = this.gen.linearPath(points, options);
|
||||
return this.draw(d);
|
||||
}
|
||||
polygon(points, options) {
|
||||
const d = this.gen.polygon(points, options);
|
||||
return this.draw(d);
|
||||
}
|
||||
arc(x, y, width, height, start, stop, closed = false, options) {
|
||||
const d = this.gen.arc(x, y, width, height, start, stop, closed, options);
|
||||
return this.draw(d);
|
||||
}
|
||||
curve(points, options) {
|
||||
const d = this.gen.curve(points, options);
|
||||
return this.draw(d);
|
||||
}
|
||||
path(d, options) {
|
||||
const drawing = this.gen.path(d, options);
|
||||
return this.draw(drawing);
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,74 @@
|
||||
{
|
||||
"_from": "roughjs",
|
||||
"_id": "roughjs@4.3.1",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-m42+OBaBR7x5UhIKyjBCnWqqkaEkBKLkXvHv4pOWJXPofvMnQY4ZcFEQlqf3coKKyZN2lfWMyx7QXSg2GD7SGA==",
|
||||
"_location": "/roughjs",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "tag",
|
||||
"registry": true,
|
||||
"raw": "roughjs",
|
||||
"name": "roughjs",
|
||||
"escapedName": "roughjs",
|
||||
"rawSpec": "",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "latest"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"#USER",
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.3.1.tgz",
|
||||
"_shasum": "b7af0b205c94bc3b79ee5a7eae1e09d5063bc3fe",
|
||||
"_spec": "roughjs",
|
||||
"_where": "/Users/1009738/PracticalVision",
|
||||
"author": {
|
||||
"name": "Preet Shihn",
|
||||
"email": "preetshihn@gmail.com"
|
||||
},
|
||||
"browser": "bundled/rough.js",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pshihn/rough/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {
|
||||
"path-data-parser": "^0.1.0",
|
||||
"points-on-curve": "^0.2.0",
|
||||
"points-on-path": "^0.2.1"
|
||||
},
|
||||
"deprecated": false,
|
||||
"description": "Create graphics using HTML Canvas or SVG with a hand-drawn, sketchy, appearance.",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^2.1.0",
|
||||
"rollup": "^1.32.1",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"tslint": "^5.20.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"homepage": "https://roughjs.com",
|
||||
"keywords": [
|
||||
"canvas",
|
||||
"svg",
|
||||
"graphics",
|
||||
"sketchy",
|
||||
"hand drawn",
|
||||
"hand-drawn"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "bundled/rough.cjs.js",
|
||||
"module": "bundled/rough.esm.js",
|
||||
"name": "roughjs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/pshihn/rough.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf bin && tsc && rollup -c",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"types": "bin/rough.d.ts",
|
||||
"version": "4.3.1"
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"declaration": true,
|
||||
"outDir": "./bin",
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"path-data-parser": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
|
||||
"integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="
|
||||
},
|
||||
"points-on-curve": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
|
||||
"integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="
|
||||
},
|
||||
"points-on-path": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
|
||||
"integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
|
||||
"requires": {
|
||||
"path-data-parser": "0.1.0",
|
||||
"points-on-curve": "0.2.0"
|
||||
}
|
||||
},
|
||||
"roughjs": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.3.1.tgz",
|
||||
"integrity": "sha512-m42+OBaBR7x5UhIKyjBCnWqqkaEkBKLkXvHv4pOWJXPofvMnQY4ZcFEQlqf3coKKyZN2lfWMyx7QXSg2GD7SGA==",
|
||||
"requires": {
|
||||
"path-data-parser": "^0.1.0",
|
||||
"points-on-curve": "^0.2.0",
|
||||
"points-on-path": "^0.2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,458 @@
|
||||
/*
|
||||
This file contains functions for drawing 2d primitives with a handy sketchy look in p5.js.
|
||||
|
||||
Author: Janneck Wullschleger in 07/2016
|
||||
Web: http://itsjw.de
|
||||
Mail: jw@itsjw.de
|
||||
|
||||
Updated: 24.02.2017 to use with a reference to the p5 instance.
|
||||
Just put it in as param to the constructor.
|
||||
|
||||
Much of the source code is taken from the handy library for processing,
|
||||
written by Jo Wood, giCentre, City University London based on an idea by Nikolaus Gradwohl.
|
||||
The handy library is licensed under the GNU Lesser General Public License: http://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
function Scribble(p) {
|
||||
this.sketch = p || window;
|
||||
this.bowing = 1;
|
||||
this.roughness = 1;
|
||||
this.maxOffset = 2;
|
||||
this.numEllipseSteps = 9;
|
||||
this.ellipseInc = (Math.PI*2)/this.numEllipseSteps;
|
||||
|
||||
this.getOffset = function( minVal, maxVal ) {
|
||||
return this.roughness*(this.sketch.random()*(maxVal-minVal)+minVal);
|
||||
}
|
||||
|
||||
this.buildEllipse = function( cx, cy, rx, ry, offset, overlap ) {
|
||||
var radialOffset = this.getOffset( -0.5, 0.5 )-Math.PI/2;
|
||||
|
||||
this.sketch.beginShape();
|
||||
this.sketch.curveVertex( this.getOffset( -offset, offset )+cx+0.9*rx*Math.cos( radialOffset-this.ellipseInc ),
|
||||
this.getOffset( -offset, offset )+cy+0.9*ry*Math.sin( radialOffset-this.ellipseInc ) );
|
||||
|
||||
for ( var theta = radialOffset; theta < Math.PI*2+radialOffset-0.01; theta+=this.ellipseInc ) {
|
||||
this.sketch.curveVertex( this.getOffset( -offset, offset )+cx+rx*Math.cos( theta ),
|
||||
this.getOffset( -offset, offset )+cy+ry*Math.sin( theta ) );
|
||||
}
|
||||
|
||||
this.sketch.curveVertex( this.getOffset( -offset, offset )+cx+rx*Math.cos( radialOffset+Math.PI*2+overlap*0.5 ),
|
||||
this.getOffset( -offset, offset )+cy+ry*Math.sin( radialOffset+Math.PI*2+overlap*0.5 ) );
|
||||
|
||||
this.sketch.curveVertex( this.getOffset( -offset, offset )+cx+0.98*rx*Math.cos( radialOffset+overlap ),
|
||||
this.getOffset( -offset, offset )+cy+0.98*ry*Math.sin( radialOffset+overlap ) );
|
||||
|
||||
this.sketch.curveVertex( this.getOffset( -offset, offset )+cx+0.9*rx*Math.cos( radialOffset+overlap*0.5 ),
|
||||
this.getOffset( -offset, offset )+cy+0.9*ry*Math.sin( radialOffset+overlap*0.5 ) );
|
||||
this.sketch.endShape();
|
||||
}
|
||||
|
||||
this.getIntersectingLines = function( lineCoords, xCoords, yCoords ) {
|
||||
var intersections = [];
|
||||
var s1 = new Segment( lineCoords[0], lineCoords[1], lineCoords[2], lineCoords[3] );
|
||||
|
||||
for ( var i = 0; i < xCoords.length; i++ ) {
|
||||
var s2 = new Segment( xCoords[i], yCoords[i], xCoords[(i+1)%xCoords.length], yCoords[(i+1)%xCoords.length] );
|
||||
|
||||
if ( s1.compare(s2) == Relation.INTERSECTS ) {
|
||||
intersections.push( [s1.getIntersectionX(), s1.getIntersectionY()] );
|
||||
}
|
||||
}
|
||||
return intersections;
|
||||
}
|
||||
|
||||
this.scribbleLine = function( x1, y1, x2, y2 ) {
|
||||
var lenSq = (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2);
|
||||
var offset = this.maxOffset;
|
||||
|
||||
if ( this.maxOffset*this.maxOffset*100 > lenSq ) {
|
||||
offset = Math.sqrt( lenSq )/10;
|
||||
}
|
||||
|
||||
var halfOffset = offset/2;
|
||||
var divergePoint = 0.2 + this.sketch.random()*0.2;
|
||||
var midDispX = this.bowing*this.maxOffset*(y2-y1)/200;
|
||||
var midDispY = this.bowing*this.maxOffset*(x1-x2)/200;
|
||||
midDispX = this.getOffset( -midDispX, midDispX );
|
||||
midDispY = this.getOffset( -midDispY, midDispY );
|
||||
|
||||
this.sketch.noFill();
|
||||
|
||||
this.sketch.beginShape();
|
||||
this.sketch.vertex( x1 + this.getOffset( -offset, offset ), y1 + this.getOffset( -offset, offset ) );
|
||||
this.sketch.curveVertex(x1 + this.getOffset( -offset, offset ), y1 + this.getOffset( -offset, offset ) );
|
||||
this.sketch.curveVertex(midDispX+x1+(x2 -x1)*divergePoint + this.getOffset( -offset, offset ), midDispY+y1 + (y2-y1)*divergePoint + this.getOffset( -offset, offset ) );
|
||||
this.sketch.curveVertex(midDispX+x1+2*(x2-x1)*divergePoint + this.getOffset( -offset, offset ), midDispY+y1+ 2*(y2-y1)*divergePoint + this.getOffset( -offset,offset ) );
|
||||
this.sketch.curveVertex(x2 + this.getOffset( -offset, offset ), y2 + this.getOffset( -offset, offset ) );
|
||||
this.sketch.vertex( x2 + this.getOffset( -offset, offset ), y2 + this.getOffset( -offset, offset ) );
|
||||
this.sketch.endShape();
|
||||
|
||||
this.sketch.beginShape();
|
||||
this.sketch.vertex( x1 + this.getOffset( -halfOffset, halfOffset ), y1 + this.getOffset( -halfOffset, halfOffset ) );
|
||||
this.sketch.curveVertex(x1 + this.getOffset( -halfOffset, halfOffset ), y1 + this.getOffset( -halfOffset, halfOffset ) );
|
||||
this.sketch.curveVertex(midDispX+x1+(x2 -x1)*divergePoint + this.getOffset( -halfOffset, halfOffset ), midDispY+y1 + (y2-y1)*divergePoint + this.getOffset( -halfOffset, halfOffset ) );
|
||||
this.sketch.curveVertex(midDispX+x1+2*(x2-x1)*divergePoint + this.getOffset( -halfOffset, halfOffset ), midDispY+y1+ 2*(y2-y1)*divergePoint + this.getOffset( -halfOffset, halfOffset ) );
|
||||
this.sketch.curveVertex(x2 + this.getOffset( -halfOffset, halfOffset ), y2 + this.getOffset( -halfOffset, halfOffset ) );
|
||||
this.sketch.vertex( x2 + this.getOffset( -halfOffset, halfOffset ), y2 + this.getOffset( -halfOffset, halfOffset ) );
|
||||
this.sketch.endShape();
|
||||
}
|
||||
|
||||
this.scribbleCurve = function( x1, y1, x2, y2, x3, y3, x4, y4 ) {
|
||||
this.sketch.bezier( x1+this.getOffset( -2, 2 ), y1+this.getOffset( -2, 2 ),
|
||||
x3+this.getOffset( -4, 4 ), y3+this.getOffset( -3, 3 ),
|
||||
x4+this.getOffset( -3, 3 ), y4+this.getOffset( -3, 3 ),
|
||||
x2+this.getOffset( -1, 1 ), y2+this.getOffset( -1, 1 ) );
|
||||
|
||||
this.sketch.bezier( x1+this.getOffset( -2, 2 ), y1+this.getOffset( -2, 2 ),
|
||||
x3+this.getOffset( -3, 3 ), y3+this.getOffset( -3, 3 ),
|
||||
x4+this.getOffset( -3, 3 ), y4+this.getOffset( -4, 4 ),
|
||||
x2+this.getOffset( -2, 2 ), y2+this.getOffset( -2, 2 ) );
|
||||
}
|
||||
|
||||
this.scribbleRect = function( x, y, w, h ) {
|
||||
var halfWidth = w/2;
|
||||
var halfHeight = h/2;
|
||||
var left = Math.min( x-halfWidth, x+halfWidth );
|
||||
var right = Math.max( x-halfWidth, x+halfWidth );
|
||||
var top = Math.min( y-halfHeight, y+halfHeight );
|
||||
var bottom = Math.max( y-halfHeight, y+halfHeight );
|
||||
|
||||
this.scribbleLine( left, top, right, top );
|
||||
this.scribbleLine( right, top, right, bottom );
|
||||
this.scribbleLine( right, bottom, left, bottom );
|
||||
this.scribbleLine( left, bottom, left, top );
|
||||
}
|
||||
|
||||
this.scribbleRoundedRect = function( x, y, w, h, radius ) {
|
||||
var halfWidth = w/2;
|
||||
var halfHeight = h/2;
|
||||
|
||||
if ( radius == 0 || radius > halfWidth || radius > halfHeight ) {
|
||||
this.scribbleRect( x, y, w, h );
|
||||
return;
|
||||
}
|
||||
|
||||
var left = Math.min( x-halfWidth, x+halfWidth );
|
||||
var right = Math.max( x-halfWidth, x+halfWidth );
|
||||
var top = Math.min( y-halfHeight, y+halfHeight );
|
||||
var bottom = Math.max( y-halfHeight, y+halfHeight );
|
||||
|
||||
this.scribbleLine( left+radius, top, right-radius, top, 1.5 );
|
||||
this.scribbleLine( right, top+radius, right, bottom-radius, 1.5 );
|
||||
this.scribbleLine( right-radius, bottom, left+radius, bottom, 1.5 );
|
||||
this.scribbleLine( left, bottom-radius, left, top+radius, 1.5 );
|
||||
|
||||
this.scribbleCurve( left+radius, top, left, top+radius, left+radius*0.1, top+radius*0.1, left+radius*0.1, top+radius*0.1 );
|
||||
this.scribbleCurve( right-radius, top, right, top+radius, right-radius*0.1, top+radius*0.1, right-radius*0.1, top+radius*0.1 );
|
||||
this.scribbleCurve( left+radius, bottom, left, bottom-radius, left+radius*0.1, bottom-radius*0.1, left+radius*0.1, bottom-radius*0.1 );
|
||||
this.scribbleCurve( right-radius, bottom, right, bottom-radius, right-radius*0.1, bottom-radius*0.1, right-radius*0.1, bottom-radius*0.1 );
|
||||
}
|
||||
|
||||
this.scribbleEllipse = function( x, y, w, h ) {
|
||||
var rx = Math.abs(w/2);
|
||||
var ry = Math.abs(h/2);
|
||||
|
||||
rx += this.getOffset( -rx*0.05, rx*0.05 );
|
||||
ry += this.getOffset( -ry*0.05, ry*0.05 );
|
||||
|
||||
this.buildEllipse( x, y, rx, ry, 1, this.ellipseInc*this.getOffset( 0.1, this.getOffset( 0.4, 1 ) ) );
|
||||
this.buildEllipse( x, y, rx, ry, 1.5, 0 );
|
||||
}
|
||||
|
||||
this.scribbleFilling = function( xCoords, yCoords, gap, angle ) {
|
||||
if ((xCoords == null) || (yCoords == null) || (xCoords.length == 0) || (yCoords.length == 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hachureAngle = this.sketch.radians( angle%180 );
|
||||
var cosAngle = Math.cos( hachureAngle );
|
||||
var sinAngle = Math.sin( hachureAngle );
|
||||
var tanAngle = Math.tan( hachureAngle );
|
||||
|
||||
var left = xCoords[0];
|
||||
var right = xCoords[0];
|
||||
var top = yCoords[0];
|
||||
var bottom = yCoords[0];
|
||||
|
||||
for ( var i = 1; i < xCoords.length; i++ ) {
|
||||
left = Math.min( left, xCoords[i] );
|
||||
right = Math.max( right, xCoords[i] );
|
||||
top = Math.min( top, yCoords[i] );
|
||||
bottom = Math.max( bottom, yCoords[i] );
|
||||
}
|
||||
|
||||
var it = new HachureIterator( top-1, bottom+1, left-1, right+1, gap, sinAngle, cosAngle, tanAngle );
|
||||
var rectCoords = null;
|
||||
|
||||
while ( (rectCoords = it.getNextLine()) != null ) {
|
||||
var lines = this.getIntersectingLines( rectCoords, xCoords, yCoords );
|
||||
|
||||
for ( var i = 0; i < lines.length; i+=2 ) {
|
||||
if ( i < lines.length-1 ) {
|
||||
var p1 = lines[i];
|
||||
var p2 = lines[i+1];
|
||||
this.scribbleLine( p1[0], p1[1], p2[0], p2[1], 2 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function HachureIterator( _top, _bottom, _left, _right, _gap, _sinAngle, _cosAngle, _tanAngle ) {
|
||||
var sinAngle = _sinAngle;
|
||||
var tanAngle = _tanAngle;
|
||||
var top = _top;
|
||||
var bottom = _bottom;
|
||||
var left = _left;
|
||||
var right = _right;
|
||||
var gap = _gap;
|
||||
|
||||
var pos;
|
||||
var deltaX, hGap;
|
||||
var sLeft, sRight;
|
||||
|
||||
if (Math.abs(sinAngle) < 0.0001) {
|
||||
pos = left+gap;
|
||||
} else if (Math.abs(sinAngle) > 0.9999) {
|
||||
pos = top+gap;
|
||||
} else {
|
||||
deltaX = (bottom-top)*Math.abs(tanAngle);
|
||||
pos = left-Math.abs(deltaX);
|
||||
hGap = Math.abs(gap / _cosAngle);
|
||||
sLeft = new Segment(left, bottom, left, top);
|
||||
sRight = new Segment(right, bottom, right, top);
|
||||
}
|
||||
|
||||
this.getNextLine = function() {
|
||||
if (Math.abs(sinAngle) < 0.0001) {
|
||||
if (pos < right) {
|
||||
var line = [pos, top, pos, bottom];
|
||||
pos += gap;
|
||||
return line;
|
||||
}
|
||||
} else if (Math.abs(sinAngle) > 0.9999) {
|
||||
if (pos<bottom) {
|
||||
var line = [left, pos, right, pos];
|
||||
pos += gap;
|
||||
return line;
|
||||
}
|
||||
} else {
|
||||
var xLower = pos-deltaX/2;
|
||||
var xUpper = pos+deltaX/2;
|
||||
var yLower = bottom;
|
||||
var yUpper = top;
|
||||
|
||||
if (pos < right+deltaX) {
|
||||
while (((xLower < left) && (xUpper < left)) || ((xLower > right) && (xUpper > right))) {
|
||||
pos += hGap;
|
||||
xLower = pos-deltaX/2;
|
||||
xUpper = pos+deltaX/2;
|
||||
|
||||
if (pos > right+deltaX) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var s = new Segment(xLower, yLower, xUpper, yUpper);
|
||||
|
||||
if (s.compare(sLeft) == Relation.INTERSECTS) {
|
||||
xLower = s.getIntersectionX();
|
||||
yLower = s.getIntersectionY();
|
||||
}
|
||||
if (s.compare(sRight) == Relation.INTERSECTS) {
|
||||
xUpper = s.getIntersectionX();
|
||||
yUpper = s.getIntersectionY();
|
||||
}
|
||||
if (tanAngle > 0) {
|
||||
xLower = right-(xLower-left);
|
||||
xUpper = right-(xUpper-left);
|
||||
}
|
||||
|
||||
var line = [xLower, yLower, xUpper, yUpper];
|
||||
pos += hGap;
|
||||
return line;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function Segment( _x1, _y1, _x2, _y2 ) {
|
||||
var x1 = _x1;
|
||||
var y1 =_y1;
|
||||
var x2 = _x2;
|
||||
var y2 = _y2;
|
||||
var a, b, c;
|
||||
var undef;
|
||||
var xi = Number.MAX_VALUE;
|
||||
var yi = Number.MAX_VALUE;
|
||||
|
||||
a=y2-y1;
|
||||
b=x1-x2;
|
||||
c=x2*y1-x1*y2;
|
||||
|
||||
if ((a==0) && (b==0) && (c==0)) {
|
||||
undef = true;
|
||||
} else {
|
||||
undef = false;
|
||||
}
|
||||
|
||||
this.compare = function( otherSegment ) {
|
||||
if ((this.isUndefined()) || (otherSegment.isUndefined())) {
|
||||
return Relation.UNDEFINED;
|
||||
}
|
||||
|
||||
var grad1 = Number.MAX_VALUE;
|
||||
var grad2 = Number.MAX_VALUE;
|
||||
var int1 = 0;
|
||||
var int2 = 0;
|
||||
|
||||
if (Math.abs(b) > 0.00001) {
|
||||
grad1 = -a/b;
|
||||
int1 = -c/b;
|
||||
}
|
||||
|
||||
if (Math.abs(otherSegment.getB()) > 0.00001) {
|
||||
grad2 = -otherSegment.getA()/otherSegment.getB();
|
||||
int2 = -otherSegment.getC()/otherSegment.getB();
|
||||
}
|
||||
|
||||
if (grad1 == Number.MAX_VALUE) {
|
||||
if (grad2 == Number.MAX_VALUE) {
|
||||
if (-c/a != -otherSegment.getC()/otherSegment.getA()) {
|
||||
return Relation.SEPARATE;
|
||||
}
|
||||
|
||||
if ((y1 >= Math.min(otherSegment.getPy1(),otherSegment.getPy2())) &&
|
||||
(y1 <= Math.max(otherSegment.getPy1(),otherSegment.getPy2()))) {
|
||||
xi = x1;
|
||||
yi = y1;
|
||||
return Relation.INTERSECTS;
|
||||
}
|
||||
|
||||
if ((y2 >= Math.min(otherSegment.getPy1(),otherSegment.getPy2())) &&
|
||||
(y2 <= Math.max(otherSegment.getPy1(),otherSegment.getPy2()))) {
|
||||
xi = x2;
|
||||
yi = y2;
|
||||
return Relation.INTERSECTS;
|
||||
}
|
||||
|
||||
return Relation.SEPARATE;
|
||||
}
|
||||
|
||||
xi = x1;
|
||||
yi = grad2*xi+int2;
|
||||
|
||||
if (((y1-yi)*(yi-y2) < -0.00001) || ((otherSegment.getPy1()-yi)*(yi-otherSegment.getPy2()) < -0.00001)) {
|
||||
return Relation.SEPARATE;
|
||||
}
|
||||
|
||||
if (Math.abs(otherSegment.getA()) < 0.00001) {
|
||||
if ((otherSegment.getPx1()-xi)*(xi-otherSegment.getPx2()) < -0.00001) {
|
||||
return Relation.SEPARATE;
|
||||
}
|
||||
return Relation.INTERSECTS;
|
||||
}
|
||||
return Relation.INTERSECTS;
|
||||
}
|
||||
|
||||
if (grad2 == Number.MAX_VALUE) {
|
||||
xi = otherSegment.getPx1();
|
||||
yi = grad1*xi+int1;
|
||||
|
||||
if (((otherSegment.getPy1()-yi)*(yi-otherSegment.getPy2()) < -0.00001) || ((y1-yi)*(yi-y2) < -0.00001)) {
|
||||
return Relation.SEPARATE;
|
||||
}
|
||||
|
||||
if (Math.abs(a) < 0.00001) {
|
||||
if ((x1-xi)*(xi-x2) < -0.00001) {
|
||||
return Relation.SEPARATE;
|
||||
}
|
||||
return Relation.INTERSECTS;
|
||||
}
|
||||
return Relation.INTERSECTS;
|
||||
}
|
||||
|
||||
if (grad1 == grad2) {
|
||||
if (int1 != int2) {
|
||||
return Relation.SEPARATE;
|
||||
}
|
||||
|
||||
if ((x1 >= Math.min(otherSegment.getPx1(),otherSegment.getPx2())) &&
|
||||
(x1 <= Math.max(otherSegment.getPy1(),otherSegment.getPy2()))) {
|
||||
xi = x1;
|
||||
yi = y1;
|
||||
return Relation.INTERSECTS;
|
||||
}
|
||||
|
||||
if ((x2 >= Math.min(otherSegment.getPx1(),otherSegment.getPx2())) &&
|
||||
(x2 <= Math.max(otherSegment.getPx1(),otherSegment.getPx2()))) {
|
||||
xi = x2;
|
||||
yi = y2;
|
||||
return Relation.INTERSECTS;
|
||||
}
|
||||
|
||||
return Relation.SEPARATE;
|
||||
}
|
||||
|
||||
xi = (int2-int1)/(grad1-grad2);
|
||||
yi = grad1*xi + int1;
|
||||
|
||||
if (((x1-xi)*(xi-x2) < -0.00001) || ((otherSegment.getPx1()-xi)*(xi-otherSegment.getPx2()) < -0.00001)) {
|
||||
return Relation.SEPARATE;
|
||||
}
|
||||
return Relation.INTERSECTS;
|
||||
}
|
||||
|
||||
this.getPx1 = function() {
|
||||
return x1;
|
||||
}
|
||||
|
||||
this.getPy1 = function() {
|
||||
return y1;
|
||||
}
|
||||
|
||||
this.getPx2 = function() {
|
||||
return x2;
|
||||
}
|
||||
|
||||
this.getPy2 = function() {
|
||||
return y2;
|
||||
}
|
||||
|
||||
this.isUndefined = function() {
|
||||
return undef;
|
||||
}
|
||||
|
||||
this.getA = function() {
|
||||
return a;
|
||||
}
|
||||
|
||||
this.getB = function() {
|
||||
return b;
|
||||
}
|
||||
|
||||
this.getC = function() {
|
||||
return c;
|
||||
}
|
||||
|
||||
this.getIntersectionX = function() {
|
||||
return xi;
|
||||
}
|
||||
|
||||
this.getIntersectionY = function() {
|
||||
return yi;
|
||||
}
|
||||
|
||||
this.getLength = function( tx1, ty1, tx2, ty2 ) {
|
||||
var dx = tx2 - tx1;
|
||||
var dy = ty2 - ty1;
|
||||
return Math.sqrt(dx*dx + dy*dy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var Relation = { LEFT:1, RIGHT:2, INTERSECTS:3, AHEAD:4, BEHIND:5, SEPARATE:6, UNDEFINED:7 };
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,23 @@
|
||||
import rough from 'roughjs';
|
||||
const rc = require('roughjs')
|
||||
const rc = rough.canvas(document.getElementById('canvas'));
|
||||
rc.rectangle(100, 100, 200, 200); // x, y, width, height
|
||||
|
||||
// function getOffset( el ) {
|
||||
// var _x = 0;
|
||||
// var _y = 0;
|
||||
// while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
|
||||
// _x += el.offsetLeft - el.scrollLeft;
|
||||
// _y += el.offsetTop - el.scrollTop;
|
||||
// el = el.offsetParent;
|
||||
// }
|
||||
// return { top: _y, left: _x };
|
||||
// }
|
||||
// var x = getOffset( document.getElementById('complexities') ).left;
|
||||
// var y = getOffset( document.getElementById('complexities') ).top;
|
||||
|
||||
|
||||
|
||||
|
||||
// var scribble = new Scribble();
|
||||
// scribble.scribbleEllipse(200,300,300,200)
|
Loading…
Reference in New Issue