Search code examples
javascripthtml5-canvas

How do I create a canvas circle with a image on it?


So, I followed a tutorial to make a game. Obviously, that isn't the best way to learn to code so I began to modify it. Currently the game has enemies that slowly move towards the player, but these enemies are colored circles and nothing else. I'd like to add a picture that I can put on the enemies, but I have no Idea how. Here's some code that you might like to know:

The enemy class (update is called every frame):

class Enemy {
    constructor(x, y, radius, color, velocity) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.color = color;
        this.velocity = velocity;
    }

    draw() {
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
        ctx.fillStyle = this.color;
        ctx.fill();
    }

    update() {
        this.draw();
        this.x = this.x + this.velocity.x;
        this.y = this.y + this.velocity.y;
    }
}

The function for creating enemies:

function spawnEnemies() {
    setInterval(() => {
        const radius = Math.random() * (30 - 4) + 4;
        let x;
        let y;
        if (Math.random() < 0.5) {
            x = Math.random() < 0.5 ? 0 - radius : canvas.width + radius;
            y = Math.random() * canvas.height;
        } else {
            y = Math.random() < 0.5 ? 0 - radius : canvas.height + radius;
            x = Math.random() * canvas.width;
        }
        const color = `hsl(${Math.random() * 360}, 50%, 50%)`;
        const angle = Math.atan2(canvas.height / 2 - y, canvas.width / 2 - x);
    const velocity = {
        x: Math.cos(angle),
        y: Math.sin(angle)
    }
        enemies.push(new Enemy(x, y, radius, color, velocity));
    }, 1000)
}

This code is ran in the animate function:

enemies.forEach((enemy, index) => {
        enemy.update();
        const dist = Math.hypot(player.x - enemy.x, player.y - enemy.y);
        
        if (isHacking) {
            if (dist - enemy.radius - player.radius < 11) {
                setTimeout(() => {
                for (let i = 0; i < enemy.radius * 2; i++) {
                    particles.push(new Particle(enemy.x, enemy.y, Math.random() * 2, enemy.color, {
                        x: (Math.random() - 0.5) * (Math.random() * 8),
                        y: (Math.random() - 0.5) * (Math.random() * 8)}
                    ));
                }}, 0)
                score += 25;
                scoreEl.innerHTML = score;
                setTimeout(() => {
                    enemies.splice(index, 1);
                    projectiles.splice(proIndex, 1);
                }, 0)
            }         
        } else if (dist - enemy.radius - player.radius < 1) {
            cancelAnimationFrame(animationId);
            modal.style.display = 'flex';
            modalScore.innerHTML = score;
        }

Also, this is my first time posting on stack overflow, so if there's something I should've done that I didn't, or vice versa, please let me know!


Solution

  • If supportable by your use case, I suggest using a png with pre-cropped circular transparency to improve performance and avoid having to code this.

    But let's carry on and answer your question from the comment. I felt this was different enough from Canvas clip image with two quadraticCurves to add a new answer, but that thread shows the general approach: use context.clip() after a path with a context.arc, then finish with context.drawImage.

    Since you're drawing other things as well, wrap your clip with context.save() and context.restore() to prevent your clip from affecting everything you draw afterwards.

    Here's a minimal example:

    const canvas = document.createElement("canvas");
    canvas.height = canvas.width = 100;
    const {width: w, height: h} = canvas;
    document.body.appendChild(canvas);
    const ctx = canvas.getContext("2d");
    const img = new Image();
    img.src = `https://picsum.photos/${w}/${h}`;
    img.decode().then(() => {
      ctx.fillRect(10, 0, 20, 20); // normal drawing before save()  
      ctx.save();
      ctx.beginPath();
      ctx.arc(w / 2, h / 2, w / 2, 0, Math.PI * 2);
      ctx.clip();
      ctx.drawImage(img, 0, 0);
      ctx.restore();
      ctx.fillRect(0, 10, 20, 20); // back to normal drawing after restore()
    });

    If you have multiple images, I suggest using promises as described in Images onload in one function.