From 1f63516bf4bbdc8f2075b6737af43e077409f5a1 Mon Sep 17 00:00:00 2001 From: Preet Shihn Date: Mon, 6 Apr 2020 15:53:49 -0700 Subject: [PATCH] support filling of curves --- src/generator.ts | 24 ++++++++++++--- src/geometry.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++++-- src/renderer.ts | 18 +++++++++++ 3 files changed, 115 insertions(+), 7 deletions(-) diff --git a/src/generator.ts b/src/generator.ts index 88684d4..310589e 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -1,6 +1,6 @@ import { Config, DrawingSurface, Options, Drawable, OpSet, ResolvedOptions, PathInfo, PatternInfo, SVGNS } from './core'; -import { Point } from './geometry.js'; -import { line, solidFillPolygon, patternFillPolygon, rectangle, ellipseWithParams, generateEllipseParams, linearPath, arc, patternFillArc, curve, svgPath } from './renderer.js'; +import { Point, getPointsOnBezierCurves } from './geometry.js'; +import { line, solidFillPolygon, patternFillPolygon, rectangle, ellipseWithParams, generateEllipseParams, linearPath, arc, patternFillArc, curve, svgPath, curveAsBezierPoints } from './renderer.js'; import { randomSeed } from './math'; const hasSelf = typeof self !== 'undefined'; @@ -125,12 +125,28 @@ export class RoughGenerator { curve(points: Point[], options?: Options): Drawable { const o = this._options(options); - return this._drawable('curve', [curve(points, o)], o); + const paths: OpSet[] = []; + const outline = curve(points, o); + if (o.fill && o.fill !== NOS) { + const bezPoints = curveAsBezierPoints(points, o); + if (bezPoints.length >= 4) { + const polyPoints = getPointsOnBezierCurves(bezPoints, Math.min(50, 50 * o.roughness)); + if (o.fillStyle === 'solid') { + paths.push(solidFillPolygon(polyPoints, o)); + } else { + paths.push(patternFillPolygon(polyPoints, o)); + } + } + } + if (o.stroke !== NOS) { + paths.push(outline); + } + return this._drawable('curve', paths, o); } polygon(points: Point[], options?: Options): Drawable { const o = this._options(options); - const paths = []; + const paths: OpSet[] = []; const outline = linearPath(points, true, o); if (o.fill) { if (o.fillStyle === 'solid') { diff --git a/src/geometry.ts b/src/geometry.ts index e845f33..1cebd4e 100644 --- a/src/geometry.ts +++ b/src/geometry.ts @@ -1,6 +1,5 @@ -export declare type Point = [number, number]; - -export declare type Line = [Point, Point]; +export type Point = [number, number]; +export type Line = [Point, Point]; export interface Rectangle { x: number; @@ -33,4 +32,79 @@ 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)); +} + +export function getPointsOnBezierCurves(points: Point[], tolerance: number): Point[] { + const newPoints: Point[] = []; + const numSegments = (points.length - 1) / 3; + for (let i = 0; i < numSegments; i++) { + const offset = i * 3; + getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints); + } + return newPoints; +} + +function lerp(a: Point, b: Point, t: number): Point { + return [ + a[0] + (b[0] - a[0]) * t, + a[1] + (b[1] - a[1]) * t, + ]; +} + +function flatness(points: Point[], offset: number): number { + const p1 = points[offset + 0]; + const p2 = points[offset + 1]; + const p3 = points[offset + 2]; + const p4 = points[offset + 3]; + + let ux = 3 * p2[0] - 2 * p1[0] - p4[0]; ux *= ux; + let uy = 3 * p2[1] - 2 * p1[1] - p4[1]; uy *= uy; + let vx = 3 * p3[0] - 2 * p4[0] - p1[0]; vx *= vx; + let vy = 3 * p3[1] - 2 * p4[1] - p1[1]; vy *= vy; + + if (ux < vx) { + ux = vx; + } + + if (uy < vy) { + uy = vy; + } + + return ux + uy; +} + +function getPointsOnBezierCurveWithSplitting(points: Point[], offset: number, tolerance: number, newPoints: Point[]) { + const outPoints = newPoints || []; + if (flatness(points, offset) < tolerance) { + const p0 = points[offset + 0]; + if (outPoints.length) { + const d = lineLength([outPoints[outPoints.length - 1], p0]); + if (d > 1) { + outPoints.push(p0); + } + } else { + outPoints.push(p0); + } + outPoints.push(points[offset + 3]); + } else { + // subdivide + const t = .5; + const p1 = points[offset + 0]; + const p2 = points[offset + 1]; + const p3 = points[offset + 2]; + const p4 = points[offset + 3]; + + const q1 = lerp(p1, p2, t); + const q2 = lerp(p2, p3, t); + const q3 = lerp(p3, p4, t); + + const r1 = lerp(q1, q2, t); + const r2 = lerp(q2, q3, t); + + const red = lerp(r1, r2, t); + + getPointsOnBezierCurveWithSplitting([p1, q1, r1, red], 0, tolerance, outPoints); + getPointsOnBezierCurveWithSplitting([red, r2, q3, p4], 0, tolerance, outPoints); + } + return outPoints; } \ No newline at end of file diff --git a/src/renderer.ts b/src/renderer.ts index 0955b6e..155dfc5 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -56,6 +56,24 @@ export function curve(points: Point[], o: ResolvedOptions): OpSet { return { type: 'path', ops: o1.concat(o2) }; } +export function curveAsBezierPoints(points: Point[], o: ResolvedOptions): Point[] { + const ops = _curveWithOffset(points, 1 * (1 + o.roughness * 0.2), o); + const bez: Point[] = []; + ops.forEach((op) => { + switch (op.op) { + case 'move': + bez.push([op.data[0], op.data[1]]); + break; + case 'bcurveTo': + bez.push([op.data[0], op.data[1]]); + bez.push([op.data[2], op.data[3]]); + bez.push([op.data[4], op.data[5]]); + break; + } + }); + return bez; +} + export interface EllipseResult { opset: OpSet; estimatedPoints: Point[];