From 876f90079eebb57ff1a235fa7084572d4b794973 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 7 Dec 2024 17:06:03 -0500 Subject: [PATCH 1/5] Start refactoring to work in WebGL mode too --- preview/index.html | 16 +-- src/app.js | 8 +- src/core/p5.Renderer.js | 2 +- src/type/text2d.js | 281 ++++++++++++++++++++++------------------ src/webgl/text.js | 39 +++--- 5 files changed, 183 insertions(+), 163 deletions(-) diff --git a/preview/index.html b/preview/index.html index ac5bedefcc..7b9da225f8 100644 --- a/preview/index.html +++ b/preview/index.html @@ -20,25 +20,21 @@ import p5 from '../src/app.js'; const sketch = function (p) { - let g, f; + let f; - p.setup = function () { - p.createCanvas(200, 200); - g = p.createGraphics(200, 200); - f = p.createGraphics(200, 200, p.WEBGL); + p.setup = async function () { + // TODO: make this work without a name + f = await p.loadFont('font/BricolageGrotesque-Variable.ttf', 'Bricolage') + p.createCanvas(200, 200, p.WEBGL); }; p.draw = function () { p.background(0, 50, 50); - p.circle(100, 100, 50); p.fill('white'); p.textSize(30); + p.textFont(f) p.text('hello', 10, 30); - - // f.fill('red'); - f.sphere(); - p.image(f, 0, 0); }; }; diff --git a/src/app.js b/src/app.js index dc2f302094..9de84f07f2 100644 --- a/src/app.js +++ b/src/app.js @@ -49,10 +49,6 @@ io(p5); import math from './math'; math(p5); -// typography -import type from './type' -type(p5); - // utilities import utilities from './utilities'; utilities(p5); @@ -61,6 +57,10 @@ utilities(p5); import webgl from './webgl'; webgl(p5); +// typography +import type from './type' +type(p5); + import './core/init'; export default p5; diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index f7e59b3436..739fca66d5 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -486,7 +486,7 @@ class Renderer { * Helper function to check font type (system or otf) */ _isOpenType(f = this.states.textFont) { - return typeof f === 'object' && f.font && f.font.supported; + return typeof f === 'object' && f.data; } _updateTextMetrics() { diff --git a/src/type/text2d.js b/src/type/text2d.js index 44f4582807..3262c70f80 100644 --- a/src/type/text2d.js +++ b/src/type/text2d.js @@ -1,3 +1,4 @@ +import { Renderer } from '../core/p5.Renderer'; /* * TODO: @@ -88,11 +89,12 @@ function text2d(p5, fn) { // attach each text func to p5, delegating to the renderer textFunctions.forEach(func => { fn[func] = function (...args) { - if (!(func in p5.Renderer2D.prototype)) { + if (!(func in Renderer.prototype)) { throw Error(`Renderer2D.prototype.${func} is not defined.`); } return this._renderer[func](...args); }; + // TODO: is this necessary? p5.Graphics.prototype[func] = function (...args) { return this._renderer[func](...args); }; @@ -129,9 +131,9 @@ function text2d(p5, fn) { ////////////////////////////// start API /////////////////////////////// - p5.Renderer2D.prototype.text = function (str, x, y, width, height) { + Renderer.prototype.text = function (str, x, y, width, height) { - let setBaseline = this.drawingContext.textBaseline; // store baseline + 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)); @@ -145,7 +147,7 @@ function text2d(p5, fn) { // render each line at the adjusted position lines.forEach(line => this._renderText(line.text, line.x, line.y)); - this.drawingContext.textBaseline = setBaseline; // restore baseline + this.textDrawingContext().textBaseline = setBaseline; // restore baseline }; /** @@ -157,7 +159,7 @@ function text2d(p5, fn) { * @param {number} height - the max height of the text block * @returns - a bounding box object for the text block: {x,y,w,h} */ - p5.Renderer2D.prototype.textBounds = function (str, x, y, width, height) { + Renderer.prototype.textBounds = function (str, x, y, width, height) { //console.log('TEXT BOUNDS: ', str, x, y, width, height); // delegate to _textBoundsSingle measure function return this._computeBounds(fn._TEXT_BOUNDS, str, x, y, width, height).bounds; @@ -172,17 +174,17 @@ function text2d(p5, fn) { * @param {number} height - the max height of the text block * @returns - a bounding box object for the text block: {x,y,w,h} */ - p5.Renderer2D.prototype.fontBounds = function (str, x, y, width, height) { + Renderer.prototype.fontBounds = function (str, x, y, width, height) { // delegate to _fontBoundsSingle measure function return this._computeBounds(fn._FONT_BOUNDS, str, x, y, width, height).bounds; }; /** * Get the width of a text string in pixels (tight bounds) - * @param {string} theText + * @param {string} theText * @returns - the width of the text in pixels */ - p5.Renderer2D.prototype.textWidth = function (theText) { + Renderer.prototype.textWidth = function (theText) { let lines = this._processLines(theText, null, null); // return the max width of the lines (using tight bounds) let widths = lines.map(l => this._textWidthSingle(l)); @@ -191,10 +193,10 @@ function text2d(p5, fn) { /** * Get the width of a text string in pixels (loose bounds) - * @param {string} theText + * @param {string} theText * @returns - the width of the text in pixels */ - p5.Renderer2D.prototype.fontWidth = function (theText) { + Renderer.prototype.fontWidth = function (theText) { // return the max width of the lines (using loose bounds) let lines = this._processLines(theText, null, null); let widths = lines.map(l => this._fontWidthSingle(l)); @@ -202,21 +204,21 @@ function text2d(p5, fn) { }; /** - * + * * @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 */ - p5.Renderer2D.prototype.textAscent = function (txt = '') { + Renderer.prototype.textAscent = function (txt = '') { if (!txt.length) return this.fontAscent(); - return this.drawingContext.measureText(txt)[prop]; + return this.textDrawingContext().measureText(txt)[prop]; }; /** * @returns - returns the ascent for the current font */ - p5.Renderer2D.prototype.fontAscent = function () { - return this.drawingContext.measureText('_').fontBoundingBoxAscent; + Renderer.prototype.fontAscent = function () { + return this.textDrawingContext().measureText('_').fontBoundingBoxAscent; }; /** @@ -224,21 +226,21 @@ function text2d(p5, fn) { * be used to compute the descent, otherwise the font's descent will be used * @returns - the descent of the text */ - p5.Renderer2D.prototype.textDescent = function (txt = '') { + Renderer.prototype.textDescent = function (txt = '') { if (!txt.length) return this.fontDescent(); - return this.drawingContext.measureText(txt)[prop]; + return this.textDrawingContext().measureText(txt)[prop]; }; /** * @returns - returns the descent for the current font */ - p5.Renderer2D.prototype.fontDescent = function () { - return this.drawingContext.measureText('_').fontBoundingBoxDescent; + Renderer.prototype.fontDescent = function () { + return this.textDrawingContext().measureText('_').fontBoundingBoxDescent; }; // setters/getters for text properties ////////////////////////// - p5.Renderer2D.prototype.textAlign = function (h, v) { + Renderer.prototype.textAlign = function (h, v) { // the setter if (typeof h !== 'undefined') { @@ -264,7 +266,7 @@ function text2d(p5, fn) { * @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 */ - p5.Renderer2D.prototype.textFont = function (font, size, options) { + Renderer.prototype.textFont = function (font, size, options) { if (arguments.length === 0) { return this.states.textFont; @@ -299,7 +301,7 @@ function text2d(p5, fn) { } // update font properties in this.states - this.states.textFont = family; + this.states.textFont = this.textFontState(font, family, size); // convert/update the size in this.states if (typeof size !== 'undefined') { @@ -312,11 +314,11 @@ function text2d(p5, fn) { } this._applyTextProperties(); - //console.log('ctx.font="' + this.drawingContext.font + '"'); + //console.log('ctx.font="' + this.textDrawingContext().font + '"'); return this._pInst; } - p5.Renderer2D.prototype._directSetFontString = function (font, debug = 0) { + Renderer.prototype._directSetFontString = function (font, debug = 0) { if (debug) console.log('_directSetFontString"' + font + '"'); let defaults = ShorthandFontProps.reduce((props, p) => { props[p] = RendererTextProps[p].default; @@ -334,7 +336,7 @@ function text2d(p5, fn) { return { family: style.fontFamily, size: style.fontSize }; } - p5.Renderer2D.prototype.textLeading = function (leading) { + Renderer.prototype.textLeading = function (leading) { // the setter if (typeof leading === 'number') { this.states.leadingSet = true; @@ -345,7 +347,7 @@ function text2d(p5, fn) { return this.states.textLeading; } - p5.Renderer2D.prototype.textWeight = function (weight) { + Renderer.prototype.textWeight = function (weight) { // the setter if (typeof weight === 'number') { this.states.fontWeight = weight; @@ -358,18 +360,18 @@ function text2d(p5, fn) { /** * @param {*} size - the size of the text, can be a number or a css-style string */ - p5.Renderer2D.prototype.textSize = function (size) { + Renderer.prototype.textSize = function (size) { // the setter if (typeof size !== 'undefined') { this._setTextSize(size); return this._applyTextProperties(); } - // the getter + // the getter return this.states.textSize; } - p5.Renderer2D.prototype.textStyle = function (style) { + Renderer.prototype.textStyle = function (style) { // the setter if (typeof style !== 'undefined') { @@ -380,7 +382,7 @@ function text2d(p5, fn) { return this.states.fontStyle; } - p5.Renderer2D.prototype.textWrap = function (wrapStyle) { + Renderer.prototype.textWrap = function (wrapStyle) { if (wrapStyle === fn.WORD || wrapStyle === fn.CHAR) { this.states.textWrap = wrapStyle; @@ -390,7 +392,7 @@ function text2d(p5, fn) { return this.states.textWrap; }; - p5.Renderer2D.prototype.textDirection = function (direction) { + Renderer.prototype.textDirection = function (direction) { if (typeof direction !== 'undefined') { this.states.direction = direction; @@ -401,19 +403,19 @@ function text2d(p5, fn) { /** * Sets/gets a single text property for the renderer (eg. fontStyle, fontStretch, etc.) - * The property to be set can be a mapped or unmapped property on `this.states` or a property - * on `this.drawingContext` or on `this.canvas.style` - * The property to get can exist in `this.states` or `this.drawingContext` or `this.canvas.style` + * The property to be set can be a mapped or unmapped property on `this.states` or a property + * on `this.textDrawingContext()` or on `this.canvas.style` + * The property to get can exist in `this.states` or `this.textDrawingContext()` or `this.canvas.style` */ - p5.Renderer2D.prototype.textProperty = function (prop, value, opts) { + Renderer.prototype.textProperty = function (prop, value, opts) { let modified = false, debug = opts?.debug || false; - // getter: return option from this.states or this.drawingContext + // getter: return option from this.states or this.textDrawingContext() if (typeof value === 'undefined') { let props = this.textProperties(); if (prop in props) return props[prop]; - throw Error('Unknown text option "' + prop + '"'); // FES? + throw Error('Unknown text option "' + prop + '"'); // FES? } // set the option in this.states if it exists @@ -425,7 +427,7 @@ function text2d(p5, fn) { } } // does it exist in CanvasRenderingContext2D ? - else if (prop in this.drawingContext) { + else if (prop in this.textDrawingContext()) { this._setContextProperty(prop, value, debug); modified = true; } @@ -445,7 +447,7 @@ function text2d(p5, fn) { * Batch set/get text properties for the renderer. * The properties can be either on `states` or `drawingContext` */ - p5.Renderer2D.prototype.textProperties = function (properties) { + Renderer.prototype.textProperties = function (properties) { // setter if (typeof properties !== 'undefined') { @@ -455,9 +457,9 @@ function text2d(p5, fn) { return this._pInst; } - // getter: get props from this.drawingContext + // getter: get props from this.textDrawingContext() properties = ContextTextProps.reduce((props, p) => { - props[p] = this.drawingContext[p]; + props[p] = this.textDrawingContext()[p]; return props; }, {}); @@ -465,25 +467,25 @@ function text2d(p5, fn) { Object.keys(RendererTextProps).forEach(p => { properties[p] = this.states[p]; if (RendererTextProps[p]?.type === 'Context2d') { - properties[p] = this.drawingContext[p]; + properties[p] = this.textDrawingContext()[p]; } }); return properties; }; - p5.Renderer2D.prototype.textMode = function () { /* no-op for processing api */ }; + Renderer.prototype.textMode = function () { /* no-op for processing api */ }; /////////////////////////////// end API //////////////////////////////// /* - Compute the bounds for a block of text based on the specified + Compute the bounds for a block of text based on the specified measure function, either _textBoundsSingle or _fontBoundsSingle */ - p5.Renderer2D.prototype._computeBounds = function (type, str, x, y, width, height, opts) { + Renderer.prototype._computeBounds = function (type, str, x, y, width, height, opts) { - let setBaseline = this.drawingContext.textBaseline; + let setBaseline = this.textDrawingContext().textBaseline; let { textLeading, textAlign } = this.states; // adjust width, height based on current rectMode @@ -520,13 +522,13 @@ function text2d(p5, fn) { } if (0 && opts?.ignoreRectMode) boxes.forEach((b, i) => { // draw bounds for debugging - let ss = this.drawingContext.strokeStyle; - this.drawingContext.strokeStyle = 'green'; - this.drawingContext.strokeRect(bounds.x, bounds.y, bounds.w, bounds.h); - this.drawingContext.strokeStyle = ss; + let ss = this.textDrawingContext().strokeStyle; + this.textDrawingContext().strokeStyle = 'green'; + this.textDrawingContext().strokeRect(bounds.x, bounds.y, bounds.w, bounds.h); + this.textDrawingContext().strokeStyle = ss; }); - this.drawingContext.textBaseline = setBaseline; // restore baseline + this.textDrawingContext().textBaseline = setBaseline; // restore baseline return { bounds, lines }; }; @@ -534,7 +536,7 @@ function text2d(p5, fn) { /* Adjust width, height of bounds based on current rectMode */ - p5.Renderer2D.prototype._rectModeAdjust = function (x, y, width, height) { + Renderer.prototype._rectModeAdjust = function (x, y, width, height) { if (typeof width !== 'undefined') { switch (this.states.rectMode) { @@ -556,7 +558,7 @@ function text2d(p5, fn) { /* Attempts to set a property directly on the canvas.style object */ - p5.Renderer2D.prototype._setCanvasStyleProperty = function (opt, val, debug) { + Renderer.prototype._setCanvasStyleProperty = function (opt, val, debug) { let value = val.toString(); // ensure its a string @@ -585,7 +587,7 @@ function text2d(p5, fn) { Parses the fontVariationSettings string and sets the font properties, only font-weight working consistently across browsers at present */ - p5.Renderer2D.prototype._handleFontVariationSettings = function (value, debug = false) { + Renderer.prototype._handleFontVariationSettings = function (value, debug = false) { // check if the value is a string or an object if (typeof value === 'object') { value = Object.keys(value).map(k => k + ' ' + value[k]).join(', '); @@ -648,14 +650,14 @@ function text2d(p5, fn) { we check if it has a mapping to a property in this.states Otherwise, add the property to the context-queue for later application */ - p5.Renderer2D.prototype._setContextProperty = function (prop, val, debug = false) { + Renderer.prototype._setContextProperty = function (prop, val, debug = false) { // check if the value is actually different, else short-circuit - if (this.drawingContext[prop] === val) { + if (this.textDrawingContext()[prop] === val) { return this._pInst; } - // otherwise, we will set the property directly on the `this.drawingContext` + // otherwise, we will set the property directly on the `this.textDrawingContext()` // by adding [property, value] to context-queue for later application (contextQueue ??= []).push([prop, val]); @@ -665,7 +667,7 @@ function text2d(p5, fn) { /* Adjust parameters (x,y,w,h) based on current rectMode */ - p5.Renderer2D.prototype._handleRectMode = function (x, y, width, height) { + Renderer.prototype._handleRectMode = function (x, y, width, height) { let rectMode = this.states.rectMode; @@ -701,7 +703,7 @@ function text2d(p5, fn) { @param {string} size - the font-size string to compute @returns {number} - the computed font-size in pixels */ - p5.Renderer2D.prototype._fontSizePx = function (theSize, family = this.states.textFont) { + Renderer.prototype._fontSizePx = function (theSize, family = this.states.textFont) { const isNumString = (num) => !isNaN(num) && num.trim() !== ''; @@ -720,7 +722,7 @@ function text2d(p5, fn) { return fontSize; }; - p5.Renderer2D.prototype._cachedDiv = function (props) { + Renderer.prototype._cachedDiv = function (props) { if (typeof cachedDiv === 'undefined') { let ele = document.createElement('div'); ele.ariaHidden = 'true'; @@ -740,7 +742,7 @@ function text2d(p5, fn) { @param {array} bboxes - the bounding boxes to aggregate @returns {object} - the aggregated bounding box */ - p5.Renderer2D.prototype._aggregateBounds = function (bboxes) { + Renderer.prototype._aggregateBounds = function (bboxes) { // loop over the bounding boxes to get the min/max x/y values let minX = Math.min(...bboxes.map(b => b.x)); let minY = Math.min(...bboxes.map(b => b.y)); @@ -749,7 +751,7 @@ function text2d(p5, fn) { return { x: minX, y: minY, w: maxX - minX, h: maxY - minY }; }; - // p5.Renderer2D.prototype._aggregateBounds = function (tx, ty, bboxes) { + // Renderer.prototype._aggregateBounds = function (tx, ty, bboxes) { // let x = Math.min(...bboxes.map(b => b.x)); // let y = Math.min(...bboxes.map(b => b.y)); // // the width is the max of the x-offset + the box width @@ -763,7 +765,7 @@ function text2d(p5, fn) { /* Position the lines of text based on their textAlign/textBaseline properties */ - p5.Renderer2D.prototype._positionLines = function (x, y, width, height, lines) { + Renderer.prototype._positionLines = function (x, y, width, height, lines) { let { textLeading, textAlign } = this.states; let adjustedX, lineData = new Array(lines.length); @@ -798,11 +800,11 @@ function text2d(p5, fn) { @param {number} width - the width to wrap the text to @returns {array} - the processed lines of text */ - p5.Renderer2D.prototype._processLines = function (str, width, height) { + Renderer.prototype._processLines = function (str, width, height) { if (typeof width !== 'undefined') { // only for text with bounds - if (this.drawingContext.textBaseline === fn.BASELINE) { - this.drawingContext.textBaseline = fn.TOP; + if (this.textDrawingContext().textBaseline === fn.BASELINE) { + this.textDrawingContext().textBaseline = fn.TOP; } } @@ -838,10 +840,10 @@ function text2d(p5, fn) { return lines; }; - /* + /* Get the x-offset for text given the width and textAlign property */ - p5.Renderer2D.prototype._xAlignOffset = function (textAlign, width) { + Renderer.prototype._xAlignOffset = function (textAlign, width) { switch (textAlign) { case fn.LEFT: return 0; @@ -858,10 +860,10 @@ function text2d(p5, fn) { } } - /* + /* Get the y-offset for text given the height, leading, line-count and textBaseline property */ - p5.Renderer2D.prototype._yAlignOffset = function (dataArr, height) { + Renderer.prototype._yAlignOffset = function (dataArr, height) { if (typeof height === 'undefined') { throw Error('_yAlignOffset: height is required'); @@ -895,7 +897,7 @@ function text2d(p5, fn) { /* Align the bounding box based on the current rectMode setting */ - p5.Renderer2D.prototype._rectModeAlign = function (bb, width, height) { + Renderer.prototype._rectModeAlign = function (bb, width, height) { if (typeof width !== 'undefined') { switch (this.states.rectMode) { @@ -918,7 +920,7 @@ function text2d(p5, fn) { } } - p5.Renderer2D.prototype._rectModeAlignRevert = function (bb, width, height) { + Renderer.prototype._rectModeAlignRevert = function (bb, width, height) { if (typeof width !== 'undefined') { switch (this.states.rectMode) { @@ -944,8 +946,8 @@ function text2d(p5, fn) { /* Get the (tight) width of a single line of text */ - p5.Renderer2D.prototype._textWidthSingle = function (s) { - let metrics = this.drawingContext.measureText(s); + Renderer.prototype._textWidthSingle = function (s) { + let metrics = this.textDrawingContext().measureText(s); let abl = metrics.actualBoundingBoxLeft; let abr = metrics.actualBoundingBoxRight; return abr + abl; @@ -954,16 +956,16 @@ function text2d(p5, fn) { /* Get the (loose) width of a single line of text as specified by the font */ - p5.Renderer2D.prototype._fontWidthSingle = function (s) { - return this.drawingContext.measureText(s).width; + Renderer.prototype._fontWidthSingle = function (s) { + return this.textDrawingContext().measureText(s).width; }; /* Get the (tight) bounds of a single line of text based on its actual bounding box */ - p5.Renderer2D.prototype._textBoundsSingle = function (s, x = 0, y = 0) { + Renderer.prototype._textBoundsSingle = function (s, x = 0, y = 0) { - let metrics = this.drawingContext.measureText(s); + let metrics = this.textDrawingContext().measureText(s); let asc = metrics.actualBoundingBoxAscent; let desc = metrics.actualBoundingBoxDescent; let abl = metrics.actualBoundingBoxLeft; @@ -974,9 +976,9 @@ function text2d(p5, fn) { /* Get the (loose) bounds of a single line of text based on its font's bounding box */ - p5.Renderer2D.prototype._fontBoundsSingle = function (s, x = 0, y = 0) { + Renderer.prototype._fontBoundsSingle = function (s, x = 0, y = 0) { - let metrics = this.drawingContext.measureText(s); + let metrics = this.textDrawingContext().measureText(s); let asc = metrics.fontBoundingBoxAscent; let desc = metrics.fontBoundingBoxDescent; x -= this._xAlignOffset(this.states.textAlign, metrics.width); @@ -988,7 +990,7 @@ function text2d(p5, fn) { @param {number | string} theSize - the font-size to set @returns {boolean} - true if the size was changed, false otherwise */ - p5.Renderer2D.prototype._setTextSize = function (theSize) { + Renderer.prototype._setTextSize = function (theSize) { if (typeof theSize === 'string') { // parse the size string via computed style, eg '2em' @@ -1002,7 +1004,7 @@ function text2d(p5, fn) { if (this.states.textSize !== theSize) { this.states.textSize = theSize; - // handle leading here, if not set otherwise + // handle leading here, if not set otherwise if (!this.states.leadingSet) { this.states.textLeading = this.states.textSize * LeadingScale; } @@ -1023,7 +1025,7 @@ function text2d(p5, fn) { @param {object} opts - additional options for splitting the lines @returns {array} - the split lines of text */ - p5.Renderer2D.prototype._lineate = function (textWrap, lines, maxWidth = Infinity, opts = {}) { + Renderer.prototype._lineate = function (textWrap, lines, maxWidth = Infinity, opts = {}) { let splitter = opts.splitChar ?? (textWrap === fn.WORD ? ' ' : ''); let line, testLine, testWidth, words, newLines = []; @@ -1049,7 +1051,7 @@ function text2d(p5, fn) { /* Split the text into lines based on line-breaks and tabs */ - p5.Renderer2D.prototype._splitOnBreaks = function (s) { + Renderer.prototype._splitOnBreaks = function (s) { if (!s || s.length === 0) return ['']; return s.replace(TabsRe, ' ').split(LinebreakRe); }; @@ -1057,7 +1059,7 @@ function text2d(p5, fn) { /* Parse the font-family string to handle complex names, fallbacks, etc. */ - p5.Renderer2D.prototype._parseFontFamily = function (familyStr) { + Renderer.prototype._parseFontFamily = function (familyStr) { let parts = familyStr.split(CommaDelimRe); let family = parts.map(part => { @@ -1071,12 +1073,12 @@ function text2d(p5, fn) { return family; }; - p5.Renderer2D.prototype._applyFontString = function () { + Renderer.prototype._applyFontString = function () { /* Create the font-string according to the CSS font-string specification: If font is specified as a shorthand for several font-related properties, then: - it must include values for: and - - it may optionally include values for: + - it may optionally include values for: [, , , , ] Format: - font-style, font-variant and font-weight must precede font-size @@ -1095,14 +1097,14 @@ function text2d(p5, fn) { //console.log('fontString="' + fontString + '"'); // set the font string on the context - this.drawingContext.font = fontString; + this.textDrawingContext().font = fontString; // verify that it was set successfully - if (this.drawingContext.font !== fontString) { + if (this.textDrawingContext().font !== fontString) { let expected = fontString; - let actual = this.drawingContext.font; + let actual = this.textDrawingContext().font; if (expected !== actual) { - //console.warn(`Unable to set font property on context2d. It may not be supported.`); + //console.warn(`Unable to set font property on context2d. It may not be supported.`); //console.log('Expected "' + expected + '" but got: "' + actual + '"'); // TMP return false; } @@ -1111,78 +1113,99 @@ function text2d(p5, fn) { } /* - Apply the text properties in `this.states` to the `this.drawingContext` + Apply the text properties in `this.states` to the `this.textDrawingContext()` Then apply any properties in the context-queue */ - p5.Renderer2D.prototype._applyTextProperties = function (debug = false) { + Renderer.prototype._applyTextProperties = function (debug = false) { this._applyFontString(); // set these after the font so they're not overridden - this.drawingContext.direction = this.states.direction; - this.drawingContext.textAlign = this.states.textAlign; - this.drawingContext.textBaseline = this.states.textBaseline; + this.textDrawingContext().direction = this.states.direction; + this.textDrawingContext().textAlign = this.states.textAlign; + this.textDrawingContext().textBaseline = this.states.textBaseline; // set manually as (still) not fully supported as part of font-string let stretch = this.states.fontStretch; - if (FontStretchKeys.includes(stretch) && this.drawingContext.fontStretch !== stretch) { - this.drawingContext.fontStretch = stretch; + if (FontStretchKeys.includes(stretch) && this.textDrawingContext().fontStretch !== stretch) { + this.textDrawingContext().fontStretch = stretch; } - // apply each property in queue after the font so they're not overridden + // apply each property in queue after the font so they're not overridden while (contextQueue?.length) { let [prop, val] = contextQueue.shift(); if (debug) console.log('apply context property "' + prop + '" = "' + val + '"'); - this.drawingContext[prop] = val; + this.textDrawingContext()[prop] = val; // check if the value was set successfully - if (this.drawingContext[prop] !== val) { + if (this.textDrawingContext()[prop] !== val) { console.warn(`Unable to set '${prop}' property on context2d. It may not be supported.`); // FES? - console.log('Expected "' + val + '" but got: "' + this.drawingContext[prop] + '"'); + console.log('Expected "' + val + '" but got: "' + this.textDrawingContext()[prop] + '"'); } } return this._pInst; }; - /* - Render a single line of text at the given position - called by text() to render each line - */ - p5.Renderer2D.prototype._renderText = function (text, x, y, maxY, minY) { - let states = this.states; - - if (y < minY || y >= maxY) { - return; // don't render lines beyond minY/maxY - } - - this._pInst.push(); + if (p5.Renderer2D) { + p5.Renderer2D.prototype.textDrawingContext = function() { + return this.drawingContext; + }; + p5.Renderer2D.prototype.textFontState = function(font, family, size) { + return family; + }; + p5.Renderer2D.prototype._renderText = function (text, x, y, maxY, minY) { + let states = this.states; - // no stroke unless specified by user - if (states.doStroke && states.strokeSet) { - this.drawingContext.strokeText(text, x, y); - } + if (y < minY || y >= maxY) { + return; // don't render lines beyond minY/maxY + } - if (!this._clipping && states.doFill) { + this.push(); - // if fill hasn't been set by user, use default text fill - if (!states.fillSet) { - this._setFill(DefaultFill); + // no stroke unless specified by user + if (states.doStroke && states.strokeSet) { + this.textDrawingContext().strokeText(text, x, y); } - //console.log(`fillText(${x},${y},'${text}') font='${this.drawingContext.font}'`); - this.drawingContext.fillText(text, x, y); - } + if (!this._clipping && states.doFill) { - this._pInst.pop(); + // if fill hasn't been set by user, use default text fill + if (!states.fillSet) { + this._setFill(DefaultFill); + } - return this._pInst; - }; + //console.log(`fillText(${x},${y},'${text}') font='${this.textDrawingContext().font}'`); + this.textDrawingContext().fillText(text, x, y); + } + + this.pop(); + }; + } + if (p5.RendererGL) { + p5.RendererGL.prototype.textDrawingContext = function() { + if (!this._textDrawingContext) { + this._textCanvas = document.createElement('canvas'); + this._textCanvas.width = 1; + this._textCanvas.height = 1; + this._textDrawingContext = this._textCanvas.getContext('2d'); + } + return this._textDrawingContext; + }; + p5.RendererGL.prototype.textFontState = function(font, family, size) { + if (typeof font === 'string' || font instanceof String) { + throw new Error( + 'In WebGL mode, textFont() needs to be given the result of loadFont() instead of a font family name.' + ); + } + return font; + }; + } } export default text2d; if (typeof p5 !== 'undefined') { text2d(p5, p5.prototype); -} \ No newline at end of file +} diff --git a/src/webgl/text.js b/src/webgl/text.js index ffef73167b..743f6422c8 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -14,7 +14,7 @@ function text(p5, fn){ RendererGL.prototype.textWidth = function(s) { if (this._isOpenType()) { - return this._textFont._textWidth(s, this._textSize); + return this.states.textFont._textWidth(s, this.states.textSize); } return 0; // TODO: error @@ -635,14 +635,14 @@ function text(p5, fn){ } } - RendererGL.prototype._renderText = function(p, line, x, y, maxY) { - if (!this._textFont || typeof this._textFont === 'string') { + RendererGL.prototype._renderText = function(line, x, y, maxY, minY) { + if (!this.states.textFont || typeof this.states.textFont === 'string') { console.log( 'WEBGL: you must load and set a font before drawing text. See `loadFont` and `textFont` for more details.' ); return; } - if (y >= maxY || !this.states.doFill) { + if (y > maxY || y < minY || !this.states.doFill) { return; // don't render lines beyond our maxY position } @@ -653,7 +653,7 @@ function text(p5, fn){ return p; } - p.push(); // fix to #803 + this.push(); // fix to #803 // remember this state, so it can be restored later const doStroke = this.states.doStroke; @@ -663,16 +663,17 @@ function text(p5, fn){ this.states.drawMode = constants.TEXTURE; // get the cached FontInfo object - const font = this._textFont.font; - let fontInfo = this._textFont._fontInfo; + const font = this.states.textFont; + let fontInfo = this.states.textFont._fontInfo; if (!fontInfo) { - fontInfo = this._textFont._fontInfo = new FontInfo(font); + fontInfo = this.states.textFont._fontInfo = new FontInfo(font); } // calculate the alignment and move/scale the view accordingly - const pos = this._textFont._handleAlignment(this, line, x, y); - const fontSize = this._textSize; - const scale = fontSize / font.unitsPerEm; + // TODO: check this + const pos = { x, y } // this.states.textFont._handleAlignment(this, line, x, y); + const fontSize = this.states.textSize; + const scale = fontSize / font.data.head.unitsPerEm; this.translate(pos.x, pos.y, 0); this.scale(scale, scale, 1); @@ -692,10 +693,10 @@ function text(p5, fn){ } this._applyColorBlend(this.states.curFillColor); - let g = this.retainedMode.geometry['glyph']; + let g = this.geometryBufferCache.getGeometryByID('glyph'); if (!g) { // create the geometry for rendering a quad - const geom = (this._textGeom = new Geometry(1, 1, function() { + g = (this._textGeom = new Geometry(1, 1, function() { for (let i = 0; i <= 1; i++) { for (let j = 0; j <= 1; j++) { this.vertices.push(new Vector(j, i, 0)); @@ -703,12 +704,13 @@ function text(p5, fn){ } } }, this) ); - geom.computeFaces().computeNormals(); - g = this.geometryBufferCache.ensureCached(geom); + g.gid = 'glyph'; + g.computeFaces().computeNormals(); + this.geometryBufferCache.ensureCached(g); } // bind the shader buffers - for (const buff of this.retainedMode.buffers.text) { + for (const buff of this.buffers.text) { buff._prepareBuffer(g, sh); } this._bindBuffer(g.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); @@ -721,6 +723,7 @@ function text(p5, fn){ let dx = 0; // the x position in the line let glyphPrev = null; // the previous glyph, used for kerning // fetch the glyphs in the line of text + // TODO: replace with Typr.U.shape(font, str, ltr) const glyphs = font.stringToGlyphs(line); for (const glyph of glyphs) { @@ -756,10 +759,8 @@ function text(p5, fn){ this.states.drawMode = drawMode; gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - p.pop(); + this.pop(); } - - return p; }; } From 579a56c05f587ee460ee94484af8918cdfef4285 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 8 Dec 2024 13:05:22 -0500 Subject: [PATCH 2/5] Get path data sent to the shader correctly --- preview/index.html | 9 ++-- src/type/p5.Font.js | 103 +++++++++++++++++++++++++++----------------- src/webgl/text.js | 74 +++++++++++++++++++++---------- 3 files changed, 122 insertions(+), 64 deletions(-) diff --git a/preview/index.html b/preview/index.html index 7b9da225f8..d8c8fb6bfe 100644 --- a/preview/index.html +++ b/preview/index.html @@ -21,20 +21,23 @@ const sketch = function (p) { let f; + const testWebgl = true p.setup = async function () { // TODO: make this work without a name f = await p.loadFont('font/BricolageGrotesque-Variable.ttf', 'Bricolage') - p.createCanvas(200, 200, p.WEBGL); + p.createCanvas(200, 200, testWebgl ? p.WEBGL : undefined); }; p.draw = function () { p.background(0, 50, 50); + if (testWebgl) p.translate(-p.width/2, -p.height/2); p.fill('white'); - p.textSize(30); + p.textSize(80); + p.textAlign(p.LEFT, p.TOP) p.textFont(f) - p.text('hello', 10, 30); + p.text('hello,\nworld!', 10, 30, p.width); }; }; diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index aae0d8878d..aaf5f02264 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -1,23 +1,23 @@ -/** +/** * API: * loadFont("https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&display=swap") * loadFont("{ font-family: "Bricolage Grotesque", serif; font-optical-sizing: auto; font-weight: font-style: normal; font-variation-settings: "wdth" 100; }); - * loadFont({ - * fontFamily: '"Bricolage Grotesque", serif'; + * loadFont({ + * fontFamily: '"Bricolage Grotesque", serif'; * fontOpticalSizing: 'auto'; * fontWeight: ''; * fontStyle: 'normal'; - * fontVariationSettings: '"wdth" 100'; + * fontVariationSettings: '"wdth" 100'; * }); * loadFont("https://fonts.gstatic.com/s/bricolagegrotesque/v1/pxiAZBhjZQIdd8jGnEotWQ.woff2"); * loadFont("./path/to/localFont.ttf"); * loadFont("system-font-name"); - * - * + * + * * NEXT: * extract axes from font file - * - * TEST: + * + * TEST: * const font = new FontFace("Inter", "url(./fonts/inter-latin-variable-full-font.woff2)", { style: "oblique 0deg 10deg", weight: "100 900", @@ -254,20 +254,63 @@ function font(p5, fn) { return lines.map(coordify); } - _lineToGlyphs(line, scale) { + _lineToGlyphs(line, scale = 1) { if (!this.data) { throw Error('No font data available for "' + this.name + '"\nTry downloading a local copy of the font file'); } let glyphShapes = Typr.U.shape(this.data, line.text); + line.glyphShapes = glyphShapes; line.glyphs = this._shapeToPaths(glyphShapes, line, scale); return line; } - _shapeToPaths(glyphs, line, scale) { + _positionGlyphs(text) { + const glyphShapes = Typr.U.shape(this.data, text); + const positionedGlyphs = []; + let x = 0; + for (const glyph of glyphShapes) { + positionedGlyphs.push({ x, index: glyph.g, shape: glyph }); + x += glyph.ax; + } + return positionedGlyphs; + } + + _singleShapeToPath(shape, { scale = 1, x = 0, y = 0, lineX = 0, lineY = 0 } = {}) { let font = this.data; + let crdIdx = 0; + let { g, ax, ay, dx, dy } = shape; + let { crds, cmds } = Typr.U.glyphToPath(font, g); + + // can get simple points for each glyph here, but we don't need them ? + let glyph = { /*g: line.text[i], points: [],*/ path: { commands: [] } }; + + for (let j = 0; j < cmds.length; j++) { + let type = cmds[j], command = [type]; + if (type in pathArgCounts) { + let argCount = pathArgCounts[type]; + for (let k = 0; k < argCount; k += 2) { + let gx = crds[k + crdIdx] + x + dx; + let gy = crds[k + crdIdx + 1] + y + dy; + let fx = lineX + gx * scale; + let fy = lineY + gy * -scale; + command.push(fx); + command.push(fy); + /*if (k === argCount - 2) { + glyph.points.push({ x: fx, y: fy }); + }*/ + } + crdIdx += argCount; + } + glyph.path.commands.push(command); + } + + return { glyph, ax, ay }; + } + + _shapeToPaths(glyphs, line, scale = 1) { let x = 0, y = 0, paths = []; if (glyphs.length !== line.text.length) { @@ -277,32 +320,14 @@ function font(p5, fn) { // iterate over the glyphs, converting each to a glyph object // with a path property containing an array of commands for (let i = 0; i < glyphs.length; i++) { - let crdIdx = 0; - let { g, ax, ay, dx, dy } = glyphs[i]; - let { crds, cmds } = Typr.U.glyphToPath(font, g); - - // can get simple points for each glyph here, but we don't need them ? - let glyph = { g: line.text[i], /*points: [],*/ path: { commands: [] } }; - - for (let j = 0; j < cmds.length; j++) { - let type = cmds[j], command = [type]; - if (type in pathArgCounts) { - let argCount = pathArgCounts[type]; - for (let k = 0; k < argCount; k += 2) { - let gx = crds[k + crdIdx] + x + dx; - let gy = crds[k + crdIdx + 1] + y + dy; - let fx = line.x + gx * scale; - let fy = line.y + gy * -scale; - command.push(fx); - command.push(fy); - /*if (k === argCount - 2) { - glyph.points.push({ x: fx, y: fy }); - }*/ - } - crdIdx += argCount; - } - glyph.path.commands.push(command); - } + const { glyph, ax, ay } = this._singleShapeToPath(glyphs[i], { + scale, + x, + y, + lineX: line.x, + lineY: line.y, + }); + paths.push(glyph); x += ax; y += ay; } @@ -411,7 +436,7 @@ function font(p5, fn) { /** * Load a font and returns a p5.Font instance. The font can be specified by its path or a url. - * Optional arguments include the font name, descriptors for the FontFace object, + * Optional arguments include the font name, descriptors for the FontFace object, * and callbacks for success and error. * @param {...any} args - path, name, onSuccess, onError, descriptors * @returns a Promise that resolves with a p5.Font instance @@ -430,7 +455,7 @@ function font(p5, fn) { // parse the font data let fonts = Typr.parse(result); - + if (fonts.length !== 1 || fonts[0].cmap === undefined) { throw Error(23); } @@ -1059,4 +1084,4 @@ export default font; if (typeof p5 !== 'undefined') { font(p5, p5.prototype); -} \ No newline at end of file +} diff --git a/src/webgl/text.js b/src/webgl/text.js index 743f6422c8..9f196e3f8a 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -165,23 +165,60 @@ function text(p5, fn){ * calculates rendering info for a glyph, including the curve information, * row & column stripes compiled into textures. */ - getGlyphInfo (glyph) { + getGlyphInfo(glyph) { // check the cache let gi = this.glyphInfos[glyph.index]; if (gi) return gi; - // get the bounding box of the glyph from opentype.js - const bb = glyph.getBoundingBox(); - const xMin = bb.x1; - const yMin = bb.y1; - const gWidth = bb.x2 - xMin; - const gHeight = bb.y2 - yMin; - const cmds = glyph.path.commands; + const { glyph: { path: { commands } } } = this.font._singleShapeToPath(glyph.shape); + let xMin = Infinity; + let xMax = -Infinity; + let yMin = Infinity; + let yMax = -Infinity; + + for (const cmd of commands) { + for (let i = 1; i < cmd.length; i += 2) { + xMin = Math.min(xMin, cmd[i]); + xMax = Math.max(xMax, cmd[i]); + yMin = Math.min(yMin, cmd[i + 1]); + yMax = Math.max(yMax, cmd[i + 1]); + } + } + // don't bother rendering invisible glyphs - if (gWidth === 0 || gHeight === 0 || !cmds.length) { + if (xMin >= xMax || yMin >= yMax || !commands.length) { return (this.glyphInfos[glyph.index] = {}); } + const gWidth = xMax - xMin; + const gHeight = yMax - yMin; + + // Convert arrays to named objects + const cmds = commands.map((command) => { + const type = command[0]; + switch (type) { + case 'Z': { + return { type }; + } + case 'M': + case 'L': { + const [, x, y] = command; + return { type, x, y }; + } + case 'Q': { + const [, x1, y1, x, y] = command; + return { type, x, y, x1, y1 }; + } + case 'C': { + const [, x1, y1, x2, y2, x, y] = command; + return { type, x, y, x1, y1, x2, y2 }; + } + default: { + throw new Error(`Unexpected path command: ${type}`); + } + } + }) + let i; const strokes = []; // the strokes in this glyph const rows = []; // the indices of strokes in each row @@ -624,7 +661,7 @@ function text(p5, fn){ // initialize the info for this glyph gi = this.glyphInfos[glyph.index] = { glyph, - uGlyphRect: [bb.x1, -bb.y1, bb.x2, -bb.y2], + uGlyphRect: [xMin, yMin, xMax, yMax], strokeImageInfo, strokes, colInfo: layout(cols, this.colDimImageInfos, this.colCellImageInfos), @@ -673,7 +710,7 @@ function text(p5, fn){ // TODO: check this const pos = { x, y } // this.states.textFont._handleAlignment(this, line, x, y); const fontSize = this.states.textSize; - const scale = fontSize / font.data.head.unitsPerEm; + const scale = fontSize / (font.data?.head?.unitsPerEm || 1000); this.translate(pos.x, pos.y, 0); this.scale(scale, scale, 1); @@ -691,6 +728,7 @@ function text(p5, fn){ sh.setUniform('uStrokeImageSize', [strokeImageWidth, strokeImageHeight]); sh.setUniform('uGridSize', [charGridWidth, charGridHeight]); } + this._setGlobalUniforms(sh); this._applyColorBlend(this.states.curFillColor); let g = this.geometryBufferCache.getGeometryByID('glyph'); @@ -713,23 +751,17 @@ function text(p5, fn){ for (const buff of this.buffers.text) { buff._prepareBuffer(g, sh); } - this._bindBuffer(g.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); + this._bindBuffer(this.geometryBufferCache.cache.glyph.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); // this will have to do for now... sh.setUniform('uMaterialColor', this.states.curFillColor); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); try { - let dx = 0; // the x position in the line - let glyphPrev = null; // the previous glyph, used for kerning // fetch the glyphs in the line of text - // TODO: replace with Typr.U.shape(font, str, ltr) - const glyphs = font.stringToGlyphs(line); + const glyphs = font._positionGlyphs(line); for (const glyph of glyphs) { - // kern - if (glyphPrev) dx += font.getKerningValue(glyphPrev, glyph); - const gi = fontInfo.getGlyphInfo(glyph); if (gi.uGlyphRect) { const rowInfo = gi.rowInfo; @@ -741,15 +773,13 @@ function text(p5, fn){ sh.setUniform('uSamplerCols', colInfo.dimImageInfo.imageData); sh.setUniform('uGridOffset', gi.uGridOffset); sh.setUniform('uGlyphRect', gi.uGlyphRect); - sh.setUniform('uGlyphOffset', dx); + sh.setUniform('uGlyphOffset', glyph.x); sh.bindTextures(); // afterwards, only textures need updating // draw it gl.drawElements(gl.TRIANGLES, 6, this.GL.UNSIGNED_SHORT, 0); } - dx += glyph.advanceWidth; - glyphPrev = glyph; } } finally { // clean up From 84b937af7e1a6c77b38aaab54287034535b696f7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 8 Dec 2024 13:30:10 -0500 Subject: [PATCH 3/5] Fix weird letter misalignments --- preview/index.html | 2 +- src/webgl/shaders/font.vert | 2 +- src/webgl/text.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/preview/index.html b/preview/index.html index d8c8fb6bfe..e049faf722 100644 --- a/preview/index.html +++ b/preview/index.html @@ -37,7 +37,7 @@ p.textSize(80); p.textAlign(p.LEFT, p.TOP) p.textFont(f) - p.text('hello,\nworld!', 10, 30, p.width); + p.text('hello, world!', 10, 30, p.width); }; }; diff --git a/src/webgl/shaders/font.vert b/src/webgl/shaders/font.vert index 4655bca0da..ce8b84ab18 100644 --- a/src/webgl/shaders/font.vert +++ b/src/webgl/shaders/font.vert @@ -24,7 +24,7 @@ void main() { 1. / length(newOrigin - newDX), 1. / length(newOrigin - newDY) ); - vec2 offset = pixelScale * normalize(aTexCoord - vec2(0.5, 0.5)) * vec2(1., -1.); + vec2 offset = pixelScale * normalize(aTexCoord - vec2(0.5, 0.5)); vec2 textureOffset = offset * (1. / vec2( uGlyphRect.z - uGlyphRect.x, uGlyphRect.w - uGlyphRect.y diff --git a/src/webgl/text.js b/src/webgl/text.js index 9f196e3f8a..7bf41c3682 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -207,11 +207,11 @@ function text(p5, fn){ } case 'Q': { const [, x1, y1, x, y] = command; - return { type, x, y, x1, y1 }; + return { type, x1, y1, x, y }; } case 'C': { const [, x1, y1, x2, y2, x, y] = command; - return { type, x, y, x1, y1, x2, y2 }; + return { type, x1, y1, x2, y2, x, y }; } default: { throw new Error(`Unexpected path command: ${type}`); From f8ed1fe679fba5e1b60cad5171a6a4671e514c02 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 8 Dec 2024 13:49:08 -0500 Subject: [PATCH 4/5] Fix line breaking --- src/core/p5.Renderer.js | 4 ++-- src/core/p5.Renderer2D.js | 4 ++-- src/type/text2d.js | 19 ++++--------------- src/webgl/text.js | 13 +++++++++---- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 739fca66d5..4f7813d4b0 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -34,7 +34,7 @@ class Renderer { rectMode: constants.CORNER, ellipseMode: constants.CENTER, - textFont: 'sans-serif', + textFont: { family: 'sans-serif' }, textLeading: 15, leadingSet: false, textSize: 12, @@ -485,7 +485,7 @@ class Renderer { /** * Helper function to check font type (system or otf) */ - _isOpenType(f = this.states.textFont) { + _isOpenType({ font: f } = this.states.textFont) { return typeof f === 'object' && f.data; } diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index 75cf1b953d..0c1e3c08fe 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -1405,7 +1405,7 @@ class Renderer2D extends Renderer { return p; } - _applyTextProperties() { + /*_applyTextProperties() { let font; const p = this._pInst; @@ -1435,7 +1435,7 @@ class Renderer2D extends Renderer { } return p; - } + }*/ ////////////////////////////////////////////// // STRUCTURE diff --git a/src/type/text2d.js b/src/type/text2d.js index 3262c70f80..e8e83186a5 100644 --- a/src/type/text2d.js +++ b/src/type/text2d.js @@ -103,7 +103,7 @@ function text2d(p5, fn) { const RendererTextProps = { textAlign: { default: fn.LEFT, type: 'Context2d' }, textBaseline: { default: fn.BASELINE, type: 'Context2d' }, - textFont: { default: 'sans-serif' }, + textFont: { default: { family: 'sans-serif' } }, textLeading: { default: 15 }, textSize: { default: 12 }, textWrap: { default: fn.WORD }, @@ -301,7 +301,7 @@ function text2d(p5, fn) { } // update font properties in this.states - this.states.textFont = this.textFontState(font, family, size); + this.states.textFont = { font, family, size }; // convert/update the size in this.states if (typeof size !== 'undefined') { @@ -703,7 +703,7 @@ function text2d(p5, fn) { @param {string} size - the font-size string to compute @returns {number} - the computed font-size in pixels */ - Renderer.prototype._fontSizePx = function (theSize, family = this.states.textFont) { + Renderer.prototype._fontSizePx = function (theSize, { family } = this.states.textFont) { const isNumString = (num) => !isNaN(num) && num.trim() !== ''; @@ -1088,7 +1088,7 @@ function text2d(p5, fn) { - font-family must be the last value specified. */ let { textFont, textSize, lineHeight, fontStyle, fontWeight, fontVariant } = this.states; - let family = this._parseFontFamily(textFont); + let family = this._parseFontFamily(textFont.family); let style = fontStyle !== fn.NORMAL ? `${fontStyle} ` : ''; let weight = fontWeight !== fn.NORMAL ? `${fontWeight} ` : ''; let variant = fontVariant !== fn.NORMAL ? `${fontVariant} ` : ''; @@ -1152,9 +1152,6 @@ function text2d(p5, fn) { p5.Renderer2D.prototype.textDrawingContext = function() { return this.drawingContext; }; - p5.Renderer2D.prototype.textFontState = function(font, family, size) { - return family; - }; p5.Renderer2D.prototype._renderText = function (text, x, y, maxY, minY) { let states = this.states; @@ -1193,14 +1190,6 @@ function text2d(p5, fn) { } return this._textDrawingContext; }; - p5.RendererGL.prototype.textFontState = function(font, family, size) { - if (typeof font === 'string' || font instanceof String) { - throw new Error( - 'In WebGL mode, textFont() needs to be given the result of loadFont() instead of a font family name.' - ); - } - return font; - }; } } diff --git a/src/webgl/text.js b/src/webgl/text.js index 7bf41c3682..9fa77b6b45 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -7,14 +7,14 @@ import { Geometry } from './p5.Geometry'; function text(p5, fn){ // Text/Typography // @TODO: - RendererGL.prototype._applyTextProperties = function() { + //RendererGL.prototype._applyTextProperties = function() { //@TODO finish implementation //console.error('text commands not yet implemented in webgl'); - }; + //}; RendererGL.prototype.textWidth = function(s) { if (this._isOpenType()) { - return this.states.textFont._textWidth(s, this.states.textSize); + return this.states.textFont.font._textWidth(s, this.states.textSize); } return 0; // TODO: error @@ -700,7 +700,12 @@ function text(p5, fn){ this.states.drawMode = constants.TEXTURE; // get the cached FontInfo object - const font = this.states.textFont; + const { font } = this.states.textFont; + if (!font) { + throw new Error( + 'In WebGL mode, textFont() needs to be given the result of loadFont() instead of a font family name.' + ); + } let fontInfo = this.states.textFont._fontInfo; if (!fontInfo) { fontInfo = this.states.textFont._fontInfo = new FontInfo(font); From d918802596dd65c49266d0efaf1fa6a381cfc186 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 8 Dec 2024 15:04:02 -0500 Subject: [PATCH 5/5] Fix some alignment issues --- preview/index.html | 9 ++- src/type/p5.Font.js | 10 +++ src/type/text2d.js | 192 +++++++++++++++++++++++++++++--------------- 3 files changed, 141 insertions(+), 70 deletions(-) diff --git a/preview/index.html b/preview/index.html index e049faf722..8390b6e72f 100644 --- a/preview/index.html +++ b/preview/index.html @@ -25,7 +25,7 @@ p.setup = async function () { // TODO: make this work without a name - f = await p.loadFont('font/BricolageGrotesque-Variable.ttf', 'Bricolage') + f = await p.loadFont('font/Lato-Black.ttf', 'Lato') p.createCanvas(200, 200, testWebgl ? p.WEBGL : undefined); }; @@ -34,15 +34,16 @@ if (testWebgl) p.translate(-p.width/2, -p.height/2); p.fill('white'); - p.textSize(80); - p.textAlign(p.LEFT, p.TOP) + p.textSize(60); + p.textAlign(p.RIGHT, p.CENTER) p.textFont(f) - p.text('hello, world!', 10, 30, p.width); + p.text('hello, world!', 0, p.height/2, p.width); }; }; new p5(sketch); +

hello, world!

\ No newline at end of file diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index aaf5f02264..b039cd84f2 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -53,6 +53,16 @@ function font(p5, fn) { this.face = fontFace; } + verticalAlign(size) { + const { sCapHeight } = this.data?.['OS/2'] || {}; + const { unitsPerEm = 1000 } = this.data?.head || {}; + const { ascender = 0, descender = 0 } = this.data?.hhea || {}; + const current = ascender / 2; + const target = (sCapHeight || (ascender + descender)) / 2; + const offset = target - current; + return offset * size / unitsPerEm; + } + variations() { let vars = {}; if (this.data) { diff --git a/src/type/text2d.js b/src/type/text2d.js index e8e83186a5..23e9c49d89 100644 --- a/src/type/text2d.js +++ b/src/type/text2d.js @@ -762,38 +762,6 @@ function text2d(p5, fn) { // return { x, y, w, h }; // }; - /* - Position the lines of text based on their textAlign/textBaseline properties - */ - Renderer.prototype._positionLines = function (x, y, width, height, lines) { - - let { textLeading, textAlign } = this.states; - let adjustedX, lineData = new Array(lines.length); - let adjustedW = typeof width === 'undefined' ? 0 : width; - let adjustedH = typeof height === 'undefined' ? 0 : height; - - for (let i = 0; i < lines.length; i++) { - switch (textAlign) { - case fn.START: - throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT - case fn.LEFT: - adjustedX = x; - break; - case fn.CENTER: - adjustedX = x + adjustedW / 2; - break; - case fn.RIGHT: - adjustedX = x + adjustedW; - break; - case fn.END: // TODO: add fn.END: - throw new Error('textBounds: END not yet supported for textAlign'); - } - lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading }; - } - - return this._yAlignOffset(lineData, adjustedH); - }; - /* Process the text string to handle line-breaks and text wrapping @param {string} str - the text to process @@ -860,40 +828,6 @@ function text2d(p5, fn) { } } - /* - Get the y-offset for text given the height, leading, line-count and textBaseline property - */ - Renderer.prototype._yAlignOffset = function (dataArr, height) { - - if (typeof height === 'undefined') { - throw Error('_yAlignOffset: height is required'); - } - - let { textLeading, textBaseline } = this.states; - let yOff = 0, numLines = dataArr.length; - let ydiff = height - (textLeading * (numLines - 1)); - switch (textBaseline) { // drawingContext ? - case fn.TOP: - break; // ?? - case fn.BASELINE: - break; - case fn._CTX_MIDDLE: - yOff = ydiff / 2; - break; - case fn.BOTTOM: - yOff = ydiff; - break; - case fn.IDEOGRAPHIC: - console.warn('textBounds: IDEOGRAPHIC not yet supported for textBaseline'); // FES? - break; - case fn.HANGING: - console.warn('textBounds: HANGING not yet supported for textBaseline'); // FES? - break; - } - dataArr.forEach(ele => ele.y += yOff); - return dataArr; - } - /* Align the bounding box based on the current rectMode setting */ @@ -1179,6 +1113,72 @@ function text2d(p5, fn) { this.pop(); }; + + /* + Position the lines of text based on their textAlign/textBaseline properties + */ + p5.Renderer2D.prototype._positionLines = function (x, y, width, height, lines) { + + let { textLeading, textAlign } = this.states; + let adjustedX, lineData = new Array(lines.length); + let adjustedW = typeof width === 'undefined' ? 0 : width; + let adjustedH = typeof height === 'undefined' ? 0 : height; + + for (let i = 0; i < lines.length; i++) { + switch (textAlign) { + case fn.START: + throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT + case fn.LEFT: + adjustedX = x; + break; + case fn.CENTER: + adjustedX = x + adjustedW / 2; + break; + case fn.RIGHT: + adjustedX = x + adjustedW; + break; + case fn.END: // TODO: add fn.END: + throw new Error('textBounds: END not yet supported for textAlign'); + } + lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading }; + } + + return this._yAlignOffset(lineData, adjustedH); + }; + + /* + Get the y-offset for text given the height, leading, line-count and textBaseline property + */ + p5.Renderer2D.prototype._yAlignOffset = function (dataArr, height) { + + if (typeof height === 'undefined') { + throw Error('_yAlignOffset: height is required'); + } + + let { textLeading, textBaseline } = this.states; + let yOff = 0, numLines = dataArr.length; + let ydiff = height - (textLeading * (numLines - 1)); + switch (textBaseline) { // drawingContext ? + case fn.TOP: + break; // ?? + case fn.BASELINE: + break; + case fn._CTX_MIDDLE: + yOff = ydiff / 2; + break; + case fn.BOTTOM: + yOff = ydiff; + break; + case fn.IDEOGRAPHIC: + console.warn('textBounds: IDEOGRAPHIC not yet supported for textBaseline'); // FES? + break; + case fn.HANGING: + console.warn('textBounds: HANGING not yet supported for textBaseline'); // FES? + break; + } + dataArr.forEach(ele => ele.y += yOff); + return dataArr; + } } if (p5.RendererGL) { p5.RendererGL.prototype.textDrawingContext = function() { @@ -1190,6 +1190,66 @@ function text2d(p5, fn) { } return this._textDrawingContext; }; + + p5.RendererGL.prototype._positionLines = function (x, y, width, height, lines) { + + let { textLeading, textAlign } = this.states; + const widths = lines.map((line) => this._fontWidthSingle(line)); + let adjustedX, lineData = new Array(lines.length); + let adjustedW = typeof width === 'undefined' ? Math.max(0, ...widths) : width; + let adjustedH = typeof height === 'undefined' ? 0 : height; + + for (let i = 0; i < lines.length; i++) { + switch (textAlign) { + case fn.START: + throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT + case fn.LEFT: + adjustedX = x; + break; + case fn._CTX_MIDDLE: + adjustedX = x + (adjustedW - widths[i]) / 2; + break; + case fn.RIGHT: + adjustedX = x + adjustedW - widths[i]; + break; + case fn.END: // TODO: add fn.END: + throw new Error('textBounds: END not yet supported for textAlign'); + } + lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading }; + } + + return this._yAlignOffset(lineData, adjustedH); + }; + + p5.RendererGL.prototype._yAlignOffset = function (dataArr, height) { + + if (typeof height === 'undefined') { + throw Error('_yAlignOffset: height is required'); + } + + let { textLeading, textBaseline, textSize, textFont } = this.states; + let yOff = 0, numLines = dataArr.length; + let totalHeight = textSize * numLines + ((textLeading - textSize) * (numLines - 1)); + switch (textBaseline) { // drawingContext ? + case fn.TOP: + yOff = textSize; + break; + case fn.BASELINE: + break; + case fn._CTX_MIDDLE: + yOff = -totalHeight / 2 + textSize; + break; + case fn.BOTTOM: + yOff = -(totalHeight - textSize); + break; + default: + console.warn(`${textBaseline} is not supported in WebGL mode.`); // FES? + break; + } + yOff += this.states.textFont.font?.verticalAlign(textSize) || 0; + dataArr.forEach(ele => ele.y += yOff); + return dataArr; + } } }