3

I use the wheel of fortune by Roko C. Buljan from here: how to draw a wheel of fortune?

This is my first experience with JS and canvas. I want to put long sentences in labels, but they go out of bounds.

I've tried using a flexible font, but that doesn't really work and isn't exactly what I need. I need the text inside each block to move to a new line and reduce the font if it goes out of bounds.

Can this be implemented in this code, or would I need to write everything from scratch?

const sectors = [
  {color:"#f82", label:"parallellogram into parallellogram"},
  {color:"#0bf", label:"10"},
  {color:"#fb0", label:"StackStack StackStack"},
  {color:"#0fb", label:"50"},
  {color:"#b0f", label:"StackStackStackStackStackStack"},
  {color:"#f0b", label:"Stack Stack"},
  {color:"#bf0", label:"Stack"},
];

const rand = (m, M) => Math.random() * (M - m) + m;
const tot = sectors.length;
const EL_spin = document.querySelector("#spin");
const ctx = document.querySelector("#wheel").getContext('2d');
const dia = ctx.canvas.width;
const rad = dia / 2;
const PI = Math.PI;
const TAU = 2 * PI;
const arc = TAU / sectors.length;

const friction = 0.991; // 0.995=soft, 0.99=mid, 0.98=hard
let angVel = 0; // Angular velocity
let ang = 0; // Angle in radians

const getIndex = () => Math.floor(tot - ang / TAU * tot) % tot;

function drawSector(sector, i) {
  const ang = arc * i;
  ctx.save();
  // COLOR
  ctx.beginPath();
  ctx.fillStyle = sector.color;
  ctx.moveTo(rad, rad);
  ctx.arc(rad, rad, rad, ang, ang + arc);
  ctx.lineTo(rad, rad);
  ctx.fill();
  // TEXT
  ctx.translate(rad, rad);
  ctx.rotate(ang + arc / 2);
  ctx.textAlign = "right";
  ctx.fillStyle = "#fff";
  ctx.font = "bold 14px sans-serif";
  ctx.fillText(sector.label, rad - 10, 10);
  //
  ctx.restore();
};

function rotate() {
  const sector = sectors[getIndex()];
  ctx.canvas.style.transform = `rotate(${ang - PI / 2}rad)`;
  EL_spin.textContent = !angVel ? "SPIN" : sector.label;
  EL_spin.style.background = sector.color;
}

function finishedSpinning() { // Called when the wheel stops spinning
  const sector = sectors[getIndex()];
  alert(sector.label);
}

function frame() {
  if (!angVel) return;
  const isSpinning = angVel > 0; // Check if the wheel is currently spinning
  angVel *= friction; // Decrement velocity by friction
  if (angVel < 0.002) angVel = 0; // Bring to stop
  ang += angVel; // Update angle
  ang %= TAU; // Normalize angle
  rotate();
  
  if (isSpinning && angVel === 0) { // If the wheel was spinning, but isn't anymore, it has just stopped
    finishedSpinning();
  }
}

function engine() {
  frame();
  requestAnimationFrame(engine)
}

// INIT
sectors.forEach(drawSector);
rotate(); // Initial rotation
engine(); // Start engine
EL_spin.addEventListener("click", () => {
  if (!angVel) angVel = rand(0.25, 0.35);
});
    #wheelOfFortune {
  display: inline-block;
  position: relative;
  overflow: hidden;
}

#wheel {
  display: block;
}

#spin {
  font: 1.5em/0 sans-serif;
  user-select: none;
  cursor: pointer;
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  top: 50%;
  left: 50%;
  width: 30%;
  height: 30%;
  margin: -15%;
  background: #fff;
  color: #fff;
  box-shadow: 0 0 0 8px currentColor, 0 0px 15px 5px rgba(0, 0, 0, 0.6);
  border-radius: 50%;
  transition: 0.8s;
}

#spin::after {
  content: "";
  position: absolute;
  top: -17px;
  border: 10px solid transparent;
  border-bottom-color: currentColor;
  border-top: none;
}
<div id="wheelOfFortune">
  <canvas id="wheel" width="300" height="300"></canvas>
  <div id="spin">SPIN</div>
</div>

That's what I'm trying to do enter image description here

Solution: HTML5 canvas ctx.fillText won't do line breaks?

New problem - long words as in the first label

cheech
  • 45
  • 4
  • It's hard (needs a good load of code) to make the text **wrap** inside a canvas. The simplest you could do is resize it depending on the arc length vs. characters length. Yes, it can be implemented in the code. The function that paints the sectors text `drawSector()` needs a tweak to get the right font size. Instead of the hardcoded `14px` you need to pass a variable into template literals. – Roko C. Buljan Jan 07 '23 at 21:13
  • @RokoC.Buljan Thanks for the reply. I liked your code, but apparently I will have to redo it using div and span – cheech Jan 07 '23 at 21:17
  • (Thank you) Yes, it would be a bit easier to let the HTML and CSS `rotate: Adeg;`, `text-align: right;` handle rotation, the test wrapping and alignment inside sectors. Keep in mind that even if you use HTML elements for default wrapping, you will still at some point get out of space, and some text resizing depending on the available space will still be needed. monospace fonts are easy to calculate, but otherwise it's still a difficult task. Also keep in mind that text and rotation will not play well. On wheel stop you'll see the browser adjusting the text aliasing. Quite an ugly result. – Roko C. Buljan Jan 07 '23 at 21:20
  • Not to say that multiline text inside an arc... we're talking about a rectangular shape inside a circle's sector.... Calculating that ideal area width height is hard. The result is also visually pretty ugly. I'd suggest that you go for a single line of text. Or minimally 2 lines, but for 2 lines here you go - you need to create a separate function that calculates the font size, line height etc. Splits ideally the text and then sets the ideal font size.... etc etc. You're back in the jungle again I'm afraid just for just those two+ lines – Roko C. Buljan Jan 07 '23 at 21:27
  • Also, it depends on if you want to calculate one "ideal" font size and apply it to every sector's label or you want to manipulate the labels font size individually. – Roko C. Buljan Jan 07 '23 at 21:40
  • 1
    Yes, I want to calculate one font size and apply it to every sector's label, added a picture of what I want to do to the question – cheech Jan 07 '23 at 21:53
  • Given your image - In the sector where you have four lines of text - you just got lucky that the first and last words are short. – Roko C. Buljan Jan 07 '23 at 21:57
  • Yes, however, I do not think that a label with a longer length will be needed – cheech Jan 07 '23 at 22:00
  • For a single line: https://jsbin.com/witovehore/edit?html,css,js,console,output - For a multiline text I might create a demo but not today. I'll review your question shortly. – Roko C. Buljan Jan 07 '23 at 22:03
  • I will be very grateful to you, but in general I already :) – cheech Jan 07 '23 at 22:07
  • 2
    I want everyone to know that I can click this button all day - endless entertainment for my last 2 brain cells. – Shmack Jan 08 '23 at 00:43
  • @RokoC.Buljan I found the solution to the problem here https://stackoverflow.com/questions/5026961/html5-canvas-ctx-filltext-wont-do-line-breaks . However, thanks a lot for the feedback! – cheech Jan 08 '23 at 19:18
  • @cheech amazing and glad you made it all alone to incorporate that last piece! You can always provide an answer to your own question. Might end up helpful to someone. Sharing is caring. – Roko C. Buljan Jan 08 '23 at 19:29
  • sure, the answer added – cheech Jan 08 '23 at 20:25

1 Answers1

1

Here's what I got, ready for any suggestions for improvement (mainly the part with text centering - line 89)

It would also be nice to add a shadow to the text to stand out against the background of bright colors

New problem - long words as in the first label

  const sectors = [
  { color: "#0fb", label: "Параллелограмм в паралеллограмме" },
  { color: "#0bf", label: "Бесплатная настройка виджета" },
  { color: "#fb0", label: "Два пресета цветов по цене одного" },
  { color: "#0fb", label: "1строчка" },
  { color: "#b0f", label: "Год премиум поддержки в подарок в подарок" },
  { color: "#f0b", label: "Скидка 10% на любой пакет" },
  { color: "#bf0", label: "Виджет 'Juice Contact' в подарок" },
  { color: "#f82", label: "Скидка 5% на любой пакет" },
  { color: "#bf0", label: "" },
];

function printAtWordWrap(context, text, x, y, lineHeight, fitWidth) {
  fitWidth = fitWidth || 0;

  if (fitWidth <= 0) {
    context.fillText(text, x, y);
    return;
  }
  let words = text.split(" ");
  let currentLine = 0;
  let idx = 1;
  while (words.length > 0 && idx <= words.length) {
    const str = words.slice(0, idx).join(" ");
    const w = context.measureText(str).width;
    if (w > fitWidth) {
      if (idx == 1) {
        idx = 2;
      }
      context.fillText(
        words.slice(0, idx - 1).join(" "),
        x,
        y + lineHeight * currentLine
      );
      currentLine++;
      words = words.splice(idx - 1);
      idx = 1;
    } else {
      idx++;
    }
  }
  if (idx > 0)
    context.fillText(words.join(" "), x, y + lineHeight * currentLine);
}

const rand = (m, M) => Math.random() * (M - m) + m;
const tot = sectors.length;
const EL_spin = document.querySelector("#spin");
const ctx = document.querySelector("#wheel").getContext("2d");
const dia = ctx.canvas.width;
const rad = dia / 2;
const PI = Math.PI;
const TAU = 2 * PI;
const arc = TAU / sectors.length;

const friction = 0.991; // 0.995=soft, 0.99=mid, 0.98=hard
let angVel = 0; // Angular velocity
let ang = 0; // Angle in radians

const getIndex = () => Math.floor(tot - (ang / TAU) * tot) % tot;

// calcFontSize not used
const calcFontSize = () => {
  const maxChars = Math.max(...sectors.map((ob) => ob.label.length));
  const width = rad * 0.9 - 15;
  const w = (width / maxChars) * 1.3;
  const h = ((TAU * rad) / tot) * 0.5;
  return Math.min(w, h);
};

function drawSector(sector, i) {
  const ang = arc * i;
  ctx.save();
  // COLOR
  ctx.beginPath();
  ctx.fillStyle = sector.color;
  ctx.moveTo(rad, rad);
  ctx.arc(rad, rad, rad, ang, ang + arc);
  ctx.lineTo(rad, rad);
  ctx.fill();
  // TEXT
  ctx.translate(rad, rad);
  ctx.rotate(ang + arc / 2);
  ctx.textAlign = "center";
  ctx.fillStyle = "#fff";

  const fontSize = 15;
  ctx.font = `bold ${fontSize}px sans-serif`;

  // values for centering text - not ideal for now (need tuning)
  const w = ctx.measureText(sector.label).width;
  console.log(sector.label, w);
  let y;
  const lineWidth = 130; // width before line break

  switch (true) {
    case w < lineWidth:
      y = fontSize / 3;
      break;
    case w >= lineWidth && w < 2 * lineWidth:
      y = fontSize / 3 - 10;
      break;
    case w >= 2 * lineWidth && w < 3 * lineWidth:
      y = fontSize / 3 - 20;
      break;
    case w >= 3 * lineWidth && w < 4 * lineWidth:
      y = fontSize / 3 - 30;
      break;
    case w >= 4 * lineWidth:
      y = fontSize / 3 - 40;
      break;
    default:
      y = fontSize / 3;
  }
  printAtWordWrap(ctx, sector.label, rad * 0.7, y, 21, lineWidth);

  ctx.restore();
}

function rotate() {
  const sector = sectors[getIndex()];
  ctx.canvas.style.transform = `rotate(${ang - PI / 2}rad)`;
  EL_spin.textContent = !angVel
    ? "SPIN"
    : ""; /* sector.label instead "" - if u want to display text inside spin element */
  EL_spin.style.background = sector.color;
}

function finishedSpinning() {
  // Called when the wheel stops spinning
  const sector = sectors[getIndex()];
  alert(sector.label);
}

function frame() {
  if (!angVel) return;
  const isSpinning = angVel > 0; // Check if the wheel is currently spinning
  angVel *= friction; // Decrement velocity by friction
  if (angVel < 0.002) angVel = 0; // Bring to stop
  ang += angVel; // Update angle
  ang %= TAU; // Normalize angle
  rotate();

  if (isSpinning && angVel === 0) {
    // If the wheel was spinning, but isn't anymore, it has just stopped
    finishedSpinning();
  }
}

function engine() {
  frame();
  requestAnimationFrame(engine);
}

// INIT
sectors.forEach(drawSector);
rotate(); // Initial rotation
engine(); // Start engine
EL_spin.addEventListener("click", () => {
  if (!angVel) angVel = rand(0.25, 0.35);
});
    #wheelOfFortune {
  display: inline-block;
  position: relative;
  overflow: hidden;
}

#wheel {
  display: block;
}

#spin {
  font: 1.5em/0 sans-serif;
  user-select: none;
  cursor: pointer;
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  top: 50%;
  left: 50%;
  width: 30%;
  height: 30%;
  margin: -15%;
  background: #fff;
  color: #fff;
  box-shadow: 0 0 0 8px currentColor, 0 0px 15px 5px rgba(0, 0, 0, 0.6);
  border-radius: 50%;
  transition: 0.8s;
}

#spin::after {
  content: "";
  position: absolute;
  top: -17px;
  border: 10px solid transparent;
  border-bottom-color: currentColor;
  border-top: none;
}
 

 <body style="background-color: darkcyan">
    <div id="wheelOfFortune">
      <canvas id="wheel" width="480" height="480"></canvas>
      <div id="spin">SPIN</div>
    </div>
  </body>
cheech
  • 45
  • 4
  • Nice implementation! I just want to add that the above will not work for dynamic number of sectors. But I believe it can be easily tweaked by using the `sectors.length` as a multiplication factor for the font-size. Good job anyways. – Roko C. Buljan Jan 08 '23 at 22:05
  • Also, an improvement would to add acceleration (angular velocity) - like in the original example demo: https://stackoverflow.com/a/33850748/383904 – Roko C. Buljan Jan 08 '23 at 22:22