UNPKG

svg-getpointatlength

Version:

alternative to native pointAtLength() and getTotalLength() method

342 lines (258 loc) 10.9 kB
import { splitSubpaths } from './pathData_split.js'; import { getAngle, bezierhasExtreme, getPathDataVertices, svgArcToCenterParam, getSquareDistance, commandIsFlat } from "./geometry.js"; import { getPolygonArea, getPathArea } from './geometry_area.js'; import { getPolyBBox } from './geometry_bbox.js'; import { renderPoint, renderPath } from "./visualize.js"; /** * check if path is closed * either by explicit Z commands * or coinciding start and end points */ export function checkClosePath(pathData){ let pathDataL = pathData.length; let closed = pathData[pathDataL - 1]["type"] == "Z" ? true : false; if(closed){ //console.log('is closed'); return true } let M = pathData[0]; let [x0, y0] = [M.values[0], M.values[1]].map(val => { return +val.toFixed(8) }); let lastCom = pathData[pathDataL - 1]; let lastComL = lastCom.values.length; //check distance between start and end let [xE, yE] = [lastCom.values[lastComL - 2], lastCom.values[lastComL - 1]].map(val => { return +val.toFixed(8) }); let closedByCoords = x0 === xE && y0 === yE //console.log('closedByCoords', closedByCoords); if(closedByCoords) return true return false } /** * analyze path data for * decimal detection * sub paths * directions * crucial geometry properties */ export function analyzePathData(pathData = [], debug = true) { // clone pathData = JSON.parse(JSON.stringify(pathData)); // split to sub paths let pathDataSubArr = splitSubpaths(pathData) // collect more verbose data let pathDataPlus = []; // log let simplyfy_debug_log = []; /** * analyze sub paths * add simplified bbox (based on on-path-points) * get area */ pathDataSubArr.forEach(pathData => { let pathDataArea = getPathArea(pathData); let pathPoly = getPathDataVertices(pathData); let bb = getPolyBBox(pathPoly) let { left, right, top, bottom, width, height } = bb; // initial starting point coordinates let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] }; let M = { x: pathData[0].values[0], y: pathData[0].values[1] }; let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] }; let p; // init starting point data pathData[0].p0 = M; pathData[0].p = M; pathData[0].lineto = false; pathData[0].corner = false; pathData[0].extreme = false; pathData[0].directionChange = false; pathData[0].closePath = false; // add first M command let pathDataProps = [pathData[0]]; let area0 = 0; let len = pathData.length; for (let c = 2; len && c <= len; c++) { let com = pathData[c - 1]; let { type, values } = com; let valsL = values.slice(-2); /** * get command points for * flatness checks: * this way we can skip certain tests */ let commandPts = [p0]; let isFlat = false; // init properties com.lineto = false; com.corner = false; com.extreme = false; com.directionChange = false; com.closePath = false; /** * define angle threshold for * corner detection */ let angleThreshold = 0.05 p = valsL.length ? { x: valsL[0], y: valsL[1] } : M; // update M for Z starting points if (type === 'M') { M = p; p0 = p //p0 = p } else if (type.toLowerCase() === 'z') { //p0 = M; p = M; } // add on-path points com.p0 = p0; com.p = p; let cp1, cp2, cp1N, cp2N, pN, typeN, area1; /** * explicit and implicit linetos * - introduced by Z */ if (type === 'L') com.lineto = true; if (type === 'Z') { com.closePath = true; // if Z introduces an implicit lineto with a length if (M.x !== M0.x && M.y !== M0.y) { com.lineto = true; } } // if bezier if (type === 'Q' || type === 'C') { cp1 = { x: values[0], y: values[1] } cp2 = type === 'C' ? { x: values[2], y: values[3] } : null; com.cp1 = cp1; if (cp2) com.cp2 = cp2; } /** * check command flatness * we leave it to the bezier simplifier * to convert flat beziers to linetos * otherwise we may strip rather flat starting segments * preventing a better simplification */ if(values.length>2){ if (type === 'Q' || type === 'C' ) commandPts.push(cp1); if (type === 'C') commandPts.push(cp2); commandPts.push(p); let commandFlatness = commandIsFlat(commandPts); isFlat = commandFlatness.flat; com.flat = isFlat; if(isFlat){ com.extreme = false; /* pathDataProps.push(com) p0 = p; continue; */ } } /** * is extreme relative to bounding box * in case elements are rotated we can't rely on 90degree angles * so we interpret maximum x/y on-path points as well as extremes * but we ignore linetos to allow chunk compilation */ if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) { com.extreme = true; } // add to average //let squareDist = getSquareDistance(p0, p) //com.size = squareDist; let dimA = (width + height) / 2; com.dimA = dimA; //console.log('decimals', decimals, size); //next command let comN = pathData[c] ? pathData[c] : null; let comNValsL = comN ? comN.values.slice(-2) : null; typeN = comN ? comN.type : null; // get bezier control points if (comN) { pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null; if (comN.type === 'Q' || comN.type === 'C') { cp1N = { x: comN.values[0], y: comN.values[1] } cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null; } } /** * Detect direction change points * this will prevent distortions when simplifying * e.g in the "spine" of an "S" glyph */ area1 = getPolygonArea(commandPts) let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false; // update area area0 = area1 if (signChange) { //renderPoint(svg1, p0, 'orange', '1%', '0.75') com.directionChange = true; } /** * check extremes or corners for adjacent curves by control point angles */ if ((type === 'Q' || type === 'C') ) { if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) { // check extremes let cpts = commandPts.slice(1); let w = Math.abs(pN.x - p0.x) let h = Math.abs(pN.y - p0.y) let thresh = (w + h) / 2 * 0.1; let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN]; let flatness2 = commandIsFlat(pts1, thresh) let isFlat2 = flatness2.flat; //console.log('isFlat2', isFlat2, isFlat); /** * if current and next cubic are flat * we don't flag them as extremes to allow simplification */ let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true); if (hasExtremes) { com.extreme = true } // check corners else { let cpts1 = cp2 ? [cp2, p] : [cp1, p]; let cpts2 = cp2 ? [p, cp1N] : [p, cp1N]; let angCom1 = getAngle(...cpts1, true) let angCom2 = getAngle(...cpts2, true) let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI let cpDist1 = getSquareDistance(...cpts1) let cpDist2 = getSquareDistance(...cpts2) let cornerThreshold = 10 let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2 if (isCorner) { com.corner = true; } } } } let debug = false //debug = true if (debug) { if (com.signChange) { renderPoint(svg1, p0, 'orange', '1.5%', '0.75') } if (com.extreme) { renderPoint(svg1, p, 'cyan', '1%', '0.75') } if (com.corner) { renderPoint(svg1, p, 'magenta') } } pathDataProps.push(com) p0 = p; } //decimalsAV = Array.from(decimalsAV) //decimalsAV = Math.ceil(decimalsAV.reduce((a, b) => a + b) / decimalsAV.length); //console.log('decimalsAV', decimalsAV); //pathDataProps[0].decimals = decimalsAV //decimalsAV = Math.floor(decimalsAV/decimalsAV.length); let dimA = (width + height) / 2 pathDataPlus.push({ pathData: pathDataProps, bb: bb, area: pathDataArea, dimA: dimA }) if (simplyfy_debug_log.length) { console.log(simplyfy_debug_log); } }) return pathDataPlus }