From 06019f3a13a85e17fb8a67bd1a9c4470e06d8691 Mon Sep 17 00:00:00 2001 From: Nalin Date: Sun, 14 Sep 2025 23:09:55 +0530 Subject: [PATCH] Add fontWidth() and clarify textWidth vs fontWidth docs in attributes.js --- src/typography/attributes.js | 605 +++++++++++++++++++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 src/typography/attributes.js diff --git a/src/typography/attributes.js b/src/typography/attributes.js new file mode 100644 index 0000000000..a5ebaf0257 --- /dev/null +++ b/src/typography/attributes.js @@ -0,0 +1,605 @@ +/** + * Calculates the advance width of a string of text drawn when + * text() is called. + * + * fontWidth() returns the advance width (a "loose" measurement) of the text, including all leading and trailing whitespace. This is the full width the text would occupy, as measured by the browser's measureText().width property. + * + * textWidth() returns the width of the smallest bounding box containing the text (a "tight" measurement), which may ignore leading and trailing whitespace. This is measured using the bounding box properties of the text. + * + * Comparison: + * + * + * See also: textWidth() for tight bounding box measurement. + * + * @method fontWidth + * @param {String} str string of text to measure. + * @return {Number} width measured in units of pixels. + * @example + *
+ * + * function setup() { + * createCanvas(200, 100); + * textSize(32); + * let s1 = 'x y'; + * let s2 = ' x y '; + * let w1 = fontWidth(s1); + * let w2 = fontWidth(s2); + * text(s1, 10, 40); + * text(s2, 10, 80); + * line(10, 45, 10 + w1, 45); // underline advance width + * line(10, 85, 10 + w2, 85); // underline advance width (includes whitespace) + * describe('Shows difference in measured width for text with and without leading/trailing spaces using fontWidth.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(200, 100); + * textSize(32); + * let s = ' '; + * let w = fontWidth(s); + * text('[' + s + ']', 10, 60); // visualize the space + * line(10, 65, 10 + w, 65); // advance width for a space is nonzero + * describe('Shows that fontWidth for a single space is nonzero, even though textWidth may be 0.'); + * } + * + *
+ */ +p5.prototype.fontWidth = function (...args) { + args[0] += ""; + p5._validateParameters("fontWidth", args); + if (args[0].length === 0) { + return 0; + } + // Use the renderer's fontWidth method if available, otherwise fallback to measureText width + if (typeof this._renderer.fontWidth === "function") { + return this._renderer.fontWidth(args[0]); + } else { + // fallback: use measureText width + const ctx = this._renderer.textDrawingContext(); + return ctx.measureText(args[0]).width; + } +}; +/** + * @module Typography + * @submodule Attributes + * @for p5 + * @requires core + * @requires constants + */ + +import p5 from "../core/main"; + +/** + * Sets the way text is aligned when text() 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 + * text() 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 + * text() 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 + * @param {Constant} horizAlign horizontal alignment, either LEFT, + * CENTER, or RIGHT. + * @param {Constant} [vertAlign] vertical alignment, either TOP, + * BOTTOM, CENTER, or BASELINE. + * @chainable + * @example + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ */ +/** + * @method textAlign + * @return {Object} + */ +p5.prototype.textAlign = function (horizAlign, vertAlign) { + p5._validateParameters("textAlign", arguments); + return this._renderer.textAlign(...arguments); +}; + +/** + * Sets the spacing between lines of text when + * text() is called. + * + * Note: Spacing is measured in pixels. + * + * Calling `textLeading()` without an argument returns the current spacing. + * + * @method textLeading + * @param {Number} leading spacing between lines of text in units of pixels. + * @chainable + * + * @example + *
+ * + * 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.'); + * } + * + *
+ */ +/** + * @method textLeading + * @return {Number} + */ +p5.prototype.textLeading = function (theLeading) { + p5._validateParameters("textLeading", arguments); + return this._renderer.textLeading(...arguments); +}; + +/** + * Sets the font size when + * text() is called. + * + * Note: Font size is measured in pixels. + * + * Calling `textSize()` without an argument returns the current size. + * + * @method textSize + * @param {Number} size size of the letters in units of pixels. + * @chainable + * + * @example + *
+ * + * 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.'); + * } + * + *
+ */ +/** + * @method textSize + * @return {Number} + */ +p5.prototype.textSize = function (theSize) { + p5._validateParameters("textSize", arguments); + return this._renderer.textSize(...arguments); +}; + +/** + * Sets the style for system fonts when + * text() 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 loadFont(). + * + * @method textStyle + * @param {Constant} style styling for text, either NORMAL, + * ITALIC, BOLD or BOLDITALIC. + * @chainable + * @example + *
+ * + * 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.'); + * } + * + *
+ */ +/** + * @method textStyle + * @return {String} + */ +p5.prototype.textStyle = function (theStyle) { + p5._validateParameters("textStyle", arguments); + return this._renderer.textStyle(...arguments); +}; + +/** + * Calculates the maximum width of a string of text drawn when + * text() is called. + * + * textWidth() returns the width of the smallest bounding box containing the text (a "tight" measurement), which may ignore leading and trailing whitespace. This is different from fontWidth(), which returns the advance width (a "loose" measurement) including all whitespace. + * + * fontWidth() returns the advance width (loose), the full width the text would occupy, as measured by the browser's measureText().width property. + * + * Comparison: + * + * + * See also: fontWidth() for advance width measurement. + * + * @method textWidth + * @param {String} str string of text to measure. + * @return {Number} width measured in units of pixels. + * @example + *
+ * + * function setup() { + * createCanvas(200, 100); + * textSize(32); + * let s1 = 'x y'; + * let s2 = ' x y '; + * let w1 = textWidth(s1); + * let w2 = textWidth(s2); + * text(s1, 10, 40); + * text(s2, 10, 80); + * line(10, 45, 10 + w1, 45); // underline tight bounding box + * line(10, 85, 10 + w2, 85); // underline tight bounding box (ignores leading/trailing spaces) + * describe('Shows difference in measured width for text with and without leading/trailing spaces.'); + * } + * + *
+ * + *
+ * + * function setup() { + * createCanvas(200, 100); + * textSize(32); + * let s = ' '; + * let w = textWidth(s); + * text('[' + s + ']', 10, 60); // visualize the space + * line(10, 65, 10 + w, 65); // may be 0 for bounding box + * describe('Shows that textWidth for a single space may be 0, even though the space is visible.'); + * } + * + *
+ */ +p5.prototype.textWidth = function (...args) { + args[0] += ""; + p5._validateParameters("textWidth", args); + if (args[0].length === 0) { + return 0; + } + + // Only use the line with the longest width, and replace tabs with double-space + const textLines = args[0].replace(/\t/g, " ").split(/\r?\n|\r|\n/g); + + const newArr = []; + + // Return the textWidth for every line + for (let i = 0; i < textLines.length; i++) { + newArr.push(this._renderer.textWidth(textLines[i])); + } + + // Return the largest textWidth + const largestWidth = Math.max(...newArr); + + return largestWidth; +}; + +/** + * Calculates the ascent of the current font at its current size. + * + * The ascent represents the distance, in pixels, of the tallest character + * above the baseline. + * + * @method textAscent + * @return {Number} ascent measured in units of pixels. + * @example + *
+ * + * let font; + * + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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".'); + * } + * + *
+ */ +p5.prototype.textAscent = function (...args) { + p5._validateParameters("textAscent", args); + return this._renderer.textAscent(); +}; + +/** + * Calculates the descent of the current font at its current size. + * + * The descent represents the distance, in pixels, of the character with the + * longest descender below the baseline. + * + * @method textDescent + * @return {Number} descent measured in units of pixels. + * @example + *
+ * + * let font; + * + * function preload() { + * font = loadFont('assets/inconsolata.otf'); + * } + * + * function setup() { + * 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".'); + * } + * + *
+ */ +p5.prototype.textDescent = function (...args) { + p5._validateParameters("textDescent", args); + return this._renderer.textDescent(); +}; + +/** + * Helper function to measure ascent and descent. + */ +p5.prototype._updateTextMetrics = function () { + return this._renderer._updateTextMetrics(); +}; + +/** + * Sets the style for wrapping text when + * text() 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 + * @param {Constant} style text wrapping style, either WORD or CHAR. + * @return {String} style + * @example + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ * + *
+ * + * 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.'); + * } + * + *
+ */ +p5.prototype.textWrap = function (wrapStyle) { + p5._validateParameters("textWrap", [wrapStyle]); + + return this._renderer.textWrap(wrapStyle); +}; + +export default p5;