При применении преобразования с помощью canvas результирующий текст также (очевидно) также трансформируется. Есть ли способ предотвратить определенные преобразования, такие как отражение, воздействия на текст?

Например, я установил матрицу глобального преобразования так, чтобы ось Y указывала вверх, ось X была направлена, а точка (0, 0) находилась в центре экрана (что вы ожидаете от математической системы координат ) .

Тем не менее, это также делает текст с ног на голову.

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>

Есть ли «умный» способ получить текст в «правильной» ориентации, кроме ручного сброса матриц преобразования?

15
Lazar Ljubenović 9 Янв 2017 в 19:00

3 ответа

Лучший ответ

Чтобы составить ответ Тай, который является фантастическим, вы можете рассмотреть следующее:

    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));

Вышесказанное полезно, если для вашего реального приложения вы будете перемещаться между рисованием нетекстовых объектов и рисованием текста, и вам не нужно будет не забывать переворачивать холст взад и вперед. (В текущем примере это не большая проблема, потому что вы рисуете треугольник, а затем рисуете весь текст, поэтому вам нужен только один переворот. Но если вы имеете в виду другое более сложное приложение, это может вызвать раздражение.) В приведенном выше примере я заменил метод fillText на собственный метод, который переворачивает холст, рисует текст, а затем снова переворачивает его, чтобы вам не приходилось делать это каждый раз, когда вы хотите нарисовать текст.

Результат:

enter image description here

Если вам не нравится переопределение по умолчанию fillText, тогда, очевидно, вы можете просто создать метод с новым именем; таким образом, вы также можете избежать создания фиктивного контекста и просто использовать this.fillText в своем пользовательском методе.

РЕДАКТИРОВАТЬ: вышеупомянутый подход также работает с произвольным масштабированием и переводом. scale(1, -1) просто отражает холст по оси x: после этого преобразования точка, которая была в (x, y), теперь будет в (x, -y). Это верно независимо от перевода и масштаба. Если вы хотите, чтобы размер текста оставался неизменным независимо от масштаба, вам просто нужно масштабировать размер шрифта с помощью увеличения. Например:

<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>

РЕДАКТИРОВАТЬ: Модифицированный метод масштабирования текста, основанный на предложении Blindman67. Также улучшена демонстрация, сделав движение камеры плавным.

7
cjg 2 Май 2017 в 17:45

Мое решение - повернуть холст, а затем нарисовать текст.

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
});

Надеюсь, это поможет :)

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>
7
Tai Le 26 Апр 2017 в 01:45

Я бы пошел с подходом, который хранит «состояние» вашего рисунка без фактических пикселей и определяет метод draw, который может визуализировать это состояние в любой точке.

Вы должны будете реализовать свои собственные методы scale и translate для своих очков, но я думаю, что это стоит того.

Итак, в пулях:

  • Хранить список «вещей для рисования» (точки с метками)
  • Разоблачить методы scale и translate, которые изменяют эти «вещи»
  • Раскройте метод draw, который отображает эти "вещи"

В качестве примера я создал класс Figure, который показывает реализацию этих функций в версии 1.0. Я создаю новый экземпляр, который ссылается на холст. Затем я добавляю к нему очки, передавая x, y и label. scale и transform обновляют свойства этих точек x и y. draw проходит через точки, чтобы а) нарисовать "точку", и б) нарисовать метку.

const Figure = function(canvas) {
  const ctx = canvas.getContext('2d');
  const origin = {
    x: canvas.width / 2,
    y: canvas.height / 2
  };
  const shift = p => Object.assign(p, {
    x: origin.x + p.x,
    y: origin.y - p.y
  });

  let points = [];

  this.addPoint = (x, y, label) => {
    points = points.concat({
      x,
      y,
      label
    });
  }

  this.translate = (tx, ty) => {
    points = points.map(
      p => Object.assign(p, {
        x: p.x + tx,
        y: p.y + ty
      })
    );
  };

  this.scale = (sx, sy) => {
    points = points.map(
      p => Object.assign(p, {
        x: p.x * sx,
        y: p.y * sy
      })
    );
  };

  this.draw = function() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.beginPath();

    const sPoints = points.map(shift);

    sPoints.forEach(p => drawDot(ctx, 5, p.x, p.y));
    sPoints.forEach(p => drawLabel(ctx, p.label, p.x + 5, p.y));

    ctx.fill();
  }
}

const init = () => {
  const canvas = document.getElementById('canvas');
  const fig = new Figure(canvas);

  // Generate some test data
  for (let i = 0, labels = "ABCD"; i < labels.length; i += 1) {
    fig.addPoint(i * 3, (i + 1) * 10, labels[i]);
  }

  const sX = parseFloat(document.querySelector(".js-scaleX").value);
  const sY = parseFloat(document.querySelector(".js-scaleY").value);
  const tX = parseFloat(document.querySelector(".js-transX").value);
  const tY = parseFloat(document.querySelector(".js-transY").value);

  fig.scale(sX, sY);
  fig.translate(tX, tY);
  fig.draw();
}

Array
  .from(document.querySelectorAll("input"))
  .forEach(el => el.addEventListener("change", init));

init();



// Utilities for drawing
function drawDot(ctx, d, x, y) {
  ctx.arc(x, y, d / 2, 0, 2 * Math.PI);
}

function drawLabel(ctx, label, x, y) {
  ctx.fillText(label, x, y);
}
canvas {
  background: #efefef;
  margin: 1rem;
}

input {
  width: 50px;
}
<div>
  <p>
    Scales first, translates second (hard coded, can be changed)
  </p>
  <label>Scale x <input type="number" class="js-scaleX" value="1"></label>
  <label>Scale y <input type="number" class="js-scaleY" value="1"></label>
  <br/>
  <label>Translate x <input type="number" class="js-transX" value="0"></label>
  <label>translate y <input type="number" class="js-transY" value="0"></label>
</div>
<canvas id="canvas" width="250" height="250"></canvas>

Примечание. Используйте входные данные для примера того, как это работает. Я решил «зафиксировать» изменения в масштабе и немедленно перевести, так что порядок важен! Возможно, вы захотите нажать в полноэкранном режиме, чтобы отобразить холст и входные данные.

1
user3297291 2 Май 2017 в 17:28