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.
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?
Well just using phaser, I would use the phaser graphics object, for a quick and easy solution. It's abit cumbersome, but works.
Demo:
(to optimize the whole drawing[reduce one for - loop], one could add an extra graphics object, and like that one would be able to draw everthing in one for - loop, but I didn't want to make demo too complex)
document.body.style = 'margin:0;';
var config = {
type: Phaser.AUTO,
width: 536,
height: 183,
scene: {
create,
update
},
banner: false
};
let foregroundGraphics;
let backgroundGraphics;
let snake_segs = [ [1,1], [2,1], [3,1], [3,2], [3,3], [4,3], [5,3]];
let length_seg = 20;
function create () {
backgroundGraphics = this.add.graphics();
foregroundGraphics = this.add.graphics();
}
function update(){
drawSnake(0x00a000, 0xffffff, 3);
}
function drawSnake(fgColor, bgColor, padding){
foregroundGraphics.clear();
backgroundGraphics.clear();
drawCorners(fgColor, bgColor, padding);
drawSegments(fgColor, bgColor, padding);
}
function drawCorners(fgColor, bgColor, padding){
foregroundGraphics.lineStyle(length_seg/4, fgColor);
backgroundGraphics.lineStyle((length_seg + padding * 2)/4, bgColor);
snake_segs.forEach( point => {
backgroundGraphics.strokeCircle(point[0] * length_seg, point[1] * length_seg, (length_seg + padding * 2)/8);
foregroundGraphics.strokeCircle(point[0] * length_seg, point[1] * length_seg, length_seg/8);
});
}
function drawSegments(fgColor, bgColor, padding){
backgroundGraphics.beginPath();
foregroundGraphics.beginPath();
backgroundGraphics.lineStyle((length_seg + padding * 2)/2, bgColor);
foregroundGraphics.lineStyle(length_seg /2, fgColor);
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);
}
}
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)
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>