SVG obtém a largura do elemento de texto

105

Estou trabalhando em ECMAScript / JavaScript para um arquivo SVG e preciso obter o widthe heightde um textelemento para poder redimensionar um retângulo que o rodeia. Em HTML, seria capaz de usar os atributos offsetWidthe offsetHeightno elemento, mas parece que essas propriedades não estão disponíveis.

Aqui está um fragmento com o qual preciso trabalhar. Preciso mudar a largura do retângulo sempre que mudo o texto, mas não sei como obter o real width(em pixels) do textelemento.

<rect x="100" y="100" width="100" height="100" />
<text>Some Text</text>

Alguma ideia?

Stephen Sorensen
fonte

Respostas:

155
var bbox = textElement.getBBox();
var width = bbox.width;
var height = bbox.height;

e defina os atributos do ret de acordo.

Link: getBBox()no padrão SVG v1.1.

NickFitz
fonte
4
Obrigado. Eu também encontrei outra função que poderia ter ajudado com largura. textElement.getComputedTextLength (). Vou experimentar os dois esta noite e ver o que funciona melhor para mim.
Stephen Sorensen,
5
Eu estava brincando com eles na semana passada; o método getComputedTextLength () retornou o mesmo resultado que getBBox (). width para as coisas que experimentei, então optei pela caixa delimitadora, pois precisava de largura e altura. Não tenho certeza se há alguma circunstância em que o comprimento do texto seja diferente da largura: acho que talvez esse método seja fornecido para ajudar no layout do texto, como dividir o texto em várias linhas, enquanto a caixa delimitadora é um método genérico disponível em todos os elementos (?).
NickFitz
2
O problema é que, se o texto segue um caminho, então a largura não é a largura real do texto (por exemplo, se o texto segue um caminho circular, então o bbox é aquele que circunda o círculo, e obtendo a largura desse isn a largura do texto antes de contornar o círculo). Outra coisa é que bbox.width é maior por um fator de 2, então se o inspetor de elemento mostra uma largura de 200, então bbox.width é 400. Não tenho certeza do motivo.
trusktr
Como você pode calcular o comprimento antes de ser desenhado?
Nikos
7

Em relação ao comprimento do texto, o link parece indicar BBox e getComputedTextLength () pode retornar valores ligeiramente diferentes, mas bastante próximos uns dos outros.

http://bl.ocks.org/MSCAU/58bba77cdcae42fc2f44

andrew pate
fonte
Obrigado por esse link !! Tive que passar por todos os cenários sozinho, mas este é um resumo muito bom ... Meu problema era usar getBBox () em um elemento Tspan no firefox ... Não poderia ser um problema mais específico e irritante .. . obrigado novamente!
Andres Elizondo,
4
document.getElementById('yourTextId').getComputedTextLength();

trabalhou para mim em

Zumbi
fonte
Sua solução retorna o comprimento real do elemento de texto que é a resposta correta. Algumas respostas usam getBBox () que pode estar correto se o texto não for girado.
Netsi1964
2

Que tal algo assim para compatibilidade:

function svgElemWidth(elem) {
    var methods = [ // name of function and how to process its result
        { fn: 'getBBox', w: function(x) { return x.width; }, },
        { fn: 'getBoundingClientRect', w: function(x) { return x.width; }, },
        { fn: 'getComputedTextLength', w: function(x) { return x; }, }, // text elements only
    ];
    var widths = [];
    var width, i, method;
    for (i = 0; i < methods.length; i++) {
        method = methods[i];
        if (typeof elem[method.fn] === 'function') {
            width = method.w(elem[method.fn]());
            if (width !== 0) {
                widths.push(width);
            }
        }
    }
    var result;
    if (widths.length) {
        result = 0;
        for (i = 0; i < widths.length; i++) {
            result += widths[i];
        }
        result /= widths.length;
    }
    return result;
}

Isso retorna a média de quaisquer resultados válidos dos três métodos. Você poderia melhorá-lo para lançar outliers ou favorecer getComputedTextLengthse o elemento for um elemento de texto.

Aviso: como diz o comentário, getBoundingClientRecté complicado. Remova-o dos métodos ou use-o apenas em elementos onde getBoundingClientRectretornará bons resultados, portanto, sem rotação e provavelmente sem escala (?)

HostedMetrics.com
fonte
1
getBoundingClientRect funciona em um sistema de coordenadas potencialmente diferente dos outros dois.
Robert Longson
2

Não sei por que, mas nenhum dos métodos acima funciona para mim. Tive algum sucesso com o método da tela, mas tive que aplicar todos os tipos de fatores de escala. Mesmo com os fatores de escala, ainda tinha resultados inconsistentes entre Safari, Chrome e Firefox.

Então, tentei o seguinte:

            var div = document.createElement('div');
            div.style.position = 'absolute';
            div.style.visibility = 'hidden';
            div.style.height = 'auto';
            div.style.width = 'auto';
            div.style.whiteSpace = 'nowrap';
            div.style.fontFamily = 'YOUR_FONT_GOES_HERE';
            div.style.fontSize = '100';
            div.style.border = "1px solid blue"; // for convenience when visible

            div.innerHTML = "YOUR STRING";
            document.body.appendChild(div);
            
            var offsetWidth = div.offsetWidth;
            var clientWidth = div.clientWidth;
            
            document.body.removeChild(div);
            
            return clientWidth;

Funcionou de forma incrível e super precisa, mas apenas no Firefox. Fatores de escala para salvar o Chrome e o Safari, mas sem alegria. Acontece que os erros do Safari e Chrome não são lineares com o comprimento da string ou tamanho da fonte.

Então, abordagem número dois. Não me importo muito com a abordagem de força bruta, mas depois de lutar com isso por anos, decidi tentar. Decidi gerar valores constantes para cada caractere imprimível individual. Normalmente, isso seria meio tedioso, mas felizmente o Firefox é super preciso. Aqui está minha solução de força bruta de duas partes:

<body>
        <script>
            
            var div = document.createElement('div');
            div.style.position = 'absolute';
            div.style.height = 'auto';
            div.style.width = 'auto';
            div.style.whiteSpace = 'nowrap';
            div.style.fontFamily = 'YOUR_FONT';
            div.style.fontSize = '100';          // large enough for good resolution
            div.style.border = "1px solid blue"; // for visible convenience
            
            var character = "";
            var string = "array = [";
            for(var i=0; i<127; i++) {
                character = String.fromCharCode(i);
                div.innerHTML = character;
                document.body.appendChild(div);
                
                var offsetWidth = div.offsetWidth;
                var clientWidth = div.clientWidth;
                console.log("ASCII: " + i + ", " + character + ", client width: " + div.clientWidth);
                
                string = string + div.clientWidth;
                if(i<126) {
                    string = string + ", ";
                }

                document.body.removeChild(div);
                
            }
        
            var space_string = "! !";
            div.innerHTML = space_string;
            document.body.appendChild(div);
            var space_string_width = div.clientWidth;
            document.body.removeChild(div);
            var no_space_string = "!!";
            div.innerHTML = no_space_string;
            document.body.appendChild(div);
            var no_space_string_width = div.clientWidth;
            console.log("space width: " + (space_string_width - no_space_string_width));
            document.body.removeChild(div);


            string = string + "]";
            div.innerHTML = string;
            document.body.appendChild(div);
            </script>
    </body>

Nota: O snippet acima deve ser executado no Firefox para gerar uma matriz precisa de valores. Além disso, você deve substituir o item 32 da matriz pelo valor da largura do espaço no log do console.

Simplesmente copio o texto do Firefox na tela e colo no meu código javascript. Agora que tenho a matriz de comprimentos de caracteres imprimíveis, posso implementar uma função get width. Aqui está o código:

const LCARS_CHAR_SIZE_ARRAY = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 26, 46, 63, 42, 105, 45, 20, 25, 25, 47, 39, 21, 34, 26, 36, 36, 28, 36, 36, 36, 36, 36, 36, 36, 36, 27, 27, 36, 35, 36, 35, 65, 42, 43, 42, 44, 35, 34, 43, 46, 25, 39, 40, 31, 59, 47, 43, 41, 43, 44, 39, 28, 44, 43, 65, 37, 39, 34, 37, 42, 37, 50, 37, 32, 43, 43, 39, 43, 40, 30, 42, 45, 23, 25, 39, 23, 67, 45, 41, 43, 42, 30, 40, 28, 45, 33, 52, 33, 36, 31, 39, 26, 39, 55];


    static getTextWidth3(text, fontSize) {
        let width = 0;
        let scaleFactor = fontSize/100;
        
        for(let i=0; i<text.length; i++) {
            width = width + LCARS_CHAR_SIZE_ARRAY[text.charCodeAt(i)];
        }
        
        return width * scaleFactor;
    }

Bem, é isso. Força bruta, mas é super preciso em todos os três navegadores, e meu nível de frustração foi para zero. Não tenho certeza de quanto tempo vai durar conforme os navegadores evoluem, mas deve ser o suficiente para que eu desenvolva uma técnica de métrica de fonte robusta para meu texto SVG.

agente-p
fonte
Melhor solução até agora! Obrigado por compartilhar!
just_user
Isso não lida com kerning
pat
É verdade, mas não importa para as fontes que uso. Vou revisitar este ano que vem quando tiver algum tempo. Tenho algumas ideias sobre como tornar as métricas de texto mais precisas para meus casos que não usam uma abordagem de força bruta.
agente-p
Ótimo. Permite que eu alinhe os rótulos dos gráficos SVG estaticamente, em vez de ter que alinhá-los rapidamente no script local. Isso torna a vida muito mais simples ao fornecer gráficos em Ruby a partir de RoR. Obrigado!
Lex Lindsey
0

A especificação SVG tem um método específico para retornar esta informação: getComputedTextLength()

var width = textElement.getComputedTextLength(); // returns a pixel number
cuixiping
fonte