UNPKG

tldraw

Version:

A tiny little drawing editor.

8 lines (7 loc) • 10.3 kB
{ "version": 3, "sources": ["../../../../../src/lib/shapes/shared/freehand/svgInk.ts"], "sourcesContent": ["import { Vec, VecLike, assert, average, precise, toDomPrecision } from '@tldraw/editor'\nimport { getStrokeOutlineTracks } from './getStrokeOutlinePoints'\nimport { getStrokePoints } from './getStrokePoints'\nimport { setStrokePointRadii } from './setStrokePointRadii'\nimport { StrokeOptions, StrokePoint } from './types'\n\nexport function svgInk(rawInputPoints: VecLike[], options: StrokeOptions = {}) {\n\tconst { start = {}, end = {} } = options\n\tconst { cap: capStart = true } = start\n\tconst { cap: capEnd = true } = end\n\tassert(!start.taper && !end.taper, 'cap taper not supported here')\n\tassert(!start.easing && !end.easing, 'cap easing not supported here')\n\tassert(capStart && capEnd, 'cap must be true')\n\n\tconst points = getStrokePoints(rawInputPoints, options)\n\tsetStrokePointRadii(points, options)\n\tconst partitions = partitionAtElbows(points)\n\tlet svg = ''\n\tfor (const partition of partitions) {\n\t\tsvg += renderPartition(partition, options)\n\t}\n\n\treturn svg\n}\n\nfunction partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {\n\tif (points.length <= 2) return [points]\n\n\tconst result: StrokePoint[][] = []\n\tlet currentPartition: StrokePoint[] = [points[0]]\n\tlet prevV = Vec.Sub(points[1].point, points[0].point).uni()\n\tlet nextV: Vec\n\tlet dpr: number\n\tlet prevPoint: StrokePoint, thisPoint: StrokePoint, nextPoint: StrokePoint\n\tfor (let i = 1, n = points.length; i < n - 1; i++) {\n\t\tprevPoint = points[i - 1]\n\t\tthisPoint = points[i]\n\t\tnextPoint = points[i + 1]\n\n\t\tnextV = Vec.Sub(nextPoint.point, thisPoint.point).uni()\n\t\tdpr = Vec.Dpr(prevV, nextV)\n\t\tprevV = nextV\n\n\t\tif (dpr < -0.8) {\n\t\t\t// always treat such acute angles as elbows\n\t\t\t// and use the extended .input point as the elbow point for swooshiness in fast zaggy lines\n\t\t\tconst elbowPoint = {\n\t\t\t\t...thisPoint,\n\t\t\t\tpoint: thisPoint.input,\n\t\t\t}\n\t\t\tcurrentPartition.push(elbowPoint)\n\t\t\tresult.push(cleanUpPartition(currentPartition))\n\t\t\tcurrentPartition = [elbowPoint]\n\t\t\tcontinue\n\t\t}\n\t\tcurrentPartition.push(thisPoint)\n\n\t\tif (dpr > 0.7) {\n\t\t\t// Not an elbow\n\t\t\tcontinue\n\t\t}\n\n\t\t// so now we have a reasonably acute angle but it might not be an elbow if it's far\n\t\t// away from it's neighbors, angular dist is a normalized representation of how far away the point is from it's neighbors\n\t\t// (normalized by the radius)\n\t\tif (\n\t\t\t(Vec.Dist2(prevPoint.point, thisPoint.point) + Vec.Dist2(thisPoint.point, nextPoint.point)) /\n\t\t\t\t((prevPoint.radius + thisPoint.radius + nextPoint.radius) / 3) ** 2 <\n\t\t\t1.5\n\t\t) {\n\t\t\t// if this point is kinda close to its neighbors and it has a reasonably\n\t\t\t// acute angle, it's probably a hard elbow\n\t\t\tcurrentPartition.push(thisPoint)\n\t\t\tresult.push(cleanUpPartition(currentPartition))\n\t\t\tcurrentPartition = [thisPoint]\n\t\t\tcontinue\n\t\t}\n\t}\n\tcurrentPartition.push(points[points.length - 1])\n\tresult.push(cleanUpPartition(currentPartition))\n\n\treturn result\n}\n\nfunction cleanUpPartition(partition: StrokePoint[]) {\n\t// clean up start of partition (remove points that are too close to the start)\n\tconst startPoint = partition[0]\n\tlet nextPoint: StrokePoint\n\twhile (partition.length > 2) {\n\t\tnextPoint = partition[1]\n\t\tif (\n\t\t\tVec.Dist2(startPoint.point, nextPoint.point) <\n\t\t\t(((startPoint.radius + nextPoint.radius) / 2) * 0.5) ** 2\n\t\t) {\n\t\t\tpartition.splice(1, 1)\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\t// clean up end of partition in the same fashion\n\tconst endPoint = partition[partition.length - 1]\n\tlet prevPoint: StrokePoint\n\twhile (partition.length > 2) {\n\t\tprevPoint = partition[partition.length - 2]\n\t\tif (\n\t\t\tVec.Dist2(endPoint.point, prevPoint.point) <\n\t\t\t(((endPoint.radius + prevPoint.radius) / 2) * 0.5) ** 2\n\t\t) {\n\t\t\tpartition.splice(partition.length - 2, 1)\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\t// now readjust the cap point vectors to point to their nearest neighbors\n\tif (partition.length > 1) {\n\t\tpartition[0] = {\n\t\t\t...partition[0],\n\t\t\tvector: Vec.Sub(partition[0].point, partition[1].point).uni(),\n\t\t}\n\t\tpartition[partition.length - 1] = {\n\t\t\t...partition[partition.length - 1],\n\t\t\tvector: Vec.Sub(\n\t\t\t\tpartition[partition.length - 2].point,\n\t\t\t\tpartition[partition.length - 1].point\n\t\t\t).uni(),\n\t\t}\n\t}\n\treturn partition\n}\n\nfunction circlePath(cx: number, cy: number, r: number) {\n\treturn (\n\t\t'M ' +\n\t\tcx +\n\t\t' ' +\n\t\tcy +\n\t\t' m -' +\n\t\tr +\n\t\t', 0 a ' +\n\t\tr +\n\t\t',' +\n\t\tr +\n\t\t' 0 1,1 ' +\n\t\tr * 2 +\n\t\t',0 a ' +\n\t\tr +\n\t\t',' +\n\t\tr +\n\t\t' 0 1,1 -' +\n\t\tr * 2 +\n\t\t',0'\n\t)\n}\n\nfunction renderPartition(strokePoints: StrokePoint[], options: StrokeOptions = {}): string {\n\tif (strokePoints.length === 0) return ''\n\tif (strokePoints.length === 1) {\n\t\treturn circlePath(strokePoints[0].point.x, strokePoints[0].point.y, strokePoints[0].radius)\n\t}\n\n\tconst { left, right } = getStrokeOutlineTracks(strokePoints, options)\n\tright.reverse()\n\tlet svg = `M${precise(left[0])}T`\n\n\t// draw left track\n\tfor (let i = 1; i < left.length; i++) {\n\t\tsvg += average(left[i - 1], left[i])\n\t}\n\t// draw end cap arc\n\t{\n\t\tconst point = strokePoints[strokePoints.length - 1]\n\t\tconst radius = point.radius\n\t\tconst direction = point.vector.clone().per().neg()\n\t\tconst arcStart = Vec.Add(point.point, Vec.Mul(direction, radius))\n\t\tconst arcEnd = Vec.Add(point.point, Vec.Mul(direction, -radius))\n\t\tsvg += `${precise(arcStart)}A${toDomPrecision(radius)},${toDomPrecision(\n\t\t\tradius\n\t\t)} 0 0 1 ${precise(arcEnd)}T`\n\t}\n\t// draw right track\n\tfor (let i = 1; i < right.length; i++) {\n\t\tsvg += average(right[i - 1], right[i])\n\t}\n\t// draw start cap arc\n\t{\n\t\tconst point = strokePoints[0]\n\t\tconst radius = point.radius\n\t\tconst direction = point.vector.clone().per()\n\t\tconst arcStart = Vec.Add(point.point, Vec.Mul(direction, radius))\n\t\tconst arcEnd = Vec.Add(point.point, Vec.Mul(direction, -radius))\n\t\tsvg += `${precise(arcStart)}A${toDomPrecision(radius)},${toDomPrecision(\n\t\t\tradius\n\t\t)} 0 0 1 ${precise(arcEnd)}Z`\n\t}\n\treturn svg\n}\n"], "mappings": "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", "names": [] }