NodeJS 中 Canvas 上的多行居中文本

Center Text with Multi Lines on Canvas in NodeJS

我想在 canvas 上居中显示文本。水平看起来不错,但垂直仍然是个问题:我不知道如何以编程方式执行此操作。

我以前可以用 1 行来做到这一点,但现在我想让它适用于多行文本。

现在的图像是这样的:

文字应该放高一点,还是我弄错了?

代码如下:


const fs = require('fs')
const {  createCanvas } = require('canvas')


const width = 2000;
const height = 2000;

const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')

context.fillStyle = '#edf4ff'
context.fillRect(0, 0, width, height)


context.textAlign = 'center'
context.textBaseline = 'middle';
context.fillStyle = '#002763'


const fontSizeUsed = drawMultilineText(
    context,
    "This is yet another test",
    {
        rect: {
            x: 1000,
            y: 0,
            width: 2000,
            height: 2000 
        },
        font: 'Arial',
        verbose: true,
        lineHeight: 1,
        minFontSize: 100,
        maxFontSize: 200
      }
)

const buffer = canvas.toBuffer('image/png')
  fs.writeFileSync('./image.png', buffer)

和关键函数 drawMultiLineText,应该对齐文本,是这个:

function drawMultilineText(ctx, text, opts) {

    // Default options
    if(!opts)
        opts = {}
    if (!opts.font)
        opts.font = 'sans-serif'
    if (typeof opts.stroke == 'undefined')
        opts.stroke = false
    if (typeof opts.verbose == 'undefined')
        opts.verbose = false
    if (!opts.rect)
        opts.rect = {
            x: 0,
            y: 0,
            width: ctx.canvas.width,
            height: ctx.canvas.height
        }
    if (!opts.lineHeight)
        opts.lineHeight = 1.1
    if (!opts.minFontSize)
        opts.minFontSize = 30
    if (!opts.maxFontSize)
        opts.maxFontSize = 100
    // Default log function is console.log - Note: if verbose il false, nothing will be logged anyway
    if (!opts.logFunction)
        opts.logFunction = function(message) { console.log(message) }


        const words = require('words-array')(text)
        if (opts.verbose) opts.logFunction('Text contains ' + words.length + ' words')
        var lines = []
        let y;  //New Line

    // Finds max font size  which can be used to print whole text in opts.rec
    for (var fontSize = opts.minFontSize; fontSize <= opts.maxFontSize; fontSize++) {

        // Line height
        var lineHeight = fontSize * opts.lineHeight

        // Set font for testing with measureText()
        ctx.font = ' ' + fontSize + 'px ' + opts.font

        // Start
        var x = opts.rect.x;
        y = fontSize; //modified line
        lines = []
        var line = ''

        // Cycles on words
        for (var word of words) {
            // Add next word to line
            var linePlus = line + word + ' '
            // If added word exceeds rect width...
            if (ctx.measureText(linePlus).width > (opts.rect.width)) {
                // ..."prints" (save) the line without last word
                lines.push({ text: line, x: x, y: y })
                // New line with ctx last word
                line = word + ' '
                y += lineHeight
            } else {
                // ...continues appending words
                line = linePlus
            }
        }

        // "Print" (save) last line
        lines.push({ text: line, x: x, y: y })

        // If bottom of rect is reached then breaks "fontSize" cycle
        if (y > opts.rect.height)
            break

  }
  
    if (opts.verbose) opts.logFunction("Font used: " + ctx.font);
    const offset = opts.rect.y + (opts.rect.height - y) / 2; //New line, calculates offset
    for (var line of lines)
            // Fill or stroke
            if (opts.stroke)
                    ctx.strokeText(line.text.trim(), line.x, line.y + offset) //modified line
            else
                    ctx.fillText(line.text.trim(), line.x, line.y + offset) //modified line
    
    // Returns font size
    return fontSize

}

不在浏览器中,我正在使用node.js。

你说得对,第一张图片中的文字也应该放得更高。

代码中有 3 个问题

  1. y的初始值设置为fontSize
  2. 计算变量offset
  3. 当超过 canvas 高度时退出 for 循环而不返回到前一个 fontSize(即最后一个拟合)

第 1 期

y 的初始值应设置为 lineHeight 而不是 fontSize

第 2 期

变量 offset 的计算没有反映以下事实:a) 第一个文本行的起始 y 坐标是 lineHeight 而不是 0 和 b ) 有一个 textBaseline 设置为中间。下面是我的代码中的一个改编 offset 计算示例。

第 3 期

一旦y的值超过了canvas的高度(条件y > opts.rect.height),就应该退回到之前的fontSize。为了解决这个问题,可以引入新变量来存储先前(即最后一次拟合)迭代的值,并用于此必要的 'step back'。 (我下面代码中的那些变量是 lastFittingLineslastFittingFontlastFittingYlastFittingLineHeight。)

示例图片:

修改后的代码如下:

const fs = require('fs')
const { createCanvas } = require('canvas')


const width = 2000;
const height = 2000;

const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')

context.fillStyle = '#edf4ff'
context.fillRect(0, 0, width, height)


context.textAlign = 'center'
context.textBaseline = 'middle';
context.fillStyle = '#002763'


const fontSizeUsed = drawMultilineText(
    context,
    "This is a text with multiple lines that is vertically centered as expected.",
    {
        rect: {
            x: 1000,
            y: 0,
            width: 2000,
            height: 2000
        },
        font: 'Arial',
        verbose: true,
        lineHeight: 1,
        minFontSize: 100,
        maxFontSize: 200
    }
)

const buffer = canvas.toBuffer('image/png')
fs.writeFileSync('./image3.png', buffer)

function drawMultilineText(ctx, text, opts) {

    // Default options
    if (!opts)
        opts = {}
    if (!opts.font)
        opts.font = 'sans-serif'
    if (typeof opts.stroke == 'undefined')
        opts.stroke = false
    if (typeof opts.verbose == 'undefined')
        opts.verbose = false
    if (!opts.rect)
        opts.rect = {
            x: 0,
            y: 0,
            width: ctx.canvas.width,
            height: ctx.canvas.height
        }
    if (!opts.lineHeight)
        opts.lineHeight = 1.1
    if (!opts.minFontSize)
        opts.minFontSize = 30
    if (!opts.maxFontSize)
        opts.maxFontSize = 100
    // Default log function is console.log - Note: if verbose il false, nothing will be logged anyway
    if (!opts.logFunction)
        opts.logFunction = function (message) { console.log(message) }


    const words = require('words-array')(text)
    if (opts.verbose) opts.logFunction('Text contains ' + words.length + ' words')
    var lines = []
    let y;  //New Line

    // Finds max font size  which can be used to print whole text in opts.rec

    
    let lastFittingLines;                       // declaring 4 new variables (addressing issue 3)
    let lastFittingFont;
    let lastFittingY;
    let lastFittingLineHeight;
    for (var fontSize = opts.minFontSize; fontSize <= opts.maxFontSize; fontSize++) {

        // Line height
        var lineHeight = fontSize * opts.lineHeight

        // Set font for testing with measureText()
        ctx.font = ' ' + fontSize + 'px ' + opts.font

        // Start
        var x = opts.rect.x;
        y = lineHeight; //modified line        // setting to lineHeight as opposed to fontSize (addressing issue 1)
        lines = []
        var line = ''

        // Cycles on words

       
        for (var word of words) {
            // Add next word to line
            var linePlus = line + word + ' '
            // If added word exceeds rect width...
            if (ctx.measureText(linePlus).width > (opts.rect.width)) {
                // ..."prints" (save) the line without last word
                lines.push({ text: line, x: x, y: y })
                // New line with ctx last word
                line = word + ' '
                y += lineHeight
            } else {
                // ...continues appending words
                line = linePlus
            }
        }

        // "Print" (save) last line
        lines.push({ text: line, x: x, y: y })

        // If bottom of rect is reached then breaks "fontSize" cycle
            
        if (y > opts.rect.height)                                           
            break;
            
        lastFittingLines = lines;               // using 4 new variables for 'step back' (issue 3)
        lastFittingFont = ctx.font;
        lastFittingY = y;
        lastFittingLineHeight = lineHeight;

    }

    lines = lastFittingLines;                   // assigning last fitting values (issue 3)                    
    ctx.font = lastFittingFont;                                                                   
    if (opts.verbose) opts.logFunction("Font used: " + ctx.font);
    const offset = opts.rect.y - lastFittingLineHeight / 2 + (opts.rect.height - lastFittingY) / 2;     // modifying calculation (issue 2)
    for (var line of lines)
        // Fill or stroke
        if (opts.stroke)
            ctx.strokeText(line.text.trim(), line.x, line.y + offset) //modified line
        else
            ctx.fillText(line.text.trim(), line.x, line.y + offset) //modified line

    // Returns font size
    return fontSize
}