如何让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>
关键修改说明
- CSS统一:把
select-container的样式和#canvas对齐,去掉了多余的flex:1,确保容器的尺寸计算逻辑和Canvas完全一样,这样两者的显示尺寸就会同步。 - 添加Stage适配函数:
fitStageIntoContainer函数会计算容器和原始1000x1000尺寸的比例,设置Stage的scale属性,让Stage内容缩放后刚好填满容器,同时保持居中。 - 监听窗口resize:每次窗口大小变化时自动重新适配,保证在任何屏幕尺寸下都对齐。
- 坐标一致性:Konva的
stage.getPointerPosition()会自动处理缩放后的坐标,所以选择区域的逻辑不需要额外修改,获取的坐标和Canvas的原始坐标是完全对应的,确保选择、裁剪的位置准确。
现在你可以运行修改后的代码,试试调整窗口大小——Konva容器和Canvas会完美同步缩放,选择、拖拽、复制粘贴的操作也都能准确对应Canvas的内容啦!
备注:内容来源于stack exchange,提问作者PPExpress
相关产品推荐
相关产品推荐

