UNPKG

prototypo-library

Version:

This is prototypo parametric font library

758 lines (670 loc) 16.8 kB
import {add2D, mulScalar2D, subtract2D} from '../utils/linear'; const infinityPointScale = 5000000; const epsilon = 0.000001; export function approximately(a, b, precision = epsilon) { return Math.abs(a - b) <= precision; } // The following function should be useless, thanks to paper export function lineLineIntersection(p1, p2, p3, p4) { const x1 = p1.x; const y1 = p1.y; const x2 = p2.x; const y2 = p2.y; const x3 = p3.x; const y3 = p3.y; const x4 = p4.x; const y4 = p4.y; const d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); if (d === 0) { return p1; } return { x: ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d, y: ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d, }; } // Find the intersection of two rays. // A ray is defined by a point and an angle. export function rayRayIntersection(p1, a1, p2, a2) { // line equations const a = Math.tan(a1); const b = Math.tan(a2); const c = p1.y - a * p1.x; const d = p2.y - b * p2.x; let x; let y; // When searching for lines intersection, // angles can be normalized to 0 < a < PI // This will be helpful in detecting special cases below. /* eslint-disable no-param-reassign */ a1 %= Math.PI; if (a1 < 0) { a1 += Math.PI; } a2 %= Math.PI; if (a2 < 0) { a2 += Math.PI; } // no intersection if (a1 === a2) { return { x: p1.x + infinityPointScale * Math.cos(a1), y: p1.y + infinityPointScale * Math.sin(a1), }; } // We want to round a1, a2 and PI to avoid problems with approximation a1 = a1.toFixed(6); a2 = a2.toFixed(6); /* eslint-enable no-param-reassign */ const piOver2 = (Math.PI / 2).toFixed(6); // Optimize frequent and easy special cases. // Without optimization, results would be incorrect when cos(a) === 0 if (a1 === 0) { y = p1.y; } else if (a1 === piOver2) { x = p1.x; } if (a2 === 0) { y = p2.y; } else if (a2 === piOver2) { x = p2.x; } // easiest case if (x !== undefined && y !== undefined) { return {x, y}; } // other cases that can be optimized if (a1 === 0) { return {x: (y - d) / b, y}; } if (a1 === piOver2) { return {x, y: b * x + d}; } if (a2 === 0) { return {x: (y - c) / a, y}; } if (a2 === piOver2) { return {x, y: a * x + c}; } // intersection from two line equations // algo: http://en.wikipedia.org/wiki/Line–line_intersection#Given_the_equations_of_the_lines const newX = (d - c) / (a - b); return { x: newX, // this should work equally well with ax+c or bx+d y: a * newX + c, }; } // return the angle between two points export function lineAngle(p0, p1) { return Math.atan2(p1.y - p0.y, p1.x - p0.x) % (2 * Math.PI); } export function onLine(params) { if (params.on[0].x === params.on[1].x && params.on[0].y === params.on[1].y) { return 'x' in params ? params.on[0].y : params.on[0].x; } const origin = params.on[0]; const vector = [ params.on[1].x - params.on[0].x, params.on[1].y - params.on[0].y, ]; return 'x' in params ? (params.x - origin.x) / vector[0] * vector[1] + origin.y : (params.y - origin.y) / vector[1] * vector[0] + origin.x; } export function pointOnCurve( pointHandleOut, handleOut, pointHandleIn, handleIn, distanceFromOut, inverseOrientation, linePrecision = 3, ) { let length = 0; let previousPoint; let points; if (inverseOrientation) { points = [pointHandleIn, handleIn, handleOut, pointHandleOut]; } else { points = [pointHandleOut, handleOut, handleIn, pointHandleIn]; } for (let i = 0; i < linePrecision; i++) { const point = getPointOnCurve(points, i / (linePrecision - 1)); if (previousPoint) { length += distance(previousPoint.x, previousPoint.y, point.x, point.y); } previousPoint = point; } let t = length === 0 ? 0 : distanceFromOut / length; t = Math.max(0.001, Math.min(1, t)); return getPointOnCurve(points, t); } export function getPointOnCurve(points, t) { const inverseT = 1 - t; const a = inverseT * inverseT * inverseT; const b = inverseT * inverseT * t * 3; const c = inverseT * t * t * 3; const d = t * t * t; return { x: a * points[0].x + b * points[1].x + c * points[2].x + d * points[3].x, y: a * points[0].y + b * points[1].y + c * points[2].y + d * points[3].y, normal: lineAngle( { x: 0, y: 0, }, { x: (points[1].x - points[0].x) * inverseT * inverseT + 2 * (points[2].x - points[1].x) * t * inverseT + (points[3].x - points[2].x) * t * t, y: (points[1].y - points[0].y) * inverseT * inverseT + 2 * (points[2].y - points[1].y) * t * inverseT + (points[3].y - points[2].y) * t * t, }, ), }; } export function split(points, t = 1, base) { let result = points; let current = points; while (current.length > 1) { const newPoints = []; for (let i = 1; i < current.length; i++) { newPoints.push( add2D(mulScalar2D(1 - t, current[i - 1]), mulScalar2D(t, current[i])), ); } result = result.concat(newPoints); current = newPoints; } if (t === 1) { return { left: [base[1], base[0]], right: [base[1], base[1]], }; } const splitBezier = { left: [ { x: result[0].x, y: result[0].y, handleOut: { x: result[4].x, y: result[4].y, }, }, { x: result[9].x, y: result[9].y, handleIn: { x: result[7].x, y: result[7].y, }, handleOut: { x: result[8].x, y: result[8].y, }, }, ], right: [ { x: result[9].x, y: result[9].y, handleIn: { x: result[7].x, y: result[7].y, }, handleOut: { x: result[8].x, y: result[8].y, }, }, { x: result[3].x, y: result[3].y, handleIn: { x: result[6].x, y: result[6].y, }, }, ], }; return splitBezier; } export function distance(x1, y1, x2, y2) { return Math.sqrt((x2 - x1) ** 2 + (y1 - y2) ** 2); } export function align(points, lineStart, lineEnd) { const tx = lineStart.x; const ty = lineStart.y; const a = -Math.atan2(lineEnd.y - ty, lineEnd.x - tx); return points.map(v => ({ x: (v.x - tx) * Math.cos(a) - (v.y - ty) * Math.sin(a), y: (v.x - tx) * Math.sin(a) + (v.y - ty) * Math.cos(a), })); } function crt(v) { return v < 0 ? -((-v) ** (1 / 3)) : v ** (1 / 3); } export function getIntersectionTValue( pointHandleOut, handleOut, pointHandleIn, handleIn, lineStart = {x: 0, y: 0}, lineEnd = {x: 1, y: 0}, points = [pointHandleOut, handleOut, handleIn, pointHandleIn], ) { const p = align(points, lineStart, lineEnd); function reduce(t) { return t >= 0 && t <= 1; } // eslint-disable-line yoda // see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm const pa = p[0].y; const pb = p[1].y; const pc = p[2].y; const pd = p[3].y; const d = -pa + 3 * pb - 3 * pc + pd; let a = 3 * pa - 6 * pb + 3 * pc; let b = -3 * pa + 3 * pb; let c = pa; let u1; let v1; let x1; let x2; let x3; if (approximately(d, 0)) { // this is not a cubic curve. if (approximately(a, 0)) { // in fact, this is not a quadratic curve either. if (approximately(b, 0)) { // in fact in fact, there are no solutions. return []; } // linear solution: return [-c / b].filter(reduce); } // quadratic solution: var q = Math.sqrt(b * b - 4 * a * c), a2 = 2 * a; return [(q - b) / a2, (-b - q) / a2].filter(reduce); } a /= d; b /= d; c /= d; const p3 = (3 * b - a * a) / 3 / 3; const q = (2 * a ** 3 - 9 * a * b + 27 * c) / 27; const q2 = q / 2; const discriminant = q2 ** 2 + p3 ** 3; let result; if (discriminant < 0) { const mp3 = -p3; const mp33 = mp3 * mp3 * mp3; const r = Math.sqrt(mp33); const t = -q / (2 * r); const cosphi = t < -1 // eslint-disable-line no-nested-ternary ? -1 : t > 1 ? 1 : t; const phi = Math.acos(cosphi); const crtr = crt(r); const t1 = 2 * crtr; x1 = t1 * Math.cos(phi / 3) - a / 3; x2 = t1 * Math.cos((phi + Math.PI * 2) / 3) - a / 3; x3 = t1 * Math.cos((phi + 4 * Math.PI) / 3) - a / 3; result = [x1, x2, x3].filter(reduce); } else if (discriminant === 0) { u1 = q2 < 0 ? crt(-q2) : -crt(q2); x1 = 2 * u1 - a / 3; x2 = -u1 - a / 3; result = [x1, x2].filter(reduce); } else { const sd = Math.sqrt(discriminant); u1 = crt(-q2 + sd); v1 = crt(q2 + sd); result = [u1 - v1 - a / 3].filter(reduce); } return result; } // see https://github.com/Pomax/bezierjs/blob/gh-pages/lib/utils.js line 313 export function lineCurveIntersection( pointHandleOut, handleOut, pointHandleIn, handleIn, lineStart = {x: 0, y: 0}, lineEnd = {x: 1, y: 0}, ) { const points = [pointHandleOut, handleOut, handleIn, pointHandleIn]; const result = getIntersectionTValue( pointHandleOut, handleOut, pointHandleIn, handleIn, lineStart, lineEnd, points, ); return split(points, result[0] || 0, [pointHandleIn, pointHandleOut]); } export function log(...rest) { /* eslint-disable no-console */ console.log(rest); /* eslint-enable no-console */ return rest[0]; } export function normalize(vector) { const x = vector.x; const y = vector.y; const norm = distance(0, 0, x, y); if (norm === 0) { return { x: 0, y: 0, }; } return { x: x / norm, y: y / norm, }; } export function vectorFromPoints(a, b) { return { x: b.x - a.x, y: b.y - a.y, }; } export function parseInt(int) { return parseInt(int); } export function makeCurveInsideSerif( pAnchors, serifHeight, serifWidth, serifMedian, serifCurve, serifTerminal, thickness, midWidth, serifRotate, ) { const yDir = pAnchors.down ? -1 : 1; const xDir = pAnchors.left ? -1 : 1; const rotateRad = (serifRotate * pAnchors.rotationAngle || 0) * Math.PI / 180; const baseWidth = pAnchors.baseWidth; const baseDir = pAnchors.baseDir; const baseHeight = pAnchors.baseHeight; const stumpOpposite = pAnchors.opposite; const stumpBase = baseHeight; let stumpVector = { x: stumpOpposite.x - stumpBase.x, y: stumpOpposite.y - stumpBase.y, }; if (baseHeight.x === stumpOpposite.x && baseHeight.y === stumpOpposite.y) { stumpVector = { x: -stumpOpposite.x + baseWidth.x, y: -stumpOpposite.y + baseWidth.y, }; } const stumpNorm = distance(0, 0, stumpVector.x, stumpVector.y); const rotationCenter = pAnchors.rotationCenter; stumpVector = normalize(stumpVector); rotationCenter.typeIn = 'line'; const topLeft = { x: rotationCenter.x + (baseHeight.x - rotationCenter.x - serifHeight * xDir) * Math.cos(rotateRad) - (baseWidth.y - rotationCenter.y + serifWidth * yDir) * Math.sin(rotateRad), y: rotationCenter.y + (baseWidth.y - rotationCenter.y + serifWidth * yDir) * Math.cos(rotateRad) + (baseHeight.x - rotationCenter.x - serifHeight * xDir) * Math.sin(rotateRad), }; const bottomLeft = { x: rotationCenter.x + (baseHeight.x - rotationCenter.x - serifHeight * xDir) * Math.cos(rotateRad) - (baseHeight.y - rotationCenter.y) * Math.sin(rotateRad), y: rotationCenter.y + (baseHeight.y - rotationCenter.y) * Math.cos(rotateRad) + (baseHeight.x - rotationCenter.x - serifHeight * xDir) * Math.sin(rotateRad), }; // We get the intersection with the left edge of the serif and the curve support // this operation is direction dependent let splitBase; if (pAnchors.inverseOrder) { splitBase = lineCurveIntersection( pAnchors.curveEnd, pAnchors.curveEnd.handleOut, pAnchors.baseWidth, pAnchors.baseWidth.handleIn, {x: topLeft.x, y: topLeft.y}, {x: bottomLeft.x, y: bottomLeft.y}, ); } else { splitBase = lineCurveIntersection( pAnchors.baseWidth, pAnchors.baseWidth.handleOut, pAnchors.curveEnd, pAnchors.curveEnd.handleIn, {x: topLeft.x, y: topLeft.y}, {x: bottomLeft.x, y: bottomLeft.y}, ); } // We chose a serifCenter depending on if the left edge intersect or not with // the curve support let serifCenter; let splitCurveEnd; if (!pAnchors.inverseOrder) { if ( splitBase.right[0].x !== splitBase.right[1].x || splitBase.right[0].y !== splitBase.right[1].y ) { serifCenter = splitBase.right[0]; splitCurveEnd = splitBase.right[1]; } else { serifCenter = splitBase.left[0]; splitCurveEnd = splitBase.left[1]; } } else if ( splitBase.left[0].x !== splitBase.left[1].x || splitBase.left[0].y !== splitBase.left[1].y ) { serifCenter = splitBase.left[1]; splitCurveEnd = splitBase.left[0]; } else { serifCenter = splitBase.right[1]; splitCurveEnd = splitBase.right[0]; } // The serif direction is the line from the serif center // to the serif left edge const serifDirection = vectorFromPoints(serifCenter, { x: rotationCenter.x + (baseHeight.x - rotationCenter.x - serifHeight * xDir) * serifMedian * Math.cos(rotateRad) - (baseWidth.y - rotationCenter.y + serifWidth * yDir) * Math.sin(rotateRad), y: rotationCenter.y + (baseWidth.y - rotationCenter.y + serifWidth * yDir) * Math.cos(rotateRad) + (baseHeight.x - rotationCenter.x - serifHeight * xDir) * serifMedian * Math.sin(rotateRad), }); const serifBasis = normalize(serifDirection); const serifRadDirection = Math.atan2(serifBasis.y, serifBasis.x); let pointOnCurveVar; let pointOnSerif; let pointWithCurve = {}; let normalToCurve; if (pAnchors.inverseOrder) { pointWithCurve = pointOnCurve( splitCurveEnd, splitCurveEnd.handleOut, serifCenter, serifCenter.handleIn, serifCurve, true, 200, ); } else { pointWithCurve = pointOnCurve( serifCenter, serifCenter.handleOut, splitCurveEnd, splitCurveEnd.handleIn, serifCurve, false, 200, ); } if (serifCurve > 0) { normalToCurve = pointWithCurve.normal; pointOnCurveVar = { x: pointWithCurve.x, y: pointWithCurve.y, dirOut: pointWithCurve.normal, typeIn: 'line', }; const curveRatio = Math.min( serifCurve / distance(0, 0, serifDirection.x, serifDirection.y), 0.75, ); pointOnSerif = { x: serifCenter.x + serifDirection.x * curveRatio, y: serifCenter.y + serifDirection.y * curveRatio, dirIn: serifRadDirection, dirOut: serifRadDirection, }; } else { if (pAnchors.inverseOrder) { const relHandle = subtract2D(serifCenter, serifCenter.handleIn); normalToCurve = Math.atan2(relHandle.y, relHandle.x) + Math.PI; } else { const relHandle = subtract2D(serifCenter, serifCenter.handleOut); normalToCurve = Math.atan2(relHandle.y, relHandle.x) + Math.PI; } pointOnCurveVar = { x: serifCenter.x, y: serifCenter.y, typeIn: 'line', }; pointOnSerif = { x: serifCenter.x, y: serifCenter.y, type: 'corner', }; } const leftEdge = { x: serifCenter.x + serifDirection.x, y: serifCenter.y + serifDirection.y, dirIn: serifRadDirection, dirOut: rotateRad, }; const rightEdge = { x: rotationCenter.x - (baseWidth.y - rotationCenter.y + serifWidth * midWidth * yDir) * Math.sin(rotateRad), y: rotationCenter.y + (baseWidth.y - rotationCenter.y + serifWidth * midWidth * yDir) * Math.cos(rotateRad), dirIn: rotateRad, typeOut: 'line', }; const serifRoot = { x: baseHeight.x, y: baseHeight.y, typeIn: 'line', }; const rootVector = normalize(vectorFromPoints(serifRoot, rightEdge)); const medianVector = normalize(vectorFromPoints(pointOnSerif, leftEdge)); const terminalVector = normalize({ x: rootVector.x + medianVector.x, y: rootVector.y + medianVector.y, }); const midPoint = { x: (leftEdge.x + rightEdge.x) / 2 + serifTerminal * serifHeight * terminalVector.x, y: (leftEdge.y + rightEdge.y) / 2 + serifTerminal * serifHeight * terminalVector.y, dirIn: rotateRad, dirOut: rotateRad, }; if (serifTerminal !== 0) { leftEdge.dirOut = Math.atan2(medianVector.y, medianVector.x); rightEdge.dirIn = Math.atan2(rootVector.y, rootVector.x); } else if (midWidth !== 1) { const dirOut = Math.atan2( leftEdge.y - rightEdge.y, leftEdge.x - rightEdge.x, ); leftEdge.dirOut = dirOut; rightEdge.dirIn = dirOut; midPoint.dirIn = dirOut; midPoint.dirOut = dirOut; } const midStump = { x: serifRoot.x + stumpNorm / 2 * stumpVector.x, y: serifRoot.y + stumpNorm / 2 * stumpVector.y, dirOut: baseDir, typeIn: 'line', }; const lastPoint = { x: pointOnCurveVar.x - stumpNorm / 2 * Math.sin(normalToCurve) * yDir * xDir, y: pointOnCurveVar.y + stumpNorm / 2 * Math.cos(normalToCurve) * yDir * xDir, dirIn: normalToCurve, typeOut: 'line', type: 'corner', }; if (serifCurve + serifHeight < 70) { midStump.tensionOut = 0; lastPoint.tensionIn = 0; } else { midStump.tensionOut = 1; lastPoint.tensionIn = 1; } return [ pointOnCurveVar, pointOnSerif, leftEdge, midPoint, rightEdge, rotationCenter, serifRoot, midStump, lastPoint, ]; }