chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -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,214 @@
import type {
Config,
Drawable,
OpSet,
Options,
ResolvedOptions,
} from './core.js';
import { RoughGenerator } from './generator.js';
import type { Point } from './geometry.js';
export class RoughCanvas {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private gen: RoughGenerator;
get generator(): RoughGenerator {
return this.gen;
}
constructor(canvas: HTMLCanvasElement, config?: Config) {
this.canvas = canvas;
this.ctx = this.canvas.getContext('2d')!;
this.gen = new RoughGenerator(config);
}
private _drawToContext(
ctx: CanvasRenderingContext2D,
drawing: OpSet,
fixedDecimals?: number,
rule: CanvasFillRule = 'nonzero'
) {
ctx.beginPath();
for (const item of drawing.ops) {
const data =
typeof fixedDecimals === 'number' && fixedDecimals >= 0
? item.data.map(d => +d.toFixed(fixedDecimals))
: 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();
}
}
private fillSketch(
ctx: CanvasRenderingContext2D,
drawing: OpSet,
o: ResolvedOptions
) {
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, o.fixedDecimalPlaceDigits);
ctx.restore();
}
arc(
x: number,
y: number,
width: number,
height: number,
start: number,
stop: number,
closed = false,
options?: Options
): Drawable {
const d = this.gen.arc(x, y, width, height, start, stop, closed, options);
this.draw(d);
return d;
}
circle(x: number, y: number, diameter: number, options?: Options): Drawable {
const d = this.gen.circle(x, y, diameter, options);
this.draw(d);
return d;
}
curve(points: Point[], options?: Options): Drawable {
const d = this.gen.curve(points, options);
this.draw(d);
return d;
}
draw(drawable: Drawable): void {
const sets = drawable.sets || [];
const o = drawable.options || this.getDefaultOptions();
const ctx = this.ctx;
const precision = drawable.options.fixedDecimalPlaceDigits;
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, precision);
ctx.restore();
break;
case 'fillPath': {
ctx.save();
ctx.fillStyle = o.fill || '';
const fillRule: CanvasFillRule =
drawable.shape === 'curve' ||
drawable.shape === 'polygon' ||
drawable.shape === 'path'
? 'evenodd'
: 'nonzero';
this._drawToContext(ctx, drawing, precision, fillRule);
ctx.restore();
break;
}
case 'fillSketch':
this.fillSketch(ctx, drawing, o);
break;
}
}
}
ellipse(
x: number,
y: number,
width: number,
height: number,
options?: Options
): Drawable {
const d = this.gen.ellipse(x, y, width, height, options);
this.draw(d);
return d;
}
getDefaultOptions(): ResolvedOptions {
return this.gen.defaultOptions;
}
line(
x1: number,
y1: number,
x2: number,
y2: number,
options?: Options
): Drawable {
const d = this.gen.line(x1, y1, x2, y2, options);
this.draw(d);
return d;
}
linearPath(points: Point[], options?: Options): Drawable {
const d = this.gen.linearPath(points, options);
this.draw(d);
return d;
}
path(d: string, options?: Options): Drawable {
const drawing = this.gen.path(d, options);
this.draw(drawing);
return drawing;
}
polygon(points: Point[], options?: Options): Drawable {
const d = this.gen.polygon(points, options);
this.draw(d);
return d;
}
rectangle(
x: number,
y: number,
width: number,
height: number,
options?: Options
): Drawable {
const d = this.gen.rectangle(x, y, width, height, options);
this.draw(d);
return d;
}
}
@@ -0,0 +1,93 @@
import type { Point } from './geometry.js';
import type { Random } from './math.js';
export 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;
strokeLineDash?: number[];
strokeLineDashOffset?: number;
fillLineDash?: number[];
fillLineDashOffset?: number;
disableMultiStroke?: boolean;
disableMultiStrokeFill?: boolean;
preserveVertices?: boolean;
fixedDecimalPlaceDigits?: number;
}
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;
randomizer?: Random;
disableMultiStroke: boolean;
disableMultiStrokeFill: boolean;
preserveVertices: 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,62 @@
import type { Op, OpSet, ResolvedOptions } from '../core.js';
import type { Line, Point } from '../geometry.js';
import { lineLength } from '../geometry.js';
import type { PatternFiller, RenderHelper } from './filler-interface.js';
import { polygonHachureLines } from './scan-line-hachure.js';
export class DashedFiller implements PatternFiller {
private helper: RenderHelper;
constructor(helper: RenderHelper) {
this.helper = helper;
}
private dashedLine(lines: Line[], o: ResolvedOptions): Op[] {
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: Op[] = [];
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: Point = [
p1[0] + lstart * Math.cos(alpha) + startOffset * Math.cos(alpha),
p1[1] + lstart * Math.sin(alpha) + startOffset * Math.sin(alpha),
];
const end: Point = [
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;
}
fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet {
const lines = polygonHachureLines(polygonList, o);
return { type: 'fillSketch', ops: this.dashedLine(lines, o) };
}
}
@@ -0,0 +1,50 @@
import type { Op, OpSet, ResolvedOptions } from '../core.js';
import type { Line, Point } from '../geometry.js';
import { lineLength } from '../geometry.js';
import type { PatternFiller, RenderHelper } from './filler-interface.js';
import { polygonHachureLines } from './scan-line-hachure.js';
export class DotFiller implements PatternFiller {
private helper: RenderHelper;
constructor(helper: RenderHelper) {
this.helper = helper;
}
private dotsOnLines(lines: Line[], o: ResolvedOptions): OpSet {
const ops: Op[] = [];
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 = x - ro + Math.random() * 2 * ro;
const cy = y - ro + Math.random() * 2 * ro;
const el = this.helper.ellipse(cx, cy, fweight, fweight, o);
ops.push(...el.ops);
}
}
return { type: 'fillSketch', ops };
}
fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet {
o = Object.assign({}, o, { hachureAngle: 0 });
const lines = polygonHachureLines(polygonList, o);
return this.dotsOnLines(lines, o);
}
}
@@ -0,0 +1,25 @@
import type { Op, OpSet, ResolvedOptions } from '../core.js';
import type { Point } from '../geometry.js';
export interface PatternFiller {
fillPolygons(polygonList: 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,54 @@
import type { ResolvedOptions } from '../core.js';
import { DashedFiller } from './dashed-filler.js';
import { DotFiller } from './dot-filler.js';
import type { PatternFiller, RenderHelper } from './filler-interface.js';
import { HachureFiller } from './hachure-filler.js';
import { HatchFiller } from './hatch-filler.js';
import { ZigZagFiller } from './zigzag-filler.js';
import { ZigZagLineFiller } from './zigzag-line-filler.js';
const fillers: Record<string, PatternFiller> = {};
export function getFiller(
o: ResolvedOptions,
helper: RenderHelper
): PatternFiller {
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,38 @@
import type { Op, OpSet, ResolvedOptions } from '../core.js';
import type { Line, Point } from '../geometry.js';
import type { PatternFiller, RenderHelper } from './filler-interface.js';
import { polygonHachureLines } from './scan-line-hachure.js';
export class HachureFiller implements PatternFiller {
private helper: RenderHelper;
constructor(helper: RenderHelper) {
this.helper = helper;
}
protected _fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet {
const lines = polygonHachureLines(polygonList, o);
const ops = this.renderLines(lines, o);
return { type: 'fillSketch', ops };
}
fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet {
return this._fillPolygons(polygonList, o);
}
protected renderLines(lines: Line[], o: ResolvedOptions): Op[] {
const ops: Op[] = [];
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;
}
}
@@ -0,0 +1,13 @@
import type { OpSet, ResolvedOptions } from '../core.js';
import type { Point } from '../geometry.js';
import { HachureFiller } from './hachure-filler.js';
export class HatchFiller extends HachureFiller {
override fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet {
const set = this._fillPolygons(polygonList, o);
const o2 = Object.assign({}, o, { hachureAngle: o.hachureAngle + 90 });
const set2 = this._fillPolygons(polygonList, o2);
set.ops = set.ops.concat(set2.ops);
return set;
}
}
@@ -0,0 +1,152 @@
import type { ResolvedOptions } from '../core.js';
import type { Line, Point } from '../geometry.js';
import { rotateLines, rotatePoints } from '../geometry.js';
interface EdgeEntry {
ymin: number;
ymax: number;
x: number;
islope: number;
}
interface ActiveEdgeEntry {
s: number;
edge: EdgeEntry;
}
export function polygonHachureLines(
polygonList: Point[][],
o: ResolvedOptions
): Line[] {
const angle = o.hachureAngle + 90;
let gap = o.hachureGap;
if (gap < 0) {
gap = o.strokeWidth * 4;
}
gap = Math.max(gap, 0.1);
const rotationCenter: Point = [0, 0];
if (angle) {
for (const polygon of polygonList) {
rotatePoints(polygon, rotationCenter, angle);
}
}
const lines = straightHachureLines(polygonList, gap);
if (angle) {
for (const polygon of polygonList) {
rotatePoints(polygon, rotationCenter, -angle);
}
rotateLines(lines, rotationCenter, -angle);
}
return lines;
}
function straightHachureLines(polygonList: Point[][], gap: number): Line[] {
const vertexArray: Point[][] = [];
for (const polygon of polygonList) {
const vertices = [...polygon];
if (vertices[0].join(',') !== vertices[vertices.length - 1].join(',')) {
vertices.push([vertices[0][0], vertices[0][1]]);
}
if (vertices.length > 2) {
vertexArray.push(vertices);
}
}
const lines: Line[] = [];
gap = Math.max(gap, 0.1);
// Create sorted edges table
const edges: EdgeEntry[] = [];
for (const vertices of vertexArray) {
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: ActiveEdgeEntry[] = [];
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,31 @@
import type { OpSet, ResolvedOptions } from '../core.js';
import type { Line, Point } from '../geometry.js';
import { lineLength } from '../geometry.js';
import { HachureFiller } from './hachure-filler.js';
import { polygonHachureLines } from './scan-line-hachure.js';
export class ZigZagFiller extends HachureFiller {
override fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet {
let gap = o.hachureGap;
if (gap < 0) {
gap = o.strokeWidth * 4;
}
gap = Math.max(gap, 0.1);
const o2 = Object.assign({}, o, { hachureGap: gap });
const lines = polygonHachureLines(polygonList, o2);
const zigZagAngle = (Math.PI / 180) * o.hachureAngle;
const zigzagLines: Line[] = [];
const dgx = gap * 0.5 * Math.cos(zigZagAngle);
const dgy = gap * 0.5 * Math.sin(zigZagAngle);
for (const [p1, p2] of lines) {
if (lineLength([p1, p2])) {
zigzagLines.push(
[[p1[0] - dgx, p1[1] + dgy], [...p2]],
[[p1[0] + dgx, p1[1] - dgy], [...p2]]
);
}
}
const ops = this.renderLines(zigzagLines, o);
return { type: 'fillSketch', ops };
}
}
@@ -0,0 +1,64 @@
import type { Op, OpSet, ResolvedOptions } from '../core.js';
import type { Line, Point } from '../geometry.js';
import { lineLength } from '../geometry.js';
import type { PatternFiller, RenderHelper } from './filler-interface.js';
import { polygonHachureLines } from './scan-line-hachure.js';
export class ZigZagLineFiller implements PatternFiller {
private helper: RenderHelper;
constructor(helper: RenderHelper) {
this.helper = helper;
}
private zigzagLines(lines: Line[], zo: number, o: ResolvedOptions): Op[] {
const ops: Op[] = [];
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: Point = [
p1[0] + lstart * Math.cos(alpha),
p1[1] + lstart * Math.sin(alpha),
];
const end: Point = [
p1[0] + lend * Math.cos(alpha),
p1[1] + lend * Math.sin(alpha),
];
const middle: Point = [
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;
}
fillPolygons(polygonList: Point[][], o: ResolvedOptions): OpSet {
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(polygonList, o);
return { type: 'fillSketch', ops: this.zigzagLines(lines, zo, o) };
}
}
@@ -0,0 +1,340 @@
import { curveToBezier } from '../points-on-curve/curve-to-bezier.js';
import { pointsOnBezierCurves } from '../points-on-curve/index.js';
import { pointsOnPath } from '../points-on-path/index.js';
import type {
Config,
Drawable,
OpSet,
Options,
PathInfo,
ResolvedOptions,
} from './core.js';
import type { Point } from './geometry.js';
import { randomSeed } from './math.js';
import {
arc,
curve,
ellipseWithParams,
generateEllipseParams,
line,
linearPath,
patternFillArc,
patternFillPolygons,
rectangle,
solidFillPolygon,
svgPath,
} from './renderer.js';
const NOS = 'none';
export class RoughGenerator {
private config: Config;
defaultOptions: ResolvedOptions = {
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,
disableMultiStroke: false,
disableMultiStrokeFill: false,
preserveVertices: false,
};
constructor(config?: Config) {
this.config = config || {};
if (this.config.options) {
this.defaultOptions = this._o(this.config.options);
}
}
static newSeed(): number {
return randomSeed();
}
private _d(shape: string, sets: OpSet[], options: ResolvedOptions): Drawable {
return { shape, sets: sets || [], options: options || this.defaultOptions };
}
private _o(options?: Options): ResolvedOptions {
return options
? Object.assign({}, this.defaultOptions, options)
: this.defaultOptions;
}
private fillSketch(drawing: OpSet, o: ResolvedOptions): PathInfo {
let fweight = o.fillWeight;
if (fweight < 0) {
fweight = o.strokeWidth / 2;
}
return {
d: this.opsToPath(drawing),
stroke: o.fill || NOS,
strokeWidth: fweight,
fill: NOS,
};
}
arc(
x: number,
y: number,
width: number,
height: number,
start: number,
stop: number,
closed = false,
options?: Options
): Drawable {
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 fillOptions: ResolvedOptions = { ...o };
fillOptions.disableMultiStroke = true;
const shape = arc(
x,
y,
width,
height,
start,
stop,
true,
false,
fillOptions
);
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);
}
circle(x: number, y: number, diameter: number, options?: Options): Drawable {
const ret = this.ellipse(x, y, diameter, diameter, options);
ret.shape = 'circle';
return ret;
}
curve(points: Point[], options?: Options): Drawable {
const o = this._o(options);
const paths: OpSet[] = [];
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(patternFillPolygons([polyPoints], o));
}
}
if (o.stroke !== NOS) {
paths.push(outline);
}
return this._d('curve', paths, o);
}
ellipse(
x: number,
y: number,
width: number,
height: number,
options?: Options
): Drawable {
const o = this._o(options);
const paths: OpSet[] = [];
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(patternFillPolygons([ellipseResponse.estimatedPoints], o));
}
}
if (o.stroke !== NOS) {
paths.push(ellipseResponse.opset);
}
return this._d('ellipse', paths, o);
}
line(
x1: number,
y1: number,
x2: number,
y2: number,
options?: Options
): Drawable {
const o = this._o(options);
return this._d('line', [line(x1, y1, x2, y2, o)], o);
}
linearPath(points: Point[], options?: Options): Drawable {
const o = this._o(options);
return this._d('linearPath', [linearPath(points, false, o)], o);
}
opsToPath(drawing: OpSet, fixedDecimals?: number): string {
let path = '';
for (const item of drawing.ops) {
const data =
typeof fixedDecimals === 'number' && fixedDecimals >= 0
? item.data.map(d => +d.toFixed(fixedDecimals))
: 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();
}
path(d: string, options?: Options): Drawable {
const o = this._o(options);
const paths: OpSet[] = [];
if (!d) {
return this._d('path', paths, o);
}
d = (d || '')
.replace(/\n/g, ' ')
.replace(/(-\s)/g, '-')
.replace('/(ss)/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.fillStyle === 'solid') {
paths.push(solidFillPolygon(sets, o));
} else {
paths.push(patternFillPolygons(sets, 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);
}
polygon(points: Point[], options?: Options): Drawable {
const o = this._o(options);
const paths: OpSet[] = [];
const outline = linearPath(points, true, o);
if (o.fill) {
if (o.fillStyle === 'solid') {
paths.push(solidFillPolygon([points], o));
} else {
paths.push(patternFillPolygons([points], o));
}
}
if (o.stroke !== NOS) {
paths.push(outline);
}
return this._d('polygon', paths, o);
}
rectangle(
x: number,
y: number,
width: number,
height: number,
options?: Options
): Drawable {
const o = this._o(options);
const paths = [];
const outline = rectangle(x, y, width, height, o);
if (o.fill) {
const points: Point[] = [
[x, y],
[x + width, y],
[x + width, y + height],
[x, y + height],
];
if (o.fillStyle === 'solid') {
paths.push(solidFillPolygon([points], o));
} else {
paths.push(patternFillPolygons([points], o));
}
}
if (o.stroke !== NOS) {
paths.push(outline);
}
return this._d('rectangle', paths, o);
}
toPaths(drawable: Drawable): PathInfo[] {
const sets = drawable.sets || [];
const o = drawable.options || this.defaultOptions;
const paths: PathInfo[] = [];
for (const drawing of sets) {
let path: PathInfo | null = 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;
}
}
@@ -0,0 +1,43 @@
export type Point = [number, number];
export type Line = [Point, Point];
export interface Rectangle {
x: number;
y: number;
width: number;
height: number;
}
export function rotatePoints(
points: Point[],
center: Point,
degrees: number
): void {
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: Line[],
center: Point,
degrees: number
): void {
const points: Point[] = [];
lines.forEach(line => points.push(...line));
rotatePoints(points, center, degrees);
}
export function lineLength(line: Line): number {
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));
}
@@ -0,0 +1,21 @@
export function randomSeed(): number {
return Math.floor(Math.random() * 2 ** 31);
}
export class Random {
private seed: number;
constructor(seed: number) {
this.seed = seed;
}
next(): number {
if (this.seed) {
return (
((2 ** 31 - 1) & (this.seed = Math.imul(48271, this.seed))) / 2 ** 31
);
} else {
return Math.random();
}
}
}
@@ -0,0 +1,742 @@
import { absolutize } from '../path-data-parser/absolutize.js';
import { normalize } from '../path-data-parser/normalize.js';
import { parsePath } from '../path-data-parser/parser.js';
import type { Op, OpSet, ResolvedOptions } from './core.js';
import { getFiller } from './fillers/filler.js';
import type { RenderHelper } from './fillers/filler-interface.js';
import type { Point } from './geometry.js';
import { Random } from './math.js';
interface EllipseParams {
rx: number;
ry: number;
increment: number;
}
const helper: RenderHelper = {
randOffset,
randOffsetWithRange,
ellipse,
doubleLineOps: doubleLineFillOps,
};
export function line(
x1: number,
y1: number,
x2: number,
y2: number,
o: ResolvedOptions
): OpSet {
return { type: 'path', ops: _doubleLine(x1, y1, x2, y2, o) };
}
export function linearPath(
points: Point[],
close: boolean,
o: ResolvedOptions
): OpSet {
const len = (points || []).length;
if (len > 2) {
const ops: Op[] = [];
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: Point[], o: ResolvedOptions): OpSet {
return linearPath(points, true, o);
}
export function rectangle(
x: number,
y: number,
width: number,
height: number,
o: ResolvedOptions
): OpSet {
const points: Point[] = [
[x, y],
[x + width, y],
[x + width, y + height],
[x, y + height],
];
return polygon(points, o);
}
export function curve(points: Point[], o: ResolvedOptions): OpSet {
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 interface EllipseResult {
opset: OpSet;
estimatedPoints: Point[];
}
export function ellipse(
x: number,
y: number,
width: number,
height: number,
o: ResolvedOptions
): OpSet {
const params = generateEllipseParams(width, height, o);
return ellipseWithParams(x, y, o, params).opset;
}
export function generateEllipseParams(
width: number,
height: number,
o: ResolvedOptions
): EllipseParams {
const psq = Math.sqrt(
Math.PI *
2 *
Math.sqrt((Math.pow(width / 2, 2) + Math.pow(height / 2, 2)) / 2)
);
const stepCount = Math.ceil(
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: number,
y: number,
o: ResolvedOptions,
ellipseParams: EllipseParams
): EllipseResult {
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 && o.roughness !== 0) {
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: number,
y: number,
width: number,
height: number,
start: number,
stop: number,
closed: boolean,
roughClosure: boolean,
o: ResolvedOptions
): OpSet {
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: string, o: ResolvedOptions): OpSet {
const segments = normalize(absolutize(parsePath(path)));
const ops: Op[] = [];
let first: Point = [0, 0];
let current: Point = [0, 0];
for (const { key, data } of segments) {
switch (key) {
case 'M': {
const ro = 1 * (o.maxRandomnessOffset || 0);
const pv = o.preserveVertices;
ops.push({
op: 'move',
data: data.map(d => d + (pv ? 0 : _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(
polygonList: Point[][],
o: ResolvedOptions
): OpSet {
const ops: Op[] = [];
for (const points of polygonList) {
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 patternFillPolygons(
polygonList: Point[][],
o: ResolvedOptions
): OpSet {
return getFiller(o, helper).fillPolygons(polygonList, o);
}
export function patternFillArc(
x: number,
y: number,
width: number,
height: number,
start: number,
stop: number,
o: ResolvedOptions
): OpSet {
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: Point[] = [];
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 patternFillPolygons([points], o);
}
export function randOffset(x: number, o: ResolvedOptions): number {
return _offsetOpt(x, o);
}
export function randOffsetWithRange(
min: number,
max: number,
o: ResolvedOptions
): number {
return _offset(min, max, o);
}
export function doubleLineFillOps(
x1: number,
y1: number,
x2: number,
y2: number,
o: ResolvedOptions
): Op[] {
return _doubleLine(x1, y1, x2, y2, o, true);
}
// Private helpers
function cloneOptionsAlterSeed(ops: ResolvedOptions): ResolvedOptions {
const result: ResolvedOptions = { ...ops };
result.randomizer = undefined;
if (ops.seed) {
result.seed = ops.seed + 1;
}
return result;
}
function random(ops: ResolvedOptions): number {
if (!ops.randomizer) {
ops.randomizer = new Random(ops.seed || 0);
}
return ops.randomizer.next();
}
function _offset(
min: number,
max: number,
ops: ResolvedOptions,
roughnessGain = 1
): number {
return ops.roughness * roughnessGain * (random(ops) * (max - min) + min);
}
function _offsetOpt(
x: number,
ops: ResolvedOptions,
roughnessGain = 1
): number {
return _offset(-x, x, ops, roughnessGain);
}
function _doubleLine(
x1: number,
y1: number,
x2: number,
y2: number,
o: ResolvedOptions,
filling = false
): Op[] {
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: number,
y1: number,
x2: number,
y2: number,
o: ResolvedOptions,
move: boolean,
overlay: boolean
): Op[] {
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: Op[] = [];
const randomHalf = () => _offsetOpt(halfOffset, o, roughnessGain);
const randomFull = () => _offsetOpt(offset, o, roughnessGain);
const preserveVertices = o.preserveVertices;
if (move) {
if (overlay) {
ops.push({
op: 'move',
data: [
x1 + (preserveVertices ? 0 : randomHalf()),
y1 + (preserveVertices ? 0 : randomHalf()),
],
});
} else {
ops.push({
op: 'move',
data: [
x1 + (preserveVertices ? 0 : _offsetOpt(offset, o, roughnessGain)),
y1 + (preserveVertices ? 0 : _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 + (preserveVertices ? 0 : randomHalf()),
y2 + (preserveVertices ? 0 : 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 + (preserveVertices ? 0 : randomFull()),
y2 + (preserveVertices ? 0 : randomFull()),
],
});
}
return ops;
}
function _curveWithOffset(
points: Point[],
offset: number,
o: ResolvedOptions
): Op[] {
const ps: Point[] = [];
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: Point[],
closePoint: Point | null,
o: ResolvedOptions
): Op[] {
const len = points.length;
const ops: Op[] = [];
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: number,
cx: number,
cy: number,
rx: number,
ry: number,
offset: number,
overlap: number,
o: ResolvedOptions
): Point[][] {
const coreOnly = o.roughness === 0;
const corePoints: Point[] = [];
const allPoints: Point[] = [];
if (coreOnly) {
increment = increment / 4;
allPoints.push([
cx + rx * Math.cos(-increment),
cy + ry * Math.sin(-increment),
]);
for (let angle = 0; angle <= Math.PI * 2; angle = angle + increment) {
const p: Point = [cx + rx * Math.cos(angle), cy + ry * Math.sin(angle)];
corePoints.push(p);
allPoints.push(p);
}
allPoints.push([cx + rx * Math.cos(0), cy + ry * Math.sin(0)]);
allPoints.push([
cx + rx * Math.cos(increment),
cy + ry * Math.sin(increment),
]);
} else {
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),
]);
const endAngle = Math.PI * 2 + radOffset - 0.01;
for (let angle = radOffset; angle < endAngle; angle = angle + increment) {
const p: Point = [
_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: number,
cx: number,
cy: number,
rx: number,
ry: number,
strt: number,
stp: number,
offset: number,
o: ResolvedOptions
) {
const radOffset = strt + _offsetOpt(0.1, o);
const points: Point[] = [];
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: number,
y1: number,
x2: number,
y2: number,
x: number,
y: number,
current: Point,
o: ResolvedOptions
): Op[] {
const ops: Op[] = [];
const ros = [o.maxRandomnessOffset || 1, (o.maxRandomnessOffset || 1) + 0.3];
let f: Point = [0, 0];
const iterations = o.disableMultiStroke ? 1 : 2;
const preserveVertices = o.preserveVertices;
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] + (preserveVertices ? 0 : _offsetOpt(ros[0], o)),
current[1] + (preserveVertices ? 0 : _offsetOpt(ros[0], o)),
],
});
}
f = preserveVertices
? [x, y]
: [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,22 @@
import { RoughCanvas } from './canvas.js';
import type { Config } from './core.js';
import { RoughGenerator } from './generator.js';
import { RoughSVG } from './svg.js';
export default {
canvas(canvas: HTMLCanvasElement, config?: Config): RoughCanvas {
return new RoughCanvas(canvas, config);
},
svg(svg: SVGSVGElement, config?: Config): RoughSVG {
return new RoughSVG(svg, config);
},
generator(config?: Config): RoughGenerator {
return new RoughGenerator(config);
},
newSeed(): number {
return RoughGenerator.newSeed();
},
};
@@ -0,0 +1,182 @@
import type {
Config,
Drawable,
OpSet,
Options,
ResolvedOptions,
} from './core.js';
import { SVGNS } from './core.js';
import { RoughGenerator } from './generator.js';
import type { Point } from './geometry.js';
export class RoughSVG {
private gen: RoughGenerator;
private svg: SVGSVGElement;
get generator(): RoughGenerator {
return this.gen;
}
constructor(svg: SVGSVGElement, config?: Config) {
this.svg = svg;
this.gen = new RoughGenerator(config);
}
private fillSketch(
doc: Document,
drawing: OpSet,
o: ResolvedOptions
): SVGPathElement {
let fweight = o.fillWeight;
if (fweight < 0) {
fweight = o.strokeWidth / 2;
}
const path = doc.createElementNS(SVGNS, 'path');
path.setAttribute('d', this.opsToPath(drawing, o.fixedDecimalPlaceDigits));
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;
}
arc(
x: number,
y: number,
width: number,
height: number,
start: number,
stop: number,
closed = false,
options?: Options
): SVGGElement {
const d = this.gen.arc(x, y, width, height, start, stop, closed, options);
return this.draw(d);
}
circle(
x: number,
y: number,
diameter: number,
options?: Options
): SVGGElement {
const d = this.gen.circle(x, y, diameter, options);
return this.draw(d);
}
curve(points: Point[], options?: Options): SVGGElement {
const d = this.gen.curve(points, options);
return this.draw(d);
}
draw(drawable: Drawable): SVGGElement {
const sets = drawable.sets || [];
const o = drawable.options || this.getDefaultOptions();
const doc = this.svg.ownerDocument || window.document;
const g = doc.createElementNS(SVGNS, 'g');
const precision = drawable.options.fixedDecimalPlaceDigits;
for (const drawing of sets) {
let path = null;
switch (drawing.type) {
case 'path': {
path = doc.createElementNS(SVGNS, 'path');
path.setAttribute('d', this.opsToPath(drawing, precision));
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, precision));
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.append(path);
}
}
return g;
}
ellipse(
x: number,
y: number,
width: number,
height: number,
options?: Options
): SVGGElement {
const d = this.gen.ellipse(x, y, width, height, options);
return this.draw(d);
}
getDefaultOptions(): ResolvedOptions {
return this.gen.defaultOptions;
}
line(
x1: number,
y1: number,
x2: number,
y2: number,
options?: Options
): SVGGElement {
const d = this.gen.line(x1, y1, x2, y2, options);
return this.draw(d);
}
linearPath(points: Point[], options?: Options): SVGGElement {
const d = this.gen.linearPath(points, options);
return this.draw(d);
}
opsToPath(drawing: OpSet, fixedDecimalPlaceDigits?: number): string {
return this.gen.opsToPath(drawing, fixedDecimalPlaceDigits);
}
path(d: string, options?: Options): SVGGElement {
const drawing = this.gen.path(d, options);
return this.draw(drawing);
}
polygon(points: Point[], options?: Options): SVGGElement {
const d = this.gen.polygon(points, options);
return this.draw(d);
}
rectangle(
x: number,
y: number,
width: number,
height: number,
options?: Options
): SVGGElement {
const d = this.gen.rectangle(x, y, width, height, options);
return this.draw(d);
}
}