diff --git a/preview/index.html b/preview/index.html index 702811727d..db915f3db8 100644 --- a/preview/index.html +++ b/preview/index.html @@ -20,25 +20,22 @@ import p5 from '../src/app.js'; const sketch = function (p) { - p.setup = function () { - p.createCanvas(100, 100, p.WEBGL); + let font, geom; + p.setup = async function () { + font = await p.loadFont('font/Lato-Black.ttf'); + p.createCanvas(400, 400, p.WEBGL); + p.textSize(120); + p.textAlign(p.CENTER) + geom = font.textToModel('p5*js', 0, 0, { extrude: 20 }); }; p.draw = function () { p.background(200); - p.strokeCap(p.SQUARE); - p.strokeJoin(p.MITER); - p.translate(-p.width/2, -p.height/2); - p.noStroke(); - p.beginShape(); - p.bezierOrder(2); - p.fill('red'); - p.vertex(10, 10); - p.fill('lime'); - p.bezierVertex(40, 25); - p.fill('blue'); - p.bezierVertex(10, 40); - p.endShape(); + p.normalMaterial(); + p.drawingContext.enable(p.drawingContext.CULL_FACE); + p.drawingContext.cullFace(p.drawingContext.FRONT); + p.orbitControl(); + p.model(geom); }; }; diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index 7d8008de87..12d883a2a0 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -139,7 +139,7 @@ function font(p5, fn) { }, []); } - textToContours(str, x, y, width, height, options) { + textToContours(str, x = 0, y = 0, width, height, options) { ({ width, height, options } = this._parseArgs(width, height, options)); const cmds = this.textToPaths(str, x, y, width, height, options); @@ -151,7 +151,61 @@ function font(p5, fn) { cmdContours[cmdContours.length - 1].push(cmd); } - return cmdContours.map((commands) => pathToPoints(commands, options)); + return cmdContours.map((commands) => pathToPoints(commands, options, this)); + } + + textToModel(str, x, y, width, height, options) { + ({ width, height, options } = this._parseArgs(width, height, options)); + const extrude = options?.extrude || 0; + const contours = this.textToContours(str, x, y, width, height, options); + const geom = this._pInst.buildGeometry(() => { + if (extrude === 0) { + this._pInst.beginShape(); + this._pInst.normal(0, 0, 1); + for (const contour of contours) { + this._pInst.beginContour(); + for (const { x, y } of contour) { + this._pInst.vertex(x, y); + } + this._pInst.endContour(this._pInst.CLOSE); + } + this._pInst.endShape(); + } else { + // Draw front faces + for (const side of [1, -1]) { + this._pInst.beginShape(); + for (const contour of contours) { + this._pInst.beginContour(); + for (const { x, y } of contour) { + this._pInst.vertex(x, y, side * extrude * 0.5); + } + this._pInst.endContour(this._pInst.CLOSE); + } + this._pInst.endShape(); + this._pInst.beginShape(); + } + // Draw sides + for (const contour of contours) { + this._pInst.beginShape(this._pInst.QUAD_STRIP); + for (const v of contour) { + for (const side of [-1, 1]) { + this._pInst.vertex(v.x, v.y, side * extrude * 0.5); + } + } + this._pInst.endShape(); + } + } + }); + if (extrude !== 0) { + geom.computeNormals(); + for (const face of geom.faces) { + if (face.every((idx) => geom.vertices[idx].z <= -extrude * 0.5 + 0.1)) { + for (const idx of face) geom.vertexNormals[idx].set(0, 0, -1); + face.reverse(); + } + } + } + return geom; } static async list(log = false) { // tmp @@ -370,7 +424,7 @@ function font(p5, fn) { _measureTextDefault(renderer, str) { let { textAlign, textBaseline } = renderer.states; - let ctx = renderer.drawingContext; + let ctx = renderer.textDrawingContext(); ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'; let metrics = ctx.measureText(str); @@ -624,7 +678,7 @@ function font(p5, fn) { return path; }; - function pathToPoints(cmds, options) { + function pathToPoints(cmds, options, font) { const parseOpts = (options, defaults) => { if (typeof options !== 'object') { @@ -666,10 +720,25 @@ function font(p5, fn) { const totalPoints = Math.ceil(path.getTotalLength() * opts.sampleFactor); let points = []; + const mode = font._pInst.angleMode(); + const DEGREES = font._pInst.DEGREES; for (let i = 0; i < totalPoints; i++) { - points.push( - path.getPointAtLength(path.getTotalLength() * (i / (totalPoints - 1))) - ); + const length = path.getTotalLength() * (i / (totalPoints - 1)); + points.push({ + ...path.getPointAtLength(length), + get angle() { + const angle = path.getAngleAtLength(length); + if (mode === DEGREES) { + return angle * 180 / Math.PI; + } else { + return angle; + } + }, + // For backwards compatibility + get alpha() { + return this.angle; + } + }); } if (opts.simplifyThreshold) { diff --git a/test/unit/visual/cases/typography.js b/test/unit/visual/cases/typography.js index e9aa7a26be..e8984da241 100644 --- a/test/unit/visual/cases/typography.js +++ b/test/unit/visual/cases/typography.js @@ -440,6 +440,33 @@ visualSuite("Typography", function () { p5.endShape(); screenshot(); }); + + for (const mode of ['RADIANS', 'DEGREES']) { + visualTest(`Fonts point angles work in ${mode} mode`, async function(p5, screenshot) { + p5.createCanvas(100, 100); + const font = await p5.loadFont( + '/unit/assets/Inconsolata-Bold.ttf' + ); + p5.background(255); + p5.strokeWeight(2); + p5.textSize(50); + p5.angleMode(p5[mode]); + const pts = font.textToPoints('p5*js', 0, 50, { sampleFactor: 0.25 }); + p5.beginShape(p5.LINES); + for (const { x, y, angle } of pts) { + p5.vertex( + x - 5 * p5.cos(angle), + y - 5 * p5.sin(angle) + ); + p5.vertex( + x + 5 * p5.cos(angle), + y + 5 * p5.sin(angle) + ); + } + p5.endShape(); + screenshot(); + }); + } }); visualSuite('textToContours', function() { diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index f32f9c2f31..e503f80981 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -492,4 +492,45 @@ visualSuite('WebGL', function() { }); } }); + + visualSuite('textToModel', () => { + visualTest('Flat', async (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const font = await p5.loadFont( + '/unit/assets/Inconsolata-Bold.ttf' + ); + p5.textSize(20); + const geom = font.textToModel('p5*js', 0, 0, { + sampleFactor: 2 + }); + geom.normalize(); + p5.background(255); + p5.normalMaterial(); + p5.rotateX(p5.PI*0.1); + p5.rotateY(p5.PI*0.1); + p5.scale(50/200); + p5.model(geom); + screenshot(); + }); + + visualTest('Extruded', async (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const font = await p5.loadFont( + '/unit/assets/Inconsolata-Bold.ttf' + ); + p5.textSize(20); + const geom = font.textToModel('p5*js', 0, 0, { + extrude: 10, + sampleFactor: 2 + }); + geom.normalize(); + p5.background(255); + p5.normalMaterial(); + p5.rotateX(p5.PI*0.1); + p5.rotateY(p5.PI*0.1); + p5.scale(50/200); + p5.model(geom); + screenshot(); + }); + }); }); diff --git a/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in DEGREES mode/000.png b/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in DEGREES mode/000.png new file mode 100644 index 0000000000..6dbe5f2148 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in DEGREES mode/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in DEGREES mode/metadata.json b/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in DEGREES mode/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in DEGREES mode/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in RADIANS mode/000.png b/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in RADIANS mode/000.png new file mode 100644 index 0000000000..6dbe5f2148 Binary files /dev/null and b/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in RADIANS mode/000.png differ diff --git a/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in RADIANS mode/metadata.json b/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in RADIANS mode/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in RADIANS mode/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/textToModel/Extruded/000.png b/test/unit/visual/screenshots/WebGL/textToModel/Extruded/000.png new file mode 100644 index 0000000000..5751dac472 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/textToModel/Extruded/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/textToModel/Extruded/metadata.json b/test/unit/visual/screenshots/WebGL/textToModel/Extruded/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/textToModel/Extruded/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/textToModel/Flat/000.png b/test/unit/visual/screenshots/WebGL/textToModel/Flat/000.png new file mode 100644 index 0000000000..194da82c84 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/textToModel/Flat/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/textToModel/Flat/metadata.json b/test/unit/visual/screenshots/WebGL/textToModel/Flat/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/textToModel/Flat/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file