import { Bezier } from 'bezier-js' import { getAdjustedT, reverseBezier } from './bezier' import { getPointOnCurve } from './getPointOnCurve' import { Position, SuperPath2D } from './SuperPath2D' import { clone } from 'lodash' export type NemiCurveArc = { type: 'arc' radians: number centerPoint: Position radius: number startAngle: number } export type NemiCurveBezier = { type: 'bezier' startPoint: Position controlPoint1: Position controlPoint2: Position endPoint: Position } type NemiCurveLine = { type: 'line' startPoint: Position endPoint: Position } export type NemiCurve = NemiCurveLine | NemiCurveArc | NemiCurveBezier export function calculateCurveLength(curve: NemiCurve): number { switch (curve.type) { case 'line': { return Math.sqrt( Math.pow(curve.endPoint.x - curve.startPoint.x, 2) + Math.pow(curve.endPoint.y - curve.startPoint.y, 2), ) } case 'arc': { return curve.radians * curve.radius } case 'bezier': { const bezier = new Bezier(curve.startPoint, curve.controlPoint1, curve.controlPoint2, curve.endPoint) return bezier.length() } } } export const makePartialCurve = ( curve: NemiCurve, path: SuperPath2D, { startPercentage, endPercentage, }: { startPercentage: number endPercentage: number }, ) => { const isReversed = startPercentage > endPercentage switch (curve.type) { case 'line': { const startPoint = getPointOnCurve(curve, startPercentage) const endPoint = getPointOnCurve(curve, endPercentage) path.lineTo(startPoint) path.lineTo(endPoint) break } case 'arc': { const startAngle = curve.startAngle + startPercentage * curve.radians const endAngle = curve.startAngle + endPercentage * curve.radians path.arc(curve.centerPoint, curve.radius, startAngle, endAngle, isReversed) break } case 'bezier': { const bezier = new Bezier(curve.startPoint, curve.controlPoint1, curve.controlPoint2, curve.endPoint) let tStart = getAdjustedT(bezier, startPercentage) let tEnd = getAdjustedT(bezier, endPercentage) let partialBezier = bezier.split(tStart, tEnd) as Bezier | { right: Bezier; left: Bezier } if (tStart === 1) { // see test case for this edge case partialBezier = reverseBezier(bezier.split(tEnd).right) } if (!(partialBezier instanceof Bezier)) { partialBezier = reverseBezier(partialBezier.left) } const length = partialBezier.length() if (isNaN(length)) { console.warn('partialBezier.length() is NaN', { partialBezier, tStart, tEnd, bezier, startPercentage, endPercentage, points: { startPoint: curve.startPoint, controlPoint1: curve.controlPoint1, controlPoint2: curve.controlPoint2, endPoint: curve.endPoint, }, }) } const startPoint = partialBezier.points[0] path.lineTo(startPoint) path.bezierCurveTo(partialBezier.points[1], partialBezier.points[2], partialBezier.points[3]) break } } } export function translateNemiCurve(curve: NemiCurve, { dx = 0, dy = 0 }) { switch (curve.type) { case 'line': return { ...curve, startPoint: { x: curve.startPoint.x + dx, y: curve.startPoint.y + dy }, endPoint: { x: curve.endPoint.x + dx, y: curve.endPoint.y + dy }, } case 'arc': return { ...curve, centerPoint: { x: curve.centerPoint.x + dx, y: curve.centerPoint.y + dy }, } case 'bezier': return { ...curve, startPoint: { x: curve.startPoint.x + dx, y: curve.startPoint.y + dy }, controlPoint1: { x: curve.controlPoint1.x + dx, y: curve.controlPoint1.y + dy }, controlPoint2: { x: curve.controlPoint2.x + dx, y: curve.controlPoint2.y + dy }, endPoint: { x: curve.endPoint.x + dx, y: curve.endPoint.y + dy }, } } } export const getBoundsOfNemiCurves = (nemiCurves: NemiCurve[]) => { let xMin = Infinity let xMax = -Infinity let yMin = Infinity let yMax = -Infinity for (const curve of nemiCurves) { switch (curve.type) { case 'arc': { const { centerPoint, radius, radians, startAngle } = curve let endAngle = startAngle + radians if (endAngle > Math.PI * 2) { endAngle -= Math.PI * 2 } xMin = Math.min(xMin, centerPoint.x - radius) xMax = Math.max(xMax, centerPoint.x + radius) yMin = Math.min(yMin, centerPoint.y - radius) yMax = Math.max(yMax, centerPoint.y + radius) break } case 'line': { const { startPoint, endPoint } = curve xMin = Math.min(xMin, startPoint.x, endPoint.x) xMax = Math.max(xMax, startPoint.x, endPoint.x) yMin = Math.min(yMin, startPoint.y, endPoint.y) yMax = Math.max(yMax, startPoint.y, endPoint.y) break } case 'bezier': { const bezier = new Bezier(curve.startPoint, curve.controlPoint1, curve.controlPoint2, curve.endPoint) const bounds = bezier.bbox() xMin = Math.min(xMin, bounds.x.min) xMax = Math.max(xMax, bounds.x.max) yMin = Math.min(yMin, bounds.y.min) yMax = Math.max(yMax, bounds.y.max) break } } } const width = xMax - xMin const height = yMax - yMin return { xMin, xMax, yMin, yMax, xCenter: xMin + width / 2, yCenter: yMin + height / 2, width, height, } } export const getTweenBetweenCurves = (curveA: NemiCurve, curveB: NemiCurve, t: number) => { switch (curveA.type) { case 'line': { if (curveB.type !== 'line') { throw new Error('Cannot tween between different curve types') } return { ...curveA, startPoint: { x: curveA.startPoint.x + t * (curveB.startPoint.x - curveA.startPoint.x), y: curveA.startPoint.y + t * (curveB.startPoint.y - curveA.startPoint.y), }, endPoint: { x: curveA.endPoint.x + t * (curveB.endPoint.x - curveA.endPoint.x), y: curveA.endPoint.y + t * (curveB.endPoint.y - curveA.endPoint.y), }, } } case 'arc': { if (curveB.type !== 'arc') { throw new Error('Cannot tween between different curve types') } return { ...curveA, centerPoint: { x: curveA.centerPoint.x + t * (curveB.centerPoint.x - curveA.centerPoint.x), y: curveA.centerPoint.y + t * (curveB.centerPoint.y - curveA.centerPoint.y), }, radius: curveA.radius + t * (curveB.radius - curveA.radius), startAngle: curveA.startAngle + t * (curveB.startAngle - curveA.startAngle), radians: curveA.radians + t * (curveB.radians - curveA.radians), } } case 'bezier': { if (curveB.type !== 'bezier') { throw new Error('Cannot tween between different curve types') } return { ...curveA, startPoint: { x: curveA.startPoint.x + t * (curveB.startPoint.x - curveA.startPoint.x), y: curveA.startPoint.y + t * (curveB.startPoint.y - curveA.startPoint.y), }, controlPoint1: { x: curveA.controlPoint1.x + t * (curveB.controlPoint1.x - curveA.controlPoint1.x), y: curveA.controlPoint1.y + t * (curveB.controlPoint1.y - curveA.controlPoint1.y), }, controlPoint2: { x: curveA.controlPoint2.x + t * (curveB.controlPoint2.x - curveA.controlPoint2.x), y: curveA.controlPoint2.y + t * (curveB.controlPoint2.y - curveA.controlPoint2.y), }, endPoint: { x: curveA.endPoint.x + t * (curveB.endPoint.x - curveA.endPoint.x), y: curveA.endPoint.y + t * (curveB.endPoint.y - curveA.endPoint.y), }, } } } } export const scaleNemiCurve = (curve: NemiCurve, scale: number) => { const scaleX = scale const scaleY = scale switch (curve.type) { case 'line': return { ...curve, startPoint: { x: curve.startPoint.x * scaleX, y: curve.startPoint.y * scaleY }, endPoint: { x: curve.endPoint.x * scaleX, y: curve.endPoint.y * scaleY }, } case 'arc': return { ...curve, centerPoint: { x: curve.centerPoint.x * scaleX, y: curve.centerPoint.y * scaleY }, radius: curve.radius * Math.max(scaleX, scaleY), } case 'bezier': return { ...curve, startPoint: { x: curve.startPoint.x * scaleX, y: curve.startPoint.y * scaleY }, controlPoint1: { x: curve.controlPoint1.x * scaleX, y: curve.controlPoint1.y * scaleY }, controlPoint2: { x: curve.controlPoint2.x * scaleX, y: curve.controlPoint2.y * scaleY }, endPoint: { x: curve.endPoint.x * scaleX, y: curve.endPoint.y * scaleY }, } } }