Javascript Canvas - CSS-旋转 canvas 元素在 -- 圆形菜单之间留下空白

Javascript Canvas - CSS-rotating canvas elements leaves empty spaces between -- circular menu

我正在为我的数独游戏制作一个菜单,以便为选定的单元格选择数字。
我设法制作了一个圆形动画菜单,其中包含 X canvas 个元素,每个元素代表一个数字(+ 一个为空)。然后我用 CSS 变换旋转所有这些元素以创建一个完美的圆。 问题出现了——每个元素之间都有难看的透明笔画,我无法删除。

我是这样画的:

我的应用程序有点复杂,但我设法创建了一个演示:

let parent = document.getElementsByClassName('menu')[0];
let elSize = parent.getBoundingClientRect().width;
let upscale = 2;

let total = 10;
let length = elSize / 2;

for (let i = 0; i < total; i++) {
  // create new canvas
  let val = document.createElement('canvas');
  val.classList.add("value");

  let deg = 360 / total;

  //set sizes and rotation
  val.height = length * upscale;
  val.width = elSize * upscale;
  val.style.width = elSize + "px";
  val.style.height = length + "px";
  val.style.setProperty("--rotation", (i / total * 360) + "deg");

  // get context
  let ctx = val.getContext("2d");
  ctx.fillStyle = "blue";
  ctx.imageSmoothingEnabled = true;

  // full circle center
  let center = {
    x: length * upscale,
    y: length * upscale
  }

  //function to fill the circle part (step 1 and 2 on the image)
  const fillWedge = (cx, cy, radius, startAngle, endAngle, fillcolor, stroke = false) => {
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.arc(cx, cy, radius, startAngle, endAngle);
    ctx.closePath();
    if (stroke) {
      ctx.lineWidth = 1;
      ctx.strokeStyle = fillcolor;
      ctx.stroke();
    } else {
      ctx.fillStyle = fillcolor;
      ctx.fill();
    }
  }

  const degToAngle = (deg) => {
    let start = -Math.PI / 2;
    let fullCircle = Math.PI * 2;
    return (start + fullCircle * (deg / 360));
  }

  ctx.save();
  ctx.imageSmoothingEnabled = false;
  ctx.globalCompositeOperation = "source-out"; {
    //smaller circle
    let cx = center.x;
    let cy = center.y;
    let radius = length * upscale / 3;
    let startAngle = -((deg + 1) / 2) % 360;
    let endAngle = ((deg + 1) / 2) % 360;

    fillWedge(cx, cy, radius, degToAngle(startAngle), degToAngle(endAngle), ctx.fillStyle);
  }
  //make it semi-transparent
  ctx.globalAlpha = 0.8; {
    //bigger circle
    let cx = center.x;
    let cy = center.y;
    let radius = length * upscale;
    let startAngle = -(deg / 2) % 360;
    let endAngle = (deg / 2) % 360;

    fillWedge(cx, cy, radius, degToAngle(startAngle), degToAngle(endAngle), ctx.fillStyle);
  }
  ctx.restore();

  //draw text
  if (i !== 0) {
    ctx.save();
    ctx.translate(length * upscale, length / 3 * upscale);
    ctx.rotate(-(i / total * 360) / 180 * Math.PI);
    ctx.font = "600 " + (18 * upscale) + 'px Consolas';
    ctx.textAlign = "center";
    ctx.fillStyle = "white";
    ctx.fillText((i) + "", 0, 5 * upscale);
    ctx.restore()
  }

  //add element to menu
  parent.appendChild(val);
}
html {
  background: url(https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdn.cienradios.com%2Fwp-content%2Fuploads%2Fsites%2F13%2F2020%2F06%2FShrek-portada.jpg&f=1&nofb=1) no-repeat center;
  display: flex;
  justify-content: center;
  align-content: center;
}

.menu {
  width: 400px;
  aspect-ratio: 1;
  position: relative;
  border-radius: 50%;
}

.menu .value {
  --rotation: 0deg;
  position: absolute;
  top: 0;
  bottom: 50%;
  left: 50%;
  transform-origin: bottom;
  transform: translate(-50%, 0) rotate(var(--rotation));
}

p {
  color: white;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Sudoku Test</title>
</head>

<body>
  <div class="menu">

  </div>
  <p>
    See, it's semi-transparent and you can clearly see lines between elements

    <br>
    <b>How to fix it?!</b>
  </p>
</body>

</html>

有什么方法可以删除这些空格?我知道这是一个过于详细的问题,但我真的无法解决它。谢谢。

这些是由抗锯齿引起的。由于您确实在对角线上绘制,因此形状不会落在完整的像素边界上。因此,为了平滑线条,浏览器将使那些本应仅部分绘制的像素更加透明。堆叠时,这些透明像素不会完全加起来完全不透明(0.5 不透明度 + 0.5 不透明度 = 0.75 不透明度,而不是 1)。所以你会看到这些行。

移除此处的平滑处理无济于事,因为另一种方法是填充 marching-square 样式,但这会导致某些地方出现完整的孔洞,或者出现重叠像素,这将是可见,因为您的形状不是完全不透明的。

通常解决该问题的廉价技巧是在形状周围用与填充颜色相同的颜色描边几个像素。但是再一次,因为你的形状充满了 semi-transparent 颜色,所以这个技巧不会成功。

您可以通过以完全不透明的方式绘制所有形状并在公共容器上应用透明度来解决问题。但这意味着您的文本需要它们自己的 canvas 和它们自己的容器(否则它们也会是透明的)。

let parent = document.getElementsByClassName('menu')[0];
const textsParent = document.getElementsByClassName('texts')[0];
let elSize = parent.getBoundingClientRect().width;
let upscale = 2;

let total = 10;
let length = elSize / 2;

for (let i = 0; i < total; i++) {
  // create new canvas
  let val = document.createElement('canvas');
  val.classList.add("value");

  let deg = 360 / total;

  //set sizes and rotation
  val.height = length * upscale;
  val.width = elSize * upscale;
  val.style.width = elSize + "px";
  val.style.height = length + "px";
  val.style.setProperty("--rotation", (i / total * 360) + "deg");

  // get context
  let ctx = val.getContext("2d");
  ctx.fillStyle = "blue";

  // full circle center
  let center = {
    x: length * upscale,
    y: length * upscale
  }

  //function to fill the circle part (step 1 and 2 on the image)
  const fillWedge = (cx, cy, radius, startAngle, endAngle, fillcolor, stroke = false) => {
    ctx.beginPath();
    ctx.moveTo(cx, cy);
    ctx.arc(cx, cy, radius, startAngle, endAngle);
    ctx.closePath();
    ctx.lineWidth = 2;
    ctx.strokeStyle = fillcolor;
    ctx.stroke();
    ctx.fillStyle = fillcolor;
    ctx.fill();
  }

  const degToAngle = (deg) => {
    let start = -Math.PI / 2;
    let fullCircle = Math.PI * 2;
    return (start + fullCircle * (deg / 360));
  }

  ctx.save();
  ctx.imageSmoothingEnabled = false;
  {
    //smaller circle
    let cx = center.x;
    let cy = center.y;
    let radius = length * upscale / 3;
    let startAngle = -((deg + 1) / 2) % 360;
    let endAngle = ((deg + 1) / 2) % 360;

    fillWedge(cx, cy, radius, degToAngle(startAngle), degToAngle(endAngle), ctx.fillStyle);
  }
  
  {
    //bigger circle
    let cx = center.x;
    let cy = center.y;
    let radius = length * upscale;
    let startAngle = -(deg / 2) % 360;
    let endAngle = (deg / 2) % 360;

    fillWedge(cx, cy, radius, degToAngle(startAngle), degToAngle(endAngle), ctx.fillStyle);
  }
  ctx.restore();

  //draw text
  if (i !== 0) {
    // we need a new canvas just for the text
    const val2 = val.cloneNode();
    const ctx = val2.getContext("2d");
    ctx.save();
    ctx.translate(length * upscale, length / 3 * upscale);
    ctx.rotate(-(i / total * 360) / 180 * Math.PI);
    ctx.font = "600 " + (18 * upscale) + 'px Consolas';
    ctx.textAlign = "center";
    ctx.fillStyle = "white";
    ctx.fillText((i) + "", 0, 5 * upscale);
    ctx.restore()
    textsParent.appendChild(val2);
  }

  //add element to menu
  parent.appendChild(val);
}
html {
  background: url(https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fcdn.cienradios.com%2Fwp-content%2Fuploads%2Fsites%2F13%2F2020%2F06%2FShrek-portada.jpg&f=1&nofb=1) no-repeat center;
  display: flex;
  justify-content: center;
  align-content: center;
}

.menu, .texts {
  width: 400px;
  aspect-ratio: 1;
  position: relative;
  border-radius: 50%;
}

.menu .value, .texts canvas {
  --rotation: 0deg;
  position: absolute;
  top: 0;
  bottom: 50%;
  left: 50%;
  transform-origin: bottom;
  transform: translate(-50%, 0) rotate(var(--rotation));
}
.menu { opacity: 0.8 }
.text-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh; 
  display: flex;
  justify-content: center;
  align-content: center;
}  
<div class="menu">

</div>
<div class="text-container">
  <div class="texts">
  </div>
</div>

因此,最好的办法可能是完全重构您的代码以使用单个 canvas。如果您告诉浏览器在单个子路径中绘制所有形状,它将能够将描摹完美地放置在它应该在的位置,并且能够在中间没有任何线条的情况下绘制所有这些:

const canvas = document.querySelector("canvas");
canvas.width = canvas.height = 500;
const ctx = canvas.getContext("2d");
const parts = 10;
const theta = Math.PI / (parts / 2);

const trace = (cx, cy, r1, r2, t) => {
  ctx.moveTo(cx + r2, cy);
  ctx.lineTo(cx + r1, cy);
  ctx.arc(cx, cy, r1, 0, t);
  ctx.arc(cx, cy, r2, t, 0, true);
};

ctx.translate(250, 250);
ctx.rotate(-Math.PI / 2 - theta);
// draw all but the last part
for (let i = 0; i < parts - 1; i++) {
  ctx.rotate(theta);
  trace(0, 0, 50, 200, theta);
}
ctx.globalAlpha = 0.8;
ctx.fillStyle = "blue";
ctx.fill(); // in a single pass
// draw the last part in red
ctx.fillStyle = "red";
ctx.rotate(theta);
ctx.beginPath();
trace(0, 0, 50, 200, theta);
ctx.fill();
canvas {
  /* checkered effect from  */
  --tint:rgba(255,255,255,0.9);background-image:linear-gradient(to right,var(--tint),var(--tint)),linear-gradient(to right,black 50%,white 50%),linear-gradient(to bottom,black 50%,white 50%);background-blend-mode:normal,difference,normal;background-size:2em 2em;
}
<canvas></canvas>