import { isNumberInsideRange } from './helperFunctions' import { getPointOnCurve } from './getPointOnCurve' import { calculateCurveLength, getBoundsOfNemiCurves, getTweenBetweenCurves, makePartialCurve, NemiCurve, scaleNemiCurve, translateNemiCurve, } from './nemiCurve' import { NemiShape, scaleNemiShape } from './nemiShapes' import { Position, SuperPath2D } from './SuperPath2D' type DaumiElement = { innerCurve: NemiCurve outerCurve: NemiCurve length: number startsAtPercentage: number endsAtPercentage: number percentage: number connectsToPrevious: boolean } // daumiShape is a nemishape that has more calculations done on it export type DaumiShape = { elements: DaumiElement[] totalLengthOuter: number totalLengthInner: number } export const nemiToDaumiShape = ( _nemiShape: NemiShape, { width, height }: { width: number; height: number }, ): DaumiShape => { const bounds = getBoundsOfNemiCurves(_nemiShape.outer.elements) const nemiShape = scaleNemiShape(_nemiShape, Math.min(width / bounds.width, height / bounds.height) * 0.8) const nemiShapeInner = nemiShape.inner const nemiShapeOuter = nemiShape.outer let totalLengthOuter = 0 let totalLengthInner = 0 const daumiShapeElements: Omit< DaumiElement, 'percentage' | 'startsAtPercentage' | 'endsAtPercentage' | 'connectsToPrevious' >[] = nemiShapeInner.elements.map((innerElement, index) => { const outerElement = nemiShapeOuter.elements[index] const outerLength = calculateCurveLength(outerElement) const innerLength = calculateCurveLength(innerElement) totalLengthInner += innerLength totalLengthOuter += outerLength return { innerCurve: innerElement, outerCurve: outerElement, length: outerLength, } }) let currentPercentage = 0 const daumiShapeElementsWithPercentage: Omit[] = daumiShapeElements.map( (element, index) => { const percentage = element.length / totalLengthOuter const isLast = index === daumiShapeElements.length - 1 const endsAtPercentage = isLast ? 1 : currentPercentage + percentage const daumiElement = { ...element, percentage, startsAtPercentage: currentPercentage, endsAtPercentage: endsAtPercentage, } currentPercentage += percentage return daumiElement }, ) const daumiShapeElementsWithConnectsToPrevious: DaumiElement[] = daumiShapeElementsWithPercentage.map( (element, index) => { const previousIndex = index === 0 ? daumiShapeElementsWithPercentage.length - 1 : index - 1 const previousElement = daumiShapeElementsWithPercentage[previousIndex] const endPointOfPrev = getPointOnCurve(previousElement.outerCurve, 1) const startPointOfCurrent = getPointOnCurve(element.outerCurve, 0) const connectsToPrevious = endPointOfPrev.x === startPointOfCurrent.x && endPointOfPrev.y === startPointOfCurrent.y return { ...element, connectsToPrevious, } }, ) const daumiShape = { elements: daumiShapeElementsWithConnectsToPrevious, totalLengthOuter, totalLengthInner, } return centerDaumiShape(daumiShape, { width, height, }) } export const getDaumiElementLocation = (daumiShape: DaumiShape, _percentage: number) => { let percentage = _percentage if (percentage > 1) { percentage = _percentage % 1 } for (const element of daumiShape.elements) { if (isNumberInsideRange(percentage, element.startsAtPercentage, element.endsAtPercentage)) { const percentageOnElement = (percentage - element.startsAtPercentage) / element.percentage return { element, percentageOnElement, } } } throw new Error(`getDaumiElement: percentage out of bounds. percentage: ${percentage}`) } type DaumiInstruction = { startPercentage: number endPercentage: number element: DaumiElement } export const getDaumiInstructions = ( daumiShape: DaumiShape, { startPercentage, endPercentage, reverse, }: { startPercentage: number endPercentage: number reverse: boolean }, ): DaumiInstruction[] => { const startElementLocation = getDaumiElementLocation(daumiShape, startPercentage) const endElementLocation = getDaumiElementLocation(daumiShape, endPercentage) let elements = reverse ? [...daumiShape.elements].reverse() : daumiShape.elements if (reverse) { const startIndex = elements.indexOf(startElementLocation.element) const endIndex = elements.indexOf(endElementLocation.element) if (startIndex == -1 || endIndex == -1) { throw new Error('getDaumiElements: start or end element not found') } const isOneElementAndStartIsBeforeEnd = startElementLocation.element === endElementLocation.element && endPercentage > startPercentage if (startIndex > endIndex || isOneElementAndStartIsBeforeEnd) { elements = elements.slice(startIndex).concat(elements.slice(0, endIndex + 1)) } else { elements = elements.slice(startIndex, endIndex + 1) } } else { const startIndex = elements.indexOf(startElementLocation.element) const endIndex = elements.indexOf(endElementLocation.element) if (startIndex == -1 || endIndex == -1) { throw new Error('getDaumiElements: start or end element not found') } const isOneElementAndEndIsBeforeStart = startElementLocation.element === endElementLocation.element && endPercentage < startPercentage if (startIndex > endIndex || isOneElementAndEndIsBeforeStart) { elements = elements.slice(startIndex).concat(elements.slice(0, endIndex + 1)) } else { elements = elements.slice(startIndex, endIndex + 1) } } return elements.map((element, index) => { const drawUntil = reverse ? 0 : 1 const drawFrom = reverse ? 1 : 0 return { startPercentage: index === 0 ? startElementLocation.percentageOnElement : drawFrom, endPercentage: index === elements.length - 1 ? endElementLocation.percentageOnElement : drawUntil, element, } }) } export const getPointOnDaumiShape = (daumiShape: DaumiShape, percentage: number): Position => { const { element, percentageOnElement } = getDaumiElementLocation(daumiShape, percentage) return getPointOnCurve(element.innerCurve, percentageOnElement) } export const makeDaumiCurve = ( daumiShape: DaumiShape, path: SuperPath2D, { startPercentage, endPercentage, lanePercentage, reverse = false, }: { startPercentage: number endPercentage: number lanePercentage: number reverse?: boolean }, ) => { const daumiInstructions = getDaumiInstructions(daumiShape, { startPercentage, endPercentage, reverse }) for (const daumiInstruction of daumiInstructions) { const { startPercentage, endPercentage, element } = daumiInstruction const curve = getTweenBetweenCurves(element.innerCurve, element.outerCurve, lanePercentage) if (!element.connectsToPrevious) { path.moveTo(getPointOnCurve(curve, 0)) } makePartialCurve(curve, path, { startPercentage: startPercentage, endPercentage: endPercentage, }) } return path } export const translateDaumiShape = (daumiShape: DaumiShape, { dx, dy }: { dx: number; dy: number }) => { return { ...daumiShape, elements: daumiShape.elements.map((element) => { return { ...element, innerCurve: translateNemiCurve(element.innerCurve, { dx, dy }), outerCurve: translateNemiCurve(element.outerCurve, { dx, dy }), } }), } } export const centerDaumiShape = (daumiShape: DaumiShape, { width, height }: { width: number; height: number }) => { const allInnerCurves = daumiShape.elements.map((element) => element.innerCurve) const allOuterCurves = daumiShape.elements.map((element) => element.outerCurve) const bounds = getBoundsOfNemiCurves([...allInnerCurves, ...allOuterCurves]) const centerX = width / 2 const centerY = height / 2 return translateDaumiShape(daumiShape, { dx: centerX - bounds.xCenter, dy: centerY - bounds.yCenter, }) } export const scaleDaumiShape = (daumiShape: DaumiShape, scale: number): DaumiShape => { return { ...daumiShape, elements: daumiShape.elements.map((element) => { return { ...element, innerCurve: scaleNemiCurve(element.innerCurve, scale), outerCurve: scaleNemiCurve(element.outerCurve, scale), } }), } } export const lengthToPercentage = (daumiShape: DaumiShape, length: number, laneRadius: number) => { const differenceBetweenInnerAndOuter = daumiShape.totalLengthOuter - daumiShape.totalLengthInner const relevantLength = daumiShape.totalLengthInner + differenceBetweenInnerAndOuter * laneRadius return length / relevantLength } export const percentageToLength = ( daumiShape: DaumiShape, startPercentage: number, endPercentage: number, laneRadius: number, ) => { const percentage = endPercentage < startPercentage ? endPercentage + 1 - startPercentage : endPercentage - startPercentage const differenceBetweenInnerAndOuter = daumiShape.totalLengthOuter - daumiShape.totalLengthInner const relevantLength = daumiShape.totalLengthInner + differenceBetweenInnerAndOuter * laneRadius return percentage * relevantLength }