kopia lustrzana https://github.com/Tldraw/Tldraw
197 wiersze
5.6 KiB
TypeScript
197 wiersze
5.6 KiB
TypeScript
import { Vec, VecLike, assert, average, precise, toDomPrecision } from '@tldraw/editor'
|
|
import { getStrokeOutlineTracks } from './getStrokeOutlinePoints'
|
|
import { getStrokePoints } from './getStrokePoints'
|
|
import { setStrokePointRadii } from './setStrokePointRadii'
|
|
import { StrokeOptions, StrokePoint } from './types'
|
|
|
|
export function svgInk(rawInputPoints: VecLike[], options: StrokeOptions = {}) {
|
|
const { start = {}, end = {} } = options
|
|
const { cap: capStart = true } = start
|
|
const { cap: capEnd = true } = end
|
|
assert(!start.taper && !end.taper, 'cap taper not supported here')
|
|
assert(!start.easing && !end.easing, 'cap easing not supported here')
|
|
assert(capStart && capEnd, 'cap must be true')
|
|
|
|
const points = getStrokePoints(rawInputPoints, options)
|
|
setStrokePointRadii(points, options)
|
|
const partitions = partitionAtElbows(points)
|
|
let svg = ''
|
|
for (const partition of partitions) {
|
|
svg += renderPartition(partition, options)
|
|
}
|
|
|
|
return svg
|
|
}
|
|
|
|
function partitionAtElbows(points: StrokePoint[]): StrokePoint[][] {
|
|
if (points.length <= 2) return [points]
|
|
|
|
const result: StrokePoint[][] = []
|
|
let currentPartition: StrokePoint[] = [points[0]]
|
|
let prevV = Vec.Sub(points[1].point, points[0].point).uni()
|
|
let nextV: Vec
|
|
let dpr: number
|
|
let prevPoint: StrokePoint, thisPoint: StrokePoint, nextPoint: StrokePoint
|
|
for (let i = 1, n = points.length; i < n - 1; i++) {
|
|
prevPoint = points[i - 1]
|
|
thisPoint = points[i]
|
|
nextPoint = points[i + 1]
|
|
|
|
nextV = Vec.Sub(nextPoint.point, thisPoint.point).uni()
|
|
dpr = Vec.Dpr(prevV, nextV)
|
|
prevV = nextV
|
|
|
|
if (dpr < -0.8) {
|
|
// always treat such acute angles as elbows
|
|
// and use the extended .input point as the elbow point for swooshiness in fast zaggy lines
|
|
const elbowPoint = {
|
|
...thisPoint,
|
|
point: thisPoint.input,
|
|
}
|
|
currentPartition.push(elbowPoint)
|
|
result.push(cleanUpPartition(currentPartition))
|
|
currentPartition = [elbowPoint]
|
|
continue
|
|
}
|
|
currentPartition.push(thisPoint)
|
|
|
|
if (dpr > 0.7) {
|
|
// Not an elbow
|
|
continue
|
|
}
|
|
|
|
// so now we have a reasonably acute angle but it might not be an elbow if it's far
|
|
// away from it's neighbors, angular dist is a normalized representation of how far away the point is from it's neighbors
|
|
// (normalized by the radius)
|
|
if (
|
|
(Vec.Dist2(prevPoint.point, thisPoint.point) + Vec.Dist2(thisPoint.point, nextPoint.point)) /
|
|
((prevPoint.radius + thisPoint.radius + nextPoint.radius) / 3) ** 2 <
|
|
1.5
|
|
) {
|
|
// if this point is kinda close to its neighbors and it has a reasonably
|
|
// acute angle, it's probably a hard elbow
|
|
currentPartition.push(thisPoint)
|
|
result.push(cleanUpPartition(currentPartition))
|
|
currentPartition = [thisPoint]
|
|
continue
|
|
}
|
|
}
|
|
currentPartition.push(points[points.length - 1])
|
|
result.push(cleanUpPartition(currentPartition))
|
|
|
|
return result
|
|
}
|
|
|
|
function cleanUpPartition(partition: StrokePoint[]) {
|
|
// clean up start of partition (remove points that are too close to the start)
|
|
const startPoint = partition[0]
|
|
let nextPoint: StrokePoint
|
|
while (partition.length > 2) {
|
|
nextPoint = partition[1]
|
|
if (
|
|
Vec.Dist2(startPoint.point, nextPoint.point) <
|
|
(((startPoint.radius + nextPoint.radius) / 2) * 0.5) ** 2
|
|
) {
|
|
partition.splice(1, 1)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
// clean up end of partition in the same fashion
|
|
const endPoint = partition[partition.length - 1]
|
|
let prevPoint: StrokePoint
|
|
while (partition.length > 2) {
|
|
prevPoint = partition[partition.length - 2]
|
|
if (
|
|
Vec.Dist2(endPoint.point, prevPoint.point) <
|
|
(((endPoint.radius + prevPoint.radius) / 2) * 0.5) ** 2
|
|
) {
|
|
partition.splice(partition.length - 2, 1)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
// now readjust the cap point vectors to point to their nearest neighbors
|
|
if (partition.length > 1) {
|
|
partition[0] = {
|
|
...partition[0],
|
|
vector: Vec.Sub(partition[0].point, partition[1].point).uni(),
|
|
}
|
|
partition[partition.length - 1] = {
|
|
...partition[partition.length - 1],
|
|
vector: Vec.Sub(
|
|
partition[partition.length - 2].point,
|
|
partition[partition.length - 1].point
|
|
).uni(),
|
|
}
|
|
}
|
|
return partition
|
|
}
|
|
|
|
function circlePath(cx: number, cy: number, r: number) {
|
|
return (
|
|
'M ' +
|
|
cx +
|
|
' ' +
|
|
cy +
|
|
' m -' +
|
|
r +
|
|
', 0 a ' +
|
|
r +
|
|
',' +
|
|
r +
|
|
' 0 1,1 ' +
|
|
r * 2 +
|
|
',0 a ' +
|
|
r +
|
|
',' +
|
|
r +
|
|
' 0 1,1 -' +
|
|
r * 2 +
|
|
',0'
|
|
)
|
|
}
|
|
|
|
function renderPartition(strokePoints: StrokePoint[], options: StrokeOptions = {}): string {
|
|
if (strokePoints.length === 0) return ''
|
|
if (strokePoints.length === 1) {
|
|
return circlePath(strokePoints[0].point.x, strokePoints[0].point.y, strokePoints[0].radius)
|
|
}
|
|
|
|
const { left, right } = getStrokeOutlineTracks(strokePoints, options)
|
|
right.reverse()
|
|
let svg = `M${precise(left[0])}T`
|
|
|
|
// draw left track
|
|
for (let i = 1; i < left.length; i++) {
|
|
svg += average(left[i - 1], left[i])
|
|
}
|
|
// draw end cap arc
|
|
{
|
|
const point = strokePoints[strokePoints.length - 1]
|
|
const radius = point.radius
|
|
const direction = point.vector.clone().per().neg()
|
|
const arcStart = Vec.Add(point.point, Vec.Mul(direction, radius))
|
|
const arcEnd = Vec.Add(point.point, Vec.Mul(direction, -radius))
|
|
svg += `${precise(arcStart)}A${toDomPrecision(radius)},${toDomPrecision(
|
|
radius
|
|
)} 0 0 1 ${precise(arcEnd)}T`
|
|
}
|
|
// draw right track
|
|
for (let i = 1; i < right.length; i++) {
|
|
svg += average(right[i - 1], right[i])
|
|
}
|
|
// draw start cap arc
|
|
{
|
|
const point = strokePoints[0]
|
|
const radius = point.radius
|
|
const direction = point.vector.clone().per()
|
|
const arcStart = Vec.Add(point.point, Vec.Mul(direction, radius))
|
|
const arcEnd = Vec.Add(point.point, Vec.Mul(direction, -radius))
|
|
svg += `${precise(arcStart)}A${toDomPrecision(radius)},${toDomPrecision(
|
|
radius
|
|
)} 0 0 1 ${precise(arcEnd)}Z`
|
|
}
|
|
return svg
|
|
}
|