Pure Three.js component — thousands of particles scattered randomly in 3D space, gradually converging into an image sampled from pixels when the user scrolls, and dispersing again when scrolling back.
WHAT
Purpose and context
- Purpose: build a Three.js particle cloud (
THREE.Points+BufferGeometry) that converges from random positions into an image/logo based on page scroll progress and disperses again when scrolling back — prefer copying the original source, the vanilla version inassets/is the fallback. - Trigger (when to use):
- Hero section/portfolio needs an "image assembled from particles" effect when scrolled to.
- User says "particle image", "particles converging into an image", "point cloud morph", "three.js particle scroll".
- Non-goals: NOT used for seamless zoom-blur-style image transitions (that is
blur— shader on 1 plane, no discrete particles) and NOT used for subtle scroll effects that don't need WebGL (that isscroll-effects). Do not rewrite the author's original code.
Mental model
ảnh nguồn → canvas ẩn (sampleResolution) → getImageData + stride + alphaThreshold → targets (+ màu) · starts random → scroll → targetProgress → progress (damping) → position = lerp(starts, targets, progress) → PointsMaterial render.
Input and output contract
| Field | Required? | Meaning | |
|---|---|---|---|
| In | source image/logo (imageUrl) |
no | missing → demo draws sample text onto a hidden canvas to sample |
| In | particleCount, particleSize, sampleResolution, alphaThreshold |
no | initialization parameters ParticleMorph |
| In | project constraints (is pulling external libs allowed?) | yes | decides the original version (default) or assets/ vanilla |
| Out | component in the project | yes | the original file copied as-is (keeping the attribution comment) or assets/demo.* adapted; there is destroy() |
| Out | verify evidence | yes | screenshots at progress 0/0.5/1 + clean console |
Rules and capabilities
- RULE-01 (MUST): Performance: the more particles, the heavier the CPU load (the loop that writes
positionevery frame) as well as the GPU. Suggested safe threshold for mid-range machines: ≤ 5,000–10,000 particles. Measure with DevTools Performance/FPS meter BEFORE increasingparticleCount, don't guess — especially when adding particles for a large/detailed image (increasingsampleResolutionalso increases the sampling cost atinit(), even though it only runs once). - RULE-02 (MUST): Three containers (
three) must be installed via npm (npm install three) and imported via a bundler for production — the demo usesimportmappointing to a CDN (unpkg.com/three@0.169.0) ONLY for running standalone without a bundler, do not use the old CDN version pinned to a version likeblurused to do. When wiring into a real project: delete<script type="importmap">,npm install three, changeimport * as THREE from 'three'to import via bundler. - RULE-03 (SHOULD):
alphaThreshold/brightness filter in_buildSamples()excludes transparent/dark background pixels — a transparent-background image (PNG logo) gives cleaner results than a solid-background image (JPEG); a solid-background image will sample the background too unless cropped first. - RULE-04 (MUST): Verify with
playwright-verify— capture at progress 0/0.5/1 (scroll to the 3 markers), read the console (WebGL context errors are usually silent, do not throw a clear JS error). - RULE-05 (MUST): Use the ORIGINAL first — copy the file as-is, do not rewrite it; keep the attribution comment on the first line; only change content/images/colors; run locally over HTTP, not
file://. - Capabilities: read/copy assets from a public external repo; write front-end files into the project; run the page over HTTP + a headless browser to capture/read the console.
Failure boundaries
- Cannot open the original link / cannot pull the external lib → use
assets/vanilla (branch B01). - FPS drops when increasing
particleCount→ blocked the increase further: re-measure, keep ≤ 5,000–10,000 particles. - Console reports a WebGL context error (usually silent) → failed verify, not handed off.
- Solid-background image samples the whole background → crop first or use a transparent-background PNG.
HOW
Main workflow
| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | judgment | project constraints | Choose source: original Rheinmir/uiux-asset (default) or assets/ vanilla |
chosen source | cannot pull external lib → B01 |
| W02 | effect | running link + code | Open the link run to see the correct effect → copy the file as-is .html (+ _assets/) into the project |
file in the project, with the attribution comment remaining | — |
| W03 | effect | file | Only change content/images/colors (or imageUrl/particleCount/particleSize with the vanilla version) |
adapted component | — |
| W04 | deterministic | page over HTTP | Verify with playwright-verify: capture progress 0/0.5/1, read the console |
3 images + clean console | WebGL/FPS error → fix, repeat W03 |
| W05 | effect | real project | Wire production: delete importmap, npm install three, import via bundler |
build runs | — |
Step-by-step details (source of truth for W01–W05):
Original — use FIRST (copy the file as-is, don't rewrite)
The author's ORIGINAL VERBATIM code (public CodePen = MIT) is in an external repo Rheinmir/uiux-asset — live gallery: https://rheinmir.github.io/uiux-asset/scroll-effects/ .
Process: open the link run to see the exact effect → copy the file as-is .html (+ the folder _assets/ if the file references it) into the project → keep the attribution comment on the first line → only change the content/images/colors. Run locally over HTTP, not file://.
git clone --depth 1 https://github.com/Rheinmir/uiux-asset if you need the whole set offline.
| Original Pen — author | Link |
|---|---|
| Scroll-Driven Particle WebGL Image Matrix — ycw | run · code |
| GSAP ScrollSmoother and Three.js — cmalven | run · code |
| GSAP ScrollTrigger Disintegration Effect — dev_loop | run · code |
assets/ in this skill is just a self-written vanilla pared-down version — use it when the project must NOT pull in external libraries; by default use the original above.
Assets
assets/demo.html + assets/demo.css + assets/demo.js — read then ADAPT (change imageUrl, particleCount, particleSize when initializing ParticleMorph). The demo draws sample text onto a hidden canvas to sample when no imageUrl, so it runs immediately without images/network.
Mechanism
- Image sampling: draw the source image (or a hand-drawn canvas) onto a hidden
<canvas>sizedsampleResolution, readgetImageData(). Iterate pixels with a step size (stridecomputed from the desiredparticleCountversus the total pixels) — do NOT take every single pixel, too heavy — keep pixels bright enough/not transparent (alphaThreshold) as destination coordinates + RGB color. - Particle: each sample coordinate = 1 particle in the
THREE.Points(BufferGeometryattributeposition). Initial position (starts) is random within a large volume of space ("explosion"), destination position (targets) is the sampled pixel coordinate, stored in parallel in two separateFloat32Array(not two GPU attributes). - Scroll → progress: a single scroll listener, rAF-throttled (no GSAP/ScrollTrigger — the demo is deliberately written in vanilla to avoid dependency overlap with
gsap-scrolltrigger-pin), computestargetProgressfromscrollY / (scrollHeight - innerHeight). - Lerp each frame:
progressis gradually smoothed towardtargetProgress(simple damping, not directly animatingscrollY), then each elementposition[i] = lerp(starts[i], targets[i], progress), setgeometry.attributes.position.needsUpdate = true. - Color + size:
PointsMaterial({ size, sizeAttenuation: true, vertexColors: true })— keeps the original image color via the staticcolorattribute (set once at build time, does not change with progress). - Cleanup: written as a class with
destroy()— cancelrequestAnimationFrame, remove thescroll/resizelistener, callgeometry.dispose(),material.dispose(),renderer.dispose().
Branches
| ID | Kind | Guard | Behavior | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | conditional_required | the project is NOT allowed to pull external libs | use assets/demo.html + assets/demo.css + assets/demo.js, ADAPT the init parameters ParticleMorph |
— | W04 |
| B02 | capability_optional | needs the whole offline set | git clone --depth 1 https://github.com/Rheinmir/uiux-asset |
if there is network access to the gallery → skip | W02 |
Validation and stopping
Verify by actually running it: playwright-verify capture 3 scroll milestones + read the console (WebGL errors are usually silent); measure performance with DevTools Performance/FPS meter, do not guess. Visual review: the image converges into the right shape at progress 1, disintegrates at 0. Stop when the 3 images are correct + the console is clean.
Examples
- Positive: a hero portfolio needs a PNG logo to converge when scrolling → copy exactly
scroll-driven-particle-webgl-image-matrix.htmlfrom the gallery, keep the attribution comment, change the logo image → capture 0/0.5/1 to see scattered particles → half the image → full logo, console clean. - Boundary/failure: the project forbids external CDN libs → use
assets/demo.js,particleCount30,000 makes the FPS drop → lower to ≤ 10,000 based on measurement, then verify again. - Boundary: the user wants to transition image A→B with a zoom-blur effect on a single plane → do not use this skill, switch to
blur.