Zooming stage relative to pointer position
This demo shows how to implement zooming that is relative to the mouse pointer position. This creates a more natural zooming experience where the content scales around the mouse cursor.
Instructions: Use your mouse wheel or trackpad to zoom in and out. Notice how the content scales around the position of your cursor, rather than the center of the stage.
- Vanilla
- React
- Vue
import Konva from 'konva'; const width = window.innerWidth; const height = window.innerHeight; const stage = new Konva.Stage({ container: 'container', width: width, height: height, }); const layer = new Konva.Layer(); stage.add(layer); const circle = new Konva.Circle({ x: stage.width() / 2, y: stage.height() / 2, radius: 50, fill: 'green', }); layer.add(circle); const scaleBy = 1.01; stage.on('wheel', (e) => { // stop default scrolling e.evt.preventDefault(); const oldScale = stage.scaleX(); const pointer = stage.getPointerPosition(); const mousePointTo = { x: (pointer.x - stage.x()) / oldScale, y: (pointer.y - stage.y()) / oldScale, }; // how to scale? Zoom in? Or zoom out? let direction = e.evt.deltaY > 0 ? 1 : -1; // when we zoom on trackpad, e.evt.ctrlKey is true // in that case lets revert direction if (e.evt.ctrlKey) { direction = -direction; } const newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy; stage.scale({ x: newScale, y: newScale }); const newPos = { x: pointer.x - mousePointTo.x * newScale, y: pointer.y - mousePointTo.y * newScale, }; stage.position(newPos); });
import { Stage, Layer, Circle } from 'react-konva'; import { useRef } from 'react'; const App = () => { const width = window.innerWidth; const height = window.innerHeight; const stageRef = useRef(null); const handleWheel = (e) => { e.evt.preventDefault(); const stage = stageRef.current; const oldScale = stage.scaleX(); const pointer = stage.getPointerPosition(); const mousePointTo = { x: (pointer.x - stage.x()) / oldScale, y: (pointer.y - stage.y()) / oldScale, }; // how to scale? Zoom in? Or zoom out? let direction = e.evt.deltaY > 0 ? 1 : -1; // when we zoom on trackpad, e.evt.ctrlKey is true // in that case lets revert direction if (e.evt.ctrlKey) { direction = -direction; } const scaleBy = 1.01; const newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy; stage.scale({ x: newScale, y: newScale }); const newPos = { x: pointer.x - mousePointTo.x * newScale, y: pointer.y - mousePointTo.y * newScale, }; stage.position(newPos); }; return ( <Stage width={width} height={height} ref={stageRef} onWheel={handleWheel} > <Layer> <Circle x={width / 2} y={height / 2} radius={50} fill="green" /> </Layer> </Stage> ); }; export default App;
<template> <v-stage ref="stageRef" :config="stageConfig" @wheel="handleWheel" > <v-layer> <v-circle :config="circleConfig" /> </v-layer> </v-stage> </template> <script setup> import { ref, computed } from 'vue'; const width = window.innerWidth; const height = window.innerHeight; const stageRef = ref(null); const stageConfig = computed(() => ({ width, height, })); const circleConfig = computed(() => ({ x: width / 2, y: height / 2, radius: 50, fill: 'green', })); const handleWheel = (e) => { e.evt.preventDefault(); const stage = stageRef.value.getNode(); const oldScale = stage.scaleX(); const pointer = stage.getPointerPosition(); const mousePointTo = { x: (pointer.x - stage.x()) / oldScale, y: (pointer.y - stage.y()) / oldScale, }; // how to scale? Zoom in? Or zoom out? let direction = e.evt.deltaY > 0 ? 1 : -1; // when we zoom on trackpad, e.evt.ctrlKey is true // in that case lets revert direction if (e.evt.ctrlKey) { direction = -direction; } const scaleBy = 1.01; const newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy; stage.scale({ x: newScale, y: newScale }); const newPos = { x: pointer.x - mousePointTo.x * newScale, y: pointer.y - mousePointTo.y * newScale, }; stage.position(newPos); }; </script>