为了解决让普通的SVG也能作为clip-path成为其它图片的遮罩用

为了解决让普通的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;

}

Leave a comment

Your email address will not be published. Required fields are marked *