Javascript Canvas — CSS-вращающиеся элементы холста оставляют пустые места между -- круговое меню

Я делаю меню для своей игры Судоку, чтобы выбирать числа для выбранных ячеек.
Мне удалось сделать круглое анимированное меню с X элементами холста, каждый из которых представляет одно число (+ один пуст). Затем я поворачиваю все эти элементы с помощью преобразования CSS, чтобы создать идеальный круг. И тут возникает проблема - между каждым элементом некрасивые прозрачные штрихи, которые никак не получается убрать.

Javascript Canvas — CSS-вращающиеся элементы холста оставляют пустые места между -- круговое меню

Вот как я это рисую:

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

Любой способ удалить эти пробелы? Я знаю, что это слишком подробный вопрос, но я действительно не могу его исправить. Спасибо.

Это сглаживание, мало что можно сделать с этим дизайном кода. Есть ли причины, по которым вы используете много холстов вместо одного? Используя один холст, вы можете сделать весь свой путь одним подпутем и избежать этой проблемы: jsfiddle.net/503ruyea

Kaiido 21.03.2022 08:08

@Kaiido Потому что я анимирую каждую ячейку с помощью CSS. Поэтому при отображении меню воспроизводится всплывающая анимация. Сделать это с одним холстом было бы довольно сложно. Можно как-нибудь убрать сглаживание?

matez 21.03.2022 10:15
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
2
32
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Это вызвано сглаживанием. Поскольку вы рисуете по диагоналям, форма не попадает на полные границы пикселей. Таким образом, чтобы сгладить линии, браузер сделает те пиксели, которые должны были быть окрашены лишь частично, более прозрачными. При наложении эти прозрачные пиксели не будут в сумме точно соответствовать полной непрозрачности (0,5 непрозрачности + 0,5 непрозрачности = 0,75 непрозрачности, а не 1). Итак, вы увидите эти строки.

Удаление сглаживания здесь не поможет, потому что альтернативой было бы заполнение в стиле марширующего квадрата, но это привело бы либо к полным дырам в некоторых местах, либо к перекрывающимся пикселям, которые были бы видны, поскольку ваши фигуры не полностью непрозрачный.

Обычно дешевый трюк для решения этой проблемы состоит в том, чтобы обвести пару пикселей вокруг фигуры того же цвета, что и заливка. Но еще раз, поскольку ваши фигуры заполнены полупрозрачными цветами, этот трюк не сработает.

Вы можете схитрить, нарисовав все свои фигуры с полной непрозрачностью и применив прозрачность к обычному контейнеру. Но это означает, что вашим текстам потребуется собственный холст и собственный контейнер (иначе они тоже будут прозрачными).

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>

Поэтому вместо этого лучше всего полностью реорганизовать код, чтобы вместо этого использовать один холст. Если вы скажете браузеру отрисовывать все ваши фигуры в одном подпути, он сможет идеально разместить трассировку там, где она должна быть, и сможет отрисовать все это без какой-либо промежуточной линии:

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 https://stackoverflow.com/a/51054396/3702797 */
  --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>

Спасибо! Не могли бы вы описать, что вы делаете в методе trace, чтобы меньший круг исчез без globalCompositeOperation? Я хотел бы использовать ваш первый метод, но у меня не получается сделать заливку + обводку с правильным внешним видом, поэтому я проверил второй метод, и вы делаете это по-другому.

matez 21.03.2022 19:28

Ну, он буквально перемещается в точку на расстоянии r2 от центра, проводит линию вниз до r1, затем внутреннюю дугу и, наконец, внешнюю дугу, в порядке против часовой стрелки. Но вам следует избегать первого метода (на основе вашего), вы столкнетесь со многими проблемами на этом пути. Если они не подразумевают 3D-преобразования, ваши CSS-анимации могут быть легко выполнены в 2D-контексте.

Kaiido 22.03.2022 01:43

Другие вопросы по теме