Search code examples
javascriptcanvaspath-2d

On a canvas, why does a Path2D object still pick up on event listener at original location, even after the object has moved?


I'm making a game via canvas and Javascript.

I have several Path2D objects on the canvas, and I want one of the objects to move when you have it selected(defined as a function within its class) and click on a different Path2D object.

The 'selected' & unselected methods on my class work: they outline the shape in cyan to show it's selected, and when clicked again, it clears the rectangle and redraws the shape without the border.

I have a listener on a different shape(const = grinder), saying if the original shape(const = portafilter) is selected, and you click on the grinder, move the portafilter to grinder, change its size a bit and unselect the portafilter. It works.

However, even though the portafilter is on the grinder now, if you click the original location of the portafilter at the espresso machine, it still activates the event listener and the methods associated with it. I would expect the canvas wouldn't register the portafilter being there anymore, since I cleared the area before moving it..

Also, the portafilter is redrawn at the original location and the new location, even though the method uses the instances current coordinates, which are set to the new location before calling the 'draw' method.

I've tried rewriting the methods on the portafilter class to prevent the drawing twice from happening and clearing the entire canvas before redrawing everything back into new positions. Even when the canvas is entirely blank, the event listener still sees that initial position as the portafilter.

Is there a way to prevent the canvas from recognizing the first instance of a Path2D object as permanent?

here's a link to codepen to see the barebones working quickly: https://codepen.io/d-nnym/pen/vYzPJWw

here's the JS:

const canvasContainer = document.querySelector('.canvas-container');
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const canvasWidth = canvas.width = canvasContainer.offsetWidth;
const canvasHeight = canvas.height = canvasContainer.offsetHeight;


// functions that apply proportions of canvas aspect ratio to make creating/moving entities at
// different screen sizes easier

function proportionY(value) {
    const referenceHeight = 1105.875;
    const proportion = value / referenceHeight;

    const output = canvasHeight * proportion;
    return output;
}

function proportionX(value) {
    const referenceWidth = 1966;
    const proportion = value / referenceWidth;

    const output = canvasWidth * proportion;
    return output;
}

// draws objects to canvas
function redrawObjects() {
    ctx.fillStyle = 'purple';  
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    espressoMachine.draw(espressoMachine);
    grinder.draw(grinder);
    portafilter.draw(portafilter);
}


// CLASSES

// base for all objects, using Path2D to allow use of .isPointInPath method
// img property just means color right now
class Entity extends Path2D {
    constructor(positionX, positionY, width, height, img) {
            super();
            this.positionX = positionX;
            this.positionY = positionY;
            this.width = width;
            this.height = height;
            this.img = img;
    }

    draw(item) {
        item.rect(this.positionX, this.positionY, this.width, this.height);
        ctx.fillStyle = this.img;
        ctx.fill(item);
    
    }

}

class Grinder extends Entity {
    constructor(positionX, positionY, width, height, img) {
        super(positionX, positionY, width, height, img);
        this.positionX = positionX;
        this.positionY = positionY;
        this.width = width;
        this.height = height;
        this.img = img;

        this.active = false;
    }
}


class EspressoMachine extends Entity {
    constructor(positionX, positionY, width, height, img) {
        super(positionX, positionY, width, height, img);
        this.positionX = positionX;
        this.positionY = positionY;
        this.width = width;
        this.height = height;
        this.img = img;

        this.active = false;
        

    }
}

class Portafilter extends Entity {
    constructor(positionX, positionY, width, height, img) {
        super(positionX, positionY, width, height, img);
        this.positionX = positionX;
        this.positionY = positionY;
        this.width = width;
        this.height = height;
        this.img = img;

        this.active = false;
        this.inEspressoMachine = true;
        this.atGrinder = false;
      
    }

    unselected() {
        ctx.clearRect(this.positionX, this.positionY, this.width, this.height);
        portafilter.active = false;
        portafilter.draw(portafilter);
    }

    selected() {
        ctx.beginPath();
        ctx.strokeStyle = 'cyan';
        ctx.lineWidth = 2;
        //make border visible
        ctx.strokeRect(this.positionX + 1, this.positionY + 1, this.width - 2, this.height - 1.75);
        portafilter.active = true;

    }

}





// declaring objects to draw
const grinder = new Grinder(proportionX(100), proportionY(400), proportionX(250), proportionY(400), 'blue');

const espressoMachine = new EspressoMachine(proportionX(900), proportionY(400), proportionX(800), proportionY(400), 'green');

const portafilter = new Portafilter(proportionX(1025), proportionY(525), proportionX(185), proportionY(65), 'maroon')




// temporary background
ctx.fillStyle = 'purple';  
ctx.fillRect(0, 0, canvas.width, canvas.height);



// listener toggling portafilter active/inactive
canvas.addEventListener('click', (event) => {
    if(ctx.isPointInPath(portafilter, event.offsetX, event.offsetY) && !portafilter.active) {
        portafilter.selected();
        return;
    } 

    if (ctx.isPointInPath(portafilter, event.offsetX, event.offsetY) && portafilter.active) {
        portafilter.unselected();
        return;
    }
})



// move portafilter to grinder
canvas.addEventListener('click', (event) => {
    if(ctx.isPointInPath(grinder, event.offsetX, event.offsetY) && portafilter.active && portafilter.atGrinder == false) {
        // clear portafilter from espresso machine
        ctx.clearRect(0, 0, canvasWidth, canvasHeight);
        portafilter.unselected();
        portafilter.atGrinder = true;
        portafilter.inEspressoMachine = false;
        portafilter.positionX = grinder.positionX * 1.75;
        portafilter.positionY = grinder.positionY * 1.5;
        portafilter.width = grinder.width / 3;
        //draw portafilter at grinder with new coordinates
        redrawObjects();
    }
})



//initial drawing
espressoMachine.draw(espressoMachine);
grinder.draw(grinder);
portafilter.draw(portafilter);

Thank you for looking...


Solution

  • Path2D#rect() will add a new rectangle to the Path2D instance every time it's called. There is no beginPath() equivalent, you need to use a new instance instead.

    So for what you do, extending Path2D may not be the best option. Instead you could add a hitTest() method on your own class, which would call the context's isPointInPath() with the updated rectangle.

    class Entity {
    
        constructor(positionX, positionY, width, height, img) {
            this.positionX = positionX;
            this.positionY = positionY;
            this.width = width;
            this.height = height;
            this.img = img;
        }
    
        get path() {
            // this is a naive implementation that does return a new object every time. 
            // The best would be to change all the properties to setters and raise a "#dirty" flag
            // so we can create a new object only when needed
            const path = new Path2D();
            path.rect(this.positionX, this.positionY, this.width, this.height);
            return path;
        }
    
        draw(ctx) {
            ctx.fillStyle = this.img;
            ctx.fill(this.path); 
        }
    
        hitTest(ctx, x, y) {
            return ctx.isPointInPath(this.path, x, y);
        }
    
    }
    

    Now all your entities will have only their current rectangle.