类型:Site模板 · 分类:未分类 · 收录:2026-09-19 14:02:01 · 源码:会员可见
Recreate this exact one-page website: “Particle Field”.
Standalone HTML file. CSS and JS inlined. Mobile-responsive.
Pixel-faithful to the spec below. Do not invent extra sections, extra fonts,
extra colours, extra copy, extra pages, or a video.
This is NOT the menuchi / 面打 site. There is no CloudFront film, no Japanese
logotype, no element slides, no WebGPU fire. One dark page. One THREE.Points
cloud. One CPU solver. No bloom pass, no depth of field, no loaded texture,
no loaded 3D model.
Serve over http:// (ES modules will not run from file://).
════════════════════════════════════════════════════════════════
1. PAGE IDENTITY
════════════════════════════════════════════════════════════════
<!doctype html> lang="en"
<title>Particle Field</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
No favicon. No meta description. No Open Graph. No analytics.
════════════════════════════════════════════════════════════════
2. FONTS — system only. Do not load Google Fonts.
════════════════════════════════════════════════════════════════
Body / headings:
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-synthesis-weight: none;
-webkit-font-smoothing: antialiased;
Code and stats:
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
No Inter Tight. No Noto Serif JP. No @font-face. No webfonts at all.
════════════════════════════════════════════════════════════════
3. CDN IMPORT MAP — pin these exact versions
════════════════════════════════════════════════════════════════
<script type="importmap">{
"imports": {
"three": "https://unpkg.com/three@0.185.1/build/three.module.min.js",
"lil-gui": "https://unpkg.com/lil-gui@0.19.2/dist/lil-gui.esm.js",
"lenis": "https://unpkg.com/lenis@1.1.18/dist/lenis.mjs"
}
}</script>
Then in a type="module" script:
import * as THREE from 'three'
import Lenis from 'lenis'
import GUI from 'lil-gui'
════════════════════════════════════════════════════════════════
4. COLOUR TOKENS
════════════════════════════════════════════════════════════════
:root {
--bg: #050706; /* near-black with a green cast */
--ink: #eef2f0;
--muted: #7c8a85;
--accent: #2ee6a0; /* mint */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
html, body background: var(--bg)
body color: var(--ink)
.lede color: #b6c4bf
code color: var(--accent)
overflow-x: hidden
Particle palette (baked into vertex colours, never CSS):
pale #9FF5D4
accent #2EE6A0
deep #14805A
sparkle #FFFFFF (1.5% of interior points)
Starfield colours: #dddddd / #2ee6a0 (6–10%) / #888888
SVG fallback gradient stops: #0c4632 → #14805a → #2ee6a0 → #9ff5d4
stroke #9ff5d4
════════════════════════════════════════════════════════════════
5. DOM — five scroll panels, nothing else
════════════════════════════════════════════════════════════════
<body>
<div id="field" aria-hidden="true"></div> <!-- WebGL canvas host -->
<div id="smooth-wrapper">
<div id="smooth-content">
<section class="panel hero" id="hero">
<p class="eyebrow">three.js · points · no post-processing</p>
<h1 id="title">Particle Field</h1>
<p class="lede" id="lede">
One <code>THREE.Points</code> cloud, a procedural sprite, and a CPU solver.
No bloom pass, no depth of field, no texture, no model.
</p>
<p class="hint">Scroll</p>
</section>
<section class="panel" id="arrival">
<h2>It arrives</h2>
<p>
The field dollies in from 106 world units to 16, interpolated in log space so the
approach reads as even rather than rushing at the end. Every particle starts as
identical fine dust and grows into its own size class on the way in.
</p>
</section>
<section class="panel" id="cursor">
<h2>It answers the cursor</h2>
<p>
Move across it. Particles inside the radius are not pushed away with a falloff —
they are relocated to sit exactly on the rim of a disc, measured perpendicular to
the view axis so the hole stays circular however far the field has rotated.
It snaps in four times faster than it relaxes out.
</p>
</section>
<section class="panel" id="shapes">
<h2>Any shape</h2>
<p>
The silhouette is a swappable sampler. The superellipse is one implementation;
an SVG path is another. Try the shape control, top right.
</p>
</section>
<section class="panel last" id="dissolve">
<h2>Then it goes</h2>
<p>
Each particle travels along a fixed random direction while growing and softening,
so the cloud evaporates instead of merely flying apart.
</p>
</section>
</div>
</div>
<div id="stats" aria-live="off"></div>
</body>
No nav. No “Fire Mask” link. No footer. No logo. Copy is verbatim — do not rewrite.
════════════════════════════════════════════════════════════════
6. CSS LAYOUT — use these values, not approximations
════════════════════════════════════════════════════════════════
#field {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
}
#field canvas { display: block; }
#field .pf-fallback {
position: absolute; top: 50%; left: 50%;
width: min(60vw, 60vh); transform: translate(-50%, -50%);
}
#smooth-wrapper { position: relative; z-index: 1; }
.panel {
min-height: 100vh;
display: flex; flex-direction: column; justify-content: center;
gap: 1.1rem; max-width: 46rem;
padding: 8vh clamp(1.5rem, 6vw, 7rem);
pointer-events: none;
}
.panel.last { min-height: 130vh; }
.panel:nth-child(even) { margin-left: auto; text-align: right; }
.panel:nth-child(even) p { margin-left: auto; }
.eyebrow {
font-size: 0.72rem; letter-spacing: 0.16em;
text-transform: uppercase; color: var(--muted);
}
h1 {
font-size: clamp(2.6rem, 8vw, 5.5rem);
font-weight: 300; letter-spacing: -0.035em; line-height: 0.95;
}
h2 {
font-size: clamp(1.7rem, 4vw, 2.9rem);
font-weight: 300; letter-spacing: -0.03em;
}
p {
font-size: clamp(0.95rem, 1.3vw, 1.08rem);
line-height: 1.65; color: var(--muted);
max-width: 34rem; text-wrap: pretty;
}
code { font-size: 0.9em; color: var(--accent); }
.hint {
font-size: 0.72rem; letter-spacing: 0.18em;
text-transform: uppercase; color: var(--muted); margin-top: 2rem;
}
#stats {
position: fixed; left: 1rem; bottom: 1rem; z-index: 3;
font-size: 0.68rem; line-height: 1.7; color: var(--muted);
white-space: pre; pointer-events: none; opacity: 0.75;
}
/* four lines, padded, updated every 0.25s:
" 53 fpsn6,000 particlesn 0 displacedn 0.00 energy (idle)"
if !webgl: "WebGL unavailable — SVG fallback" */
.lil-gui { --width: 300px; z-index: 4 !important; } /* top-right, default lil-gui */
@media (prefers-reduced-motion: reduce) { .hint { display: none; } }
Also include the official Lenis CSS snippet:
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }
.lenis.lenis-smooth iframe { pointer-events: none; }
MOBILE: the layout is already fluid (clamp + even/odd sides).
Below 768px viewport width, keep 60% of the particle budget
count = round(6000 * 0.6) = 3600
cloudCount = round(260 * 0.6) = 156
Do not hide the GUI. Do not hide stats. Do not stack the even panels
back to the left. Cursor hole only engages on fine pointers
matchMedia('(hover: hover) and (pointer: fine)')
On touch, the cloud wanders on layered sines instead.
════════════════════════════════════════════════════════════════
7. ONE rAF — frame bus
════════════════════════════════════════════════════════════════
A single requestAnimationFrame loop. One Set of subscribers. One scroll
read per frame. NO window 'scroll' listener anywhere.
state = { y, H, time, dt }
y = scroll source (Lenis.scroll, else window.scrollY)
H = innerHeight
time = seconds since loop start
dt = seconds since last frame, CLAMPED to 0.1 (kills tab-switch spikes)
Lenis({ autoRaf: false, smoothWheel: true })
subscribe(() => lenis.raf(performance.now()))
setScrollSource(() => lenis.scroll)
Skip Lenis entirely when prefers-reduced-motion: reduce.
════════════════════════════════════════════════════════════════
8. SCROLL → FIELD DRIVE
════════════════════════════════════════════════════════════════
t = clamp01( easedScrollY / (document.scrollHeight - innerHeight) )
progress = clamp01(t / 0.34) // arrival owns the first ~third
burst = clamp01((t - 0.76) / 0.24) // dissolve owns the last quarter
progress 0 → 1:
camera-to-field distance dollies 106 → 16 in LOG space:
distance = far * (near/far) ^ (0.7*t + 0.3*smoothstep(t))
group.z = cameraZ(16) - distance
extra yaw: progress * π * 1.15
particles grow from identical seedSize 0.115 into their own size class
uLife = 0.18 + 0.82 * smoothstep(clamp01((p - 0.05) / 0.70))
parallax drift fades 1→0 over progress 0.10 → 0.60
burst 0 → 1:
each particle travels along a pre-rolled unit-sphere direction
distance = rMax * (1.6 + rand*2.6)
size *= (1 + burst*0.6); softness mixes toward 1; opacity 0.95 → 0.35
the cloud evaporates, it does not explode
════════════════════════════════════════════════════════════════
9. DEFAULT LOOK (what you see on first paint)
════════════════════════════════════════════════════════════════
shape: Hannya mask (3D) — NOT the star
hornLength: 1
mouthOpen: 1
count: 6000
rimFraction: 0.26 // 26% of non-halo budget on feature edges
thickness: 0.09 // ignored for the volume mask
sizeMultiplier: 1
bigMix: 0.05 // 5% large soft “bokeh” points
sway: 0.15 rad
autoSpin: 0.18 rad/s
holeRadius: 0.17 * max(halfW, halfH)
springIn: 0.22 // snap onto the hole rim
springOut: 0.055 // ~4× slower relax — viscous trail
fov: 50
cameraZ: 16
maxDpr: 2
starfield: ON
layer A: 900 pts, r 30–90, size 0.5, 6% accent
layer B: 700 pts, r 25–80, size 0.32, 10% accent
both pushed behind the field (z = -abs(...) * 0.6 - 4)
slow yaw 0.004 / -0.006; tiny pitch from cursor ndc.y
Size classes (interior):
mix 0.70 size [0.08, 0.135] soft 0.10 // crisp
mix 0.25 size [0.16, 0.25 ] soft 0.45
mix 0.05 size [0.32, 0.58 ] soft 1.00 // the glow. There is NO blur pass.
Rim: size [0.075, 0.13], soft 0.08
Halo: 260 points, spread [1.15, 1.9], amplitude 0.38, dimmed ×(0.55–0.90)
Motion defaults (overridden by the hannya spin above):
swaySpeed 0.16, parallaxReach 0.9, tilt 0.2, wobble 0.07,
wander [0.8, 0.55], oscillation [0.08, 0.05], driftLerp 0.05
group.rotation.x = tilt + sin(time*0.09)*wobble
group.rotation.y = time*autoSpin + sin(time*swaySpeed)*sway + p*scrollSpin
When the GUI switches OFF the 3D mask onto a flat shape:
autoSpin → 0, sway → 0.6 (a thin slab collapses to a line if it spins)
════════════════════════════════════════════════════════════════
10. MATERIAL — custom ShaderMaterial, AdditiveBlending, depthWrite false
════════════════════════════════════════════════════════════════
No texture is loaded. The sprite is procedural in the fragment shader:
q = gl_PointCoord - 0.5; d2 = dot(q,q)*4; discard if d2 >= 1
tight = min(1.0, (1-d2)*1.9) // crisp disc
soft = (1-d2)^2 // wide quadratic glow
a = mix(tight, soft*0.55, vSoft) * vA * uOpacity
discard if a < 0.004
gl_FragColor = vec4(vCol, a)
Vertex:
three-frequency wander from aDrift * uLife * uMotion
gl_PointSize = clamp(size * uSizeMul * uScale / depth, uMinPx, uScale*0.05)
uScale = drawingBufferHeight * 0.5 // resolution-independent
uMinPx = 1.4 * min(devicePixelRatio, 2)
twinkle, depth attenuation, then CALM SDF:
CALM (lets type stay readable on additive particles, no scrim, no blur):
two rounded-box SDFs in clip space after projection
dim particles behind #title and #lede by up to 70%
uCalmOn fades 1 → 0 over progress 0.35 → 0.55
measure getBoundingClientRect of those two elements on resize ONLY,
never inside the frame loop
park unused rects at (99,99)
renderer: WebGLRenderer({ antialias:true, alpha:true }), SRGBColorSpace
clamp viewport to min 1×1 so a hidden tab cannot NaN the projection
frustumCulled = false on the Points (positions move every frame)
If WebGL fails: SVG fallback, class pf-fallback, radial gradient, outline
traced from the sampler (volume → 240 feature dots; 2D → closed path).
════════════════════════════════════════════════════════════════
11. CURSOR HOLE — a punch, not a repulsion field
════════════════════════════════════════════════════════════════
Only when:
motion on AND fine pointer AND pointer live
AND progress >= 0.98 AND burst < 0.5 AND |ndc.x| <= 1
Ray-plane hit → field local space.
Distance is PERPENDICULAR to the view axis (drop the along-view component)
so the hole stays a circle on screen no matter the yaw.
Every particle with d² < holeR² is moved to sit EXACTLY on the rim
(holeR * per-particle jitter ±8%). Dead-centre particles pick a stored angle
in the view-perpendicular basis.
Asymmetric spring: k = inside ? 0.22 : 0.055
When the cursor leaves, keep solving until energy < 0.02, then zero offsets
and idle. That is a real rest, not a small leftover.
════════════════════════════════════════════════════════════════
12. SHAPE SAMPLERS — four, default is the 3D mask
════════════════════════════════════════════════════════════════
A) HANNYA (default). VolumeSampler, is3D:true, scale 0.72.
~14 parametric primitives, y-up, +z toward camera.
After build: YSHIFT -1.68, ZSHIFT -1.19, then * 0.72
bounds: halfW 3.7*s, halfH 6.95*s, halfD 2.25*s
Parts (area-weighted, then cut-rejected):
ellipsoid face shell radii [3.6, 5.0, 2.4], open back (n.z > -0.12),
chin taper warp
two swept-tube horns, cubic Bézier spines, radius 0.06+0.55*(1-s)^0.85
two brow wedges, two cheek ellipsoids
chin knob, nose wedge
two lip ribbons
four fangs as cones INSIDE the mouth cavity (no cuts on purpose)
Cuts: tilted almond eye sockets, nostrils r=0.3, mouth cavity 0.52*mouthOpen
Feature curves (identity): upper lip, 4 fang triangles, nose walls,
horn spines, brow crests, eye rims seated on the shell, nose ridge,
nostril rings, face border, skull dome.
Depth cue: signed frontness — nose/brows/horn tips pale, hollow back dark.
B) SUPERELLIPSE STAR. Lamé polar form.
r(θ) = [(|cosθ|/a)^(2/n) + (|sinθ|/b)^(2/n)]^(-n/2)
n=3.7, aspect=1.05, radius a=5.3, b=a*aspect
Rim: 1440-entry arc-length LUT, binary search, slot jitter 0.7
Fill: rejection-sample angle by (r/rMax)², then sqrt(u)*0.93 for area.
C) BOLT SVG: d = "M12 2 L4 13 H10 L9 22 L20 9 H13 L14 2 Z"
D) RING SVG (hole via opposite winding, nonzero fill rule):
d = "M10,50 a40,40 0 1,0 80,0 a40,40 0 1,0 -80,0 M25,50 a25,25 0 1,1 50,0 a25,25 0 1,1 -50,0"
SVG sampler: getPointAtLength for the outline; rasterise once at 512 and
sample opaque pixels for the interior. y-flip SVG→WebGL. radius 5.3.
════════════════════════════════════════════════════════════════
13. LIL-GUI — title “Particle Field”, width 300, top-right
════════════════════════════════════════════════════════════════
Shape (open)
silhouette ['Hannya mask (3D)', 'Superellipse star', 'Bolt (SVG)', 'Ring (SVG, has a hole)']
horns (mask) 0.5–1.6 step 0.05
grimace (mask) 0.6–1.8 step 0.05
exponent (star) 2–8 step 0.05
aspect 0.6–1.6 step 0.01
slab depth 0–0.6 step 0.005
count 1000–20000 step 500
outline share 0–0.8 step 0.01
Look
size 0.2–3 step 0.05 (live uniform, no rebuild)
bokeh share 0–0.4 step 0.005
Motion
yaw sway (rad) 0–1.5 step 0.01 (live)
continuous spin 0–0.4 step 0.005 (live)
Cursor (folder CLOSED)
hole radius 0.02–0.5 step 0.005
springIn 0.02–1 step 0.01
springOut 0.005–0.5 step 0.005
Rebuild (setShape) onFinishChange for geometry; onChange for live uniforms.
════════════════════════════════════════════════════════════════
14. HARD CONSTRAINTS
════════════════════════════════════════════════════════════════
- One file. One page. No router. No menuchi / fire / hero / model pages.
- No video. No CloudFront. No images. No models. No postprocessing composer.
- Additive blending on a DARK background only. Do not put this on white.
- The “bokeh / bloom” look IS the 5% large soft points. Do not add UnrealBloom,
BokehPass, or a CSS filter:blur.
- One rAF. Lenis rides that loop. WebGL reads the same eased y as the DOM.
- window.field = the field handle (scene, camera, renderer, group, material,
motion, webgl, step, setCalmTargets, setShape, stats, destroy).
- Output a single standalone.html. If WebGL is missing, show the SVG fallback
and the stats line “WebGL unavailable — SVG fallback”.[/hidecontent]