diff --git a/src/lib/svg/index.js b/src/lib/svg/index.js new file mode 100644 index 0000000..e830816 --- /dev/null +++ b/src/lib/svg/index.js @@ -0,0 +1,55 @@ +/** + * {} + * @author yutent + * @date 2024/02/05 16:19:45 + */ + +import { DOMParser } from '@xmldom/xmldom' +import svgpath from 'svgpath' + +import { PATH_DECIMAL } from '../config.js' +import { normalizePath, pathify } from './path.js' +import { generateAmendTrans } from './view_box.js' + +/** + * 翻转svgpath + * @param {string} path path序列 + * @return {string} 反转后的path序列 + */ +export function reversal(path) { + return svgpath(path).scale(1, -1).round(PATH_DECIMAL).toString() +} + +/** + * 转换svg为一个path,并且按照目标高度,偏移进行拉伸 + * @param {string} svg 原始svg + * @param {object} options.targetHeight 目标高度 + * @return {object} path序列和viewbox + */ +export function normalizeSvg(svg, options) { + let svgDocNode = new DOMParser().parseFromString( + pathify(svg), + 'application/xml' + ) + let svgNode = svgDocNode.getElementsByTagName('svg')[0] + + //解决所有的变换,生成一个path + let path = normalizePath(svgNode) + + let trans = svgpath(path) + + //根据目标viewbox进行变换 + let targetHeight = options.targetHeight + + if (targetHeight) { + let viewObj = generateAmendTrans(svgNode, targetHeight) + viewObj.transforms.forEach(function (viewTrans) { + trans[viewTrans[0]].apply(trans, viewTrans[1]) + }) + } + + return { + viewbox: viewObj.targetViewbox, + path: trans.round(PATH_DECIMAL).toString() + } +} diff --git a/src/lib/svg/path.js b/src/lib/svg/path.js new file mode 100644 index 0000000..4eb34fe --- /dev/null +++ b/src/lib/svg/path.js @@ -0,0 +1,175 @@ +/** + * {} + * @author yutent + * @date 2024/02/05 16:12:14 + */ + +import { DOMParser, XMLSerializer } from '@xmldom/xmldom' +import svgpath from 'svgpath' + +const shape = { + $translatePath(convertObj, attributes) { + let data = {} + attributes.forEach(function (attribute) { + data[attribute.name] = attribute.value + }) + + return convertObj.template(data) + }, + generatePathNode(doc, node) { + let convertObj, attributes, path, tagName + + tagName = String(node.tagName).toLowerCase() + //polyline与polygon是同一个东西 + if (tagName === 'polyline') { + tagName = 'polygon' + } + + convertObj = shape[tagName] + if (!convertObj) { + return + } + + path = shape.$translatePath(convertObj, node.attributes) + if (!path) { + return + } + + newPathNode = doc.createElement('path') + //赋值d属性 + newPathNode.setAttribute('d', path) + //保留其他属性 + node.attributes.forEach(function (attribute) { + if (!convertObj.attrs.includes(attribute.name)) { + newPathNode.setAttribute(attribute.name, attribute.value) + } + }) + + return newPathNode + }, + rect: { + attrs: ['x', 'y', 'width', 'height', 'rx', 'ry'], + template(data) { + // 需要做一个保护,如果没有写属性值那么默认为0 + if (!data.x) { + data.x = '0' + } + + if (!data.y) { + data.y = '0' + } + + if (!data.rx && data.ry) { + data.rx = data.ry + } + + if (!data.ry && data.rx) { + data.ry = data.rx + } + + if (!data.ry && !data.rx) { + data.ry = data.rx = '0' + } + + return `M${data.x},${data.y}m${data.rx},0l${data.width - 2 * data.rx},0q${ + data.rx + },0 ${data.rx},${data.ry}l0,${data.height - 2 * data.ry}q0,${ + data.ry + } ${-data.rx},${data.ry}l${ + -data.width + 2 * data.rx + },0q${-data.rx},0 ${-data.rx},${-data.ry}l0,${ + -data.height + 2 * data.ry + }q0,${-data.ry},${data.rx},${-data.ry}Z` + } + }, + circle: { + attrs: ['cx', 'cy', 'r'], + template(data) { + return `M${data.cx},${data.cy}m${-data.r},0a${data.r},${data.r},0,1,0,${ + data.r * 2 + },0a${data.r},${data.r},0,1,0,-${data.r * 2},0Z` + } + }, + line: { + attrs: ['x1', 'y1', 'x2', 'y2'], + template(data) { + return `M${data.x1},${data.y1}L${data.x2},${data.y2}L${data.x1},${data.y1}Z` + } + }, + ellipse: { + attrs: ['cx', 'cy', 'rx', 'ry'], + template(data) { + return `M${data.cx - data.rx},${data.cy}a${data.rx},${data.ry},0,1,0,${ + data.rx * 2 + },0a${data.rx},${data.ry},0,1,0,-${data.rx * 2},0Z` + } + }, + polygon: { + attrs: ['points'], + template(data) { + data.points = data.points.replace(/(^\s*)|(\s*$)/g, '') + let points = data.points.split(' ') + let p = '' + points.forEach(function (point, index) { + if (index === 0) { + p += 'M' + point + } else { + p += 'L' + point + } + }) + p += 'Z' + + return p + } + } +} + +function generatePath(path, transforms) { + let t = svgpath(path).abs() + transforms.forEach(it => t.transform(it)) + return t.toString() +} + +export function normalizePath(node, transforms) { + let path = '' + let newTransForms = transforms ? [...transforms] : [] + + if (node.getAttribute && node.getAttribute('transform')) { + newTransForms.push(node.getAttribute('transform')) + } + + if (!node.hasChildNodes() && node.tagName === 'path') { + path = generatePath(node.getAttribute('d'), newTransForms) + } + + if (node.hasChildNodes()) { + node.childNodes.forEach(function (childNode) { + path += normalizePath(childNode, newTransForms) + }) + } + + return path +} + +function transNode(doc, node) { + let convertObj, newPathNode + + //基本的图形都是没有子结点的 + if (!node.hasChildNodes() && node.nodeName !== 'path') { + newPathNode = shape.generatePathNode(doc, node) + newPathNode && node.parentNode.replaceChild(newPathNode, node) + return + } + + node.childNodes.forEach(function (child) { + transNode(doc, child) + }) +} + +export function pathify(svgString) { + let doc = new DOMParser().parseFromString(svgString, 'application/xml') + + transNode(doc, doc) + + return new XMLSerializer().serializeToString(doc) +} diff --git a/src/lib/svg/view_box.js b/src/lib/svg/view_box.js new file mode 100644 index 0000000..21c6104 --- /dev/null +++ b/src/lib/svg/view_box.js @@ -0,0 +1,122 @@ +/** + * {} + * @author yutent + * @date 2024/02/05 16:04:06 + */ + +function getViewBox(svgNode) { + if (!svgNode) { + return + } + + let width, height + let viewbox = svgNode.getAttribute('viewBox').replace(',', ' ').split(' ') + + if (viewbox && viewbox.length === 4) { + return viewbox.map(v => +v) + } + + width = +svgNode.getAttribute('width') + height = +svgNode.getAttribute('height') + if (width != 0 && height != 0) { + return [0, 0, width, height] + } +} + +function getViewPort(svgNode) { + let width, height, viewbox + width = +svgNode.getAttribute('width') + height = +svgNode.getAttribute('height') + + viewbox = svgNode.getAttribute('viewBox').replace(',', ' ').split(' ') + if (!width) { + width = +viewbox[2] + } + if (!height) { + height = +viewbox[3] + } + + return [width, height] +} + +// 根据 targetHeight 缩放 width +// 如: normalizeViewport([1024, 2048], 1024) +// #=> [0,0,512, 1024] +function getTargetViewbox(viewport, targetHeight) { + let width = viewport[0] + let height = viewport[1] + + if (height != targetHeight) { + width = ~~((targetHeight / height) * width) + height = targetHeight + } + + return [0, 0, width, height] +} + +function normalizeXY(viewbox, targetViewbox) { + let x, y, targetX, targetY + + x = viewbox[0] + y = viewbox[1] + targetX = targetViewbox[0] + targetY = targetViewbox[1] + //可能会有x,y偏移的情况,所以这边需要做出相应的转换 + if (x != targetX || y != targetY) { + return [['translate', [targetX - x, targetY - y]]] + } + return [] +} + +function normalizeWidthHeight(viewbox, targetViewbox) { + let width, height, targetWidth, targetHeight, widthScale, heightScale + + width = viewbox[2] + height = viewbox[3] + targetWidth = targetViewbox[2] + targetHeight = targetViewbox[3] + + widthScale = width / targetWidth + heightScale = height / targetHeight + //比较正常的等比缩放 + if (widthScale == heightScale) { + return [['scale', [1 / widthScale, 1 / widthScale]]] + } + + //下面是不等比缩放 + //不等比缩放后 + let maxScale = Math.max(widthScale, heightScale) + let transforms = [], + newWidth, + newHeight + + if (widthScale < heightScale) { + newWidth = targetWidth * maxScale + transforms.push(['translate', [(newWidth - width) / 2, 0]]) + } else if (widthScale > heightScale) { + newHeight = newHeight * maxScale + transforms.push(['translate', [0, (newHeight - height) / 2]]) + } + + transforms.push(['scale', [1 / maxScale, 1 / maxScale]]) + + return transforms +} + +//返回需要进行的转换数组 +function normalizeViewBox(viewbox, targetViewbox) { + return normalizeXY(viewbox, targetViewbox).concat( + normalizeWidthHeight(viewbox, targetViewbox) + ) +} + +export function generateAmendTrans(svgNode, targetHeight) { + let viewport = getViewPort(svgNode) + let viewbox = getViewBox(svgNode) + let targetViewbox = getTargetViewbox(viewport, targetHeight) + + return { + transforms: normalizeViewBox(viewbox, targetViewbox), + targetViewbox: targetViewbox + } +}