p5
Version:
[](https://www.npmjs.com/package/p5)
1,666 lines (1,593 loc) • 81 kB
JavaScript
import { R as Renderer } from '../p5.Renderer-R23xoC7s.js';
import '../creating_reading-Cr8L2Jnm.js';
import 'colorjs.io/fn';
import '../color/color_spaces/hsb.js';
import '../constants-BRcElHU3.js';
import '../image/filters.js';
import '../math/p5.Vector.js';
import '../shape/custom_shapes.js';
import '../core/States.js';
import '../io/utilities.js';
import 'file-saver';
/**
* @module Typography
* @requires core
*/
const textCoreConstants = {
IDEOGRAPHIC: 'ideographic',
RIGHT_TO_LEFT: 'rtl',
LEFT_TO_RIGHT: 'ltr',
_CTX_MIDDLE: 'middle',
_TEXT_BOUNDS: '_textBoundsSingle',
_FONT_BOUNDS: '_fontBoundsSingle',
HANGING: 'hanging',
START: 'start',
END: 'end',
};
function textCore(p5, fn) {
const LeadingScale = 1.275;
const DefaultFill = '#000000';
const LinebreakRe = /\r?\n/g;
const CommaDelimRe = /,\s+/;
const QuotedRe = /^".*"$/;
const TabsRe = /\t/g;
const FontVariationSettings = 'fontVariationSettings';
const VariableAxes = ['wght', 'wdth', 'ital', 'slnt', 'opsz'];
const VariableAxesRe = new RegExp(`(?:${VariableAxes.join('|')})`);
const textFunctions = [
'text',
'textAlign',
'textAscent',
'textDescent',
'textLeading',
'textMode',
'textFont',
'textSize',
'textStyle',
'textWidth',
'textWrap',
'textBounds',
'textDirection',
'textProperty',
'textProperties',
'fontBounds',
'fontWidth',
'fontAscent',
'fontDescent',
'textWeight'
];
/**
* Draws text to the canvas.
*
* The first parameter, `str`, is the text to be drawn. The second and third
* parameters, `x` and `y`, set the coordinates of the text's bottom-left
* corner. See <a href="#/p5/textAlign">textAlign()</a> for other ways to
* align text.
*
* The fourth and fifth parameters, `maxWidth` and `maxHeight`, are optional.
* They set the dimensions of the invisible rectangle containing the text. By
* default, they set its maximum width and height. See
* <a href="#/p5/rectMode">rectMode()</a> for other ways to define the
* rectangular text box. Text will wrap to fit within the text box. Text
* outside of the box won't be drawn.
*
* Text can be styled a few ways. Call the <a href="#/p5/fill">fill()</a>
* function to set the text's fill color. Call
* <a href="#/p5/stroke">stroke()</a> and
* <a href="#/p5/strokeWeight">strokeWeight()</a> to set the text's outline.
* Call <a href="#/p5/textSize">textSize()</a> and
* <a href="#/p5/textFont">textFont()</a> to set the text's size and font,
* respectively.
*
* Note: `WEBGL` mode only supports fonts loaded with
* <a href="#/p5/loadFont">loadFont()</a>. Calling
* <a href="#/p5/stroke">stroke()</a> has no effect in `WEBGL` mode.
*
* @method text
* @param {String|Object|Array|Number|Boolean} str text to be displayed.
* @param {Number} x x-coordinate of the text box.
* @param {Number} y y-coordinate of the text box.
* @param {Number} [maxWidth] maximum width of the text box. See
* <a href="#/p5/rectMode">rectMode()</a> for
* other options.
* @param {Number} [maxHeight] maximum height of the text box. See
* <a href="#/p5/rectMode">rectMode()</a> for
* other options.
*
* @for p5
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
* background(200);
* text('hi', 50, 50);
*
* describe('The text "hi" written in black in the middle of a gray square.');
* }
* </code>
* </div>
*
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
* background('skyblue');
* textSize(100);
* text('🌈', 0, 100);
*
* describe('A rainbow in a blue sky.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
* textSize(32);
* fill(255);
* stroke(0);
* strokeWeight(4);
* text('hi', 50, 50);
*
* describe('The text "hi" written in white with a black outline.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
* background('black');
* textSize(22);
* fill('yellow');
* text('rainbows', 6, 20);
* fill('cornflowerblue');
* text('rainbows', 6, 45);
* fill('tomato');
* text('rainbows', 6, 70);
* fill('limegreen');
* text('rainbows', 6, 95);
*
* describe('The text "rainbows" written on several lines, each in a different color.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
* background(200);
* let s = 'The quick brown fox jumps over the lazy dog.';
* text(s, 10, 10, 70, 80);
*
* describe('The sample text "The quick brown fox..." written in black across several lines.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
* background(200);
* rectMode(CENTER);
* let s = 'The quick brown fox jumps over the lazy dog.';
* text(s, 50, 50, 70, 80);
*
* describe('The sample text "The quick brown fox..." written in black across several lines.');
* }
* </code>
* </div>
*
* @example
* <div modernizr='webgl'>
* <code>
* let font;
*
* async function setup() {
* createCanvas(100, 100, WEBGL);
* font = await loadFont('assets/inconsolata.otf');
* textFont(font);
* textSize(32);
* textAlign(CENTER, CENTER);
* }
*
* function draw() {
* background(200);
* rotateY(frameCount / 30);
* text('p5*js', 0, 0);
*
* describe('The text "p5*js" written in white and spinning in 3D.');
* }
* </code>
* </div>
*/
/**
* Sets the way text is aligned when <a href="#/p5/text">text()</a> is called.
*
* By default, calling `text('hi', 10, 20)` places the bottom-left corner of
* the text's bounding box at (10, 20).
*
* The first parameter, `horizAlign`, changes the way
* <a href="#/p5/text">text()</a> interprets x-coordinates. By default, the
* x-coordinate sets the left edge of the bounding box. `textAlign()` accepts
* the following values for `horizAlign`: `LEFT`, `CENTER`, or `RIGHT`.
*
* The second parameter, `vertAlign`, is optional. It changes the way
* <a href="#/p5/text">text()</a> interprets y-coordinates. By default, the
* y-coordinate sets the bottom edge of the bounding box. `textAlign()`
* accepts the following values for `vertAlign`: `TOP`, `BOTTOM`, `CENTER`,
* or `BASELINE`.
*
* @method textAlign
* @for p5
* @param {LEFT|CENTER|RIGHT} horizAlign horizontal alignment
* @param {TOP|BOTTOM|CENTER|BASELINE} [vertAlign] vertical alignment
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Draw a vertical line.
* strokeWeight(0.5);
* line(50, 0, 50, 100);
*
* // Top line.
* textSize(16);
* textAlign(RIGHT);
* text('ABCD', 50, 30);
*
* // Middle line.
* textAlign(CENTER);
* text('EFGH', 50, 50);
*
* // Bottom line.
* textAlign(LEFT);
* text('IJKL', 50, 70);
*
* describe('The letters ABCD displayed at top-left, EFGH at center, and IJKL at bottom-right. A vertical line divides the canvas in half.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* strokeWeight(0.5);
*
* // First line.
* line(0, 12, width, 12);
* textAlign(CENTER, TOP);
* text('TOP', 50, 12);
*
* // Second line.
* line(0, 37, width, 37);
* textAlign(CENTER, CENTER);
* text('CENTER', 50, 37);
*
* // Third line.
* line(0, 62, width, 62);
* textAlign(CENTER, BASELINE);
* text('BASELINE', 50, 62);
*
* // Fourth line.
* line(0, 97, width, 97);
* textAlign(CENTER, BOTTOM);
* text('BOTTOM', 50, 97);
*
* describe('The words "TOP", "CENTER", "BASELINE", and "BOTTOM" each drawn relative to a horizontal line. Their positions demonstrate different vertical alignments.');
* }
* </code>
* </div>
*/
/**
* Returns the ascent of the text.
*
* The `textAscent()` function calculates the distance from the baseline to the
* highest point of the current font. This value represents the ascent, which is essential
* for determining the overall height of the text along with `textDescent()`. If
* a text string is provided as an argument, the ascent is calculated based on that specific
* string; otherwise, the ascent of the current font is returned.
*
* @method textAscent
* @for p5
*
* @param {String} [txt] - (Optional) The text string for which to calculate the ascent.
* If omitted, the function returns the ascent for the current font.
* @returns {Number} The ascent value in pixels.
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(400, 300);
* background(220);
*
* textSize(48);
* textAlign(LEFT, BASELINE);
* textFont('Georgia');
*
* let s = "Hello, p5.js!";
* let x = 50, y = 150;
*
* fill(0);
* text(s, x, y);
*
* // Get the ascent of the current font
* let asc = textAscent();
*
* // Draw a red line at the baseline and a blue line at the ascent position
* stroke('red');
* line(x, y, x + 200, y); // Baseline
* stroke('blue');
* line(x, y - asc, x + 200, y - asc); // Ascent (top of text)
*
* noStroke();
* fill(0);
* textSize(16);
* text("textAscent: " + asc.toFixed(2) + " pixels", x, y - asc - 10);
* }
* </code>
* </div>
*
*
* @example
* <div>
* <code>
* let font;
*
* async function setup() {
* font = await loadFont('assets/inconsolata.otf');
*
* createCanvas(100, 100);
*
* background(200);
*
* // Style the text.
* textFont(font);
*
* // Different for each font.
* let fontScale = 0.8;
*
* let baseY = 75;
* strokeWeight(0.5);
*
* // Draw small text.
* textSize(24);
* text('dp', 0, baseY);
*
* // Draw baseline and ascent.
* let a = textAscent() * fontScale;
* line(0, baseY, 23, baseY);
* line(23, baseY - a, 23, baseY);
*
* // Draw large text.
* textSize(48);
* text('dp', 45, baseY);
*
* // Draw baseline and ascent.
* a = textAscent() * fontScale;
* line(45, baseY, 91, baseY);
* line(91, baseY - a, 91, baseY);
*
* describe('The letters "dp" written twice in different sizes. Each version has a horizontal baseline. A vertical line extends upward from each baseline to the top of the "d".');
* }
* </code>
* </div>
*/
/**
* Returns the descent of the text.
*
* The `textDescent()` function calculates the distance from the baseline to the
* lowest point of the current font. This value represents the descent, which, when combined
* with the ascent (from `textAscent()`), determines the overall vertical span of the text.
* If a text string is provided as an argument, the descent is calculated based on that specific string;
* otherwise, the descent of the current font is returned.
*
* @method textDescent
* @for p5
*
* @param {String} [txt] - (Optional) The text string for which to calculate the descent.
* If omitted, the function returns the descent for the current font.
* @returns {Number} The descent value in pixels.
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(400, 300);
* background(220);
*
* textSize(48);
* textAlign(LEFT, BASELINE);
* textFont('Georgia');
*
* let s = "Hello, p5.js!";
* let x = 50, y = 150;
*
* fill(0);
* text(s, x, y);
*
* // Get the descent of the current font
* let desc = textDescent();
*
* // Draw a red line at the baseline and a blue line at the bottom of the text
* stroke('red');
* line(x, y, x + 200, y); // Baseline
* stroke('blue');
* line(x, y + desc, x + 200, y + desc); // Descent (bottom of text)
*
* noStroke();
* fill(0);
* textSize(16);
* text("textDescent: " + desc.toFixed(2) + " pixels", x, y + desc + 20);
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* let font;
*
* async function setup() {
* font = await loadFont('assets/inconsolata.otf');
*
* createCanvas(100, 100);
*
* background(200);
*
* // Style the font.
* textFont(font);
*
* // Different for each font.
* let fontScale = 0.9;
*
* let baseY = 75;
* strokeWeight(0.5);
*
* // Draw small text.
* textSize(24);
* text('dp', 0, baseY);
*
* // Draw baseline and descent.
* let d = textDescent() * fontScale;
* line(0, baseY, 23, baseY);
* line(23, baseY, 23, baseY + d);
*
* // Draw large text.
* textSize(48);
* text('dp', 45, baseY);
*
* // Draw baseline and descent.
* d = textDescent() * fontScale;
* line(45, baseY, 91, baseY);
* line(91, baseY, 91, baseY + d);
*
* describe('The letters "dp" written twice in different sizes. Each version has a horizontal baseline. A vertical line extends downward from each baseline to the bottom of the "p".');
* }
* </code>
* </div>
*/
/**
* Sets the spacing between lines of text when
* <a href="#/p5/text">text()</a> is called.
*
* Note: Spacing is measured in pixels.
*
* Calling `textLeading()` without an argument returns the current spacing.
*
* @method textLeading
* @for p5
* @param {Number} leading The new text leading to apply, in pixels
* @returns {Number} If no arguments are provided, the current text leading
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // "\n" starts a new line of text.
* let lines = 'one\ntwo';
*
* // Left.
* text(lines, 10, 25);
*
* // Right.
* textLeading(30);
* text(lines, 70, 25);
*
* describe('The words "one" and "two" written on separate lines twice. The words on the left have less vertical spacing than the words on the right.');
* }
* </code>
* </div>
*/
/*
* @method textLeading
* @for p5
*/
/**
* Sets the font used by the <a href="#/p5/text">text()</a> function.
*
* The first parameter, `font`, sets the font. `textFont()` recognizes either
* a <a href="#/p5.Font">p5.Font</a> object or a string with the name of a
* system font. For example, `'Courier New'`.
*
* The second parameter, `size`, is optional. It sets the font size in pixels.
* This has the same effect as calling <a href="#/p5/textSize">textSize()</a>.
*
* Note: `WEBGL` mode only supports fonts loaded with
* <a href="#/p5/loadFont">loadFont()</a>.
*
* @method textFont
* @param {p5.Font|String|Object} font The font to apply
* @param {Number} [size] An optional text size to apply.
* @for p5
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
* background(200);
* textFont('Courier New');
* textSize(24);
* text('hi', 35, 55);
*
* describe('The text "hi" written in a black, monospace font on a gray background.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
* background('black');
* fill('palegreen');
* textFont('Courier New', 10);
* text('You turn to the left and see a door. Do you enter?', 5, 5, 90, 90);
* text('>', 5, 70);
*
* describe('A text prompt from a game is written in a green, monospace font on a black background.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
* background(200);
* textFont('Verdana');
* let currentFont = textFont();
* text(currentFont, 25, 50);
*
* describe('The text "Verdana" written in a black, sans-serif font on a gray background.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* let fontRegular;
* let fontItalic;
* let fontBold;
*
* async function setup() {
* createCanvas(100, 100);
* fontRegular = await loadFont('assets/Regular.otf');
* fontItalic = await loadFont('assets/Italic.ttf');
* fontBold = await loadFont('assets/Bold.ttf');
*
* background(200);
* textFont(fontRegular);
* text('I am Normal', 10, 30);
* textFont(fontItalic);
* text('I am Italic', 10, 50);
* textFont(fontBold);
* text('I am Bold', 10, 70);
*
* describe('The statements "I am Normal", "I am Italic", and "I am Bold" written in black on separate lines. The statements have normal, italic, and bold fonts, respectively.');
* }
* </code>
* </div>
*/
/**
* Sets or gets the current text size.
*
* The `textSize()` function is used to specify the size of the text
* that will be rendered on the canvas. When called with an argument, it sets the
* text size to the specified value (which can be a number representing pixels or a
* CSS-style string, e.g., '32px', '2em'). When called without an argument, it
* returns the current text size in pixels.
*
* @method textSize
* @for p5
*
* @param {Number} size - The size to set for the text.
* @returns {Number} If no arguments are provided, the current text size in pixels.
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(600, 200);
* background(240);
*
* // Set the text size to 48 pixels
* textSize(48);
* textAlign(CENTER, CENTER);
* textFont("Georgia");
*
* // Draw text using the current text size
* fill(0);
* text("Hello, p5.js!", width / 2, height / 2);
*
* // Retrieve and display the current text size
* let currentSize = textSize();
* fill(50);
* textSize(16);
* text("Current text size: " + currentSize, width / 2, height - 20);
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Top.
* textSize(12);
* text('Font Size 12', 10, 30);
*
* // Middle.
* textSize(14);
* text('Font Size 14', 10, 60);
*
* // Bottom.
* textSize(16);
* text('Font Size 16', 10, 90);
*
* describe('The text "Font Size 12" drawn small, "Font Size 14" drawn medium, and "Font Size 16" drawn large.');
* }
* </code>
* </div>
*/
/**
* @method textSize
* @for p5
* @returns {Number} The current text size in pixels.
*/
/**
* Sets the style for system fonts when
* <a href="#/p5/text">text()</a> is called.
*
* The parameter, `style`, can be either `NORMAL`, `ITALIC`, `BOLD`, or
* `BOLDITALIC`.
*
* `textStyle()` may be overridden by CSS styling. This function doesn't
* affect fonts loaded with <a href="#/p5/loadFont">loadFont()</a>.
*
* @method textStyle
* @for p5
* @param {NORMAL|ITALIC|BOLD|BOLDITALIC} style The style to use
* @returns {NORMAL|ITALIC|BOLD|BOLDITALIC} If no arguments are provided, the current style
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Style the text.
* textSize(12);
* textAlign(CENTER);
*
* // First row.
* textStyle(NORMAL);
* text('Normal', 50, 15);
*
* // Second row.
* textStyle(ITALIC);
* text('Italic', 50, 40);
*
* // Third row.
* textStyle(BOLD);
* text('Bold', 50, 65);
*
* // Fourth row.
* textStyle(BOLDITALIC);
* text('Bold Italic', 50, 90);
*
* describe('The words "Normal" displayed normally, "Italic" in italic, "Bold" in bold, and "Bold Italic" in bold italics.');
* }
* </code>
* </div>
*/
/**
* @method textStyle
* @for p5
* @returns {NORMAL|BOLD|ITALIC|BOLDITALIC}
*/
/**
* Calculates the width of the given text string in pixels.
*
* The `textWidth()` function processes the provided text string to determine its tight bounding box
* based on the current text properties such as font, textSize, and textStyle. Internally, it splits
* the text into individual lines (if line breaks are present) and computes the bounding box for each
* line using the renderer’s measurement functions. The final width is determined as the maximum width
* among all these lines.
*
* For example, if the text contains multiple lines due to wrapping or explicit line breaks, textWidth()
* will return the width of the longest line.
*
* @method textWidth
* @for p5
* @param {String} text The text to measure
* @returns {Number} The width of the text
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(200, 200);
* background(220);
*
* // Set text size and alignment
* textSize(48);
* textAlign(LEFT, TOP);
*
* let myText = "Hello";
*
* // Calculate the width of the text
* let tw = textWidth(myText);
*
* // Draw the text on the canvas
* fill(0);
* text(myText, 50, 50);
*
* // Display the text width below
* noStroke();
* fill(0);
* textSize(20);
* text("Text width: " + tw, 10, 150);
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Style the text.
* textSize(28);
* strokeWeight(0.5);
*
* // Calculate the text width.
* let s = 'yoyo';
* let w = textWidth(s);
*
* // Display the text.
* text(s, 22, 55);
*
* // Underline the text.
* line(22, 55, 22 + w, 55);
*
* describe('The word "yoyo" underlined.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Style the text.
* textSize(28);
* strokeWeight(0.5);
*
* // Calculate the text width.
* // "\n" starts a new line.
* let s = 'yo\nyo';
* let w = textWidth(s);
*
* // Display the text.
* text(s, 22, 55);
*
* // Underline the text.
* line(22, 55, 22 + w, 55);
*
* describe('The word "yo" written twice, one copy beneath the other. The words are divided by a horizontal line.');
* }
* </code>
* </div>
*/
/**
* Sets the style for wrapping text when
* <a href="#/p5/text">text()</a> is called.
*
* The parameter, `style`, can be one of the following values:
*
* `WORD` starts new lines of text at spaces. If a string of text doesn't
* have spaces, it may overflow the text box and the canvas. This is the
* default style.
*
* `CHAR` starts new lines as needed to stay within the text box.
*
* `textWrap()` only works when the maximum width is set for a text box. For
* example, calling `text('Have a wonderful day', 0, 10, 100)` sets the
* maximum width to 100 pixels.
*
* Calling `textWrap()` without an argument returns the current style.
*
* @method textWrap
* @for p5
*
* @param {WORD|CHAR} style The wrapping style to use
* @returns {CHAR|WORD} If no arguments are provided, the current wrapping style
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Style the text.
* textSize(20);
* textWrap(WORD);
*
* // Display the text.
* text('Have a wonderful day', 0, 10, 100);
*
* describe('The text "Have a wonderful day" written across three lines.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Style the text.
* textSize(20);
* textWrap(CHAR);
*
* // Display the text.
* text('Have a wonderful day', 0, 10, 100);
*
* describe('The text "Have a wonderful day" written across two lines.');
* }
* </code>
* </div>
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(100, 100);
*
* background(200);
*
* // Style the text.
* textSize(20);
* textWrap(CHAR);
*
* // Display the text.
* text('祝你有美好的一天', 0, 10, 100);
*
* describe('The text "祝你有美好的一天" written across two lines.');
* }
* </code>
* </div>
*/
/**
* @method textWrap
* @for p5
* @returns {CHAR|WORD} The current wrapping style
*/
/**
* Computes the tight bounding box for a block of text.
*
* The `textBounds()` function calculates the precise pixel boundaries that enclose
* the rendered text based on the current text properties (such as font, textSize, textStyle, and
* alignment). If the text spans multiple lines (due to line breaks or wrapping), the function
* measures each line individually and then aggregates these measurements into a single bounding box.
* The resulting object contains the x and y coordinates along with the width (w) and height (h)
* of the text block.
*
* @method textBounds
* @for p5
*
* @param {String} str - The text string to measure.
* @param {Number} x - The x-coordinate where the text is drawn.
* @param {Number} y - The y-coordinate where the text is drawn.
* @param {Number} [width] - (Optional) The maximum width available for the text block.
* When specified, the text may be wrapped to fit within this width.
* @param {Number} [height] - (Optional) The maximum height available for the text block.
* Any lines exceeding this height will be truncated.
* @returns {Object} An object with properties `x`, `y`, `w`, and `h` that represent the tight
* bounding box of the rendered text.
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(300, 200);
* background(220);
*
* // Set up text properties for clarity
* textSize(32);
* textAlign(LEFT, TOP);
*
* let txt = "Hello, World!";
* // Compute the bounding box for the text starting at (50, 50)
* let bounds = textBounds(txt, 50, 50);
*
* // Draw the text
* fill(0);
* text(txt, 50, 50);
*
* // Draw the computed bounding box in red to visualize the measured area
* noFill();
* stroke('red');
* rect(bounds.x, bounds.y, bounds.w, bounds.h);
* }
* </code>
* </div>
*/
/**
* Sets or gets the text drawing direction.
*
* The <code>textDirection()</code> function allows you to specify the direction in which text is
* rendered on the canvas. When provided with a <code>direction</code> parameter (such as "ltr" for
* left-to-right, "rtl" for right-to-left, or "inherit"), it updates the renderer's state with that
* value and applies the new setting. When called without any arguments, it returns the current text
* direction. This function is particularly useful for rendering text in languages with different
* writing directions.
*
* @method textDirection
* @for p5
*
* @param {String} direction - The text direction to set ("ltr", "rtl", or "inherit").
* @returns {String} If no arguments are provided, the current text direction, either "ltr", "rtl", or "inherit"
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(300, 300);
* background(240);
*
* textSize(32);
* textFont("Georgia");
* textAlign(LEFT, TOP);
*
* // Set text direction to right-to-left and draw Arabic text.
* textDirection("rtl");
* fill(0);
* text("مرحبًا!", 50, 50);
*
* // Set text direction to left-to-right and draw English text.
* textDirection("ltr");
* text("Hello, p5.js!", 50, 150);
*
* // Display the current text direction.
* textSize(16);
* fill(50);
* textAlign(LEFT, TOP);
* text("Current textDirection: " + textDirection(), 50, 250);
* }
* </code>
* </div>
*/
/**
* @method textDirection
* @for p5
* @returns {String} The current text direction, either "ltr", "rtl", or "inherit"
*/
/**
* Sets or gets a single text property for the renderer.
*
* The `textProperty()` function allows you to set or retrieve a single text-related property,
* such as `textAlign`, `textBaseline`, `fontStyle`, or any other property
* that may be part of the renderer's state, its drawing context, or the canvas style.
*
* When called with a `prop` and a `value`, the function sets the property by checking
* for its existence in the renderer's state, the drawing context, or the canvas style. If the property is
* successfully modified, the function applies the updated text properties. If called with only the
* `prop` parameter, the function returns the current value of that property.
*
* @method textProperty
* @for p5
*
* @param {String} prop - The name of the text property to set or get.
* @param value - The value to set for the specified text property. If omitted, the current
* value of the property is returned
* @returns If no arguments are provided, the current value of the specified text property
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(300, 300);
* background(240);
*
* // Set the text alignment to CENTER and the baseline to TOP using textProperty.
* textProperty("textAlign", CENTER);
* textProperty("textBaseline", TOP);
*
* // Set additional text properties and draw the text.
* textSize(32);
* textFont("Georgia");
* fill(0);
* text("Hello, World!", width / 2, 50);
*
* // Retrieve and display the current text properties.
* let currentAlign = textProperty("textAlign");
* let currentBaseline = textProperty("textBaseline");
*
* textSize(16);
* textAlign(LEFT, TOP);
* fill(50);
* text("Current textAlign: " + currentAlign, 50, 150);
* text("Current textBaseline: " + currentBaseline, 50, 170);
* }
* </code>
* </div>
*/
/**
* @method textProperty
* @for p5
* @param {String} prop - The name of the text property to set or get.
* @returns The current value of the specified text property
*/
/**
* Gets or sets text properties in batch, similar to calling `textProperty()`
* multiple times.
*
* If an object is passed in, `textProperty(key, value)` will be called for you
* on every key/value pair in the object.
*
* If no arguments are passed in, an object will be returned with all the current
* properties.
*
* @method textProperties
* @for p5
* @param {Object} properties An object whose keys are properties to set, and whose
* values are what they should be set to.
*/
/**
* @method textProperties
* @for p5
* @returns {Object} An object with all the possible properties and their current values.
*/
/**
* Computes a generic (non-tight) bounding box for a block of text.
*
* The `fontBounds()` function calculates the bounding box for the text based on the
* font's intrinsic metrics (such as `fontBoundingBoxAscent` and
* `fontBoundingBoxDescent`). Unlike `textBounds()`, which measures the exact
* pixel boundaries of the rendered text, `fontBounds()` provides a looser measurement
* derived from the font’s default spacing. This measurement is useful for layout purposes where
* a consistent approximation of the text's dimensions is desired.
*
* @method fontBounds
* @for p5
*
* @param {String} str - The text string to measure.
* @param {Number} x - The x-coordinate where the text is drawn.
* @param {Number} y - The y-coordinate where the text is drawn.
* @param {Number} [width] - (Optional) The maximum width available for the text block.
* When specified, the text may be wrapped to fit within this width.
* @param {Number} [height] - (Optional) The maximum height available for the text block.
* Any lines exceeding this height will be truncated.
* @returns {Object} An object with properties `x`, `y`, `w`, and `h` representing the loose
* bounding box of the text based on the font's intrinsic metrics.
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(300, 200);
* background(240);
*
* textSize(32);
* textAlign(LEFT, TOP);
* textFont('Georgia');
*
* let txt = "Hello, World!";
* // Compute the bounding box based on the font's intrinsic metrics
* let bounds = fontBounds(txt, 50, 50);
*
* fill(0);
* text(txt, 50, 50);
*
* noFill();
* stroke('green');
* rect(bounds.x, bounds.y, bounds.w, bounds.h);
*
* noStroke();
* fill(50);
* textSize(15);
* text("Font Bounds: x=" + bounds.x.toFixed(1) + ", y=" + bounds.y.toFixed(1) +
* ", w=" + bounds.w.toFixed(1) + ", h=" + bounds.h.toFixed(1), 8, 100);
* }
* </code>
* </div>
*/
/**
* Returns the loose width of a text string based on the current font.
*
* The `fontWidth()` function measures the width of the provided text string using
* the font's default measurement (i.e., the width property from the text metrics returned by
* the browser). Unlike `textWidth()`, which calculates the tight pixel boundaries
* of the text glyphs, `fontWidth()` uses the font's intrinsic spacing, which may include
* additional space for character spacing and kerning. This makes it useful for scenarios where
* an approximate width is sufficient for layout and positioning.
*
* @method fontWidth
* @for p5
*
* @param {String} theText - The text string to measure.
* @returns {Number} The loose width of the text in pixels.
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(300, 200);
* background(240);
*
* textSize(32);
* textAlign(LEFT, TOP);
* textFont('Georgia');
*
* let s = "Hello, World!";
* let fw = fontWidth(s);
*
* fill(0);
* text(s, 50, 50);
*
* stroke('blue');
* line(50, 90, 50 + fw, 90);
*
* noStroke();
* fill(50);
* textSize(16);
* text("Font width: " + fw.toFixed(2) + " pixels", 50, 100);
* }
* </code>
* </div>
*/
/**
* Returns the loose ascent of the text based on the font's intrinsic metrics.
*
* The `fontAscent()` function calculates the ascent of the text using the font's
* intrinsic metrics (e.g., `fontBoundingBoxAscent`). This value represents the space
* above the baseline that the font inherently occupies, and is useful for layout purposes when
* an approximate vertical measurement is required.
*
* @method fontAscent
* @for p5
*
* @returns {Number} The loose ascent value in pixels.
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(300, 300);
* background(220);
*
* textSize(35);
* textAlign(LEFT, BASELINE);
* textFont('Georgia');
*
* let s = "Hello, p5.js!";
* let x = 50, y = 150;
*
* fill(0);
* text(s, x, y);
*
* // Get the font descent of the current font
* let fasc = fontAscent();
*
* // Draw a red line at the baseline and a blue line at the ascent position
* stroke('red');
* line(x, y, x + 200, y); // Baseline
* stroke('blue');
* line(x, y - fasc, x + 200, y - fasc); // Font ascent position
*
* noStroke();
* fill(0);
* textSize(16);
* text("fontAscent: " + fasc.toFixed(2) + " pixels", x, y + fdesc + 20);
* }
* </code>
* </div>
*/
/**
* Returns the loose descent of the text based on the font's intrinsic metrics.
*
* The `fontDescent()` function calculates the descent of the text using the font's
* intrinsic metrics (e.g., `fontBoundingBoxDescent`). This value represents the space
* below the baseline that the font inherently occupies, and is useful for layout purposes when
* an approximate vertical measurement is required.
*
* @method fontDescent
* @for p5
*
* @returns {Number} The loose descent value in pixels.
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(300, 300);
* background(220);
*
* textSize(48);
* textAlign(LEFT, BASELINE);
* textFont('Georgia');
*
* let s = "Hello, p5.js!";
* let x = 50, y = 150;
*
* fill(0);
* text(s, x, y);
*
* // Get the font descent of the current font
* let fdesc = fontDescent();
*
* // Draw a red line at the baseline and a blue line at the descent position
* stroke('red');
* line(x, y, x + 200, y); // Baseline
* stroke('blue');
* line(x, y + fdesc, x + 200, y + fdesc); // Font descent position
*
* noStroke();
* fill(0);
* textSize(16);
* text("fontDescent: " + fdesc.toFixed(2) + " pixels", x, y + fdesc + 20);
* }
* </code>
* </div>
*/
/**
*
* Sets or gets the current font weight.
*
* The <code>textWeight()</code> function is used to specify the weight (thickness) of the text.
* When a numeric value is provided, it sets the font weight to that value and updates the
* rendering properties accordingly (including the "font-variation-settings" on the canvas style).
* When called without an argument, it returns the current font weight setting.
*
* @method textWeight
* @for p5
*
* @param {Number} weight - The numeric weight value to set for the text.
* @returns {Number} If no arguments are provided, the current font weight
*
* @example
* <div>
* <code>
* function setup() {
* createCanvas(300, 200);
* background(240);
*
* // Set text alignment, size, and font
* textAlign(LEFT, TOP);
* textSize(20);
* textFont("Georgia");
*
* // Draw text with a normal weight (lighter appearance)
* push();
* textWeight(400); // Set font weight to 400
* fill(0);
* text("Normal", 50, 50);
* let normalWeight = textWeight(); // Should return 400
* pop();
*
* // Draw text with a bold weight (heavier appearance)
* push();
* textWeight(900); // Set font weight to 900
* fill(0);
* text("Bold", 50, 100);
* let boldWeight = textWeight(); // Should return 900
* pop();
*
* // Display the current font weight values on the canvas
* textSize(16);
* fill(50);
* text("Normal Weight: " + normalWeight, 150, 52);
* text("Bold Weight: " + boldWeight, 150, 100);
* }
* </code>
* </div>
*
* <div>
* <code>
* let font;
*
* async function setup() {
* createCanvas(100, 100);
* font = await loadFont(
* 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap'
* );
* }
*
* function draw() {
* background(255);
* textFont(font);
* textAlign(LEFT, TOP);
* textSize(35);
* textWeight(sin(millis() * 0.002) * 200 + 400);
* text('p5*js', 0, 10);
* describe('The text p5*js pulsing its weight over time');
* }
* </code>
* </div>
*/
/**
* @method textWeight
* @for p5
* @returns {Number} The current font weight
*/
// attach each text func to p5, delegating to the renderer
textFunctions.forEach(func => {
fn[func] = function (...args) {
if (!(func in Renderer.prototype)) {
throw Error(`Renderer2D.prototype.${func} is not defined.`);
}
return this._renderer[func](...args);
};
// attach also to p5.Graphics.prototype
p5.Graphics.prototype[func] = function (...args) {
return this._renderer[func](...args);
};
});
const RendererTextProps = {
textAlign: { default: fn.LEFT, type: 'Context2d' },
textBaseline: { default: fn.BASELINE, type: 'Context2d' },
textFont: { default: { family: 'sans-serif' } },
textLeading: { default: 15 },
textSize: { default: 12 },
textWrap: { default: fn.WORD },
fontStretch: { default: fn.NORMAL, isShorthand: true }, // font-stretch: { default: normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded }
fontWeight: { default: fn.NORMAL, isShorthand: true }, // font-stretch: { default: normal | ultra-condensed | extra-condensed | condensed | semi-condensed | semi-expanded | expanded | extra-expanded | ultra-expanded }
lineHeight: { default: fn.NORMAL, isShorthand: true }, // line-height: { default: normal | number | length | percentage }
fontVariant: { default: fn.NORMAL, isShorthand: true }, // font-variant: { default: normal | small-caps }
fontStyle: { default: fn.NORMAL, isShorthand: true }, // font-style: { default: normal | italic | oblique } [was 'textStyle' in v1]
direction: { default: 'inherit' }, // direction: { default: inherit | ltr | rtl }
};
// note: font must be first here otherwise it may reset other properties
const ContextTextProps = ['font', 'direction', 'fontKerning', 'fontStretch', 'fontVariantCaps', 'letterSpacing', 'textAlign', 'textBaseline', 'textRendering', 'wordSpacing'];
// shorthand font properties that can be set with context2d.font
const ShorthandFontProps = Object.keys(RendererTextProps).filter(p => RendererTextProps[p].isShorthand);
// allowable values for font-stretch property for context2d.font
const FontStretchKeys = ["ultra-condensed", "extra-condensed", "condensed", "semi-condensed", "normal", "semi-expanded", "expanded", "extra-expanded", "ultra-expanded"];
let contextQueue, cachedDiv; // lazy
////////////////////////////// start API ///////////////////////////////
Renderer.prototype.text = function (str, x, y, width, height) {
let setBaseline = this.textDrawingContext().textBaseline; // store baseline
// adjust {x,y,w,h} properties based on rectMode
({ x, y, width, height } = this._handleRectMode(x, y, width, height));
// parse the lines according to width, height & linebreaks
let lines = this._processLines(str, width, height);
// add the adjusted positions [x,y] to each line
lines = this._positionLines(x, y, width, height, lines);
// render each line at the adjusted position
lines.forEach(line => this._renderText(line.text, line.x, line.y));
this.textDrawingContext().textBaseline = setBaseline; // restore baseline
};
/**
* Computes the precise (tight) bounding box for a block of text
* @param {String} str - the text to measure
* @param {Number} x - the x-coordinate of the text
* @param {Number} y - the y-coordinate of the text
* @param {Number} width - the max width of the text block
* @param {Number} height - the max height of the text block
* @returns - a bounding box object for the text block: {x,y,w,h}
* @private
*/
Renderer.prototype.textBounds = function (str, x, y, width, height) {
// delegate to _textBoundsSingle for measuring
return this._computeBounds(textCoreConstants._TEXT_BOUNDS, str, x, y, width, height).bounds;
};
/**
* Computes a generic (non-tight) bounding box for a block of text
* @param {String} str - the text to measure
* @param {Number} x - the x-coordinate of the text
* @param {Number} y - the y-coordinate of the text
* @param {Number} width - the max width of the text block
* @param {Number} height - the max height of the text block
* @returns - a bounding box object for the text block: {x,y,w,h}
* @private
*/
Renderer.prototype.fontBounds = function (str, x, y, width, height) {
// delegate to _fontBoundsSingle for measuring
return this._computeBounds(textCoreConstants._FONT_BOUNDS, str, x, y, width, height).bounds;
};
/**
* Get the width of a text string in pixels (tight bounds)
* @param {String} theText
* @returns - the width of the text in pixels
* @private
*/
Renderer.prototype.textWidth = function (theText) {
let lines = this._processLines(theText);
// return the max width of the lines (using tight bounds)
return Math.max(...lines.map(l => this._textWidthSingle(l)));
};
/**
* Get the width of a text string in pixels (loose bounds)
* @param {String} theText
* @returns - the width of the text in pixels
* @private
*/
Renderer.prototype.fontWidth = function (theText) {
// return the max width of the lines (using loose bounds)
let lines = this._processLines(theText);
return Math.max(...lines.map(l => this._fontWidthSingle(l)));
};
/**
* @param {*} txt - optional text to measure, if provided will be
* used to compute the ascent, otherwise the font's ascent will be used
* @returns - the ascent of the text
* @private
*/
Renderer.prototype.textAscent = function (txt = '') {
if (!txt.length) return this.fontAscent();
return this.textDrawingContext().measureText(txt).actualBoundingBoxAscent;
};
/**
* @returns - returns the ascent for the current font
* @private
*/
Renderer.prototype.fontAscent = function () {
return this.textDrawingContext().measureText('_').fontBoundingBoxAscent;
};
/**
* @param {*} txt - optional text to measure, if provided will
* be used to compute the descent, otherwise the font's descent will be used
* @returns - the descent of the text
* @private
*/
Renderer.prototype.textDescent = function (txt = '') {
if (!txt.length) return this.fontDescent();
return this.textDrawingContext().measureText(txt).actualBoundingBoxDescent;
};
Renderer.prototype.fontDescent = function () {
return this.textDrawingContext().measureText('_').fontBoundingBoxDescent;
};
// setters/getters for text properties //////////////////////////
Renderer.prototype.textAlign = function (h, v) {
// the setter
if (typeof h !== 'undefined') {
this.states.setValue('textAlign', h);
if (typeof v !== 'undefined') {
if (v === fn.CENTER) {
v = textCoreConstants._CTX_MIDDLE;
}
this.states.setValue('textBaseline', v);
}
return this._applyTextProperties();
}
// the getter
return {
horizontal: this.states.textAlign,
vertical: this.states.textBaseline
};
};
Renderer.prototype._currentTextFont = function () {
return this.states.textFont.font || this.states.textFont.family;
};
/**
* Set the font and [size] and [options] for rendering text
* @param {p5.Font | string} font - the font to use for rendering text
* @param {Number} size - the size of the text, can be a number or a css-style string
* @param {Object} options - additional options for rendering text, see FontProps
* @private
*/
Renderer.prototype.textFont = function (font, size, options) {
if (arguments.length === 0) {
return this._currentTextFont();
}
let family = font;
// do we have a custon loaded font ?
if (font instanceof p5.Font) {
family = font.face.family;
}
else if (font.data instanceof Uint8Array) {
family = font.name.fontFamily;
if (font.name?.fontSubfamily) {
family += '-' + font.name.fontSubfamily;
}
}
else if (typeof font === 'string') {
// direct set the font-string if it contains size
if (typeof size === 'undefined' && /[.0-9]+(%|em|p[xt])/.test(family)) {
//console.log('direct set font-string: ', family);
({ family, size } = this._directSetFontString(family));
}
}
if (typeof family !== 'string') throw Error('null font in textFont()');
// handle two-arg case: textFont(font, options)
if (arguments.length === 2 && typeof size === 'object') {
options = size;
size = undefined;
}
// update font properties in this.states
this.states.setValue('textFont', { font, family, size });
// convert/update the size in this.states
if (typeof size !== 'undefined') {
this._setTextSize(size);
}
// apply any options to this.states
if (typeof options === 'object') {
this.textProperties(options);
}
return this._applyTextProperties();
};
Renderer.prototype._directSetFontString = function (font, debug = 0) {
if (debug) console.log('_directSetFontString"' + font + '"');
let defaults = ShorthandFontProps.reduce((props, p) => {
props[p] = RendererTextProps[p].default;
return props;
}, {});
let el = this._cachedDiv(defaults);
el.style.font = font;
let style = getComputedStyle(el);
ShorthandFontProps.forEach(prop => {
this.states[prop] = style[prop];
if (debug) console.log(' this.states.' + prop + '="' + style[prop] + '"');
});
return { family: style.fontFamily, size: style.fontSize };
};
Renderer.prototype.textLeading = function (leading) {
// the setter
if (typeof leading === 'number') {
this.states