2017-01-09 19 views
12

Podczas stosowania transformacji z kanwą wynikowy tekst jest również (oczywiście) transformowany. Czy istnieje sposób zapobiegania pewnym przekształceniom, takim jak odbicie, wpływu na tekst?"Undo" transformacje kanwy do pisania tekstu

Na przykład ustawiam globalną macierz transformacji, tak aby oś Y była skierowana w górę, oś X w prawo, a punkt (0, 0) znajduje się na środku ekranu (czego można oczekiwać od matematycznego układu współrzędnych).

Powoduje to jednak, że tekst jest do góry nogami.

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>

Czy istnieje „inteligentny” sposób, aby uzyskać tekst w „poprawnej” orientacji, oprócz ręcznego zerowania macierzy transformacji?

+0

Oto bardziej ogólny przykład nagród, w którym nie tylko obróciłem oś y, ale także powiększam i tłumaczę. Jak narysować tekst obok punktów w prawidłowej orientacji i skali? https://jsfiddle.net/7ryfwvfm/2/ – Matsemann

Odpowiedz

6

Aby zbudować od odpowiedzi Tai, który jest niepowtarzalny, może warto rozważyć następujące kwestie:

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

Powyższe przydaje się, jeśli do swojej prawdziwej aplikacji będziesz się przechodzić między rysowaniem obiektów nietekstowych a rysowaniem tekstu i nie będziesz musiał pamiętać, aby odwrócić płótno w tę iz powrotem. (W obecnym przykładzie nie jest to duży problem, ponieważ narysujesz trójkąt, a następnie narysujesz cały tekst, więc potrzebujesz tylko jednej klapki, ale jeśli masz na myśli inną, bardziej złożoną aplikację, może to być irytujące). W powyższym przykładzie zastąpiłem metodę fillText niestandardową metodą, która odwraca płótno, rysuje tekst, a następnie odwraca je z powrotem, aby nie było potrzeby ręcznego wykonywania go za każdym razem, gdy chcesz narysować tekst.

Rezultat:

enter image description here

Jeśli nie podoba przesłanianie domyślny fillText, to oczywiście można po prostu stworzyć metodę z nową nazwą; w ten sposób można również uniknąć tworzenia fikcyjnego kontekstu i po prostu użyć this.fillText w swojej niestandardowej metodzie.

EDYCJA: Powyższe podejście działa również z dowolnym zoomem i tłumaczeniem. scale(1, -1) po prostu odzwierciedla płótno na osi X: po tej transformacji punkt znajdujący się na (x, y) będzie teraz w (x, -y). Jest to prawda niezależnie od tłumaczenia i powiększenia. Jeśli chcesz, aby tekst pozostał stałym rozmiarem, niezależnie od powiększenia, wystarczy przeskalować rozmiar czcionki przez powiększenie. Na przykład:

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

 
<script> 
 

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

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

 
\t var camera = getRandomCamera(); 
 
\t moveCamera(); 
 

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

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

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

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

 
\t function redrawCanvas() { 
 

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

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

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

 
\t \t ctx.restore(); 
 
\t } 
 

 
</script> 
 

 
</html>

EDIT: Zmodyfikowana metoda skalowania tekstu na podstawie sugestii przez Blindman67.Ulepszono również demo, zwiększając ruch kamery.

+0

Dodano bardziej ogólny przykład, który chcę wyjaśnić jako komentarz do pytania. – Matsemann

+0

To samo ogólne podejście powinno zadziałać nawet w bardziej ogólnym przypadku. Zobacz dodany przykład w mojej edycji powyżej. – cjg

+2

Metoda skalowania czcionek nie powiedzie się, gdy rozmiar px będzie zbyt mały, a niektóre przeglądarki upuszczają ułamki dla rozmiaru czcionki na płótnie, a zmiana rozmiaru czcionki może być wolniejsza niż skalowanie czcionki. Twoja funkcja drawText będzie bardziej wydajna (i niezawodna) w następujący sposób: 'c.save(); c.setTransform (1/camera.zoom, 0, 0, -1/camera.zoom, x, -y); c.fillText (tekst, 0, 0); c.restore(); 'i nie zawodzi, gdy powiększenie jest duże i znacznie szybsze, jeśli zoom lub czcionka zmienia się pomiędzy połączeniami. – Blindman67

6

Moje rozwiązanie polega na obróceniu płótna, a następnie narysowaniu tekstu.

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

nadzieję, że pomoże :)

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>

+0

Dodano bardziej ogólny przykład, który chcę wyjaśnić jako komentarz do pytania. – Matsemann

1

Podejdę z podejściem, które przechowuje "stan" twojego rysunku bez rzeczywistych pikseli i definiuje metodę draw, która może renderować ten stan w dowolnym punkcie.

Będziesz musiał wprowadzić własne metody scale i translate dla swoich punktów, ale myślę, że warto na końcu.

Więc w Pociski:

  • przechowywać listę "rzeczy do rysować" (punkty z etykietami)
  • Expose scale i translate metody, które modyfikują tych "rzeczy"
  • Expose metodę draw który renderuje te "rzeczy"

Jako przykład, stworzyłem klasę o nazwie Figure, która pokazuje implementację 1.0 tych cechy. Tworzę nową instancję odwołującą się do płótna. Dodaję do tego punkty, przekazując x, y i label. scale i transform aktualizują te punkty "x i y właściwości. draw wykonuje pętle przez punkty do a) narysuj "kropkę" i b) narysuj etykietę.

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>

Uwaga: za pomocą wejść na przykład, jak to działa. Zdecydowałem się "zatwierdzić" zmiany ze skali i natychmiast je przetłumaczyć, więc zamówienie ma znaczenie! Możesz chcieć nacisnąć pełnoekranowy, aby wyświetlić zarówno płótno, jak i wejścia do widoku.