首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >使用代理简化SVG的创建和操作

使用代理简化SVG的创建和操作
EN

Code Review用户
提问于 2018-01-31 06:40:29
回答 1查看 121关注 0票数 2

A需要

我总是发现动态创建SVG内容,并过滤一个痛苦和混乱的过程。

  • 经常需要将JavaScript值转换为SVG和back,
  • 通过setAttributesetAttribute访问SVG属性会使代码变得一团糟。
  • 记住哪个属性名称使用哪个命名约定。例如clip-pathclipPathUnit是正确的,而clipPathclip-path-unit是不正确的。

由于这些和其他原因,我通常不使用SVG,但是由于HTML CanvasRenderingContext2D现在对filter属性有很好的支持,填补了API中一个急需的漏洞,我发现我自己写SVG的内容越来越多。

因此,在SVG代码中隐藏了一个特别讨厌的bug之后,我决定编写以下代码。

进行评审的代码.

代码语言:javascript
复制
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节点并返回保存该节点的对象的代理。代理getset处理程序很难在JavaScript友好格式和JavaScript格式之间转换属性名称和属性值,并根据属性类型执行正确的操作。

  • 单位值被转换到javascript编号和从javascript编号。eg "20px"变成20
  • 数组值被转换为适当的字符串和返回。
  • 如果需要"strokeWidth“成为”笔画宽度“,则将属性名称转换为SVG属性名。
  • 根据赋值类型适当地转换引用值。例如,引用svg.circle.filter = svg.blurFilter // blurfilter id。
  • 将节点分配给属性名称也会追加该节点。节点可以作为属性访问。例如svg.circle = createSVG("circle");一个新的圆节点被附加到svg节点。svg.circle.r正确地访问圆节点的radius属性。

示例用法

代码语言:javascript
复制
 //=====================================================
 // 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
 // 

 //=====================================================
 // 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);     

运行示例

下面的代码片段包含一个示例使用。创建一个SVG节点,添加到DOM,小延迟然后更新节点属性,添加其他节点,并启动Javascript控制的动画。

代码语言: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;
})();
代码语言:javascript
复制
body {
  font-family : arial;
}
#XMLResult {
  font-family : consola;
  font-size : 12px;
}
代码语言:javascript
复制
SVG node added. In two seconds is modified.


The XML;

已知问题

  • 慢一点,代理处理程序隐藏了大量执行的代码。
  • 参考损耗分配节点当前不测试现有节点和添加节点。任何现有的命名节点的引用都会丢失。例如,svg.a = createSVG("a"); svg.a = createSVG("a");创建两个节点,但只创建一个引用。
  • 不完全属性值转换。未知属性按原样分配。
  • 很小的测试,我在周末写的。
EN

回答 1

Code Review用户

回答已采纳

发布于 2018-06-01 19:22:43

通用反馈

我不能声称自己在SVG创建方面做了很多事情,但是我可以为创建SVG项的代码提供一些评论点。看起来,您已经做了很多工作来抽象出满足您的需求所需的通用SVG代码。对于没有重新分配的值,可以很好地使用const,对于其他变量,可以使用let。而且没有嵌套的三值算子是很好的。

复习点

有很多行检查参数是否是数组.例如,onlyArr2Str检查value是否为数组,如果是数组,则检查每个元素是否为数组(在.map()回调中)。函数似乎只调用两次(至少用示例代码调用),每次value都是一个数组。在某些情况下,情况并非如此?

只需将str2Num替换为Number即可--类似地,onlyArr2Str()中的这一行:

const str2NumArr =值=> value.split(“”).map(值=>数( value ));

可简化为:

代码语言:javascript
复制
const str2NumArr   = value => value.split(" ").map(Number);

为什么不为animatetransformpropNameTransform使用箭头函数语法?目标是将变量的范围保存在内部吗?

可以使用svg缩短代理返回的对象的至少一个属性(即PSVG()中的速记财产名称 ),因为属性名称与变量名称相同:

代码语言:javascript
复制
const svg  = {
    isPSVG   : true,
    nodeType : type,
    node,

如果对nodeType函数的第一个参数(即type)作了相应的重命名,也可能是如此。

这一行:

Object.keys(props).forEach(key => PSVG = props);

如果这是错误的,请纠正我,但这似乎可以通过使用Object.assign()来简化:

代码语言:javascript
复制
  Object.assign(PSVG, props);
票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/186411

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档