<?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[Surface Detail]]></title>
        <description><![CDATA[Posts from Liam Egan]]></description>
        <link>https://www.surface-detail.com</link>
        <image>
            <url>https://www.surface-detail.com/favicon.ico</url>
            <title>Surface Detail</title>
            <link>https://www.surface-detail.com</link>
        </image>
        <generator>RSS for Node</generator>
        <lastBuildDate>Fri, 08 May 2026 11:09:49 GMT</lastBuildDate>
        <atom:link href="https://www.surface-detail.com/api/rss" rel="self" type="application/rss+xml"/>
        <pubDate>Fri, 08 May 2026 11:09:49 GMT</pubDate>
        <copyright><![CDATA[2026 Surface Detail]]></copyright>
        <language><![CDATA[en]]></language>
        <managingEditor><![CDATA[Surface Detail]]></managingEditor>
        <webMaster><![CDATA[Surface Detail]]></webMaster>
        <ttl>60</ttl>
        <item>
            <title><![CDATA[Waves and oscillators]]></title>
            <description><![CDATA[<p>There's a particular quality to water. The way a single disturbance spreads outward, fades, reflects off the edges and comes back quieter than it left, the way crossing ripples interfere with each other. It feels complex. Simulating it feels like it should be difficult.</p>
<p>It isn't. Three numbers per point. One rule about neighbours. That's the whole thing, the rest is just iteration.</p>
<h3>Part 1: One oscillator</h3>
<p>Start with a single point with three properties:</p>
<ul><li><code>height</code> - how far it is from its resting position</li><li><code>speed</code> - how fast it's moving</li><li><code>acceleration</code> - how hard it's being pushed</li></ul>
<p>Each frame, we apply one rule: if the point is displaced from rest, pull it back. The further it's displaced, the harder the pull. This is Hooke's law. This is the same principle behind a mass on a spring, a pendulum and a plucked guitar string.</p>
<blockquote>Aside: Yes math fold, while they all share the same oscillatory DNA, a pendulum actually follows <code>F = -mg * sin(θ)</code>. For small angles, <code>sin(θ) approx θ</code>, which is why we can treat it as a linear spring in most simulations (certainly for this simple demonstration)!</blockquote>
<p>In code, it's a single multiply:</p>
<pre><code>acceleration = -elasticity * height;</code></pre>
<p>That minus sign is doing all the work. If <code>height</code> is positive (point is above rest), acceleration is negative - it gets pushed down. If <code>height</code> is negative, acceleration flips positive - pushed up. The point always gets pulled back toward zero. The magnitude of <code>elasticity</code> controls how aggressively.</p>
<p>Then we just integrate - add acceleration to speed, add speed to position:</p>
<pre><code>speed += acceleration;
height += speed;</code></pre>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/04a99338d3ba2d4f53ceba1ade6dee179b795baf-1000x628.png?w=800" alt="A diagram of a vertical spring system against a gray background. A blue coiled spring is anchored to a ceiling. Below the spring, a dashed circle labeled rest indicates the equilibrium position. A solid blue circular weight is attached to the spring and pulled down below the rest position. A vertical bracket to the right labels this displacement as height. On the left, a blue upward-pointing arrow is labeled with the formula $-k \cdot \text{height}$, representing the restorative force, where k is the spring constant labelled next to the coils." />
<p>Left alone, this rings forever: the point overshoots, gets pulled back, overshoots the other way. It never settles because nothing is bleeding the energy away. Add a friction term proportional to speed and we get a result that starts to look as expected:</p>
<pre><code>acceleration = -elasticity * height - friction * speed;
speed += acceleration;
height += speed;</code></pre>
<p>Now every frame, a little speed is subtracted. Fast motion loses energy faster than slow motion. The oscillation decays.</p>
<p>Kick it, and it oscillates back to rest. That's the whole oscillator - three lines of maths.</p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/dPpvgMd">https://codepen.io/shubniggurath/pen/dPpvgMd</a></p>
<pre><code>const canvas = document.getElementById(&quot;c&quot;);
const ctx = canvas.getContext(&quot;2d&quot;);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// How aggressively the point is pulled back toward rest.
// Higher = snappier, faster oscillation.
const ELASTICITY = 0.004;

// How much energy is bled off per frame, proportional to current speed.
// Higher = settles quickly. Lower = rings for longer.
const FRICTION = 0.03;

const point = { height: 0, speed: 0, acceleration: 0 };

canvas.addEventListener(&quot;click&quot;, (e) =&gt; {
  // Distance from the baseline — clicking far from centre gives a bigger kick,
  // and the sign determines direction (above baseline kicks up, below kicks down).
  const distFromCentre = e.clientY - canvas.height / 2;
  point.speed = distFromCentre * 0.08;
});

function loop() {
  const baseline = canvas.height / 2;

  ctx.fillStyle = &quot;#0f0f14&quot;;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Hooke's law + damping:  a = -k·x - c·v
  // The first term pulls the point back toward zero.
  // The second term resists motion — the faster it moves, the more drag.
  point.acceleration = -ELASTICITY * point.height - FRICTION * point.speed;
  point.speed += point.acceleration;
  point.height += point.speed;

  ctx.fillStyle = &quot;#5b8dee&quot;;
  ctx.beginPath();
  ctx.arc(canvas.width / 2, baseline + point.height, 10, 0, Math.PI * 2);
  ctx.fill();

  requestAnimationFrame(loop);
}

loop();</code></pre>
<p>This is a linear spring, using a basic, linear implementation of <a href="https://en.wikipedia.org/wiki/Hooke's_law">Hooke's law</a>.</p>
<p>This is the only concept in the post that requires any real thought. Everything from here is just asking: what if there were more of these? What if they could see each other?</p>
<p>The two constants - <code>elasticity</code> and <code>friction</code> - control the character of the motion. High elasticity, low friction: fast, ringing. Low elasticity, high friction: slow, sluggish. Most of the aesthetic tuning in the final surface comes down to these two numbers.</p>
<h3>Part 2: Two oscillators</h3>
<p>Now put two points side by side. They have their own heights and speeds, but we're going to let them talk to each other.</p>
<p>The rule: each point is attracted not just toward zero, but toward its neighbour's position. If the neighbour is above, it pulls up. If it's below, it pulls down. The strength of that pull is the coupling:</p>
<pre><code>acceleration =
  -elasticity * height +
  (neighbourHeight - height) * coupling -
  friction * speed;</code></pre>
<p>Kick one point, and watch the other respond. Energy transfers across the gap. The displaced point pulls its neighbour toward it; the neighbour resists and eventually pulls back. They trade energy back and forth, both gradually settling.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/8df57ca576271bab7e527e84f0669b02a377b794-1000x628.png?w=800" alt="A diagram showing two connected weights, A and B, illustrating the interaction between restoring forces and coupling forces in a multi-body system." />
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/WbGRLRY">https://codepen.io/shubniggurath/pen/WbGRLRY</a></p>
<p>This is the core mechanism. Everything that follows is this rule, repeated.</p>
<h3>Part 3: The chain</h3>
<p>Extend to N points. Each one follows the same rule, now applied on both sides:</p>
<pre><code>acceleration =
  -elasticity * height +
  (leftHeight - height) * coupling +
  (rightHeight - height) * coupling -
  friction * speed;</code></pre>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/RNGpGgL">https://codepen.io/shubniggurath/pen/RNGpGgL</a></p>
<p>Make sure to open the control panel and increase the number of oscillators. <a href="https://codepen.io/shubniggurath/pen/RNGpGgL">Click here</a> to view the full code, you can see that it’s not a significant amount more than the original example.</p>
<p>A few things to notice as you play with it:</p>
<p><strong>Propagation speed</strong> isn't fixed; instead, it depends on the ratio of coupling strength to the implicit mass of each point (which here is 1). Higher coupling, faster, wider wave.</p>
<p><strong>Reflections</strong> happen at the ends because the boundary points have no second neighbour to pull from. The wave hits the wall and bounces back in phase. This reflection is a free side effect of how the algorithm works.</p>
<p><strong>Second-neighbour coupling</strong> might be worth adding, depending on your requirements. If each point also looks two steps away - at half the coupling strength - the wave shape smooths out considerably. It's a small change that makes a visible difference:</p>
<pre><code>acceleration =
  -elasticity * height +
  (left1Height - height) * coupling +
  (right1Height - height) * coupling +
  (left2Height - height) * (coupling / 2) +
  (right2Height - height) * (coupling / 2) -
  friction * speed;</code></pre>
<p>The second-neighbour term rounds off the peaks and gives the wave a more organic shape. You can see how you might expand this out to N neighbours.</p>
<h3>Part 4: From dots to surface</h3>
<h4>4.1 - The naive line</h4>
<p>The physics hasn't changed. The only thing changing now is how we draw.</p>
<p>Instead of individual shapes, treat each point's `height` as a vertical offset at a fixed x-position. Connect them with straight lines, close the path downward, and fill it. You've got a surface.</p>
<pre><code>ctx.beginPath();
ctx.moveTo(0, baseline);
points.forEach((point, i) =&gt; {
  ctx.lineTo(segWidth * i, baseline + point.height);
});
ctx.lineTo(width, baseline);
ctx.lineTo(width, height);
ctx.lineTo(0, height);
ctx.closePath();
ctx.fill();</code></pre>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/vEXxyKz">https://codepen.io/shubniggurath/pen/vEXxyKz</a></p>
<p>It works. And honestly, at low N it's quite legible - you can see the individual oscillators as corners in the line. But it looks mechanical. As N increases and the segments get shorter, it starts to approach smooth, but you're paying for physics on every point. There must be a better way.</p>
<h4>4.2 - Smoothing with beziers</h4>
<p>Let's smooth things out a bit. Instead of just adding additional points, we can use the points we have to draw bezier curves.</p>
<p>The trick: use the midpoint between each pair of adjacent points as the bezier anchor, and let the points themselves act as control points. The curve passes smoothly through every midpoint, with the shape of each segment influenced by the points on either side.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/448cef6f16dd4b0f3fc97324382a33cea7faae5e-1000x628.png?w=800" alt="A diagram of a quadratic Bézier curve showing the relationship between two gray anchor points and a central blue control point that influences the curve's shape." />
<pre><code>ctx.beginPath();
ctx.moveTo(0, baseline);
points.forEach((point, i) =&gt; {
  const p1 = {
    x: segWidth * (i - 1),
    y: baseline + (points[i - 1]?.height ?? 0),
  };
  const p2 = { x: segWidth * i, y: baseline + point.height };
  const xc = (p1.x + p2.x) / 2;
  const yc = (p1.y + p2.y) / 2;
  ctx.quadraticCurveTo(p1.x, p1.y, xc, yc);
});</code></pre>
<p>Same physics. Same N. The surface goes from mechanical to liquid. Scroll back up to the previous demo, or <a href="https://codepen.io/shubniggurath/pen/vEXxyKz">click here</a>, open the control panel and click <code>smooth (bezier)</code> to see the effect of this small update.</p>
<p>A small chain of coupled oscillators, drawn with a handful of bezier curves, looks almost like a real fluid surface, and by playing with the different oscillator properties, you can simulate different fluid types.</p>
<h3>Part 5: Interaction</h3>
<p>The last piece is making the surface respond to the mouse/finger in a way that feels a little more natural than clicking+dragging.</p>
<p>What we want is to fire a single velocity impulse when something meaningful happens: when the cursor crosses the surface.</p>
<p>The elegant fix is a sign-change test - ie when the direction of the cursor to the surface changes from negative to positive, or vice versa. In each frame, compare the cursor’s y-position relative to the surface midline with its position in the last frame. If the sign flips - positive to negative, or vice versa - the cursor has just crossed. Fire the impulse at that moment only:</p>
<pre><code>const normalisedPos1 = mouseY - (surfaceTop + height / 2);
const normalisedPos2 = lastMouseY - (surfaceTop + height / 2);
const crossed = normalisedPos1 * normalisedPos2 &lt; 0;

if (crossed) {
  const closestPoint = points[Math.round(mouseX / segWidth)];
  const power = clamp(deltaY * 0.02, -5, 5);
  closestPoint.speed += -power;
}</code></pre>
<p>The impulse magnitude comes from <code>deltaY</code> - how fast the cursor was moving when it crossed. A slow drift barely disturbs the surface; a fast slash sends a big wave.</p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/bNwqBMd">https://codepen.io/shubniggurath/pen/bNwqBMd</a></p>
<p><a href="https://codepen.io/shubniggurath/pen/bNwqBMd">Click here</a> to see the code in action. Cross the surface fast, cross it slow, cross it repeatedly and watch the waves stack and interfere. All of it falls out of the same three-number update rule.</p>
<h3>Part 6: WebGPU</h3>
<p>At a modest point count - say, 64 oscillators - the Canvas 2D approach is fine. Crank it to a couple hundred and you're running physics on every point in JavaScript, every frame, single-threaded; performance starts to become a concern.</p>
<p>The physics update is embarrassingly parallel. Each oscillator reads from its two neighbours and writes its own new state. No oscillator depends on what any other oscillator is doing this frame, only what they were doing last frame. That's exactly the kind of work GPUs are built for.</p>
<p>Moving to WebGPU means writing a compute shader that runs the physics update. One thread per oscillator. The entire chain updates simultaneously:</p>
<pre><code>@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id : vec3&lt;u32&gt;) {
  let i  = id.x;
  let o  = src[i];
  let lh = select(0.0, src[i - 1].height, i &gt;= 1u);
  let rh = select(0.0, src[i + 1].height, i + 1u &lt; uni.count);

  let accel =
    - uni.elasticity * o.height
    + ((lh - o.height) + (rh - o.height)) * uni.coupling
    - uni.friction * o.speed;

  dst[i] = Osc(o.height + (o.speed + accel * 5.0 * uni.dt) * 10.0 * uni.dt,
               o.speed  +  accel * 5.0 * uni.dt);</code></pre>
<p>The trick with GPU physics is that you can't read and write the same buffer simultaneously - writing one oscillator's new state while another thread is still reading its old state gives you race conditions. The fix is ping-pong buffers: two copies of the oscillator array, <code>src</code> and <code>dst</code>. The compute shader reads <code>src</code>, writes <code>dst</code>. Next frame, swap them. The GPU never trips over itself.</p>
<p>With the physics running on the GPU, it's worth doing more in the render pipeline too. The vertex shader takes the key oscillators and interpolates between them using Catmull-Rom splines - smoother than quadratic beziers, and it runs entirely on GPU, so you can drive a dense mesh of render vertices from a sparse set of physics points. The fragment shader has access to the same oscillator buffer, so it can sample the surface curvature (second derivative of height) to simulate per-fragment surface normals for specular highlights.</p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/wBzJQzW">https://codepen.io/shubniggurath/pen/wBzJQzW</a></p>
<p><a href="https://codepen.io/shubniggurath/pen/wBzJQzW">Click here</a> to see the code. Same physics, same three-number update rule, just a different execution model.</p>
<h3>Part 7: The blob</h3>
<p>So far we've been operating on the y value of each oscillator, using that to simulate waves on the surface of a body of water, but there's nothing that says we can't operate on different linear values.</p>
<p>Take the same chain. Instead of letting the two end points sit without a second neighbour, connect them to each other. Oscillator 0's left neighbour becomes oscillator N-1. Oscillator N-1's right neighbour becomes oscillator 0. One line of change in the compute shader:</p>
<pre><code>let lh = src[(i + uni.count - 1u) % uni.count].height;
let rh = src[(i + 1u)             % uni.count].height;</code></pre>
<p>This just loops the waves around the chain.</p>
<p>Now change what &quot;height&quot; means. Instead of a vertical offset from a horizontal baseline, treat each oscillator's value as a radial displacement from a base circle. Oscillator 0 sits at angle 0, oscillator 1 at angle `2π/N`, oscillator `k` at angle `k * 2π/N`. Its displacement pushes it outward or inward from the circle's edge.</p>
<p>The vertex shader converts that to screen coordinates using classic trigonometry:</p>
<pre><code>let angle = t * TAU;
let r     = (uni.baseRadius + h) * uni.invHalf;
let x     = r * cos(angle) / uni.aspect;
let y     = r * sin(angle);</code></pre>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/OPRpagX">https://codepen.io/shubniggurath/pen/OPRpagX</a></p>
<p><a href="https://codepen.io/shubniggurath/pen/OPRpagX?editors=1010">Click here</a> to play with the code.</p>
<h3>Conclusion</h3>
<p>The thing I keep coming back to with simulations like this is how little causality there actually is. No point &quot;knows&quot; a wave is coming. No point decides to pass energy along. Each one just responds to its immediate neighbours according to a fixed local rule, and the wave emerges from that.</p>
<p>It's the same property that makes cellular automata interesting, or flocking algorithms, or most things in nature that look broadly coordinated from a distance. The global behaviour isn't designed. It's just what happens when simple local rules run long enough.</p>
<p>The surface doesn't know it's a surface, it's just a chain of springs.</p>]]></description>
            <link>https://www.surface-detail.com/posts/waves-and-oscillators</link>
            <guid isPermaLink="false">a08d52cf-be76-4f20-8bf9-8351bdd3e22e</guid>
            <dc:creator><![CDATA[Surface Detail]]></dc:creator>
            <pubDate>Mon, 16 Mar 2026 20:24:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Arc plotter - drawing with arcs]]></title>
            <description><![CDATA[<img src="https://cdn.sanity.io/images/wrgwufkn/production/5dfbe888bc76c93e8c6e55ef2db346766d58be72-1000x628.png?w=800" alt="Full-bleed screenshot of the output — rainbow palette, a nice composition, dark background" />
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/QwKbVja">https://codepen.io/shubniggurath/pen/QwKbVja</a></p>
<p>There's something satisfying about a curve that flows through a set of points and doesn't break a sweat doing it. No kinks, no sharp corners - each segment picks up exactly where the last one left off, at the same angle, heading in the same direction.</p>
<p>Arcer is a small class that takes a list of points and connects them with smooth, tangent-continuous circular arcs and outputs an array of objects that can be used to construct arcs with tools like Canvas or SVG. The renderer takes those arcs and fans them out into parallel offset ribbons, coloured across their width with a cosine palette. The result is something between a topographic map and a neon sign.</p>
<p>This post walks through how it works.</p>
<p>If you just want to go ahead and play with the code:</p>
<p><a href="https://codepen.io/shubniggurath/pen/QwKbVja">https://codepen.io/shubniggurath/pen/QwKbVja</a></p>
<p>Please share anything you make with me on <a href="https://x.com/liamegan">X</a> or <a href="https://bsky.app/profile/liamegan.bsky.social">Bluesky</a>, I'd love to see.</p>
<h3>Part 1: The Arcer class</h3>
<h4>1.1 The problem</h4>
<p>Joining two points with a straight line is trivial. Joining them with a curve that arrives at P2 heading in a specific direction - and does so while departing P1 in a specific direction - is a bit more work.</p>
<p>The constraint here is <em>tangent continuity</em>. Each arc must leave P1 in whatever direction the previous arc arrived, and must arrive at P2 in whatever direction the next arc will depart. No jolt at the join.</p>
<p>For circular arcs, it turns out there's exactly one circle that passes through two given points and is tangent to a given direction at the first of them. Find that circle, and you've got your arc.</p>
<h4>1.2 Alternating direction</h4>
<p>One more constraint: the arcs alternate clockwise and anticlockwise. This is what produces the S-curve quality - the line snakes through the points rather than spiralling away from them or appearing at tangents.</p>
<pre><code>const clockwise = this.points.length % 2 !== 0;</code></pre>
<p>Even indices go one way, odd indices go the other. Simple. This will become a little more complicated as we start to deal with arcs that may loop back on themselves, but we'll get to that in a while.</p>
<h4>1.3 Finding the circle</h4>
<p>Given P1, a tangent direction at P1, and P2, here's how to find the center of the unique arc.</p>
<p>There are a few things we can reason about a circle that bisects two points:</p>
<ol><li>The center of any circle is equidistant from every point on it's circumfurence.</li><li>The &quot;normal&quot; can be defined as the normalized vector between a point on the edge and the center.</li><li>The normal can also be defined as the normalized vector perpendicular to the opening angle of the arc.</li><li>To determine a perpendicular normal, you would take the opening angle of the arc, and rotate it by 90 degrees.</li><li>The center lies on this normal.</li><li>The center also lies on the perpendicular bisector of the chord P1&gt;P2 (equidistant from both endpoints).</li></ol>
<p>So both the normal and bisector are lines, and where they intersect is the center.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/7b2a2f3498ce0695e45f162ddf64e18af27c19c8-1000x628.png?w=800" alt="P1 and P2 as dots, tangent arrow at P1, normal ray extending from P1, perpendicular bisector of the chord, center marked at their intersection, arc drawn through both points" />
<p>In code:</p>
<pre><code>const normalAngle = clockwise
  ? tangentAngle - Math.PI / 2
  : tangentAngle + Math.PI / 2;
const normalDir = new Vec2(Math.cos(normalAngle), Math.sin(normalAngle));

const midpoint = P1.addNew(P2).scale(0.5);
const chordDir = P2.subtractNew(P1);
const bisectorDir = new Vec2(-chordDir.y, chordDir.x);</code></pre>
<p>Then it's a standard line-intersection: parameterise both lines and solve. The <code>denominator</code> here is the cross product of the two direction vectors; if it's near zero, the lines are parallel (the chord is already parallel to the normal), which means we have a degenerate case: a straight line.</p>
<pre><code>const denominator = normalDir.x * bisectorDir.y - normalDir.y * bisectorDir.x;

if (Math.abs(denominator) &lt; 1e-10) {
  return { type: &quot;line&quot;, ... };
}</code></pre>
<p>Otherwise, solve for `t` along the normal direction and get the center:</p>
<pre><code>const t =
  ((midpoint.x - P1.x) * bisectorDir.y - (midpoint.y - P1.y) * bisectorDir.x) /
  denominator;
const center = new Vec2(P1.x + t * normalDir.x, P1.y + t * normalDir.y);</code></pre>
<p>Radius is just the distance from center to P1. Start and end angles are <code>atan2</code> of the vectors from center to each point.</p>
<h4>1.4 The direction correction</h4>
<p>There's a subtle problem. Depending on where P2 lands relative to the tangent direction, the &quot;predicted&quot; clockwise direction might actually curl the arc backwards rather than forwards.</p>
<p>The fix is a cross-product check. The cross product of the tangent direction and the chord direction tells you which side of the tangent P2 is on:</p>
<pre><code>const cross = tangentDir.x * chordDir.y - tangentDir.y * chordDir.x;
if ((!clockwise &amp;&amp; cross &lt; 0) || (clockwise &amp;&amp; cross &gt; 0))
  clockwise = !clockwise;</code></pre>
<p>If the point is on the wrong side for the requested direction, flip it.
You can see, in the following image, the consequence of using the wrong canonical arc direction for a point that appeared in the wrong position releative to the previous arc's position.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/4c83f326654927b0195668671f2fd64642ec673a-1000x628.png?w=800" alt="Two cases side by side. Left: P2 on the expected side, arc flows naturally. Right: P2 on the wrong side — dashed arc shows where the uncorrected direction goes, solid arc shows the corrected result." />
<p>The arc object carries an <code>exitTangent</code> - the direction of travel at P2. This is the tangent at the end of the arc, which is perpendicular to the radius at P2 (in whichever rotational direction the arc is going):</p>
<pre><code>const exitTangent = clockwise ? endAngle - Math.PI / 2 : endAngle + Math.PI / 2;</code></pre>
<p>When the next arc is computed, it reads this value and uses it as its starting tangent. That's the whole mechanism for tangent continuity - each arc hands off to the next one.</p>
<h3>Part 2: Into SVG</h3>
<h4>2.1 Arc to SVG path</h4>
<p>SVG's path `A` command looks like this:</p>
<pre><code class="language-html">A rx ry x-rotation large-arc-flag sweep-flag x y</code></pre>
<p>For circular arcs, <code>rx</code> and <code>ry</code> are both the radius, and <code>x-rotation</code> is always 0. The two flags are the interesting part.</p>
<p><em><code>large-arc-flag</code></em> - for any two points on a circle, there are two arcs connecting them: the short way round and the long way round. This flag picks which one. It's 1 if the arc spans more than 180°.
<em><code>sweep-flag</code></em> - 1 for clockwise, 0 for anticlockwise.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/fd14a0d69d705a81a09d3ccb85fce885861153ea-1000x628.png?w=800" alt="Diagram of a circle with two points marked, showing all four arc combinations from the two flags" />
<p>To get the large-arc flag, calculate the angular span in the direction of travel:</p>
<pre><code>const span = clockwise
  ? (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI)
  : (startAngle - endAngle + 2 * Math.PI) % (2 * Math.PI);
const largeArc = span &gt; Math.PI ? 1 : 0;</code></pre>
<p>The modulo wrapping handles the case where start and end angles cross ±π.</p>
<h4>2.2 Offset arcs (parallel curves)</h4>
<p><code>projectArc</code> creates a copy of an arc at a different radius - shifted outward or inward from the original. Since all arcs in a segment share the same center and the same start/end angles, a radius change is all you need. The new P1 and P2 fall out of the new radius and the existing angles:</p>
<pre><code>function projectArc(arc, distance) {
  const r = arc.radius + distance;
  if (r &lt;= 0) return null;
  return {
    ...arc,
    radius: r,
    P1: new Vec2(
      arc.center.x + Math.cos(arc.startAngle) * r,
      arc.center.y + Math.sin(arc.startAngle) * r,
    ),
    P2: new Vec2(
      arc.center.x + Math.cos(arc.endAngle) * r,
      arc.center.y + Math.sin(arc.endAngle) * r,
    ),
  };
}</code></pre>
<p>One subtlety: anticlockwise arcs need their offset negated so both directions project consistently to the same side of the direction of travel. Without this, every other arc would fan out in the opposite direction to its neighbours and the ribbons would diverge at each join.</p>
<pre><code>const effectiveDistance = arc.clockwise ? distance : -distance;</code></pre>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/3683b40cd692b50dbf2b335fb0f6cfafdcc64c1e-500x314.png?w=800" alt="A single arc with six projected offset copies fanned out on each side, showing how the ribbon forms" />
<h4>2.3 The step loop and rendering order</h4>
<p>Each arc is drawn <code>steps</code> times on each side, at offsets of <code>stepOffset</code> pixels per step. The order this happens in changes the look significantly.</p>
<p><em>Segments-first (default):</em> all arcs are drawn at offset −N, then all at −N+1, and so on up to +N. Each step gets one colour from the palette, and because all arcs share that colour at the same step, the ribbons read as continuous coloured contour lines flowing across the whole composition.</p>
<p><em>Arcs-first</em>: each arc draws all of its offset copies before moving to the next one. The ribbons now read per-arc - you see each segment as a discrete object. The colour continuity is broken at the joins.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/6da6fa631ef65d1a6f447c6d09227e7d69372576-1000x628.png?w=800" alt="Side-by-side screenshots — same point layout, same palette. Left: steps-first (ribbons read as continuous lines). Right: arcs-first (each arc is a distinct band)" />
<h3>Part 3: Using it to make things</h3>
<p>The parameters aren't just knobs.</p>
<p><em><code>steps</code></em><em> × </em><em><code>stepOffset</code></em> is the total ribbon width. Keeping the width constant while adjusting the ratio changes the resolution: more steps and smaller offset produce smooth gradients; fewer steps and larger offset give bold, discrete bands.</p>
<p><em><code>pointCount</code></em> trades complexity for legibility. Three or four points give clean, readable forms. Ten or more, and it starts to feel like a frequency plot — still interesting, but you lose the sense of a single continuous line.</p>
<p><em><code>lineWidth</code></em> interacts with offset spacing. At small offsets with thin lines, there's breathing room between strokes - you get hatching. At large offsets with thick lines, the strokes merge into solid bands. The sweet spot depends on the palette and what you're going for.</p>
<p><em><code>Draggable points</code></em> (enable &quot;Edit Points&quot;) Let you sculpt the composition by hand. It's a fast way to find a layout that works before locking it in. The composition is still randomisable - hit Regenerate to get a fresh layout - but dragging lets you chase a specific shape without writing coordinates by hand.</p>
<p><a href="https://codepen.io/shubniggurath/live/QwKbVja">https://codepen.io/shubniggurath/live/QwKbVja</a></p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/QwKbVja">https://codepen.io/shubniggurath/pen/QwKbVja</a></p>
<p>Please share anything you make with me on <a href="https://x.com/liamegan">X</a> or <a href="https://bsky.app/profile/liamegan.bsky.social">Bluesky</a>, I'd love to see.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/11f4a873554da7fd88c85d8362b45e85fa9cc5d5-1000x628.png?w=800" alt="" />
<img src="https://cdn.sanity.io/images/wrgwufkn/production/a8695ef7a6842ee4022e41f9e5e72d0d4f2ae6bf-1000x628.png?w=800" alt="" />
<img src="https://cdn.sanity.io/images/wrgwufkn/production/a3d570c8805851e03982210e4b680230163a6858-1000x628.png?w=800" alt="" />
<img src="https://cdn.sanity.io/images/wrgwufkn/production/a008048fc1c1be16cd1d60a00885baa723509259-1000x628.png?w=800" alt="" />]]></description>
            <link>https://www.surface-detail.com/posts/arc-plotter</link>
            <guid isPermaLink="false">81ac1461-6ff0-4019-803b-771048b60491</guid>
            <dc:creator><![CDATA[Surface Detail]]></dc:creator>
            <pubDate>Mon, 09 Mar 2026 23:04:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Rubber banding]]></title>
            <description><![CDATA[<p>Imagine you have a series of points and you want to draw a single line around all of them. This sounds relatively simple, but if you just drew lines between the centers of the balls, it would look like a jumbled, tangled mess. This line wouldn't wrap around the outside.</p>
<p>To achieve this, we're going to dive into a couple of really interesting algorithms, and with a little bit of geometry, we'll create a dynamic, bouncing, rubber-band effect.</p>
<p>Here's our plan:</p>
<ul><li>We'll use a physics simulation to make a series of points bounce around the screen.</li><li>We'll figure out how to draw a &quot;rope&quot; that wraps around the outside of those points.</li><li>We'll use the concept of tangent points to make sure the rope looks like it's wrapping smoothly around the circles and not just drawing straight lines from center to center.</li></ul>
<h3>Part 0: Setting up</h3>
<p>The first part of the simulation involves creating a series of animated points and, naively, drawing some lines between them. This is all pretty basic.</p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/RNrNrLV">https://codepen.io/shubniggurath/pen/RNrNrLV</a></p>
<h3>Part 1: Finding the hull</h3>
<p>The first challenge is to identify which points form the outer boundary. The line needs to be drawn only around the points on the very edge of the group, not the ones in the middle. The solution lies in an algorithm called the convex hull.</p>
<p>The convex hull is the smallest convex polygon containing all the points. A great way to visualize this is to imagine a set of nails hammered into a board. The convex hull is the shape a rubber band would form if you stretched it around all the nails and let it snap tight.</p>
<p>We'll use <strong>Jarvis March</strong>, also known as the <strong>Gift Wrapping</strong> algorithm, to find the convex hull of our points. The name is a great way to think about it - we're literally &quot;wrapping&quot; the points, one by one, to find the outer boundary.</p>
<p>The core of our demo is the <code>findHull</code> function. Let's break down how it works, here's that function:</p>
<pre><code>function findHull(ps) {
  ps.sort((a, b) =&gt; a.x - b.x); // Sort the points left-to-right
  const startPoint = ps[0]
  const hull = [startPoint.clone()];
  let currentPoint = startPoint, nextPoint;
  let i = 0;
  do {
    if(i++ &gt; 1000) break; // Break out condition
    nextPoint = ps[0] === currentPoint ? ps[1] : ps[0];
    for(let i = 0; i &lt; ps.length; i++) {
      const cp = ps[i]; // Candidate point
      if(cp === currentPoint) continue;

      const pq = nextPoint.subtractNew(currentPoint);
      const pr = cp.subtractNew(currentPoint);
      
      const cross = crossp(pr, pq);

      if(cross &gt; 0) {
        nextPoint = cp;
      } else if(cross === 0) {
        if(pr.lengthSquared &gt; pr.lengthSquared)
          nextPoint = cp;
      }
    }
    hull.push(nextPoint.clone());
    currentPoint = nextPoint;
    
  } while(currentPoint !== startPoint)
    
  return hull;
}</code></pre>
<p>Let's break down how this algorithm works:</p>
<ol><li>First, we take the list of points to run the algorithm across and sort it, making sure the points are in order from left to right.</li><li>Then we find a point that's guaranteed to be on the hull. The leftmost point is always a good place to start.</li><li>We loop through all the other points from our starting point to find the next one that creates the largest counter-clockwise (CCW) turn. This is the &quot;wrapping&quot; part.</li><li>How do we figure out which point creates the most significant CCW turn? We use a cool trick from geometry called the cross product. Our <code>crossp</code> function takes two vectors (2 point candidates) and returns a number that tells us their orientation relative to each other.</li><li>If the cross product is positive, the new candidate point is to the &quot;left&quot; of our current path, which means it's a better candidate for our hull.</li><li>If it's negative, it's to the &quot;right,&quot; and we can ignore it.</li><li>If it's zero, the points are on the same line.</li><li>We keep iterating and replacing our &quot;next best&quot; point with any new candidate that produces a positive cross-product.</li><li>Once we've found the next point on the hull, we make it our new &quot;current&quot; point and repeat the process. We continue this loop until we return to our starting point, completing the loop and our &quot;wrap.&quot;</li></ol>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/dPGPXLy">https://codepen.io/shubniggurath/pen/dPGPXLy</a></p>
<h3>Part 2: Drawing the rope with tangent points</h3>
<p>Now that we have the sequence of points that make up our convex hull, we must draw the rope. Simply drawing lines between the centers of the points won't look right (as you can see above); the rope should appear to wrap around the outside of each circle.</p>
<p>This is where the concept of tangent points comes in, and our <code>getTangentPoints</code> function.</p>
<p>Imagine a line running between the centers of two of our circles on the hull. We need to find the points on the circumference of each circle where our &quot;rope&quot; will touch. These are the tangent points.</p>
<p>The <code>getTangentPoints</code> function uses some simple vector math to find them. It takes the vector between the two circle centers, finds the vector perpendicular to it (called the <em>normal</em>), and then offsets the center of each circle along that normal by the radius. This gives us the exact coordinates for the rope to start and end on each circle.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/a04fb639d7560699febe142c6bffe4c2cc615a4d-998x626.png?w=800" alt="Projecting a line off the normal to the radius of the composite circles." />
<pre><code>const getTangentPoints = (p1, p2, radius = settings.lineDistance) =&gt; {
  const dir = p2.subtractNew(p1).normalise();
  const normal = new Vec2(dir.y, -dir.x);
  const t1 = p1.addNew(normal.scaleNew(radius));
  const t2 = p2.addNew(normal.scaleNew(radius));
  return { start: t1, end: t2, radius, normal };
};</code></pre>
<ol><li>First, the function calculates a vector that points from the center of the first circle (p1) to the center of the second (p2). The vector is then normalized to a length of 1, providing a clean direction without distance.</li><li>Next, the function computes a normal vector, which is a vector that is perpendicular to the direction vector found in the first step. This normal vector points &quot;outward&quot; from the line connecting the two circles.</li><li>Finally, the function uses the normal vector to find the exact coordinates of the tangent points. It offsets the center of each circle (<code>p1</code> and <code>p2</code>) along the <code>normal</code> vector by the specified <code>radius</code>. This gives us the final two tangent points (<code>t1</code> and <code>t2</code>) on the circumference of each circle.</li></ol>
<p>In our draw loop, we go through each pair of points on the hull. We use <code>getTangentPoints</code> to get point <strong>a</strong>, the end point of the current straight line segment, and point <strong>b</strong>, the start point of the next straight line segment. Then, we draw an arc around the curve of the circle, creating a seamless line between <strong>a</strong> and <strong>b</strong>.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/cbfb5f6083674302e2034920629d3f92c6083ed6-998x626.png?w=800" alt="Showing the arc between points a and b - the end point of the previous segment and the start point of the new one." />
<h3>Bringing it all together.</h3>
<p>We've found our convex hull and figured out how to draw the perfect &quot;rope&quot; around the circles. Now, for the final touch - the animation.</p>
<p>Our <code>runLoop</code> function does all the heavy lifting for us every frame. It updates the position of our points using a simple sine wave and some basic collision detection to keep them bouncing within the screen's boundaries. It's a simple animation that creates the illusion of the physics of tension between the rubber bands.</p>
<p>You might have noticed that we also include two &quot;anchor&quot; points in our array of points. These are just stationary points at the bottom of the screen that are also included in the convex hull calculation, giving the rope something to &quot;hang&quot; from and creating that nice, pendulous look.</p>
<p>Finally, we use the <code>Tweakpane</code> library to add some controls. This lets us play with the number of points, the speed of the animation, and the thickness of the rope, allowing us to interact with the algorithm in real-time. Make sure to play around with the properties!</p>
<p><a href="https://codepen.io/shubniggurath/pen/Qwjaeda">https://codepen.io/shubniggurath/pen/Qwjaeda</a></p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/Qwjaeda">https://codepen.io/shubniggurath/pen/Qwjaeda</a></p>]]></description>
            <link>https://www.surface-detail.com/posts/rubber-banding</link>
            <guid isPermaLink="false">d0da817b-d6a1-4460-8b3d-9f28e25dbd87</guid>
            <dc:creator><![CDATA[Surface Detail]]></dc:creator>
            <pubDate>Sat, 20 Sep 2025 15:52:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Let's go deeper]]></title>
            <description><![CDATA[<p>Perturbation, determinism, and line-drawing.</p>
<p>In the last post, we saw how we can break down a grid element into separate lines to introduce some seeming complexity, but that ultimately it's something of a simple structure. For reference, here's the pen we ended up with.</p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/ByojGMY">https://codepen.io/shubniggurath/pen/ByojGMY</a></p>
<p>In this post, we're going to take on 2 tasks:</p>
<ul><li>Make the grid irregular.</li><li>Change the width of the lines based on the edge they're coming from.</li></ul>
<p>To accomplish this, we need to implement a few new concepts:</p>
<ul><li>Hashing and pseudorandomness.</li><li>Grids with points that can be moved around.</li><li>Lerping between two points, instead of between single numeric (scalar) values.</li><li>Deterministic randomness (hashing) based on edge (remembering that two neighbouring cells share an edge).</li><li>Polygons!</li><li>Breaking a space into columns is essential to ensure the layout looks okay.</li></ul>
<p>This is going to be a lot to get through, but we'll make regular check-ins along the way, and it'll all be worth it.</p>
<h3>Hashing</h3>
<p>Pseudorandomness and hashing are really fascinating subjects that I'll write more on at some point in the future. For our purposes, we're going to go with something very simple - a function that takes a number, regardless of how large or small, and returns a random-seeming number between zero and one.</p>
<pre><code>const randomOS = Math.random() * 200;
export const hash = (x, os = randomOS) =&gt;
  Math.abs(
    Math.sin(
      (x + os) * 9174986346
    ) * 1964286753) % 1;</code></pre>
<p>That can look like a lot of nonsense to someone unfamiliar, so let me break it down a bit. This sort of hashing function takes a simple sine wave and ramps it up to an extremely large frequency (that <code>* 917...</code>). Then, it is multiplied by a huge amplitude and takes the fractional value of that resulting number. Given sufficiently large numbers for the frequency and amplitude values, it compresses the wave to a point where the numbers from the function appear random, but are deterministic.</p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/OPybVbE?editors=1010">https://codepen.io/shubniggurath/pen/OPybVbE?editors=1010</a></p>
<p>In the above codepen, you can play around with this function directly by moving a point along the sinewave to see the values it outputs. But as a further demonstration, take a look at the following code.</p>
<pre><code>hash(1); // 0.09126
hash(2); // 0.11871
hash(3); // 0.60281
hash(3); // 0.60281
hash(3); // 0.60281</code></pre>
<h3>Grid perturbation</h3>
<h4>Lerping points</h4>
<p>This is where things start to get exciting. All we really mean by perturbation is that the points can be moved around, making the grid cells irregularly shaped.</p>
<p>Like <code>lerp</code>, <code>lerpPoint</code> takes two values representing the beginning and the end of a ratio, and a scalar value representing the point along the ratio to return the value.</p>
<p>Consider the previous code we were using to lerp between two points:</p>
<pre><code>  const topEdgePoint = {
    x: lerp(
      cell.points[0].x, 
      cell.points[1].x, 
      interpolant
    ),
    y: cell.points[0].y,
  };</code></pre>
<p>Notice how the only value we're lerping is the <code>X</code> value? This is called a &quot;scalar&quot; value, because it only has a single value. The official definition is that it only has magnitude, not direction.</p>
<p>What we need is a function that can lerp both the <code>X</code> and the <code>Y</code> values at the same time. If you'll remember our scalar lerp function, extending that into 2 dimensions is relatively trivial, see:</p>
<pre><code>lerp = (a, b, t) =&gt;
  a + (b - a) * t;
lerpPoint = (p1, p2, t) =&gt; ({
  x: lerp(p1.x, p2.x, t),
  y: lerp(p1.y, p2.y, t),
});</code></pre>
<p>We need this new function because we now need to perform a lerp between two points, instead of 2 scalar values. Suddenly, with this function, we can perform lerps like this:</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/9d67b8b6449fd7d2d34542d289c35d88204ac649-998x626.png?w=800" alt="" />
<h4>Moving points</h4>
<p>Alright, here's where things start to get fun. We'll be working from <a href="https://codepen.io/shubniggurath/pen/ByojGMY">this pen</a> as a base.</p>
<p>The first thing we need to do is replace the old code that was using scalar lerp to determine the position along the horizontal or vertical sides. That looked something like this:</p>
<pre><code>
  const topEdgePoint = {
    x: lerp(
      points[0].x, 
      points[1].x, 
      e),
    y: points[0].y,
  };</code></pre>
<p>Now we can replace that with our new lerpPoint function, and it turns into this:</p>
<pre><code>
  const topEdgePoint = lerpPoint(
    points[0], 
    points[1], 
    e + topEdge.r);</code></pre>
<p>Much nicer and easier to understand, right?</p>
<p>Next, we need to update our points to add a bit of perturbation. This is going to involve:</p>
<ul><li>Getting the point's ID, a scalar number, based on the x and y position of the point (remembering that our points are all in grid units).</li><li>Getting a random angle using our hash function.</li><li>Offsetting the point using a trigonometric function.</li></ul>
<p>Before we get started, let's introduce a new variable called perturbSize. This will be a number between 0 and 0.5 and will be used to describe how far along the angle the points should move.</p>
<pre><code>const perturbSize = 0.3;</code></pre>
<p>Alright, now in our split loop, right before we calculate the values of the side points, let's update our point generation to look like this:</p>
<pre><code>
  const points = cell.points.map((point) =&gt; {
    const pointID = 
          getPointID(point.x, point.y, gridW);
    const angle = 
          hash(pointID, seed) * Math.PI * 2;
    const length = 
          hash(pointID, seed+100) * perturbSize;
    return {
      x: (
        point.x + 
        Math.cos(angle) * 
        length) * cellWidth,
      y: (
        point.y + 
        Math.sin(angle) * 
        length) * cellHeight,
    }
  })</code></pre>
<p>Alright, there's a lot to break down in this function, so let's explain it with a graphic.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/300bef23636176077d443091d4457558d56dc37a-998x626.png?w=800" alt="A diagram illustrates applying an offset to a point based on an angle and length derived from two hash values. From an origin point (labeled '0'), a vertical line establishes a reference. An angle 'a' is measured from this vertical line to a vector of length 'b', which extends to a new offset point (also labeled '0'). Text defines '0' as pointID, 'a' as angle, and 'b' as length." />
<p>This diagram explains exactly what happens in that function. First, we get the value of the point ID (0), then we generate two pseudo-random numbers based on that ID (angle and length). The reason we're adding 100 to the seed in the second hash function is to ensure we get a different hash value for the same number.</p>
<p>Finally, we use these values to perform basic trigonometry to determine how far and in what direction our point moves.</p>
<p>You might ask why we use polar coordinates and not just generate a hash value for X and one for Y. The reason for that is distribution! Modifying the X and Y coordinates like that will move the points around in a square-like pattern, which, using polar coordinates, will move them around in a circular pattern, which can be more natural.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/bba395fb4ddba7b1151a4bad255b007172474b53-998x626.png?w=800" alt="Angled lines moving around a grid who's points are offset randomly." />
<p>Take a look at <a href="https://codepen.io/shubniggurath/pen/qEOqqBZ">this pen</a> to see this all in action! This takes the work we did in the last post and adds to it what we've spoken about here.</p>
<h3>What to do next</h3>
<p>Now that we have different lines working and grid perturbation. The next thing I want to do is make it so that I can draw lines from one edge to the other edge at different widths on each edge. Something like this:</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/4a1cfdce67f273a4a0dc3919370b0f177771f638-998x626.png?w=800" alt="" />
<p>Here you can see we have those same lines running from edge to edge, but at different widths. You'll recall that we have functions to get IDs for the different edges, which will allow us to generate deterministic randomness per edge, which will provide what we need to create this.</p>
<p>Previously, we've only been drawing single lines from one edge to another, which has allowed us to ignore some pretty significant complexities that we'll get to shortly. </p>
<h3>A detour</h3>
<p>We need to take a bit of a detour next to explore a couple of things we're going to need to get in place to implement the final vision. This detour will provide us with a way to create polygons that can be filled with hatching and booleaned against each other, and will allow us to explore a way to separate a space into columns and gutters (which is essential when going from lines to polygons).</p>
<h4>Polygons</h4>
<p>I'm going to cheat on this one and use a piece of code written by Reinde Reinder Nijhoff. It's a class used to create and manage polygons specifically for the purposes of providing clipping and hatching. You can see the version of his class I've borrowed for this purpose and modified to work with Turtleman <a href="https://codepen.io/shubniggurath/pen/MYaYEpP">here</a>. This class starts by instantiating a polygon manager, which tracks polygons drawn to it for the purpose of subtracting them from other polygons.</p>
<p>The relevant code in the above pen is this:</p>
<pre><code>// Create a new polygon
const polygon1 = polygons.create();
const points = drawNgon(
  width/2 + Math.cos(angle) * distance,
  height/2 + Math.sin(angle) * distance,
  minSize + i/divisions*maxSize,
  sides,
  angleFollowsRotor ? angle : rotor
  );
// Draw some points to it
polygon1.addPoints(...points);
// Add some hatching.
// Parameters are angle and distance apart 
polygon1.addHatching(rotor, 5);
// Outline the polygon (draw a line)
polygon1.outline();
// Draw the polygons to the toy
polygons.draw(toy,polygon1);</code></pre>
<p>We'll do a bit of a deep dive into this class some other time, but for now, we have some code that can do what we want it to - create shapes with points, and fill them with shading in the form of lines.</p>
<h4>Columns and gutters</h4>
<p>Now we need to come up with a way to divide a space into columns and gutters. This is necessary for our purposes because if we were to naively draw from edge-to-edge, you'd end up with a thick black line down the middle, consider:</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/734263ba05cce404760c3b4d345edb3c386ce3b1-998x626.png?w=800" alt="Illustrating the naive approach, where no gutters are calculated and how that results in a thick black like down the middle of the cell." />
<p>For this exercise, we should review <a href="https://codepen.io/shubniggurath/pen/jEbELLy">this pen</a>, which breaks the problem down into easy-to-understand components. The goal is to create a series of columns, each with a consistent width, separated by gutters. The size of these columns and gutters is determined by the <code>columnSize</code> and <code>splits</code> variables in our configuration. Let's break down the logic.</p>
<pre><code>const columns = 10;
const columnSize = 5;
const length = a - b;
const spaceWidth = 1 / 
  ( columns * columnSize + columns ) *
  length;
const columnSizeinPx = spaceWidth * columnSize;
let x = a + spaceWidth / 2;</code></pre>
<p>First, we set our total number of <code>columns</code> to 10 and a <code>columnSize</code> of 5. This <code>columnSize</code> isn't a pixel value; it's a ratio. It means that each column will be 5 times wider than the gutter.</p>
<p>The core of the logic is in the <code>spaceWidth</code> calculation. This variable represents the width of a single gutter. To figure this out, we treat all of our columns and gutters are proportional &quot;units&quot;. The number of units for all columns combined is <code>columns * columnSize</code>, and the number of units for all gutters is simply columns. This formula implicitly adds a gutter at the start and end of the row.</p>
<p>Once we have the total number of units - <code>(splits * columnSize + splits)</code> - we can find the fractional size of a single unit by taking <code>1 / total_units</code>. We then multiply that by the total available length to get our <code>spaceWidth</code> in pixels.</p>
<p>Finally, we set up the x value, which is a variable used to track the position of the &quot;play head&quot; as we're drawing across the area.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/45e4100ef74bd4b2c7a65bac7073010ef6d88736-998x626.png?w=800" alt="" />
<h5>Drawing the polygons</h5>
<p>Now we can use these calculated values to draw our columns. This is done inside a <code>for</code> loop that iterates through each split.</p>
<pre><code>for (let i = 0; i &lt; columns; i++) {
  const p = polygons.create();
  p.addPoints(
    [x, points[0][1]],
    [x + columnSizeinPx, points[0][1]],
    [x + columnSizeinPx, points[3][1]],
    [x, points[3][1]]
  );
  p.outline();
  p.addHatching(Math.PI/4,3);
  polygons.draw(toy,p)
  x += columnSizeinPx + spaceW;
}</code></pre>
<p>In each iteration, we create a new polygon. The <code>addPoints</code> function defines the four corners of a rectangle, which becomes our column. The <code>x</code> variable is the key here. It starts at the correct initial position (with or without a side margin) and is then incremented at the end of each loop by the width of one column plus one gutter (<code>columnSizeinPx + spaceW</code>). This ensures each new polygon is placed right after the previous one, with a gutter in between.</p>
<p>Again, please have a play with this on <a href="https://codepen.io/shubniggurath/pen/jEbELLy">codepen</a>, it's a pretty nice little piece of code.</p>
<h3>Bringing it all together</h3>
<p>Now that we have the essential pieces in place—hashing, <code>lerpPoint</code>, point perturbation, and a way to manage polygons—we can finally combine everything to create the final effect. We’ll be taking the logic we used to create columns and gutters and applying it to each edge of our grid cells, using our hashing function to create unique, deterministic randomness for each side.</p>
<h4>Per-edge randomness</h4>
<p>The key to this entire effect is adapting the column layout logic to work on each of the four edges of a single grid cell. Previously, we had a single <code>columnSize</code> for the entire row of columns. Now, we need a unique, random size for each edge.</p>
<p>The following code block is where we translate the static <code>columnSize</code> from our previous example into a dynamic value for each edge:</p>
<pre><code>const edges = getEdgeIdsForGridId(i, gridW, gridH);
const edgeVariance = edges.map(
  (e) =&gt; hash(e, hashSeed) * sizeVariance
);
const edgeSpaces = edges.map(
  (_, i) =&gt;
    1 / (splits * edgeVariance[i] + splits)
);
const edgeSizes = edges.map(
  (e, i) =&gt;
    edgeSpaces[i] * edgeVariance[i]
);
const headers = edgeSpaces.map((e) =&gt; e / 2);</code></pre>
<p>Let's break this down:</p>
<ul><li><code>edges</code>: This is the function we created earlier to return an array of edge IDs for a grid cell ID. See the figure below for a further exploration of why this works.</li><li><code>edgeVariance</code>: This is where we make use of our hashing function. Remember, previously we used it to derive deterministic pseudorandom numbers for our vertices? Now we're doing the same thing for our edges. We multiply this number by the <code>sizeVariance</code> value from the config, which gives each edge a unique <code>columnSize</code>. This is what will give our lines that unique wedge shape.</li><li><code>edgeSpaces</code>: This is the exact same logic we used for our columns and gutters, but applied to each edge individually. We're calculating the width of a single gutter for each edge based on its unique <code>edgeVariance</code>.</li><li><code>edgeSizes</code>: This calculates the actual proportional width of the line segments on each side, just like <code>columnSizeinPx</code> did for our full column layout.</li><li><code>headers</code>: This is a simple array that keeps track of the marching position for our drawing loop on each edge, initialized to half of a gutter's width to account for the initial space.</li></ul>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/74a421db692eb5f1ce2511a291559c80d2a24a25-998x626.png?w=800" alt="A diagram illustrating that calling getEdgeIdsForGridId will return edge IDs that are shared between adjacent cells." />
<h4>Drawing the polygons</h4>
<p>Now that we have all the necessary proportional values, we can get to the fun part: drawing the polygons. The core idea is to draw two polygons per grid cell, one connecting the top and right edges, and one connecting the bottom and left edges, or vice-versa, depending on the <code>edgeDirection</code>.</p>
<pre><code>// ...inside the main loop
const topEdgePoints = lerpedPoints(
  points[0],
  points[1],
  headers[0],
  edgeSizes[0]
);
const leftEdgePoints = lerpedPoints(
  points[0],
  points[3],
  headers[3],
  edgeSizes[3]
);

if (edgeDirection === 1) {
  bottomEdgePoints = lerpedPoints(
    points[2],
    points[3],
    headers[2],
    edgeSizes[2]
  );
  rightEdgePoints = lerpedPoints(
    points[2],
    points[1],
    headers[1],
    edgeSizes[1]
  );
  // Draw the notches from the top edge to the right edge
  a.addPoints(
    [topEdgePoints[0].x, topEdgePoints[0].y],
    [topEdgePoints[1].x, topEdgePoints[1].y],
    [rightEdgePoints[1].x, rightEdgePoints[1].y],
    [rightEdgePoints[0].x, rightEdgePoints[0].y]
  );
  // ... and the notches from the bottom edge to the left edge
  b.addPoints(
    [leftEdgePoints[0].x, leftEdgePoints[0].y],
    [leftEdgePoints[1].x, leftEdgePoints[1].y],
    [bottomEdgePoints[1].x, bottomEdgePoints[1].y],
    [bottomEdgePoints[0].x, bottomEdgePoints[0].y]
  );
} else {
  // same logic for the other direction
}</code></pre>
<p>This section of the code directly translates our previous concept of connecting lines from one edge to another into the creation of polygons. Instead of simply drawing a line, we're now defining the four points of a polygon using our <code>lerpPoint</code> function.</p>
<p>The <code>lerpPoint</code> function is the hero here, returning not just a single point, but a pair of points that define the width of our &quot;line segment&quot; on each edge. For example, <code>topEdgePoints</code> contains two points: one at the start of the line segment and one at the end. We then combine these pairs of points from two different edges (e.g., <code>topEdgePoints</code> and <code>rightEdgePoints</code>) to create a four-sided polygon.</p>
<p>This process is repeated in a loop for each of our <code>splits</code>, and with each iteration, we update the <code>headers</code> to move the drawing position forward. Finally, the code uses a boolean operation on the polygons to subtract the main polygon from the entire grid cell, leaving only the complex, hatched lines.</p>
<p>Play around with the code for that here - <a href="https://codepen.io/shubniggurath/pen/qEOEVMV">https://codepen.io/shubniggurath/pen/qEOEVMV</a>.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/a3c1398a6073a7e4c8523d556b25909eaee0b1f0-1000x628.png?w=800" alt="" />]]></description>
            <link>https://www.surface-detail.com/posts/lets-go-deeper</link>
            <guid isPermaLink="false">eb36d65b-a4be-49d5-aa30-88309535fbb2</guid>
            <dc:creator><![CDATA[Surface Detail]]></dc:creator>
            <pubDate>Sat, 02 Aug 2025 18:29:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Iterating on a theme]]></title>
            <description><![CDATA[<p>In the last post, we discussed using a grid to draw diagonal lines in one direction or another, which leads to some reasonably complex outputs. See this <a href="https://codepen.io/shubniggurath/pen/WbQbPjd">codepen</a>. This pen divides space into cells, defined by grid x and y position, and draws lines from the top-centre to the right-centre and bottom-centre to the left-centre, or vice-versa, depending on discrete rules.</p>
<p>In this post, we'll iterate on this basic setup in a couple of ways that will produce much more complex outcomes.</p>
<h3>Recap</h3>
<p>Recall this piece of code from the last post:</p>
<pre><code>const topEdgePoint = {
  x: lerp(
    cell.points[0].x, 
    cell.points[1].x, 
    0.5
  ),
  y: cell.points[0].y,
};</code></pre>
<p>This code finds a point between the top two points that make up a cell. </p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/dcf8d58c8157d7a40d9ccf76ffff3786a0c26503-998x626.png?w=800" alt="Showing a cell made up of 4 points, with an additional point 50% of the way between the top two points." />
<p>This image demonstrates this functionality and shows you exactly what the function is finding.</p>
<h3>Iterating</h3>
<p>You might remember that the third parameter of the lerp function describes a ratio between the two edges. Think of it like this:</p>
<pre><code>lerp(
  start value, // The value at 0
  end value,   // The value at 1
  ratio        // The point between the two
)</code></pre>
<p>What this realization may open up is the possibility to use a number other than .5 (50%). In fact, we can introduce a number, representing the number of lines we want per side, and loop to produce some complexity. Our new code might look something like this:</p>
<pre><code>const numLines = 5;
for(let i = 0; i &lt; numLines; i++) {
  const interpolant = i / numLines;
  const topEdgePoint = {
    x: lerp(
      cell.points[0].x, 
      cell.points[1].x, 
      interpolant
    ),
    y: cell.points[0].y,
  };
}</code></pre>
<p>Now you can imagine we have something like this.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/cafa31816db9761a434883cbabc650dcb9a76728-998x626.png?w=800" alt="An illustration of lerping between points 0 and 1 5 times using a for loop, producing an array of ratios amounting to 0=0, 1=.2, 2=.4, 3=.6, 4=.8" />
<p>Here you can see that the loop produces a series of 5 ratios from 0 to .8. If we repeat this on the right or left side, we'll get another series of points that will provide a way to draw lines. </p>
<pre><code>const numLines = 5;
for(let i = 0; i &lt; numLines; i++) {
  const interpolant = i / numLines;
  const rightEdgePoint = {
    y: lerp(
      cell.points[2].y, 
      cell.points[1].y, 
      interpolant
    ),
    x: cell.points[1].x,
  };
  const topEdgePoint = {
    x: lerp(
      cell.points[0].x, 
      cell.points[1].x, 
      interpolant
    ),
    y: cell.points[0].y,
  };
}</code></pre>
<p>Crucial to understanding the way this works is understanding the order of the points we're lerping between. Notice in our code above, we're lerping between point 0 and 1, but then we're lerping between 2 and 1. The following illustration shows why this order change is important.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/34a9d52ed6ee9ac995b918ac874a39f475e8cc25-998x626.png?w=800" alt="Repeating the iteration on the right edge gives us a second set of points we can use to draw a series of lines from the top edge, to the right." />
<p>As you can see, because we're lerping using ratios between 0 and 5, excluding 5, we never get a ratio of 1. This is necessary as drawing a line between point 1 and 1 would be meaningless.</p>
<h3>Putting it into practice</h3>
<p>All the code from the <a href="https://codepen.io/shubniggurath/pen/WbQbPjd">previous demo</a> remains essentially unchanged, with the exception of the loop. If you just want to jump straight to the codepen example, <a href="https://codepen.io/shubniggurath/pen/raOMwOz">click here</a>.</p>
<p>Here's the updated code within the cell loop:</p>
<pre><code>for(let i = 0; i &lt; splits; i++) {
  const e = i/splits;

  const topEdgePoint = {
    x: lerp(
      cell.points[0].x, 
      cell.points[1].x, 
      e + topEdge.r),
    y: cell.points[0].y,
  };
  const leftEdgePoint = {
    x: cell.points[0].x,
    y: lerp(
      cell.points[3].y, 
      cell.points[0].y, 
      e + leftEdge.r),
  };
  const rightEdgePoint = {
    x: cell.points[1].x,
    y: lerp(
      cell.points[2].y, 
      cell.points[1].y, 
      e + rightEdge.r),
  };

  let a,b,c,d;
  if (edgeDirection === 1) {
    a = topEdgePoint;
    b = rightEdgePoint;
  } else {
    const topEdgePoint = {
      x: lerp(
        cell.points[1].x, 
        cell.points[0].x, 
        e + topEdge.r),
      y: cell.points[0].y,
    };
    a = topEdgePoint;
    b = leftEdgePoint;
  }

  toy.jump(a.x * cellWidth, a.y * cellHeight);
  toy.goto(b.x * cellWidth, b.y * cellHeight);
}</code></pre>
<p>Here you can see exactly what we've implemented previously, with a small update to the logic depending on whether the edge direction is left or right. This is necessary for the reason explained in the last section - if left and right are iterating from the lower point to the upper, we need the top points to reflect a coincidental arrangement.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/4a68740e37c916f1a22db7a1b8783225d75b5a96-1000x630.png?w=800" alt="The image shows a black and white pattern. It's made up of many rows and columns of the same basic shape. That shape looks like a letter &quot;V&quot; or an open angle, created by several parallel black lines." />
<h3>Putting it all together</h3>
<p>Extending this to the other side of the cell we have both the top edge and the bottom edge extending to the left or right, depending on direction.</p>
<pre><code>for(let i = 0; i &lt; splits; i++) {
  const e = i/splits;

  const topEdgePoint = {
    x: lerp(
      cell.points[0].x, 
      cell.points[1].x, 
      e + topEdge.r),
    y: cell.points[0].y,
  };
  const bottomEdgePoint = {
    x: lerp(
      cell.points[3].x, 
      cell.points[2].x, 
      e + bottomEdge.r),
    y: cell.points[2].y,
  };
  const leftEdgePoint = {
    x: cell.points[0].x,
    y: lerp(
      cell.points[3].y, 
      cell.points[0].y, 
      e + rightEdge.r),
  };
  const rightEdgePoint = {
    x: cell.points[1].x,
    y: lerp(
      cell.points[2].y, 
      cell.points[1].y, 
      e + rightEdge.r),
  };

  let a,b,c,d;
  if (edgeDirection === 1) {
    a = topEdgePoint;
    b = rightEdgePoint;
    c = bottomEdgePoint;
    d = leftEdgePoint;
  } else {
    const topEdgePoint = {
      x: lerp(
        cell.points[1].x, 
        cell.points[0].x, 
        e + topEdge.r),
      y: cell.points[0].y,
    };
    const bottomEdgePoint = {
      x: lerp(
        cell.points[2].x, 
        cell.points[3].x, 
        e + bottomEdge.r),
      y: cell.points[2].y,
    };
    a = topEdgePoint;
    b = leftEdgePoint;
    c = bottomEdgePoint;
    d = rightEdgePoint;
  }

  toy.jump(a.x * cellWidth, a.y * cellHeight);
  toy.goto(b.x * cellWidth, b.y * cellHeight);
  if(i==0) continue;
  toy.jump(c.x * cellWidth, c.y * cellHeight);
  toy.goto(d.x * cellWidth, d.y * cellHeight);
}</code></pre>
<p>You may notice that <em>if(i==0) continue; </em>in there. That just avoids a case where the points between top and bottom would produce the same line.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/4ac954f837c6da4c38a3b274148fbc2639acf4ed-1000x630.png?w=800" alt="" />
<p><a href="https://codepen.io/shubniggurath/pen/ByojGMY">Click here</a> to play around with the codepen for this post.</p>
<h3>Wrapping up</h3>
<p>We'll extend this idea even further in the next article, building out the system further with both irregular cells and variable width lines. But for now, consider the following code:</p>
<pre><code>export const lerpPoint = (p1, p2, t) =&gt; ({
  x: lerp(p1.x, p2.x, t),
  y: lerp(p1.y, p2.y, t),
});</code></pre>
<p>This is an extension of our lerp function that takes two points and returns a lerp between them. This will be very useful in the next post.</p>]]></description>
            <link>https://www.surface-detail.com/posts/iterating-and-adding-complexity</link>
            <guid isPermaLink="false">33ace414-f1c1-4f73-aa38-aadc1af566b3</guid>
            <dc:creator><![CDATA[Surface Detail]]></dc:creator>
            <pubDate>Sun, 20 Jul 2025 22:28:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Some useful functions]]></title>
            <description><![CDATA[<p>Before we move onto some more fun stuff, it makes sense to spend some time creating some useful utility functions. These functions will be used over and over again through your grid creation experiments and are really useful.</p>
<p>If you just want to see all the utility functions in one place, <a href="https://codepen.io/shubniggurath/pen/OPyPdmm">click here</a>. It can be useful to add some console logs to see what comes out of them.</p>
<h3>Lerp</h3>
<p>A pretty standard lerp function. This function returns a number between <em>a</em> and <em>b</em> at the point represented by <em>t</em>.</p>
<pre><code>lerp = (a, b, t) =&gt; a + (b - a) * t;</code></pre>
<p>For example:</p>
<pre><code>lerp(100, 200, .5) // 150</code></pre>
<p>Other people have explained lerp far better than I ever could. For a really in-depth look at, take a look at <a href="https://www.youtube.com/watch?v=YJB1QnEmlTs">SimonDev's great YouTube video</a>.</p>
<h3>Grid positions</h3>
<p>When you're programming grids, it often makes sense to store them 1-dimensionally, that is: in an array. This makes things relatively simple and safe from a developmental perspective, but can cause challenges in trying to think about them. This being the case, you can create a couple of functions that make conversion to and from the array index (ID) trivial.</p>
<p>I explained these a bit in my previous post, see here.</p>
<p>But we can take this further. If we think through our grid, each cell is made up of 4 points and 4 edges, so given the cell ID, it would be super convenient to be able to find, those. Too.</p>
<pre><code>function getPointsForGridId(id, w) {
  const { x: col, y: row } = getGridPosition(id, w);
  return [
    { x: col, y: row }, // top left
    { x: col + 1, y: row }, // top right
    { x: col + 1, y: row + 1 }, // bottom right
    { x: col, y: row + 1 }, // bottom left
  ];
}</code></pre>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/5a0808dc8cb19402d588a6da97405c2cca54f0b6-998x626.png?w=800" alt="" />
<p>This function takes the cell ID (<em>id</em>) and the grid width (<em>w</em>) and returns an array containing the points that make the cell up in a clockwise direction from the top-left.</p>
<h3>Edge IDs</h3>
<p>Similarly, it can be very useful to be able to derive the IDs of edges from the grid cell to which they belong. This way you can create deterministic functionality based on the edge, without having to store a state for the edge.</p>
<p>Remember, these functions expect grid-specific units. So width and height in the number of cells in each direction, and points and positions in grid-relative coordinates ([1,1], [2, 1], [3, 1] etc).</p>
<pre><code>getHorizontalEdgeId = (x, y, w) =&gt; x * w + y;</code></pre>
<p>This function returns a unique ID for a <strong>horizontal edge</strong>. It's defined by the <em>x</em> position of its left endpoint and the <em>y</em> position of both its endpoints (the y position will be the same for both points). The w parameter expects the <strong>width</strong> of the grid, which helps calculate a unique ID for each horizontal edge based on its position within the grid.</p>
<pre><code>getVerticalEdgeId = (x, y, w, h) =&gt; 
  w * h + w + (x * h + y);</code></pre>
<p>Similarly, this function returns the unique ID of a <strong>vertical edge</strong>. </p>
<p>It takes the <em>x</em> position of both its endpoints and the <em>y</em> position of its top endpoint. The w and h parameters represent the <strong>width</strong> and <strong>height</strong> of the grid, respectively. </p>
<p>We add w * h + w to the calculated vertical edge position to ensure its ID is distinct from any possible horizontal edge IDs. This design effectively creates a continuous sequence of edge IDs, where all horizontal edge IDs come first, followed by all vertical edge IDs, as if they were laid out in a single array.</p>
<pre><code>export function getEdgeIDBetweenPoints(a, b, w, h) {
  // Check for horizontal adjacency
  if (a.y === b.y &amp;&amp; Math.abs(a.x - b.x) === 1) {
    const canonicalX = Math.min(a.x, b.x);
    return getHorizontalEdgeId(canonicalX, a.y, w);
  }
  // Check for vertical adjacency
  if (a.x === b.x &amp;&amp; Math.abs(a.y - b.y) === 1) {
    const canonicalY = Math.min(a.y, b.y);
    return getVerticalEdgeId(a.x, canonicalY, w, h);
  }

  console.warn(&quot;No edge between these points&quot;);
  return null;
}</code></pre>
<p>This function is a helper that figures out if two given points, <strong>a</strong> and <strong>b</strong>, are connected by an edge in the grid. If they are, it then calls the correct function (<em>getHorizontalEdgeId</em> or <em>getVerticalEdgeId</em>) to get that edge's unique ID.</p>
<p>It first checks if the points are <strong>horizontally adjacent</strong> (same <em>y, x</em> coordinates one unit apart). If so, it uses the smaller <em>x</em> value to get the horizontal edge ID.</p>
<p>If not horizontal, it then checks for <strong>vertical adjacency</strong> and gets the vertical edge ID.</p>
<h3>Joining the dots</h3>
<p>Joining the concepts of points and edges together is relatively straightforward at this point, we have all of the pieces we need to retrieve the 4 grid IDs, given a cell ID.</p>
<pre><code>function getEdgeIdsForGridId(id, w, h) {
  const points = getPointsForGridId(id, w, h);
  return [
    getEdgeIDBetweenPoints(
      points[0], 
      points[1], 
      w, h), // top edge
    getEdgeIDBetweenPoints(
      points[1], 
      points[2], 
      w, h), // right edge
    getEdgeIDBetweenPoints(
      points[2], 
      points[3], 
      w, h), // bottom edge
    getEdgeIDBetweenPoints(
      points[3], 
      points[0], 
      w, h), // left edge
  ];
}</code></pre>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/59db0e60d051bc2000e00f2908d8d95368a65b5f-998x626.png?w=800" alt="" />
<p>This function takes the cell ID and the grid width and height (<em>h</em>) and returns an array of the IDs of the edges that make up the cell.</p>
<p>Take a look at this <a href="https://codepen.io/shubniggurath/pen/QwjbNEq?editors=1111">codepen link</a> to see a demonstration of all of these functions in action.</p>
<h3>Making something of all this</h3>
<p>Alright, so what can we do with all this? Let's use the information we can derive about grids, using these functions, to draw a random-seeming, but functional generative system.</p>
<h4>Boilerplating</h4>
<p>We'll need some basic configuration, and probably a control panel so we can play around with our config.</p>
<pre><code>import { Turtleman } from &quot;https://esm.sh/turtleman@1.0.7&quot;;
import { Pane } from &quot;https://cdn.jsdelivr.net/npm/tweakpane@4.0.5/dist/tweakpane.min.js&quot;;
import {
  lerp,
  getPointsForGridId,
  getEdgeIdsForGridId,
} from &quot;https://codepen.io/shubniggurath/pen/OPyPdmm.js&quot;;

const CONFIG = {
  width: 800,
  height: 800,
  gridW: 20,
  gridH: 20,
};
// --- PANE ---
const pane = new Pane();
const f1 = pane.addFolder({
  title: &quot;Config&quot;,
  
});
f1.addBinding(CONFIG, &quot;width&quot;);
f1.addBinding(CONFIG, &quot;height&quot;);
f1.addBinding(CONFIG, &quot;gridW&quot;, {
  step: 1,
  min: 1,
  max: 100,
});
f1.addBinding(CONFIG, &quot;gridH&quot;, {
  step: 1,
  min: 1,
  max: 100,
});
f1.on(&quot;change&quot;, (ev) =&gt; {
  rebuild();
});


// --- MAIN ---
let toy;
const rebuild = () =&gt; {
  if (toy) toy.element.remove();

  const { width, height, gridW, gridH } = CONFIG;
  const cellWidth = width / gridW;
  const cellHeight = height / gridH;

  toy = new Turtleman({
    width,
    height,
    strokeWidth: 2,
    angleType: &quot;radians&quot;,
  });
  container.appendChild(toy.element);
}
rebuild()</code></pre>
<p>This just sets up the imports, the basic configuration (canvas dimensions and grid dimensions) and the control panel that'll allow you to play with these in real time. It also sets a rebuild function which is where most of our work will take place.</p>
<h4>Create our state objects</h4>
<p>These objects will hold critical information about the grid.</p>
<pre><code>  const cells = [...Array(gridW * gridH)].map((_, i) =&gt; {
    return {
      points: getPointsForGridId(i, gridW),
      edges: getEdgeIdsForGridId(i, gridW, gridH),
      direction: Math.random() &gt; 0.5 ? 1 : -1,
    };
  });</code></pre>
<p>Cells is an array of objects that define each cell's properties, including the points that make it up, the edge IDs, and the direction, which we'll use to change the way the cell behaves. Notice direction has an equal chance of being either 1 or -1.</p>
<h4>Draw in the cell</h4>
<p>For each cell, we want to draw a line on the halfway point of each edge to an adjacent edge.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/ee20ddb74110c5de953213c20912ac6e665981f9-998x626.png?w=800" alt="" />
<p>Picking which adjacent edge - the left or right - is based on that direction variable we defined earlier. And so you can imagine, but repeating this process, we can get very simple rule-based generation that ends up looking quite complex.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/8904b66dee0556dbef1c090263bb57de41969dc0-998x626.png?w=800" alt="" />
<p>Alright, let's create our basic loop and then iterate from there.</p>
<pre><code>
  cells.forEach((cell) =&gt; {
    const edgeDirection = cell.direction;

    const topEdgePoint = {
      x: lerp(cell.points[0].x, cell.points[1].x, 0.5),
      y: cell.points[0].y,
    };
    const bottomEdgePoint = {
      x: lerp(cell.points[3].x, cell.points[2].x, 0.5),
      y: cell.points[2].y,
    };
    const leftEdgePoint = {
      x: cell.points[0].x,
      y: lerp(cell.points[0].y, cell.points[3].y, 0.5),
    };
    const rightEdgePoint = {
      x: cell.points[1].x,
      y: lerp(cell.points[1].y, cell.points[2].y, 0.5),
    };
    
    let a,b,c,d;
    if (edgeDirection === 1) {
      a = topEdgePoint;
      b = rightEdgePoint;
      c = bottomEdgePoint;
      d = leftEdgePoint;
    } else {
      a = topEdgePoint;
      b = leftEdgePoint;
      c = bottomEdgePoint;
      d = rightEdgePoint;
    }
    
    toy.jump(a.x * cellWidth, a.y * cellHeight);
    toy.goto(b.x * cellWidth, b.y * cellHeight);
    toy.jump(c.x * cellWidth, c.y * cellHeight);
    toy.goto(d.x * cellWidth, d.y * cellHeight);
  });</code></pre>
<p>This loops through the cell array we created earlier, creates points at the halfway point of each edge and then assembles the points to connect, and draws them. There are a couple of smaller pieces of code in here that it makes sense to take a closer look at.</p>
<pre><code>    const topEdgePoint = {
      x: lerp(cell.points[0].x, cell.points[1].x, 0.5),
      y: cell.points[0].y,
    };</code></pre>
<p>This is the code used for finding the point half way along the top edge. You can see we take the interpolation of the top-left point's x position and the top-right point's x position using an interpolant of 0.5 (for half way). This is then repeated for each side.</p>
<pre><code>    let a,b,c,d;
    if (edgeDirection === 1) {</code></pre>
<p>This code just assembled the 4 points to use for drawing. Two lines are drawn for each cell: a-&gt;b and c-&gt;d. We just swap b and d depending on the cell's direction.</p>
<p>You can see this code in action over on <a href="https://codepen.io/shubniggurath/pen/WbQbPjd">codepen</a>. What other sorts of patterns can you make with this system?</p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/WbQbPjd">https://codepen.io/shubniggurath/pen/WbQbPjd</a></p>]]></description>
            <link>https://www.surface-detail.com/posts/some-useful-functions</link>
            <guid isPermaLink="false">e2c340ab-7026-4154-80ee-736a5c132595</guid>
            <dc:creator><![CDATA[Surface Detail]]></dc:creator>
            <pubDate>Sun, 13 Jul 2025 14:21:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[On Grids]]></title>
            <description><![CDATA[<p>Grids form a backbone for so many generative art projects, from providing a structural canvas onto which to create, to providing convenient methods by which to group disparate objects. Learning how to create, use, and express ideas with grids is essential to learning to be creative with code.</p>
<h3>Grids as a structural canvas</h3>
<p>At their core, a grid is a framework of organized cells (usually square, but things can get exotic - more on that later) that divide space into smaller, uniform (and sometimes non-uniform, again, more on that later) units. Depending on what and how you're coding, these individual units - or cells - can provide various uses in generative art (and beyond).</p>
<ul><li><strong>Individual units</strong>
As an individual unit, a grid cell can provide a discrete area onto which you can draw, providing a method by which to create highly complex appearing patterns with a relatively low cost. Some examples here might include <a href="https://en.wikipedia.org/wiki/Truchet_tiles">Truchet tiles</a>, <a href="https://en.wikipedia.org/wiki/Wang_tile">Wang tiles</a> or <a href="https://10print.org/">10 PRINT</a>.</li><li><strong>Performance</strong>
Grids are very frequently used in programming to provide convenient ways to break down space for the purpose of optimizing in-simulation elements using spatial indexation and fast lookups. Think of how expensive collision-detection is in video games. By breaking the game field down into grids, you can perform collision detection against limited sub-sets of components, only calculating against elements in the neighbourhood. See: <a href="https://en.wikipedia.org/wiki/Quadtree">Quadtrees</a>, <a href="https://en.wikipedia.org/wiki/Octree">Octrees</a>.</li><li><strong>State machines</strong>
When you use a grid as a state machine, each cell becomes an individual <a href="https://en.wikipedia.org/wiki/Finite-state_machine#:~:text=A%20finite%2Dstate%20machine%20(FSM,states%20at%20any%20given%20time.">finite state machine</a>. The state of a cell could be anything relevant to your generative art project: c colour, a numerical value, a boolean, or even a complex object.
The core idea is that the state of a cell at any given iteration is determined by its own previous state and the states of its neighbours. This idea is the very basis of an entire branch of generative systems called <a href="https://en.wikipedia.org/wiki/Cellular_automaton">cellular automata</a>. Some further reading: <a href="https://en.wikipedia.org/wiki/Elementary_cellular_automaton">Elementary cellular automaton</a>, <a href="https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life">Conway's Game of Life</a>, <a href="https://en.wikipedia.org/wiki/Reaction%E2%80%93diffusion_system">Reaction Diffusion</a>.</li><li><strong>Predictable determinism</strong>
When used within a system that makes use of a hashing function, grids provide a very convenient way of addressing cells in a predictable way. This means that a grid cell, passed to a hashing function, can produce the same random-seeming number every time. This sort of system provides a very convenient way to know the state of a cell without needing a lookup object. This concept forms the very core of a many shader programs.</li></ul>
<p>Alright, that's a lot, and I really hope to get into practical demonstrations of a lot of them, but for right now let's get into a program. Using our previously created Turtleman class (I'll want to plot this later on), we can now program up. basic grid and then iterate on that, making it more complex and interesting!</p>
<h3>A basic grid</h3>
<p>If you just want to jump straight into the code, please feel free just to jump straight on over to the <a href="https://codepen.io/shubniggurath/pen/ZYGgWGE">Codepen</a> example.</p>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/ZYGgWGE">https://codepen.io/shubniggurath/pen/ZYGgWGE</a></p>
<p>TA-DAA!
Alright, I know this doesn't look all that impressive, but bear with me, I promise we'll get to some more interesting stuff. The purpose of this program is to demonstrate some of the fundamental concepts, ideas and functions that go into creating a grid using a program like Turtleman. The methods and approaches will change somewhat, depending on the technology, but the core reasons for programming a grid remain largely the same. Onward!</p>
<h4>The grid itself</h4>
<p>Alright, the core of the grid is the structure you use to describe your grid. This can be accomplished in many different ways, the use of which should be determined by what you want to achieve, but for our purposes I'm going to chose a straightforward structure that hopefully keeps things clear and simple.</p>
<p>Let's start with our primary configuration variables, these will provide all of the information we need to create a grid,</p>
<pre><code>const width = 1000,
  height = 800,
  // number of grid columns
  gridW = 5,
  // Number of grid rows
  gridH = 4,
  // Width of a cell in pixels
  gridWFactor = width / gridW,
  // Height of a cell in pixels
  gridHFactor = height / gridH; </code></pre>
<p>We're going to create our grid in a 1-dimensional array. You could conceivably do yours in a 2 dimensional array, which can be easier to think of as you have an array representing rows, and then each of those arrays contains an array representing the columns and cells. I prefer 1-dimensional arrays because they're more resilient and, once you get your head around the maths to convert index to rows and columns, and vice-versa, they're very straightforward.</p>
<p>Ok, so to start with, to convert from array index to rows and columns looks like this:</p>
<pre><code>getGridPosition = (index, columns) =&gt; {
  return {
    column: index % columns, 
    row: Math.floor(index / columns)
  };
}</code></pre>
<p>This function takes an index (representing the index in the 1-dimensional array) and an integer (representing the number of columns in the grid) and returns the cells position in the grid. Column is calculated as the modulo of the index to the columns (so if you have 8 columns, and the provided index is 12, the column will be 4). Row is calculated as the floor of the index divided by the number of columns. </p>
<p>Similarly we want a function to turn a grid position into an array index. This can be achieved using something like this:</p>
<pre><code>getGridPosition = (x, y, columns) =&gt; {
  return y*columns+x;
}</code></pre>
<p>This is just the reverse of the previous function. Please take some time to try to understand these functions, if you need to. They're very powerful and used frequently.</p>
<p>Alright, now we can finally create the grid structure.</p>
<pre><code>const gridCells = [];
for(let i=0;i&lt;gridW*gridH;i++) {
  const row = Math.floor(i / gridW);
  const col = i % gridW;

  const cell = {};
  cell.points = [
    { x: col, y: row },
    { x: col + 1, y: row },
    { x: col + 1, y: row + 1 },
    { x: col, y: row + 1 },
  ];
  cell.bounds = {
    x: col * gridWFactor,
    y: row * gridHFactor,
    w: gridWFactor,
    h: gridHFactor
  }
  gridCells.push(cell);
}</code></pre>
<p>So you can see, we create a 1-dimensional array for storing the grid cells, loop through the whole number of cells we have and construct a cell object with:</p>
<ul><li>Points
The 4 points that make up the cell, clockwise from the top-left.</li><li>Bounds
The bounds of the cell, in pixels.</li></ul>
<h4>Aside, some drawing functions</h4>
<p>Before we get into some more grids, I wanted to take a moment to look at some useful drawing functions. These use the Turtleman API and provide a convenient way to draw some interesting shapes.</p>
<pre><code>const drawCircle = (r, x, y, steps) =&gt; {
  toy.seth(0);
  const TAU = Math.PI * 2;
  const step = (TAU * r) / steps;
  toy.jump(x-step/2, y-r);
  for(let i=0;i&lt;steps;i++) {
    toy.forward(step);
    toy.right(TAU/steps);
  }
}</code></pre>
<p>The circle draws a circle by moving into the position (x,y) at the top of the circle and then drawing forward and turning right by an amount equal to: ( 2π * radius (r)) / number of steps.</p>
<pre><code>const drawSquare = (r, x, y) =&gt; {
  toy.seth(0);
  toy.jump(x - r, y - r);
  toy.forward(r * 2);
  toy.right(1.5708);
  toy.forward(r * 2);
  toy.right(1.5708);
  toy.forward(r * 2);
  toy.right(1.5708);
  toy.forward(r * 2);
};</code></pre>
<p>The square moves to the top-left of the square shape as defined by the x, y and radius (width) of the square. Then it walks forward, and turns right (1.57 = 90 degrees) repeating until the square is complete.</p>
<pre><code>const drawTriangle = (r, x, y) =&gt; {
  toy.seth(0);
  toy.jump(x, y - r);
  toy.goto(x + r, y + r);
  toy.goto(x - r, y + r);
  toy.goto(x, y - r);
};</code></pre>
<p>Draws a triangle by first jumping to its top vertex, defined by the x and y coordinates of its centre and its radius. It then uses goto commands to draw lines connecting the top vertex to the bottom-right vertex, then to the bottom-left vertex, and finally back to the starting point.</p>
<pre><code>const drawDiamond = (r, x, y) =&gt; {
  toy.jump(x, y - r);
  toy.right(1.5708 / 2);
  toy.goto(x + r, y);
  toy.goto(x, y + r);
  toy.goto(x - r, y);
  toy.goto(x, y - r);
};</code></pre>
<p>The diamond shape is drawn by first jumping to the top-most point of the diamond, as defined by its centre coordinates and r radius. Then, it draws lines connecting the top point to the right point, then to the top point etc.</p>
<pre><code>const drawFlower = (r, x, y, steps) =&gt; {
  toy.seth(0);
  const TAU = Math.PI * 2;
  const step = TAU / steps;
  for (let i = 0; i &lt;= steps; i++) {
    const _r = r + Math.sin(i * step * 10) * 30;
    const _x = Math.cos(i * step) * _r + x;
    const _y = Math.sin(i * step) * _r + y;
    if (i === 0) toy.jump(_x, _y);
    else toy.goto(_x, _y);
  }
};</code></pre>
<p>The drawFlower function draws a flower-like shape by iterating through a series of steps around a central point, defined by x and y. In each step, it calculates a slightly varied radius using a sine wave, which creates the &quot;petal&quot; effect. It then determines the _x and _y coordinates for that step based on the varied radius and the current angle.</p>
<h4>Finally, let's draw the grid!</h4>
<p>Alright, the first thing we want to do is just draw the cells as squares so we can see our basic grid!</p>
<pre><code>gridCells.forEach((grid) =&gt; {
  const { bounds, points } = grid;
  
  // Draw a squre representing each grid item
  toy.jump(
    bounds.x, 
    bounds.y
  );
  toy.goto(
    bounds.x + bounds.w, 
    bounds.y
  );
  toy.goto(
    bounds.x + bounds.w, 
    bounds.y + bounds.h
  );
  toy.goto(
    bounds.x, 
    bounds.y + bounds.h
  );
  toy.goto(
    bounds.x, 
    bounds.y
  );
});</code></pre>
<p>And all things being equal, you should see your beautiful grid drawn on screen!</p>
<h4>Round up and next steps</h4>
<p>Alright, I know that was a lot of reading for such a basic looking thing, but I promise it'll pay off - there are a lot of places you can go from here. Please take a look at the <a href="https://codepen.io/shubniggurath/pen/ByNXNMX?editors=1111">Codepen</a> to see some additional bits, and try some other drawing functions in those cells yourself.</p>
<p>Extra: I'm creating the meta images for these articles using the Turtleman class, you can see this one over on <a href="https://codepen.io/shubniggurath/pen/qEdeVmM">Codepen</a>.</p>]]></description>
            <link>https://www.surface-detail.com/posts/on-grids</link>
            <guid isPermaLink="false">610859fa-56f0-4b6e-b46a-cfdb5fe9e8f4</guid>
            <dc:creator><![CDATA[Surface Detail]]></dc:creator>
            <pubDate>Sat, 05 Jul 2025 18:59:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Turtles all the way down]]></title>
            <description><![CDATA[<p>I've been meaning to get back into working with plotted generative art for some time now, and I have a few ideas for the development of a few posts about it. But in order to get there, I wanted a simple, SVG based canvas to create such artwork. There are a number of prebuilt options out there - paper.js, SCG.js etc. - but they're either too fully-featured or too opinionated for what I want to achieve.</p>
<p>Something that's always been really cool in this space is the <a href="https://en.wikipedia.org/wiki/Turtle_graphics">turtle graphics API</a>. It's super simple, and very easy to understand. It uses a convention of a turtle that has three attributes - location, orientation, and a pen. You send commands to the turtle to change one of these properties in a variety of ways, and it draws lines.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/2b50298a7c84fce6026f151e81b6ccde8bc0f624-250x250.gif?w=800" alt="A turtle moving around and drawing a star." />
<p>The turtle moves around the canvas with simple-to-understand commands like forward, backward, left, right etc.</p>
<p>There is a fantastic website called <a href="https://turtletoy.net/">Turtletoy</a>, created and operated by the incredibly talented <a href="https://reindernijhoff.net/">Reinder Nijhoff</a>, that implements this API using javascript and provides a community tool to allow people to create and publish tutles. Make sure you check it out, there is some incredible work over there.</p>
<p>So I've created a simple class called <em>Turtleman</em> that allows me to create these sorts of drawings, you can check it out on <a href="https://codepen.io/shubniggurath/pen/dPoEyOy">codepen</a> or <a href="https://github.com/liamegan/Turtleman">github</a>. This library provides a relatively simple API, as demonstrated here:</p>
<pre><code class="language-javascript">const toy = new Turtleman();
container.appendChild(toy.svg);
const squareCommands = `
  forward 100
  right 90
  forward 100
  right 90
  forward 100
  right 90
  forward 100
`;
toy.drawCommands(squareCommands);</code></pre>
<p>CodePen iframe: <a href="https://codepen.io/shubniggurath/pen/dPoEyOy">https://codepen.io/shubniggurath/pen/dPoEyOy</a></p>
<p>What's cool about this is that you can generate some relatively complex drawings with relatively simple building blocks. Here's the code to generate the spiral in the above example:</p>
<pre><code> // Coil spacing
const tightness = .6;
 // The distance between points
const step_size = 3;
// Current angle in radians
let theta = 0;
// Current radius
let radius = .1; 
i = 0;
while (radius &lt; 500) {
  const d_theta = step_size / 
    Math.sqrt(
      tightness * tightness + 
      radius * radius
    );
  theta += d_theta;
  radius = tightness * theta;
  let x = Math.cos(theta) * radius + 
    toy.width / 2;
  let y = Math.sin(theta) * radius + 
    toy.height / 2;
  toy.goto(x, y);
}</code></pre>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/40a787b57ece522c847f62daafed0c91c9b87c3d-500x315.png?w=800" alt="" />
<p>But with some relatively straightforward changes, you can create something which appears much more complex.</p>
<pre><code>const tightness = .6;
const step_size = 3;
let theta = 0;
let radius = .1;
i = 0;
while (radius &lt; 500) {
  const d_theta = step_size / 
    Math.sqrt(
      tightness * tightness + 
      radius * radius
    );
  theta += d_theta;
  radius = tightness * theta;
  let x = Math.cos(theta) * radius + 
    toy.width / 2;
  let y = Math.sin(theta) * radius + 
    toy.height / 2;

  const scale = .04;
  const size = 20;
  x += (
    Math.sin((x*scale)) + 
    Math.cos(y*scale)) * size;
  y += (
    Math.cos((x*scale)) + 
      Math.sin(y*scale)) * size;

  toy.goto(x, y);
}</code></pre>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/a79a4192985309c8a73cc257a47906af834670f1-500x313.png?w=800" alt="" />
<h3>What's next?</h3>
<p>I think I want to do a series of posts on generative art using this system. Probably starting with a breakdown of grids and their uses with systems like this.</p>]]></description>
            <link>https://www.surface-detail.com/posts/turtles-all-the-way-down</link>
            <guid isPermaLink="false">590b73ec-15e3-4a61-96fc-ca10eb4d4985</guid>
            <dc:creator><![CDATA[Surface Detail]]></dc:creator>
            <pubDate>Tue, 01 Jul 2025 17:25:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Old man energy]]></title>
            <description><![CDATA[<h3>The interview</h3>
<p>I was in an interview yesterday and the topic came around the the candidate's website. The development manager (the person who would be managing this person) asked them about their &quot;digital garden&quot;, and the candidate proceeded to describe their personal website in the following general terms:</p>
<ul><li>A personal space where I can add my thoughts.</li><li>Where I can learn in public.</li><li>Where it can grow and evolve as I do.</li><li>Where I can just add whatever I want to.</li></ul>
<p>Everything old is new again. I vividly remember the early days of the internet. I remember the early blog space, with people furiously creating, experimenting, and sharing. I remember Geocities, where everyone had an opportunity to do all of the above, creating a space that was just about them. Admittedly, inevitably, it was mostly just a bunch of people's favourite gif files and a bunch of promises to add more content, complete with pictures of hard-hats and orange and black booms emblazoned with &quot;Under construction&quot; - hell even the Simpsons referenced this - but it was still an exciting time.</p>
<img src="https://cdn.sanity.io/images/wrgwufkn/production/7762b1d466aacc5db4bf1d289ff80e222a8c8d2a-500x320.gif?w=800" alt="Homer's Web Page" />
<p>All of this to say that suddenly I felt like an old man. That early internet was too soon replaced with the convenience, polish, and in-built discoverability of social media, starting with MySpace and eventually becoming today's wasteland of an online social landscape. Depressing though The Website Formally Known As Twitter, Facebook, and LinkedIn have become today, they were started with laudable intentions,</p>
<p>In the world of AI (and, subsequently, AI slop), where the rise of influencer culture has created a straight line between loud voices and profitability, and where the resultant culture shift has made it such that sincerity is largely sacrificed on the alter of popularism, where people use their platforms - big and small - to broadcast to the world their opinions and outrage, conflating tribalism and authenticity. It was such an encouraging conversation to talk to someone about their humble website and blog, to hear them talk about the small rewards of experimentation, communication and learning.</p>
<h3>Learning in public</h3>
<p>One idea this person presented that really struck home was this idea of &quot;learning in public&quot;. It's such a nice idea: putting out to the world what you're learning <em>as</em> you're learning it. Creating a cycle of experimentation, creation, learning and communication. It really is what we were doing in those early days, but to hear it put so eloquently was fantastic. Maybe it's a well known term that I'm only just getting around to learning now (see: OME), but it's encouraged me to pick up these tools and do exactly that.</p>
<p>In the course of my career, some of the most enjoyable moments were when I was playing, putting something out there for people to use, and play with, and develop their own ideas off of. It's why I've always been drawn to open source software, outside of the core philosophical ideas inherent to the OSS community, ideas just improve if they're made free.</p>
<h3>OME</h3>
<p>It's always funny having those moments where I'm in conversation with someone and they say or do something that feels so familiar, but so disconnected in time that my first reaction is to say something like &quot;that's something we used to do in the old days!&quot;. That there is some good old man energy (OME).</p>
<h3>CTA</h3>
<p>So this is a call to action, to myself I guess, to go back to my roots and start learning in public again. I don't know if this site will go anywhere, and I don't know if anyone will read this, but I'm going to try.</p>
<p>Here's what I really hope to achieve here:</p>
<ul><li>Gather my thoughts when/how(/if?) I have them.</li><li>Communicate my learnings.
It can be very easy to get lost in the process of learning and experimentation, so hopefully this can be a way to shortcut the resultism I often feel when achieving a goal.</li><li>Track side-quests.
When learning/experimenting, some of the greatest discoveries come from &quot;happy accidents&quot;, but those can often be very distracting from original goals.
Inspiration is the better part of mistakes (that's the saying, right?).</li></ul>]]></description>
            <link>https://www.surface-detail.com/posts/old-man-energy</link>
            <guid isPermaLink="false">66c518de-29dc-42ba-8f94-6946771a2111</guid>
            <dc:creator><![CDATA[Surface Detail]]></dc:creator>
            <pubDate>Thu, 26 Jun 2025 19:57:00 GMT</pubDate>
        </item>
    </channel>
</rss>