Skip to content

Replace static profile image with animated dot-art portrait#72

Merged
itamarwe merged 6 commits into
masterfrom
claude/profile-dot-art-sampling-uo3vkz
Jun 29, 2026
Merged

Replace static profile image with animated dot-art portrait#72
itamarwe merged 6 commits into
masterfrom
claude/profile-dot-art-sampling-uo3vkz

Conversation

@itamarwe

Copy link
Copy Markdown
Owner

Summary

Replaces the static JPEG profile photo on the About page with an animated dot-art rendering using weighted Voronoi stippling. The portrait is now drawn as white dots on black, sized by brightness, with continuous animation driven by spring physics, curl-noise shimmer, breathing, and pointer repulsion.

Changes

  • components/ProfileDots.tsx (new): Client-side React component that renders the stipple portrait with physics-based animation. Loads pre-computed dot positions from JSON, runs a spring-damped simulation with curl-noise perturbation and breathing, responds to pointer hover, and respects prefers-reduced-motion. The loop pauses when off-screen or the tab is hidden for performance.

  • research/profile-dots/stipple.mjs (new): Node.js script implementing the StippleGen 2 algorithm (Adrian Secord, 2002). Seeds N points by importance sampling, then runs Lloyd relaxation toward density-weighted centroids using Jump-Flooding Algorithm (JFA) for Voronoi computation. Outputs quantized point coordinates and brightness values to public/img/profile-dots/points.json.

  • research/profile-dots/README.md (new): Documentation of the stippling algorithm, parameters, and regeneration instructions.

  • app/about/page.tsx: Swaps next/image for the static JPEG with the new ProfileDots component. Removes image optimization props (width, height, sizes, priority) since the canvas is client-rendered.

  • styles/globals.scss: Adds aspect-ratio: 1 / 1 to .post .profile-photo to maintain square layout for the canvas element.

  • public/img/profile-dots/points.json (new): Pre-computed stipple data (8000 points, 4096-unit quantization).

Implementation notes

  • The animation runs in normalized [0,1] space and rescales on resize; physics parameters (spring stiffness, damping, force amplitudes) are tuned for smooth, recognizable motion.
  • Dots are blitted from a pre-rendered sprite (64×64 white circle) for performance.
  • The loop respects visibility (IntersectionObserver, visibilitychange) and accessibility (prefers-reduced-motion), rendering static stipple when animation is disabled or off-screen.
  • Pointer repulsion creates an interactive "parting" effect around the cursor.

https://claude.ai/code/session_017TDdCdTJbUkH7ZoWDhj7AV

claude added 3 commits June 29, 2026 09:39
Replace the next/image profile photo with a canvas-based "halftone"
rendering. The same-origin portrait is sampled once into a grid of luma
values; each cell becomes a white dot whose radius scales with local
brightness, on a pure-black background. Because the portrait already sits
on black, the background samples to zero-radius dots and disappears.

Sampling (sampleGrid) and drawing (draw) are split so a later animation
pass can re-draw against the cached grid without re-reading pixels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TDdCdTJbUkH7ZoWDhj7AV
Replace the grid sampling with a weighted Voronoi stipple (Secord 2002,
the StippleGen 2 algorithm): points are seeded by importance sampling and
relaxed with Lloyd's iteration toward density-weighted Voronoi centroids
(Voronoi computed per-iteration via jump flooding), yielding a centroidal
Voronoi tessellation whose local dot *density* tracks image brightness.
The dots are now scattered organically rather than on a grid; each dot's
radius also scales with brightness, and the dark background draws nothing.

The point set is precomputed offline by research/profile-dots/stipple.mjs
(decodes the portrait with sharp, ~40 Lloyd iterations) into
public/img/profile-dots/points.json; ProfileDots.tsx just loads and draws
it, so there is no per-load relaxation. Generator + README committed under
research/ per the repo's "keep the figure-generation code" rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TDdCdTJbUkH7ZoWDhj7AV
Each stipple dot now keeps its precomputed spot as a "home"; every frame
a spring pulls it home while a time-varying force field perturbs it, so
the portrait stays recognizable but alive and loops forever:

- curl-noise shimmer: a divergence-free flow field (curl of a scalar
  potential) gives gentle in-place swirling drift, no net drift/clumping
- breathing: a slow radial inhale/exhale about the dot centroid
- pointer repulsion: dots part around the cursor and spring back

Physics runs in normalized space (resize just rescales the draw). Dots
are blitted from a pre-rendered sprite via drawImage for speed, DPR is
capped at 2, and the rAF loop pauses when the canvas is off-screen
(IntersectionObserver) or the tab is hidden. Honors prefers-reduced-motion:
renders the static stipple and never starts the loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TDdCdTJbUkH7ZoWDhj7AV
@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
itamarwe-github-io Ready Ready Preview, Comment Jun 29, 2026 1:18pm

… face

- breathing amplitude +30% (0.012 -> 0.0156) for a more visible inhale/exhale
- pointer repulsion stronger and wider (strength 1.1 -> 2.6, radius 0.16 -> 0.20)
- mask the curl shimmer over the head: a soft elliptical region around the
  face/hair where the swirl ramps to zero, so it no longer distorts the
  features. Breathing and pointer repulsion still apply there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TDdCdTJbUkH7ZoWDhj7AV
Make the pointer repulsion wider (radius 0.20 -> 0.50, ~2.5x) and replace
the linear falloff — which peaked at the cursor and punched a clean hole —
with a soft (1-q^2)^2 bump that has a rounded, zero-slope peak. Dots under
the cursor are now nudged apart gently instead of being completely repelled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TDdCdTJbUkH7ZoWDhj7AV
Animation: run the relaxation at half speed. The spring's natural
frequency is halved (SPRING 120->30, DAMP 22->11) and the forces are
scaled to match (CURL_AMP, POINTER_STR /4; CURL_SPEED /2) so resting
displacements are unchanged — only the settling is slower. Breathing is
untouched.

Pages: merge the Portfolio page into About. The portfolio Background,
Education, and Projects now render below the About intro in
content/pages/about.md; the redundant consulting-recap paragraph that
duplicated the About intro is dropped. Remove the /portfolio route,
content file, nav link, and sitemap entry, and add a permanent
/portfolio -> /about redirect so existing links keep working.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TDdCdTJbUkH7ZoWDhj7AV
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants