Use canvas to draw a pill shaped segmented line, draw a thick line with rounded corners

Viewed 103

I'm working on a snake game in JavaScript and the Phaser3 framework, and I want to draw a pill shaped line to use for the snake. So I want to draw rounded line segments filled with one color (green) and outlined with another color (black). It is not one straight line, but rather several connected line segments. They are all connected at right angles, so 0, 90, 180 or 270 degrees.

See the mock up image below.

enter image description here

Here is my code to draw a line, there is an array with the grid locations of line segments positions:

    var TILE_SIZE = 32;
    var c = document.getElementById("myCanvas");
    var ctx = c.getContext("2d");
    
    // points of each snake segment
    snake_segs = [ [9, 11], [10, 11], [10, 12], [11, 12], [11, 13], [12, 13], [12, 12] ];
    
    // set line color
    ctx.strokeStyle = "#FFFF00";
    ctx.beginPath();

    // draw all line segments
    for (var i = 0; i < snake_segs.length-1; i++) {
        var xpos1 = snake_segs[i][0] * TILE_SIZE;
        var ypos1 = snake_segs[i][1] * TILE_SIZE;
        
        var xpos2 = snake_segs[i+1][0] * TILE_SIZE;
        var ypos2 = snake_segs[i+1][1] * TILE_SIZE;
        
        ctx.moveTo(xpos1, ypos1);
        ctx.lineTo(xpos2, ypos2);
    }
    
    ctx.stroke();
    

How to turn this into a rounded pill shaped line?

What is the best approach to do this? This is harder than I initially thought. Is there an algorithm to draw a pill shaped outline? Or is there a clever way to using rounded rectangles that overlap? Or is it best to draw the outline in a clock-wise fashion? But then how do you find the shape of that outline?

2 Answers

You can do this quite easily by first drawing the snake using it's outlinecolor with a line thickness of e.g. 18 pixels and afterwards redraw the same snake with it's body color and a smaller line thickness of say 15 pixels.

Unfortunately this would result in a solid shaped snake with sharp 90° corners. Luckily the CanvasRenderingContext2D has an additional property called lineCap which controls the look of line endpoints. If you set this to 'round' you have your pill-shaped-snake.

Here's an example:

var TILE_SIZE = 32;
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

snake_segs = [
  [9, 11],
  [10, 11],
  [10, 12],
  [11, 12],
  [11, 13],
  [12, 13],
  [12, 12]
];

ctx.fillStyle = "#00a000";
ctx.fillRect(0, 0, c.width, c.height)
ctx.lineCap = "round";

function drawSnake(color, thickness) {
  ctx.beginPath();
  ctx.lineWidth = thickness;
  ctx.strokeStyle = color;
  for (var i = 0; i < snake_segs.length - 1; i++) {
    var xpos1 = snake_segs[i][0] * TILE_SIZE;
    var ypos1 = snake_segs[i][1] * TILE_SIZE;

    var xpos2 = snake_segs[i + 1][0] * TILE_SIZE;
    var ypos2 = snake_segs[i + 1][1] * TILE_SIZE;

    ctx.moveTo(xpos1, ypos1);
    ctx.lineTo(xpos2, ypos2);
  }
  ctx.moveTo(xpos2, ypos2);
  ctx.closePath();
  ctx.stroke();
}

drawSnake("#ffff00", 18);
drawSnake("#00c000", 15);
<canvas id="myCanvas" width="500" height="500">
  </canvas>

Well just if you want tp use only phaser, I would use the phaser graphics object, for a quick and easy solution. It's abit cumbersome, but works.

[UPDATED] short Demo:
The updated Demo now renders the snake, by iterating only once over the segments array. It adds two more Phaser graphics objects, but reduces the loops. (And uses not the original snake_segs array)

document.body.style = 'margin:0;';

var config = {
    type: Phaser.AUTO,
    width: 536,
    height: 183,
    scene: {
        create,
        update
    },
    banner: false
}; 

let foregroundGraphics;
let backgroundGraphics;
let cornersGraphics;
let cornersBackgroundGraphics;

// values are altered to fix demo / Scene
let snake_segs = [ [9, 11], [10, 11], [10, 12], [11, 12], [11, 13], [12, 13], [12, 12] ].map(point => [point[0]-8, point[1] - 10]);

console.info(snake_segs);

let length_seg = 40;
    
function create () {
    backgroundGraphics = this.add.graphics();
    cornersBackgroundGraphics = this.add.graphics();
    foregroundGraphics = this.add.graphics();
    cornersGraphics = this.add.graphics();
}

function update(){
   // update snake in game loop
   drawSnake(0x00a000, 0xffffff, 3);
}

function drawSnake(fgColor, bgColor, padding){
    // Just needed for Update and redraws
    foregroundGraphics.clear();
    backgroundGraphics.clear();
    cornersGraphics.clear();
    cornersBackgroundGraphics.clear();

    drawSegments(fgColor, bgColor, padding); 
}

function drawSegments(fgColor, bgColor, padding){
    backgroundGraphics.beginPath();
    foregroundGraphics.beginPath();
    backgroundGraphics.lineStyle((length_seg + padding * 2)/2, bgColor);
    foregroundGraphics.lineStyle(length_seg /2, fgColor);
    cornersGraphics.lineStyle(length_seg/4, fgColor); 
    cornersBackgroundGraphics.lineStyle((length_seg + padding * 2)/4, bgColor);
   
    for(let idx = 0; idx < snake_segs.length; idx++){
      let point = snake_segs[idx];
      if(idx==0){
        backgroundGraphics.moveTo(point[0] * length_seg, point[1] * length_seg);
        foregroundGraphics.moveTo(point[0] * length_seg, point[1] * length_seg);
      }else {
        backgroundGraphics.lineTo(point[0]  * length_seg, point[1]  * length_seg);
        foregroundGraphics.lineTo(point[0]  * length_seg, point[1]  * length_seg);
        }
        
      cornersBackgroundGraphics.strokeCircle(point[0]  * length_seg, point[1]  * length_seg, (length_seg + padding * 2)/8);
      cornersGraphics.strokeCircle(point[0]  * length_seg, point[1]  * length_seg, length_seg/8);
    }
    backgroundGraphics.strokePath();
    foregroundGraphics.strokePath();
}

new Phaser.Game(config);
<script src="https://cdn.jsdelivr.net/npm/phaser@3.55.2/dist/phaser.js"></script>

Disclaimer: there might be a better way to solve this, but this was the first that came to mind.

(personally I would try to adapt this example to my needs, since I like experimenting, and until now I never had the chance to use the follower and paths features in phaser. But I'm currently not sure, if this features, could be used for solving this task)

Related