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 个问题:
- 将
y
的初始值设置为fontSize
- 计算变量
offset
- 当超过 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'。 (我下面代码中的那些变量是 lastFittingLines
、lastFittingFont
、lastFittingY
和 lastFittingLineHeight
。)
示例图片:
修改后的代码如下:
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
}
我想在 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 个问题:
- 将
y
的初始值设置为fontSize
- 计算变量
offset
- 当超过 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'。 (我下面代码中的那些变量是 lastFittingLines
、lastFittingFont
、lastFittingY
和 lastFittingLineHeight
。)
示例图片:
修改后的代码如下:
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
}