Cómo usar en un lienzo un elemento de texto con una fuente descrita en CSS

8

Esto está dentro del proyecto Bismon (un software GPLv3 + financiado por proyectos europeos H2020), git commit0e9a8eccc2976f . Este borrador del informe describe el software. Esta pregunta da más contexto y motivaciones. Se trata del archivo webroot / jscript / bismon-hwroot.js (escrito a mano) , utilizado en alguna página HTML cuyo código es generado por Bismon (un servidor web especializado sobre libonion ).

Agregué algunas clases de CSS para span, por ejemplo span.bmcl_evalprompt(por ejemplo, en mi archivo first-theme.css ).

¿Cómo codifico JavaScript para agregar una pieza de texto en un lienzo (preferiblemente usando jcanvas con jquery) que tiene el mismo estilo (misma fuente, color, etc.) que ese span.bmcl_evalprompt? ¿Necesito crear un elemento span en mi DOM? ¿Es eso simplemente posible?

Solo me importa un Firefox reciente (68 al menos) en Linux. JQuery es 3.4. También estoy usando Jquery UI 1.12.1

La idea que tenía en mi mente era crear un solo <span class='bmcl_evalprompt'>elemento con coordenadas lejos de la ventana gráfica del navegador (o ventana X11), por ejemplo, en x= -10000y y= -10000 (en píxeles), luego agregar ese elemento único mal colocado en el DOM del documento, luego usar el tradicional Técnicas de jquery para obtener la familia de fuentes, el tamaño de fuente y el tamaño de elemento. ¿Pero hay alguna forma mejor? ¿O alguna biblioteca compatible con Jquery haciendo eso?

Basile Starynkevitch
fuente

Respuestas:

5

¿Fuente de DOMs coincidente en lienzo?

La respuesta simple es: "¡Muy difícil!" y "Nunca será perfecto".

Lo mejor que puede hacer es una aproximación que se encuentra en el ejemplo al final de la respuesta, que también mostrará que la coincidencia del estilo visible no está relacionada con la calidad visible.

Extendiéndose desde solo reglas CSS.

Si desea que la fuente coincida lo más posible con el elemento, existen algunas preocupaciones adicionales además de obtener el CSS como se indica en la respuesta de Spark Fountain .

Tamaño de fuente y tamaño de píxeles CSS

  • El tamaño de fuente está relacionado con el tamaño de píxel CSS. El elemento HTMLCanvasElement
  • El tamaño de píxel CSS no siempre coincide con los píxeles de visualización del dispositivo. Por ejemplo, pantallas HiDPI / Retina. Puede acceder a la ración de píxeles CSS del dispositivo a través dedevicePixelRatio
  • El tamaño de píxel CSS no es una constante y puede cambiar por muchas razones. Los cambios se pueden monitorear a través MediaQueryListEventdel changeevento y escucharlo
  • Los elementos pueden ser transformados. No se CanvasRenderingContext2Dpueden realizar transformaciones en 3D, por lo tanto, es el elemento o el lienzo tiene una transformación en 3D. No podrá hacer coincidir la fuente renderizada del lienzo con los elementos representados.

  • La resolución del lienzo y el tamaño de la pantalla son independientes.

    • Puede obtener la resolución del lienzo a través de las propiedades HTMLCanvasElement.width, yHTMLCanvasElement.height
    • Puede obtener el tamaño de visualización del lienzo a través de las propiedades de estilo ancho y alto, o mediante una variedad de otros métodos, vea el ejemplo.
    • El aspecto del píxel del lienzo puede no coincidir con el aspecto del píxel CSS y debe calcularse al representar la fuente en el lienzo.
    • La representación de fuentes de lienzo en tamaños de fuente pequeños es terrible. Por ejemplo, una fuente de 4px con un tamaño de 16px no se puede leer. ctx.font = "4px arial"; ctx.scale(4,4); ctx.fillText("Hello pixels");Debe usar un tamaño de fuente fijo que tenga resultados de representación de lienzo de buena calidad y reducir la representación al usar fuentes pequeñas.

Color de fuente

El estilo de color de los elementos solo representa el color representado. No representa el color real visto por el usuario.

Como esto se aplica tanto al lienzo como al elemento del que está obteniendo el color y cualquier elemento sobre o debajo, la cantidad de trabajo requerida para igualar visualmente el color es enorme y está más allá del alcance de una respuesta de desbordamiento de pila (las respuestas tienen un longitud máxima de 30K)

Renderizado de fuentes

El motor de representación de fuentes del lienzo es diferente al del DOM. El DOM puede usar una variedad de técnicas de renderizado para mejorar la calidad aparente de las fuentes aprovechando la forma en que están dispuestos los subpíxeles RGB físicos del dispositivo. Por ejemplo, fuentes TrueType y sugerencias relacionadas utilizadas por el renderizador, y el subpíxel del ClearType derivado con representación de sugerencias.

Estos métodos de representación de fuentes PUEDEN coincidir en el lienzo , aunque para la coincidencia en tiempo real tendrá que usar WebGL.

El problema es que la representación de la fuente DOM está determinada por muchos factores, incluida la configuración del navegador. JavaScript no puede acceder a ninguna de la información necesaria para determinar cómo se representa la fuente. En el mejor de los casos, puede hacer una suposición educada.

Complicaciones adicionales

También hay otros factores que afectan la fuente y cómo las reglas de estilo de fuente CSS se relacionan con el resultado visual de la fuente mostrada. Por ejemplo, unidades CSS, animación, alineación, dirección, transformaciones de fuente y modo peculiar.

Personalmente para renderizar y colorear no me molesto. Evento si escribí un motor de fuente completo usando WebGL para que coincida con cada fuente, filtrado, composición y variante de representación, no son parte del estándar y, por lo tanto, están sujetas a cambios sin previo aviso. Por lo tanto, el proyecto siempre estaría abierto y, en cualquier momento, podría fallar al nivel de resultados ilegibles. Simplemente no vale la pena el esfuerzo.


Ejemplo

El ejemplo tiene un lienzo de renderizado a la izquierda. El texto y la fuente se centran en la parte superior. Una vista ampliada a la derecha, que muestra una vista ampliada del lienzo del lienzo izquierdo

El primer estilo utilizado son las páginas predeterminadas. La resolución del lienzo es de 300by150, pero se ajusta a 500 por 500 píxeles CSS. Esto da como resultado un texto de lienzo de MUY mala calidad. Al ciclar la resolución del lienzo, se mostrará cómo la resolución del lienzo afecta la calidad.

Las funciones

  • drawText(text, x, y, fontCSS, sizeCSSpx, colorStyleCSS)dibuja el texto usando valores de propiedad CSS. Escalando la fuente para que coincida con el tamaño visual DOM y la relación de aspecto lo más cerca posible.

  • getFontStyle(element) devuelve los estilos de fuente necesarios como un objeto de element

Uso de UI

  • HAGA CLIC en la fuente central para ciclos de estilos de fuente.

  • HAGA CLIC en el lienzo izquierdo para alternar las resoluciones del lienzo.

  • En la parte inferior se encuentra la configuración utilizada para representar el texto en el lienzo.

Verá que la calidad del texto depende de la resolución del lienzo.

Para ver cómo el zoom DOM afecta la representación, debe acercar o alejar la página. Las pantallas HiDPI y retina tendrán un texto de lienzo de calidad mucho menor debido al hecho de que el lienzo es la mitad de la resolución de los píxeles CSS.

const ZOOM_SIZE = 16;
canvas1.width = ZOOM_SIZE;
canvas1.height = ZOOM_SIZE;
const ctx = canvas.getContext("2d");
const ctx1 = canvas1.getContext("2d");
const mouse = {x:0, y:0};
const CANVAS_FONT_BASE_SIZE = 32; // the size used to render the canvas font.
const TEXT_ROWS = 12;
var currentFontClass = 0;
const fontClasses = "fontA,fontB,fontC,fontD".split(",");

const canvasResolutions = [[canvas.scrollWidth, canvas.scrollHeight],[300,150],[200,600],[600,600],[1200,1200],[canvas.scrollWidth * devicePixelRatio, canvas.scrollHeight * devicePixelRatio]];
var currentCanvasRes = canvasResolutions.length - 1;
var updateText = true;
var updating = false;
setTimeout(updateDisplay, 0, true);

function drawText(text, x, y, fontCSS, sizeCSSpx, colorStyleCSS) { // Using px as the CSS size unit
    ctx.save();
    
    // Set canvas state to default
    ctx.globalAlpha = 1;
    ctx.filter = "none";
    ctx.globalCompositeOperation = "source-over";
    
    const pxSize = Number(sizeCSSpx.toString().trim().replace(/[a-z]/gi,"")) * devicePixelRatio;
    const canvasDisplayWidthCSSpx = ctx.canvas.scrollWidth; // these are integers
    const canvasDisplayHeightCSSpx = ctx.canvas.scrollHeight;
    
    const canvasResWidth = ctx.canvas.width;
    const canvasResHeight = ctx.canvas.height;
    
    const scaleX = canvasResWidth / (canvasDisplayWidthCSSpx * devicePixelRatio);
    const scaleY = canvasResHeight / (canvasDisplayHeightCSSpx * devicePixelRatio);
    const fontScale = pxSize / CANVAS_FONT_BASE_SIZE
    
    ctx.setTransform(scaleX * fontScale, 0, 0, scaleY * fontScale, x, y); // scale and position rendering
    
    ctx.font = CANVAS_FONT_BASE_SIZE + "px " + fontCSS;
    ctx.textBaseline = "hanging";
    ctx.fillStyle = colorStyleCSS;
    ctx.fillText(text, 0, 0);
    
    ctx.restore();
}
    
function getFontStyle(element) {
    const style = getComputedStyle(element);    
    const color = style.color;
    const family = style.fontFamily;
    const size = style.fontSize;    
    styleView.textContent = `Family: ${family} Size: ${size} Color: ${color} Canvas Resolution: ${canvas.width}px by ${canvas.height}px Canvas CSS size 500px by 500px CSS pixel: ${devicePixelRatio} to 1 device pixels`
    
    return {color, family, size};
}

function drawZoomView(x, y) {
    ctx1.clearRect(0, 0, ctx1.canvas.width, ctx1.canvas.height);
    //x -= ZOOM_SIZE / 2;
    //y -= ZOOM_SIZE / 2;
    const canvasDisplayWidthCSSpx = ctx.canvas.scrollWidth; // these are integers
    const canvasDisplayHeightCSSpx = ctx.canvas.scrollHeight;
    
    const canvasResWidth = ctx.canvas.width;
    const canvasResHeight = ctx.canvas.height;
    
    const scaleX = canvasResWidth / (canvasDisplayWidthCSSpx * devicePixelRatio);
    const scaleY = canvasResHeight / (canvasDisplayHeightCSSpx * devicePixelRatio);
    
    x *= scaleX;
    y *= scaleY;
    x -= ZOOM_SIZE / 2;
    y -= ZOOM_SIZE / 2;
    
    ctx1.drawImage(ctx.canvas, -x, -y);
}

displayFont.addEventListener("click", changeFontClass);
function changeFontClass() {
   currentFontClass ++;
   myFontText.className = fontClasses[currentFontClass % fontClasses.length];
   updateDisplay(true);
}
canvas.addEventListener("click", changeCanvasRes);
function changeCanvasRes() {
   currentCanvasRes ++;
   if (devicePixelRatio === 1 && currentCanvasRes === canvasResolutions.length - 1) {
       currentCanvasRes ++;
   }
   updateDisplay(true);
}
   
   

addEventListener("mousemove", mouseEvent);
function mouseEvent(event) {
    const bounds = canvas.getBoundingClientRect();
    mouse.x = event.pageX - scrollX - bounds.left;
    mouse.y = event.pageY - scrollY - bounds.top;    
    updateDisplay();
}

function updateDisplay(andRender = false) {
    if(updating === false) {
        updating = true;
        requestAnimationFrame(render);
    }
    updateText = andRender;
}

function drawTextExamples(text, textStyle) {
    
    var i = TEXT_ROWS;
    const yStep = ctx.canvas.height / (i + 2);
    while (i--) {
        drawText(text, 20, 4 + i * yStep, textStyle.family, textStyle.size, textStyle.color);
    }
}



function render() {
    updating = false;

    const res = canvasResolutions[currentCanvasRes % canvasResolutions.length];
    if (res[0] !== canvas.width || res[1] !== canvas.height) {
        canvas.width = res[0];
        canvas.height = res[1];
        updateText = true;
    }
    if (updateText) {
        ctx.setTransform(1,0,0,1,0,0);
        ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
        updateText = false;
        const textStyle = getFontStyle(myFontText);
        const text = myFontText.textContent;
        drawTextExamples(text, textStyle);
        
    }
    
    
    
    drawZoomView(mouse.x, mouse.y)


}
.fontContainer {
  position: absolute;
  top: 8px;
  left: 35%;
  background: white;
  border: 1px solid black;
  width: 30%;   
  cursor: pointer;
  text-align: center;
}
#styleView {
}
  

.fontA {}
.fontB {
  font-family: arial;
  font-size: 12px;
  color: #F008;
}
.fontC {
  font-family: cursive;
  font-size: 32px;
  color: #0808;
}
.fontD {
  font-family: monospace;
  font-size: 26px;
  color: #000;
}

.layout {
   display: flex;
   width: 100%;
   height: 128px;
}
#container {
   border: 1px solid black;
   width: 49%;
   height: 100%;
   overflow-y: scroll;
}
#container canvas {
   width: 500px;
   height: 500px;
}

#magViewContainer {
   border: 1px solid black;
   display: flex;
   width: 49%;
   height: 100%; 
}

#magViewContainer canvas {
   width: 100%;
   height: 100%;
   image-rendering: pixelated;
}
<div class="fontContainer" id="displayFont"> 
   <span class="fontA" id="myFontText" title="Click to cycle font styles">Hello Pixels</span>
</div>


<div class="layout">
  <div id="container">
      <canvas id="canvas" title="Click to cycle canvas resolution"></canvas>
  </div>
  <div id="magViewContainer">
      <canvas id="canvas1"></canvas>
  </div>
</div>
<code id="styleView"></code>

Ciego67
fuente
3

Si simplemente desea representar el texto de su tramo en un lienzo, puede acceder a los atributos de estilo utilizando la función window.getComputedStyle . Para hacer invisible el tramo original, establezca su estilo en display: none.

// get the span element
const span = document.getElementsByClassName('bmcl_evalprompt')[0];

// get the relevant style properties
const font = window.getComputedStyle(span).font;
const color = window.getComputedStyle(span).color;

// get the element's text (if necessary)
const text = span.innerHTML;

// get the canvas element
const canvas = document.getElementById('canvas');

// set the canvas styling
const ctx = canvas.getContext('2d');
ctx.font = font;
ctx.fillStyle = color;

// print the span's content with correct styling
ctx.fillText(text, 35, 110);
#canvas {
  width: 300px;
  height: 200px;
  background: lightgrey;
}

span.bmcl_evalprompt {
  display: none;           // makes the span invisible
  font-family: monospace;  // change this value to see the difference
  font-size: 32px;         // change this value to see the difference
  color: rebeccapurple;    // change this value to see the difference
}
<span class="bmcl_evalprompt">Hello World!</span>
<canvas id="canvas" width="300" height="200"></canvas>

Fuente de chispa
fuente