From e93a497cd86587e92102af00996ab356bf49872a Mon Sep 17 00:00:00 2001 From: casperlamboo Date: Wed, 8 Nov 2017 22:01:31 +0100 Subject: [PATCH] add curve utils --- src/shape/shapeToPoints.js | 1 + src/utils/curveUtils.js | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/utils/curveUtils.js diff --git a/src/shape/shapeToPoints.js b/src/shape/shapeToPoints.js index 4e0edca..e2863b0 100644 --- a/src/shape/shapeToPoints.js +++ b/src/shape/shapeToPoints.js @@ -7,6 +7,7 @@ import { CLIPPER_PRECISION } from '../constants/d2Constants.js'; import { MAX_ANGLE } from '../constants/d3Constants.js'; import { SHAPE_CACHE_LIMIT } from '../constants/general.js'; import { createText } from '../utils/textUtils.js'; +import { segmentBezierPath } from '../utils/curveUtils.js'; const HEART_BEZIER_PATH = [ new Vector(0.0, -0.5), diff --git a/src/utils/curveUtils.js b/src/utils/curveUtils.js new file mode 100644 index 0000000..47bebe1 --- /dev/null +++ b/src/utils/curveUtils.js @@ -0,0 +1,70 @@ +import fitCurve from 'fit-curve'; +import Bezier from 'bezier-js'; +import { Vector } from 'cal'; + +const DEFAULT_CURVE_ERROR = 3.0; +const DEFAULT_DISTANCE_THRESHOLD = 1.0; + +export function fitPath(path, curveError = DEFAULT_CURVE_ERROR) { + if (!path || path.length === 0) throw new Error('Path contains no points in fitPath'); + const first = path[0].clone(); // store first point so it can be used in reduce function + path = path.map(({ x, y }) => [x, y]); // convert path to [...[x, y]] format + return fitCurve(path, curveError) + .reduce((bezierPath, [ // convert to [...Vector] format. n+0 is path point, n+1 and n+2 are control points + start, + [controlPoint1X, controlPoint1Y], + [controlPoint2X, controlPoint2Y], + [endX, endY] + ]) => { + bezierPath.push( + new Vector(controlPoint1X, controlPoint1Y), + new Vector(controlPoint2X, controlPoint2Y), + new Vector(endX, endY) + ); + + return bezierPath; + }, [first]); // always add first point +} + +export function segmentBezierPath(bezierPath, distanceThreshold = DEFAULT_DISTANCE_THRESHOLD) { + const path = []; + for (let i = 0; i < bezierPath.length - 3; i += 3) { + const bezierCurve = new Bezier( + bezierPath[i].x, bezierPath[i].y, + bezierPath[i + 1].x, bezierPath[i + 1].y, + bezierPath[i + 2].x, bezierPath[i + 2].y, + bezierPath[i + 3].x, bezierPath[i + 3].y + ); + path.push(...recursiveBezierSegmenter(distanceThreshold, bezierCurve)); + } + path.push(new Vector().copy(bezierPath[bezierPath.length - 1])); + return path; +} + +function recursiveBezierSegmenter( + distanceThreshold, bezierCurve, + tStart = 0.0, tEnd = 1.0, + pStart = new Vector().copy(bezierCurve.get(tStart)), pEnd = new Vector().copy(bezierCurve.get(tEnd)) +) { + const tCenter = (tStart + tEnd) * 0.5; + const pCenter = new Vector().copy(bezierCurve.get(tCenter)); + + const distance = Math.abs(pEnd.subtract(pStart).normal().normalize().dot(pCenter.subtract(pStart))); + + if (distance > distanceThreshold) { + const segmentStart = recursiveBezierSegmenter( + distanceThreshold, bezierCurve, + tStart, tCenter, + pStart, pCenter + ); + const segmentEnd = recursiveBezierSegmenter( + distanceThreshold, bezierCurve, + tCenter, tEnd, + pCenter, pEnd + ); + + return segmentStart.concat(segmentEnd); + } else { + return [pStart]; + } +}