您可以控制 SVG 描边宽度的绘制方式吗?

发布于 2024-12-01 20:10:59 字数 515 浏览 2 评论 0 原文

目前正在构建一个基于浏览器的 SVG 应用程序。在此应用程序中,用户可以设置各种形状的样式和位置,包括矩形。

当我将 lines-width 应用于 1px 的 SVG rect 元素时,笔划将应用于 rect > 不同浏览器的偏移和插入方式不同。事实证明,这很麻烦,尤其是当我尝试计算矩形的外部宽度和视觉位置并将其放置在其他元素旁边时。

例如:

  • Firefox 添加 1px 插入(底部和左侧)和 1px 偏移(顶部和右侧)
  • Chrome 添加 1px 插入(顶部和左侧)和 1px 偏移(底部和右侧)

到目前为止我唯一的解决方案是绘制实际的我自己设置边框(可能使用 path 工具)并将边框放置在描边元素后面。但这个解决方案是一个令人不愉快的解决方法,如果可能的话,我不想走这条路。

所以我的问题是,你能控制 SVG 的描边宽度如何在元素上绘制吗?

Currently building a browser-based SVG application. Within this app, various shapes can be styled and positioned by the user, including rectangles.

When I apply a stroke-width to an SVG rect element of say 1px, the stroke is applied to the rect’s offset and inset in different ways by different browsers. This is proving to be troublesome, especially when I try to calculate the outer width and visual position of a rectangle and position it next to other elements.

For example:

  • Firefox adds 1px inset (bottom and left), and 1px offset (top and right)
  • Chrome adds 1px inset (top and left), and 1px offset (bottom and right)

My only solution so far would be to draw the actual borders myself (probably with the path tool) and position the borders behind the stroked element. But this solution is an unpleasant workaround, and I’d prefer not to go down this road if possible.

So my question is, can you control how an SVG’s stroke-width is drawn on elements?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(14

街角迷惘 2024-12-08 20:10:59

不可以,您无法指定笔划是在元素内部还是外部绘制。为此,我向 SVG 工作组提出了提案该功能于 2003 年推出,但没有得到支持(或讨论)。

SVG 建议的笔画位置示例,来自 phrogz.net/SVG/Stroke-location.svg

正如我在提案中指出的那样,

  • 通过将描边宽度加倍,然后使用剪切路径将对象剪辑到自身,您可以实现与“内部”相同的视觉结果;
  • 通过将描边宽度加倍,然后使用剪切路径,您可以实现与“外部”相同的视觉结果将对象的无笔画副本覆盖在其自身之上。

编辑:这个答案将来可能是错误的。使用 SVG 矢量效果应该可以实现这些结果,通过将 veStrokePathveIntersect(用于“内部”)或与 veExclude(用于“外部”)相结合。然而,矢量效果仍然是一个工作草案模块,我还没有找到任何实现。

编辑 2:SVG 2 草案规范包括 笔划-alignment 属性(具有 center|inside|outside 可能值)。该属性最终可能会进入 UA。

编辑 3:有趣又令人失望的是,SVG 工作组已从 SVG 2 中删除了 笔划对齐。您可以看到散文 此处

No, you cannot specify whether the stroke is drawn inside or outside an element. I made a proposal to the SVG working group for this functionality in 2003, but it received no support (or discussion).

SVG proposed stroke-location example, from phrogz.net/SVG/stroke-location.svg

As I noted in the proposal,

  • you can achieve the same visual result as "inside" by doubling your stroke width and then using a clipping path to clip the object to itself, and
  • you can achieve the same visual result as 'outside' by doubling the stroke width and then overlaying a no-stroke copy of the object on top of itself.

Edit: This answer may be wrong in the future. It should be possible to achieve these results using SVG Vector Effects, by combining veStrokePath with veIntersect (for 'inside') or with veExclude (for 'outside). However, Vector Effects are still a working draft module with no implementations that I can yet find.

Edit 2: The SVG 2 draft specification includes a stroke-alignment property (with center|inside|outside possible values). This property may make it into UAs eventually.

Edit 3: Amusingly and dissapointingly, the SVG working group has removed stroke-alignment from SVG 2. You can see some of the concerns described after the prose here.

憧憬巴黎街头的黎明 2024-12-08 20:10:59

我找到了一种简单的方法,它有一些限制,但对我有用:

  • 在 defs 中定义形状
  • 定义引用形状的剪辑路径
  • 使用它并在外部被剪辑时将笔划加倍

这里有一个工作示例:

<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"/>
	<clipPath id="clip">
		<use xlink:href="#ld"/>
	</clipPath>
</defs>
<g>
	<use xlink:href="#ld" stroke="#0081C6" stroke-width="160" fill="#00D2B8" clip-path="url(#clip)"/>
</g>
</svg>

I found an easy way, which has a few restrictions, but worked for me:

  • define the shape in defs
  • define a clip path referencing the shape
  • use it and double the stroke with as the outside is clipped

Here a working example:

<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"/>
	<clipPath id="clip">
		<use xlink:href="#ld"/>
	</clipPath>
</defs>
<g>
	<use xlink:href="#ld" stroke="#0081C6" stroke-width="160" fill="#00D2B8" clip-path="url(#clip)"/>
</g>
</svg>

心欲静而疯不止 2024-12-08 20:10:59

更新: lines-alignment 属性 于 4 月发布2015 年 1 月 1 日,采用了名为 SVG Strokes 的全新规范。

截至 2015 年 2 月 26 日的 SVG 2.0 编辑草案(可能自 2 月 13 日起) , <删除>stroke-alignment 属性存在 以及值内部中心(默认)外部

它的工作方式似乎与 @Phrogz 和后来的 笔画位置建议。该房产至少从 2011 年就开始规划,但除了注释说

SVG 2 应包含一种指定笔划位置的方法

,它从未在规范中详细说明,因为它被推迟了 - 直到现在,似乎如此。

目前还没有浏览器支持此属性,或者据我所知,支持任何新的 SVG 2 功能,但希望随着规范的成熟,它们会很快支持。这是我个人一直敦促拥有的属性,我真的很高兴它终于出现在规范中。

关于该属性在开放路径和循环上的表现似乎存在一些问题。这些问题很可能会延长跨浏览器的实现时间。但是,当浏览器开始支持此属性时,我将用新信息更新此答案。

UPDATE: The stroke-alignment attribute was on April 1st, 2015 moved to a completely new spec called SVG Strokes.

As of the SVG 2.0 Editor’s Draft of February 26th, 2015 (and possibly since February 13th), the stroke-alignment property is present with the values inner, center (default) and outer.

It seems to work the same way as the stroke-location property proposed by @Phrogz and the later stroke-position suggestion. This property has been planned since at least 2011, but apart from an annotation that said

SVG 2 shall include a way to specify stroke position

, it has never been detailed in the spec as it was deferred - until now, it seems.

No browser support this property, or, as far as I know, any of the new SVG 2 features, yet, but hopefully they will soon as the spec matures. This has been a property I personally have been urging to have, and I'm really happy that it's finally there in the spec.

There seems to be some issues as to how to the property should behave on open paths as well as loops. These issues will, most probably, prolong implementations across browsers. However, I will update this answer with new information as browsers begin to support this property.

鸵鸟症 2024-12-08 20:10:59

您可以使用 CSS 来设置描边和填充顺序的样式。即先描边,再填充,即可得到想要的效果。

MDN 关于 paint-order https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/paint-order

CSS 代码:

paint-order: stroke;

You can use CSS to style the order of stroke and fills. That is, stroke first and then fill second, and get the desired effect.

MDN on paint-order: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/paint-order

CSS code:

paint-order: stroke;
身边 2024-12-08 20:10:59

这是一个函数,它将计算您需要添加多少像素(使用给定的笔划)到顶部、右侧、底部和左侧,所有这些都基于浏览器:

var getStrokeOffsets = function(stroke){

        var strokeFloor =       Math.floor(stroke / 2),                                                                 // max offset
            strokeCeil =        Math.ceil(stroke / 2);                                                                  // min offset

        if($.browser.mozilla){                                                                                          // Mozilla offsets

            return {
                bottom:     strokeFloor,
                left:       strokeFloor,
                top:        strokeCeil,
                right:      strokeCeil
            };

        }else if($.browser.webkit){                                                                                     // WebKit offsets

            return {
                bottom:     strokeCeil,
                left:       strokeFloor,
                top:        strokeFloor,
                right:      strokeCeil
            };

        }else{                                                                                                          // default offsets

            return {
                bottom:     strokeCeil,
                left:       strokeCeil,
                top:        strokeCeil,
                right:      strokeCeil
            };

        }

    };

Here's a function that will calculate how many pixels you need to add - using the given stroke - to the top, right, bottom and left, all based on the browser:

var getStrokeOffsets = function(stroke){

        var strokeFloor =       Math.floor(stroke / 2),                                                                 // max offset
            strokeCeil =        Math.ceil(stroke / 2);                                                                  // min offset

        if($.browser.mozilla){                                                                                          // Mozilla offsets

            return {
                bottom:     strokeFloor,
                left:       strokeFloor,
                top:        strokeCeil,
                right:      strokeCeil
            };

        }else if($.browser.webkit){                                                                                     // WebKit offsets

            return {
                bottom:     strokeCeil,
                left:       strokeFloor,
                top:        strokeFloor,
                right:      strokeCeil
            };

        }else{                                                                                                          // default offsets

            return {
                bottom:     strokeCeil,
                left:       strokeCeil,
                top:        strokeCeil,
                right:      strokeCeil
            };

        }

    };
羅雙樹 2024-12-08 20:10:59

正如上面的人所指出的,您要么必须重新计算笔划路径坐标的偏移量,要么将其宽度加倍,然后遮盖一侧或另一侧,因为不仅 SVG 本身不支持 Illustrator 的笔划对齐,而且 PostScript 也不支持。

Adobe PostScript 手册第二版中的笔划规范指出:
4.5.1 抚摸:
描边运算符沿着当前路径绘制一条一定粗细的线。对于路径中的每个直线或曲线段,描边会绘制一条以该段为中心且边与该段平行的线。" (强调他们的)

规范的其余部分没有用于偏移线位置的属性。当 Illustrator 让您在内部或外部对齐时,它会重新计算实际路径的偏移(因为它在计算上仍然比叠印路径更便宜)。 .ai 文档中的坐标是参考,而不是光栅化或导出为最终格式的内容,

因为 Inkscape 的本机格式是规范 SVG,因此它无法提供规范所缺乏的功能。

As people above have noted you'll either have to recalculate an offset to the stroke's path coordinates or double its width and then mask one side or the other, because not only does SVG not natively support Illustrator's stroke alignment, but PostScript doesn't either.

The specification for strokes in Adobe's PostScript Manual 2nd edition states:
"4.5.1 Stroking:
The stroke operator draws a line of some thickness along the current path. For each straight or curved segment in the path, stroke draws a line that is centered on the segment with sides parallel to the segment." (emphasis theirs)

The rest of the specification has no attributes for offsetting the line's position. When Illustrator lets you align inside or outside, it's recalculating the actual path's offset (because it's still computationally cheaper than overprinting then masking). The path coordinates in the .ai document are reference, not what gets rastered or exported to a final format.

Because Inkscape's native format is spec SVG, it can't offer a feature the spec lacks.

锦爱 2024-12-08 20:10:59

2023 年更新:当前草案将该属性重命名为 < code>lines-align

浏览器支持 2023:

请参阅 caniuse

任何现代浏览器都不支持此 CSS 属性,也不支持
有任何已知的计划来支持它。

类似 Polyfill 的辅助函数

基于之前组合 paint-ordermaskclip-path 的方法。
(正如@Xavier Ho
@Jorg Janke

这个助手/polyfill 实际上复制了 Adob​​e Illustrator 等应用程序的行为。

function emulateStrokeAlign() {
  // if natively supported quit
  let supportsSvgStrokeAlign = CSS.supports("stroke-align", "outer");
  if (supportsSvgStrokeAlign) return false;

  let ns = "http://www.w3.org/2000/svg";
  let strokeAlignmentEls = document.querySelectorAll("*[stroke-align]");

  for (let s = 0, len = strokeAlignmentEls.length; len && s < len; s++) {
    let el = strokeAlignmentEls[s];
    let strokeAlignment = el.getAttribute("stroke-align");

    // create unique ID
    let maskId = `emulateStrokeAlignMask-${s}`;
    let clipId = `emulateStrokeAlignClip-${s}`;
    let fillCloneID = `emulateStrokeAlignFill-${s}`;
    let hasClip = document.getElementById(clipId);
    let hasMask = document.getElementById(maskId);
    let hasFillClone = document.getElementById(fillCloneID);
    
    let strokeWidthData = parseFloat(el.dataset.strokeWidth);
    let fillData = el.dataset.fill;
    
   //reset clip or mask 
    el.removeAttribute("mask");
    el.removeAttribute("clip-path");
    

    // stroke align center - quit
    if (strokeAlignment === "center" && !hasClip && !hasMask) continue;
    
    // get stroke properties
    let style = window.getComputedStyle(el);
    let strokeWidth = strokeWidthData ? strokeWidthData :  parseFloat(style.strokeWidth);
    let fill = fillData ? fillData : style.fill;
    
    
    if(strokeAlignment === "center"){
      el.removeAttribute('fill')
      el.removeAttribute('stroke')
      el.style.strokeWidth = strokeWidth
      el.style.fill = fill
      continue
    }
    
    
    
    let svg = el.closest("svg");

    // if path is not closed - quit
    let type = el.nodeName.toLowerCase();
    let isPath = type === "path";
    let d = el.getAttribute("d");
    let isClosed = d?.match(/z/gi)?.length >= 0 ? true : !isPath ? true : false;

    // open path - quit
    if (!isClosed) continue;

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


    // if element has fill - clone element
    let hasFill = style.fill !== "none";
    if (!hasFillClone && hasFill) {
      let cloneFill = el.cloneNode(true);
      cloneFill.removeAttribute("stroke");
      cloneFill.removeAttribute("stroke-align");
      cloneFill.style.stroke = "none";
      cloneFill.id = fillCloneID;
      el.parentNode.insertBefore(cloneFill, el);
    }

    // create clone for mask or clip path
    let cloneMaskElID = `emulateStrokeAlignMaskClone-${s}`;
    let cloneMaskEl = document.getElementById(cloneMaskElID);
    if (!cloneMaskEl) {
      cloneMaskEl = el.cloneNode(true);
      cloneMaskEl.id = cloneMaskElID;
      cloneMaskEl.removeAttribute("stroke-align");
      cloneMaskEl.setAttribute("fill-rule", "evenodd");
      cloneMaskEl.removeAttribute("fill");
      cloneMaskEl.removeAttribute("stroke-width");
      cloneMaskEl.removeAttribute("stroke");
      cloneMaskEl.style.stroke = '#fff'
      cloneMaskEl.style.strokeWidth = strokeWidth * 2+'px';
      cloneMaskEl.style.fill = '#000';
      cloneMaskEl.style.paintOrder = "stroke";

    }

    let cloneCliplID = `emulateStrokeAlignClipClone-${s}`;
    let cloneClipEl = document.getElementById(cloneCliplID);
    if (!cloneClipEl) {
      cloneClipEl = el.cloneNode(true);
      cloneClipEl.id = cloneCliplID;
      cloneClipEl.removeAttribute("stroke-align");
      cloneClipEl.removeAttribute("stroke-width");
      cloneClipEl.removeAttribute("fill");
      cloneClipEl.removeAttribute("stroke");
      cloneClipEl.style.clipRule="evenodd";
      cloneClipEl.style.removeProperty("stroke");
      cloneClipEl.style.removeProperty("fill");
      cloneClipEl.style.removeProperty("stroke-width");

    }
    

    if (strokeAlignment === "outer") {
      // create mask if not previously added
      let maskEl = document.getElementById(maskId);

      if (!maskEl) {
        maskEl = document.createElementNS(ns, "mask");
        maskEl.id = maskId;
        maskEl.setAttribute("maskUnits", "userSpaceOnUse");
        maskEl.append(cloneMaskEl);
        defs.append(maskEl);
      } 
      el.setAttribute("mask", `url(#${maskId})`);
    }

    if (strokeAlignment === "inner") {
      //create clipPath
      let clipEl = document.getElementById(clipId);
      //console.log(clipId, clipEl)
      if (!clipEl) {
        clipEl = document.createElementNS(ns, "clipPath");
        clipEl.id = clipId;
        clipEl.append(cloneClipEl);
        defs.append(clipEl);
      }

      el.setAttribute("clip-path", `url(#${clipId})`);
    }

    el.style.strokeWidth = strokeWidth * 2+'px';
    /**
     * save stroke width in data attribute
     * for toggling
     */
    if (!strokeWidthData) el.dataset.strokeWidth = strokeWidth;
    if (!fillData) el.dataset.fill = fill;
  }
}
body{
  font-family: sans-serif;
}

svg {
  display: block;
  outline: 1px solid #ccc;
  overflow: visible;
}

svg>text,
svg>circle,
svg>path{
  fill:#ccc;
  stroke: red;
  stroke-opacity:1;
}

label{
  user-select:none;
}
<fieldset>
  <legend>Stroke-align</legend>
  <label ><input class="inputAlign" type="radio" name="strokeAlign" value="center" checked>center (default)</label>
  <label ><input class="inputAlign" type="radio" name="strokeAlign" value="inner" >inner</label>
  <label ><input class="inputAlign" type="radio" name="strokeAlign" value="outer" >outer</label>
</fieldset>

<svg viewBox="0 0 2400 320">
  
  <!-- open path - do nothing! -->
  <path d="M 10 150
c 50 -50 100 -50 150 0
s 100 50 150 0" stroke-width="16" stroke-align="center"/>
  
  <!-- stroked path - open - do nothing!  -->
  <path
d="M 360 10
l 300 150
l -300 150" stroke-dasharray="50 50" stroke-width="16" stroke-align="center" />
  
  
  <!-- loop path -->
  <path 
d="M 850 20
c -200 0 29.9 398.2 129.9 225
s -359.8 -173.2 -259.8 0
s 329.9 -225 129.9 -225
z" stroke-width="16" stroke-align="center" fill-rule="evenodd"/>
  
  <!-- compound path -->
  <path
d="M 1200 10
a 150 150 0 110 300
a 150 150 0 110 -300
z
m 0 100
a 50 50 0 100 100
a 50 50 0 100 -100
z
"  stroke-width="16" stroke-align="center" fill="#ccc" />
  
  
  <!-- compound path with self intersecting inner -->
  <path
d="M 1600 10
a 150 150 0 110 300
a 150 150 0 110 -300
z
M 1605 70
c -120 0 17.94 238.92 77.94 135
s -215.88 -103.92 -155.88 0
s 197.94 -135 77.94 -135
z
"  stroke-width="16" stroke-align="center" fill="#ccc" />
  
  
  <circle cx="1950" cy="160" r="140" stroke-width="16" stroke-align="center"/>
  
  <text x="2150" y="290" font-size="320" font-weight="700" stroke-width="16" stroke-align="center">8</text>
  
</svg>


<script>
// sample UI
window.addEventListener('DOMContentLoaded', e=>{

let inputAlign = document.querySelectorAll(".inputAlign");
let strokeAlignmentEls = document.querySelectorAll("*[stroke-align]");

inputAlign.forEach((inp) => {
  inp.addEventListener("input", (e) => {
    let align = document.querySelector(".inputAlign:checked").value;

    //change alignment
    setStrokeAlign(strokeAlignmentEls, align);

    // emulate
    emulateStrokeAlign();
  });
});

function setStrokeAlign(els, align) {
  els.forEach((el) => {
    el.setAttribute("stroke-align", align);
  });
}

})
</script>

通过 paper.js offset glenzli 插件 进行硬编码偏移

这种方法实际上会增大/缩小你的 元素来获取所需的笔划位置(使用默认的中间笔划对齐)。

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/[email protected]/dist/paper-full.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/paperjs-offset.js"></script>

然而,图书馆在处理复杂的形状方面遇到了困难。

SVG 草稿文档中非常令人困惑的是关于几个“边缘情况”(例如开放或自相交路径)的问题注释。
事实上,SVG 功能应该适应开发人员/设计师已经从 Adob​​e Illustrator、Affinity Designer、Figma 等应用程序中了解的常见行为。

我还在 SVGWG github 存储库中发表了评论。
“‘笔画对齐’属性问题:为什么不调整图形应用程序的行为?”

Update 2023: The current draft renamed the attribute to stroke-align

Browser Support 2023:

See caniuse

This CSS property is not supported in any modern browser, nor are
there any known plans to support it.

Polyfill-like helper function

Based on the previous approaches to combine paint-order, mask and clip-path.
(As suggested by @Xavier Ho
@Jorg Janke)

This helper/polyfill actually replicates the behavior from applications like Adobe Illustrator.

function emulateStrokeAlign() {
  // if natively supported quit
  let supportsSvgStrokeAlign = CSS.supports("stroke-align", "outer");
  if (supportsSvgStrokeAlign) return false;

  let ns = "http://www.w3.org/2000/svg";
  let strokeAlignmentEls = document.querySelectorAll("*[stroke-align]");

  for (let s = 0, len = strokeAlignmentEls.length; len && s < len; s++) {
    let el = strokeAlignmentEls[s];
    let strokeAlignment = el.getAttribute("stroke-align");

    // create unique ID
    let maskId = `emulateStrokeAlignMask-${s}`;
    let clipId = `emulateStrokeAlignClip-${s}`;
    let fillCloneID = `emulateStrokeAlignFill-${s}`;
    let hasClip = document.getElementById(clipId);
    let hasMask = document.getElementById(maskId);
    let hasFillClone = document.getElementById(fillCloneID);
    
    let strokeWidthData = parseFloat(el.dataset.strokeWidth);
    let fillData = el.dataset.fill;
    
   //reset clip or mask 
    el.removeAttribute("mask");
    el.removeAttribute("clip-path");
    

    // stroke align center - quit
    if (strokeAlignment === "center" && !hasClip && !hasMask) continue;
    
    // get stroke properties
    let style = window.getComputedStyle(el);
    let strokeWidth = strokeWidthData ? strokeWidthData :  parseFloat(style.strokeWidth);
    let fill = fillData ? fillData : style.fill;
    
    
    if(strokeAlignment === "center"){
      el.removeAttribute('fill')
      el.removeAttribute('stroke')
      el.style.strokeWidth = strokeWidth
      el.style.fill = fill
      continue
    }
    
    
    
    let svg = el.closest("svg");

    // if path is not closed - quit
    let type = el.nodeName.toLowerCase();
    let isPath = type === "path";
    let d = el.getAttribute("d");
    let isClosed = d?.match(/z/gi)?.length >= 0 ? true : !isPath ? true : false;

    // open path - quit
    if (!isClosed) continue;

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


    // if element has fill - clone element
    let hasFill = style.fill !== "none";
    if (!hasFillClone && hasFill) {
      let cloneFill = el.cloneNode(true);
      cloneFill.removeAttribute("stroke");
      cloneFill.removeAttribute("stroke-align");
      cloneFill.style.stroke = "none";
      cloneFill.id = fillCloneID;
      el.parentNode.insertBefore(cloneFill, el);
    }

    // create clone for mask or clip path
    let cloneMaskElID = `emulateStrokeAlignMaskClone-${s}`;
    let cloneMaskEl = document.getElementById(cloneMaskElID);
    if (!cloneMaskEl) {
      cloneMaskEl = el.cloneNode(true);
      cloneMaskEl.id = cloneMaskElID;
      cloneMaskEl.removeAttribute("stroke-align");
      cloneMaskEl.setAttribute("fill-rule", "evenodd");
      cloneMaskEl.removeAttribute("fill");
      cloneMaskEl.removeAttribute("stroke-width");
      cloneMaskEl.removeAttribute("stroke");
      cloneMaskEl.style.stroke = '#fff'
      cloneMaskEl.style.strokeWidth = strokeWidth * 2+'px';
      cloneMaskEl.style.fill = '#000';
      cloneMaskEl.style.paintOrder = "stroke";

    }

    let cloneCliplID = `emulateStrokeAlignClipClone-${s}`;
    let cloneClipEl = document.getElementById(cloneCliplID);
    if (!cloneClipEl) {
      cloneClipEl = el.cloneNode(true);
      cloneClipEl.id = cloneCliplID;
      cloneClipEl.removeAttribute("stroke-align");
      cloneClipEl.removeAttribute("stroke-width");
      cloneClipEl.removeAttribute("fill");
      cloneClipEl.removeAttribute("stroke");
      cloneClipEl.style.clipRule="evenodd";
      cloneClipEl.style.removeProperty("stroke");
      cloneClipEl.style.removeProperty("fill");
      cloneClipEl.style.removeProperty("stroke-width");

    }
    

    if (strokeAlignment === "outer") {
      // create mask if not previously added
      let maskEl = document.getElementById(maskId);

      if (!maskEl) {
        maskEl = document.createElementNS(ns, "mask");
        maskEl.id = maskId;
        maskEl.setAttribute("maskUnits", "userSpaceOnUse");
        maskEl.append(cloneMaskEl);
        defs.append(maskEl);
      } 
      el.setAttribute("mask", `url(#${maskId})`);
    }

    if (strokeAlignment === "inner") {
      //create clipPath
      let clipEl = document.getElementById(clipId);
      //console.log(clipId, clipEl)
      if (!clipEl) {
        clipEl = document.createElementNS(ns, "clipPath");
        clipEl.id = clipId;
        clipEl.append(cloneClipEl);
        defs.append(clipEl);
      }

      el.setAttribute("clip-path", `url(#${clipId})`);
    }

    el.style.strokeWidth = strokeWidth * 2+'px';
    /**
     * save stroke width in data attribute
     * for toggling
     */
    if (!strokeWidthData) el.dataset.strokeWidth = strokeWidth;
    if (!fillData) el.dataset.fill = fill;
  }
}
body{
  font-family: sans-serif;
}

svg {
  display: block;
  outline: 1px solid #ccc;
  overflow: visible;
}

svg>text,
svg>circle,
svg>path{
  fill:#ccc;
  stroke: red;
  stroke-opacity:1;
}

label{
  user-select:none;
}
<fieldset>
  <legend>Stroke-align</legend>
  <label ><input class="inputAlign" type="radio" name="strokeAlign" value="center" checked>center (default)</label>
  <label ><input class="inputAlign" type="radio" name="strokeAlign" value="inner" >inner</label>
  <label ><input class="inputAlign" type="radio" name="strokeAlign" value="outer" >outer</label>
</fieldset>

<svg viewBox="0 0 2400 320">
  
  <!-- open path - do nothing! -->
  <path d="M 10 150
c 50 -50 100 -50 150 0
s 100 50 150 0" stroke-width="16" stroke-align="center"/>
  
  <!-- stroked path - open - do nothing!  -->
  <path
d="M 360 10
l 300 150
l -300 150" stroke-dasharray="50 50" stroke-width="16" stroke-align="center" />
  
  
  <!-- loop path -->
  <path 
d="M 850 20
c -200 0 29.9 398.2 129.9 225
s -359.8 -173.2 -259.8 0
s 329.9 -225 129.9 -225
z" stroke-width="16" stroke-align="center" fill-rule="evenodd"/>
  
  <!-- compound path -->
  <path
d="M 1200 10
a 150 150 0 110 300
a 150 150 0 110 -300
z
m 0 100
a 50 50 0 100 100
a 50 50 0 100 -100
z
"  stroke-width="16" stroke-align="center" fill="#ccc" />
  
  
  <!-- compound path with self intersecting inner -->
  <path
d="M 1600 10
a 150 150 0 110 300
a 150 150 0 110 -300
z
M 1605 70
c -120 0 17.94 238.92 77.94 135
s -215.88 -103.92 -155.88 0
s 197.94 -135 77.94 -135
z
"  stroke-width="16" stroke-align="center" fill="#ccc" />
  
  
  <circle cx="1950" cy="160" r="140" stroke-width="16" stroke-align="center"/>
  
  <text x="2150" y="290" font-size="320" font-weight="700" stroke-width="16" stroke-align="center">8</text>
  
</svg>


<script>
// sample UI
window.addEventListener('DOMContentLoaded', e=>{

let inputAlign = document.querySelectorAll(".inputAlign");
let strokeAlignmentEls = document.querySelectorAll("*[stroke-align]");

inputAlign.forEach((inp) => {
  inp.addEventListener("input", (e) => {
    let align = document.querySelector(".inputAlign:checked").value;

    //change alignment
    setStrokeAlign(strokeAlignmentEls, align);

    // emulate
    emulateStrokeAlign();
  });
});

function setStrokeAlign(els, align) {
  els.forEach((el) => {
    el.setAttribute("stroke-align", align);
  });
}

})
</script>

Hardcoded offset via paper.js offset glenzli's plugin

This approach will actually grow/shrink your <path> elements to get the desired stroke position (using the default middle stroke-alignment).

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/[email protected]/dist/paper-full.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/paperjs-offset.js"></script>

However, the library struggles with complex shapes.

What's quite confusing in the SVG drafts docs are the issue annotations about several "edge cases" like open or self-intersecting paths.
In fact the SVG feature should adapt the common behavior devs/designers already know from applications like Adobe Illustrator, Affinity Designer, Figma etc.

I also posted a comment in the SVGWG github repo.
"‘stroke-align’ property issues: why not adapt graphic apps behavior?"

送君千里 2024-12-08 20:10:59

这是使用 symboluse 解决内边框 rect 的方法。

示例https://jsbin.com/yopemiwame/edit?html,输出

SVG

<svg>
  <symbol id="inner-border-rect">
    <rect class="inner-border" width="100%" height="100%" style="fill:rgb(0,255,255);stroke-width:10;stroke:rgb(0,0,0)">
  </symbol>
  ...
  <use xlink:href="#inner-border-rect" x="?" y="?" width="?" height="?">
</svg>

注意:请务必将use中的?替换为实际值。

背景:之所以有效,是因为symbol通过用svg替换symbol并在shadow DOM中创建一个元素来建立一个新的视口。然后,影子 DOM 的 svg 会链接到您当前的 SVG 元素。请注意,svg 可以嵌套,并且每个 svg 都会创建一个新视口,该视口会剪辑重叠的所有内容,包括重叠边框。有关正在发生的事情的更详细概述,请阅读 Sara Soueidan 撰写的这篇精彩文章

Here is a work around for inner bordered rect using symbol and use.

Example: https://jsbin.com/yopemiwame/edit?html,output

SVG:

<svg>
  <symbol id="inner-border-rect">
    <rect class="inner-border" width="100%" height="100%" style="fill:rgb(0,255,255);stroke-width:10;stroke:rgb(0,0,0)">
  </symbol>
  ...
  <use xlink:href="#inner-border-rect" x="?" y="?" width="?" height="?">
</svg>

Note: Make sure to replace the ? in use with real values.

Background: The reason why this works is because symbol establishes a new viewport by replacing symbol with svg and creating an element in the shadow DOM. This svg of the shadow DOM is then linked into your current SVG element. Note that svgs can be nested and every svg creates a new viewport, which clips everything that overlaps, including the overlapping border. For a much more detailed overview of whats going on read this fantastic article by Sara Soueidan.

凡间太子 2024-12-08 20:10:59

我不知道这会有多大帮助,但就我而言,我只是创建了另一个仅带有边框的圆圈并将其放置在另一个形状的“内部”。

I don’t know how helpful will that be but in my case I just created another circle with border only and placed it “inside” the other shape.

无悔心 2024-12-08 20:10:59

一个(肮脏的)可能的解决方案是使用模式,

这里是一个带有内部描边三角形的示例:

https://jsfiddle .net/qr3p7php/5/

<style>
#triangle1{
  fill: #0F0;
  fill-opacity: 0.3;
  stroke: #000;
  stroke-opacity: 0.5;
  stroke-width: 20;
}
#triangle2{
  stroke: #f00;
  stroke-opacity: 1;
  stroke-width: 1;
}    
</style>

<svg height="210" width="400" >
    <pattern id="fagl" patternUnits="objectBoundingBox" width="2" height="1" x="-50%">
        <path id="triangle1" d="M150 0 L75 200 L225 200 Z">
    </pattern>    
    <path id="triangle2" d="M150 0 L75 200 L225 200 Z" fill="url(#fagl)"/>
</svg>

A (dirty) possible solution is by using patterns,

here is an example with an inside stroked triangle :

https://jsfiddle.net/qr3p7php/5/

<style>
#triangle1{
  fill: #0F0;
  fill-opacity: 0.3;
  stroke: #000;
  stroke-opacity: 0.5;
  stroke-width: 20;
}
#triangle2{
  stroke: #f00;
  stroke-opacity: 1;
  stroke-width: 1;
}    
</style>

<svg height="210" width="400" >
    <pattern id="fagl" patternUnits="objectBoundingBox" width="2" height="1" x="-50%">
        <path id="triangle1" d="M150 0 L75 200 L225 200 Z">
    </pattern>    
    <path id="triangle2" d="M150 0 L75 200 L225 200 Z" fill="url(#fagl)"/>
</svg>
忆梦 2024-12-08 20:10:59

Xavier 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>

The solution from Xavier Ho of doubling the width of the stroke and changing the paint-order is brilliant, although only works if the fill is a solid color, with no transparency.

I have developed other approach, more complicated but works for any fill. It also works in ellipses or paths (with the later there are some corner cases with strange behaviour, for example open paths that crosses theirselves, but not much).

The trick is to display the shape in two layers. One without stroke (only fill), and another one only with stroke at double width (transparent fill) and passed through a mask that shows the whole shape, but hides the original shape without stroke.

  <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>
金兰素衣 2024-12-08 20:10:59

我发现的最简单的方法是将剪辑路径添加到圆圈中

添加 clip-path="circle()"

然后描边宽度=“5 ” “ 会神奇地变成内在5 像素描边,绝对半径 100 像素。

The easiest way I found is to add clip-path into circle

Add clip-path="circle()"

<circle id="circle" clip-path="circle()" cx="100" cy="100" r="100" fill="none" stroke="currentColor" stroke-width="5" />

Then the stroke-width="5" will magically become inner 5px stroke with absolute 100px radius.

记忆之渊 2024-12-08 20:10:59

这对我有用:

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

This worked for me:

.btn {
 border: 1px solid black;
 box-shadow: inset 0 0 0 1px black;
}
誰ツ都不明白 2024-12-08 20:10:59

自从我自己寻找解决方案以来,我已经阅读了本主题中的答案。就我而言,我无法内联编辑 SVG,因此我需要使用外部 CSS 绘制笔画。这样做将使描边不完全可见,因为它是在路径的外部绘制的。路径变得比视图框更大,因此它将被隐藏。

解决此问题的一个简单方法是将 overflow:visible; 放在 svg 本身上。您可以添加描边宽度一半大小的填充,以使其达到原始大小。

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>

I've read the answers in this topic since I was looking for a solution myself. In my case I couldn't edit the SVG inline so I needed to draw the stroke with external CSS. Doing this will make the stroke not fully visible because it's drawn on the outside of the path. The path becomes bigger than the viewbox so it will be hidden.

A simple fix for this is to put overflow: visible; on the svg itself. You can add a padding with half the size as the stroke-width to make it the original size.

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>

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文