From 5ef0da1be024c1d876911982793c335f83d0389a Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 10 Apr 2025 20:07:42 -0400 Subject: [PATCH 1/2] Support variable fonts in textToPoints --- src/type/p5.Font.js | 50 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index fd65fa403d..cccc674aa2 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -669,7 +669,9 @@ class Font { // convert lines to paths let uPE = this.data?.head?.unitsPerEm || 1000; let scale = renderer.states.textSize / uPE; - let pathsForLine = lines.map(l => this._lineToGlyphs(l, scale)); + + const axs = this._currentAxes(renderer); + let pathsForLine = lines.map(l => this._lineToGlyphs(l, { scale, axs })); // restore the baseline renderer.drawingContext.textBaseline = setBaseline; @@ -677,6 +679,32 @@ class Font { return pathsForLine; } + _currentAxes(renderer) { + let axs; + if ((this.data?.fvar?.length ?? 0) > 0) { + const fontAxes = this.data.fvar[0]; + axs = fontAxes.map(([tag, minVal, maxVal, defaultVal, flags, name]) => { + if (!renderer) return defaultVal; + if (tag === 'wght') { + return renderer.states.fontWeight; + } else if (tag === 'wdth') { + return renderer.states.fontStretch + } else if (renderer.canvas.style.fontVariationSettings) { + const match = new RegExp(`\\b${tag}\s+(\d+)`) + .exec(renderer.canvas.style.fontVariationSettings); + if (match) { + return parseInt(match[1]); + } else { + return defaultVal; + } + } else { + return defaultVal; + } + }); + } + return axs; + } + _textToPathPoints(str, x, y, width, height, options) { ({ width, height, options } = this._parseArgs(width, height, options)); @@ -760,21 +788,24 @@ class Font { return lines.map(coordify); } - _lineToGlyphs(line, scale = 1) { + _lineToGlyphs(line, { scale = 1, axs } = {}) { 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); + let glyphShapes = Typr.U.shape(this.data, line.text, { axs }); line.glyphShapes = glyphShapes; - line.glyphs = this._shapeToPaths(glyphShapes, line, scale); + + line.glyphs = this._shapeToPaths(glyphShapes, line, { scale, axs }); return line; } - _positionGlyphs(text) { - const glyphShapes = Typr.U.shape(this.data, text); + _positionGlyphs(text, options) { + let renderer = options?.graphics?._renderer || this._pInst._renderer; + const axs = this._currentAxes(renderer); + const glyphShapes = Typr.U.shape(this.data, text, { axs }); const positionedGlyphs = []; let x = 0; for (const glyph of glyphShapes) { @@ -784,11 +815,11 @@ class Font { return positionedGlyphs; } - _singleShapeToPath(shape, { scale = 1, x = 0, y = 0, lineX = 0, lineY = 0 } = {}) { + _singleShapeToPath(shape, { scale = 1, x = 0, y = 0, lineX = 0, lineY = 0, axs } = {}) { let font = this.data; let crdIdx = 0; let { g, ax, ay, dx, dy } = shape; - let { crds, cmds } = Typr.U.glyphToPath(font, g); + let { crds, cmds } = Typr.U.glyphToPath(font, g, true, axs); // can get simple points for each glyph here, but we don't need them ? let glyph = { /*g: line.text[i], points: [],*/ path: { commands: [] } }; @@ -816,7 +847,7 @@ class Font { return { glyph, ax, ay }; } - _shapeToPaths(glyphs, line, scale = 1) { + _shapeToPaths(glyphs, line, { scale = 1, axs } = {}) { let x = 0, y = 0, paths = []; if (glyphs.length !== line.text.length) { @@ -832,6 +863,7 @@ class Font { y, lineX: line.x, lineY: line.y, + axs, }); paths.push(glyph); From e3b92d3ca550e0464b67eefc319e845c96ae08e1 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 12 Apr 2025 11:21:40 -0400 Subject: [PATCH 2/2] Get variable font rendering working in WebGL --- src/type/p5.Font.js | 13 +- src/type/textCore.js | 36 +- src/webgl/p5.RendererGL.js | 7 + src/webgl/p5.Texture.js | 8 + src/webgl/text.js | 438 ++++++++++-------- test/unit/visual/cases/typography.js | 20 + .../000.png | Bin 0 -> 931 bytes .../001.png | Bin 0 -> 911 bytes .../002.png | Bin 0 -> 1003 bytes .../003.png | Bin 0 -> 969 bytes .../004.png | Bin 0 -> 965 bytes .../metadata.json | 3 + 12 files changed, 326 insertions(+), 199 deletions(-) create mode 100644 test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/000.png create mode 100644 test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/001.png create mode 100644 test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/002.png create mode 100644 test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/003.png create mode 100644 test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/004.png create mode 100644 test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/metadata.json diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index cccc674aa2..0b9a5095b1 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -51,8 +51,8 @@ const invalidFontError = 'Sorry, only TTF, OTF and WOFF files are supported.'; / const fontFaceVariations = ['weight', 'stretch', 'style']; - -class Font { +let nextId = 0; +export class Font { constructor(p, fontFace, name, path, data) { if (!(fontFace instanceof FontFace)) { throw Error('FontFace is required'); @@ -62,6 +62,7 @@ class Font { this.path = path; this.data = data; this.face = fontFace; + this.id = nextId++; } /** @@ -688,10 +689,12 @@ class Font { if (tag === 'wght') { return renderer.states.fontWeight; } else if (tag === 'wdth') { - return renderer.states.fontStretch - } else if (renderer.canvas.style.fontVariationSettings) { + // TODO: map from keywords (normal, ultra-condensed, etc) to values + // return renderer.states.fontStretch + return defaultVal; + } else if (renderer.textCanvas().style.fontVariationSettings) { const match = new RegExp(`\\b${tag}\s+(\d+)`) - .exec(renderer.canvas.style.fontVariationSettings); + .exec(renderer.textCanvas().style.fontVariationSettings); if (match) { return parseInt(match[1]); } else { diff --git a/src/type/textCore.js b/src/type/textCore.js index 312346a3a1..75cb718034 100644 --- a/src/type/textCore.js +++ b/src/type/textCore.js @@ -1749,7 +1749,7 @@ function textCore(p5, fn) { modified = true; } // does it exist in the canvas.style ? - else if (prop in this.canvas.style) { + else if (prop in this.textCanvas().style) { this._setCanvasStyleProperty(prop, value, debug); modified = true; } @@ -1913,16 +1913,16 @@ function textCore(p5, fn) { } // lets try to set it on the canvas style - this.canvas.style[opt] = value; + this.textCanvas().style[opt] = value; // check if the value was set successfully - if (this.canvas.style[opt] !== value) { + if (this.textCanvas().style[opt] !== value) { // fails on precision for floating points, also quotes and spaces if (0) console.warn(`Unable to set '${opt}' property` // FES? + ' on canvas.style. It may not be supported. Expected "' - + value + '" but got: "' + this.canvas.style[opt] + "'"); + + value + '" but got: "' + this.textCanvas().style[opt] + "'"); } }; @@ -2075,7 +2075,7 @@ function textCore(p5, fn) { Object.entries(props).forEach(([prop, val]) => { ele.style[prop] = val; }); - this.canvas.appendChild(ele); + this.textCanvas().appendChild(ele); cachedDiv = ele; } return cachedDiv; @@ -2435,7 +2435,9 @@ function textCore(p5, fn) { }; if (p5.Renderer2D) { - + p5.Renderer2D.prototype.textCanvas = function () { + return this.canvas; + }; p5.Renderer2D.prototype.textDrawingContext = function () { return this.drawingContext; }; @@ -2535,15 +2537,31 @@ function textCore(p5, fn) { } if (p5.RendererGL) { - p5.RendererGL.prototype.textDrawingContext = function () { - if (!this._textDrawingContext) { + p5.RendererGL.prototype.textCanvas = function() { + if (!this._textCanvas) { this._textCanvas = document.createElement('canvas'); this._textCanvas.width = 1; this._textCanvas.height = 1; - this._textDrawingContext = this._textCanvas.getContext('2d'); + this._textCanvas.style.display = 'none'; + // Has to be added to the DOM for measureText to work properly! + this.canvas.parentElement.insertBefore(this._textCanvas, this.canvas); + } + return this._textCanvas; + }; + p5.RendererGL.prototype.textDrawingContext = function() { + if (!this._textDrawingContext) { + const textCanvas = this.textCanvas(); + this._textDrawingContext = textCanvas.getContext('2d'); } return this._textDrawingContext; }; + const oldRemove = p5.RendererGL.prototype.remove; + p5.RendererGL.prototype.remove = function() { + if (this._textCanvas) { + this._textCanvas.parentElement.removeChild(this._textCanvas); + } + oldRemove.call(this); + }; p5.RendererGL.prototype._positionLines = function (x, y, width, height, lines) { diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index ce3bcf411e..a5ea3b85a9 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -457,6 +457,13 @@ class RendererGL extends Renderer { }; } + remove() { + this.wrappedElt.remove(); + this.wrappedElt = null; + this.canvas = null; + this.elt = null; + } + ////////////////////////////////////////////// // Geometry Building ////////////////////////////////////////////// diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 164b71615a..42cf18e118 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -86,6 +86,14 @@ class Texture { return this; } + remove() { + if (this.glTex) { + const gl = this._renderer.GL; + gl.deleteTexture(this.glTex); + this.glTex = undefined; + } + } + _getTextureDataFromSource () { let textureData; if (this.isFramebufferTexture) { diff --git a/src/webgl/text.js b/src/webgl/text.js index 266a243460..633297406f 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -1,10 +1,47 @@ -import * as constants from '../core/constants'; -import { RendererGL } from './p5.RendererGL'; -import { Vector } from '../math/p5.Vector'; -import { Geometry } from './p5.Geometry'; -import { arrayCommandsToObjects } from '../type/p5.Font'; +import * as constants from "../core/constants"; +import { RendererGL } from "./p5.RendererGL"; +import { Vector } from "../math/p5.Vector"; +import { Geometry } from "./p5.Geometry"; +import { Font, arrayCommandsToObjects } from "../type/p5.Font"; + +function text(p5, fn) { + RendererGL.prototype.maxCachedGlyphs = function() { + // TODO: use more than vibes to find a good value for this + return 200 + }; + + RendererGL.prototype.freeGlyphInfo = function(gi) { + const datas = [ + gi.strokeImageInfo.imageData, + gi.rowInfo.cellImageInfo.imageData, + gi.rowInfo.dimImageInfo.imageData, + gi.colInfo.cellImageInfo.imageData, + gi.colInfo.dimImageInfo.imageData, + ]; + for (const data of datas) { + const tex = this.textures.get(data); + if (tex) { + tex.remove(); + this.textures.delete(data); + } + } + } + + Font.prototype._getFontInfo = function(axs) { + // For WebGL, a cache of font data to use on the GPU. + this._fontInfos = this._fontInfos || {}; + + const key = JSON.stringify(axs); + if (this._fontInfos[key]) { + const val = this._fontInfos[key]; + return val; + } else { + const val = new FontInfo(this, { axs }); + this._fontInfos[key] = val; + return val; + } + } -function text(p5, fn){ // Text/Typography (see src/type/textCore.js) /* RendererGL.prototype.textWidth = function(s) { @@ -49,17 +86,17 @@ function text(p5, fn){ this.infos = []; // the list of images } /** - * - * @param {Integer} space - * @return {Object} contains the ImageData, and pixel index into that - * ImageData where the free space was allocated. - * - * finds free space of a given size in the ImageData list - */ - findImage (space) { + * + * @param {Integer} space + * @return {Object} contains the ImageData, and pixel index into that + * ImageData where the free space was allocated. + * + * finds free space of a given size in the ImageData list + */ + findImage(space) { const imageSize = this.width * this.height; if (space > imageSize) - throw new Error('font is too complex to render in 3D'); + throw new Error("font is too complex to render in 3D"); // search through the list of images, looking for one with // anough unused space. @@ -81,15 +118,15 @@ function text(p5, fn){ } catch (err) { // for browsers that don't support ImageData constructors (ie IE11) // create an ImageData using the old method - let canvas = document.getElementsByTagName('canvas')[0]; + let canvas = document.getElementsByTagName("canvas")[0]; const created = !canvas; if (!canvas) { // create a temporary canvas - canvas = document.createElement('canvas'); - canvas.style.display = 'none'; + canvas = document.createElement("canvas"); + canvas.style.display = "none"; document.body.appendChild(canvas); } - const ctx = canvas.getContext('2d'); + const ctx = canvas.getContext("2d"); if (ctx) { imageData = ctx.createImageData(this.width, this.height); } @@ -140,10 +177,14 @@ function text(p5, fn){ * contains cached images and glyph information for an opentype font */ class FontInfo { - constructor(font) { + constructor(font, { axs } = {}) { this.font = font; + this.axs = axs; // the bezier curve coordinates - this.strokeImageInfos = new ImageInfos(strokeImageWidth, strokeImageHeight); + this.strokeImageInfos = new ImageInfos( + strokeImageWidth, + strokeImageHeight, + ); // lists of curve indices for each row/column slice this.colDimImageInfos = new ImageInfos(gridImageWidth, gridImageHeight); this.rowDimImageInfos = new ImageInfos(gridImageWidth, gridImageHeight); @@ -155,18 +196,23 @@ function text(p5, fn){ this.glyphInfos = {}; } /** - * @param {Glyph} glyph the x positions of points in the curve - * @returns {Object} the glyphInfo for that glyph - * - * calculates rendering info for a glyph, including the curve information, - * row & column stripes compiled into textures. - */ + * @param {Glyph} glyph the x positions of points in the curve + * @returns {Object} the glyphInfo for that glyph + * + * calculates rendering info for a glyph, including the curve information, + * row & column stripes compiled into textures. + */ getGlyphInfo(glyph) { // check the cache let gi = this.glyphInfos[glyph.index]; if (gi) return gi; - const { glyph: { path: { commands } } } = this.font._singleShapeToPath(glyph.shape); + const axs = this.axs; + const { + glyph: { + path: { commands }, + }, + } = this.font._singleShapeToPath(glyph.shape, { axs }); let xMin = Infinity; let xMax = -Infinity; let yMin = Infinity; @@ -200,25 +246,25 @@ function text(p5, fn){ for (i = charGridHeight - 1; i >= 0; --i) rows.push([]); /** - * @function push - * @param {Number[]} xs the x positions of points in the curve - * @param {Number[]} ys the y positions of points in the curve - * @param {Object} v the curve information - * - * adds a curve to the rows & columns that it intersects with - */ + * @function push + * @param {Number[]} xs the x positions of points in the curve + * @param {Number[]} ys the y positions of points in the curve + * @param {Object} v the curve information + * + * adds a curve to the rows & columns that it intersects with + */ function push(xs, ys, v) { const index = strokes.length; // the index of this stroke strokes.push(v); // add this stroke to the list /** - * @function minMax - * @param {Number[]} rg the list of values to compare - * @param {Number} min the initial minimum value - * @param {Number} max the initial maximum value - * - * find the minimum & maximum value in a list of values - */ + * @function minMax + * @param {Number[]} rg the list of values to compare + * @param {Number} min the initial minimum value + * @param {Number} max the initial maximum value + * + * find the minimum & maximum value in a list of values + */ function minMax(rg, min, max) { for (let i = rg.length; i-- > 0; ) { const v = rg[i]; @@ -240,34 +286,34 @@ function text(p5, fn){ const mmX = minMax(xs, 1, 0); const ixMin = Math.max( Math.floor(mmX.min * charGridWidth - cellOffset), - 0 + 0, ); const ixMax = Math.min( Math.ceil(mmX.max * charGridWidth + cellOffset), - charGridWidth + charGridWidth, ); for (let iCol = ixMin; iCol < ixMax; ++iCol) cols[iCol].push(index); const mmY = minMax(ys, 1, 0); const iyMin = Math.max( Math.floor(mmY.min * charGridHeight - cellOffset), - 0 + 0, ); const iyMax = Math.min( Math.ceil(mmY.max * charGridHeight + cellOffset), - charGridHeight + charGridHeight, ); for (let iRow = iyMin; iRow < iyMax; ++iRow) rows[iRow].push(index); } /** - * @function clamp - * @param {Number} v the value to clamp - * @param {Number} min the minimum value - * @param {Number} max the maxmimum value - * - * clamps a value between a minimum & maximum value - */ + * @function clamp + * @param {Number} v the value to clamp + * @param {Number} min the minimum value + * @param {Number} max the maxmimum value + * + * clamps a value between a minimum & maximum value + */ function clamp(v, min, max) { if (v < min) return min; if (v > max) return max; @@ -275,25 +321,25 @@ function text(p5, fn){ } /** - * @function byte - * @param {Number} v the value to scale - * - * converts a floating-point number in the range 0-1 to a byte 0-255 - */ + * @function byte + * @param {Number} v the value to scale + * + * converts a floating-point number in the range 0-1 to a byte 0-255 + */ function byte(v) { return clamp(255 * v, 0, 255); } /** - * @private - * @class Cubic - * @param {Number} p0 the start point of the curve - * @param {Number} c0 the first control point - * @param {Number} c1 the second control point - * @param {Number} p1 the end point - * - * a cubic curve - */ + * @private + * @class Cubic + * @param {Number} p0 the start point of the curve + * @param {Number} c0 the first control point + * @param {Number} c1 the second control point + * @param {Number} p1 the end point + * + * a cubic curve + */ class Cubic { constructor(p0, c0, c1, p1) { this.p0 = p0; @@ -302,46 +348,46 @@ function text(p5, fn){ this.p1 = p1; } /** - * @return {Object} the quadratic approximation - * - * converts the cubic to a quadtratic approximation by - * picking an appropriate quadratic control point - */ - toQuadratic () { + * @return {Object} the quadratic approximation + * + * converts the cubic to a quadtratic approximation by + * picking an appropriate quadratic control point + */ + toQuadratic() { return { x: this.p0.x, y: this.p0.y, x1: this.p1.x, y1: this.p1.y, cx: ((this.c0.x + this.c1.x) * 3 - (this.p0.x + this.p1.x)) / 4, - cy: ((this.c0.y + this.c1.y) * 3 - (this.p0.y + this.p1.y)) / 4 + cy: ((this.c0.y + this.c1.y) * 3 - (this.p0.y + this.p1.y)) / 4, }; } /** - * @return {Number} the error - * - * calculates the magnitude of error of this curve's - * quadratic approximation. - */ - quadError () { + * @return {Number} the error + * + * calculates the magnitude of error of this curve's + * quadratic approximation. + */ + quadError() { return ( Vector.sub( Vector.sub(this.p1, this.p0), - Vector.mult(Vector.sub(this.c1, this.c0), 3) + Vector.mult(Vector.sub(this.c1, this.c0), 3), ).mag() / 2 ); } /** - * @param {Number} t the value (0-1) at which to split - * @return {Cubic} the second part of the curve - * - * splits the cubic into two parts at a point 't' along the curve. - * this cubic keeps its start point and its end point becomes the - * point at 't'. the 'end half is returned. - */ - split (t) { + * @param {Number} t the value (0-1) at which to split + * @return {Cubic} the second part of the curve + * + * splits the cubic into two parts at a point 't' along the curve. + * this cubic keeps its start point and its end point becomes the + * point at 't'. the 'end half is returned. + */ + split(t) { const m1 = Vector.lerp(this.p0, this.c0, t); const m2 = Vector.lerp(this.c0, this.c1, t); const mm1 = Vector.lerp(m1, m2, t); @@ -355,18 +401,18 @@ function text(p5, fn){ } /** - * @return {Cubic[]} the non-inflecting pieces of this cubic - * - * returns an array containing 0, 1 or 2 cubics split resulting - * from splitting this cubic at its inflection points. - * this cubic is (potentially) altered and returned in the list. - */ - splitInflections () { + * @return {Cubic[]} the non-inflecting pieces of this cubic + * + * returns an array containing 0, 1 or 2 cubics split resulting + * from splitting this cubic at its inflection points. + * this cubic is (potentially) altered and returned in the list. + */ + splitInflections() { const a = Vector.sub(this.c0, this.p0); const b = Vector.sub(Vector.sub(this.c1, this.c0), a); const c = Vector.sub( Vector.sub(Vector.sub(this.p1, this.c1), a), - Vector.mult(b, 2) + Vector.mult(b, 2), ); const cubics = []; @@ -410,27 +456,27 @@ function text(p5, fn){ } /** - * @function cubicToQuadratics - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} cx0 - * @param {Number} cy0 - * @param {Number} cx1 - * @param {Number} cy1 - * @param {Number} x1 - * @param {Number} y1 - * @returns {Cubic[]} an array of cubics whose quadratic approximations - * closely match the civen cubic. - * - * converts a cubic curve to a list of quadratics. - */ + * @function cubicToQuadratics + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} cx0 + * @param {Number} cy0 + * @param {Number} cx1 + * @param {Number} cy1 + * @param {Number} x1 + * @param {Number} y1 + * @returns {Cubic[]} an array of cubics whose quadratic approximations + * closely match the civen cubic. + * + * converts a cubic curve to a list of quadratics. + */ function cubicToQuadratics(x0, y0, cx0, cy0, cx1, cy1, x1, y1) { // create the Cubic object and split it at its inflections const cubics = new Cubic( new Vector(x0, y0), new Vector(cx0, cy0), new Vector(cx1, cy1), - new Vector(x1, y1) + new Vector(x1, y1), ).splitInflections(); const qs = []; // the final list of quadratics @@ -478,14 +524,14 @@ function text(p5, fn){ } /** - * @function pushLine - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} x1 - * @param {Number} y1 - * - * add a straight line to the row/col grid of a glyph - */ + * @function pushLine + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} x1 + * @param {Number} y1 + * + * add a straight line to the row/col grid of a glyph + */ function pushLine(x0, y0, x1, y1) { const mx = (x0 + x1) / 2; const my = (y0 + y1) / 2; @@ -493,15 +539,15 @@ function text(p5, fn){ } /** - * @function samePoint - * @param {Number} x0 - * @param {Number} y0 - * @param {Number} x1 - * @param {Number} y1 - * @return {Boolean} true if the two points are sufficiently close - * - * tests if two points are close enough to be considered the same - */ + * @function samePoint + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} x1 + * @param {Number} y1 + * @return {Boolean} true if the two points are sufficiently close + * + * tests if two points are close enough to be considered the same + */ function samePoint(x0, y0, x1, y1) { return Math.abs(x1 - x0) < 0.00001 && Math.abs(y1 - y0) < 0.00001; } @@ -517,25 +563,25 @@ function text(p5, fn){ if (samePoint(x0, y0, x1, y1)) continue; switch (cmd.type) { - case 'M': { + case "M": { // move xs = x1; ys = y1; break; } - case 'L': { + case "L": { // line pushLine(x0, y0, x1, y1); break; } - case 'Q': { + case "Q": { // quadratic const cx = (cmd.x1 - xMin) / gWidth; const cy = (cmd.y1 - yMin) / gHeight; push([x0, x1, cx], [y0, y1, cy], { x: x0, y: y0, cx, cy }); break; } - case 'Z': { + case "Z": { // end if (!samePoint(x0, y0, xs, ys)) { // add an extra line closing the loop, if necessary @@ -546,7 +592,7 @@ function text(p5, fn){ } break; } - case 'C': { + case "C": { // cubic const cx1 = (cmd.x1 - xMin) / gWidth; const cy1 = (cmd.y1 - yMin) / gHeight; @@ -578,16 +624,16 @@ function text(p5, fn){ } /** - * @function layout - * @param {Number[][]} dim - * @param {ImageInfo[]} dimImageInfos - * @param {ImageInfo[]} cellImageInfos - * @return {Object} - * - * lays out the curves in a dimension (row or col) into two - * images, one for the indices of the curves themselves, and - * one containing the offset and length of those index spans. - */ + * @function layout + * @param {Number[][]} dim + * @param {ImageInfo[]} dimImageInfos + * @param {ImageInfo[]} cellImageInfos + * @return {Object} + * + * lays out the curves in a dimension (row or col) into two + * images, one for the indices of the curves themselves, and + * one containing the offset and length of those index spans. + */ function layout(dim, dimImageInfos, cellImageInfos) { const dimLength = dim.length; // the number of slices in this dimension const dimImageInfo = dimImageInfos.findImage(dimLength); @@ -613,7 +659,7 @@ function text(p5, fn){ cellLineIndex >> 7, cellLineIndex & 0x7f, strokeCount >> 7, - strokeCount & 0x7f + strokeCount & 0x7f, ); // for each stroke index in that slice @@ -627,7 +673,7 @@ function text(p5, fn){ return { cellImageInfo, dimOffset, - dimImageInfo + dimImageInfo, }; } @@ -638,17 +684,17 @@ function text(p5, fn){ strokeImageInfo, strokes, colInfo: layout(cols, this.colDimImageInfos, this.colCellImageInfos), - rowInfo: layout(rows, this.rowDimImageInfos, this.rowCellImageInfos) + rowInfo: layout(rows, this.rowDimImageInfos, this.rowCellImageInfos), }; gi.uGridOffset = [gi.colInfo.dimOffset, gi.rowInfo.dimOffset]; return gi; } } - RendererGL.prototype._renderText = function(line, x, y, maxY, minY) { - if (!this.states.textFont || typeof this.states.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.' + "WEBGL: you must load and set a font before drawing text. See `loadFont` and `textFont` for more details.", ); return; } @@ -658,7 +704,7 @@ function text(p5, fn){ if (!p5.Font.hasGlyphData(this.states.textFont)) { console.log( - 'WEBGL: only Opentype (.otf) and Truetype (.ttf) fonts with glyph data are supported' + "WEBGL: only Opentype (.otf) and Truetype (.ttf) fonts with glyph data are supported", ); return; } @@ -669,24 +715,22 @@ function text(p5, fn){ const doStroke = this.states.strokeColor; const drawMode = this.states.drawMode; - this.states.setValue('strokeColor', null); - this.states.setValue('drawMode', constants.TEXTURE); + this.states.setValue("strokeColor", null); + this.states.setValue("drawMode", constants.TEXTURE); // get the cached FontInfo object 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.' + "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); - } + const axs = font._currentAxes(this); + let fontInfo = font._getFontInfo(axs); // calculate the alignment and move/scale the view accordingly // TODO: check this - const pos = { x, y } // this.states.textFont._handleAlignment(this, line, x, y); + const pos = { x, y }; // this.states.textFont._handleAlignment(this, line, x, y); const fontSize = this.states.textSize; const scale = fontSize / (font.data?.head?.unitsPerEm || 1000); this.translate(pos.x, pos.y, 0); @@ -701,29 +745,36 @@ function text(p5, fn){ if (initializeShader) { // these are constants, really. just initialize them one-time. - sh.setUniform('uGridImageSize', [gridImageWidth, gridImageHeight]); - sh.setUniform('uCellsImageSize', [cellImageWidth, cellImageHeight]); - sh.setUniform('uStrokeImageSize', [strokeImageWidth, strokeImageHeight]); - sh.setUniform('uGridSize', [charGridWidth, charGridHeight]); + sh.setUniform("uGridImageSize", [gridImageWidth, gridImageHeight]); + sh.setUniform("uCellsImageSize", [cellImageWidth, cellImageHeight]); + sh.setUniform("uStrokeImageSize", [strokeImageWidth, strokeImageHeight]); + sh.setUniform("uGridSize", [charGridWidth, charGridHeight]); } - const curFillColor = this.states.fillSet ? this.states.curFillColor : [0, 0, 0, 255]; + const curFillColor = this.states.fillSet + ? this.states.curFillColor + : [0, 0, 0, 255]; this._setGlobalUniforms(sh); this._applyColorBlend(curFillColor); - let g = this.geometryBufferCache.getGeometryByID('glyph'); + let g = this.geometryBufferCache.getGeometryByID("glyph"); if (!g) { // create the geometry for rendering a quad - 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)); - this.uvs.push(j, i); + 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)); + this.uvs.push(j, i); + } } - } - }, this) ); - g.gid = 'glyph'; + }, + this, + ); + g.gid = "glyph"; g.computeFaces().computeNormals(); this.geometryBufferCache.ensureCached(g); } @@ -732,12 +783,17 @@ function text(p5, fn){ for (const buff of this.buffers.text) { buff._prepareBuffer(g, sh); } - this._bindBuffer(this.geometryBufferCache.cache.glyph.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', curFillColor); + sh.setUniform("uMaterialColor", curFillColor); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + this.fontCache = this.fontCache || new Map(); + try { // fetch the glyphs in the line of text const glyphs = font._positionGlyphs(line); @@ -745,16 +801,28 @@ function text(p5, fn){ for (const glyph of glyphs) { const gi = fontInfo.getGlyphInfo(glyph); if (gi.uGlyphRect) { + + const cacheKey = JSON.stringify({ font: font.id, axs, glyph: glyph.shape.g }); + // Bump this font to the end of the cache list by deleting and re-adding it + this.fontCache.delete(cacheKey); + this.fontCache.set(cacheKey, gi); + if (this.fontCache.size > this.maxCachedGlyphs()) { + const keyToRemove = this.fontCache.keys().next().value; + const val = this.fontCache.get(keyToRemove); + this.fontCache.delete(keyToRemove); + this.freeGlyphInfo(val); + } + const rowInfo = gi.rowInfo; const colInfo = gi.colInfo; - sh.setUniform('uSamplerStrokes', gi.strokeImageInfo.imageData); - sh.setUniform('uSamplerRowStrokes', rowInfo.cellImageInfo.imageData); - sh.setUniform('uSamplerRows', rowInfo.dimImageInfo.imageData); - sh.setUniform('uSamplerColStrokes', colInfo.cellImageInfo.imageData); - sh.setUniform('uSamplerCols', colInfo.dimImageInfo.imageData); - sh.setUniform('uGridOffset', gi.uGridOffset); - sh.setUniform('uGlyphRect', gi.uGlyphRect); - sh.setUniform('uGlyphOffset', glyph.x); + sh.setUniform("uSamplerStrokes", gi.strokeImageInfo.imageData); + sh.setUniform("uSamplerRowStrokes", rowInfo.cellImageInfo.imageData); + sh.setUniform("uSamplerRows", rowInfo.dimImageInfo.imageData); + sh.setUniform("uSamplerColStrokes", colInfo.cellImageInfo.imageData); + sh.setUniform("uSamplerCols", colInfo.dimImageInfo.imageData); + sh.setUniform("uGridOffset", gi.uGridOffset); + sh.setUniform("uGlyphRect", gi.uGlyphRect); + sh.setUniform("uGlyphOffset", glyph.x); sh.bindTextures(); // afterwards, only textures need updating @@ -766,8 +834,8 @@ function text(p5, fn){ // clean up sh.unbindShader(); - this.states.setValue('strokeColor', doStroke); - this.states.setValue('drawMode', drawMode); + this.states.setValue("strokeColor", doStroke); + this.states.setValue("drawMode", drawMode); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); this.pop(); diff --git a/test/unit/visual/cases/typography.js b/test/unit/visual/cases/typography.js index 46fbab5777..8d0f37cf8a 100644 --- a/test/unit/visual/cases/typography.js +++ b/test/unit/visual/cases/typography.js @@ -125,6 +125,26 @@ visualSuite("Typography", function () { screenshot(); } }); + + visualTest('can control variable fonts from files in WebGL', async function (p5, screenshot) { + p5.createCanvas(100, 100, p5.WEBGL); + const font = await p5.loadFont( + '/unit/assets/BricolageGrotesque-Variable.ttf', + { weight: '200 800' } + ); + for (let weight = 400; weight <= 800; weight += 100) { + p5.push(); + p5.background(255); + p5.translate(-p5.width/2, -p5.height/2); + p5.textFont(font); + p5.textAlign(p5.LEFT, p5.TOP); + p5.textSize(35); + p5.textWeight(weight); + p5.text('p5*js', 0, 10, p5.width); + p5.pop(); + screenshot(); + } + }); }); visualSuite("textAlign", function () { diff --git a/test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/000.png b/test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/000.png new file mode 100644 index 0000000000000000000000000000000000000000..8a03d92c019e95bcceb6e2234a4a21a059d9d566 GIT binary patch literal 931 zcmeAS@N?(olHy`uVBq!ia0vp^DImV2<^4aSW-5 zdppaq@3n$}i~8+5TI%OFZaNdLWcg`(!@6TLY_v^RUu1C(>~AugdHVCuntwmpA0IgC zV4!o)iKpf0VTS}8o+Az!dW%|+sqNorsX#Mn4Pc)^T{>SICC#LZ0poe zP5F6y5>sjP><78m1%eyC@uXPT!t;uFR@GUmE_JKRYbUR}Q zFILwl?#I4_-OhBVx;$;}iw@mv?i8_}?>p9R+U~ij=%RS2+p0q|S)YDzj-7d8P0O0& zCq*Q;zFMDd-ZAf|q(bD2o8Km0eD(ZrJkMs~mn%|dSa=40ix zA}VyPY5#?#RRt%+LvD(1m>7B{L%}$4Pi^-)zi)09Q4<%duX*=(;<}05F23z7Dwib} zuH|Z3uyu)|4@c;ax!=LK* ztM9Da{`2Pf-mTS_>`&ERe|KN%u0z5e-XjhPdVD|KMaSW-5 zdpj$!_>h8t%k2OE7r#9+)53(QTHK&=vOmVOi3kBwR-Oe^=M<>tKpSKiRXzG>EO$(yTx-P^HNf=5NT zJ1K|P;q_Lbpv)c9*BrVqQFV2CR!jcd&*dJ+%(h>cGk~o-a9b=7+Rb6Mf@+|9K=|yd`{fZ`!_k%L!s4p#rB`0u{m! z&g65EG_Bg#5f@*YxVOYjx^Km_GdtF^eabRzR{!R`TW4#E%Xfj&xtpi2x%kB8*<{Zn zbvfS?Dwf!vFY(>WB|Ep`%g;Hx{8=7wFvv_$W>98xXe!?1ztIxd(K5FoJ*FyPIXZI{zf3^I8 zLxO0M0vl5?gW7RUBGS_17730sj5B1^k2oao9e84)I0&@vuYD8CwCll({lFZ?;OXk; Jvd$@?2>=0mlN0~| literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/002.png b/test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/002.png new file mode 100644 index 0000000000000000000000000000000000000000..877827a966d1e1a1d2148c76a2684c88a3f3fce1 GIT binary patch literal 1003 zcmeAS@N?(olHy`uVBq!ia0vp^DImU|#C!;uum9 z_jcC9!dC_YErsb9IH!JSchYFRbmBmdao>b@-_A|jGvVz1CpRx;?Y}*lCBMo>uD`i1 zLuLj`OWRTJj)sDd0s=;IOiBtld=p+?lv7ej;hS(W(21qx45N{yS4YDHO9wS~PC)@9 z_7hJu6q$^!HC}%C#romjI(h5Lq;37)Cro_vPv_Jw6|OXU7&|?D7vsvAjq48lI>%$W z)71X=_dn=MNyY1EDQl)4P8QKjsI*nA9sm*DV4jnJec$5 zEtm5<-W-}(X#6|&{EK7#_US=$PZEq^?KRN_Q&|+d7^(G=ef`{)pDcaF>`Z|!zVlhUFO9uzV7QG8Myj< zmHWL<7O%g)(T#FaQM^_-Td2cbZtsW6#}R9VD#de6;~XoBa{~>o_9fnR4!C&Y?X~Cy zp$lK^ep(!ty7;yB@wcppb0RN_iQJTrV$0tfz~8Y|zVFhHFqad)wz~qWbCW}kDmtX; zdPHs0n^AE3)V40$`=5Jc6JO2xx;+0?w%XgT@4i;q#^{L`S1`VOT9+L2B*QKuC}Y=p zua2#ro7Eqe>pPo$6?imPzAxvAwf9;-{=e>)ZV&IT@^mTDWmG*a6(35 y^5PZ_j+x9UMNU~cwwaSW-5 zdpql5?;!&L7thQu4(wZ8*+m>PEd@0vZG3b$GCh~=?w#iki;sOSyH>U%HTL?~MeBh!vsqQHFu!A5&MZJ8j4I#&lqOP zsJb|uvo6>hXRlVdH}2Vz)0=8)&i&~fmxI0Gkh}JW^y6R@o5C3<|%t^MfYcm_ut;vxcVFa`lAaux5gtG1V~lkM2P?bgBk?Uv#l=^iP@y?IOSZQff{W6!U< z?8N3puRoMbn?KtjVB%JXD=aP=s}{~X?RBDhO{s@NNpI67mZlD^;|kn8GfKYH@ca2k z?X}dEcXRM$4how)Rp5NX3)b~hd;Wfpi?X$y;xOTrd&7d2&d~|kb}soT^)oAL%7l$} z^Da=={UUO8^0j%Ui;q6*HEwjd-M-y@N?P^WL&i(L>X^iaIW2O1I8)|ljh4Xqlze-Q zX!pC*+(i9fKM*psH2U`1!R`C=z}de#gs(kweZ4Gh{#AQD{gdz8Z7wT`hV{EX{}%OP zVw+$9%aS51E;Z>>0t%7u`;Ek2s~)-3b>QTM9~W!aOuL#D9qI0w{eMGL+LG4P%&!OB zOd^vHe;0V4yW+d$t;hM>Z$JH6GcPV(Y(m&aAAgS{f?wpC&dl6;K~CYTtx8nNiCqD4#GzHxe&hdq3A-h)wS?}y3+Bk|5J|Kc=eS}7`Ayw3gNpTX8g z2dsldPH{+lI4ZDmQ<{H{L{Pu{HV1)@@<*$`i|Rk!JHPy{bCtZ=>-Q6~no%@ zVR3OdA)_#PaSI2>Oy-oLDFTd&#SK1vLP`oLd=pLvIuS~YgG2K_hRsajOfjB$Ou)R! N;OXk;vd$@?2>`pIpacK_ literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/004.png b/test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/004.png new file mode 100644 index 0000000000000000000000000000000000000000..eeaf58be7801f3e83e3207ee9760a7eac45ea3a3 GIT binary patch literal 965 zcmeAS@N?(olHy`uVBq!ia0vp^DImV6OLcaSW-5 zdpj$z@3De_%j51RdOIu6P3le&@t&r`f8p*0i>EWU&(Aiv`gH|cep%)I`|qXeQ)D6- zlny6}G70F)F$g|p6lhpssj#fwm_;Fpuj8bmB72KEs4ts`Q%wjg zDE8jHpXI{udB(@rI;;^3xSeWSdoMa>wQcI&#m|KdtRq7eZXcRAH%?1uwQt3>d`7LU z+yDMp{Ov@Q@wxeG6Mp}#tKJ*8j`wA*;YB4Swy7;ujbGLYuRU(Gne)i3nwq?=lH9YF zTb$nE5%vA;@v8Mx53LB_`f)k)N0EECZ)b-`zCL_m@k@WE&NquD-m9~zsBUyIskroZ z<$WIymyjn)37wm7MPK`KO;R|sKA!o~uM4MG9^HJp)mVE|=Njk#H`r|Fb6iWyv^8J9 zcj?K!x(iC$x8Ey`skh(9qOxhVyIDY(Qr*??-v&9g43TP4=f5i{C2y~sZ@EwL#$I{-v>)CaO^JK)EH{}dZa*p?YWhhcq=z4pq^t8}t3zhUINqB*FhfRY5NLhQzOXx-iS1IQ R3@~3Zc)I$ztaD0e0szGHrf&cM literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/metadata.json b/test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/metadata.json new file mode 100644 index 0000000000..01dd8f26ca --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textWeight/can control variable fonts from files in WebGL/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 5 +} \ No newline at end of file