<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>blog</title>
    <link>https://jpcharalambosh.co/</link>
    <description>Recent content on blog</description>
    <image>
      <title>blog</title>
      <url>https://jpcharalambosh.co/papermod-cover.png</url>
      <link>https://jpcharalambosh.co/papermod-cover.png</link>
    </image>
    <generator>Hugo</generator>
    <language>en</language>
    <lastBuildDate>Mon, 18 May 2026 11:20:13 -0500</lastBuildDate>
    <atom:link href="https://jpcharalambosh.co/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Streamlining Minesweeper Revisited</title>
      <link>https://jpcharalambosh.co/posts/minesweeper_revisited/</link>
      <pubDate>Mon, 18 May 2026 11:20:13 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/minesweeper_revisited/</guid>
      <description>&lt;p&gt;In object-oriented programming (OOP), maximizing API use streamlines development, leading to simpler and more concise code. This clarity not only improves readability but also eases debugging. Leveraging an API effectively in OOP thus ensures a blend of simplicity, efficiency and maintainability in software development.&lt;/p&gt;
&lt;p&gt;This demo (partially) implements the &lt;a href=&#34;https://en.wikipedia.org/wiki/Minesweeper_%28video_game%29&#34;&gt;minesweeper&lt;/a&gt; video game using &lt;a href=&#34;https://beta.p5js.org/&#34;&gt;p5-v2&lt;/a&gt; and the &lt;a href=&#34;https://objetos.github.io/p5.quadrille.js/&#34;&gt;Quadrille API&lt;/a&gt; with a four-layer strategy. Each layer is one quadrille with a single responsibility:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mines&lt;/code&gt;&lt;/strong&gt; — the bombs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;counts&lt;/code&gt;&lt;/strong&gt; — the numbers next to bombs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cover&lt;/code&gt;&lt;/strong&gt; — a two-tone internal layer (red over filled cells, green over empty cells) that distinguishes the two click zones and drives flood fill.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mask&lt;/code&gt;&lt;/strong&gt; — the uniformly magenta layer the player actually sees, derived from &lt;code&gt;cover&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As gameplay progresses, clicking on cells uncovers them by clearing either individual or connected cells from &lt;code&gt;cover&lt;/code&gt;; &lt;code&gt;mask&lt;/code&gt; is re-derived from &lt;code&gt;cover&lt;/code&gt; after each click.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Streamlining Minesweeper</title>
      <link>https://jpcharalambosh.co/posts/minesweeper/</link>
      <pubDate>Wed, 13 May 2026 16:43:48 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/minesweeper/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;strong&gt;See also&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A four-layer refinement of this design — splitting &lt;code&gt;board&lt;/code&gt; into single-purpose &lt;code&gt;mines&lt;/code&gt; and &lt;code&gt;counts&lt;/code&gt; quadrilles, then using &lt;a href=&#34;https://objetos.github.io/p5.quadrille.js/docs/algebra/or/&#34;&gt;&lt;code&gt;Quadrille.or&lt;/code&gt;&lt;/a&gt; to merge them into the two-tone &lt;code&gt;cover&lt;/code&gt; in two lines — appears in &lt;a href=&#34;https://jpcharalambosh.co/posts/minesweeper_revisited/&#34;&gt;Streamlining Minesweeper, revisited&lt;/a&gt;. Same game, each variable with one fixed meaning throughout.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In object-oriented programming (OOP), maximizing API use streamlines development, leading to simpler and more concise code. This clarity not only improves readability but also eases debugging. Leveraging an API effectively in OOP thus ensures a blend of simplicity, efficiency and maintainability in software development.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Visibility testing</title>
      <link>https://jpcharalambosh.co/posts/visibility_testing/</link>
      <pubDate>Sat, 25 Apr 2026 15:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/visibility_testing/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Per-object frustum culling with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#visibility-testing&#34;&gt;&lt;code&gt;visibility&lt;/code&gt;&lt;/a&gt;. Sixty mixed primitives drift around in a wide volume; each frame their bounds — AABBs for boxes, spheres for spheres — are tested against a fixed test camera&amp;rsquo;s frustum, with the six frustum planes pre-computed once per frame via &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#visibility-testing&#34;&gt;&lt;code&gt;bounds({ mat4Eye })&lt;/code&gt;&lt;/a&gt;. Three return states drive the render: solid lit material for &lt;code&gt;VISIBLE&lt;/code&gt;, amber wireframe for &lt;code&gt;SEMIVISIBLE&lt;/code&gt; (the bounds straddle a plane), faint grey for &lt;code&gt;INVISIBLE&lt;/code&gt;. Toggle the test camera&amp;rsquo;s projection between perspective and orthographic with the gold checkbox; drag to orbit the observer and watch the cyan wireframe stay put while objects drift through it.&lt;/p&gt;</description>
    </item>
    <item>
      <title>CPU proximity picking</title>
      <link>https://jpcharalambosh.co/posts/cpu_proximity_picking/</link>
      <pubDate>Sat, 25 Apr 2026 14:30:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/cpu_proximity_picking/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Zero-GPU hit testing with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#cpu-proximity-picking&#34;&gt;&lt;code&gt;mouseHit&lt;/code&gt;&lt;/a&gt;. Ten waypoint nodes around a soft loop — each one tested every frame against the cursor by projecting its origin to screen space and comparing against a configurable radius. The hit zone is drawn explicitly with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#cpu-proximity-picking&#34;&gt;&lt;code&gt;bullsEye&lt;/code&gt;&lt;/a&gt;, which takes the same &lt;code&gt;size&lt;/code&gt; and &lt;code&gt;shape&lt;/code&gt; parameters as &lt;code&gt;mouseHit&lt;/code&gt; — the gizmo &lt;em&gt;is&lt;/em&gt; the hit zone, no guessing where the click registers. Use the panel to tune the radius and switch between circular and square hit shapes.&lt;/p&gt;</description>
    </item>
    <item>
      <title>GPU color-ID picking</title>
      <link>https://jpcharalambosh.co/posts/gpu_color_id_picking/</link>
      <pubDate>Sat, 25 Apr 2026 14:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/gpu_color_id_picking/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Pixel-perfect mouse picking with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#gpu-color-id-picking&#34;&gt;&lt;code&gt;mousePick&lt;/code&gt;&lt;/a&gt;. Fifty mixed primitives — boxes, spheres, torii, cones, cylinders — drawn at random positions and tagged with a unique integer id encoded as a CSS hex colour by &lt;a href=&#34;https://github.com/VisualComputing/p5.tree#gpu-color-id-picking&#34;&gt;&lt;code&gt;tag&lt;/code&gt;&lt;/a&gt;. Each frame the scene renders twice: once into a 1×1 framebuffer with each shape filled by its id, then &lt;code&gt;gl.readPixels&lt;/code&gt; returns whichever id sits under the cursor; the visible pass renders normally and lights up the hit. Cost is one extra geometry submission per frame regardless of object count, and the answer is exactly the rendered pixel — through the hole of a torus, behind a partially-occluding box, anywhere a real fragment landed.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Camera interpolation</title>
      <link>https://jpcharalambosh.co/posts/camera_interpolation/</link>
      <pubDate>Sun, 19 Apr 2026 12:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/camera_interpolation/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Lookat-camera keyframe animation with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createCameraTrack&lt;/code&gt;&lt;/a&gt;. A bound &lt;code&gt;animCam&lt;/code&gt; plays a four-keyframe track; a separate &lt;code&gt;viewCam&lt;/code&gt; orbits around the abstraction — eye polyline, gaze rays, per-keyframe mini-camera markers. A live frustum follows playback, and an FBO inset shows what &lt;code&gt;animCam&lt;/code&gt; actually sees. Toggle &lt;code&gt;TEX&lt;/code&gt; and that inset slides onto the live frustum&amp;rsquo;s near plane — the frustum becomes a window. Two &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createPanel&lt;/code&gt;&lt;/a&gt; instances drive the whole thing: one binds checkboxes to UI state, one binds transport controls to the track.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Pose interpolation</title>
      <link>https://jpcharalambosh.co/posts/pose_interpolation/</link>
      <pubDate>Sun, 19 Apr 2026 10:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/pose_interpolation/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TRS keyframe animation with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createPoseTrack&lt;/code&gt;&lt;/a&gt;. Four &lt;code&gt;{ pos, rot }&lt;/code&gt; keyframes, cubic Hermite interpolation of position with auto-Catmull-Rom tangents, and slerp on rotations. A transport panel scrubs the track; a second panel toggles the &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;trackPath&lt;/code&gt;&lt;/a&gt; overlay bits — PATH, CONTROLS, TANGENTS_IN, TANGENTS_OUT — and switches interpolation modes live. Unlike &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createCameraTrack&lt;/code&gt;&lt;/a&gt;, which applies its result to a camera automatically, &lt;code&gt;PoseTrack&lt;/code&gt; produces an interpolated pose you fold into the transform stack yourself with &lt;code&gt;applyPose(track.eval(out))&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
















&lt;iframe
  id=&#39;p5-0&#39;
  style=&#39;border:none; display:block; width:0; height:0;&#39;
  srcdoc=&#34;
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
      &lt;head&gt;
        &lt;style&gt;body{margin:0;padding:0;overflow:hidden}&lt;/style&gt;
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@2.2.3/lib/p5.min.js&#39;&gt;&lt;/script&gt;
        
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.min.js&#39;&gt;&lt;/script&gt;
        
        
        
        
        
        
        
        
        &lt;script&gt;
          
&amp;#39;use strict&amp;#39;

let track
let uiTrack, uiViz
const out = { pos: [0, 0, 0], rot: [0, 0, 0, 1], scl: [1, 1, 1] }

function setup() {
  createCanvas(600, 400, WEBGL)

  // ── PoseTrack ────────────────────────────────────────────────────────────
  track = createPoseTrack()
  track.add({ pos: [-220, -40,   0], rot: { axis: [0, 1, 0], angle:  0         } })
  track.add({ pos: [ -70, -40,  80], rot: { axis: [0, 1, 0], angle:  Math.PI/3 } })
  track.add({ pos: [  70, -40, -80], rot: { axis: [0, 1, 0], angle: -Math.PI/4 } })
  track.add({ pos: [ 220, -40,   0], rot: { axis: [0, 1, 0], angle:  Math.PI/2 } })

  track.play({ loop: true, bounce: true, duration: 90 })

  // ── Transport panel ──────────────────────────────────────────────────────
  uiTrack = createPanel(track, {
    x: 10, y: 10, width: 130,
    title: &amp;#39;PoseTrack&amp;#39;, info: true,
    reset: false, camera: null,
    color: &amp;#39;white&amp;#39;,
  })

  // ── Path viz &amp;#43; interp modes ─────────────────────────────────────────────
  uiViz = createPanel({
    path:        { value: true },
    controls:    { value: true },
    tangentsIn:  { value: true },
    tangentsOut: { value: true },
    posInterp:   { type: &amp;#39;select&amp;#39;, value: &amp;#39;hermite&amp;#39;,
                   options: [{ label: &amp;#39;hermite&amp;#39;, value: &amp;#39;hermite&amp;#39; },
                             { label: &amp;#39;linear&amp;#39;,  value: &amp;#39;linear&amp;#39;  },
                             { label: &amp;#39;step&amp;#39;,    value: &amp;#39;step&amp;#39;    }] },
    rotInterp:   { type: &amp;#39;select&amp;#39;, value: &amp;#39;slerp&amp;#39;,
                   options: [{ label: &amp;#39;slerp&amp;#39;, value: &amp;#39;slerp&amp;#39; },
                             { label: &amp;#39;nlerp&amp;#39;, value: &amp;#39;nlerp&amp;#39; },
                             { label: &amp;#39;step&amp;#39;,  value: &amp;#39;step&amp;#39;  }] },
  }, {
    x: 460, y: 10, width: 130, labels: true,
    title: &amp;#39;Path viz&amp;#39;, color: &amp;#39;white&amp;#39;,
  })

  noFill()
  strokeWeight(1.2)
}

function draw() {
  background(&amp;#39;#0a0a0e&amp;#39;)
  orbitControl()

  stroke(70, 90, 110)
  grid({ size: 400, subdivisions: 8 })

  // live interp modes — read once per frame
  track.posInterp = uiViz.posInterp.value()
  track.rotInterp = uiViz.rotInterp.value()

  // path overlays — each bit in its own ambient stroke colour
  const { PATH, CONTROLS, TANGENTS_IN, TANGENTS_OUT } = p5.Tree
  if (uiViz.path.value())        { stroke(&amp;#39;#e8ecf1&amp;#39;); trackPath(track, { bits: PATH,         marker: null }) }
  if (uiViz.controls.value())    { stroke(&amp;#39;#4a5566&amp;#39;); trackPath(track, { bits: CONTROLS,     marker: null }) }
  if (uiViz.tangentsIn.value())  { stroke(&amp;#39;#5cd0ff&amp;#39;); trackPath(track, { bits: TANGENTS_IN,  marker: null }) }
  if (uiViz.tangentsOut.value()) { stroke(&amp;#39;#ff6ec7&amp;#39;); trackPath(track, { bits: TANGENTS_OUT, marker: null }) }

  // per-keyframe markers — default six-axis cross oriented by the keyframe&amp;#39;s pose
  stroke(180)
  trackPath(track, { bits: 0 })

  // interpolated object at the track cursor
  track.eval(out)
  push()
  applyPose(out)
  stroke(&amp;#39;#ffd166&amp;#39;)
  noFill()
  box(42)
  pop()
}

const mouseWheel = () =&amp;gt; false

        &lt;/script&gt;
        
        &lt;script&gt;
          (function() {
            var t = setInterval(function() {
              var c = document.querySelector(&#39;canvas&#39;)
              if (c &amp;&amp; c.offsetWidth &gt; 0) {
                clearInterval(t)
                window.parent.postMessage({ type: &#39;p5resize&#39;, id: &#39;p5-0&#39;, w: c.offsetWidth, h: c.offsetHeight }, &#39;*&#39;)
              }
            }, 100)
          })()
        &lt;/script&gt;
      &lt;/head&gt;
      &lt;body&gt;&lt;/body&gt;
    &lt;/html&gt;
  &#34;
&gt;&lt;/iframe&gt;
&lt;script&gt;
window.addEventListener(&#39;message&#39;, function(e) {
  if (e.data &amp;&amp; e.data.type === &#39;p5resize&#39; &amp;&amp; e.data.id === &#39;p5-0&#39;) {
    var f = document.getElementById(&#39;p5-0&#39;)
    if (f) { f.style.width = e.data.w + &#39;px&#39;; f.style.height = e.data.h + &#39;px&#39; }
  }
})
&lt;/script&gt;



&lt;hr&gt;
&lt;h1 id=&#34;four-keyframes-one-object&#34;&gt;Four keyframes, one object&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;createPoseTrack&lt;/code&gt; returns a &lt;code&gt;PoseTrack&lt;/code&gt; — a renderer-agnostic state machine for &lt;code&gt;{ pos, rot, scl }&lt;/code&gt; keyframes. &lt;code&gt;track.add(spec)&lt;/code&gt; appends one; adjacent duplicates are skipped by default:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Depth-of-field blur with focal target</title>
      <link>https://jpcharalambosh.co/posts/blur/</link>
      <pubDate>Thu, 09 Apr 2026 15:52:34 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/blur/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A depth-of-field blur effect where the focal plane is driven by a live world-space position. The scene is rendered into a &lt;a href=&#34;https://beta.p5js.org/reference/p5/createframebuffer/&#34;&gt;&lt;code&gt;p5.Framebuffer&lt;/code&gt;&lt;/a&gt;, then a &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;p5.strands&lt;/a&gt; DOF pass is applied via &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;pipe()&lt;/code&gt;&lt;/a&gt;. The magenta sphere is the focal target — its screen-space z is recomputed every frame with &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;mapLocation()&lt;/code&gt;&lt;/a&gt; and fed directly into the shader, so the blur follows the sphere continuously. A first-person directional light tracks the viewer using &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;mapDirection()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
















&lt;iframe
  id=&#39;p5-0&#39;
  style=&#39;border:none; display:block; width:0; height:0;&#39;
  srcdoc=&#34;
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
      &lt;head&gt;
        &lt;style&gt;body{margin:0;padding:0;overflow:hidden}&lt;/style&gt;
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@2.2.3/lib/p5.min.js&#39;&gt;&lt;/script&gt;
        
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.min.js&#39;&gt;&lt;/script&gt;
        
        
        
        
        
        
        
        
        &lt;script&gt;
          
&amp;#39;use strict&amp;#39;

// zero-alloc buffers — allocated once, reused every frame
const _dir = new Float32Array(3)
const _loc = new Float32Array(3)

let layer, dofFilter, models, panel
let focusVal = 0

function dofCallback() {
  const depthTex      = uniformTexture(() =&amp;gt; layer.depth)
  const focus         = uniformFloat(() =&amp;gt; focusVal)
  const blurIntensity = uniformFloat(&amp;#39;blurIntensity&amp;#39;)

  const getBlurriness = (d) =&amp;gt; abs(d - focus) * 40 * blurIntensity
  const maxBlurDist   = (b) =&amp;gt; b * 0.01

  getColor((inputs, canvasContent) =&amp;gt; {
    let colour = getTexture(canvasContent, inputs.texCoord)
    let samples = 1
    const centerDepth = getTexture(depthTex, inputs.texCoord).r
    const blur = getBlurriness(centerDepth)
    for (let i = 0; i &amp;lt; 20; i&amp;#43;&amp;#43;) {
      const angle  = float(i) * TWO_PI / 20
      const dist   = float(i) / 20 * maxBlurDist(blur)
      const offset = [cos(angle), sin(angle)] * dist
      const sampleDepth = getTexture(depthTex, inputs.texCoord &amp;#43; offset).r
      if (sampleDepth &amp;gt;= centerDepth ||
          maxBlurDist(getBlurriness(sampleDepth)) &amp;gt;= dist) {
        colour &amp;#43;= getTexture(canvasContent, inputs.texCoord &amp;#43; offset)
        samples&amp;#43;&amp;#43;
      }
    }
    colour /= float(samples)
    return [colour.rgb, 1]
  })
}

function setup() {
  createCanvas(600, 400, WEBGL)

  layer     = createFramebuffer()
  dofFilter = baseFilterShader().modify(dofCallback)

  panel = createPanel({
    blurIntensity: { min: 0, max: 4, value: 2, step: 0.1, label: &amp;#39;blur&amp;#39; }
  }, { target: dofFilter, x: 10, y: 10, width: 140, labels: true, color: &amp;#39;white&amp;#39; })

  const trange = 200
  models = []
  for (let i = 0; i &amp;lt; 50; i&amp;#43;&amp;#43;) {
    models.push({
      position: createVector(
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange
      ),
      size:  random() * 25 &amp;#43; 8,
      color: i === 0 ? color(255, 0, 220)
                     : color(int(random(256)), int(random(256)), int(random(256))),
      type:  i === 0 ? &amp;#39;ball&amp;#39; : i &amp;lt; 25 ? &amp;#39;torus&amp;#39; : &amp;#39;box&amp;#39;
    })
  }
}

function draw() {
  layer.begin()
  background(0)
  axes()
  orbitControl()
  noStroke()
  ambientLight(100)

  // first-person directional light — camera forward in world space
  mapDirection(p5.Tree._k, { out: _dir, from: p5.Tree.EYE, to: p5.Tree.WORLD })
  directionalLight(255, 255, 255, _dir[0], _dir[1], _dir[2])

  specularMaterial(255)
  shininess(150)

  models.forEach(m =&amp;gt; {
    push()
    fill(m.color)
    translate(m.position)
    m.type === &amp;#39;box&amp;#39;   ? box(m.size)   :
    m.type === &amp;#39;torus&amp;#39; ? torus(m.size) : sphere(m.size)
    pop()
  })

  // focal depth from the magenta sphere&amp;#39;s world position → screen z
  mapLocation(models[0].position, { out: _loc, from: p5.Tree.WORLD, to: p5.Tree.SCREEN })
  focusVal = _loc[2]
  layer.end()

  pipe(layer, [dofFilter])
}

const mouseWheel = () =&amp;gt; false

        &lt;/script&gt;
        
        &lt;script&gt;
          (function() {
            var t = setInterval(function() {
              var c = document.querySelector(&#39;canvas&#39;)
              if (c &amp;&amp; c.offsetWidth &gt; 0) {
                clearInterval(t)
                window.parent.postMessage({ type: &#39;p5resize&#39;, id: &#39;p5-0&#39;, w: c.offsetWidth, h: c.offsetHeight }, &#39;*&#39;)
              }
            }, 100)
          })()
        &lt;/script&gt;
      &lt;/head&gt;
      &lt;body&gt;&lt;/body&gt;
    &lt;/html&gt;
  &#34;
&gt;&lt;/iframe&gt;
&lt;script&gt;
window.addEventListener(&#39;message&#39;, function(e) {
  if (e.data &amp;&amp; e.data.type === &#39;p5resize&#39; &amp;&amp; e.data.id === &#39;p5-0&#39;) {
    var f = document.getElementById(&#39;p5-0&#39;)
    if (f) { f.style.width = e.data.w + &#39;px&#39;; f.style.height = e.data.h + &#39;px&#39; }
  }
})
&lt;/script&gt;



&lt;hr&gt;
&lt;h1 id=&#34;the-shader-as-a-strands-callback&#34;&gt;The shader as a strands callback&lt;/h1&gt;
&lt;p&gt;The DOF pass is authored as a &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;&lt;code&gt;p5.strands&lt;/code&gt;&lt;/a&gt; callback on &lt;code&gt;baseFilterShader().modify()&lt;/code&gt;. There is no raw GLSL string — the algorithm is expressed in JavaScript using the strands DSL, which compiles it to WebGL2 under the hood.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Post-effects pipeline</title>
      <link>https://jpcharalambosh.co/posts/post_effects/</link>
      <pubDate>Thu, 09 Apr 2026 15:52:34 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/post_effects/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Multi-pass post-processing with &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;p5.strands&lt;/a&gt; and &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;pipe()&lt;/code&gt;&lt;/a&gt;. Three filter passes — depth-of-field blur, value-noise warp, and pixelation — are chained over a scene framebuffer. Each pass is a &lt;code&gt;baseFilterShader().modify()&lt;/code&gt; callback; &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createPanel&lt;/code&gt;&lt;/a&gt; wires their uniforms automatically. Press &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, or &lt;code&gt;3&lt;/code&gt; to rotate the pass ordering at runtime.&lt;/p&gt;
&lt;/blockquote&gt;
















&lt;iframe
  id=&#39;p5-0&#39;
  style=&#39;border:none; display:block; width:0; height:0;&#39;
  srcdoc=&#34;
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
      &lt;head&gt;
        &lt;style&gt;body{margin:0;padding:0;overflow:hidden}&lt;/style&gt;
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@2.2.3/lib/p5.min.js&#39;&gt;&lt;/script&gt;
        
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.min.js&#39;&gt;&lt;/script&gt;
        
        
        
        
        
        
        
        
        &lt;script&gt;
          
&amp;#39;use strict&amp;#39;

const _dir = new Float32Array(3)
const _loc = new Float32Array(3)

let layer, models
let dofFilter, noiseFilter, pixelFilter
let uiDof, uiNoise, uiPixel
let focusVal = 0
let order = 0   // 0=dof→noise→pixel  1=noise→pixel→dof  2=pixel→dof→noise

// ── Strands callbacks ─────────────────────────────────────────────────────────

function dofCallback() {
  const depthTex      = uniformTexture(() =&amp;gt; layer.depth)
  const focus         = uniformFloat(() =&amp;gt; focusVal)
  const blurIntensity = uniformFloat(&amp;#39;blurIntensity&amp;#39;)
  const getBlurriness = (d) =&amp;gt; abs(d - focus) * 40 * blurIntensity
  const maxBlurDist   = (b) =&amp;gt; b * 0.01
  getColor((inputs, canvasContent) =&amp;gt; {
    let colour = getTexture(canvasContent, inputs.texCoord)
    let samples = 1
    const centerDepth = getTexture(depthTex, inputs.texCoord).r
    const blur = getBlurriness(centerDepth)
    for (let i = 0; i &amp;lt; 20; i&amp;#43;&amp;#43;) {
      const angle  = float(i) * TWO_PI / 20
      const dist   = float(i) / 20 * maxBlurDist(blur)
      const offset = [cos(angle), sin(angle)] * dist
      const sd     = getTexture(depthTex, inputs.texCoord &amp;#43; offset).r
      if (sd &amp;gt;= centerDepth || maxBlurDist(getBlurriness(sd)) &amp;gt;= dist) {
        colour &amp;#43;= getTexture(canvasContent, inputs.texCoord &amp;#43; offset)
        samples&amp;#43;&amp;#43;
      }
    }
    colour /= float(samples)
    return [colour.rgb, 1]
  })
}

function noiseCallback() {
  const frequency = uniformFloat(&amp;#39;frequency&amp;#39;)
  const amplitude = uniformFloat(&amp;#39;amplitude&amp;#39;)
  const speed     = uniformFloat(&amp;#39;speed&amp;#39;)
  const t         = uniformFloat(() =&amp;gt; millis() / 1000)
  const hash      = (p) =&amp;gt; fract(sin(dot(p, [127.1, 311.7, 74.7])) * 43758.5453123)
  const fade      = (t) =&amp;gt; t * t * (3 - 2 * t)
  const valueNoise = (p) =&amp;gt; {
    const i = floor(p), f = fract(p), u = fade(f)
    const n000=hash(i&amp;#43;[0,0,0]), n100=hash(i&amp;#43;[1,0,0])
    const n010=hash(i&amp;#43;[0,1,0]), n110=hash(i&amp;#43;[1,1,0])
    const n001=hash(i&amp;#43;[0,0,1]), n101=hash(i&amp;#43;[1,0,1])
    const n011=hash(i&amp;#43;[0,1,1]), n111=hash(i&amp;#43;[1,1,1])
    const nx00=mix(n000,n100,u.x), nx10=mix(n010,n110,u.x)
    const nx01=mix(n001,n101,u.x), nx11=mix(n011,n111,u.x)
    return mix(mix(nx00,nx10,u.y), mix(nx01,nx11,u.y), u.z) * 2 - 1
  }
  getColor((inputs, canvasContent) =&amp;gt; {
    const s  = frequency * inputs.texCoord.x
    const v  = frequency * inputs.texCoord.y
    const n1 = valueNoise([s,      v, speed * t])
    const n2 = valueNoise([s &amp;#43; 17, v, speed * t])
    return [getTexture(canvasContent,
              inputs.texCoord &amp;#43; [amplitude*n1, amplitude*n2]).rgb, 1]
  })
}

function pixelCallback() {
  const level = uniformFloat(&amp;#39;level&amp;#39;)
  getColor((inputs, canvasContent) =&amp;gt; {
    const snapped = floor(inputs.texCoord * level) / level
    return [getTexture(canvasContent, snapped).rgb, 1]
  })
}

// ── Setup ─────────────────────────────────────────────────────────────────────

function setup() {
  createCanvas(600, 400, WEBGL)

  layer       = createFramebuffer()
  dofFilter   = baseFilterShader().modify(dofCallback)
  noiseFilter = baseFilterShader().modify(noiseCallback)
  pixelFilter = baseFilterShader().modify(pixelCallback)

  uiDof = createPanel({
    blurIntensity: { min: 0, max: 4,   value: 1.5, step: 0.1, label: &amp;#39;blur&amp;#39;      }
  }, { target: dofFilter,   x: 10,  y: 10, width: 130, labels: true, title: &amp;#39;DOF&amp;#39;,   color: &amp;#39;white&amp;#39; })

  uiNoise = createPanel({
    frequency: { min: 0, max: 10, value: 3,   step: 0.1,  label: &amp;#39;frequency&amp;#39; },
    amplitude: { min: 0, max: 1,  value: 0.3, step: 0.01, label: &amp;#39;amplitude&amp;#39; },
    speed:     { min: 0, max: 1,  value: 0.3, step: 0.01, label: &amp;#39;speed&amp;#39;     }
  }, { target: noiseFilter, x: 155, y: 10, width: 130, labels: true, title: &amp;#39;Noise&amp;#39;, color: &amp;#39;white&amp;#39; })

  uiPixel = createPanel({
    level: { min: 2, max: 600, value: 300, step: 1, label: &amp;#39;level&amp;#39; }
  }, { target: pixelFilter, x: 300, y: 10, width: 130, labels: true, title: &amp;#39;Pixel&amp;#39;, color: &amp;#39;white&amp;#39; })

  const trange = 200
  models = []
  for (let i = 0; i &amp;lt; 50; i&amp;#43;&amp;#43;) {
    models.push({
      position: createVector(
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange
      ),
      size:  random() * 25 &amp;#43; 8,
      color: i === 0 ? color(255, 0, 220)
                     : color(int(random(256)), int(random(256)), int(random(256))),
      type:  i === 0 ? &amp;#39;ball&amp;#39; : i &amp;lt; 25 ? &amp;#39;torus&amp;#39; : &amp;#39;box&amp;#39;
    })
  }
}

// ── Draw ──────────────────────────────────────────────────────────────────────

function draw() {
  layer.begin()
  background(0)
  axes()
  orbitControl()
  noStroke()
  ambientLight(100)
  mapDirection(p5.Tree._k, { out: _dir, from: p5.Tree.EYE, to: p5.Tree.WORLD })
  directionalLight(255, 255, 255, _dir[0], _dir[1], _dir[2])
  specularMaterial(255)
  shininess(150)
  models.forEach(m =&amp;gt; {
    push()
    fill(m.color)
    translate(m.position)
    m.type === &amp;#39;box&amp;#39; ? box(m.size) : m.type === &amp;#39;torus&amp;#39; ? torus(m.size) : sphere(m.size)
    pop()
  })
  mapLocation(models[0].position, { out: _loc, from: p5.Tree.WORLD, to: p5.Tree.SCREEN })
  focusVal = _loc[2]
  layer.end()

  const passes = [[dofFilter, noiseFilter, pixelFilter],
                  [noiseFilter, pixelFilter, dofFilter],
                  [pixelFilter, dofFilter, noiseFilter]][order]
  pipe(layer, passes)

  beginHUD()
  fill(255)
  noStroke()
  textSize(12)
  text([&amp;#39;dof → noise → pixel&amp;#39;,
        &amp;#39;noise → pixel → dof&amp;#39;,
        &amp;#39;pixel → dof → noise&amp;#39;][order] &amp;#43; &amp;#39;  (1/2/3 to rotate)&amp;#39;,
       10, height - 10)
  endHUD()
}

function keyPressed() {
  if (key === &amp;#39;1&amp;#39; || key === &amp;#39;2&amp;#39; || key === &amp;#39;3&amp;#39;)
    order = (order &amp;#43; 1) % 3
}

const mouseWheel = () =&amp;gt; false

        &lt;/script&gt;
        
        &lt;script&gt;
          (function() {
            var t = setInterval(function() {
              var c = document.querySelector(&#39;canvas&#39;)
              if (c &amp;&amp; c.offsetWidth &gt; 0) {
                clearInterval(t)
                window.parent.postMessage({ type: &#39;p5resize&#39;, id: &#39;p5-0&#39;, w: c.offsetWidth, h: c.offsetHeight }, &#39;*&#39;)
              }
            }, 100)
          })()
        &lt;/script&gt;
      &lt;/head&gt;
      &lt;body&gt;&lt;/body&gt;
    &lt;/html&gt;
  &#34;
&gt;&lt;/iframe&gt;
&lt;script&gt;
window.addEventListener(&#39;message&#39;, function(e) {
  if (e.data &amp;&amp; e.data.type === &#39;p5resize&#39; &amp;&amp; e.data.id === &#39;p5-0&#39;) {
    var f = document.getElementById(&#39;p5-0&#39;)
    if (f) { f.style.width = e.data.w + &#39;px&#39;; f.style.height = e.data.h + &#39;px&#39; }
  }
})
&lt;/script&gt;



&lt;hr&gt;
&lt;h1 id=&#34;shader-passes-as-strands-callbacks&#34;&gt;Shader passes as strands callbacks&lt;/h1&gt;
&lt;p&gt;Each post-processing pass is a &lt;code&gt;baseFilterShader().modify()&lt;/code&gt; callback — a plain JavaScript function using the &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;p5.strands&lt;/a&gt; DSL. The framework compiles each callback to WebGL2 at startup; no raw GLSL strings are needed.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Toon shading</title>
      <link>https://jpcharalambosh.co/posts/toon/</link>
      <pubDate>Thu, 09 Apr 2026 15:52:34 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/toon/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Toon shading, or &lt;a href=&#34;https://en.wikipedia.org/wiki/Cel_shading&#34;&gt;cel shading&lt;/a&gt;, gives 3D geometry a flat, cartoon-like look by quantizing diffuse reflection into a finite number of discrete &lt;code&gt;shades&lt;/code&gt;. In this p5.js v2 version the shader is written as a &lt;a href=&#34;https://beta.p5js.org/reference/p5/basematerialshader/&#34;&gt;&lt;code&gt;baseMaterialShader().modify()&lt;/code&gt;&lt;/a&gt; hook using the &lt;a href=&#34;https://beta.p5js.org/tutorials/intro-to-p5-strands/&#34;&gt;p5.strands&lt;/a&gt; DSL — no raw GLSL strings, no hand-written vertex shader. &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;createPanel&lt;/code&gt;&lt;/a&gt; wires the color and shades controls to the shader automatically each frame.&lt;/p&gt;
&lt;/blockquote&gt;
















&lt;iframe
  id=&#39;p5-0&#39;
  style=&#39;border:none; display:block; width:0; height:0;&#39;
  srcdoc=&#34;
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
      &lt;head&gt;
        &lt;style&gt;body{margin:0;padding:0;overflow:hidden}&lt;/style&gt;
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@2.2.3/lib/p5.min.js&#39;&gt;&lt;/script&gt;
        
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.min.js&#39;&gt;&lt;/script&gt;
        
        
        
        
        
        
        
        
        &lt;script&gt;
          
&amp;#39;use strict&amp;#39;

let models, modelsDisplayed
let toon, panel
const depth = 0.4

function setup() {
  createCanvas(600, 400, WEBGL)

  panel = createPanel({
    u_shades: { min: 1, max: 10, value: 5, step: 1,   label: &amp;#39;shades&amp;#39; },
    u_tint:   { value: &amp;#39;#ffd700&amp;#39;,                      label: &amp;#39;tint&amp;#39;   }
  }, { x: 10, y: 10, labels: true, color: &amp;#39;white&amp;#39; })

  toon = baseMaterialShader().modify({
    uniforms: {
      &amp;#39;float u_shades&amp;#39;:   5,
      &amp;#39;vec3  u_tint&amp;#39;:     [1, 0.84, 0],
      &amp;#39;vec3  u_lightDir&amp;#39;: [0, 0, 1],
      &amp;#39;vec3  u_color&amp;#39;:    [1, 1, 1]
    },
    &amp;#39;vec4 combineColors&amp;#39;: `(ColorComponents components) {
      float intensity = max(0.0, dot(normalize(vNormal), normalize(u_lightDir)));
      float shadeSize = 1.0 / clamp(u_shades, 1.0, 10.0);
      float k         = floor(intensity / shadeSize) * shadeSize;
      k = max(k, shadeSize * 0.5);
      return vec4(k * u_tint * u_color, 1.0);
    }`
  })

  colorMode(RGB, 1)
  noStroke()
  setAttributes(&amp;#39;antialias&amp;#39;, true)
  document.oncontextmenu = () =&amp;gt; false

  const trange = 200
  models = []
  for (let i = 0; i &amp;lt; 100; i&amp;#43;&amp;#43;) {
    models.push({
      position: createVector(
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange,
        (random() * 2 - 1) * trange
      ),
      angle: random(0, TWO_PI),
      axis:  p5.Vector.random3D(),
      size:  random() * 50 &amp;#43; 16,
      color: [random(), random(), random()]
    })
  }

  modelsDisplayed = createSlider(1, models.length, int(models.length / 2), 1)
  modelsDisplayed.position(width - 125, 15)
  modelsDisplayed.style(&amp;#39;width&amp;#39;, &amp;#39;120px&amp;#39;)
}

function draw() {
  orbitControl()
  background(&amp;#39;#1C1D1F&amp;#39;)

  push()
  stroke(&amp;#39;green&amp;#39;)
  axes({ size: 175 })
  grid({ size: 175 })
  pop()

  const lx  = (mouseX / width  - 0.5) * 2
  const ly  = (mouseY / height - 0.5) * 2
  const len = Math.sqrt(lx*lx &amp;#43; ly*ly &amp;#43; depth*depth)
  const tv  = panel.u_tint.value()

  shader(toon)
  toon.setUniform(&amp;#39;u_lightDir&amp;#39;, [lx/len, ly/len, depth/len])
  toon.setUniform(&amp;#39;u_shades&amp;#39;,   panel.u_shades.value())
  toon.setUniform(&amp;#39;u_tint&amp;#39;,     [tv[0], tv[1], tv[2]])

  noStroke()

  for (let i = 0; i &amp;lt; modelsDisplayed.value(); i&amp;#43;&amp;#43;) {
    toon.setUniform(&amp;#39;u_color&amp;#39;, models[i].color)
    push()
    translate(models[i].position)
    rotate(models[i].angle, models[i].axis)
    const r = models[i].size / 2
    if      (i % 3 === 0) cone(r)
    else if (i % 3 === 1) sphere(r)
    else                  torus(r, r / 4)
    pop()
  }

  resetShader()
}

function mouseWheel() { return false }

        &lt;/script&gt;
        
        &lt;script&gt;
          (function() {
            var t = setInterval(function() {
              var c = document.querySelector(&#39;canvas&#39;)
              if (c &amp;&amp; c.offsetWidth &gt; 0) {
                clearInterval(t)
                window.parent.postMessage({ type: &#39;p5resize&#39;, id: &#39;p5-0&#39;, w: c.offsetWidth, h: c.offsetHeight }, &#39;*&#39;)
              }
            }, 100)
          })()
        &lt;/script&gt;
      &lt;/head&gt;
      &lt;body&gt;&lt;/body&gt;
    &lt;/html&gt;
  &#34;
&gt;&lt;/iframe&gt;
&lt;script&gt;
window.addEventListener(&#39;message&#39;, function(e) {
  if (e.data &amp;&amp; e.data.type === &#39;p5resize&#39; &amp;&amp; e.data.id === &#39;p5-0&#39;) {
    var f = document.getElementById(&#39;p5-0&#39;)
    if (f) { f.style.width = e.data.w + &#39;px&#39;; f.style.height = e.data.h + &#39;px&#39; }
  }
})
&lt;/script&gt;



&lt;hr&gt;
&lt;h1 id=&#34;toon-shader&#34;&gt;Toon shader&lt;/h1&gt;
&lt;p&gt;The &lt;code&gt;combineColors&lt;/code&gt; hook in &lt;code&gt;baseMaterialShader().modify()&lt;/code&gt; receives the eye-space &lt;code&gt;vNormal&lt;/code&gt; varying directly — no vertex shader boilerplate required. Diffuse intensity is the dot product of the surface normal and the (normalized) light direction; it is then snapped into discrete bands to produce the cel-shading look.&lt;/p&gt;</description>
    </item>
    <item>
      <title>3D Brush Painting in VR</title>
      <link>https://jpcharalambosh.co/posts/brush/</link>
      <pubDate>Thu, 09 Apr 2026 12:00:00 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/brush/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A 3D brush painting sketch that uses depth control for VR-style experiences. &lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;&lt;code&gt;mapLocation()&lt;/code&gt;&lt;/a&gt; converts screen-space mouse coordinates and a depth slider value directly into world-space positions, so each brush stroke is placed precisely in 3D. Press &lt;code&gt;r&lt;/code&gt; to toggle recording, &lt;code&gt;c&lt;/code&gt; to clear, &lt;code&gt;f&lt;/code&gt; to re-focus the camera on the world origin.&lt;/p&gt;
&lt;/blockquote&gt;
















&lt;iframe
  id=&#39;p5-0&#39;
  style=&#39;border:none; display:block; width:0; height:0;&#39;
  srcdoc=&#34;
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
      &lt;head&gt;
        &lt;style&gt;body{margin:0;padding:0;overflow:hidden}&lt;/style&gt;
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@2.2.3/lib/p5.min.js&#39;&gt;&lt;/script&gt;
        
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.min.js&#39;&gt;&lt;/script&gt;
        
        
        
        
        
        
        
        
        &lt;script&gt;
          
&amp;#39;use strict&amp;#39;

let brushColor, depthSlider, points = []
let recording = false

function setup() {
  createCanvas(600, 400, WEBGL)
  colorMode(RGB, 1)
  document.oncontextmenu = () =&amp;gt; false

  // initialise depth slider at the screen-space z of the world origin
  const o = mapLocation(p5.Tree.ORIGIN, { from: p5.Tree.WORLD, to: p5.Tree.SCREEN })
  depthSlider = createSlider(0, 1, o.z, 0.001)
  depthSlider.position(10, 10)
  depthSlider.style(&amp;#39;width&amp;#39;, &amp;#39;580px&amp;#39;)

  brushColor = createColorPicker(&amp;#39;#C7C08D&amp;#39;)
  brushColor.position(width - 70, 40)
}

function draw() {
  mouseY &amp;gt;= 30 &amp;amp;&amp;amp; orbitControl()
  recording &amp;amp;&amp;amp; addPoint()

  background(&amp;#39;#222226&amp;#39;)
  axes({ size: 50, bits: p5.Tree.X | p5.Tree.Y | p5.Tree.Z })

  for (const pt of points) {
    push()
    noStroke()
    fill(pt.color)
    translate(pt.pos)
    sphere(1)
    pop()
  }
}

function addPoint() {
  points.push({
    pos:   mapLocation([mouseX, mouseY, depthSlider.value()],
                       { from: p5.Tree.SCREEN, to: p5.Tree.WORLD }),
    color: brushColor.color()
  })
}

function focusOrigin() {
  const eye    = mapLocation()                              // camera world position
  const up     = mapDirection(p5.Tree.j)                   // camera up in world space
  const origin = [0, 0, 0]
  camera(eye.x, eye.y, eye.z, ...origin, up.x, up.y, up.z)
  const o = mapLocation(p5.Tree.ORIGIN, { from: p5.Tree.WORLD, to: p5.Tree.SCREEN })
  depthSlider.value(o.z)
}

function keyPressed() {
  key === &amp;#39;c&amp;#39; &amp;amp;&amp;amp; (points = [])
  key === &amp;#39;f&amp;#39; &amp;amp;&amp;amp; focusOrigin()
  key === &amp;#39;r&amp;#39; &amp;amp;&amp;amp; (recording = !recording)
}

        &lt;/script&gt;
        
        &lt;script&gt;
          (function() {
            var t = setInterval(function() {
              var c = document.querySelector(&#39;canvas&#39;)
              if (c &amp;&amp; c.offsetWidth &gt; 0) {
                clearInterval(t)
                window.parent.postMessage({ type: &#39;p5resize&#39;, id: &#39;p5-0&#39;, w: c.offsetWidth, h: c.offsetHeight }, &#39;*&#39;)
              }
            }, 100)
          })()
        &lt;/script&gt;
      &lt;/head&gt;
      &lt;body&gt;&lt;/body&gt;
    &lt;/html&gt;
  &#34;
&gt;&lt;/iframe&gt;
&lt;script&gt;
window.addEventListener(&#39;message&#39;, function(e) {
  if (e.data &amp;&amp; e.data.type === &#39;p5resize&#39; &amp;&amp; e.data.id === &#39;p5-0&#39;) {
    var f = document.getElementById(&#39;p5-0&#39;)
    if (f) { f.style.width = e.data.w + &#39;px&#39;; f.style.height = e.data.h + &#39;px&#39; }
  }
})
&lt;/script&gt;



&lt;hr&gt;
&lt;h2 id=&#34;screen-to-world-mapping-with-maplocation&#34;&gt;Screen-to-world mapping with &lt;code&gt;mapLocation&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The core of the brush is a single &lt;code&gt;mapLocation&lt;/code&gt; call that lifts the 2D mouse position — together with a depth value from the slider — into 3D world space each frame:&lt;/p&gt;</description>
    </item>
    <item>
      <title>Visualizing Perspective Transformation to NDC</title>
      <link>https://jpcharalambosh.co/posts/ndc/</link>
      <pubDate>Tue, 24 Mar 2026 10:28:01 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/ndc/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://jsantell.com/3d-projection/#perspective-projection&#34;&gt;Perspective projection&lt;/a&gt; is a fundamental concept in 3D graphics. The transformation maps a view frustum — a truncated pyramid — into a cube of Normalized Device Coordinates (NDC), producing the foreshortening effect that makes 3D scenes look convincing. The sketch below morphs a set of &lt;code&gt;cajas&lt;/code&gt; (boxes) continuously from world space into NDC space, rendered from a third-person camera that also displays the frustum being transformed.&lt;/p&gt;
&lt;/blockquote&gt;
















&lt;iframe
  id=&#39;p5-0&#39;
  style=&#39;border:none; display:block; width:0; height:0;&#39;
  srcdoc=&#34;
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
      &lt;head&gt;
        &lt;style&gt;body{margin:0;padding:0;overflow:hidden}&lt;/style&gt;
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@2.2.3/lib/p5.min.js&#39;&gt;&lt;/script&gt;
        
        &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5.tree/dist/p5.tree.min.js&#39;&gt;&lt;/script&gt;
        
        
        
        
        
        
        
        
        &lt;script&gt;
          
&amp;#39;use strict&amp;#39;

// NDC morph — p5.js v2 &amp;#43; p5.tree v0.0.25
// original: https://jpcharalambosh.co/posts/ndc/

let mainCam, frustumCam
let fillMorph, outlineMorph
let frustumProj       // p5.Matrix(4) — frustum projection, updated each frame
let vBuf, eBuf        // Float32Array(16) — frustum view / eye matrices
let cajas, panel

const N    = 81       // near plane
const NDC  = 0.5      // NDC morph offset
const FOVY = 0.75     // frustum vertical fov
const Z    = 158      // frustum camera distance

// ── setup ─────────────────────────────────────────────────────────────────────
function setup() {
  createCanvas(600, 400, WEBGL)
  strokeWeight(0.8)
  colorMode(RGB, 1)

  frustumProj = createMatrix(4)
  vBuf        = new Float32Array(16)
  eBuf        = new Float32Array(16)

  panel = createPanel({
    d:       { min: 0, max: 1, value: 0, step: 0.01 },
    animate: { value: true },
  }, {
    title: &amp;#39;NDC morph&amp;#39;, collapsible: true,
    labels: true, color: &amp;#39;white&amp;#39;,
    x: width - 130, y: 10,
  })

  // Morph body is identical for fill and stroke; only the struct type differs:
  //   fill   → &amp;#39;Vertex&amp;#39;        (struct: position, normal, texCoord, color)
  //   stroke → &amp;#39;StrokeVertex&amp;#39;  (struct: position, tangentIn, tangentOut, color, weight)
  const morphBody = `{
    vec4 fPos       = uViewFrustumMatrix * vec4(inputs.position, 1.0);
    vec2 xy         = -(fPos.xy / fPos.z) * (1.0 &amp;#43; ndc) * n;
    fPos.xy         = mix(fPos.xy, xy, d);
    inputs.position = (uEyeFrustumMatrix * fPos).xyz;
    return inputs;
  }`

  const morphUniforms = {
    uniforms: {
      &amp;#39;mat4 uViewFrustumMatrix&amp;#39;: () =&amp;gt; vBuf,
      &amp;#39;mat4 uEyeFrustumMatrix&amp;#39;:  () =&amp;gt; eBuf,
      &amp;#39;float d&amp;#39;:   () =&amp;gt; panel.d.value(),
      &amp;#39;float ndc&amp;#39;: () =&amp;gt; NDC,
      &amp;#39;float n&amp;#39;:   () =&amp;gt; N,
    },
  }

  fillMorph = buildColorShader({
    ...morphUniforms,
    &amp;#39;Vertex getWorldInputs&amp;#39;: `(Vertex inputs) ${morphBody}`,
  })

  outlineMorph = buildStrokeShader({
    ...morphUniforms,
    &amp;#39;StrokeVertex getWorldInputs&amp;#39;: `(StrokeVertex inputs) ${morphBody}`,
  })

  cajas = Array.from({ length: 40 }, () =&amp;gt; ({
    position: createVector(random(-50, 50), random(-50, 50), random(-25, 75)),
    size:     random(5, 20),
    col:      color(random(), random(), random()),
  }))

  mainCam = createCamera()
  mainCam.camera(
    -250.56, -257.35, 181.54,
    -249.9,  -256.75, 181.2,
    -0.57, 0.745, 0.34
  )
  frustumCam = createCamera()
  setCamera(mainCam)

  const f = 7 / 30
  ortho(-width * f, width * f, -height * f, height * f, 1, 10000)
}

// ── draw ──────────────────────────────────────────────────────────────────────
function draw() {
  background(&amp;#39;#138d75&amp;#39;)

  if (panel.animate.value()) {
    panel.d.set(map(sin(frameCount / 60), -1, 1, 0, 1))
  }

  updateFrustum()

  // grid
  push()
  rotateX(HALF_PI)
  strokeWeight(0.2)
  stroke(&amp;#39;#FFC300&amp;#39;)
  grid({ subdivisions: 20, size: 240 })
  pop()

  // single scene pass — fill, stroke, and frustum gizmo all morphed together
  push()
  shader(fillMorph)
  strokeShader(outlineMorph)
  strokeWeight(0.8)
  stroke(&amp;#39;magenta&amp;#39;)
  scene()
  fill(1, 0, 1, 0.3)
  viewFrustum({
    mat4Eye: eBuf,
    mat4Proj: frustumProj,
    bits: p5.Tree.NEAR | p5.Tree.FAR,
    viewer: () =&amp;gt; {
      push()
      resetShader()
      axes({ size: 50, bits: p5.Tree.X | p5.Tree._Y | p5.Tree._Z })
      pop()
    },
  })
  pop()
}

// ── updateFrustum ─────────────────────────────────────────────────────────────
function updateFrustum() {
  frustumCam.camera(0, 0, Z, ...p5.Tree.k, ...p5.Tree.j)
  frustumCam.mat4View(vBuf)
  frustumCam.mat4Eye(eBuf)
  const far = N * (1 &amp;#43; 2 * tan(FOVY / 2) * (1 &amp;#43; NDC))
  frustumProj.perspective(FOVY, 1, N, far)
}

// ── scene ─────────────────────────────────────────────────────────────────────
function scene() {
  cajas.forEach(({ position: pos, size, col }) =&amp;gt; {
    push()
    fill(col)
    translate(pos.x, pos.y, pos.z)
    box(size)
    pop()
  })
}

        &lt;/script&gt;
        
        &lt;script&gt;
          (function() {
            var t = setInterval(function() {
              var c = document.querySelector(&#39;canvas&#39;)
              if (c &amp;&amp; c.offsetWidth &gt; 0) {
                clearInterval(t)
                window.parent.postMessage({ type: &#39;p5resize&#39;, id: &#39;p5-0&#39;, w: c.offsetWidth, h: c.offsetHeight }, &#39;*&#39;)
              }
            }, 100)
          })()
        &lt;/script&gt;
      &lt;/head&gt;
      &lt;body&gt;&lt;/body&gt;
    &lt;/html&gt;
  &#34;
&gt;&lt;/iframe&gt;
&lt;script&gt;
window.addEventListener(&#39;message&#39;, function(e) {
  if (e.data &amp;&amp; e.data.type === &#39;p5resize&#39; &amp;&amp; e.data.id === &#39;p5-0&#39;) {
    var f = document.getElementById(&#39;p5-0&#39;)
    if (f) { f.style.width = e.data.w + &#39;px&#39;; f.style.height = e.data.h + &#39;px&#39; }
  }
})
&lt;/script&gt;



&lt;h1 id=&#34;shaders&#34;&gt;Shaders&lt;/h1&gt;
&lt;p&gt;The morph lives in a single GLSL hook injected into p5&amp;rsquo;s base shaders through &lt;a href=&#34;https://beta.p5js.org/reference/p5/buildColorShader/&#34;&gt;&lt;code&gt;buildColorShader&lt;/code&gt;&lt;/a&gt; and &lt;a href=&#34;https://beta.p5js.org/reference/p5/buildStrokeShader/&#34;&gt;&lt;code&gt;buildStrokeShader&lt;/code&gt;&lt;/a&gt; — no shader files needed.&lt;/p&gt;</description>
    </item>
    <item>
      <title>p5.tree.js</title>
      <link>https://jpcharalambosh.co/posts/tree/</link>
      <pubDate>Tue, 17 Feb 2026 16:07:46 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/tree/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/VisualComputing/p5.tree&#34;&gt;p5.tree&lt;/a&gt; is a render pipeline layer for &lt;a href=&#34;https://beta.p5js.org/&#34;&gt;p5.js v2&lt;/a&gt; — &lt;a href=&#34;https://en.wikipedia.org/wiki/Key_frame&#34;&gt;pose and camera interpolation&lt;/a&gt;, &lt;a href=&#34;https://learnopengl.com/Getting-started/Coordinate-Systems&#34;&gt;coordinate-space conversions&lt;/a&gt; between WORLD / EYE / SCREEN / NDC, &lt;a href=&#34;https://en.wikipedia.org/wiki/Hidden-surface_determination&#34;&gt;frustum visibility&lt;/a&gt;, HUD, &lt;a href=&#34;https://en.wikipedia.org/wiki/Multiple_Render_Targets&#34;&gt;multi-pass post-processing&lt;/a&gt;, picking, and declarative control panels. The demo below exercises all of it at once.&lt;/p&gt;
&lt;p&gt;Under the hood it&amp;rsquo;s three independent packages: a renderer-agnostic numeric core (&lt;code&gt;@nakednous/tree&lt;/code&gt; — math, spaces, keyframes, visibility), a lightweight DOM layer (&lt;code&gt;@nakednous/ui&lt;/code&gt; — sliders, transport), and a &lt;a href=&#34;https://beta.p5js.org/&#34;&gt;p5.js v2&lt;/a&gt; bridge that wires them to the canvas. The dependency direction is strict and one-way — the core knows nothing about p5 or the DOM — which is what lets the same keyframe interpolation that drives a camera path also animate any object, and lets the whole stack run headless or in a future renderer without touching the math.&lt;/p&gt;</description>
    </item>
    <item>
      <title>nub: Advancing Visual Computing on the Web</title>
      <link>https://jpcharalambosh.co/posts/nub/</link>
      <pubDate>Tue, 01 Apr 2025 14:05:28 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/nub/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Tree-like affine transformation hierarchies are at the core of many tasks in &lt;strong&gt;rendering&lt;/strong&gt;, &lt;strong&gt;interaction&lt;/strong&gt;, and &lt;strong&gt;computer vision&lt;/strong&gt;—from view frustum &amp;amp; occlusion culling and collision detection to motion retargeting and post-WIMP interfaces. Our recent publication, &lt;a href=&#34;https://doi.org/10.5334/jors.477&#34;&gt;&lt;em&gt;nub: A Rendering and Interaction Library for Visual Computing in Processing&lt;/em&gt;&lt;/a&gt;, introduces a functional and declarative API, built around a &lt;a href=&#34;https://en.wikipedia.org/wiki/Dataflow&#34;&gt;dataflow&lt;/a&gt;-based architecture that integrates rendering and &lt;a href=&#34;https://en.wikipedia.org/wiki/Event-driven_programming&#34;&gt;event-driven&lt;/a&gt; interaction through a simple yet powerful scene graph model.&lt;/p&gt;
&lt;p&gt;Built on top of Processing’s 2D/3D environment, &lt;a href=&#34;https://github.com/VisualComputing/nub&#34;&gt;nub&lt;/a&gt; offers a lightweight and expressive foundation for &lt;strong&gt;education&lt;/strong&gt;, &lt;strong&gt;research&lt;/strong&gt;, and &lt;strong&gt;experimentation&lt;/strong&gt; in visual computing. It supports hierarchical rendering, multi-view scenes, view-based interaction, and extensible workflows for interactive content.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Platonic Cells</title>
      <link>https://jpcharalambosh.co/posts/platonic_cells/</link>
      <pubDate>Wed, 05 Jun 2024 20:08:45 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/platonic_cells/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This demo illustrates new capabilities of the WebGL mode in the &lt;a href=&#34;https://github.com/objetos/p5.quadrille.js/tree/next&#34;&gt;next&lt;/a&gt; major upcoming version of p5.quadrille.js, currently under development. It showcases how &lt;a href=&#34;https://jpcharalambosh.co/posts/platonic/&#34;&gt;Platonic solids&lt;/a&gt; can be stored in quadrille cells and rendered using either immediate or retained mode with the &lt;a href=&#34;https://github.com/VisualComputing/p5.platonic&#34;&gt;p5.platonic&lt;/a&gt; library.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id=&#34;platonic-cells&#34;&gt;Platonic Cells&lt;/h1&gt;
&lt;p&gt;Platonic cells are cell functions (&lt;code&gt;cellFn&lt;/code&gt;) that implement the filling of Platonic solid cells in a quadrille &lt;code&gt;game&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;retained-mode&#34;&gt;Retained Mode&lt;/h2&gt;
&lt;p&gt;(&lt;strong&gt;mouse&lt;/strong&gt; click to clear/add Platonic solids, drag to navigate; &lt;strong&gt;press&lt;/strong&gt; &lt;code&gt;s&lt;/code&gt; (or &lt;code&gt;c&lt;/code&gt;) to save)&lt;br&gt;














&lt;iframe
  id=&#34;&#34;
  style=&#34;width:525px; height:525px&#34;
  srcdoc=&#34;
        &lt;!DOCTYPE html&gt;
        &lt;html&gt;
          &lt;head&gt;
            &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@1.11.10/lib/p5.min.js&#39;&gt;&lt;/script&gt;
            
            
            
            
            
            &lt;script src=https://cdn.jsdelivr.net/gh/VisualComputing/p5.platonic/p5.platonic.min.js&gt;&lt;/script&gt;
            &lt;script src=https://cdn.jsdelivr.net/gh/objetos/p5.quadrille.js/p5.quadrille.min.js&gt;&lt;/script&gt;
            
            
            
            &lt;script&gt;
              
`use strict`
let cnv
let game, solids
let font
let images = []
const r = 5, c = 5, l = Quadrille.cellLength

function preload() {
  font = loadFont(&amp;#39;noto_sans.ttf&amp;#39;)
  for (let i = 1; i &amp;lt;= 10; i&amp;#43;&amp;#43;) {
    images.push(loadImage(`p${i}.jpg`))
  }
}

function setup() {
  cnv = createCanvas(r * l, c * l, WEBGL)
  solids = createQuadrille(r, c)
  const args = [l / 2, true, [&amp;#39;yellow&amp;#39;, &amp;#39;blue&amp;#39;, &amp;#39;red&amp;#39;,
                              &amp;#39;cyan&amp;#39;, &amp;#39;magenta&amp;#39;, &amp;#39;yellow&amp;#39;]]
  visitQuadrille(solids, (row, col) =&amp;gt; solids.fill(row, col,
                                       platonicGeometry(...args)))
  game = createQuadrille(5, 5)
  visitQuadrille(game, (row, col) =&amp;gt; game.fill(row, col, random(images)))
  game.rand(15).rand(10, cellFn)
}

function draw() {
  background(&amp;#39;Gold&amp;#39;)
  drawQuadrille(game, {
    textFont: font,
    origin: CORNER,
    options: { origin: CENTER }
  })
}

function cellFn({ row, col }) {
  push()
  background(&amp;#39;black&amp;#39;)
  stroke(&amp;#39;lime&amp;#39;)
  fill(&amp;#39;blue&amp;#39;)
  rotateX(millis() * 0.001)
  rotateY(millis() * 0.001)
  rotateZ(millis() * 0.001)
  model(solids.read(row, col))
  pop()
}

function mouseClicked() {
  const row = game.mouseRow
  const col = game.mouseCol
  game.isEmpty(row, col) ? game.fill(row, col, cellFn) : game.clear(row, col)
}

function keyPressed() {
  key === &amp;#39;s&amp;#39; &amp;amp;&amp;amp; game.toImage(&amp;#39;game.png&amp;#39;, {
    textFont: font,
    origin: CORNER,
    options: { origin: CENTER }
  })
  key === &amp;#39;c&amp;#39; &amp;amp;&amp;amp; save(cnv, &amp;#39;platonic_cells.png&amp;#39;)
}

const mouseWheel = () =&amp;gt; false

            &lt;/script&gt;
          &lt;/head&gt;
          &lt;body&gt;
          &lt;/body&gt;
        &lt;/html&gt;
      &#34;
&gt;&lt;/iframe&gt;
&lt;/p&gt;</description>
    </item>
    <item>
      <title>Platonic Solids</title>
      <link>https://jpcharalambosh.co/posts/platonic/</link>
      <pubDate>Fri, 17 May 2024 15:32:05 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/platonic/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Platonic_solid&#34;&gt;Platonic solids&lt;/a&gt;, named after the philosopher &lt;a href=&#34;https://en.wikipedia.org/wiki/Plato&#34;&gt;Plato&lt;/a&gt;, are highly symmetrical, three-dimensional shapes. Each face of a Platonic solid is the same regular polygon, and the same number of polygons meet at each vertex. This sketch demonstrates the rendering of Platonic solids using the &lt;a href=&#34;https://github.com/VisualComputing/p5.platonic&#34;&gt;p5.platonic&lt;/a&gt; library, showcasing both the immediate mode and retained mode rendering of the shapes. Check out the &lt;a href=&#34;https://jpcharalambosh.co/posts/platonic_cells/&#34;&gt;platonic cells&lt;/a&gt; example, which fills quadrille cells with the Platonic solids introduced here.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Image convolution visualizer</title>
      <link>https://jpcharalambosh.co/posts/image_convolution_visualizer/</link>
      <pubDate>Thu, 23 Nov 2023 15:45:17 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/image_convolution_visualizer/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In the field of image processing, the application of convolution matrices to images stands as a cornerstone technique. This post focuses on illustrating this process through an interactive visualization, utilizing the &lt;code&gt;Quadrille&lt;/code&gt; &lt;a href=&#34;https://objetos.github.io/p5.quadrille.js/docs/visual_algorithms/filter/&#34;&gt;filter&lt;/a&gt; method alongside custom display functions. The visualization aims to clarify how various masks affect individual pixels in a source image, making the intricate process of image convolution more comprehensible. By offering an interactive experience, it sheds light on the significant impact these matrices have on digital images, providing a deeper insight into a key image manipulation method.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chess Pattern Recognition</title>
      <link>https://jpcharalambosh.co/posts/chess_pattern_recognition/</link>
      <pubDate>Tue, 10 Oct 2023 16:57:33 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/chess_pattern_recognition/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Harnessing the art of chess pattern recognition—emphasized by experts, among them Grandmaster Jonathan Rowson in &lt;a href=&#34;http://www.gambitbooks.com/books/The_Seven_Deadly_Chess_Sins.html&#34;&gt;&amp;ldquo;The Seven Deadly Chess Sins&amp;rdquo;&lt;/a&gt;—the demonstration below utilizes the powerful &lt;code&gt;quadrille&lt;/code&gt; &lt;a href=&#34;https://objetos.github.io/p5.quadrille.js/docs/accessors/search/&#34;&gt;search method&lt;/a&gt; to identify key tactical patterns on the provided board, given in FEN notation. These patterns, which encompass maneuvers like knight forks where a piece simultaneously threatens multiple opponent pieces, can decisively influence the game’s dynamics. The &lt;a href=&#34;https://objetos.github.io/p5.quadrille.js/&#34;&gt;p5.quadrille.js&lt;/a&gt; library aids players in detecting and adeptly deploying these patterns, thereby refining gameplay and strategic decision-making on the chessboard.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Game of Life Texturing</title>
      <link>https://jpcharalambosh.co/posts/game_of_life/</link>
      <pubDate>Sun, 08 Oct 2023 15:46:53 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/game_of_life/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Before diving into development, thoroughly reading the &lt;a href=&#34;https://objetos.github.io/p5.quadrille.js/&#34;&gt;Quadrille API documentation&lt;/a&gt; is key to efficient coding, preventing mistakes, and ensuring optimal performance. By acquainting yourself with the API&amp;rsquo;s nuances, you set the stage for informed decision-making, seamless collaboration, and a smoother development journey.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The following demo illustrates the &lt;a href=&#34;https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life&#34;&gt;game of life&lt;/a&gt; using a &lt;a href=&#34;https://conwaylife.com/wiki/Pentadecathlon&#34;&gt;pentadecathlon&lt;/a&gt; pattern as its initial seed, rendered as a surface texture on p5 &lt;a href=&#34;https://p5js.org/reference/#group-Shape&#34;&gt;3D shapes&lt;/a&gt; in &lt;a href=&#34;https://p5js.org/reference/#/p5/WEBGL&#34;&gt;WEBGL&lt;/a&gt; mode:&lt;/p&gt;
&lt;p&gt;(mouse drag to navigate; [1..7] keys change shape, other keys reset)&lt;br&gt;














&lt;iframe
  id=&#34;&#34;
  style=&#34;width:425px; height:425px&#34;
  srcdoc=&#34;
        &lt;!DOCTYPE html&gt;
        &lt;html&gt;
          &lt;head&gt;
            &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@1.11.10/lib/p5.min.js&#39;&gt;&lt;/script&gt;
            
            
            
            
            
            &lt;script src=https://cdn.jsdelivr.net/gh/objetos/p5.quadrille.js/p5.quadrille.min.js&gt;&lt;/script&gt;
            
            
            
            
            &lt;script&gt;
              
`use strict`;
Quadrille.cellLength = 20
let game, pattern
let life
let graphics
let mode = 1
const shapes = {
  1: () =&amp;gt; plane(width, height),
  2: () =&amp;gt; sphere(100),
  3: () =&amp;gt; torus(100, 50),
  4: () =&amp;gt; box(200),
  5: () =&amp;gt; cylinder(100, 200, 24, 1, false, false),
  6: () =&amp;gt; cone(100, 200, 24, 1, false),
  7: () =&amp;gt; ellipsoid(100, 180)
}

function setup() {
  game = createQuadrille(20, 20)
  life = color(&amp;#39;lime&amp;#39;)
  pattern = createQuadrille(3, 16252911n, life)
  game = Quadrille.or(game, pattern, 6, 8)
  createCanvas(game.width  * Quadrille.cellLength,
               game.height * Quadrille.cellLength,
               WEBGL)
  graphics = createGraphics(game.width  * Quadrille.cellLength,
                            game.height * Quadrille.cellLength)
  update()
}

function draw() {
  background(&amp;#39;yellow&amp;#39;)
  orbitControl()
  frameCount % 20 === 0 &amp;amp;&amp;amp; update()
  noStroke()
  shapes[mode] ? shapes[mode]() : shapes[1]()
}

function update() {
  const next = game.clone()
  visitQuadrille(game, (row, col) =&amp;gt; {
                       const order = game.ring(row, col).order
                       game.isFilled(row, col) ?
                       (order &amp;lt; 3 || order &amp;gt; 4) &amp;amp;&amp;amp; next.clear(row, col) :
                       order === 3 &amp;amp;&amp;amp; next.fill(row, col, life) })
  game = next
  graphics.background(&amp;#39;blue&amp;#39;)
  drawQuadrille(game, { graphics, outline: &amp;#39;magenta&amp;#39; })
  texture(graphics)
}

function keyPressed() {
  mode = &amp;#43;key
  game.clear()
  game = Quadrille.or(game, pattern, 6, 8)
}

            &lt;/script&gt;
          &lt;/head&gt;
          &lt;body&gt;
          &lt;/body&gt;
        &lt;/html&gt;
      &#34;
&gt;&lt;/iframe&gt;
&lt;/p&gt;</description>
    </item>
    <item>
      <title>Phyllotaxis</title>
      <link>https://jpcharalambosh.co/posts/phyllotaxis/</link>
      <pubDate>Thu, 14 Sep 2023 10:33:04 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/phyllotaxis/</guid>
      <description>&lt;p&gt;La disposición de las hojas en el tallo de una planta, conocida como &lt;a href=&#34;https://en.wikipedia.org/wiki/Phyllotaxis&#34;&gt;phyllotaxis&lt;/a&gt;, se puede modelar a partir del &lt;a href=&#34;https://en.wikipedia.org/wiki/Golden_angle&#34;&gt;ángulo de oro&lt;/a&gt;, número mágico íntimamente relacionado con la &lt;a href=&#34;https://en.wikipedia.org/wiki/Fibonacci_sequence&#34;&gt;secuencia de Fibonacci&lt;/a&gt; que rige diversas formas de la naturaleza: desde una flor de girasol, el brócoli o el &lt;a href=&#34;https://es.wikipedia.org/wiki/Brassica_oleracea_var._botrytis_%28romanesco%29&#34;&gt;romanesco&lt;/a&gt;, e incluso los brazos de las galaxias.&lt;/p&gt;
&lt;p&gt;La siguiente visualización ilustra la disposición de los &lt;a href=&#34;https://en.wikipedia.org/wiki/Common_sunflower#Floret_arrangement&#34;&gt;pétalos del girasol&lt;/a&gt; obediciendo al modelo propuesto por H. Vogel hacia 1979&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>GPU-based Photomosaic</title>
      <link>https://jpcharalambosh.co/posts/photomosaic/</link>
      <pubDate>Sat, 02 Sep 2023 15:07:24 -0500</pubDate>
      <guid>https://jpcharalambosh.co/posts/photomosaic/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This demo visualization shows a &lt;a href=&#34;https://en.wikipedia.org/wiki/Photographic_mosaic&#34;&gt;photomosaic (&amp;amp; videomosaic)&lt;/a&gt; implemented in hardware:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(press &lt;code&gt;r&lt;/code&gt; to randomly pick another painting)&lt;br&gt;














&lt;iframe
  id=&#34;&#34;
  style=&#34;width:675px; height:675px&#34;
  srcdoc=&#34;
        &lt;!DOCTYPE html&gt;
        &lt;html&gt;
          &lt;head&gt;
            &lt;script src=&#39;https://cdn.jsdelivr.net/npm/p5@1.11.10/lib/p5.min.js&#39;&gt;&lt;/script&gt;
            
            &lt;script src=&#39;https://cdn.jsdelivr.net/gh/VisualComputing/p5.treegl/p5.treegl.min.js&#39;&gt;&lt;/script&gt;
            
            
            
            &lt;script src=https://cdn.jsdelivr.net/gh/objetos/p5.quadrille.js/p5.quadrille.min.js&gt;&lt;/script&gt;
            
            
            
            
            &lt;script&gt;
              
&amp;#39;use strict&amp;#39;
let palette
let fbo
let mosaic
let src
let paintings
let video_src
// ui
let resolution
let video_on
let uv
let mode

// MAX_TEXTURE_SIZE seem to be 4096 in my firefox (others don&amp;#39;t complain)
// see: https://t.ly/k7E_
// palette.width -&amp;gt; n * SAMPLE_RES
// 120 wasn&amp;#39;t disliked before
const SAMPLE_RES = navigator.userAgent.includes(&amp;#39;Firefox&amp;#39;) ? 65 : 150

function preload() {
  video_src = createVideo([&amp;#39;wagon.webm&amp;#39;])
  video_src.hide()
  mosaic = readShader(&amp;#39;mosaic.frag&amp;#39;)
  paintings = []
  for (let i = 1; i &amp;lt;= 30; i&amp;#43;&amp;#43;) {
    paintings.push(loadImage(`p${i}.jpg`))
  }
}

function setup() {
  createCanvas(650, 650, WEBGL)
  textureMode(NORMAL)
  noStroke()
  resolution = createSlider(1, 200, 40, 1)
  resolution.position(10, 35)
  resolution.style(&amp;#39;width&amp;#39;, &amp;#39;80px&amp;#39;)
  video_on = createCheckbox(&amp;#39;video&amp;#39;, false)
  video_on.style(&amp;#39;color&amp;#39;, &amp;#39;magenta&amp;#39;)
  video_on.changed(() =&amp;gt; {
    src = video_on.checked() ? video_src : random(paintings)
    video_on.checked() ? video_src.loop() : video_src.pause()
  })
  video_on.position(10, 55)
  src = random(paintings)
  uv = createCheckbox(&amp;#39;uv visualization&amp;#39;, false)
  uv.style(&amp;#39;color&amp;#39;, &amp;#39;magenta&amp;#39;)
  uv.position(10, 70)
  mode = createSelect()
  mode.position(10, 90)
  mode.option(&amp;#39;original&amp;#39;)
  mode.option(&amp;#39;keys&amp;#39;)
  mode.option(&amp;#39;symbols&amp;#39;)
  mode.selected(&amp;#39;symbols&amp;#39;)
  mode.changed(() =&amp;gt; mode.value() === &amp;#39;original&amp;#39; ? resolution.hide() : resolution.show())
  palette = createQuadrille(paintings)
  fbo = createFramebuffer({ width: SAMPLE_RES * palette.width, height: SAMPLE_RES, format: FLOAT })
  sample()
}

function sample() {
  palette.sort({ ascending: true, cellLength: SAMPLE_RES })
  fbo.begin()
  //resetShader()
  drawQuadrille(palette, { cellLength: SAMPLE_RES, outlineWeight: 0, x: -SAMPLE_RES * palette.width / 2, y: -SAMPLE_RES * palette.height / 2 })
  fbo.end()
}

function draw() {
  applyShader(mosaic, {
    uniforms: {
      source: src,
      palette: fbo.color,
      n: palette.width,
      resolution: resolution.value(),
      uv: uv.checked(),
      keys: mode.value() === &amp;#39;keys&amp;#39;,
      original: mode.value() === &amp;#39;original&amp;#39;
    }
  })
}

function keyPressed() {
  if (key === &amp;#39;r&amp;#39; &amp;amp;&amp;amp; !video_on.checked()) {
    src = random(paintings)
  }
}

            &lt;/script&gt;
          &lt;/head&gt;
          &lt;body&gt;
          &lt;/body&gt;
        &lt;/html&gt;
      &#34;
&gt;&lt;/iframe&gt;
&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
