html 如何创建带边框的Squircle?

8xiog9wr  于 2023-06-20  发布在  其他
关注(0)|答案(2)|浏览(116)

有没有办法让squircle的边框、阴影或内部发光也符合方形?
houdini可以通过CSS paintWorklet创建一个squircle,但这非常poor support from browsers,使用率只有71.35%(2023),可能是因为security vulnerabilities关于油漆工作。
其他替代方案包括using an SVG <clipPath> with the squircle shape,但添加方形边框必须使用原始剪辑路径重新绘制,这使得转换变得困难。在缩放元素时,需要重新渲染笔划路径和剪辑路径。元素的CSS属性需要转换为路径数据。

<svg xmlns="http://www.w3.org/2000/svg" width="220" height="220" viewBox="-10 -10 220 220">
  <defs>
    <clipPath id="squircle-clip">
      <path d="M20,0
        L180,0
        Q200,0 200,20
        L200,180
        Q200,200 180,200
        L20,200
        Q0,200 0,180
        L0,20
        Q0,0 20,0"
        style="vector-effect: non-scaling-stroke;" 
      />
    </clipPath>
  </defs>
  <rect x="0" y="0" width="200" height="200" fill="#222" clip-path="url(#squircle-clip)"
  />
  <path d="M20,0
    L180,0
    Q200,0 200,20
    L200,180
    Q200,200 180,200
    L20,200
    Q0,200 0,180
    L0,20
    Q0,0 20,0" 
    fill="none" stroke="#484848" stroke-width="2" style="vector-effect: non-scaling-stroke;"
  />
</svg>

是否有其他方法来创建一个带边框的方形?Houdini是一个很难选择的解决方案,因为它只有约71%的用户,并且不支持Safari(iOS和macOS)或Firefox。

m4pnthwp

m4pnthwp1#

裁剪路径(以及蒙版)将裁剪笔划和滤镜

如果你不需要任何笔触或滤镜/效果,比如dropshadow,使用clip-paths效果很好。
如果你的最终目标是创建一个类似iOS的图标svg可能是你最好的选择

.resize {
  border: 1px solid #ccc;
  resize: both;
  overflow: auto;
  width: 50%;
  max-width: 50%;
}

svg {
  width: 100%;
}

.icon {
  fill: orange;
  stroke: #000;
  stroke-width: 2px;
  filter: drop-shadow(5px 5px 2px rgba(0, 0, 0, 0.75));
}
<h3>Resize me</h3>
<div class="resize">
  <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120">
    <path d="M 60 10 h 0 c 35.385 0 50 14.615 50 50 v 0 c 0 35.385 -14.615 50 -50 50 h 0 c -35.385 0 -50 -14.615 -50 -50 v 0 c 0 -35.385 14.615 -50 50 -50" />
  </svg>
</div>

上面的squircle是用我创建的一个简单的助手生成的:
codepen "Squircle generator (clothoid rounded corners)"
如果你需要动态创建一个squircle(例如在绘图应用程序中),你可以将svg <rect>与动态更新的<path>元素同步:

同步的<path>在调整大小时动态更新

您可以创建一个<path>作为圆形<rect>元素的“克隆”,共享其宽度,高度和其他属性。

rect是隐藏的,但会响应转换。

转换rect后,<path> d属性将根据当前rect维度重新计算。

addClothoidPaths(2.5, 0.9, true);

document.querySelectorAll(".inputs").forEach((input) => {
  input.addEventListener("input", (e) => {
    let r = +inputBorderRadius.value;
    let tension = +inputTension.value;
    let cubic = +document.querySelector('input[name="bezierType"]:checked')
      .value;
    addClothoidPaths(r, tension, cubic);
  });
});

function addClothoidPaths(borderRadius = 2.5, tension = 0.9, cubic = true) {
  const ns = "http://www.w3.org/2000/svg";

  let rects = document.querySelectorAll("rect");
  rects.forEach((rect, i) => {
    let svg = rect.closest("svg");

    // create clothoid rounded path
    let rectPathGroup = svg.querySelector(".rectGroup");
    let rectPath = svg.querySelector(".rectPath" + i);

    if (!rectPathGroup) {
      rectPathGroup = document.createElementNS(ns, "g");

      rectPathGroup.classList.add("rectGroup");
      svg.append(rectPathGroup);

      rectPath = document.createElementNS(ns, "path");
      rectPath.classList.add("rectPath" + i);

      rectPathGroup.append(rectPath);
    }

    //console.log(rectPath)

    /**
     * copy rect attributes
     */
    const setAttributes = (el, attributes, exclude = []) => {
      for (key in attributes) {
        if (exclude.indexOf(key) === -1) {
          el.setAttribute(key, attributes[key]);
        }
      }
    };

    const getAttributes = (el) => {
      let attArr = [...el.attributes];
      let attObj = {};
      attArr.forEach(function (att) {
        attObj[att.nodeName] = att.nodeValue;
      });
      return attObj;
    };

    //exclude attributes not needed for paths
    let exclude = ["x", "y", "r", "rx", "ry", "height", "width", "id"];
    // copy attributes to path and set pathData
    let attributes = getAttributes(rect);
    setAttributes(rectPath, attributes, exclude);

    //hide rect
    rect.style.visibility = "hidden";

    let d = updateClothoid(rect, borderRadius, tension);
    rectPath.setAttribute("d", d);
    rectPath.style.visibility = "visible";

    let resizeObserver = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        let d = updateClothoid(entry.target, borderRadius, tension, cubic);
        rectPath.setAttribute("d", d);
        updateOutput();
      });
    });

    // Observe one or multiple elements
    resizeObserver.observe(rect);
  });
}

function updateOutput() {
  output.value = new XMLSerializer().serializeToString(svg);
}

function updateClothoid(rect, borderRadius, tension, cubic = true) {
  let x = rect.x.baseVal.value;
  let y = rect.y.baseVal.value;
  let w = rect.width.baseVal.value;
  let h = rect.height.baseVal.value;
  let r = rect.rx.baseVal.value;
  let rC = r * borderRadius;

  let lineHLength = w - rC * 2;
  let lineVLength = h - rC * 2;
  let d = "";

  // prevent border radius smaller than half width
  if (rC > w / 2 || rC > h / 2) {
    rC = Math.min(...[w, h]) / 3;
    lineHLength = w - rC * 2;
    lineVLength = h - rC * 2;
  }

  if (cubic) {
    d = `
    M ${x + rC} ${y}
    h ${lineHLength}
    c ${rC * tension} 0
     ${rC} ${rC * (1 - tension)}  
     ${rC} ${rC}  
    v ${lineVLength}
    c 0 ${rC * tension}  
     -${rC * (1 - tension)} ${rC}  
     -${rC} ${rC} 
     h -${lineHLength}
    c -${rC * tension}  0
     -${rC} -${rC * (1 - tension)}  
     -${rC} -${rC} 
     v-${lineVLength}
    c 0 -${rC * tension} 
     ${rC * (1 - tension)} -${rC}   
     ${rC} -${rC}`;
  }
  // quadratic border smoothing
  else {
    d = `
    M ${x + rC} ${y}
    h ${lineHLength}
    q ${rC} 0
    ${rC} ${rC}  
    v ${lineVLength}
    q 0 ${rC}
    -${rC} ${rC}  
    h -${lineHLength}
    q -${rC} 0
    -${rC} -${rC}  
    v -${lineVLength}
    q  0 -${rC}
     ${rC} -${rC}`;
  }

  return d.replace(/[\n\r\t]/g, "").replace(/\s{2,}/g, " ");
}
.resize {
  border: 1px solid #ccc;
  resize: both;
  overflow: auto;
  width: 50%;
  max-width: 50%;
}

svg {
  display: block;
  width: 100%;
  height: 100%;
}

textarea {
  width: 100%;
  min-height: 20em;
}
<p>Border-radius<input class="inputs" id="inputBorderRadius" type="range" min="1" max="5" step="0.1"></p>
<p>Tension (only for cubic)<input class="inputs" id="inputTension" type="range" min="0.5" max="1" step="0.1"></p>

<p><label>  <input class="inputs"  type="radio" name="bezierType" value="1" checked> Cubic</label> <label>  <input class="inputs" type="radio" name="bezierType" value="0"> Quadratic</label> </p>

<h3>Resize me</h3>
<div class="resize">
  <svg id="svg">
    <rect id="rect" x="10%" y="10%" width="80%" height="80%" rx="10" fill="#ccc" stroke="#000" stroke-width="10" transform="rotate(0)" transform-orgin="center" />
  </svg>
</div>

<fieldset>
  <legend>Output</legend>
  <textarea id="output"></textarea>
</fieldset>

计算伪回旋曲线圆角

基于矩形初始rx边界半径属性,我们可以计算二次或三次曲线段,如下所示:

function updateClothoid(rect, borderRadius, tension, cubic = true) {
  let x = rect.x.baseVal.value;
  let y = rect.y.baseVal.value;
  let w = rect.width.baseVal.value;
  let h = rect.height.baseVal.value;
  let r = rect.rx.baseVal.value;
  let rC = r * borderRadius;

  // horizontal and vertical line segments between curves
  let lineHLength = w - rC * 2;
  let lineVLength = h - rC * 2;
  let d = "";

  // prevent border radius smaller than half width
  if (rC > w / 2 || rC > h / 2) {
    rC = Math.min(...[w, h]) / 3;
    lineHLength = w - rC * 2;
    lineVLength = h - rC * 2;
  }

  if (cubic) {
    d = `
    M ${x + rC} ${y}
    h ${lineHLength}
    c ${rC * tension} 0
     ${rC} ${rC * (1 - tension)}  
     ${rC} ${rC}  
    v ${lineVLength}
    c 0 ${rC * tension}  
     -${rC * (1 - tension)} ${rC}  
     -${rC} ${rC} 
     h -${lineHLength}
    c -${rC * tension}  0
     -${rC} -${rC * (1 - tension)}  
     -${rC} -${rC} 
     v-${lineVLength}
    c 0 -${rC * tension} 
     ${rC * (1 - tension)} -${rC}   
     ${rC} -${rC}`;
  }
  // quadratic border smoothing
  else {
    d = `
    M ${x + rC} ${y}
    h ${lineHLength}
    q ${rC} 0
    ${rC} ${rC}  
    v ${lineVLength}
    q 0 ${rC}
    -${rC} ${rC}  
    h -${lineHLength}
    q -${rC} 0
    -${rC} -${rC}  
    v -${lineVLength}
    q  0 -${rC}
     ${rC} -${rC}`;
  }

  // remove whitespace
  return d.replace(/[\n\r\t]/g, "").replace(/\s{2,}/g, " ");
}

较高的rC将增加初始边界半径,以获得更平滑的曲线。
三次贝济耶允许对曲率进行更多控制。
较高的tension值将控制点“拉”到角落,从而导致视觉上较小的边界半径。
与基于添加圆弧的默认边界半径方法相比,这两个选项(二次和三次)都将在直线和曲线之间生成更平滑的过渡。

let d = updateClothoid(rect, 2.5, 0.9, true);
path.setAttribute('d', d)

function updateClothoid(rect, borderRadius, tension, cubic = true) {
  let x = rect.x.baseVal.value;
  let y = rect.y.baseVal.value;
  let w = rect.width.baseVal.value;
  let h = rect.height.baseVal.value;
  let r = rect.rx.baseVal.value;
  let rC = r * borderRadius;

  // horizontal and vertical line segments between curves
  let lineHLength = w - rC * 2;
  let lineVLength = h - rC * 2;
  let d = "";

  // prevent border radius smaller than half width
  if (rC > w / 2 || rC > h / 2) {
    rC = Math.min(...[w, h]) / 3;
    lineHLength = w - rC * 2;
    lineVLength = h - rC * 2;
  }

  if (cubic) {
    d = `
    M ${x + rC} ${y}
    h ${lineHLength}
    c ${rC * tension} 0
     ${rC} ${rC * (1 - tension)}  
     ${rC} ${rC}  
    v ${lineVLength}
    c 0 ${rC * tension}  
     -${rC * (1 - tension)} ${rC}  
     -${rC} ${rC} 
     h -${lineHLength}
    c -${rC * tension}  0
     -${rC} -${rC * (1 - tension)}  
     -${rC} -${rC} 
     v-${lineVLength}
    c 0 -${rC * tension} 
     ${rC * (1 - tension)} -${rC}   
     ${rC} -${rC}`;
  }
  // quadratic border smoothing
  else {
    d = `
    M ${x + rC} ${y}
    h ${lineHLength}
    q ${rC} 0
    ${rC} ${rC}  
    v ${lineVLength}
    q 0 ${rC}
    -${rC} ${rC}  
    h -${lineHLength}
    q -${rC} 0
    -${rC} -${rC}  
    v -${lineVLength}
    q  0 -${rC}
     ${rC} -${rC}`;
  }

  // remove whitespace
  return d.replace(/[\n\r\t]/g, "").replace(/\s{2,}/g, " ");
}
svg {
  display: block;
  width: 20em;
  border: 1px solid #ccc
}
<svg id="svg" viewBox="0 0 100 100">
    <rect id="rect" x="10%" y="10%" width="80%" height="80%" rx="10" fill="none" stroke="#ccc" stroke-width="0.5" transform="rotate(0)" transform-orgin="center" />
    <path id="path" fill="none" stroke="red" stroke-width="0.75"/>
  </svg>

CSS方法:使用CSS clip-path Package squircle

你也可以使用css的squircle生成器,比如"CSS Clothoid Corners"

<div class="cloth-wrp ">
  <div class="clothoid-corner clipped">
    <p>Test clothoid</p>
  </div>
</div>

此剪辑路径实际上是一个多边形近似。
我们需要将squircle Package 在一个相对定位的父div中。
这个 Package 器引入了一个伪元素-使用相同的clip-path进行裁剪。
伪元素具有将导致最终伪笔划颜色的背景颜色。
笔划宽度由应用于 Package 元素的填充来定义。

6kkfgxo0

6kkfgxo02#

我没有得到你的观点100%,但我试着按照我的理解。请检查我的代码。

<svg xmlns="http://www.w3.org/2000/svg" width="220" height="220" viewBox="-10 -10 220 220">
  <defs>
    <clipPath id="squircle-clip">
      <path d="M20,0
      L20,0
    Q200,-10 200,150
        L200,180
        Q200,200 180,200
        L20,200
        Q0,200 0,180
        L0,20
        Q0,0 20,0"
        style="vector-effect: non-scaling-stroke;" 
      />
    </clipPath>
  </defs>
  <rect x="0" y="0" width="200" height="200" fill="#222" clip-path="url(#squircle-clip)"
  />
  <path d="M20,0
  L20,0
    Q200,-10 200,150
    L200,180
    Q200,200 180,200
    L20,200
    Q0,200 0,180
    L0,20
    Q0,0 20,0" 
    fill="none" stroke="#484848" stroke-width="4" style="vector-effect: non-scaling-stroke;"
  />
</svg>

相关问题