"Undo" canvas 用于编写文本的转换
"Undo" canvas transformations for writing text
当使用 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>
除了手动重置转换矩阵之外,是否有 "smart" 方式让文本处于 "correct" 方向?
我的解决方案是旋转 canvas 然后绘制文本。
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>
要以 Tai 的回答为基础,这太棒了,您可能需要考虑以下内容:
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));
如果对于您的实际应用,上面的内容很有用,您将在绘制非文本对象和绘制文本之间来回切换并且不想记住将 canvas 翻转回来来回。 (在当前的示例中这不是一个大问题,因为您绘制三角形然后绘制所有文本,所以您只需要翻转一次。但是如果您有一个更复杂的不同应用程序,那可能会很烦人。)在上面的示例中,我将 fillText 方法替换为一个自定义方法,该方法翻转 canvas,绘制文本,然后再次将其翻转回来,这样您就不必每次都手动执行此操作绘制文本。
结果:
如果您不喜欢覆盖默认值 fillText
,那么显然您可以创建一个具有新名称的方法;这样你也可以避免创建虚拟上下文,只需在你的自定义方法中使用 this.fillText
。
编辑:上述方法也适用于任意缩放和平移。 scale(1, -1)
简单地反映了 x 轴上的 canvas:在这个转换之后,位于 (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 的建议修改文本缩放方法。还通过使相机运动渐变来改进演示。
我会采用一种方法来存储绘图的 "state" 而没有实际像素,并定义一个 draw
可以在任何时候呈现此状态的方法。
您必须为您的积分实施自己的 scale
和 translate
方法,但我认为最终这是值得的。
因此,在项目符号中:
- 存储 "things to draw" 列表(带标签的点)
- 公开修改这些 "things"
的 scale
和 translate
方法
- 公开呈现这些 "things"
的 draw
方法
例如,我创建了一个名为 Figure
的 class,它显示了这些功能的 1.0 实现。我创建了一个引用 canvas 的新实例。然后我通过传递一个 x
、y
和一个 label
向它添加点。 scale
和 transform
更新这些点的 x
和 y
属性。 draw
循环遍历 a) 绘制 "dot" 和 b) 绘制标签。
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>
注意:使用输入作为其工作原理的示例。我已选择 "commit" 缩放更改并立即翻译,所以顺序很重要!您可能想要按全屏以同时查看 canvas 和输入内容。
备选方案
var x = 100;
var y = 100;
var pixelRatio = 2;
var transform = {"x": 0, "y": 0, "k": 1}
context.save();
context.setTransform(pixelRatio, 0.0, 0.0, pixelRatio, 0.0, 0.0);
context.translate(transform.x, 0);
context.scale(transform.k, 1);
context.save();
// get Transformed Point
var context_transform = context.getTransform();
var pt = context_transform.transformPoint({
x: x,
y: y
});
// Reset previous transforms
context.setTransform(pixelRatio, 0.0, 0.0, pixelRatio, -pt.x, -pt.y);
// draw with the values as usual
context.textAlign = "left";
context.font = "14px Arial";
context.fillText("Hello", pt.x, pt.y);
context.restore();
context.restore();
当使用 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>
除了手动重置转换矩阵之外,是否有 "smart" 方式让文本处于 "correct" 方向?
我的解决方案是旋转 canvas 然后绘制文本。
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>
要以 Tai 的回答为基础,这太棒了,您可能需要考虑以下内容:
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));
如果对于您的实际应用,上面的内容很有用,您将在绘制非文本对象和绘制文本之间来回切换并且不想记住将 canvas 翻转回来来回。 (在当前的示例中这不是一个大问题,因为您绘制三角形然后绘制所有文本,所以您只需要翻转一次。但是如果您有一个更复杂的不同应用程序,那可能会很烦人。)在上面的示例中,我将 fillText 方法替换为一个自定义方法,该方法翻转 canvas,绘制文本,然后再次将其翻转回来,这样您就不必每次都手动执行此操作绘制文本。
结果:
如果您不喜欢覆盖默认值 fillText
,那么显然您可以创建一个具有新名称的方法;这样你也可以避免创建虚拟上下文,只需在你的自定义方法中使用 this.fillText
。
编辑:上述方法也适用于任意缩放和平移。 scale(1, -1)
简单地反映了 x 轴上的 canvas:在这个转换之后,位于 (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 的建议修改文本缩放方法。还通过使相机运动渐变来改进演示。
我会采用一种方法来存储绘图的 "state" 而没有实际像素,并定义一个 draw
可以在任何时候呈现此状态的方法。
您必须为您的积分实施自己的 scale
和 translate
方法,但我认为最终这是值得的。
因此,在项目符号中:
- 存储 "things to draw" 列表(带标签的点)
- 公开修改这些 "things" 的
- 公开呈现这些 "things" 的
scale
和 translate
方法
draw
方法
例如,我创建了一个名为 Figure
的 class,它显示了这些功能的 1.0 实现。我创建了一个引用 canvas 的新实例。然后我通过传递一个 x
、y
和一个 label
向它添加点。 scale
和 transform
更新这些点的 x
和 y
属性。 draw
循环遍历 a) 绘制 "dot" 和 b) 绘制标签。
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>
注意:使用输入作为其工作原理的示例。我已选择 "commit" 缩放更改并立即翻译,所以顺序很重要!您可能想要按全屏以同时查看 canvas 和输入内容。
备选方案
var x = 100;
var y = 100;
var pixelRatio = 2;
var transform = {"x": 0, "y": 0, "k": 1}
context.save();
context.setTransform(pixelRatio, 0.0, 0.0, pixelRatio, 0.0, 0.0);
context.translate(transform.x, 0);
context.scale(transform.k, 1);
context.save();
// get Transformed Point
var context_transform = context.getTransform();
var pt = context_transform.transformPoint({
x: x,
y: y
});
// Reset previous transforms
context.setTransform(pixelRatio, 0.0, 0.0, pixelRatio, -pt.x, -pt.y);
// draw with the values as usual
context.textAlign = "left";
context.font = "14px Arial";
context.fillText("Hello", pt.x, pt.y);
context.restore();
context.restore();