Масштабирование холста до курсора мыши без ctx.scale - PullRequest
2 голосов
/ 14 июля 2020

Я пытаюсь изменить начало масштабирования на курсор мыши вместо верхнего левого угла по умолчанию, и я не могу использовать ctx.translate с ctx.scale, поскольку буфер сетки необходимо перерисовать и его нельзя масштабировать ( одна линия всегда должна быть шириной 1 пиксель). Сетка можно масштабировать и перемещать, это просто неверное начало координат.

Я не понимаю, как вычислить новые координаты x и новые координаты y сетки после масштабирования.

Важный фрагмент кода и то, что я уже пробовал, прокомментированы в классе Camera.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

// utils //
function getCursorPos(evt) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: Math.floor(((evt.clientX - rect.left) / (rect.right - rect.left)) * canvas.offsetWidth),
        y: Math.floor(((evt.clientY - rect.top) / (rect.bottom - rect.top)) * canvas.offsetHeight),
    };
}
//////////

const scene = {
    renderer: canvas,
    context: ctx,
    width: 1200,
    height: 1000,
    cellSize: 30,
    render: function (buffer, x, y) {
        this.context.clearRect(0, 0, this.renderer.width, this.renderer.height);
        this.context.drawImage(buffer, x, y);
    },
};

class Grid {
    constructor() {
        this.width = scene.width;
        this.height = scene.height;
        this.cellSize = scene.cellSize;
        this.color = "black";
        this.buffer = document.createElement("canvas");
        this.buffer.width = this.width;
        this.buffer.height = this.height;
    }

    build() {
        // we don't directly make the draw calls on the main canvas (scene.renderer) ,
        // instead we create a buffer (a canvas element in this case),
        // which will be drawn as an image on the main canvas when we call scene.render();
        const ctx = this.buffer.getContext("2d");
        ctx.clearRect(0, 0, this.buffer.width, this.buffer.height);
        ctx.setLineDash([2, 5]);

        for (let u = 0, len = this.height; u < len; u += this.cellSize) {
            ctx.beginPath();
            ctx.moveTo(0.5, u + 0.5);
            ctx.lineTo(0.5 + this.width, u + 0.5);
            ctx.stroke();
        }

        for (let u = 0, len = this.width; u < len; u += this.cellSize) {
            ctx.beginPath();
            ctx.moveTo(u + 0.5, 0.5);
            ctx.lineTo(u + 0.5, 0.5 + this.height);
            ctx.stroke();
        }
    }

    setDimensions(w, h) {
        this.width = w;
        this.height = h;
    }

    getDimensions() {
        return { gw: this.width, gh: this.height };
    }

    setCellSize(size) {
        this.cellSize = size;
    }

    getCellSize() {
        return this.cellSize;
    }

    getBuffer() {
        return this.buffer;
    }
}

class Camera {
    constructor() {
        this.x = 0;
        this.y = 0;
        this.startDrag = null;
        this.zoom = 1;
        this.zoomInc = 0.05;
    }

    // converts screen coordinates to world coordinates
    toWorld(number) {
        return Math.floor(number / this.zoom);
    }

    toScreen(number) {
        return Math.floor(number / this.zoom);
    }

    setStartDrag(coord) {
        this.startDrag = { x: this.x + coord.x, y: this.y + coord.y };
    }

    isStartedDrag() {
        return !!this.startDrag;
    }

    drag(coord) {
        this.x = this.startDrag.x - coord.x;
        this.y = this.startDrag.y - coord.y;
    }

    stopDrag() {
        this.startDrag = null;
    }

    // the bit of code I can't figure //
    setScale({ x, y, deltaY }) {
        const step = deltaY > 0 ? -this.zoomInc : this.zoomInc;
        this.zoom += step;

        // this.x and this.y is where the grid is going to be rendered on the canvas;

        // first I thought about doing it this way :
        //this.x = -this.toScreen(this.toWorld(x) - x);
        //this.y = -this.toScreen(this.toWorld(y) - y);
        // but it only work if the grid is at x: 0 y: 0;

        // after some research I tried to shift x and y relatively to the cursor world position in the grid;
        //const worldPos = { x: this.toWorld(x) - this.x, y: this.toWorld(y) - this.y };
        //this.x = -(this.x - worldPos.x * step);
        //this.y = -(this.y - worldPos.y * step);

        // if x and y aren't changed the zoom origin defaults to the current origin of the camera;
    }

    getZoom() {
        return this.zoom;
    }
}

function init() {
    // initial setup //
    const grid = new Grid();
    const camera = new Camera();
    grid.build();
    const gridBuffer = grid.getBuffer();
    scene.context.drawImage(gridBuffer, 0, 0);

    scene.renderer.addEventListener("mousemove", (evt) => {
        if (camera.isStartedDrag()) {
            camera.drag(getCursorPos(evt));
            scene.render(gridBuffer, -camera.x, -camera.y);
        }
    });

    scene.renderer.addEventListener("mousedown", (evt) => {
        camera.setStartDrag(getCursorPos(evt));
    });

    scene.renderer.addEventListener("mouseup", () => {
        camera.stopDrag();
    });

    scene.renderer.addEventListener("wheel", (evt) => {
        evt.preventDefault();
        camera.setScale(evt);
        const zoom = camera.getZoom();
        grid.setCellSize(scene.cellSize * zoom);
        grid.setDimensions(scene.width * zoom, scene.height * zoom);

        // we rebuild a smaller or bigger grid according to the new zoom level;
        grid.build();
        const gridBuffer = grid.getBuffer();
        scene.render(gridBuffer, -camera.x, -camera.y);
    });
}

init();
    <html lang="en">
    <head>
        <script defer src="main.js"></script>
    </head>
    <body>
        <canvas id="canvas" width="800" height="600" style="border: 1px solid black"></canvas>
    </body>
    </html>

Вот скрипка: https://jsbin.com/wecupoxefe/edit?html, js, вывод

Ответы [ 2 ]

3 голосов
/ 18 июля 2020

Пояснение с кодом:

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

// utils //
function getCursorPos(evt) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: Math.floor(((evt.clientX - rect.left) / (rect.right - rect.left)) * canvas.offsetWidth),
        y: Math.floor(((evt.clientY - rect.top) / (rect.bottom - rect.top)) * canvas.offsetHeight),
    };
}
//////////

const scene = {
    renderer: canvas,
    context: ctx,
    width: 1200,
    height: 1000,
    cellSize: 30,
    render: function (buffer, x, y) {
        this.context.clearRect(0, 0, this.renderer.width, this.renderer.height);
        this.context.drawImage(buffer, x, y);
    },
};

class Grid {
    constructor() {
        this.width = scene.width;
        this.height = scene.height;
        this.cellSize = scene.cellSize;
        this.color = "black";
        this.buffer = document.createElement("canvas");
        this.buffer.width = this.width;
        this.buffer.height = this.height;
    }

    build() {
        // we don't directly make the draw calls on the main canvas (scene.renderer) ,
        // instead we create a buffer (a canvas element in this case),
        // which will be drawn as an image on the main canvas when we call scene.render();
        const ctx = this.buffer.getContext("2d");
        ctx.clearRect(0, 0, this.buffer.width, this.buffer.height);
        ctx.setLineDash([2, 5]);

        for (let u = 0, len = this.height; u < len; u += this.cellSize) {
            ctx.beginPath();
            ctx.moveTo(0.5, u + 0.5);
            ctx.lineTo(0.5 + this.width, u + 0.5);
            ctx.stroke();
        }

        for (let u = 0, len = this.width; u < len; u += this.cellSize) {
            ctx.beginPath();
            ctx.moveTo(u + 0.5, 0.5);
            ctx.lineTo(u + 0.5, 0.5 + this.height);
            ctx.stroke();
        }
    }

    setDimensions(w, h) {
        this.buffer.width = this.width = w; // GT
        this.buffer.height = this.height = h; // GT
    }

    getDimensions() {
        return { gw: this.width, gh: this.height };
    }

    setCellSize(size) {
        this.cellSize = size;
    }

    getCellSize() {
        return this.cellSize;
    }

    getBuffer() {
        return this.buffer;
    }
}

class Camera {
    constructor() {
        this.x = 0;
        this.y = 0;
        this.startDrag = null;
        this.zoom = 1;
        this.zoomInc = 0.05;
    }

    // converts screen coordinates to world coordinates
    toWorld(number) {
        return Math.floor(number / this.zoom);
    }

    toScreen(number) {
        return Math.floor(number / this.zoom);
    }

    setStartDrag(coord) {
        this.startDrag = { x: this.x + coord.x, y: this.y + coord.y };
    }

    isStartedDrag() {
        return !!this.startDrag;
    }

    drag(coord) {
        this.x = this.startDrag.x - coord.x;
        this.y = this.startDrag.y - coord.y;
    }

    stopDrag() {
        this.startDrag = null;
    }

    // the bit of code I can't figure //
    setScale({ x, y, deltaY }) {
        const step = deltaY > 0 ? -this.zoomInc : this.zoomInc;
        if (this.zoom + step <= 0) return // for extra credit ;)
        // Fix x,y:
        x -= canvas.offsetLeft
        y -= canvas.offsetTop
        const zoom = this.zoom // old zoom
        this.zoom += step;
        /* We want in-world coordinates to remain the same:
         * (x + this.x')/this.zoom = (x + this.x)/zoom
         * (y + this.y')/this.zoom = (y + this.y)/zoom
         * =>
         */
         this.x = (x + this.x)*this.zoom/zoom - x
         this.y = (y + this.y)*this.zoom/zoom - y

        // this.x and this.y is where the grid is going to be rendered on the canvas;

        // first I thought about doing it this way :
        //this.x = -this.toScreen(this.toWorld(x) - x);
        //this.y = -this.toScreen(this.toWorld(y) - y);
        // but it only work if the grid is at x: 0 y: 0;

        // after some research I tried to shift x and y relatively to the cursor world position in the grid;
        //const worldPos = { x: this.toWorld(x) - this.x, y: this.toWorld(y) - this.y };
        //this.x = -(this.x - worldPos.x * step);
        //this.y = -(this.y - worldPos.y * step);

        // if x and y aren't changed the zoom origin defaults to the current origin of the camera;
    }

    getZoom() {
        return this.zoom;
    }
}

function init() {
    // initial setup //
    const grid = new Grid();
    const camera = new Camera();
    grid.build();
    const gridBuffer = grid.getBuffer();
    scene.context.drawImage(gridBuffer, 0, 0);

    scene.renderer.addEventListener("mousemove", (evt) => {
        if (camera.isStartedDrag()) {
            camera.drag(getCursorPos(evt));
            scene.render(gridBuffer, -camera.x, -camera.y);
        }
    });

    scene.renderer.addEventListener("mousedown", (evt) => {
        camera.setStartDrag(getCursorPos(evt));
    });

    scene.renderer.addEventListener("mouseup", () => {
        camera.stopDrag();
    });

    scene.renderer.addEventListener("wheel", (evt) => {
        evt.preventDefault();
        camera.setScale(evt);
        const zoom = camera.getZoom();
        grid.setCellSize(scene.cellSize * zoom);
        grid.setDimensions(scene.width * zoom, scene.height * zoom);

        // we rebuild a smaller or bigger grid according to the new zoom level;
        grid.build();
        const gridBuffer = grid.getBuffer();
        scene.render(gridBuffer, -camera.x, -camera.y);
    });
}

init();
    <html lang="en">
    <head>
        <script defer src="main.js"></script>
    </head>
    <body>
        <canvas id="canvas" width="800" height="600" style="border: 1px solid black"></canvas>
    </body>
    </html>
1 голос
/ 16 июля 2020

Недавно я написал простой инструмент для отображения диаграмм с данными о covid-19 в Италии, который решает именно вашу проблему, и я могу сказать, что есть много аспектов, о которых нужно позаботиться. Единственная разница между моей диаграммой и тем, что вы ищете, похоже, заключается в том, что на моей диаграмме горизонтальное и вертикальное масштабирование не зависят.

Вот диаграмма , так что вы можете проверить, делает то, что вам нужно.

Если это то, что вы ищете, я предлагаю проверить класс SurfaceChart , который имеет дело с масштабированием (на основе положения мыши или касания), перетаскиванием диаграммы (касанием и мышь) и с сеткой шириной 1 пиксель. Мы говорим о 300 строках кода, поэтому я не буду их объяснять, я предлагаю взглянуть и, если вам нужны пояснения, не стесняйтесь спрашивать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...