Setting the file. One moment.
Logo · Frost Sequence Camera Orbit · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page 10.10
Three Mesh BVH LICENSE
Number 10.64
Position 64 of 76
Type TypeScript
Size 12 KB
Lines 302 source/src/shape/ logo.ts
TypeScript · 302 lines · 12 KB
{ MeshBVH }
from
"three-mesh-bvh"
;
9 import type { LogoSDF } from "./sdf" ;
10
11 export interface LogoParams {
12 /** overall width of the mark in world units */
13 width : number ;
14 /** extrusion depth as a fraction of the width */
15 depth : number ;
16 bevelThickness : number ;
17 bevelSize : number ;
18 bevelOffset : number ;
19 bevelSegments : number ;
20 curveSegments : number ;
21 /** rounding radius applied to the 2D outline corners (fraction of the width, 0 = keep hard corners) */
22 cornerRadius : number ;
23 /** normals are hard above this angle (degrees) and smooth below it */
24 creaseAngle : number ;
25 /** SDF grid resolution */
26 sdfRes : number ;
27 }
28 export const DEFAULT_LOGO_PARAMS : LogoParams = {
29 width: 2.6 ,
30 depth: 0.22 ,
31 bevelThickness: 0.06 ,
32 bevelSize: 0.05 ,
33 bevelOffset: 0 ,
34 bevelSegments: 5 ,
35 curveSegments: 24 ,
36 cornerRadius: 0.03 ,
37 creaseAngle: 40 ,
38 sdfRes: 64 ,
39 };
40
41 /** Round the corners of a closed polygon: cut each sharp corner back by `r` and bridge it with a quadratic curve. */
42 function roundPolygon ( pts : THREE . Vector2 [], r : number ) : THREE . Path {
43 // Closed paths repeat their first point. Keeping that duplicate makes both end
44 // segments zero-length and drops the first real corner (e.g. the H's left foot).
45 pts = pts. filter (( p , i ) => i === 0 || p. distanceToSquared (pts[i - 1 ]) > 1e-18 );
46 if (pts. length > 1 && pts[ 0 ]. distanceToSquared (pts[pts. length - 1 ]) < 1e-18 )
47 pts = pts. slice ( 0 , - 1 );
48 const n = pts. length ;
49 const path = new THREE . Path ();
50 if (r <= 0 || n < 3 ) {
51 path. moveTo (pts[ 0 ].x, pts[ 0 ].y);
52 for ( let i = 1 ; i < n; i ++ ) path. lineTo (pts[i].x, pts[i].y);
53 path. closePath ();
54 return path;
55 }
56 const segs : { a : THREE . Vector2 ; c : THREE . Vector2 ; b : THREE . Vector2 ; sharp : boolean }[] = [];
57 for ( let i = 0 ; i < n; i ++ ) {
58 const p = pts[i],
59 prev = pts[(i - 1 + n) % n],
60 next = pts[(i + 1 ) % n];
61 const d1 = prev. clone (). sub (p),
62 d2 = next. clone (). sub (p);
63 const l1 = d1. length (),
64 l2 = d2. length ();
65 if (l1 < 1e-9 || l2 < 1e-9 ) continue ;
66 const ang = Math. acos ( THREE .MathUtils. clamp (d1. dot (d2) / (l1 * l2), - 1 , 1 ));
67 const sharp = ang < THREE .MathUtils. degToRad ( 168 );
68 const rr = Math. min (r, l1 * 0.45 , l2 * 0.45 );
69 segs. push ({
70 a: p. clone (). addScaledVector (d1. normalize (), sharp ? rr : 0 ),
71 c: p,
72 b: p. clone (). addScaledVector (d2. normalize (), sharp ? rr : 0 ),
73 sharp,
74 });
75 }
76 segs. forEach (( s , i ) => {
77 if (i === 0 ) path. moveTo (s.a.x, s.a.y);
78 else path. lineTo (s.a.x, s.a.y);
79 if (s.sharp) path. quadraticCurveTo (s.c.x, s.c.y, s.b.x, s.b.y);
80 });
81 path. closePath ();
82 return path;
83 }
84
85 /** A relative or root-relative path, or an inline SVG data URI: never a scheme or protocol-relative host. */
86 const isProjectAssetUrl = ( url : string ) =>
87 / ^ data:image \/ svg \+ xml [,;] / i . test (url) || ! / ^ (?: [a-z][a-z0-9+.-] * : | [ \\ /] {2} )/ i . test (url. trim ());
88
89 /** The SVG's shapes fitted to `P.width`, centred, y-up (world units). */
90 export async function loadLogoShapes (
91 url : string ,
92 params : Partial < LogoParams > = {},
93 ) : Promise < THREE . Shape []> {
94 const P = { ... DEFAULT_LOGO_PARAMS , ... params };
95 if ( ! isProjectAssetUrl (url)) throw new Error ( "The logo must be a project asset path, not " + url);
96 const text = await fetch (url). then (( r ) =>
97 r.ok ? r. text () : Promise . reject ( new Error ( "no logo" )),
98 );
99 const data = new SVGLoader (). parse (text);
100 const raw : THREE . Shape [] = [];
101 for ( const p of data.paths) raw. push ( ... (p as any ). toShapes ( true ));
102 if ( ! raw. length ) return [];
103 // svg units -> world units (fit the width), y-up
104 const box = new THREE . Box2 ();
105 for ( const s of raw) for ( const pt of s. getPoints ( 8 )) box. expandByPoint (pt);
106 const size = box. getSize ( new THREE . Vector2 ()),
107 centre = box. getCenter ( new THREE . Vector2 ());
108 const k = P .width / Math. max (size.x, size.y);
109 const tx = ( v : THREE . Vector2 ) => new THREE . Vector2 ((v.x - centre.x) * k, - (v.y - centre.y) * k);
110 return raw. map (( s ) => {
111 const shape = new THREE . Shape (s. getPoints ( P .curveSegments). map (tx));
112 shape.holes = s.holes. map (( h ) => new THREE . Path (h. getPoints ( P .curveSegments). map (tx)));
113 return shape;
114 });
115 }
116
117 /**
118 * Round outline corners (radius `P.cornerRadius * ref`), extrude with a bevel (depth and bevel as fractions of
119 * `ref`), merge, crease normals and centre. `ref` is the mark's width for the logo, the font size for a headline.
120 */
121 export function extrudeShapes (
122 raw : THREE . Shape [],
123 P : LogoParams ,
124 ref : number ,
125 ) : THREE . BufferGeometry {
126 const r = P .cornerRadius * ref;
127 const rounded : THREE . Shape [] = unionOutlines (raw, P .curveSegments). map (( s ) => {
128 const outer = roundPolygon (s. getPoints ( P .curveSegments), r);
129 const shape = new THREE . Shape (outer. getPoints ( P .curveSegments));
130 shape.holes = s.holes. map (( h ) => roundPolygon (h. getPoints ( P .curveSegments), r));
131 return shape;
132 });
133 const shapes = unionOutlines (rounded, P .curveSegments);
134 const bevel = P .bevelThickness > 0 || P .bevelSize > 0 ;
135 let geo : THREE . BufferGeometry = new THREE . ExtrudeGeometry (shapes, {
136 depth: P .depth * ref,
137 bevelEnabled: bevel,
138 bevelThickness: P .bevelThickness * ref,
139 bevelSize: P .bevelSize * ref,
140 bevelOffset: P .bevelOffset * ref,
141 bevelSegments: Math. max ( 1 , Math. round ( P .bevelSegments)),
142 curveSegments: Math. max ( 2 , Math. round ( P .curveSegments)),
143 });
144 geo. center ();
145 geo = mergeVertices (geo, 1e-5 );
146 geo = toCreasedNormals (geo, THREE .MathUtils. degToRad ( P .creaseAngle));
147 geo. computeBoundingBox ();
148 // The broad caps are planes. Averaging adjacent bevel faces into their
149 // normals makes Earcut's long triangles show through reflective materials.
150 // Keep the end rings tangent to the caps, so the rounded bevel joins smoothly
151 // without tilting the entire cap. Positions/topology and the SDF stay identical.
152 if ( P .bevelThickness > 0 && P .bevelSize > 0 ) {
153 const position = geo. getAttribute ( "position" ),
154 normal = geo. getAttribute ( "normal" );
155 const { min , max } = geo.boundingBox ! ;
156 const epsilon = Math. max ((max.z - min.z) * 1e-6 , 1e-8 );
157 for ( let i = 0 ; i < position.count; i ++ ) {
158 const z = position. getZ (i);
159 if (Math. abs (z - min.z) <= epsilon) normal. setXYZ (i, 0 , 0 , - 1 );
160 else if (Math. abs (z - max.z) <= epsilon) normal. setXYZ (i, 0 , 0 , 1 );
161 }
162 normal.needsUpdate = true ;
163 }
164 return geo;
165 }
166
167 /** Half extents of a centred geometry. */
168 export function halfExtents ( geo : THREE . BufferGeometry ) {
169 if ( ! geo.boundingBox) geo. computeBoundingBox ();
170 const half = new THREE . Vector3 ();
171 geo.boundingBox ! . getSize (half). multiplyScalar ( 0.5 );
172 return half;
173 }
174
175 /** The texture samples [-sampleBound, sampleBound]^3; bound keeps the shared simulation domain and distance range. */
176 export function makeLogoSDF (
177 data3 : Float32Array ,
178 n : number ,
179 bound : number ,
180 thickness : number ,
181 sampleBound = bound,
182 ) : LogoSDF {
183 const range = bound;
184 // Sampling can be tight per shape while erosion/particles keep a common world domain.
185 const sampleDomain = { value: sampleBound };
186 const tex = new THREE . Data3DTexture (data3, n, n, n);
187 tex.format = THREE .RedFormat;
188 tex.type = THREE .FloatType;
189 tex.minFilter = tex.magFilter = THREE .LinearFilter;
190 tex.wrapS = tex.wrapT = tex.wrapR = THREE .ClampToEdgeWrapping;
191 tex.needsUpdate = true ;
192 const sample = ( x : number , y : number , z : number ) => {
193 const bound = sampleDomain.value;
194 const fx = (x / ( 2 * bound) + 0.5 ) * n - 0.5 ,
195 fy = (y / ( 2 * bound) + 0.5 ) * n - 0.5 ,
196 fz = (z / ( 2 * bound) + 0.5 ) * n - 0.5 ;
197 const ix = Math. max ( 0 , Math. min (n - 2 , Math. floor (fx))),
198 iy = Math. max ( 0 , Math. min (n - 2 , Math. floor (fy))),
199 iz = Math. max ( 0 , Math. min (n - 2 , Math. floor (fz)));
200 const tx2 = Math. max ( 0 , Math. min ( 1 , fx - ix)),
201 ty = Math. max ( 0 , Math. min ( 1 , fy - iy)),
202 tz = Math. max ( 0 , Math. min ( 1 , fz - iz));
203 const v = ( a : number , b : number , c : number ) => data3[a + b * n + c * n * n];
204 const c00 = v (ix, iy, iz) * ( 1 - tx2) + v (ix + 1 , iy, iz) * tx2,
205 c10 = v (ix, iy + 1 , iz) * ( 1 - tx2) + v (ix + 1 , iy + 1 , iz) * tx2;
206 const c01 = v (ix, iy, iz + 1 ) * ( 1 - tx2) + v (ix + 1 , iy, iz + 1 ) * tx2,
207 c11 = v (ix, iy + 1 , iz + 1 ) * ( 1 - tx2) + v (ix + 1 , iy + 1 , iz + 1 ) * tx2;
208 const c0 = c00 * ( 1 - ty) + c10 * ty,
209 c1 = c01 * ( 1 - ty) + c11 * ty;
210 const outside = Math. abs (x) > bound || Math. abs (y) > bound || Math. abs (z) > bound;
211 const distance = (c0 * ( 1 - tz) + c1 * tz) * 2 * range - range;
212 return outside
213 ? Math. max (
214 distance,
215 Math. hypot (
216 Math. max (Math. abs (x) - bound, 0 ),
217 Math. max (Math. abs (y) - bound, 0 ),
218 Math. max (Math. abs (z) - bound, 0 ),
219 ),
220 )
221 : distance;
222 };
223 return { texture: tex, res: n, bound, range, thickness, data: data3, sample, sampleDomain };
224 }
225
226 /**
227 * Voxel SDF over [-bound, bound]^3 at `res` per axis: BVH closest-point distance, signed by z-ray parity.
228 * Face normals give wrong signs near edges and bevels, so those voxels read solid and never erode.
229 */
230 export function voxelize (
231 geo : THREE . BufferGeometry ,
232 bound : number ,
233 res : number ,
234 sampleBound = bound,
235 ) : LogoSDF {
236 const half = halfExtents (geo);
237 const thickness = Math. min (half.x, half.y, half.z);
238 const bvh = new MeshBVH (geo);
239 const n = Math. max ( 16 , Math. round (res)),
240 data3 = new Float32Array (n * n * n);
241 const p = new THREE . Vector3 ();
242 const range = bound;
243 const voxel = ( 2 * sampleBound) / n;
244 const maxDist = Math. min (range, Math. max (thickness * 2.5 , voxel * 14 ));
245 const box = geo.boundingBox ! ;
246 const maxDistSquared = maxDist * maxDist;
247 const hit : any = {};
248 const ray = new THREE . Ray ( new THREE . Vector3 (), new THREE . Vector3 ( 0 , 0 , 1 ));
249 const inside = new Uint8Array (n);
250 for ( let y = 0 ; y < n; y ++ )
251 for ( let x = 0 ; x < n; x ++ ) {
252 // one ray per column, nudged off the grid so it never grazes an edge exactly
253 const cx = ((x + 0.5 ) / n - 0.5 ) * 2 * sampleBound + voxel * 0.013 ,
254 cy = ((y + 0.5 ) / n - 0.5 ) * 2 * sampleBound + voxel * 0.017 ;
255 ray.origin. set (cx, cy, - sampleBound - 1 );
256 const hits = bvh. raycast (ray, THREE .DoubleSide) as { distance : number }[];
257 const crossings = (hits as any [])
258 . map (( h ) => ({ z: h.distance - sampleBound - 1 , delta: h.face.normal.z < 0 ? 1 : - 1 }))
259 . sort (( a , b ) => a.z - b.z);
260 let k = 0 ,
261 winding = 0 ;
262 for ( let z = 0 ; z < n; z ++ ) {
263 const pz = ((z + 0.5 ) / n - 0.5 ) * 2 * sampleBound;
264 while (k < crossings. length && crossings[k].z < pz) winding += crossings[k ++ ].delta;
265 inside[z] = Number (winding !== 0 );
266 }
267 for ( let z = 0 ; z < n; z ++ ) {
268 p. set (
269 ((x + 0.5 ) / n - 0.5 ) * 2 * sampleBound,
270 ((y + 0.5 ) / n - 0.5 ) * 2 * sampleBound,
271 ((z + 0.5 ) / n - 0.5 ) * 2 * sampleBound,
272 );
273 // A thin extrusion occupies little of the cube. The box distance is a
274 // lower bound: skip BVH work only when the capped query cannot find a hit.
275 const dx = Math. max (box.min.x - p.x, 0 , p.x - box.max.x);
276 const dy = Math. max (box.min.y - p.y, 0 , p.y - box.max.y);
277 const dz = Math. max (box.min.z - p.z, 0 , p.z - box.max.z);
278 const res2 =
279 dx * dx + dy * dy + dz * dz > maxDistSquared
280 ? null
281 : bvh. closestPointToPoint (p, hit, 0 , maxDist);
282 let d = res2 ? res2.distance : range;
283 if (inside[z]) d = - d;
284 data3[x + y * n + z * n * n] = Math. min ( 1 , Math. max ( 0 , (d + range) / ( 2 * range)));
285 }
286 }
287 return makeLogoSDF (data3, n, bound, thickness, sampleBound);
288 }
289
290 /** The experiment's original entry point: SVG -> extruded geometry + its own SDF. */
291 export async function loadLogo (
292 url : string ,
293 params : Partial < LogoParams > = {},
294 ) : Promise <{ sdf : LogoSDF ; geometry : THREE . BufferGeometry } | undefined > {
295 const P = { ... DEFAULT_LOGO_PARAMS , ... params };
296 const shapes = await loadLogoShapes (url, P );
297 if ( ! shapes. length ) return undefined ;
298 const geo = extrudeShapes (shapes, P , P .width);
299 const half = halfExtents (geo);
300 const bound = Math. max (half.x, half.y, half.z) * 1.15 ;
301 return { sdf: voxelize (geo, bound, P .sdfRes), geometry: geo };
302 }