I want to center text on a canvas. Horizontally seems fine, but vertical is still a problem: I cannot figure out how to programmatically do this.
I was previously able to do this with 1 line, but now I'd like to get it to work for multiple lines of text.
The image right now looks like this:
the text should be positioned a bit higher, or am I mistaken?
Here is the code:
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)
and the crucial function drawMultiLineText, that's supposed to align the text, is this one:
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
}
I am not in a browser, I am using node.js.
You're right, the text in your first image should be positioned higher, as well.
There are 3 issues in the code:
y
to fontSize
offset
fontSize
(i.e. the last one fitting)Issue 1
The initial value of y
should be set to lineHeight
as opposed to fontSize
.
Issue 2
The calculation of variable offset
is not reflecting the facts that a) the starting y
coordinate for the 1st text line is lineHeight
instead of 0
and b) there is a textBaseline
set to middle. An example of an adapted offset
calculation is in my code below.
Issue 3
Once the value of y
exceeds the canvas height (condition y > opts.rect.height
), there should be a step back to the previous fontSize
. To solve this, new variables can be introduced to store the values from the previous (i.e. last fitting) iteration and used for this necessary 'step back'. (Those variables in my code below are lastFittingLines
, lastFittingFont
, lastFittingY
and lastFittingLineHeight
.)
Example image:
Here's the modified code:
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
}