为了解决让普通的SVG也能作为clip-path成为其它图片的遮罩用
碰到的挑战有
1.需要将非path的形态变成path
https://github.com/elrumordelaluz/element-to-path
2.需要将非相对的路径点,变成相对的
https://github.com/yoksel/relative-clip-path
const keysListByCommand = {
'm': ['x', 'y'],
'l': ['x', 'y'],
'h': ['x'],
'v': ['y'],
'c': ['x', 'y', 'x1', 'y1', 'x2', 'y2'],
's': ['x', 'y', 'x1', 'y1'],
'q': ['x', 'y', 'x1', 'y1'],
't': ['x', 'y', 'x1', 'y1'],
'a': ['rx', 'ry', 'x-axis-rotation', 'large-arc-flag', 'sweep-flag', 'x', 'y']
};
export class ClipPathConverter
{
fullCodes;
isRemoveOffset;
lastUsedCoords;
coords: any
pathSizes: any
minXY: { x: any; y: any }
container;
private static instance:ClipPathConverter
static getInstance():ClipPathConverter
{
ClipPathConverter.instance=ClipPathConverter.instance||(new ClipPathConverter());
return ClipPathConverter.instance;
}
constructor()
{
this.container=document.createElement('div');
this.container.style.position="absolute";
this.container.style.overflow="hidden";
this.container.style.width="0";
this.container.style.height="0";
document.body.append(this.container);
this.lastUsedCoords = {};
}
convert(svg,isRemoveOffset=true)
{
let inSvg=/\<svg.*?\>(.*)\<\/svg\>/ig.exec(svg)[1];
var svgEle=document.createElementNS("http://www.w3.org/2000/svg","svg");
this.container.append(svgEle);
svgEle.innerHTML=inSvg;
var ele=svgEle.querySelector("path,polygon,ellipse,circle,rect,line,polyline") as SVGSVGElement;
this.pathSizes = ele.getBBox();
var dPath=svgElementToPath(ele,{nodeName:"nodeName"});//pathEle.getAttribute("d");
this.coords = dPath;
this.isRemoveOffset=isRemoveOffset;
let rPath=this.updateView();
var outSvgEle=document.createElementNS("http://www.w3.org/2000/svg","svg");
var outPathEle=document.createElementNS("http://www.w3.org/2000/svg","path");
outPathEle.setAttribute("d",rPath);
outSvgEle.append(outPathEle);
this.container.innerHTML="";
return outSvgEle.outerHTML;
}
// ---------------------------------------------
// Update view
// ---------------------------------------------
private updateView () {
// Normalize coordinates list formating
const coordsNormalized = normalizePathCoords(this.coords);
// Collect all cordinates set from char to next char (next char not includes)
const coordsListSrc = [...coordsNormalized.matchAll(/[a-z][^(a-z)]{1,}/gi)]
.map(item => item[0]);
let coordsList = coordsListSrc.slice();
// Add omitted commands for more correct parsing
coordsList = this.addOmittedCommands(coordsListSrc.slice());
if(this.isRemoveOffset) {
// Remove path offset
coordsList = this.removeOffset(coordsList);
}
// Convert coordinates to relative
const coordsTransformed = this.transformCoords(coordsList);
let resultPath = coordsTransformed.join(' ');
// this.demoClipPathAfter.innerHTML = '';
if(resultPath.includes('Infinity')) {
throw new Error('Source path is not correct')
// this.resultTextElem.value = 'Source path is not correct';
return;
}
return resultPath;
}
// ---------------------------------------------
// Add ommitted command letters for easy parsing
// ---------------------------------------------
private addOmittedCommands (srcCoordsList) {
srcCoordsList = srcCoordsList.slice();
const coordsFixed = [];
const max = 5000;
let counter = 0;
const handledCommands = {
'a': true,
't': true,
'c': true,
's': true,
'q': true,
}
while(srcCoordsList.length > 0 && counter < max) {
let value = srcCoordsList.shift();
let {commandSrc, command, coordsList, keysList} = parseCoordsItem(value);
if(keysList) {
let coords;
if(handledCommands[command] && coordsList.length > keysList.length) {
// Fix problem with long commands like A
const cuttedTail = coordsList.splice(keysList.length);
coords = coordsList.join(',');
if(cuttedTail.length % keysList.length === 0) {
// Move part of command to the next item
cuttedTail[0] = `${commandSrc}${cuttedTail[0]}`;
srcCoordsList.unshift(cuttedTail.join(','));
}
else {
console.log('\nCommand is broken, check params:', coordsList);
}
}
else {
coords = coordsList.join(',');
}
value = `${commandSrc}${coords}`;
}
else {
console.log('Unrecognized command: ', command);
}
coordsFixed.push(value);
counter++;
}
return coordsFixed;
}
// ---------------------------------------------
// Translate relative commands to absolute
// (l -> L, a -> A, ...)
// ---------------------------------------------
private relCommandsToAbs(initialCoordsList) {
initialCoordsList = initialCoordsList.slice();
const coordsAbs = initialCoordsList.reduce((prev, value, index) => {
let {commandSrc, command, coordsList, keysList, isCommandUpperCase} = parseCoordsItem(value);
if(command === 'm') {
const [x, y] = coordsList;
this.lastUsedCoords.x = x;
this.lastUsedCoords.y = y;
}
if(!isCommandUpperCase) {
const prevCoords = prev[index - 1];
const absItemCoords = this.relItemCoordToAbs(keysList, coordsList, prevCoords, command);
value = `${commandSrc.toUpperCase()}${absItemCoords.join(',')}`;
}
prev.push(value);
return prev;
}, []);
return coordsAbs;
}
// ---------------------------------------------
private relItemCoordToAbs (keysList, coordsList, prevCoords, itemCommand) {
const valuesList = coordsList;
const prevCoordsData = parseCoordsItem(prevCoords);
const prevCoordsList = prevCoordsData.coordsList;
let [prevX, prevY] = prevCoordsList.splice(-2);
if(prevCoordsData.command === 'v') {
prevY = prevX;
prevX = this.lastUsedCoords.x;
}
else if(prevCoordsData.command === 'h') {
prevY = this.lastUsedCoords.y;
}
const transformedValuesList = valuesList.map((item, index) => {
const key = keysList[index];
if(!isFinite(item)) {
console.log('Not finite item:', item);
return item;
}
if(key && (key.includes('rotation')
|| key.includes('flag')
|| key === 'rx'
|| key === 'ry')) {
return item;
}
if(!key && itemCommand !== 'a') {
// Commands can use more than two coords
if(index % 2 == 0) {
// x
const newX = item + prevX;
if(itemCommand === 'l') {
prevX = newX;
}
this.lastUsedCoords.x = newX;
return newX;
}
else {
// y
const newY = item + prevY;
if(itemCommand === 'l') {
prevY = newY;
}
this.lastUsedCoords.y = newY;
return newY;
}
}
if(key.includes('x')) {
const newX = item + prevX;
if(itemCommand === 'l') {
prevX = newX;
}
this.lastUsedCoords.x = newX;
return newX;
}
if(key.includes('y')) {
const newY = item + prevY;
if(itemCommand === 'l') {
prevY = newY;
}
this.lastUsedCoords.y = newY;
return newY;
}
return item;
});
return transformedValuesList;
}
// ---------------------------------------------
// Removing path offset
// ---------------------------------------------
private removeOffset (srcCoordsList) {
// Find minimal value
srcCoordsList = srcCoordsList.slice();
// Make easier to get offset
const absCoordsList = this.relCommandsToAbs(srcCoordsList.slice());
srcCoordsList = absCoordsList;
this.minXY = this.findOffset(absCoordsList);
const coordsWithoutOffset = [];
const max = 5000;
let counter = 0;
while(srcCoordsList.length > 0 && counter < max) {
let value = srcCoordsList.shift();
let {commandSrc, command, coordsList, keysList, isCommandUpperCase} = parseCoordsItem(value);
if(keysList) {
if(isCommandUpperCase) {
const transformedValsList = this.removeOffsetFromValues(keysList, coordsList, command)
value = `${commandSrc}${transformedValsList.join(',')}`;
}
coordsWithoutOffset.push(value);
}
else {
console.log('Unrecognized command: ', command);
}
counter++;
}
return coordsWithoutOffset;
}
// ---------------------------------------------
private removeOffsetFromValues (keysList, coordsList, itemCommand) {
const valuesList = coordsList;
const transformedValuesList = valuesList.map((item, index) => {
if(!keysList[index] && itemCommand !== 'a') {
// L lets use more than two coords
if(index % 2 == 0) {
// x
return item - this.minXY.x;
}
else {
// y
return item - this.minXY.y;
}
}
if(keysList[index].includes('rotation')
|| keysList[index].includes('flag')
|| keysList[index] === 'rx'
|| keysList[index] === 'ry') {
return item;
}
if(keysList[index].includes('x')) {
return item - this.minXY.x;
}
if(keysList[index].includes('y')) {
return item - this.minXY.y;
}
return item;
});
return transformedValuesList;
}
// ---------------------------------------------
private findOffset (srcCoordsList) {
// Find minimal value
srcCoordsList = srcCoordsList.slice();
let minXY = { x: null, y: null};
const max = 5000;
let counter = 0;
while(srcCoordsList.length > 0 && counter < max) {
let value = srcCoordsList.shift();
let {commandSrc, command, coordsList, keysList, isCommandUpperCase} = parseCoordsItem(value);
if(!isCommandUpperCase) {
continue;
}
if(command == 'm' && minXY.x === null) {
const [x, y] = coordsList;
minXY = {x, y};
// For correct handling v & h
this.lastUsedCoords = {x, y};
continue;
}
const itemMinXY = this.findItemMinXY(keysList, coordsList, command);
if(itemMinXY.x >= 0 && itemMinXY.x < minXY.x) {
minXY.x = itemMinXY.x;
}
if(itemMinXY.y >= 0 && itemMinXY.y < minXY.y) {
minXY.y = itemMinXY.y;
}
counter++;
}
return minXY;
}
// ---------------------------------------------
private findItemMinXY(keysList, coordsList, itemCommand) {
let valuesList = coordsList;
let minXY = {x: null, y: null};
const max = 10000;
let counter = 0;
// Handling short paths
if(itemCommand === 'v') {
valuesList = [
this.lastUsedCoords.x,
valuesList[0]
]
}
else if(itemCommand === 'h') {
valuesList = [
valuesList[0],
this.lastUsedCoords.y
]
}
else if(itemCommand === 'a') {
valuesList = valuesList.splice(-2);
}
if(valuesList.length === 2) {
const [x, y] = valuesList;
minXY = {x, y};
this.lastUsedCoords = {x, y};
return minXY;
}
// Handling long paths
while(valuesList.length > 0 && counter < max) {
let [x, y] = valuesList.splice(0,2);
if(minXY.x === null) {
minXY = {x, y};
continue;
}
if(x >= 0 && x < minXY.x) {
minXY.x = x;
}
if(y >= 0 && y < minXY.y) {
minXY.y = y;
}
counter++;
}
return minXY;
}
// ---------------------------------------------
// Transforming coordinates from userSpaceOnUse
// coordinate system to objectBoundingBox
// M281.5 0L563 563H0z -> M0.5,0, L1,1, H0
// ---------------------------------------------
private transformCoords (srcCoordsList) {
srcCoordsList = srcCoordsList.slice();
const coordsTransformed = [];
const max = 5000;
let counter = 0;
while(srcCoordsList.length > 0 && counter < max) {
let value = srcCoordsList.shift();
let {commandSrc, command, coordsList, keysList} = parseCoordsItem(value);
if(keysList) {
const transformedValsList = this.transformValuesByKeys(keysList, coordsList, command)
value = `${commandSrc}${transformedValsList.join(',')}`;
}
else {
console.log('Unrecognized command: ', command);
}
coordsTransformed.push(value);
counter++;
}
return coordsTransformed;
}
// ---------------------------------------------
private transformValuesByKeys (keysList, coordsList, itemCommand) {
const valuesList = coordsList;
const transformedValuesList = valuesList.map((item, index) => {
if(!keysList[index] && itemCommand !== 'a') {
// L lets use more than two coords
if(index % 2 == 0) {
return this.getTransformedByKey('width', item);
}
else {
return this.getTransformedByKey('height', item);
}
}
if(keysList[index].includes('rotation')|| keysList[index].includes('flag')) {
return item;
}
if(keysList[index].includes('x')) {
return this.getTransformedByKey('width', item);
}
if(keysList[index].includes('y')) {
return this.getTransformedByKey('height', item);
}
return item;
});
return transformedValuesList;
}
// ---------------------------------------------
private getTransformedByKey (key = 'height', value) {
let result = 0;
if(key === 'width') {
result = round(value/this.pathSizes.width);
}
else {
result = round(value/this.pathSizes.height);
}
// Reduce of maximum coordinates to 1
if(result > 1) {
result = Math.floor(result);
}
return result;
}
// ---------------------------------------------
// Removing start spaces from output codes
// ---------------------------------------------
private removeStartSpaces (str, key) {
str = str.trim();
let minSpace = this.fullCodes.spaces[key];
if(minSpace === undefined) {
minSpace = findMinSpaces(str);
this.fullCodes.spaces[key] = minSpace;
}
const regexp = new RegExp(`^\\s{${minSpace}}`,'gm');
return str.replace(regexp,'');
}
}
const rect = attrs => {
let awidth=(attrs.width.value||attrs.width);
let aheight=(attrs.height.value||attrs.height);
let ax=(attrs.x.value||attrs.x);
let ay=(attrs.y.value||attrs.y);
let arx=(attrs.rx.value||attrs.rx);
let ary=(attrs.ry.value||attrs.ry);
const w = +awidth
const h = +aheight
const x = ax ? +ax : 0
const y = ay ? +ay : 0
let rx = arx || 'auto'
let ry = ary || 'auto'
if (rx === 'auto' && ry === 'auto') {
rx = ry = 0
} else if (rx !== 'auto' && ry === 'auto') {
rx = ry = calcValue(rx, w)
} else if (ry !== 'auto' && rx === 'auto') {
ry = rx = calcValue(ry, h)
} else {
rx = calcValue(rx, w)
ry = calcValue(ry, h)
}
if (rx > w / 2) {
rx = w / 2
}
if (ry > h / 2) {
ry = h / 2
}
const hasCurves = rx > 0 && ry > 0
return [
`M${x + rx} ${y}`,
`H${x + w - rx}`,
...(hasCurves ? [`A${rx} ${ry} 0 0 1 ${x + w} ${y + ry}`] : []),
`V${y + h - ry}`,
...(hasCurves ? [`A${rx} ${ry} 0 0 1 ${x + w - rx} ${y + h}`] : []),
`H${x + rx}`,
...(hasCurves ? [`A${rx} ${ry} 0 0 1 ${x} ${y + h - ry}`] : []),
`V${y + ry}`,
...(hasCurves ? [`A${rx} ${ry} 0 0 1 ${x + rx} ${y}`] : []),
'z',
]
}
const ellipse = attrs => {
let acx=(attrs.cx.value||attrs.cx);
let acy=(attrs.cy.value||attrs.cy);
let ar=(attrs.r.value||attrs.r);
let arx=(attrs.rx.value||attrs.rx);
let ary=(attrs.ry.value||attrs.ry);
const cx = +acx
const cy = +acy
const rx = arx ? +arx : +ar
const ry = ary ? +ary : +ar
return [
`M${cx + rx} ${cy}`,
`A${rx} ${ry} 0 0 1 ${cx} ${cy + ry}`,
`A${rx} ${ry} 0 0 1 ${cx - rx} ${cy}`,
`A${rx} ${ry} 0 0 1 ${cx + rx} ${cy}`,
'z',
]
}
const line = ({ x1, y1, x2, y2 }) => {
let ax1=(x1.value||x1);
let ax2=(x2.value||x2);
let ay1=(y1.value||y1);
let ay2=(y2.value||y2);
return [`M${+ax1} ${+ay1}`, `L${+ax2} ${+ay2}`]
}
const poly = attrs => {
const { points } = attrs
const pointsArray = (points.value||points)
.trim()
.split(' ')
.reduce((arr, point) => {
return [...arr, ...(point.includes(',') ? point.split(',') : [point])]
}, [])
const pairs = chunkArray(pointsArray, 2)
return pairs.map(([x, y], i) => {
return `${i === 0 ? 'M' : 'L'}${x} ${y}`
})
}
const toPathString = d => {
return Array.isArray(d) ? d.join(' ') : ''
}
export const svgElementToPath = (
node,
{ nodeName = 'name', nodeAttrs = 'attributes' } = {}
) => {
const name = node[nodeName]
const attributes = node[nodeAttrs]
let d
if (name === 'rect') {
d = rect(attributes)
}
if (name === 'circle' || name === 'ellipse') {
d = ellipse(attributes)
}
if (name === 'line') {
d = line(attributes)
}
if (name === 'polyline') {
d = poly(attributes)
}
if (name === 'polygon') {
d = [...poly(attributes), 'Z']
}
if (name === 'path') {
return attributes.d.value||attributes.d
}
return toPathString(d)
}
// ---------------------------------------------
// Helpers
// ---------------------------------------------
const chunkArray = (arr, size = 2) => {
let results = []
while (arr.length) {
results.push(arr.splice(0, size))
}
return results
}
const calcValue = (val, base) => {
return /%$/.test(val) ? (val.replace('%', '') * 100) / base : +val
}
function findMinSpaces(str) {
const spaces = str
.match(/(^\s{1,})/gm);
if(!spaces) {
return 0;
}
const minSpace = spaces
.reduce((prev, item) => {
const spacesLength = item.length;
if(prev == null || spacesLength < prev) {
prev = spacesLength;
return prev;
}
return prev;
}, null);
return minSpace;
}
// ---------------------------------------------
function parseCoordsItem(item) {
const commandSrc = item.substring(0,1);
const command = commandSrc.toLowerCase();
const isCommandUpperCase = command !== commandSrc;
const keysList = keysListByCommand[command];
let coordsList = item
.substring(1)
.replace(/,$/,'')
.split(',')
.map(item => +item);
return {
commandSrc,
command,
isCommandUpperCase,
keysList,
coordsList
}
}
// ---------------------------------------------
function normalizePathCoords(coords) {
let result = coords
.replace(/([a-z]) /gi, '$1')
.replace(/([a-z])/gi, ' $1')
.trim()
.replace(/(\d{1,})(-)/gi, '$1 $2')
.replace(/\s00/gi, ' 0 0 ')
.replace(/z/gi, ' ')
.replace(/,\s{1,}/gi, ',')
.replace(/\s{1,},/gi, ',')
.replace(/\s{1,}/gi, ',');
// .345.279
while(result.match(/\.\d{1,}\.\d{1,}/gi)) {
result = result.replace(/(\.\d{1,})(\.\d{1,})/gi, '$1,$2');
}
return result;
}
// ---------------------------------------------
function round(num) {
return Math.round(num * 1000) / 1000;
}