Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Undo" canvas transformations for writing text

When applying a transformation with canvas, the resulting text is also (obviously) transformed. Is there a way to prevent certain transformations, such as reflection, of affecting text?

For example, I set a global transformation matrix so the Y-axis points upwards, X-axis to the right, and the (0, 0) point is in the center of the screen (what you'd expect of a mathematical coordinate system).

However, this also makes the text upside-down.

const size = 200;

const canvas = document.getElementsByTagName('canvas')[0]
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');

ctx.setTransform(1, 0, 0, -1, size / 2, size / 2);

const triangle = [
  {x: -70, y: -70, label: 'A'},
  {x:  70, y: -70, label: 'B'},
  {x:   0, y:  70, label: 'C'},
];

// draw lines  
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.moveTo(triangle[2].x, triangle[2].y);
triangle.forEach(v => ctx.lineTo(v.x, v.y));
ctx.stroke();
ctx.closePath();
  
// draw labels
ctx.textAlign = 'center';
ctx.font = '24px Arial';
triangle.forEach(v => ctx.fillText(v.label, v.x, v.y - 8));
<canvas></canvas>

Is there a "smart" way to get the text in "correct" orientation, apart from manually resetting transformation matrices?

like image 859
Lazar Ljubenović Avatar asked Jan 09 '17 16:01

Lazar Ljubenović


People also ask

How do I change text in canvas?

In the canvas toolbar, click the Select/Transform tool, then double-click text in the canvas. In the Layer's list, select a text layer, then in the Text editor (at the bottom of the Text Inspector's Format pane), drag within or double-click text.

How do I remove text from canvas?

You use context. clearRect(), but first you have to figure out the rectangle to clear. This is based off a number of factors, such as the size of the text and the textAlign property of the canvas context when the text was originally drawn.


2 Answers

To build off of Tai's answer, which is fantastic, you might want to consider the following:

    const size = 200;

    const canvas = document.getElementsByTagName('canvas')[0]
    canvas.width = canvas.height = size;
    const ctx = canvas.getContext('2d');

    // Create a custom fillText funciton that flips the canvas, draws the text, and then flips it back
    ctx.fillText = function(text, x, y) {
      this.save();       // Save the current canvas state
      this.scale(1, -1); // Flip to draw the text
      this.fillText.dummyCtx.fillText.call(this, text, x, -y); // Draw the text, invert y to get coordinate right
      this.restore();    // Restore the initial canvas state
    }
    // Create a dummy canvas context to use as a source for the original fillText function
    ctx.fillText.dummyCtx = document.createElement('canvas').getContext('2d');

    ctx.setTransform(1, 0, 0, -1, size / 2, size / 2);

    const triangle = [
      {x: -70, y: -70, label: 'A'},
      {x:  70, y: -70, label: 'B'},
      {x:   0, y:  70, label: 'C'},
    ];

    // draw lines  
    ctx.beginPath();
    ctx.strokeStyle = 'black';
    ctx.moveTo(triangle[2].x, triangle[2].y);
    triangle.forEach(v => ctx.lineTo(v.x, v.y));
    ctx.stroke();
    ctx.closePath();
      
    // draw labels
    ctx.textAlign = 'center';
    ctx.font = '24px Arial';
    // For this particular example, multiplying x and y by small factors >1 offsets the labels from the triangle vertices
    triangle.forEach(v => ctx.fillText(v.label, 1.2*v.x, 1.1*v.y));

The above is useful if for your real application, you'll be going back and forth between drawing non-text objects and drawing text and don't want to have to remember to flip the canvas back and forth. (It's not a huge problem in the current example, because you draw the triangle and then draw all the text, so you only need one flip. But if you have in mind a different application that's more complex, that could be an annoyance.) In the above example, I've replaced the fillText method with a custom method that flips the canvas, draws the text, and then flips it back again so that you don't have to do it manually every time you want to draw text.

The result:

enter image description here

If you don't like overriding the default fillText, then obviously you can just create a method with a new name; that way you could also avoid creating the dummy context and just use this.fillText within your custom method.

EDIT: The above approach also works with arbitrary zoom and translation. scale(1, -1) simply reflects the canvas over the x-axis: after this transformation, a point that was at (x, y) will now be at (x, -y). This is true regardless of translation and zoom. If you want the text to remain a constant size regardless of zoom, then you just have to scale the font size by zoom. For example:

<html>
<body>
	<canvas id='canvas'></canvas>
</body>

<script>

	const canvas = document.getElementById('canvas');
	const ctx = canvas.getContext('2d');
	var framesPerSec = 100;
	var msBetweenFrames = 1000/framesPerSec;
	ctx.font = '12px Arial';

	function getRandomCamera() {
		return {x: ((Math.random() > 0.5) ? -1 : 1) * Math.random()*5,
			    y: ((Math.random() > 0.5) ? -1 : 1) * Math.random()*5+5,
			    zoom: Math.random()*20+0.1,
				};
	}

	var camera = getRandomCamera();
	moveCamera();

	function moveCamera() {
		var newCamera = getRandomCamera();
		var transitionFrames = Math.random()*500+100;
		var animationTime = transitionFrames*msBetweenFrames;

		var cameraSteps = {	x: (newCamera.x-camera.x)/transitionFrames,
						   	y: (newCamera.y-camera.y)/transitionFrames,
						   	zoom: (newCamera.zoom-camera.zoom)/transitionFrames };
		
		for (var t=0; t<animationTime; t+=msBetweenFrames) {
			window.setTimeout(updateCanvas, t);
		}
		window.setTimeout(moveCamera, animationTime);

		function updateCanvas() {
			camera.x += cameraSteps.x;
			camera.y += cameraSteps.y;
			camera.zoom += cameraSteps.zoom;
			redrawCanvas();
		}
	}

	ctx.drawText = function(text, x, y) {
		this.save();
		this.transform(1 / camera.zoom, 0, 0, -1 / camera.zoom, x, y);
		this.fillText(text, 0, 0);
		this.restore();
	}

	function redrawCanvas() {

		ctx.clearRect(0, 0, canvas.width, canvas.height);

		ctx.save();
		ctx.translate(canvas.width / 2 - (camera.x * camera.zoom),
		            canvas.height / 2 + (camera.y * camera.zoom));
		ctx.scale(camera.zoom, -camera.zoom);

		for (var i = 0; i < 10; i++) {
		    ctx.beginPath();
		    ctx.arc(5, i * 2, .5, 0, 2 * Math.PI);
		    ctx.drawText(i, 7, i*2-0.5);
		    ctx.fill();
		}

		ctx.restore();
	}

</script>

</html>

EDIT: Modified text scaling method based on suggestion by Blindman67. Also improved demo by making camera motion gradual.

like image 163
cjg Avatar answered Sep 26 '22 13:09

cjg


My solution is rotate the canvas and then draw the text.

ctx.scale(1,-1); // rotate the canvas
triangle.forEach(v => {
ctx.fillText(v.label, v.x, -v.y + 25); // draw with a bit adapt position
});

Hope that helps :)

const size = 200;

const canvas = document.getElementsByTagName('canvas')[0]
canvas.width = canvas.height = size;
const ctx = canvas.getContext('2d');

ctx.setTransform(1, 0, 0, -1, size / 2, size / 2);

const triangle = [
  {x: -70, y: -70, label: 'A'},
  {x:  70, y: -70, label: 'B'},
  {x:   0, y:  70, label: 'C'},
];

// draw lines  

ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.moveTo(triangle[2].x, triangle[2].y);
triangle.forEach(v => ctx.lineTo(v.x, v.y));
ctx.stroke();
ctx.closePath();

// draw labels
ctx.textAlign = 'center';
ctx.font = '24px Arial';
ctx.scale(1,-1);
triangle.forEach(v => {
ctx.fillText(v.label, v.x, -v.y + 25);
});
<canvas></canvas>
like image 40
taile Avatar answered Sep 22 '22 13:09

taile