diff --git a/src/core/constants.js b/src/core/constants.js index ece1333037..4a0afa15e6 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/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 21ce3bd64b..5354e64340 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -375,6 +375,66 @@ class Renderer extends p5.Element { ); y += p.textLeading(); } + } else if ( + textWrapStyle === constants.PRETTY || + textWrapStyle === constants.BALANCE + ) { + const nlines = []; + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const wordsArr = lines[lineIndex] + .split(' ') + .filter(s => s.length > 0); + const spaceW = this.textWidth(' '); + const N = wordsArr.length; + const widths = wordsArr.map(s => this.textWidth(s)); + const dp = new Array(N + 1).fill(Infinity); + const next = new Array(N + 1).fill(-1); + dp[N] = 0; + for (let i = N - 1; i >= 0; i--) { + let sum = 0; + for (let j = i; j < N; j++) { + sum += widths[j]; + const gaps = j - i; + const total = sum + gaps * spaceW; + if (total > maxWidth) break; + const slack = maxWidth - total; + const cost = (j === N - 1 ? 0 : slack * slack) + dp[j + 1]; + if (cost < dp[i]) { + dp[i] = cost; + next[i] = j + 1; + } + } + } + let i = 0; + while (i < N) { + const j = next[i] > 0 ? next[i] : i + 1; + nlines.push(wordsArr.slice(i, j).join(' ')); + i = j; + } + } + + let offset = 0; + if (this._textBaseline === constants.CENTER) { + offset = (nlines.length - 1) * p.textLeading() / 2; + } else if (this._textBaseline === constants.BOTTOM) { + offset = (nlines.length - 1) * p.textLeading(); + } + + for (let i = 0; i < nlines.length; i++) { + this._justifyActive = this._textAlign === constants.JUSTIFIED; + this._justifyWidth = maxWidth; + this._justifyIsLastLine = i === nlines.length - 1; + this._renderText( + p, + nlines[i], + x, + y - offset, + finalMaxHeight, + finalMinHeight + ); + this._justifyActive = false; + y += p.textLeading(); + } } else { let nlines = []; for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { 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..bff75f86de --- /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); +}