From 05aa09ae48f8fe43f7198a1fdb7e4cd82a1fcf58 Mon Sep 17 00:00:00 2001 From: Vansh0204 Date: Wed, 26 Nov 2025 02:23:48 +0530 Subject: [PATCH 1/4] feat(typography): add JUSTIFIED alignment for bounded text in 2D renderer - Justifies non-final lines in bounded text() - Keeps final line ragged - Maps Canvas2D textAlign to LEFT when JUSTIFIED is set - WEBGL remains left-aligned for now Refs #7712 --- src/core/constants.js | 1 + src/core/p5.Renderer.js | 12 ++++++++++++ src/core/p5.Renderer2D.js | 41 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/core/constants.js b/src/core/constants.js index ece1333037..afd951f141 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -1058,6 +1058,7 @@ export const WORD = 'WORD'; export const _DEFAULT_TEXT_FILL = '#000000'; export const _DEFAULT_LEADMULT = 1.25; export const _CTX_MIDDLE = 'middle'; +export const JUSTIFIED = 'justified'; // VERTICES /** diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 21ce3bd64b..6c8f58749e 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -49,6 +49,9 @@ class Renderer extends p5.Element { this._textAlign = constants.LEFT; this._textBaseline = constants.BASELINE; this._textWrap = constants.WORD; + this._justifyActive = false; + this._justifyWidth = 0; + this._justifyIsLastLine = false; this._rectMode = constants.CORNER; this._ellipseMode = constants.CENTER; @@ -351,6 +354,9 @@ class Renderer extends p5.Element { testLine = `${line + words[wordIndex]}` + ' '; testWidth = this.textWidth(testLine); if (testWidth > maxWidth && line.length > 0) { + this._justifyActive = this._textAlign === constants.JUSTIFIED; + this._justifyWidth = maxWidth; + this._justifyIsLastLine = false; this._renderText( p, line.trim(), @@ -359,12 +365,16 @@ class Renderer extends p5.Element { finalMaxHeight, finalMinHeight ); + this._justifyActive = false; line = `${words[wordIndex]}` + ' '; y += p.textLeading(); } else { line = testLine; } } + this._justifyActive = this._textAlign === constants.JUSTIFIED; + this._justifyWidth = maxWidth; + this._justifyIsLastLine = true; this._renderText( p, line.trim(), @@ -373,6 +383,7 @@ class Renderer extends p5.Element { finalMaxHeight, finalMinHeight ); + this._justifyActive = false; y += p.textLeading(); } } else { @@ -446,6 +457,7 @@ class Renderer extends p5.Element { // Renders lines of text at any line breaks present in the original string for (let i = 0; i < lines.length; i++) { + this._justifyActive = false; this._renderText( p, lines[i], diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 85d3ac7009..b1804c6f17 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1217,7 +1217,41 @@ class Renderer2D extends p5.Renderer { p.push(); // fix to #803 if (!this._isOpenType()) { - // a system/browser font + if ( + this._textAlign === constants.JUSTIFIED && + this._justifyActive && + !this._justifyIsLastLine && + this._justifyWidth > 0 + ) { + const words = line.split(/\s+/).filter(s => s.length > 0); + if (words.length > 1) { + const widths = words.map(s => this.textWidth(s)); + const sum = widths.reduce((a, b) => a + b, 0); + const extra = this._justifyWidth - sum; + if (extra > 0) { + const gap = extra / (words.length - 1); + let cx = x; + if (this._doStroke && this._strokeSet) { + for (let i = 0; i < words.length; i++) { + this.drawingContext.strokeText(words[i], cx, y); + cx += widths[i] + (i < words.length - 1 ? gap : 0); + } + } + cx = x; + if (!this._clipping && this._doFill) { + if (!this._fillSet) { + this._setFill(constants._DEFAULT_TEXT_FILL); + } + for (let i = 0; i < words.length; i++) { + this.drawingContext.fillText(words[i], cx, y); + cx += widths[i] + (i < words.length - 1 ? gap : 0); + } + } + p.pop(); + return p; + } + } + } // no stroke unless specified by user if (this._doStroke && this._strokeSet) { @@ -1272,7 +1306,10 @@ class Renderer2D extends p5.Renderer { this.drawingContext.font = `${this._textStyle || 'normal'} ${this._textSize || 12}px ${fontNameString}`; - this.drawingContext.textAlign = this._textAlign; + const _ta = this._textAlign === constants.JUSTIFIED + ? constants.LEFT + : this._textAlign; + this.drawingContext.textAlign = _ta; if (this._textBaseline === constants.CENTER) { this.drawingContext.textBaseline = constants._CTX_MIDDLE; } else { From 881635f44d827a46effd4ebb17073d587c82588a Mon Sep 17 00:00:00 2001 From: Vansh0204 Date: Sat, 29 Nov 2025 11:07:35 +0530 Subject: [PATCH 2/4] test(visual): add JUSTIFIED alignment and wrap cases to typography visual suite --- test/unit/visual/cases/typography.js | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/unit/visual/cases/typography.js b/test/unit/visual/cases/typography.js index 664c7b71ca..d56fb275b3 100644 --- a/test/unit/visual/cases/typography.js +++ b/test/unit/visual/cases/typography.js @@ -17,4 +17,46 @@ visualSuite('Typography', function() { screenshot(); }); }); + + visualSuite('JUSTIFIED alignment', function() { + visualTest('WORD wrap justified non-final lines', function (p5, screenshot) { + p5.createCanvas(300, 160); + p5.textSize(16); + p5.textAlign(p5.JUSTIFIED, p5.TOP); + p5.textWrap(p5.WORD); + const s = 'This is a line of text that should justify on non-final lines.'; + p5.text(s, 10, 10, 280, 140); + screenshot(); + }); + }); + + visualSuite('PRETTY/BALANCE wrap', function() { + visualTest('PRETTY wrap LEFT', function (p5, screenshot) { + p5.createCanvas(300, 160); + p5.textSize(16); + p5.textAlign(p5.LEFT, p5.TOP); + p5.textWrap(p5.PRETTY); + const s = 'Balanced wrapping aims to reduce raggedness of lines.'; + p5.text(s, 10, 10, 280, 140); + screenshot(); + }); + visualTest('BALANCE wrap RIGHT', function (p5, screenshot) { + p5.createCanvas(300, 160); + p5.textSize(16); + p5.textAlign(p5.RIGHT, p5.TOP); + p5.textWrap(p5.BALANCE); + const s = 'Balanced wrapping aims to reduce raggedness of lines.'; + p5.text(s, 290, 10, 280, 140); + screenshot(); + }); + visualTest('PRETTY wrap with JUSTIFIED', function (p5, screenshot) { + p5.createCanvas(300, 160); + p5.textSize(16); + p5.textAlign(p5.JUSTIFIED, p5.TOP); + p5.textWrap(p5.PRETTY); + const s = 'Pretty wrapping combined with justification for non-final lines.'; + p5.text(s, 10, 10, 280, 140); + screenshot(); + }); + }); }); From a3b87cb9e0d46698bd4c59b0b0a960c7e96cefd2 Mon Sep 17 00:00:00 2001 From: Vansh0204 Date: Mon, 1 Dec 2025 00:40:33 +0530 Subject: [PATCH 3/4] Add PRETTY/BALANCE constants and manual tests for textWrap/textAlign --- src/core/constants.js | 10 +++ test/manual-test-examples/type/index.html | 20 ++++++ test/manual-test-examples/type/sketch.js | 88 +++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 test/manual-test-examples/type/index.html create mode 100644 test/manual-test-examples/type/sketch.js diff --git a/src/core/constants.js b/src/core/constants.js index afd951f141..554d14eff3 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -1053,6 +1053,16 @@ export const CHAR = 'CHAR'; * @final */ export const WORD = 'WORD'; +/** + * @property {String} PRETTY + * @final + */ +export const PRETTY = 'pretty'; +/** + * @property {String} BALANCE + * @final + */ +export const BALANCE = 'balance'; // TYPOGRAPHY-INTERNAL export const _DEFAULT_TEXT_FILL = '#000000'; diff --git a/test/manual-test-examples/type/index.html b/test/manual-test-examples/type/index.html new file mode 100644 index 0000000000..cafa515103 --- /dev/null +++ b/test/manual-test-examples/type/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/manual-test-examples/type/sketch.js b/test/manual-test-examples/type/sketch.js new file mode 100644 index 0000000000..92ad0c5340 --- /dev/null +++ b/test/manual-test-examples/type/sketch.js @@ -0,0 +1,88 @@ +function setup() { + console.log('Setup called'); + createCanvas(1000, 1200); + background(255); + textSize(14); + textFont('sans-serif'); + + let alignments = [LEFT, CENTER, RIGHT]; + let vertAlignments = [BASELINE, BOTTOM, CENTER, TOP]; + // Using string literals as constants might not be exposed in this build environment yet + let wrapModes = ['pretty', 'balance']; + console.log('window.PRETTY:', window.PRETTY); + let sampleText = 'text gonna wrap when it gets too long and is then breaking.'; + + let xStart = 50; + let yStart = 50; + let boxWidth = 200; + let boxHeight = 80; + let xGap = 220; + let yGap = 100; + + // Test PRETTY and BALANCE with different alignments + for (let w = 0; w < wrapModes.length; w++) { + let mode = wrapModes[w]; + let modeName = (mode === PRETTY) ? 'PRETTY' : 'BALANCE'; + + fill(0); + noStroke(); + text(`textWrap(${modeName})`, xStart, yStart - 20); + + for (let i = 0; i < vertAlignments.length; i++) { + for (let j = 0; j < alignments.length; j++) { + let x = xStart + j * xGap; + let y = yStart + i * yGap; + + let horiz = alignments[j]; + let vert = vertAlignments[i]; + + let horizName = (horiz === LEFT) ? 'LEFT' : (horiz === CENTER) ? 'CENTER' : 'RIGHT'; + let vertName = (vert === BASELINE) ? 'BASELINE' : (vert === BOTTOM) ? 'BOTTOM' : (vert === CENTER) ? 'CENTER' : 'TOP'; + + stroke(255, 0, 0); + noFill(); + rect(x, y, boxWidth, boxHeight); + + noStroke(); + fill(0); + textAlign(horiz, vert); + textWrap(mode); + text(`${horizName} ${vertName} ${sampleText}`, x, y, boxWidth, boxHeight); + } + } + yStart += 500; + } + + // Test JUSTIFIED + fill(0); + noStroke(); + text('textAlign(JUSTIFIED)', xStart, yStart - 20); + + let justifiedText = 'This is a longer text that should be justified when it wraps to multiple lines. It should look clean and aligned on both sides.'; + + stroke(255, 0, 0); + noFill(); + rect(xStart, yStart, boxWidth, boxHeight * 2); + + noStroke(); + fill(0); + textAlign(JUSTIFIED, TOP); + textWrap(WORD); + text(justifiedText, xStart, yStart, boxWidth, boxHeight * 2); + + stroke(255, 0, 0); + noFill(); + rect(xStart + xGap, yStart, boxWidth, boxHeight * 2); + + noStroke(); + fill(0); + textAlign(JUSTIFIED, TOP); + textWrap(PRETTY); + text(justifiedText, xStart + xGap, yStart, boxWidth, boxHeight * 2); + + fill(0); + noStroke(); + textAlign(LEFT, TOP); + text('WORD', xStart, yStart + boxHeight * 2 + 10); + text('PRETTY', xStart + xGap, yStart + boxHeight * 2 + 10); +} From 1b043d9361a54dc325951b90a7619b6aeea86abe Mon Sep 17 00:00:00 2001 From: Vansh0204 Date: Mon, 1 Dec 2025 19:40:18 +0530 Subject: [PATCH 4/4] Fix: Use uppercase values for PRETTY/BALANCE constants per naming convention --- src/core/constants.js | 4 ++-- test/manual-test-examples/type/sketch.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/constants.js b/src/core/constants.js index 554d14eff3..04ea4cb0df 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -1057,12 +1057,12 @@ export const WORD = 'WORD'; * @property {String} PRETTY * @final */ -export const PRETTY = 'pretty'; +export const PRETTY = 'PRETTY'; /** * @property {String} BALANCE * @final */ -export const BALANCE = 'balance'; +export const BALANCE = 'BALANCE'; // TYPOGRAPHY-INTERNAL export const _DEFAULT_TEXT_FILL = '#000000'; diff --git a/test/manual-test-examples/type/sketch.js b/test/manual-test-examples/type/sketch.js index 92ad0c5340..bff75f86de 100644 --- a/test/manual-test-examples/type/sketch.js +++ b/test/manual-test-examples/type/sketch.js @@ -8,7 +8,7 @@ function setup() { let alignments = [LEFT, CENTER, RIGHT]; let vertAlignments = [BASELINE, BOTTOM, CENTER, TOP]; // Using string literals as constants might not be exposed in this build environment yet - let wrapModes = ['pretty', 'balance']; + let wrapModes = ['PRETTY', 'BALANCE']; console.log('window.PRETTY:', window.PRETTY); let sampleText = 'text gonna wrap when it gets too long and is then breaking.';