你能控制SVG的描边宽度如何绘制吗?

278
目前正在构建一个基于浏览器的SVG应用程序。在这个应用程序中,用户可以对各种形状进行样式和定位,包括矩形。
当我将stroke-width应用于SVG的rect元素时,例如1px,不同的浏览器以不同的方式应用于rect的偏移和插入。这证明是麻烦的,尤其是当我尝试计算矩形的外部宽度和视觉位置,并将其放置在其他元素旁边时。
例如:
- Firefox在底部和左侧添加1px的插入,顶部和右侧添加1px的偏移。 - Chrome在顶部和左侧添加1px的插入,底部和右侧添加1px的偏移。
到目前为止,我唯一的解决方案可能是自己绘制实际的边框(可能使用路径工具),并将边框放在描边元素后面。但这个解决方案是一个不愉快的权宜之计,如果可能的话,我宁愿不走这条路。
所以我的问题是,你能控制SVG的stroke-width如何在元素上绘制吗?

有一些过滤器技巧可以用来实现这个目标,但这并不是一个很好的解决方案。 - Michael Mullany
5
有一个名为 paint-order 的参数,您可以在其中指定填充应该呈现在描边的顶部,这样您就会得到 "外部对齐",请参见 https://jsfiddle.net/hne0kyLg/1/。 - Ivan Kuckir
发现一种使用CSS“outline-”属性的方法:https://codepen.io/badcat/pen/YVzmYY。不确定各个浏览器对此的支持情况,但可能会有用。 - Bernardo Loureiro
SVG 2 还引入了新的 paint-order 属性(Chrome 正在进行 SVG 2 实现,详情请见此处)。 - Mahozad
14个回答

1

我自己在这个话题中读过答案,因为我也在寻找解决方案。在我的情况下,我无法直接编辑内联的SVG,所以我需要用外部CSS来绘制描边。这样做会导致描边不完全可见,因为它是在路径的外部绘制的。路径变得比视口大,所以它将被隐藏。

解决这个问题的一个简单方法是在svg本身上添加 overflow: visible;。您可以添加一个填充,大小为stroke-width的一半,使其恢复到原始大小。

svg {
  width: 80px;
  height: 80px;
  
  fill: transparent;
  
  & > * {
    stroke: black;
    stroke-width: 10px;
  }
}

svg.stroke-visible {
  overflow: visible;
  padding: 5px; //Half the stroke-width
}
Stroke not fully visible:
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="800" height="800" version="1.0" viewBox="0 0 64 64">
<path d="M62.799 23.737a3.941 3.941 0 0 0-3.139-2.642l-16.969-2.593-7.622-16.237a3.938 3.938 0 0 0-7.13 0l-7.623 16.238-16.969 2.593a3.937 3.937 0 0 0-2.222 6.642l12.392 12.707-2.935 17.977a3.94 3.94 0 0 0 5.797 4.082l15.126-8.365 15.126 8.365a3.94 3.94 0 0 0 5.796-4.082l-2.935-17.977 12.393-12.707a3.942 3.942 0 0 0 .914-4.001z"/>
 </svg>
 
 Stroke fully visible:
 
 <svg class="stroke-visible" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="800" height="800" version="1.0" viewBox="0 0 64 64">
<path d="M62.799 23.737a3.941 3.941 0 0 0-3.139-2.642l-16.969-2.593-7.622-16.237a3.938 3.938 0 0 0-7.13 0l-7.623 16.238-16.969 2.593a3.937 3.937 0 0 0-2.222 6.642l12.392 12.707-2.935 17.977a3.94 3.94 0 0 0 5.797 4.082l15.126-8.365 15.126 8.365a3.94 3.94 0 0 0 5.796-4.082l-2.935-17.977 12.393-12.707a3.942 3.942 0 0 0 .914-4.001z"/>
 </svg>


1

2023更新:当前草案将属性改名为stroke-align

2023年浏览器支持:

请参见caniuse

这个CSS属性在任何现代浏览器中都不受支持,也没有已知的支持计划。

类似Polyfill的辅助函数

基于之前结合使用paint-ordermaskclip-path的方法。
(建议来自@Xavier Ho@Jorg Janke

//emulateStrokeAlign();

function emulateStrokeAlign() {
  let supportsSvgStrokeAlign = CSS.supports("stroke-align", "outer") ?
    true :
    CSS.supports("stroke-alignment", "outer") ?
    true :
    false;

  console.log("supportsSvgStrokeAlign", supportsSvgStrokeAlign);

  if (!supportsSvgStrokeAlign) {
    let ns = "http://www.w3.org/2000/svg";
    let strokeAlignmentEls = document.querySelectorAll(
      "*[stroke-alignment], *[stroke-align]"
    );
    strokeAlignmentEls.forEach((el, s) => {
      let svg = el.closest("svg");
      // set auto ids to prevent non-unique mask ids
      let svgID = svg.id ? svg.id : "svg_" + s;
      svg.id = svgID;

      //create <defs> if not previously appended
      let defs = svg.querySelector("defs");
      if (!defs) {
        defs = document.createElementNS(ns, "defs");
        svg.insertBefore(defs, svg.children[0]);
      }

      let style = window.getComputedStyle(el);
      let strokeWidth = parseFloat(style.strokeWidth);
      let strokeAlignment = el.getAttribute("stroke-alignment") ?
        el.getAttribute("stroke-alignment") :
        el.getAttribute("stroke-align");
      el.removeAttribute("stroke-align");
      el.removeAttribute("stroke-alignment");
      el.setAttribute("data-stroke-align", strokeAlignment);
      let maskClipId = `mask-${svgID}-${s}`;

      if (strokeAlignment === "outer") {
        // create mask
        let mask = document.createElementNS(ns, "mask");
        mask.id = maskClipId;
        let maskEl = el.cloneNode();
        mask.appendChild(maskEl);
        defs.appendChild(mask);
        maskEl.setAttribute("fill", "#000");
        mask.setAttribute("maskUnits", "userSpaceOnUse");
        maskEl.setAttribute("stroke", "#fff");
        maskEl.removeAttribute("stroke-opacity");
        maskEl.removeAttribute("id");
        maskEl.setAttribute("paint-order", "stroke");
        maskEl.style.strokeWidth = strokeWidth * 2;

        // clone stroke
        let cloneStroke = el.cloneNode();
        cloneStroke.style.fill = "none";
        cloneStroke.style.strokeWidth = strokeWidth * 2;
        cloneStroke.removeAttribute("id");
        cloneStroke.removeAttribute("stroke-alignment");
        cloneStroke.classList.add("cloneStrokeOuter");
        cloneStroke.setAttribute("mask", `url(#${maskClipId})`);
        el.parentNode.insertBefore(cloneStroke, el.nextElementSibling);
        //remove stroke from original element
        el.style.stroke = "none";
      }

      if (strokeAlignment === "inner") {
        //create clipPath
        let clipPathEl = el.cloneNode();
        let clipPath = document.createElementNS(ns, "clipPath");
        clipPath.id = maskClipId;
        defs.appendChild(clipPath);
        clipPathEl.removeAttribute("id");
        clipPath.appendChild(clipPathEl);
        el.setAttribute("clip-path", `url(#${maskClipId})`);
        el.style.strokeWidth = strokeWidth * 2;
      }
    });
  }
}
body {
  margin: 2em;
}

svg {
  width: 100%;
  height: auto;
  overflow: visible;
  border: 1px solid #ccc;
}

body {
  margin: 2em;
}

svg {
  height: 20em;
  overflow: visible;
  border: 1px solid #ccc;
}
<p><button onclick="emulateStrokeAlign()">Emulate stroke align</button></p>

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 120">
  <g id="myGroup" style="fill:rgb(45, 130, 255); stroke:#000; stroke-width:10; stroke-opacity:1;">
    <rect id="el1" stroke-alignment="outer" x="10" y="10" width="100" height="100" />
    <rect id="el2" x="140" y="10" width="100" height="100" />
    <rect id="el3" stroke-alignment="inner" x="270" y="10" width="100" height="100" />
  </g>
</svg>

<svg viewBox="0 0 12 6" xmlns="http://www.w3.org/2000/svg" stroke-width="0.5">
  <path d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5z" fill="blue" stroke-align="outer" stroke="red" stroke-opacity="0.5" stroke-linecap="butt" />
  <path d="M7,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5z" fill="blue" stroke-align="inner" stroke="red" stroke-opacity="0.5" />
</svg>

通过{{link1:paper.js offset glenzli's plugin}}硬编码偏移量

这种方法实际上会增加/缩小您的<path>元素,以获得所需的描边位置(使用默认的中间描边对齐方式)。

const canvas = document.createElement("canvas");
canvas.style.display='none';
document.body.appendChild(canvas);
//const canvas = document.querySelector("canvas");
paper.setup(canvas);

let strokeEls = document.querySelectorAll("*[stroke-alignment]");
strokeEls.forEach((el,i) => {
  let type = el.nodeName;
  let style = window.getComputedStyle(el);
  let strokeWidth = parseFloat(style.strokeWidth);
  let strokeAlignment = el.getAttribute('stroke-alignment');
  let offset = strokeAlignment==='outer' ? strokeWidth/2 : (strokeAlignment==='inner' ? strokeWidth / -2 : 0); 
  // convert primitive
  if(type!=='path'){
    el = convertPrimitiveToPath(el);
  }
  let d = el.getAttribute("d");
  let polyPath = new paper.Path(el.getAttribute("d"));
  let dOffset = offset ? PaperOffset.offset(polyPath, offset)
    .exportSVG()
    .getAttribute("d") : d;
  el.setAttribute("d", dOffset);
});
body{
  margin:2em;
}

svg{
  width:100%;
  overflow:visible;
  border:1px solid #ccc;
}
<svg viewBox="0 0 12 6" xmlns="http://www.w3.org/2000/svg" stroke-width="0.5">
  <path d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" stroke="black" fill="none" stroke-linejoin="miter"/>
  <path d="M1,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" fill="none" stroke-linejoin="miter" stroke-alignment="outer" stroke="red" stroke-opacity="0.5" />
  <path d="M7,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" stroke="black" fill="none" stroke-linejoin="round" />
  <path d="M7,5 a2,2 0,0,0 2,-3 a3,3 0 0 1 2,3.5" fill="none" stroke-linejoin="round" stroke-alignment="inner" stroke="red" stroke-opacity="0.5" />
</svg>

<script src="https://unpkg.com/paper@0.12.15/dist/paper-full.min.js"></script>
<script src="https://unpkg.com/paperjs-offset@1.0.8/dist/paperjs-offset.js"></script>

然而,该库在处理复杂形状时存在困难。


0

Zavier Ho的解决方案是将描边宽度加倍并更改绘画顺序,非常出色,但仅适用于填充为纯色且没有透明度的情况。

我开发了另一种方法,更复杂,但适用于任何填充。它也适用于椭圆或路径(对于后者有一些奇怪行为的角落案例,例如相交的开放路径,但不多)。

诀窍是在两个图层中显示形状。一个只有填充而没有描边,另一个则仅具有双倍宽度的描边(透明填充),并通过遮罩传递,显示整个形状,但隐藏没有描边的原始形状。

  <svg width="240" height="240" viewBox="0 0 1024 1024">
  <defs>
    <path id="ld" d="M256,0 L0,512 L384,512 L128,1024 L1024,384 L640,384 L896,0 L256,0 Z"/>
    <mask id="mask">
      <use xlink:href="#ld" stroke="#FFFFFF" stroke-width="160" fill="#FFFFFF"/>
      <use xlink:href="#ld" fill="#000000"/>
    </mask>
  </defs>
  <g>
    <use xlink:href="#ld" fill="#00D2B8"/>
    <use xlink:href="#ld" stroke="#0081C6" stroke-width="160" fill="red" mask="url(#mask)"/>
  </g>
  </svg>

0
这对我有用:

这个方法适用于我:

.btn {
 border: 1px solid black;
 box-shadow: inset 0 0 0 1px black;
}

你不能将CSS属性borderbox-shadow应用于SVG元素,如<rect><path><circle>等。 - herrstrietzel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接