From 03f284877d43d2dfdbf9468a227464c1a3cc4997 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 18 Dec 2024 13:00:46 -0500 Subject: [PATCH 1/5] Add angle and alpha back to textToPoints results --- src/type/p5.Font.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index 7d8008de87..720b8d60e2 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -667,9 +667,17 @@ function font(p5, fn) { let points = []; 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() { + return path.getAngleAtLength(length) * 180 / Math.PI; + }, + // For backwards compatibility + get alpha() { + return this.angle; + } + }); } if (opts.simplifyThreshold) { From 37c10165be691f29a7d98a6d7156fdd6690de1b2 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 18 Dec 2024 13:39:03 -0500 Subject: [PATCH 2/5] Add convenience function for text to 3D model --- preview/index.html | 27 ++++---- src/type/p5.Font.js | 58 +++++++++++++++++- test/unit/visual/cases/webgl.js | 39 ++++++++++++ .../WebGL/textToModel/Extruded/000.png | Bin 0 -> 1859 bytes .../WebGL/textToModel/Extruded/metadata.json | 3 + .../WebGL/textToModel/Flat/000.png | Bin 0 -> 650 bytes .../WebGL/textToModel/Flat/metadata.json | 3 + 7 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/textToModel/Extruded/000.png create mode 100644 test/unit/visual/screenshots/WebGL/textToModel/Extruded/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/textToModel/Flat/000.png create mode 100644 test/unit/visual/screenshots/WebGL/textToModel/Flat/metadata.json 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 720b8d60e2..9e7d333492 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); @@ -154,6 +154,60 @@ function font(p5, fn) { return cmdContours.map((commands) => pathToPoints(commands, options)); } + 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 if (log) { console.log('There are', document.fonts.size, 'font-faces\n'); @@ -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); diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index f32f9c2f31..3bcea745de 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -492,4 +492,43 @@ 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); + p5.textAlign(p5.CENTER, p5.CENTER); + const geom = font.textToModel('p5*js', 0, 0, { + sampleFactor: 2 + }); + p5.background(255); + p5.normalMaterial(); + p5.rotateX(p5.PI*0.1); + p5.rotateY(p5.PI*0.1); + 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); + p5.textAlign(p5.CENTER, p5.CENTER); + const geom = font.textToModel('p5*js', 0, 0, { + extrude: 10, + sampleFactor: 2 + }); + p5.background(255); + p5.normalMaterial(); + p5.rotateX(p5.PI*0.1); + p5.rotateY(p5.PI*0.1); + p5.model(geom); + screenshot(); + }); + }); }); 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 0000000000000000000000000000000000000000..36b89c8020fc03843edbbc1c98c4efe3de959959 GIT binary patch literal 1859 zcmV-J2fX-+P)Px*{YgYYRA@u(n0-(b=N-pC9B|+(#MvN-hD0KJRZ2}#r^@km4aOM9xxA5@*P6+s zNopL8)lRUn>^js`#%Wrmi7#m+nT%zo8p#Wilu(x7S=5M0g>Yy_bYc`IOGQB5;&Gkb z?Oniwo{*zvGWYzod(S?<=lA)3f6s64F2wD2yZIiBkRZr+%*CK1-!Vf7IudkbloSY- z)+h-(;;=zSMl9VZEetv`O2UpfY|xPrOE*dj1L8=X(yOPJoMj%RH2sJD)*v~yzwouP zXuwV$pe1?Z|8HWzv9d-7(Z+<;MKt|04Z(4q@%AgI6E8q{A5?c?ul}7r|2B{Lfb^t&Mtff* z!hz}D+Lq;)DVGuv_z@~6{_EllmPm%m#Zm_)?pPE>A!qe5tVeQu64&Mj8~w}FFG%CV z)i~p;pt=)|c3|(`tjBclBI=wbD2n6$@-S+Cd73llW8lyP?DrXmKQ^m@ZS!`(Q;`TY z(-Ak{j&R_n0Xg-h)e#X^$?lV<;E{HOGz-H1Bzau;@9H9+RPgj&=}R_7Pgr$qAAxk<2p9DLA&r6d)x{SF$RKZNt?5@Wv*uspL6 zabLQAs{x!Z+sN|bC>|{s134EV*JM2VQ5Y=x5<7c+KxFxk9F>;npSP@i3+KFZi1~N< zFDJj?|r~b)Q1p1+1(<;8tRv|B}XM zD^RSZSSJ)9)Ft3};7!!{ChRY*(_?8(L2KPrt~!)m4JeNv@m5TK5S#56Cr)0o)k{KM98kx8l0vFwgZ(Vcqy027TVIAnr26x*^U5F+C8~iEwZ#qN$z2**7qIaxS+0n^Df3 z;*I-e@o4xHgsfTyAt5~eL^Q9>%pU-u-3$eyoe^8r7L;%&!b&&R!@s5RxeA=i6ljV- zSRX$$h$Y>QSiP9f-rq<;dJ=b!ea5@7e7MX|I<^mDx?y4`Vd0(pIP?NDk`s^S->>W_p8aE0j~J)sG_hu`#M@&2XXQ) z0ykjA5M*Vg6PxV?s7-~|PK1-wjPp_R%{$5WjHK54h3KC{wd)wyxqC?tyX|W79rPJR zsV2w%H>_=I^?8*#1d{b@t+_Z7HlZxN1fiE{H z=U&qcmPh=2#EA`Op)AWtwGHr43_|lX#9e6!hYY1>t~<_#^%=+~%8>s39z{`S;Va>Ju6I<_o=l%tepTlGqOzDB>OT?I4`E!V!`^P-()kNuPMDBVE zB6^7k?cqUFBH5?T5T9g}^0)qlI2`qT&RNYqB(=F x=t!WH3{;$;BY{#fP;r8e1WL(3#rfVi@*jJRZ8`V$!Yu#*002ovPDHLkV1oU{dw2i< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..87b3164a3aca0b90ee7b9c022868ccfd113dac2c GIT binary patch literal 650 zcmV;50(Jd~P)Px%L`g(JRA@u(Ry$4uF%;Z1sZvpKh};3V0c8$@L?UsRl-vNgLk>Yjm5OH3Y%;5n zV|)FH(JFe|L>t-9^LcO9i@xvs?gpNP5t*5b=E%$p>C6#xq>%#FT8)G)E^CeyOV?*}H~iEOs3(T&5`$sw#SeV!NJryAQ;avd)b0Q5a> zVG@D*Q|hTeQzRkeetR5TovKTmQ6dD+!r1iQ-;W%r+%O;K8F3E=0sMzw5U*{!kjqgb z3Yuip6%q-}hk^$Is7xZLr_Q)t5EABUL{w28PC!gCl4Uz_&X{iF+{p~6>jdkY5lu?Y zA~0n;5^D`Q;I<&%HQ@j-76=+5>Y9>eyD}7#iLm%okF(u;Z5dNqTP8%r7cO*Ya@djQ zh*b199!(MyQDuFOxIy8YHMj3m9B~sZD}L1t1;~#d)zNjF6a5aswV1OZA|UL}66YvL z1P9@8s?K*+&o`j1Yrd3WB!?0xMg)7@5CNGek(6r&i!za7LmUF0+FJY$F#xoctBIt7 z%US1hL_xDVbauU$Mr^{OsjEZ|xY%0vyijZTzDvRQy%xBfIvVY~0mNpF#D{i1-;%4Y z@i|~)Yg51Ku^O`=LV&7tZd=caNR7irq(;3Kd(Dv+s@BvrM{3k-vDX}Fp=wP{bEHPS k7JJQ+7OK|NyctLS06;m4{YasH{Qv*}07*qoM6N<$f-PAhdjJ3c literal 0 HcmV?d00001 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 From e91e894501e2474cce2a774ba2c5d35f0bbd9056 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 18 Dec 2024 14:28:02 -0500 Subject: [PATCH 3/5] Update test case alignment --- test/unit/visual/cases/webgl.js | 4 ++-- .../WebGL/textToModel/Extruded/000.png | Bin 1859 -> 1854 bytes .../WebGL/textToModel/Flat/000.png | Bin 650 -> 634 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 3bcea745de..0649b00f6b 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -500,7 +500,7 @@ visualSuite('WebGL', function() { '/unit/assets/Inconsolata-Bold.ttf' ); p5.textSize(20); - p5.textAlign(p5.CENTER, p5.CENTER); + p5.textAlign(p5.CENTER, p5.TOP); const geom = font.textToModel('p5*js', 0, 0, { sampleFactor: 2 }); @@ -518,7 +518,7 @@ visualSuite('WebGL', function() { '/unit/assets/Inconsolata-Bold.ttf' ); p5.textSize(20); - p5.textAlign(p5.CENTER, p5.CENTER); + p5.textAlign(p5.CENTER, p5.TOP); const geom = font.textToModel('p5*js', 0, 0, { extrude: 10, sampleFactor: 2 diff --git a/test/unit/visual/screenshots/WebGL/textToModel/Extruded/000.png b/test/unit/visual/screenshots/WebGL/textToModel/Extruded/000.png index 36b89c8020fc03843edbbc1c98c4efe3de959959..f23e88f4ba117beec55f9d2fb53eca7cee2c3842 100644 GIT binary patch delta 1826 zcmV+-2i^F?4!#bMF@N|;L_t(&L+zJ)P*nF7$3F`TA|MpGmZzfCa2LnNOddwN^0+pc z6f^C{sZO+Gn>4K^jkU2(I;qjhy-C_QQ=1M6Ej8pZ#i~p~V{{@;3Wl4&7ELiKf??69 zq>hqiQF*Q4vTl3t4O`9&*yy4@BHqXynkM=m;VDZg+OjG6+?>L zVuBD-B&0}?DiCU|AQd*?upvbPE?tluLy82cumOh+DH3q$g5(%dBuIq~IBZCffJ+x7 z$B-gHDr~@ELy81kx*$1*6bVvc0}gxBiWDjJw5eay6PS?~$&B1cUozVF|CZeph@xzz zNUq1qx*_v@u77&G4i~7EDcyQuZY0K#AgZcTrOeWwuO*`e)GK~fPz^$sw<3_GgPhNH z@bS7m2(_8|-o%f`8ayAth-oYDO>CLk}Y~Owx-v zDi9rF$rf;SVdE7;1^z}^MMWlrVsCy6UP6Hqf zm76&=wSN<8qOlatL;NULU*E;|{XhmkNmEX%I{$9d`H#?IyqaF-rdk28Y#gGlR8)uVnO_N}kube)4*H`u$eo&1eI$X&}2 z|DI3U+7jNlrwgIt4#Xk@_Ca|o-<7vvom-5se}9T!Dq5>(5SqR1O-50+QzRe6%FnM! zSz8}$+PNL@*VaY^iP&_2@P{P)_s3M!0AsEL2Xzy5bREPM%-vAal$d;!Lf`@^+{QhintE;U1!LW@4@XsSosO zkbe!*c^^bab~EucmXSe{7oTN|{kL$=1@vH})BsskjC1AZug0ly~^rdq5HW3`-la z@rE8^chBQ^JK_;4ZU97c*u`T&Hx_abS$}D?R^uiBG%>jO)u?y>gs%90|RGG^=TyOS;!I~e;h*8 z6vT@82!}HapqZ^BgE*Ggkk~R2;ls(g(|;gJkzF5RY05?1V_aN=ZB2Cx(vdeod)^f@ ziES5YKM`$gi!-sqw8?KG4G+?ew14phxHkr}fHV($r61wQM8v{x=!0a$!*2icYdG&e ziY4-&Z0Wn7oP@O~zut!IJpf5Ssu$sGI^t{D2zACgSI35(RIkXw^hpDB9*6h@hbKQrO*(8usM2o5X;jKkXU13WkCdYij_=H z-{7mB`q^=$rWYagvH>yHMSr@vla);3m(d2qu-2f$tf6n}B4r<|<=<(g(s~gauj*m+ zv-W-w^6%e*opZ6gItTIf9KS~l-CFuQH*>gYKC|!s*|0gO?vGL2Wu%35v)nzIZ;o4i zeTeI<7AaL!$un@`z`-sA1VoHGXH9i8(z~UQ9RuU980VuY4aT?;>VGmw2zL)sWaHDQ z^O_(xiqrPn$+-8afD;>yqoQm^HlM`0;~|75V-gLRHe4Lv`T)6oFO2sBU`!t| z<{oYf>qG5Z?+>DS8hPK-sJBQ;CW QBLDyZ07*qoM6N<$f@}_civR!s delta 1831 zcmV+?2iW+&4#N(RF@OC@L_t(&L+zM-P!#7K$3Gl!;48%0Ac%%UB70RzO;V@I@pcWy z7{|H1k($?<$)rhY9F5gZu(9kq)Ktc4TBV6EX(XA9Wu_X*3zC#jmf%^`h)IQTXhw8m z6emkXK;GhUo!#wSz=NKUqh>Pq{Iz?}KELPp`F?-TZ|^R|?SFQ=`5ugrAjo&j#h@eK zF+&JC5_Dvg6bP2qC(2)^KH%bcw;z*s+tEZQoWgew8 z{fGV5AUU?b@U^mNz)l{ZC3)ljZ*W6`6w4hXXPUiWzv9d-7(Z+<;MKt|04Z(4< zejl54l5L8lUk?WIQ}ZoUnDcxfWcd}!rDOzV4C$6E|9=T-_b0F>8X;0(OT!AZ67@F* zfXMPrBx#Sn#sgwXKgQnDH=s5OOI`+IRR$_f92ja_xCfDYe9lP!awwD?)G1+XQwp%K z2$8!61<~BHt=45SuL*ML9MA#PRkks1q+hc^_1FVXyw3 zKL0k4`G0`)qE7Cw<(Da!5)t?jDkuKy;tZBZhRVfK2PN)U6h$Ft^)akR za(oik<_8=7%hWGOYM3&Q>+=nX^Mbf^C8 zl^|Lp(gf%~{DaK<2&bzIYCGU^7j|AXK<2!JTGUEb=NHtcMDo?SNw9w$e9(iXBo(p! z4jP|7g!AbVW4{ovJhKpSU%Gy)0h}+}$nxSS9xWIHITs<VL>#O?)ix2hO_DqPt%xk$UlfW>oh`rf;YJ51*IKM+s=2ew0~I< zORPpv8i0!k%=IR@C$s{or3`RmDM-gTw@RP0)MRq2dpx->W_w?nTBFe#j%Rp4R#rA+ z`&j|p83jKHha@+`si}ysW*{6i&Ya&f;@I&z>f(B)eR4gInWmFJf0^E4Z10ppVlPZ~ zV<}5Rd@aM9fOeskWaGkWq~?9lW`8EjG8tvpY2z6aJb7<4+f$7W=ov$S+_Jd_=|nl) z7791^A)K6wn)x||_aPieMBJ2p6(D=J;=1E7&-G1V-S`~_ecrDi?lQ!>AG_TFf9{{1<41Wcpoe^8r z7L;%&!b&&R!@s5RxeA=i6ljV-SRX$$h$Y>QSiP9f-rq<;dJ=b!ea5@7e7MX|I<^mD zx?y4`Vd0(pIP?NVyZ_m*AfxlNh&I@pIN{( z(f6y#paHJ<@Tj7&Jo`FYTnBOTE&?}T#Smm=r4yU&1*lDh)=q?z(~R>`^UXWS_l%_0 z`-SMAL$&J|*SUL14!iAY@*VUUMX4so{x_^`YxQ}RIs}sSYpuCB5`Q+KEWHGwmtm3% zCicSEZiwjSW>XtmT=s#E$Q6G@Nv-Ez(+rkJ{CvcT4QHV&%Sg2i@K6jw^EAX=X$Xf5 zrDv`?&W80F$S2B>{{0?AU51H0Fs2(*csH{{S}E*W#5`fmwfz%Y?|$e01(Bb_WEV{7 zf#^%bm|OXCh@Jb#Jb&%gMCoco?s^L%dWi__;XzX(*{9ABpJbHsxBi7V9QA$9SNAB4JW4o9V*2Pno;>d_zH7tm0?#QsS z{T~aixg&vs34#Pl$w0*kIua-)0~IIeNT8GqRGgqAfl@M1aUg<@1WL(3#rfVi@*jJR VZ8`V$!Yu#*002ovPDHLkV1nG1d@TR~ diff --git a/test/unit/visual/screenshots/WebGL/textToModel/Flat/000.png b/test/unit/visual/screenshots/WebGL/textToModel/Flat/000.png index 87b3164a3aca0b90ee7b9c022868ccfd113dac2c..75114b5468077d69123179f81941fa0f59afbada 100644 GIT binary patch delta 596 zcmV-a0;~Os1^NV#F@H2kL_t(&L+w^OZUQk7-EF9-Qsofbf#e1$IZUL0l*3Tw21)L~ zAyTEHqS@%J^vKg-uf1k1R%ob@*E62?nX%ih>$>I+eyszUnToE+%mis%5m%%V18%KK z#1^M@MT)7b6zGanBDOfKD^g5dr9fAt60yZ;U6EqyDh0YCm4ApWPV0&kQ&%a_6{$pQ zaavad)E&M~J%g*KMKP081zx4d>+|m!i~<6%&D(OS8nf~Qf#fnxAXG%--aj9*3J7gt z-pzKIO~?fh6oW)$n8k&X@<0@=_Cid^ZY2ce5;6!R1?Hvw=>dOqk4g4P5CG;B7b7Ef z&>#?HnnkNJTz_M7_J*nf)z~li9|N-A9h=qqe`t&yAW-Yg9CeFJ%U<#x2zXu3y%h(08!}S`*dbJ1QvU+&i+tqXrcU$d;l2&qQqDi3|dRQ z&mvPEkM)f*Zij>QeQav!I@D0m**&78cBolHK(qj`oqr-D1|+6909^p#W=<^npK_B; z%53ey(M>h2lA&U96|vUR;SvR68`yd_1ejng<93vCdc|)4p$jq8b0BjN2-vp`t?Y%8 zrfieEaGIvSkdfO?ElY@!nFNw^uui10^MA|}g|>x~Jcpns0g?kYwMW@bO)Y((lEkZC zzp3JKzfJR=;WUiB;gHYT8TP{{o&^vc?P}B1eWOIp0tgDSPn0oFc*M^JNUjaDU%%HT iyj8p_ax3QV-dcYUXOY8)$C)4i0000jucDRXrnpONZ8`C=18%0jW(Jijf5>OTjj`pckB+IzkkEw>&Ioceprq`@cy4q z6_J;xpOJ`y28p+iyJ8*?2%_YOaX|zM4~5Q(Qt{2}>Y^}-Y__Y>jlZw3eBq8K}dmLPys!NjdkY5lu?YA~0n;5^D`Q;I<&%HQ@j-76=+5>Y9>eyD}7#iLm%okF(u;Z5dNq zTP8%r7cO*Ya@djQh*b199!(MyQDuFOxIy8YHMj3m9Di{WEh~Q24F$-LAJx%yoD=;H z!L^vPAtE5`&JyP+NCXGraH`ICRnIq|u4}%OVI+qVC`JT(+z`U0000 Date: Wed, 18 Dec 2024 14:37:25 -0500 Subject: [PATCH 4/5] Use normalize() in test --- test/unit/visual/cases/webgl.js | 6 ++++-- .../WebGL/textToModel/Extruded/000.png | Bin 1854 -> 1992 bytes .../WebGL/textToModel/Flat/000.png | Bin 634 -> 671 bytes 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 0649b00f6b..e503f80981 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -500,14 +500,15 @@ visualSuite('WebGL', function() { '/unit/assets/Inconsolata-Bold.ttf' ); p5.textSize(20); - p5.textAlign(p5.CENTER, p5.TOP); 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(); }); @@ -518,15 +519,16 @@ visualSuite('WebGL', function() { '/unit/assets/Inconsolata-Bold.ttf' ); p5.textSize(20); - p5.textAlign(p5.CENTER, p5.TOP); 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/WebGL/textToModel/Extruded/000.png b/test/unit/visual/screenshots/WebGL/textToModel/Extruded/000.png index f23e88f4ba117beec55f9d2fb53eca7cee2c3842..5751dac4725a84c0d151b3c7e84af81b28cfdccc 100644 GIT binary patch delta 1966 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 Rpd$bP002ovPDHLkV1m4FewP3M diff --git a/test/unit/visual/screenshots/WebGL/textToModel/Flat/000.png b/test/unit/visual/screenshots/WebGL/textToModel/Flat/000.png index 75114b5468077d69123179f81941fa0f59afbada..194da82c847a51956b24b77c756b9f5b3b727d91 100644 GIT binary patch delta 633 zcmeyxGM{yVVSS{hi(^QJ^V@K1KW0ObJ0>fG0t-9tG!`(Ql40YVVBzDH$FS3x&(YO& zYT88!waVYuzOCDvKb2$eRHd~-)qm2S)y{Ffy)E}M|GZY=okrq!Cu$onDRjRxai++J zE3-UrWJQ!OiP|;OY?rbzXEg8Yz7TKo%g2)3%k=Lr54cu-`t$GlXMgO1*F@e(QsVDE zukoO2b57j-e6>Q08kxg&2~+JFPdxq`eq&E8xym|D z*tAdD{rvu+WxpnMuv|WOGNk@0-zkTWbvNXj3u>}F67t(SwuP)-`PqWyQ}_ZEci9Xx1pSqc+J7_%N3>U6+TX$ol-@T_L?rYVIUeyKd z&~SB$TUg>%zDw+P-P;h|p6^@cF!%L6yS29{=PApJs~))u!QWm=GemFMBXsmbaQNRx z|CNKAu2(1bCTCn*a&Etd;4+i-8o{%U9qo*;eB-ia%9cZg?u5yojGq}JJ8w<7F_8fX NJYD@<);T3K0RRAaGo1hc delta 596 zcmV-a0;~O>1^NV#F@H2kL_t(&L+w^OZUQk7-EF9-Qsofbf#e1$IZUL0l*3Tw21)L~ zAyTEHqS@%J^vKg-uf1k1R%ob@*E62?nX%ih>$>I+eyszUnToE+%mis%5m%%V18%KK z#1^M@MT)7b6zGanBDOfKD^g5dr9fAt60yZ;U6EqyDh0YCm4ApWPV0&kQ&%a_6{$pQ zaavad)E&M~J%g*KMKP081zx4d>+|m!i~<6%&D(OS8nf~Qf#fnxAXG%--aj9*3J7gt z-pzKIO~?fh6oW)$n8k&X@<0@=_Cid^ZY2ce5;6!R1?Hvw=>dOqk4g4P5CG;B7b7Ef z&>#?HnnkNJTz_M7_J*nf)z~li9|N-A9h=qqe`t&yAW-Yg9CeFJ%U<#x2zXu3y%h(08!}S`*dbJ1QvU+&i+tqXrcU$d;l2&qQqDi3|dRQ z&mvPEkM)f*Zij>QeQav!I@D0m**&78cBolHK(qj`oqr-D1|+6909^p#W=<^npK_B; z%53ey(M>h2lA&U96|vUR;SvR68`yd_1ejng<93vCdc|)4p$jq8b0BjN2-vp`t?Y%8 zrfieEaGIvSkdfO?ElY@!nFNw^uui10^MA|}g|>x~Jcpns0g?kYwMW@bO)Y((lEkZC zzp3JKzfJR=;WUiB;gHYT8TP{{o&^vc?P}B1eWOIp0tgDSPn0oFc*M^JNUjaDU%%HT iyj8p_ax3QV-dcYUXOY8)$C)4i0000 Date: Fri, 20 Dec 2024 13:34:09 -0500 Subject: [PATCH 5/5] Make angles respect angleMode --- src/type/p5.Font.js | 13 +++++++-- test/unit/visual/cases/typography.js | 27 ++++++++++++++++++ .../000.png | Bin 0 -> 2205 bytes .../metadata.json | 3 ++ .../000.png | Bin 0 -> 2205 bytes .../metadata.json | 3 ++ 6 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in DEGREES mode/000.png create mode 100644 test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in DEGREES mode/metadata.json create mode 100644 test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in RADIANS mode/000.png create mode 100644 test/unit/visual/screenshots/Typography/textToPoints/Fonts point angles work in RADIANS mode/metadata.json diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index 9e7d333492..12d883a2a0 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -151,7 +151,7 @@ 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) { @@ -678,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') { @@ -720,12 +720,19 @@ 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++) { const length = path.getTotalLength() * (i / (totalPoints - 1)); points.push({ ...path.getPointAtLength(length), get angle() { - return path.getAngleAtLength(length) * 180 / Math.PI; + const angle = path.getAngleAtLength(length); + if (mode === DEGREES) { + return angle * 180 / Math.PI; + } else { + return angle; + } }, // For backwards compatibility get alpha() { 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/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 0000000000000000000000000000000000000000..6dbe5f2148ddbae619bd849b431dd1c187eb82af GIT binary patch literal 2205 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