<canvas></canvas>
body {
    margin: 0;
    overflow: hidden;
}

canvas {
    image-rendering: pixelated;
}
class EventEmitter {
    constructor() {
        this.handlers = new Map();
    }
    
    on(event, callback) {
        const handlers = this.handlers.has(event)
            ? this.handlers.get(event)
            : new Set();
        handlers.add(callback);
        
        this.handlers.set(event, handlers);
    }
    
    emit(event, ...args) {
        if (this.handlers.has(event)) {
            const handlers = this.handlers.get(event);

            for (const handler of handlers.values()) {
                handler(...args);
            }
        }
    }
}

class Component {
    constructor(context) {
        this.canvas = context.canvas;
        this.context = context;
        this.events = new EventEmitter();
    }
    
    on(event, callback) {
        this.events.on(event, callback);
    }
    
    emit(event, ...args) {
        this.events.emit(event, ...args);
    }
}

class Button extends Component {
    constructor({
        context,
        text = 'Button',
        x = 0,
        y = 0,
        paddingVertical = 8,
        paddingHorizontal = 16,
        fontSize = 16,
        fontFamily = 'Segoe UI',
        background = '#000000',
        color = '#ffffff'
    }) {
        super(context);
        
        this.text = text;
        this.x = x;
        this.y = y;
        this.paddingVertical = paddingVertical;
        this.paddingHorizontal = paddingHorizontal;
        this.fontSize = fontSize;
        this.fontFamily = fontFamily;
        this.background = background;
        this.color = color;
        this.shape = new Path2D();
        this.box = {
            width: null,
            height: null
        };
        
        this.create();
    }
    
    create() {
        this.context.save();

        this.context.font = `${this.fontSize}px ${this.fontFamily}`;
        [this.box.width, this.box.height] = [this.context.measureText(this.text).width, this.fontSize];
        
        this.shape.rect(this.x, this.y, this.box.width + this.paddingHorizontal * 2, this.box.height + this.paddingVertical * 2);
        
        this.context.restore();
    }
    
    draw() {
        this.context.save();
        
        this.context.fillStyle = this.background;
        this.context.fill(this.shape);
        
        this.context.font = `${this.fontSize}px ${this.fontFamily}`;
        this.context.textAlign = 'center';
        this.context.textBaseline = 'middle';
        this.context.fillStyle = this.color;
        this.context.fillText(
            this.text,
            this.x + this.box.width / 2 + this.paddingHorizontal,
            this.y + this.box.height / 2 + this.paddingVertical
        );
        
        this.context.restore();
    }
    
    intersect(event, x, y) {
        if (this.context.isPointInPath(this.shape, x, y)) {
            this.emit(event);
        }
    }
}

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');

const button = new Button({
    context,
    text: 'Start',
    x: 100,
    y: 100
});
button.on('click', () => {
    alert('clicked');
});

const update = (time) => {
    context.clearRect(0, 0, canvas.width, canvas.height);
    
    button.draw();
    
    requestAnimationFrame(update);
};

const resize = () => {
    [canvas.width, canvas.height] = [innerWidth, innerHeight];
};

const init = () => {
    canvas.addEventListener('click', event => {
        button.intersect('click', event.offsetX, event.offsetY);
    });
    
    resize();
    
    requestAnimationFrame(update);
};

window.addEventListener('load', init);
window.addEventListener('resize', resize);

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.