Упростить создание SVG и манипуляции с помощью прокси


Необходимость

Я всегда находил динамического создания SVG-содержимого и фильтры болезненный и тяжелый процесс.

  • Постоянная потребность для конвертирования значений в JavaScript в SVG и обратно,
  • Доступ к атрибутов SVG через setAttribute и setAttribute делает код беспорядок.
  • Вспомнив, какое свойство использовать имена, именования. Например clip-path и clipPathUnit правильно, пока clipPath и clip-path-unit являются неправильными.

По этим и другим причинам я, как правило, держался подальше от СВГ, но в виде HTML-холст CanvasRenderingContext2D сейчас имеет хорошую поддержку filter собственность, заполняя необходимое отверстие в API, я нахожу мой сам СВГ написания контента все больше и больше.

Поэтому после того, как особенно неприятный баг, скрытые в беспорядок кода SVG, я решил написать следующее.

Код для обзора.

const createSVG = (()=>{
    /* This code uses some abreviations
       str is string
       arr is array
       num is number
       prop is property
       props is properties
       2 for conversion eg str2Num is string to number
    */
    var   id = 0;
    var   units = "";
    const svgNamespace = "http://www.w3.org/2000/svg";
    const transformTypes = {read : "read", write : "write"};
    const transformPropsName = "accent-height,alignment-baseline,arabic-form,baseline-shift,cap-height,clip-path,clip-rule,color-interpolation,color-interpolation-filters,color-profile,color-rendering,dominant-baseline,enable-background,fill-opacity,fill-rule,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,horiz-adv-x,horiz-origin-x,image-rendering,letter-spacing,lighting-color,marker-end,marker-mid,marker-start,overline-position,overline-thickness,panose-1,paint-order,pointer-events,rendering-intent,shape-rendering,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,text-anchor,text-decoration,text-rendering,underline-position,underline-thickness,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,vert-adv-y,vert-origin-x,vert-origin-y,word-spacing,writing-mode,x-height";
    const unitPropsNames ="width,height,x,y,z,x1,x2,y1,y2,cx,cy,rx,ry,r,accentHeight,alignmentBaseline,baselineShift,capHeight,fontSize,fontSizeAdjust,overlinePosition,overlineThickness,strikethroughPosition,strikethroughThickness,strokeWidth,underlinePosition,underlineThickness,vertOriginX,vertOriginY,wordSpacing,xHeight";

    /* Transform helper functions */
    const onlyArr2Str = (value, points = false) => {
        if (points) {
            if (Array.isArray(value)) {
                return value.map(point => Array.isArray(point) ? point.join(",") : point).join(" ");
            }
            return value;
        }
        return Array.isArray(value) ? value.join(" ") : value
    }

    /* Value transform functions */
    const arr2Str      = value => onlyArr2Str(value);
    const str2NumArr   = value => value.split(" ").map(value => Number(value));
    const unitStr2Num  = value => Number(value.replace(/[a-z]/gi, ""));
    const str2Num      = value => Number(value);
    const str2NumOrStr = value => isNaN(value) ? value : Number(value);
    const num2UnitStr  = value => value + units;
    const num2Percent  = value => value + "%";
    const pointArr2Str = value => onlyArr2Str(value, true);
    const url2Str      = value => value.replace(/url\(#|)/g, "");
    const ref2Url      = value => {
        if (typeof value === "string") {
            if (value.indexOf("url(#") > -1) { return value }
            return `url(#${value})`;
        }
        if (value.isPSVG) {
            if (value.node.id) { return `url(#${value.node.id})` }
            value.node.id = "PSVG_ID_"+ (id ++);
            return `url(#${value.node.id})`;
        }
        return value;
    };
    const str2PointArr = value => value.split(" ").map(point => {
        point = point.split(",");
        point[0] = Number(point[0]);
        point[1] = Number(point[1]);
        return point;
    });

    /* property value transforms `read` from SVG `write` to SVG */
    const transforms = {
        read : {
            offset      : unitStr2Num,
            points      : str2PointArr,
            filter      : url2Str,
            clipPath    : url2Str,
            stdDeviation: str2Num,
            dx          : str2Num,  
            dy          : str2Num, 
            tableValues : str2NumArr,
            values      : str2NumArr,
            kernelMatrix: str2NumArr,
            viewbox     : str2NumArr,
            _default    : str2NumOrStr, 
        },
        write : {
            points      : pointArr2Str,
            offset      : num2Percent,
            filter      : ref2Url,
            clipPath    : ref2Url,
            tableValues : arr2Str,
            values      : arr2Str,
            kernelMatrix: arr2Str,
            viewbox     : arr2Str,
            _default(value) { return value },
        },
    }

    /* Assign additional unit value transforms */
    unitPropsNames.split(",").forEach((propName) => {
        transforms.read[propName] = unitStr2Num;
        transforms.write[propName] = num2UnitStr;
    });

    /* Create property name transform lookups */
    const propNodeNames = transformPropsName.split(",");
    const propScriptNames = transformPropsName.replace(/-./g, str => str[1].toUpperCase()).split(",");

    /* returns transformed `value` of associated property `name`  depending on `[type]` default write*/
    function transform(name, value, type = transformTypes.write) {
        return transforms[type][name] ? transforms[type][name](value) : transforms[type]._default(value);
    }

    /* returns Transformed JavaScript property name as SVG property name if needed. EG "fillRule" >> "fill-rule" */
    function propNameTransform(name) {
        const index = propScriptNames.indexOf(name);
        return index === -1 ? name : propNodeNames[index];
    }

    /* node creation function returned as the interface instanciator of the node proxy */
    /* type String representing the node type.
       props optional Object containing node properties to set
       returns a proxy holding the node */
    const createSVG = (type, props = {}) => {
        const PSVG = (()=>{  // PSVG is abreviation for Practical SVG 
            const node = document.createElementNS(svgNamespace, type);
            const set  = (name, value) => node.setAttribute(propNameTransform(name), transform(name, value));
            const get  = (name, value) => transform(name, node.getAttribute(propNameTransform(name)), transformTypes.read);
            const svg  = {
                isPSVG   : true,
                nodeType : type,
                node     : node,
                set units(postFix) { units = postFix },
                get units() { return units },
            };
            const proxyHandler = {
                get(target, name) { return svg[name] !== undefined ? target[name] : get(name) },
                set(target, name, value) {
                    if (value !== null && typeof value === "object" && value.isPSVG) {
                        node.appendChild(value.node);
                        target[name] = value;
                        return true;
                    }
                    set(name,value);
                    return true;
                },
            };
            return new Proxy(svg, proxyHandler);
        })();
        Object.keys(props).forEach(key => PSVG[key] = props[key]);
        return PSVG;
    }
    return createSVG;
})();
export default createSVG;

Почему комментарий

Мой опыт SVG является низким, и я не уверен, если это безопасно или даже практичным для диких.

Любые комментарии, предложения, предупреждения или улучшения будут оценены.

Как это работает.

Это создает XML-узлов и возвращает прокси-объект, удерживая узел. Прокси get и set обработчики делать тяжелую работу преобразования имен свойств и значений свойств между JavaScript дружелюбный и XML форматах, а также выполнение правильных действий в зависимости от типа недвижимости.

  • Единицы значения преобразуются из числа JavaScript. например "20px" становится 20
  • Значения массива преобразуются в соответствующие строки и обратно
  • Имена свойств будут преобразованы в SVG имена свойств, если это необходимо "strokeWidth" становится "инсульт-ширина"
  • Референсные значения преобразуются должным образом в зависимости от типа назначения.. например svg.circle.filter = svg.blurFilter // <circle filter="url(#blur)"> в blurfilter ID-это ссылка.
  • Назначение узла в имени свойства также добавляет узел. Узлы могут быть доступны как свойства. например svg.circle = createSVG("circle"); новый круг узел добавляется к СВГ узел. svg.circle.r правильно открыть кружок узла радиус собственность.

Пример использования

 //=====================================================
 // Create a node.
 const svg = createSVG("svg",{width : 100, height : 100});

 //=====================================================
 // Add a node 
 const svg.circle = createSVG("circle");

 //=====================================================
 // Add and set property
 svg.circle.cx = 50;  // Note that default units is px
 svg.circle.cx = 50;
 svg.circle.r = 30;
 svg.circle.fill = "Green";
 svg.circle.stroke = "black";

 //=====================================================
 // Transforming property name
 svg.circle.strokeWidth = 3; // SVG circle property name "stroke-width"

 // XML result of circle
 // <circle cx="50px" cy="50px" r="30px" fill="Green" stroke="black" stroke-width="3px"></circle>

 //=====================================================
 // Modify a property 
 svg.circle.r += 10;

 //=====================================================
 // Array value transformation 
 svg.polygon = createSVG("polygon");

 // array to string transform
 // "0,0 100,0 100,100 0,100";
 svg.polygon.points = [[0,0],[100,0],[100,100],[0,100]];

 // string to array transform
 const pointsArray = svg.polygon.points;

 //=====================================================
 // node access
 svg.text = createSVG("text");
 svg.text.node.textContent = "Hi World";     


 //=====================================================
 // Adding to DOM
 document.appendChild(svg.node);     

Пример

Следующий фрагмент кода содержит пример использования. Создает узел СВГ, добавляет в дом, небольшая задержка после обновления свойства узла, добавляет дополнительные узлы, и запускает JavaScript для эффектов анимации.

"use strict";
/* Example usage */
setTimeout(()=>{
    const width = 100;
    const height = 100;
    const resizeBy = 10;
    const pathPoints = [[0,0], [100,0], [50,50], [100,100], [0,100], [50,50], [0,0]];
    const pathStyle = {points : pathPoints, fill : "orange", stroke : "black", strokeWidth : 3 };
    
    // ======================================================================= 
    // createSVG takes two arguments
    // The node type as a string
    // optional object containing normalized properties and values
    const svg = createSVG("svg", {width : width, height : height});
    // create a polygon node
    svg.polygon = createSVG("polygon", pathStyle);

    // =======================================================================   
    // add svg node to DOM
    exampleSVG.appendChild(svg.node);
    XMLResult.textContent = exampleSVG.innerHTML;
    
    
    // =======================================================================
    // Two seconds then change some properties and add new nodes    
    setTimeout(() => {
      infoElement.textContent = "SVG properties updated and nodes added. Javascript animation";
      // resize SVG 
      var extraSize = (svg.polygon.strokeWidth + 2) * 2;
      svg.width  += resizeBy + extraSize;  // The proxy get converts string to NUMBER and
      svg.height += resizeBy + extraSize;  // the converts back to string and append units if used
      
      // The path.points as a SVG string is converted back to array of points
      // the array is assigned to point and then converted back to a points string
      svg.polygon.points = svg.polygon.points.map(point => (point[0] += 10, point[1] += 10, point));    
      
      // get polygon node "stroke-width" converts to Number add 2 and sets new value
      svg.polygon.strokeWidth += 2;
      // change the fill.
      svg.polygon.fill = "Green";
      
      // Append a new circle object to the svg
      svg.circle = createSVG("circle");
      svg.circle.cx = svg.width / 2;
      svg.circle.cy = svg.height / 2;
      svg.circle.r = Math.min(svg.width, svg.height) / 3;
      svg.circle.fill = "orange";

      // Example of setting node content      
      svg.text = createSVG("text",{x : 25, y : 20, fontFamily : "Verdana", fontSize : "10", fill : "white"});
      // Each PSVG object has node property that is the actual XML node and its properties
      // can be set directly
      svg.text.node.textContent = "Some text.";
      

      // Animate circle
      requestAnimationFrame(animate);
    },2000);

    //=========================================================================
    /* JAVASCRIPT driven animation of SVG */
    function animate(time){
        var x = svg.width / 2
        var y = svg.height / 2
        var rad = Math.cos(time / 2000) * Math.min(x,y) * (1/4) + Math.min(x,y)  * (1/2);
        svg.circle.r = rad;
        x += Math.cos(time / 1000) * rad * (1/3);
        y += Math.sin(time / 1000) * rad * (1/3);
        svg.circle.cx = x;
        svg.circle.cy = y;        
        XMLResult.textContent = exampleSVG.innerHTML;
        requestAnimationFrame(animate);   
    }
},0)




/* =============================================================================
createSVG is module for review 
===============================================================================*/
const createSVG = (()=>{
    /* This code uses some abreviations
       str is string
       arr is array
       num is number
       prop is property
       props is properties
       2 for conversion eg str2Num is string to number
    */
    var   id = 0;
    var   units = "";
    const svgNamespace = "http://www.w3.org/2000/svg";
    const transformTypes = {read : "read", write : "write"};
    const transformPropsName = "accent-height,alignment-baseline,arabic-form,baseline-shift,cap-height,clip-path,clip-rule,color-interpolation,color-interpolation-filters,color-profile,color-rendering,dominant-baseline,enable-background,fill-opacity,fill-rule,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,horiz-adv-x,horiz-origin-x,image-rendering,letter-spacing,lighting-color,marker-end,marker-mid,marker-start,overline-position,overline-thickness,panose-1,paint-order,pointer-events,rendering-intent,shape-rendering,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,text-anchor,text-decoration,text-rendering,underline-position,underline-thickness,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,vert-adv-y,vert-origin-x,vert-origin-y,word-spacing,writing-mode,x-height";
    const unitPropsNames ="width,height,x,y,z,x1,x2,y1,y2,cx,cy,rx,ry,r,accentHeight,alignmentBaseline,baselineShift,capHeight,fontSize,fontSizeAdjust,overlinePosition,overlineThickness,strikethroughPosition,strikethroughThickness,strokeWidth,underlinePosition,underlineThickness,vertOriginX,vertOriginY,wordSpacing,xHeight";
   
    /* Transform helper functions */
    const onlyArr2Str = (value, points = false) => {
        if (points) {
            if (Array.isArray(value)) {
                return value.map(point => Array.isArray(point) ? point.join(",") : point).join(" ");
            }
            return value;
        }
        return Array.isArray(value) ? value.join(" ") : value
    }
    
    /* Value transform functions */
    const arr2Str      = value => onlyArr2Str(value);
    const str2NumArr   = value => value.split(" ").map(value => Number(value));
    const unitStr2Num  = value => Number(value.replace(/[a-z]/gi, ""));
    const str2Num      = value => Number(value);
    const str2NumOrStr = value => isNaN(value) ? value : Number(value);
    const num2UnitStr  = value => value + units;
    const num2Percent  = value => value + "%";
    const pointArr2Str = value => onlyArr2Str(value, true);
    const url2Str      = value => value.replace(/url\(#|)/g, "");
    const ref2Url      = value => {
        if (typeof value === "string") {
            if (value.indexOf("url(#") > -1) { return value }
            return `url(#${value})`;
        }
        if (value.isPSVG) {
            if (value.node.id) { return `url(#${value.node.id})` }
            value.node.id = "PSVG_ID_"+ (id ++);
            return `url(#${value.node.id})`;
        }
        return value;
    };
    const str2PointArr = value => value.split(" ").map(point => {
        point = point.split(",");
        point[0] = Number(point[0]);
        point[1] = Number(point[1]);
        return point;
    });
    
    /* property value transforms `read` from SVG `write` to SVG */
    const transforms = {
        read : {
            offset      : unitStr2Num,
            points      : str2PointArr,
            filter      : url2Str,
            clipPath    : url2Str,
            stdDeviation: str2Num,
            dx          : str2Num,  
            dy          : str2Num, 
            tableValues : str2NumArr,
            values      : str2NumArr,
            kernelMatrix: str2NumArr,
            viewbox     : str2NumArr,
            _default    : str2NumOrStr, 
        },
        write : {
            points      : pointArr2Str,
            offset      : num2Percent,
            filter      : ref2Url,
            clipPath    : ref2Url,
            tableValues : arr2Str,
            values      : arr2Str,
            kernelMatrix: arr2Str,
            viewbox     : arr2Str,
            _default(value) { return value },
        },
    }
    
    /* Assign additional unit value transforms */
    unitPropsNames.split(",").forEach((propName) => {
        transforms.read[propName] = unitStr2Num;
        transforms.write[propName] = num2UnitStr;
    });
    
    /* Create property name transform lookups */
    const propNodeNames = transformPropsName.split(",");
    const propScriptNames = transformPropsName.replace(/-./g, str => str[1].toUpperCase()).split(",");

    /* returns transformed `value` of associated property `name`  depending on `[type]` default write*/
    function transform(name, value, type = transformTypes.write) {
        return transforms[type][name] ? transforms[type][name](value) : transforms[type]._default(value);
    }
    
    /* returns Transformed JavaScript property name as SVG property name if needed. EG "fillRule" >> "fill-rule" */
    function propNameTransform(name) {
        const index = propScriptNames.indexOf(name);
        return index === -1 ? name : propNodeNames[index];
    }
    
    /* node creation function returned as the interface instanciator of the node proxy */
    /* type String representing the node type.
       props optional Object containing node properties to set
       returns a proxy holding the node */
    const createSVG = (type, props = {}) => {
        const PSVG = (()=>{  // PSVG is abreviation for Practical SVG 
            const node = document.createElementNS(svgNamespace, type);
            const set  = (name, value) => node.setAttribute(propNameTransform(name), transform(name, value));
            const get  = (name, value) => transform(name, node.getAttribute(propNameTransform(name)), transformTypes.read);
            const svg  = {
                isPSVG   : true,
                nodeType : type,
                node     : node,
                set units(postFix) { units = postFix },
                get units() { return units },
            };
            const proxyHandler = {
                get(target, name) { return svg[name] !== undefined ? target[name] : get(name) },
                set(target, name, value) {
                    if (value !== null && typeof value === "object" && value.isPSVG) {
                        node.appendChild(value.node);
                        target[name] = value;
                        return true;
                    }
                    set(name,value);
                    return true;
                },
            };
            return new Proxy(svg, proxyHandler);
        })();
        Object.keys(props).forEach(key => PSVG[key] = props[key]);
        return PSVG;
    }
    return createSVG;
})();
body {
  font-family : arial;
}
#XMLResult {
  font-family : consola;
  font-size : 12px;
}
<span id="infoElement">SVG node added. In two seconds is modified.</span><br>
<div id="exampleSVG"></div>
The XML;
<div id="XMLResult"></div>

Известная проблема

  • Медленно, обработчиков, скрывать многое из исполняемого кода.
  • Ссылка убыток. Назначение узлов в настоящее время не проверить существующие и добавляет узлы. Любую существующую ссылку с именем узла теряется. например svg.a = createSVG("a"); svg.a = createSVG("a"); создает два узла, но только одна ссылка.
  • Неполное значение свойства преобразований. Неизвестные свойства назначаются как.
  • Очень мало тестов, я написал его на выходных.


128
2
задан 31 января 2018 в 06:01 Источник Поделиться
Комментарии