konvajs/konva

After stage scaling, the information returned by the transform. decompose() method shows a positional offset

shijunfeng opened this issue · 6 comments

After adding the wheel event to the Resizing Stress Test with Konva example on the official website, the test found that when the stage is scaled, the information returned by the transform. decompose() method shows a positional shift. The example code is as follows:

<script src="https://unpkg.com/konva@9.3.16/konva.min.js"></script> <title>Konva Resizing Stress Test Demo</title> <style> body { margin: 0; padding: 0; overflow: hidden; background-color: #f0f0f0; } </style>
<script> var width = window.innerWidth; var height = window.innerHeight;
	var stage = new Konva.Stage( {
		container: 'container',
		width: width,
		height: height,
	} );

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

	for ( var i = 0; i < 1; i++ ) {
		var shape = new Konva.Rect( {
			x: Math.random() * window.innerWidth,
			y: Math.random() * window.innerHeight,
			width: 100,
			height: 100,
			name: 'shape',
			fill: Konva.Util.getRandomColor(),
		} );
		layer.add( shape );
	}

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

	var group = new Konva.Group( {
		draggable: true,
	} );
	topLayer.add( group );

	var tr = new Konva.Transformer();
	topLayer.add( tr );

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

	var x1, y1, x2, y2;

	stage.on( 'wheel', ( e ) => {
		e.evt.preventDefault();
		const stageState = {
			width: stage.width(),
			height: stage.height(),
			scale: stage.scaleX(),
			x: stage.x(),
			y: stage.y(),
		};

		const oldScale = stageState.scale;

		const pos = stage.getPointerPosition();
		if ( pos ) {
			const mousePointTo = {
				x: ( pos.x - stageState.x ) / oldScale,
				y: ( pos.y - stageState.y ) / oldScale,
			};

			const direction = e.evt.deltaY > 0 ? -1 : 1;
			const newScale = direction > 0 ? oldScale + 0.1 : oldScale - 0.1;

			if ( newScale >= 0.2 && newScale < 5 ) {
				stage.scale( { x: newScale, y: newScale } );
				stage.position( {
					x: pos.x - mousePointTo.x * newScale,
					y: pos.y - mousePointTo.y * newScale,
				} );
			}
		}
	} );

	stage.on( 'mousedown touchstart', ( e ) => {
		if ( e.target.getParent() === tr ) return;
		if ( e.target.parent === group ) return;

		const pos = stage.getPointerPosition();
		const stageScale = stage.scaleX();
		const stagePos = stage.position();

		// 计算选中区域的初始位置
		if ( pos ) {
			// 坐标需要考虑缩放比例
			x1 = ( pos.x - stagePos.x ) / stageScale;
			y1 = ( pos.y - stagePos.y ) / stageScale;
			x2 = x1;
			y2 = y1;
		}

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

		group.children.slice().forEach( ( shape ) => {
			const transform = shape.getAbsoluteTransform();
			shape.moveTo( layer );
			shape.setAttrs( transform.decompose() );
		} );

		group.setAttrs( {
			x: 0,
			y: 0,
			rotation: 0,
			scaleX: 1,
			scaleY: 1,
		} );
		group.clearCache();
	} );

	stage.on( 'mousemove touchmove', () => {
		const pos = stage.getPointerPosition();
		if ( !selectionRectangle.visible() ) return;

		const stageScale = stage.scaleX();
		const stagePos = stage.position();

		if ( pos ) {
			// 计算选中区域的宽高并考虑缩放比例
			x2 = ( pos.x - stagePos.x ) / stageScale;
			y2 = ( pos.y - stagePos.y ) / stageScale;
		}

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

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

		setTimeout( () => {
			selectionRectangle.visible( false );
		} );

		var shapes = stage.find( '.shape' );
		var box = selectionRectangle.getClientRect();

		shapes.forEach( ( shape ) => {
			var rect = shape.getClientRect();
			var intersected = Konva.Util.haveIntersection( box, rect );
			if ( intersected ) {
				group.add( shape );
			} else {
				layer.add( shape );
			}
		} );

		if ( group.children.length ) {
			tr.nodes( [group] );
			group.cache();
		} else {
			tr.nodes( [] );
			group.clearCache();
		}
	} );

	stage.on( 'click tap', function ( e ) {
		const pos = stage.getPointerPosition();
		document.getElementById( "tess" ).value = pos.x + "|" + pos.y;

		if ( selectionRectangle.visible() ) return;
		if ( e.target === stage ) {
			tr.nodes( [] );
			return;
		}
	} );
</script>

Can you make the demo smaller and step to reproduce? I don't understand the issue.

The phenomenon that occurs is that after using the mouse wheel to trigger the wheel event to zoom the stage, the selected node is then clicked on any area to trigger the mousedown event. The selected node will be position shifted and scaled. I tested and found that shape.setAttrs( transform.decompose () ) the position returned by transform.decompose() is wrong

Transformation attributes of Konva.Transformer are not designed for re-user. Konva.Transformer is ignoring any transformation of parent containers (so it looks consistent on the screen).

So how should I deal with it correctly? The current goal is to optimize the efficiency of batch movement, scaling, and rotation after multiple selections, so I refer to the examples on the official website to do it. If I encounter this problem, I cannot solve it.

Looks like my previous comment is not relevant. The solution can be.

// instead of this:
const transform = shape.getAbsoluteTransform();

// write this:
const transform = shape.getAbsoluteTransform(stage);

In the second case, the transformation matrix will be relative to stage (so stage transforms are ignored).
So when you apply them again via transform.decompose() you don't have double apply of stage transform.

I get it,thank you