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