Dot絵エディタ6 : ViewPort

ViewPort

最も単純なマウス座標->グリッド座標の変換

    // handleMouseDown(event) {
    //     const rect = this.canvas.getBoundingClientRect();
    //     const x = event.clientX - rect.left;
    //     const y = event.clientY - rect.top;
    //     const cell = this.getGridIndex(x, y);

    //     this.isDrawing = true;
    //     this.lastCell = cell;
    //     this.toolManager.getCurrentTool().handleMouseDown(this, cell);
    //     this.draw();
    // }

    // handleMouseMove(event) {
    //     if (!this.isDrawing) return;

    //     const rect = this.canvas.getBoundingClientRect();
    //     const x = event.clientX - rect.left;
    //     const y = event.clientY - rect.top;
    //     const cell = this.getGridIndex(x, y);

    //     if (cell.row !== this.lastCell.row || cell.col !== this.lastCell.col) {
    //         this.toolManager.getCurrentTool().handleMouseMove(this, cell);
    //         this.lastCell = cell;
    //         this.draw();
    //     }
    // }

    // handleMouseUp(event) {
    //     if (!this.isDrawing) return;

    //     const rect = this.canvas.getBoundingClientRect();
    //     const x = event.clientX - rect.left;
    //     const y = event.clientY - rect.top;
    //     const cell = this.getGridIndex(x, y);

    //     this.isDrawing = false;
    //     this.toolManager.getCurrentTool().handleMouseUp(this, cell);
    //     this.draw();
    // }

グリッド座標への変換

    getGridIndex(x, y) {
        const col = Math.floor(x / this.cellWidth);
        const row = Math.floor(y / this.cellHeight);
        return { row, col };
    }

ViewPortを考慮する

マウス座標も必要になったのでhandleに渡す引数が(e, grid, cell)に。
cellはグリッド座標。

    // handleMouseDown(event) {
    //     const viewManager = ViewManager.getInstance(this);
    //     const rect = this.canvas.getBoundingClientRect();
    //     const x = (event.clientX - rect.left) / viewManager.viewport.scale + viewManager.viewport.x;
    //     const y = (event.clientY - rect.top) / viewManager.viewport.scale + viewManager.viewport.y;
    //     const cell = this.getGridIndex(x, y);

    //     this.isDrawing = true;
    //     this.lastCell = cell;
    //     this.toolManager.getCurrentTool().handleMouseDown(event, this, cell);
    //     this.draw();
    // }

    // handleMouseMove(event) {
    //     if (!this.isDrawing) return;

    //     const viewManager = ViewManager.getInstance(this);
    //     const rect = this.canvas.getBoundingClientRect();
    //     const x = (event.clientX - rect.left) / viewManager.viewport.scale + viewManager.viewport.x;
    //     const y = (event.clientY - rect.top) / viewManager.viewport.scale + viewManager.viewport.y;
    //     const cell = this.getGridIndex(x, y);

    //     if (cell.row !== this.lastCell.row || cell.col !== this.lastCell.col) {
    //         this.toolManager.getCurrentTool().handleMouseMove(event, this, cell);
    //         this.lastCell = cell;
    //         this.draw();
    //     }
    // }

    // handleMouseUp(event) {
    //     if (!this.isDrawing) return;

    //     const viewManager = ViewManager.getInstance(this);
    //     const rect = this.canvas.getBoundingClientRect();
    //     const x = (event.clientX - rect.left) / viewManager.viewport.scale + viewManager.viewport.x;
    //     const y = (event.clientY - rect.top) / viewManager.viewport.scale + viewManager.viewport.y;
    //     const cell = this.getGridIndex(x, y);

    //     this.isDrawing = false;
    //     this.toolManager.getCurrentTool().handleMouseUp(event, this, cell);
    //     this.draw();
    // }


ViewPortはCanvasのtransformを用いた変形を管理する。

class ViewManager {
    constructor(grid) {
        if (ViewManager.instance) {
            return ViewManager.instance;
        }
        ViewManager.instance = this;
        
        this.grid = grid;
        this.viewport = {
            x: 0,
            y: 0,
            scale: 1,
            rotation: 0
        };
        this.originalState = {
            x: 0,
            y: 0,
            scale: 1,
            rotation: 0
        };
    }

    static getInstance(grid) {
        if (!ViewManager.instance) {
            ViewManager.instance = new ViewManager(grid);
        }
        return ViewManager.instance;
    }

    rotate(angle) {
        this.viewport.rotation += angle;
        this.grid.draw();
    }

    zoom(factor, centerX, centerY) {
        const oldScale = this.viewport.scale;
        this.viewport.scale *= factor;
        
        // Adjust pan to keep the center point stationary
        this.viewport.x += (centerX / oldScale - centerX / this.viewport.scale);
        this.viewport.y += (centerY / oldScale - centerY / this.viewport.scale);
        
        this.grid.draw();
    }

    pan(dx, dy) {
        this.viewport.x += dx / this.viewport.scale;
        this.viewport.y += dy / this.viewport.scale;
        this.grid.draw();
    }

    reset() {
        this.viewport = {...this.originalState};
        this.grid.draw();
    }

    // applyTransform(ctx) {
    //     ctx.save();
    //     ctx.scale(this.viewport.scale, this.viewport.scale);
    //     ctx.translate(-this.viewport.x, -this.viewport.y);
    // }
    applyTransform(ctx) {
        ctx.save();
        ctx.translate(this.grid.width / 2, this.grid.height / 2);
        ctx.rotate(this.viewport.rotation);
        ctx.scale(this.viewport.scale, this.viewport.scale);
        ctx.translate(-this.grid.width / 2 - this.viewport.x, -this.grid.height / 2 - this.viewport.y);
    }    

    restoreTransform(ctx) {
        ctx.restore();
    }

    getViewportParams() {
        return {
            x: this.viewport.x.toFixed(2),
            y: this.viewport.y.toFixed(2),
            scale: this.viewport.scale.toFixed(2),
            rotation: (this.viewport.rotation * 180 / Math.PI).toFixed(2) // Convert to degrees
        };
    }    

    updateViewportInfo() {
        const params = this.getViewportParams();
        document.getElementById('viewportX').textContent = params.x;
        document.getElementById('viewportY').textContent = params.y;
        document.getElementById('viewportScale').textContent = params.scale;
        document.getElementById('viewportRotation').textContent = params.rotation;
    }    
}

回転考慮

本来不要な機能だが、秒で実装してくれたのでつけた。
->バグった。
回転には対応してるがスケールと平行移動でずれる。

    handleMouseDown(event) {
        const viewManager = ViewManager.getInstance(this);
        const rect = this.canvas.getBoundingClientRect();
        const [x, y] = this.getRotatedCoordinates(event, rect, viewManager);
        const cell = this.getGridIndex(x, y);

        this.isDrawing = true;
        this.lastCell = cell;
        this.toolManager.getCurrentTool().handleMouseDown(event, this, cell);
        this.draw();
    }

    handleMouseMove(event) {
        if (!this.isDrawing) return;

        const viewManager = ViewManager.getInstance(this);
        const rect = this.canvas.getBoundingClientRect();
        const [x, y] = this.getRotatedCoordinates(event, rect, viewManager);
        const cell = this.getGridIndex(x, y);

        if (cell.row !== this.lastCell.row || cell.col !== this.lastCell.col) {
            this.toolManager.getCurrentTool().handleMouseMove(event, this, cell);
            this.lastCell = cell;
            this.draw();
        }
    }

    handleMouseUp(event) {
        if (!this.isDrawing) return;

        const viewManager = ViewManager.getInstance(this);
        const rect = this.canvas.getBoundingClientRect();
        const [x, y] = this.getRotatedCoordinates(event, rect, viewManager);
        const cell = this.getGridIndex(x, y);

        this.isDrawing = false;
        this.toolManager.getCurrentTool().handleMouseUp(event, this, cell);
        this.draw();
    }

回転後マウス座標->非回転マウス座標へ

    getRotatedCoordinates(event, rect, viewManager) {
        // Calculate the center of the canvas
        const centerX = rect.width / 2;
        const centerY = rect.height / 2;

        // Translate mouse coordinates to be relative to the center
        let mouseX = event.clientX - rect.left - centerX;
        let mouseY = event.clientY - rect.top - centerY;

        // Apply inverse rotation
        const rotation = -viewManager.viewport.rotation;
        const rotatedX = mouseX * Math.cos(rotation) - mouseY * Math.sin(rotation);
        const rotatedY = mouseX * Math.sin(rotation) + mouseY * Math.cos(rotation);

        // Translate back and apply scale and pan
        const x = (rotatedX + centerX) / viewManager.viewport.scale + viewManager.viewport.x;
        const y = (rotatedY + centerY) / viewManager.viewport.scale + viewManager.viewport.y;

        return [x, y];
    }    


バグり果てた末

いまんとこ一番調子いい。
ハンドラは変わらず。

    applyTransform(ctx) {
        ctx.save();
        ctx.scale(this.viewport.scale, this.viewport.scale);
        ctx.translate(-this.viewport.x, -this.viewport.y);
        ctx.translate(this.grid.width / 2, this.grid.height / 2);
        ctx.rotate(this.viewport.rotation);
        ctx.translate(-this.grid.width / 2, -this.grid.height / 2);
    }    


    getRotatedCoordinates(event, rect, viewManager) {
        // Calculate the center of the canvas
        const centerX = rect.width / 2;
        const centerY = rect.height / 2;
    
        // Translate mouse coordinates to be relative to the center
        let mouseX = event.clientX - rect.left - centerX;
        let mouseY = event.clientY - rect.top - centerY;
    
        // Apply inverse scale
        mouseX /= viewManager.viewport.scale;
        mouseY /= viewManager.viewport.scale;
    
        // Apply inverse rotation
        const rotation = -viewManager.viewport.rotation;
        const cos = Math.cos(rotation);
        const sin = Math.sin(rotation);
        const rotatedX = mouseX * cos - mouseY * sin;
        const rotatedY = mouseX * sin + mouseY * cos;
    
        // Translate back to the canvas coordinate system and apply pan
        const x = rotatedX + centerX / viewManager.viewport.scale + viewManager.viewport.x;
        const y = rotatedY + centerY / viewManager.viewport.scale + viewManager.viewport.y;
    
        return [x, y];
    }

この記事が気に入ったらサポートをしてみませんか?