<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Logo Animation Online Engineering]]></title><description><![CDATA[Logo Animation Online Engineering]]></description><link>https://logoanimation.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Logo Animation Online Engineering</title><link>https://logoanimation.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 28 Sep 2026 14:06:03 GMT</lastBuildDate><atom:link href="https://logoanimation.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Three.js: every mesh from ExtrudeGeometry sits at the same position (and the 8-line fix)]]></title><description><![CDATA[I thought I had three bugs. Per-shape rotation threw pieces off screen. My "explode" effect spread left to right instead of bursting outward. A left-to-right reveal ignored the artwork completely and ]]></description><link>https://logoanimation.hashnode.dev/three-js-every-mesh-from-extrudegeometry-sits-at-the-same-position-and-the-8-line-fix</link><guid isPermaLink="true">https://logoanimation.hashnode.dev/three-js-every-mesh-from-extrudegeometry-sits-at-the-same-position-and-the-8-line-fix</guid><category><![CDATA[ThreeJS]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[WebGL]]></category><dc:creator><![CDATA[logoanimation.online]]></dc:creator><pubDate>Thu, 03 Sep 2026 22:36:51 GMT</pubDate><content:encoded><![CDATA[<p>I thought I had three bugs. Per-shape rotation threw pieces off screen. My "explode" effect spread left to right instead of bursting outward. A left-to-right reveal ignored the artwork completely and animated shapes in whatever order the SVG happened to list them.</p>
<p>It was one bug. If you build meshes with <code>THREE.ExtrudeGeometry</code>, you might have it too.</p>
<h2>The setup</h2>
<p>I'm building a logo animator that runs in the browser. It takes an uploaded SVG, gives every path its own mesh so elements can animate independently, then extrudes each one:</p>
<pre><code class="language-js">function extrudeShape(shape, depthLocal, bevelLocal, smooth) {
  let geo = new THREE.ExtrudeGeometry(shape, {
    depth: depthLocal,
    curveSegments: 4 + smooth * 3,
    bevelEnabled: bevelLocal &gt; 0.001,
    bevelThickness: bevelLocal * 0.7,
    bevelSize: bevelLocal * 0.55,
    bevelSegments: Math.max(1, Math.min(6, smooth))
  });
  geo = toCreasedNormals(geo);
  flattenCaps(geo);
  geo.translate(0, 0, -depthLocal / 2);
  return geo;
}
</code></pre>
<p>Each shape becomes a mesh, each mesh goes into a group, and the group gets scaled to a fixed design size. It renders beautifully. That's the problem.</p>
<h2>What I missed</h2>
<p><code>ExtrudeGeometry</code> bakes the path's coordinates into the vertex data.</p>
<p>A path sitting at <code>x = 800</code> in your SVG does not give you a mesh at <code>x = 800</code>. You get a mesh at the origin whose vertices are all clustered around <code>x = 800</code>. The position you care about lives in the geometry, not in <code>mesh.position</code>.</p>
<p>So every mesh in my group reported the same position, usually <code>(0, 0, 0)</code>, and everything still looked correct on screen.</p>
<p>Three things broke because of it. None of them announced itself.</p>
<p><strong>Rotation pivots about the wrong point.</strong> <code>mesh.rotation</code> spins around the mesh's origin. With the coordinates baked in, that origin is the SVG origin, which can sit hundreds of units away from the shape. Setting <code>rotation.z</code> doesn't spin the shape. It swings the shape around the corner of the artboard on a long invisible arm. I had no per-element flip or tumble at all, because every attempt threw pieces out of frame and I'd assumed I was getting the maths wrong.</p>
<p><strong>Anything derived from position comes out identical.</strong> I had this:</p>
<pre><code class="language-js">const dir = ch.position.clone();          // outward direction from centre
ch.userData.dir = dir.normalize();
</code></pre>
<p>If every position is the same, every direction is the same. My explode effect had a fallback for that case, which fans the pieces around a circle by index, and it had been taking that path the whole time. The result read as a spread rather than a burst. I spent an embarrassing amount of time adjusting easing curves for something that was never a timing problem.</p>
<p><strong>Spatial sorting collapses to document order.</strong> Sorting shapes left to right means sorting by <code>position.x</code>, which here is a list of identical numbers. Any reasonable comparator breaks ties by index:</p>
<pre><code class="language-js">kids.map((_, i) =&gt; i).sort((a, b) =&gt; val(a) - val(b) || a - b);
</code></pre>
<p>When <code>val</code> returns the same number for everything, the first term is always zero and the tie-break is all that's left. You get SVG document order, which an exporter can write in any sequence it likes. The comparator is fine. It's being fed a constant.</p>
<h2>How I actually found it</h2>
<p>I dumped the computed metadata for a test logo with 16 paths.</p>
<p>All 16 shared one position. Worse, three ranks I compute from different spatial axes (<code>ox</code> for left to right, <code>oy</code> for bottom to top, <code>oc</code> for centre outward) came back exactly equal to <code>si / (n - 1)</code>, where <code>si</code> is the array index.</p>
<p>That's the giveaway. Three measurements taken along different axes have no business agreeing with each other, let alone matching an array index to the decimal. When independent measures line up perfectly, they aren't measuring anything.</p>
<h2>The fix</h2>
<p>Move each geometry to its own centroid, then push the mesh position out by the same amount:</p>
<pre><code class="language-js">logoGroup.children.forEach(ch =&gt; {
  if (!ch.geometry?.attributes?.position) return;
  ch.geometry.computeBoundingBox();
  const bb = ch.geometry.boundingBox;
  const gx = (bb.min.x + bb.max.x) / 2;
  const gy = (bb.min.y + bb.max.y) / 2;
  ch.geometry.translate(-gx, -gy, 0);
  ch.position.x += gx;
  ch.position.y += gy;
});
</code></pre>
<p>The two moves cancel, so the render comes out pixel for pixel identical. Nothing about the image changes. What changes is that <code>position</code> now tells you where the shape is, rotation pivots about the shape's own centre, and directions derived from position are real.</p>
<p>One thing to watch. After re-centring, every shape's bounding box is centred on zero, so it's no use for asking where a shape sits. Grab the centroid into <code>userData</code> while you still have it.</p>
<h2>Two other things that bit me</h2>
<p><strong>Source units are not design units.</strong> My <code>home</code> values are in source SVG units, so a 1036 unit artboard and a 200 unit one are on completely different scales. An offset of <code>12</code> is a fifth of the logo on one and 1.4% on the other, which is invisible. So positional amplitudes get written against a ratio:</p>
<pre><code class="language-js">const u = maxDim / LOGO_WORLD;   // source units per design unit
// then: 18 * u  moves the same visible distance on any artboard
</code></pre>
<p>One older preset still uses a bare <code>12</code>. It's noticeably weaker than the others on large artboards. I've left it there as a reminder.</p>
<p><strong>Don't shuffle with <code>Math.random()</code>.</strong> A randomised reveal needs a value that stays put for the life of the object. <code>Math.random()</code> re-rolls every frame, so the animation boils and pieces flicker between orderings instead of revealing. A cheap hash of the index does the job and stays stable:</p>
<pre><code class="language-js">const hash = (i) =&gt; {
  const x = Math.sin(i * 127.1 + 311.7) * 43758.5453;
  return x - Math.floor(x);
};
</code></pre>
<h2>The general point</h2>
<p>Geometry space and object space are different things, and <code>ExtrudeGeometry</code>, <code>TextGeometry</code> and <code>SVGLoader</code> all hand you meshes where the interesting position sits in the wrong one. There's no visual glitch to catch it by, which is why it survives so long. The render looks right while every feature derived from position stops meaning anything.</p>
<p>If a set of independent spatial measures ever agree exactly, go and check that they're measuring anything at all.</p>
<hr />
<p><em>I build <a href="https://logoanimation.online/">logoanimation.online</a>, a free browser-based logo animator. This one came out of the 3D side, which extrudes real geometry rather than generating anything. Happy to answer Three.js questions in the comments.</em></p>
]]></content:encoded></item></channel></rss>