You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让KonvaJS容器/舞台像Canvas一样动态调整尺寸并完美对齐

如何让KonvaJS容器/舞台像Canvas一样动态调整尺寸并完美对齐

我完全懂你现在的困扰——明明给Konva容器和Canvas加了差不多的CSS,但Konva就是没法像Canvas那样跟着屏幕尺寸动态缩放,还和Canvas错位,对吧?这其实是因为Konva的Stage本身不会自动适配容器缩放,得咱们手动加一点逻辑来同步两者的行为。下面咱们一步步把问题解决掉:

问题核心

当前的CSS只处理了容器的尺寸,但Konva Stage的内容还是保持原始的1000x1000尺寸,没有跟着容器缩放;另外select-container的CSS规则和Canvas不完全一致,导致容器本身就没对齐。

完整解决方案(修改后的代码)

<!DOCTYPE html>
<html>
  <head>
    <script src="https://unpkg.com/konva@9.3.18/konva.min.js"></script>
    <meta charset="utf-8" />
    <title>Canvas to Konva Object</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        overflow: hidden;
        background-color: #f0f0f0;
      }
      .over-container {
            display: flex;
            flex-direction: column;
            width: 100%;
            height: 100vh;
            align-items: center;
            margin-top: 35px;
        }

        #canvas-container {
            position: relative;
            width: 100%;
            overflow: hidden;
            padding-top: 100%; /* 1:1 宽高比 */
            display: flex;
            justify-content: center;
        }

        #select-container {
            cursor: crosshair;
            position: absolute;
            top: 0;
            left: 50%;
            transform: translate(-50%);
            max-width: calc(100vw - 10px);
            border: 1px solid #571bfa;
            /* 和Canvas保持一致的尺寸规则 */
            touch-action: none;
        }

        #canvas {
            cursor: crosshair;
            position: absolute;
            top: 0;
            left: 50%;
            transform: translate(-50%);
            max-width: calc(100vw - 10px);
            touch-action: none; 
            image-rendering: pixelated; 
            border: 1px solid #000;
        }
    </style>
  </head>
  <body>
    <div class="over-container" id="Ocontainer">
        <div id="canvas-container">
            <canvas id="canvas"></canvas>
            <div id="select-container"></div>
        </div>
    </div>
    <script>
        var width = 1000;
        var height = 1000;

        // Get the canvas and set dimensions
        var canvas = document.getElementById('canvas');
        canvas.width = width;
        canvas.height = height;

        var ctx = canvas.getContext('2d');
        ctx.fillStyle = 'blue';
        ctx.fillRect(50, 50, 300, 200);
        ctx.fillStyle = 'orange';
        ctx.fillRect(400, 100, 200, 300);

        // Create Konva stage and layer
        var stage = new Konva.Stage({
            container: 'select-container',
            width: width,
            height: height,
        });

        var layer = new Konva.Layer();
        stage.add(layer);

        var selectionRectangle = new Konva.Rect({
            fill: 'rgba(0, 0, 255, 0.5)',
            visible: false,
            listening: false,
        });
        layer.add(selectionRectangle);

        var x1, y1, x2, y2, selecting = false, activeObject = null;
        var copiedObject = null;
        var objects = [];
        var transformers = []; // Keep track of transformers for cleanup

        // Helper function to create a transformer for a given object
        function createTransformer(object) {
            const transformer = new Konva.Transformer({
            nodes: [object],
            });
            layer.add(transformer);
            transformer.update();
            transformers.push(transformer); // Keep track of the transformer
            return transformer;
        }

        // 核心:适配Stage到容器的函数
        function fitStageIntoContainer() {
            const container = document.getElementById('select-container');
            // 获取容器的当前显示尺寸
            const containerWidth = container.offsetWidth;
            const containerHeight = container.offsetHeight;
            
            // 计算缩放比例,保持1000x1000的原始比例
            const scale = Math.min(containerWidth / width, containerHeight / height);
            // 设置Stage的缩放
            stage.scale({ x: scale, y: scale });
            // 调整Stage的位置,保持居中(因为容器是translate(-50%),Stage本身也要居中)
            stage.position({
                x: (containerWidth - width * scale) / 2,
                y: (containerHeight - height * scale) / 2
            });
            // 重新绘制
            stage.batchDraw();
        }

        // 初始化时适配一次
        fitStageIntoContainer();
        // 监听窗口resize事件,实时适配
        window.addEventListener('resize', fitStageIntoContainer);

        stage.on('mousedown touchstart', (e) => {
            if (e.target !== stage) return;

            e.evt.preventDefault();
            x1 = stage.getPointerPosition().x;
            y1 = stage.getPointerPosition().y;
            selecting = true;

            selectionRectangle.setAttrs({
            visible: true,
            x: x1,
            y: y1,
            width: 0,
            height: 0,
            });
        });

        stage.on('mousemove touchmove', (e) => {
            if (!selecting) return;

            e.evt.preventDefault();
            x2 = stage.getPointerPosition().x;
            y2 = stage.getPointerPosition().y;

            let width = Math.abs(x2 - x1);
            let height = Math.abs(y2 - y1);

            // Check if Shift key is held
            if (e.evt.shiftKey) {
            // Force square by setting width and height to the smaller of the two
            const sideLength = Math.min(width, height);
            width = sideLength;
            height = sideLength;

            // Adjust x2 and y2 to maintain the square shape
            if (x2 < x1) x2 = x1 - width; // Adjust for leftward movement
            else x2 = x1 + width; // Adjust for rightward movement

            if (y2 < y1) y2 = y1 - height; // Adjust for upward movement
            else y2 = y1 + height; // Adjust for downward movement
            }

            selectionRectangle.setAttrs({
            x: Math.min(x1, x2),
            y: Math.min(y1, y2),
            width: width,
            height: height,
            });

            layer.batchDraw();
        });

        stage.on('mouseup touchend', () => {
            if (!selecting) return;
            selecting = false;
            selectionRectangle.visible(false);

            const box = selectionRectangle.getClientRect();
            const sx = box.x;
            const sy = box.y;
            const sw = box.width;
            const sh = box.height;

            if (sw === 0 || sh === 0) return;

            // 注意:这里的sx、sy是Konva缩放后的坐标,和Canvas的原始坐标是对应的,因为我们的缩放比例和Canvas一致
            // Get the selected area from the visible canvas
            const imageData = ctx.getImageData(sx, sy, sw, sh);

            // Remove the selected portion from the canvas
            ctx.clearRect(sx, sy, sw, sh);

            // Create an off-screen canvas to crop the image data
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = sw;
            tempCanvas.height = sh;
            const tempCtx = tempCanvas.getContext('2d');
            tempCtx.putImageData(imageData, 0, 0);

            // Create a Konva.Image from the cropped area
            const image = new Konva.Image({
                x: sx,
                y: sy,
                image: tempCanvas,
                draggable: true,
            });
            layer.add(image);
            objects.push(image);

            // Create a transformer for this object
            createTransformer(image);

            activeObject = image;
            layer.batchDraw();
        });

        window.addEventListener('keydown', (e) => {
            if (e.ctrlKey && e.key === 'c' && activeObject) {
                // Copy the active object
                const clonedCanvas = document.createElement('canvas');
                clonedCanvas.width = activeObject.image().width;
                clonedCanvas.height = activeObject.image().height;
                const clonedCtx = clonedCanvas.getContext('2d');
                clonedCtx.drawImage(activeObject.image(), 0, 0);

                copiedObject = {
                    x: activeObject.x() + 10,
                    y: activeObject.y() + 10,
                    image: clonedCanvas,
                    rotation: activeObject.rotation(),
                    scaleX: activeObject.scaleX(),
                    scaleY: activeObject.scaleY(),
                };
            }
            if (e.ctrlKey && e.key === 'v' && copiedObject) {
                // Paste the copied object
                const newCopy = new Konva.Image({
                    x: copiedObject.x,
                    y: copiedObject.y,
                    image: copiedObject.image,
                    draggable: true,
                    rotation: copiedObject.rotation,
                    scaleX: copiedObject.scaleX,
                    scaleY: copiedObject.scaleY,
                });
                layer.add(newCopy);
                objects.push(newCopy);

                // Create a transformer for the new copy
                createTransformer(newCopy);

                activeObject = newCopy;
                layer.batchDraw();
            }
        });

        stage.on('click tap', (e) => {
            if (activeObject && e.target === stage) {
            // Apply all objects back to the canvas
            objects.forEach((obj) => {
                const image = obj.image(); // Get the underlying image
                const rotation = obj.rotation(); // Rotation angle in degrees

                // Use getClientRect to get transformed position
                const clientRect = obj.getClientRect();
                const boundingBoxCenterX = clientRect.x + clientRect.width / 2;
                const boundingBoxCenterY = clientRect.y + clientRect.height / 2;

                // Use the original image dimensions
                const originalWidth = image.width;
                const originalHeight = image.height;

                // Use scaleX and scaleY for scaling
                const scaledWidth = originalWidth * obj.scaleX();
                const scaledHeight = originalHeight * obj.scaleY();

                // Save the current canvas state
                ctx.save();

                // Translate to the center of the bounding box
                ctx.translate(boundingBoxCenterX, boundingBoxCenterY);
                // Rotate by the object's rotation
                ctx.rotate((rotation * Math.PI) / 180);
                // Draw the image centered at the translated/rotated origin
                ctx.drawImage(
                    image,
                    -scaledWidth / 2,
                    -scaledHeight / 2,
                    scaledWidth,
                    scaledHeight
                );

                // Restore the canvas state
                ctx.restore();
            });

            // Clear Konva objects and transformers
            objects.forEach(obj => obj.destroy());
            transformers.forEach(t => t.destroy());
            objects = [];
            transformers = [];
            activeObject = null;
            layer.batchDraw();
            }
        });
    </script>
  </body>
</html>

关键修改说明

  1. CSS统一:把select-container的样式和#canvas对齐,去掉了多余的flex:1,确保容器的尺寸计算逻辑和Canvas完全一样,这样两者的显示尺寸就会同步。
  2. 添加Stage适配函数:fitStageIntoContainer函数会计算容器和原始1000x1000尺寸的比例,设置Stage的scale属性,让Stage内容缩放后刚好填满容器,同时保持居中。
  3. 监听窗口resize:每次窗口大小变化时自动重新适配,保证在任何屏幕尺寸下都对齐。
  4. 坐标一致性:Konva的stage.getPointerPosition()会自动处理缩放后的坐标,所以选择区域的逻辑不需要额外修改,获取的坐标和Canvas的原始坐标是完全对应的,确保选择、裁剪的位置准确。

现在你可以运行修改后的代码,试试调整窗口大小——Konva容器和Canvas会完美同步缩放,选择、拖拽、复制粘贴的操作也都能准确对应Canvas的内容啦!

备注:内容来源于stack exchange,提问作者PPExpress

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 12:34:32