2

I'm learning canvas with the 2D breakout tutorial from MDN and there is an exercise: try changing the color of the ball to a random colour every time it hits the wall.

Before draw the ball, I check if the ball it's inside the wall. And if it touches the ball, first I generate a random hexadecimal value color, and then I draw the ball with that color.

But it only works sometimes. I've logged the fillStyle property of the ctx object but sometimes it doesn't change the value to the new color.

/* Reference to Canvas */
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");

/* Start Ball Position */
let x = canvas.width / 2;
let y = canvas.height - 30;

/* Move Ball */
let dx = 2;
let dy = -2;

/* Ball Dimensions */
let ballRadius = 10;

/* Ball Color */
let ballColor = "#0095DD";

/* Draw Ball */
function drawBall() {
  ctx.beginPath();
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
  ctx.fillStyle = ballColor;
  ctx.fill();
  ctx.closePath();
  x += dx;
  y += dy;
}

/* Update Canvas */
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  bouncing();
  drawBall();
}

/**
 * If the ball goes outside the boundaries,
 * change the direction to the opposite
 */
function bouncing() {
  if(x + dx < ballRadius || x + dx > canvas.width - ballRadius) {
    dx = -dx;
    randomColor();
  }

  if(y + dy < ballRadius || y + dy > canvas.height - ballRadius) {
    dy = -dy;
    randomColor();
  }
}

/* Change the ball color to a random hex value */
function randomColor() {

  const hexColor = ['#'];
  const letters = ['A', 'B', 'C', 'D', 'E', 'F'];

  for(let digit = 0; digit < 6; digit++) {
    let value = Math.floor(Math.random() * 16);
    if(value > 9) {
      value = letters[value - 9];
    }
    hexColor.push(value);
  }
  
  ballColor = hexColor.join('');

}

setInterval(draw, 10);
html {
  box-sizing: border-box;
}

*, *::before, *::after {
  box-sizing: inherit;
  padding: 0; 
  margin: 0; 
}
      
canvas { 
  background: #eee; 
  display: block; 
  margin: 0 auto; 
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>2D breakout game</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <canvas id="myCanvas" width="480" height="170"></canvas>

  <script src="script.js"></script>
</body>
</html>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
Angel Luis
  • 322
  • 4
  • 14
  • 2
    The random color generation could be simplified a _lot_: ``function randomColor() { ballColor = '#' + Math.floor(Math.random() * 0x1000000).toString(16).padStart(6, '0'); }`` – Patrick Roberts Sep 02 '18 at 18:06
  • 1
    **Typo:** Your random generator produces invalid results (4-5 hex)... Because Array are 0 indexed and that `15-9`=>6 hence `letters[6]` is undefined. But since you seem to use ES6, go with @Patrick's solution, cleaner and better. – Kaiido Sep 03 '18 at 00:41
  • @Kaiido Can you elaborate a bit more on the invalid results, I do get valid result out of that as is. – Helder Sepulveda Sep 03 '18 at 00:51
  • @HelderSepu force value to be a `15` – Kaiido Sep 03 '18 at 00:59
  • @Kaiido Ye that will cause value to be set to undefined and the join will output something like `#3856` maybe not the intended behavior, but that wont cause any error... the only issue I see is jumping into a color that is very similar to the previous one – Helder Sepulveda Sep 03 '18 at 01:11
  • @HelderSepu `#3856` color code is an RGBA value, only supported by [modern browsers](https://caniuse.com/#feat=css-rrggbbaa). This code may also output `#`, `#A`, `#AB`, which are all invalid CSS color codes and hence will get discarded by `fillStyle` setter, which is just the error being described here. Nobody talked about an Error being thrown. – Kaiido Sep 03 '18 at 01:14
  • @Kaiido Error being thrown? Yes Nobody talked about that, not sure why you brought it up. We do have logic errors those do not throw anything, right? ... I did added the color to the OP (I see you rolled back those changes) to show how likely was your point vs mine, but you bring a valid point I will adjust my answer – Helder Sepulveda Sep 03 '18 at 01:56

2 Answers2

0

The problem is in the line value = letters[value - 9];. This should be value = letters[value - 10];

This is because the mapping you want is:

10 -> A (letters[0])
11 -> B (letters[1])
12 -> C (letters[2])
13 -> D (letters[3])
14 -> E (letters[4])
15 -> F (letters[5])

So you need to subtract 10 not 9.

david
  • 17,925
  • 4
  • 43
  • 57
-2

Your code is working fine, the function randomColor is called every-time the ball bounces, but in the color generation logic you can get invalid values such as #, #A or #AB you can easily fix that change the * 16 to just * 15 and that fixes that.

But also notice that sometimes your random color is very very close to the previous one, so it looks like it did not change, but in fact it did change, This is what I believe we are seen most of the time.

You could come up with a "smart random" function that remembers the last 2 or 3 colors and prevents the next from been similar to those, for more details how to compare colors look the answers to this question:
How to compare two colors for similarity/difference


Here I changed the logic of your randomColor to include color difference logic, now the next color will really show contrast from the previous one

var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
ctx.font = "Bold 30px courier new";
let x = y = 40
let dx = dy = 2;
let ballRadius = 40;
let ballColor = "#0095DD";

/* Change the ball color to a random hex value */
function randomColor() {  
  const hexColor = ['#'];
  const letters = ['A', 'B', 'C', 'D', 'E', 'F'];

  for(let digit = 0; digit < 6; digit++) {
    let value = Math.floor(Math.random() * 15);
    if(value > 9) {
      value = letters[value - 9];
    }
    hexColor.push(value);
  }
  
  if (colorDif(ballColor, hexColor.join('')) > 50)
    ballColor = hexColor.join('');
  else 
    randomColor();
}

function colorDif(color1, color2) {
  if (color1 == color2)  return 0;

  function squaredDelta(v1, v2) {
    return Math.pow(v1 - v2, 2);
  }

  function hexToRgb(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
  }

  var sum = 0;
  
  var c1 = hexToRgb(color1);
  var c2 = hexToRgb(color2);
  sum += squaredDelta(c1.r, c2.r);
  sum += squaredDelta(c1.g, c2.g);
  sum += squaredDelta(c1.b, c2.b);  
  var conversionIndex = 19.5075;
  return Math.sqrt(sum / conversionIndex);
};

function drawBall() {
  ctx.beginPath();
  ctx.fillText(ballColor,10,50);
  ctx.fillStyle = ballColor;
  ctx.arc(x, y, ballRadius, 0, Math.PI * 2);  
  ctx.fill();
  x += dx;      y += dy;
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  bouncing();
  drawBall();
}

function bouncing() {
  if(x + dx < ballRadius || x + dx > canvas.width - ballRadius) {
    dx = -dx;        randomColor();
  }
  if(y + dy < ballRadius || y + dy > canvas.height - ballRadius) {
    dy = -dy;        randomColor();
  }
}

setInterval(draw, 10);
*, *::before, *::after {margin: 0; }
<canvas id="myCanvas" width="480" height="170"></canvas>
Helder Sepulveda
  • 15,500
  • 4
  • 29
  • 56