mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-03 02:20:19 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user