Steffen Peters: Canvas über MouseWheel skalieren funktioniert nicht

Hallo Leute,

ich versuche mich gerade an einem kleinen Canvas-Projekt. Irgendwann soll damit ein einfaches Organigramm angezeigt (und vielleicht auch noch bearbeitet) werden.

Alles funktioniert in meinem Test soweit schonmal, aber das Vergrößern/Verkleinern über das Mausrad leider nicht so ganz.

Die ursprüngliche Canvas-Anzeige bleibt stehen und die skalierte Version wird erst bei Drag&Drop "mit" angezeigt.

Wieso bleibt das alte Teil stehen und wird nicht durch das clearRect gelöscht?

Hier der aktuelle Source-Code:

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Canvas</title>
<style>
.canvas-container {
	text-align: center;
}
.canvas {
	border: 5px solid black;
	background-color: #fff;
}
</style>
</head>
<body>
<div class="canvas-container">
	<canvas id="canvas" class="canvas"></canvas>
</div>
<script>
let canvas = document.getElementById("canvas");
let sizeX = window.innerWidth - 30 ;
let sizeY = window.innerHeight - 60 ;
canvas.style.width = sizeX + "px";
canvas.style.height = sizeY + "px";
let scale = window.devicePixelRatio;
canvas.width = sizeX * scale;
canvas.height = sizeY * scale;
let canvas_width = canvas.width;
let canvas_height = canvas.height;

let ctx = canvas.getContext("2d");
ctx.scale(scale, scale);

let shapes = [];
let current_shape_index = null;
let is_dragging = false;
let startX;
let startY;
let lastX = canvas.width/2
let lastY = canvas.height/2;
let scaleFactor = 0.2;
let originx = 0;
let originy = 0;

shapes.push( { id: "1", form: 'rect', x:50, y:50, width: 180, height: 100, color: 'blue'} );
shapes.push( { id: "2", form: 'rect', x:150, y:250, width: 180, height: 100, color: 'red'} );
shapes.push( { id: "3", form: 'connector', from: "1", to: "2", from_point: "B", to_point: "T", color: 'black'} );
shapes.push( { id: "4", form: 'rect', x:350, y:150, width: 180, height: 100, color: 'lightblue'} );
shapes.push( { id: "5", form: 'connector', from: "2", to: "4", from_point: "R", to_point: "L", color: 'black'} );

let offset_x;
let offset_y;
let get_offset = function() {
	let canvas_offsets = canvas.getBoundingClientRect();
	offset_x = canvas_offsets.left;
	offset_y = canvas_offsets.top;
}

get_offset();
window.onscroll = function() {
	get_offset();
}
window.onresize = function() {
	get_offset();
}
window.onscroll = function() {
	get_offset();
}
canvas.onresize = function() {
	get_offset();
}

let is_mouse_in_shape = function(x, y, shape) {
	let shape_left = shape.x;
	let shape_right = shape.x + shape.width;
	let shape_top = shape.y;
	let shape_bottom = shape.y + shape.height;
	if (x > shape_left && x < shape_right && y > shape_top && y < shape_bottom) {
		return true;
	}
	return false;
}

let mouse_down = function(event) {
	event.preventDefault();

	startX = parseInt(event.clientX) - offset_x;
	startY = parseInt(event.clientY) - offset_y;

	let index = 0;
	for (let shape of shapes) {
		if (is_mouse_in_shape(startX, startY, shape)) {
			current_shape_index = index;
			is_dragging = true;
			return;
		}
		index ++;
	}
}

let mouse_out = function(event) {
	if (!is_dragging) {
		return;
	}
	event.preventDefault();
	is_dragging = false;
}

let mouse_up = function(event) {
	if (!is_dragging) {
		return;
	}
	event.preventDefault();
	is_dragging = false;
}

let mouse_move = function(event) {
	if (!is_dragging) {
		return;
	}
	event.preventDefault();
	let mouseX = parseInt(event.clientX) - offset_x;
	let mouseY = parseInt(event.clientY) - offset_y;
	let dx = mouseX - startX;
	let dy = mouseY - startY;

	let current_shape = shapes[current_shape_index];
	current_shape.x += dx;
	current_shape.y += dy;

	draw_shapes();

	startX = mouseX;
	startY = mouseY;
}

let mouse_wheel = function(event) {
	event.preventDefault();
	let mouseX = parseInt(event.clientX) - offset_x;
	let mouseY = parseInt(event.clientY) - offset_y;
	let wheel = event.deltaY < 0 ? 1 : -1;

	let zoom = Math.exp(wheel * scaleFactor);

	ctx.translate(originx, originy);
	ctx.scale(zoom,zoom);
	originx -= mouseX / (scale * zoom) - mouseX / scale;
	originy -= mouseY / (scale * zoom) - mouseY / scale;
	ctx.translate(-originx, -originy);
	scale *= zoom;

	draw_shapes();
	return false;
}

canvas.onmousedown = mouse_down;
canvas.onmouseup = mouse_up;
canvas.onmouseout = mouse_out;
canvas.onmousemove = mouse_move;
canvas.onwheel = mouse_wheel;

let getCoords = function(shape,point) {
    var _x = 0;
    var _y = 0;
	switch (point) {
	case "B":
		_x = shape.x + ( shape.width / 2 );
		_y = shape.y + shape.height;
		break;
	case "T":
		_x = shape.x + ( shape.width / 2 );
		_y = shape.y;
		break;
	case "L":
		_x = shape.x;
		_y = shape.y + ( shape.height / 2 );
		break;
	case "R":
		_x = shape.x + shape.width;
		_y = shape.y + ( shape.height / 2 );
		break;
	}
    return { y: _y, x: _x };
}

let draw_shapes = function() {
	ctx.clearRect(0, 0, canvas_width, canvas_height);

	for (let shape of shapes) {
		switch (shape.form) {
		case "rect":
			ctx.fillStyle = shape.color;
			ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
			break;
		case "connector":
			let from_shape = shapes.filter(s => s.id == shape.from);
			let to_shape = shapes.filter(s => s.id == shape.to);
			let from_shape_point = shape.from_point;
			let to_shape_point = shape.to_point;
			ctx.beginPath();
			let a = getCoords(from_shape[0],from_shape_point);
			ctx.moveTo(a.x,a.y);
			let b = getCoords(to_shape[0],to_shape_point);
			ctx.lineTo(b.x,b.y);
			ctx.lineWidth = 1;
			ctx.strokeStyle = shape.color;
			ctx.stroke();
			break;
		}
	}
}

draw_shapes();

</script>
</body>
</html>

  1. Hallo Steffen,

    ich habe jetzt wenig Lust, mich tief in deinen Code einzuarbeiten, aber dies hier fällt mir auf:

    • Du arbeitest viel zu viel mit globalen Variablen

    • Du definierst Funktionen mit let someFunc = function(..) { ...}; - wieso? Erstens dürftest Du kein Interesse daran haben, diese Variablen zu überschreiben. Es müsste also zumindest const sein. Zum zweiten - was spricht gegen das gute alte function someFunc(...) { ... };? Das reduziert einigen Kopfschmerz, vor allem hast Du dann das function hoisting und bist nicht gezwungen, deinen Code bottom-up zu notieren. Die

    • Du registrierst Events mit window.onXXXX. Das ist eigentlich veraltet, man verwendet heutzutage window.addEventListener("mousedown", function(event) { ... }) - an der Stelle ergeben anonyme Funktionen dann wieder einen Sinn.

    • Du solltest in deinen Canvas-Mouseevents nicht mit clientX und clientY arbeiten, sondern mit offsetX und offsetY. Dann brauchst Du keine eigene Offset-Berechnung mehr, und die Breite der Border ist auch schon berücksichtigt. Die hast Du nämlich vergessen, weswegen die Maus bei Dir 5 Pixel daneben zielt.

    • Du musst allerdings für Zeichenoperationen die aktuelle Skalierung und Translation des Canvas berücksichtigen. Denn auch clientX und clientY sind Bildschirmkoordinaten. Ohne Skalierung und Translation stimmen Bildschirm- und Canvaskoordinaten überein, aber wenn Du um 20% vergrößerst, dann sind an den Offsetkoordinaten (100,100) die Canvas-Koordinaten (83.3, 83.3). D.h. dein mathematisches Modell entspricht nicht der Wirklichkeit.

    • Ich kenne keine Canvas-Funktion, die diese Operation für Dich übernimmt. Eigentlich solltest Du durch Anwendung der im Context hinterlegten Transformationsmatrix deine Mausposition in eine Canvas-Position umrechnen können, und umgekehrt. Mir fehlen im Geometrie-API aber die entsprechenden Operatoren (oder ich bin zu blöd, sie zu finden; das Geometrie-API ist relativ neu und ich habe es noch nicht benutzt). Man braucht dafür aber keine Matrixrechnerei. Ich würde Dir deshalb raten, Dir die Koeffizienten für Translation und Skalierung zu merken und damit die Position in die Canvasposition zu übersetzen.

    Das ist alles nicht simpel, ich fürchte, beim Matheanteil musst Du nochmal von vorn beginnen.

    Rolf

    --
    sumpsi - posui - obstruxi
  2. Hi,

    habe leider keine Zeit den Code im Detail zu prüfen, aber hier eine Idee die sich auf den Zoom konzentriert.

    <!DOCTYPE html>
    <html lang="de">
    <head>
    <meta charset="UTF-8">
    <title>Canvas</title>
    <style>
    .canvas-container {
    	text-align: center;
    }
    .canvas {
    	border: 5px solid black;
    	background-color: #fff;
    }
    </style>
    </head>
    <body>
    
    <canvas id="canvas" width="800" height="600"></canvas>
    <script type="text/javascript">
    const zoomIntensity = 0.2;
    
    const canvas = document.getElementById("canvas");
    let context = canvas.getContext("2d");
    const width = 800;
    const height = 600;
    
    let scale = 1;
    let originx = 0;
    let originy = 0;
    let visibleWidth = width;
    let visibleHeight = height;
    
    
    function draw(){
    
        context.fillStyle = "white";
        context.fillRect(originx, originy, width/scale, height/scale);
    
        context.fillStyle = "black";
        context.fillRect(50, 50, 100, 100);
    
    
        window.requestAnimationFrame(draw);
    }
    
    draw();
    
    canvas.onwheel = function (event){
        event.preventDefault();
    
        const mousex = event.clientX - canvas.offsetLeft;
        const mousey = event.clientY - canvas.offsetTop;
    
        const wheel = event.deltaY < 0 ? 1 : -1;
    
        const zoom = Math.exp(wheel * zoomIntensity);
        
        context.translate(originx, originy);
    
        originx -= mousex/(scale*zoom) - mousex/scale;
        originy -= mousey/(scale*zoom) - mousey/scale;
        
        context.scale(zoom, zoom);
        context.translate(-originx, -originy);
    
        scale *= zoom;
        visibleWidth = width / scale;
        visibleHeight = height / scale;
    }
    
    </script>
    
    
    </body>
    </html>
    
  3. Servus!

    ich versuche mich gerade an einem kleinen Canvas-Projekt. Irgendwann soll damit ein einfaches Organigramm angezeigt (und vielleicht auch noch bearbeitet) werden.

    Alles funktioniert in meinem Test soweit schonmal, aber das Vergrößern/Verkleinern über das Mausrad leider nicht so ganz.

    Bei vergrößern (= Skalieren) denke ich gleich an SVG. Ich hatte schon bei diesem Thread über die "grafische Aufbereitung eines Stammbaums" mit CSS einige Links gesammelt:

    Irgendwann mach ich da mal was.

    Herzliche Grüße

    Matthias Scharwies

    --
    Einfach mal was von der ToDo-Liste auf die Was-Solls-Liste setzen.“