motionsites中文站 / Saffron Field
Saffron Field
aadmin2026-11-03 发布Site模板0 阅读

Saffron Field

类型:Site模板 · 收录:2026-11-03 21:25:01 · 源码:会员可见

[hidecontent type="vip1"]
Build a single standalone index.html file (inline <style> and one inline <script type="module">, no build step) for a scroll-driven, WebGL particle landing page called "The field of the saffron". Dark, warm, orange, cinematic. Everything is drawn in code; there are no images or videos. It must be fully mobile responsive and work by double-clicking the file (internet needed for the CDN and the font).

==================================================
1. HEAD, LIBRARIES, FONT
==================================================
- <title>The field of the saffron · Season report</title>
- meta viewport: width=device-width, initial-scale=1, viewport-fit=cover; theme-color #0b0705.
- Font: ONLY Geist from Google Fonts. No other font, no monospace:
  <link href="https://fonts.googleapis.com/css2?family=Geist:wght@300..700&display=swap" rel="stylesheet"> (with preconnect to fonts.googleapis.com and fonts.gstatic.com).
- three.js r160 through an import map:
  {"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"}}
- No all-caps text anywhere (no text-transform: uppercase). All copy is sentence case.
- No divider lines or borders between content blocks.

==================================================
2. DESIGN TOKENS (CSS custom properties on :root)
==================================================
--soot:#0b0705 (page background)
--char:#1c0e06
--ember:#ff6a13 (main orange accent)
--apricot:#ffb36b (light orange)
--ash:#fbe9d7 (main text)
--muted:#c39a7d (secondary text)
--line:rgba(255,179,107,.22)
--mono and --display: both 'Geist', 'Helvetica Neue', Arial, sans-serif
--pad: clamp(16px, 3vw, 36px)
--ease: cubic-bezier(.2,.8,.2,1)
html/body: margin 0, background var(--soot), color var(--ash), antialiased, overflow-x hidden.
On fine-pointer devices hide the native cursor (cursor:none on body and buttons).

==================================================
3. PAGE STRUCTURE (DOM order)
==================================================
1. <canvas id="gl"> fixed full-screen, z-index 0, touch-action pan-y.
2. .shade: fixed overlay, pointer-events none, z-index 1, three stacked backgrounds:
   radial-gradient(120% 80% at 50% 42%, transparent 35%, rgba(11,7,5,.82) 100%),
   linear-gradient(to top, rgba(11,7,5,.9) 0%, transparent 38%),
   linear-gradient(to bottom, rgba(11,7,5,.7) 0%, transparent 16%).
3. .grain: fixed, inset -50%, opacity .07, mix-blend-mode overlay, an inline SVG data-URI feTurbulence fractalNoise (baseFrequency .9, 2 octaves, 160x160 tile), animated with a 1.2s steps(4) infinite translate jitter.
4. #scroll-space: an empty div of height 1100vh that only creates scroll length.
5. .ui: fixed overlay (inset 0, z-index 5, pointer-events none; buttons and links inside get pointer-events auto) containing:
   a) header.top: flex, space-between, padding var(--pad) (top respects the safe area).
      - Left: brand link "saffron" (Geist 600, 19px, letter-spacing -.03em) with a 22px SVG icon before it: five ellipses (rx 2.6, ry 5, centred at 12,6.5) rotated 0/72/144/216/288 deg around 12,12, stroke #ff6a13, width 1.2, plus a centre circle r1.6 fill #ffb36b. The icon spins slowly (14s linear infinite). Clicking scrolls to the top.
      - Centre (absolutely centred): status label, a 6px round ember dot (NO glow or box-shadow; blinks with opacity 1 → .25 over 1.6s) followed by text. The text changes per scroll state: "Scroll to sink in" (hero), "Stay close" (inside a stat section), "Keep drifting" (between sections), "Almost there" (after the last stat), "The last light" (final section). When it changes it fades and slides 6px down, then the new text fades in (300ms). Debounce with a pending-target variable and clearTimeout so fast scrolling never shows a stale label.
      - Right: button "Sound off" / "Sound on" with four 2px apricot equaliser bars (3px tall; when on they animate 3px→11px, staggered).
   b) ul.rail: vertically centred on the right edge, gap 14px, six buttons labelled 00–05, each with a hidden <em> label (Field, Bloom, Harvest, Wings, Gem, Picks) that fades in on hover/active. After the number is a 4px round dot (currentColor). The active one turns ember, scales 2x and gets a soft ember glow. Clicking smooth-scrolls to that section. Hidden below 860px.
   c) Six content sections (absolutely positioned and stacked; only the active one is visible):
      - Hero (data-sec 0): a 2-column grid (1fr | max 360px), aligned to the bottom, padding-bottom clamp(80px,13vh,140px).
        Left: h1, Geist 340 weight, clamp(54px,10.6vw,180px), line-height .86, letter-spacing -.055em, two block lines: "The field" and "of the saffron" (2nd line indented .9em and coloured apricot).
        Right: paragraph, Geist 400, 15px/1.6, muted colour, max 40ch:
        "A season in the saffron field, told in shapes and picks: what bloomed, who tended the finest crocus rows, and which harvest glowed brightest."
      - Four stat sections (data-sec 1–4), centred at the bottom (bottom: clamp(76px,12vh,128px)). Each is ONLY a headline plus a small counter (no captions, no labels):
        h2 Geist 380, clamp(42px,8.6vw,132px), line-height .92, letter-spacing -.05em, followed by a <sup class="count"> in ember, clamp(13px,1.5vw,20px), font-variant-numeric: tabular-nums, inline-block, min-width 4.2ch (so the centred headline never jiggles while counting).
        1 "Blooms gathered" (742)
        2 "Harvests" (318)
        3 "Pollinators" (126)
        4 "Growers known" (254)
        When a section becomes active the counter counts from (000) to its value over 1.3s (ease-out cubic, 450ms delay, zero-padded to 3 digits).
      - Outro (data-sec 5): at the bottom, a flex row with h2 "Until the next bloom" (Geist 380, clamp(34px,5.4vw,84px), max 12ch) on the left and a pill button "Replay the season ↺" on the right (1px var(--line) border, radius 999px, padding 12px 18px; on hover the background turns ember and the text soot). The button scrolls to the top. It is not clickable when the section is inactive.
   d) footer.bottom: flex space-between, padding var(--pad) (bottom respects the safe area), 12px Geist 500: left button "Share" (navigator.share, otherwise copy the URL to the clipboard and show "Link copied" for 1.8s, or "Copy the URL" on failure); right "Made by <b>Ember Studio</b>" (muted, bold part ash). Nothing in the middle, no progress meter.
6. Custom cursor: .cursor (36px ring, 1px rgba(255,106,19,.9), follows with lerp) and .cursor-dot (4px apricot, exact position). It grows to 64px with a faint ember fill over buttons/links and shrinks to 22px while pressed. Hidden on touch devices.
7. Click/tap feedback: two DOM rings (1px apricot, then ember 120ms later) expand from 0 to 220px and fade over 1s at the pointer.
8. Loader: full-screen soot overlay (z 40), centred column: a 120px SVG ring (track stroke var(--line), arc stroke ember, round cap, dasharray 339.3, rotated -90deg) with a number 0→100 in the middle (Geist 300, 34px), and "Warming the field" under it (12px muted). Progress follows time (~1.6s) but holds at 88 until document.fonts.ready resolves. When done it fades out (1s), the inner content scales to 1.6 and fades, scrolling unlocks (body.loading has overflow hidden), and 350ms later the scroll story becomes "ready".
9. A WebGL fallback message if the renderer fails.

Base label style (.label): Geist 500, 12px, line-height 1, letter-spacing .01em.

==================================================
4. TEXT REVEAL ANIMATION
==================================================
Split every .split element into per-character spans (.ch) wrapped in no-wrap word spans (.w), with an aria-label of the full text and the spans aria-hidden. Each char gets --i = its running index (the hero's second line continues the index from 9).
.ch default: opacity 0, translateY(.45em) rotate(4deg), blur(10px); transitions: opacity .5s, transform .9s var(--ease), filter .7s; exit delay calc(var(--i)*6ms).
.is-active .ch: opacity 1, no transform, blur 0, delay calc(var(--i) * var(--s,34ms) + var(--d,0ms)).
The hero paragraph uses --s:7ms, --d:500ms and no blur or transform (fade only).
.fx elements (counters, replay button) fade up 14px with delay var(--d).
prefers-reduced-motion: no blur or transform on chars; grain and logo spin stop.

==================================================
5. WEBGL SCENE (three.js)
==================================================
Renderer: antialias on desktop only, alpha FALSE, clear colour 0x0b0705 (opaque, so additive layers never show dark silhouettes), pixel ratio min(dpr, 2) (1.6 on small screens). PerspectiveCamera fov 55 (62 when aspect < 0.8), near 0.1, far 200. Add the camera to the scene (the particle sculptures are its children).
isSmall = min(innerWidth, innerHeight) < 700.

Include the standard Ashima 3D simplex noise (snoise(vec3)) GLSL.

---- 5a. The terrain ("silk field") ----
Field space: x from -42 to 42 (width 84), z from -78 to 22. Two fields share one design:
- floor: flip = +1, base 0, seed 0
- ceiling: flip = -1, base 7.4, seed 13.7, uSide = 1 (only visible at the far left and right)
Each field = a sparse Points layer (sparkles) + a continuous PlaneGeometry surface (the main look), sharing ONE uniforms object:
uTime, uTravel, uPR, uFlip, uBase, uSeed, uSide, uCenter, uVis, uTrail (texture), uRip[6] (vec4: x, z, startTime, strength).

Shared GLSL fieldHeight(xz) returns vec3(height, rippleGlow, trail):
  zz = xz.y - uTravel
  h = snoise(vec3(x*0.045+uSeed, zz*0.045, t*0.04))*2.3 + snoise(vec3(x*0.12, zz*0.12+uSeed, t*0.09))*0.6
  trail = smoothstep(0.03, 0.9, texture(uTrail, uv).r), with uv = ((x+42)/84, (z+78)/100); h -= trail*1.0 (the cursor presses a soft dent)
  ripples: for each of 6, age = t - start (0..5s): d = distance; front = age*9; w = exp(-(d-front)^2*0.3); dec = (1-age/5)^1.6*strength; h += sin((d-front)*1.3)*w*dec*2.0; glow += w*dec*1.2
fieldCurve(x) = pow(|x|/34, 2.2)*11  (the field dips in the middle and climbs gently into the left/right corners)
sideMask(x) = mix(1, smoothstep(11, 26, |x|), uSide) * mix(1, mix(0.4, 1, smoothstep(3, 17, |x|)), uCenter)
final y = uBase + uFlip*(h + fieldCurve(x))

SURFACE (main visual), PlaneGeometry(84, 100, SEGS, SEGS*1.2) rotated to XZ and centred on z = -28; SEGS = 300 desktop / 140 mobile; additive blending, depthWrite false, DoubleSide:
- Vertex: compute the height plus two extra samples at +0.4 in x and z for a smooth normal (cross of the z-tangent and the x-tangent); pass the normal, world position, height, glow and trail. Fade = (1 - smoothstep(10, 52, depth)) * smoothstep(0.8, 5, depth) * sideMask.
- Fragment: flip the normal toward the camera; fres = (1 - N·V)^3; diff = max(N·normalize(-0.35, 0.85, 0.4), 0); ridge = smoothstep(0.3, 2.0, h).
  Silky streaks along the flow: s1 = snoise(vec3(x*0.55+seed, zz*0.09, t*0.05)), s2 = snoise(vec3(x*1.9, zz*0.22+seed, t*0.08)), streak = clamp(0.5+0.35*s1+0.2*s2, 0, 1).
  glow = min((0.22 + fres*0.85 + diff^3*0.25 + ridge*0.35)*(0.35 + streak*0.95), 1.35)
  colour: mix(deep(0.32,0.075,0.015) → ember(1,0.42,0.08) by glow*1.3), then → pale(1,0.8,0.55) by smoothstep(0.85,1.6,glow); multiply by glow*0.9.
  Hover contour lines: k = h*3.2; line = 1 - min(|fract(k)-0.5| / (fwidth(k)*1.25), 1), faded out where fwidth > 0.35..1; reveal = clamp(smoothstep(0,0.55,trail) + rippleGlow*0.7, 0, 1); iridescent tint iri = 0.5+0.5*cos(6.2831*(vec3(0,.33,.67) + h*0.32 + trail*0.5 + t*0.04)); lineColour = mix(mix(ember, pale, reveal*0.6), iri, 0.2); edge = trail*(1-trail)*4; hover = lineColour*line*reveal*0.55 + mix(ember, iri, 0.4)*(trail*0.04 + edge*0.035).
  Output: (silk*(1 - trail*0.65)*(1 - edge*0.55) + hover)*fade*uVis, alpha 1.

SPARKLES (Points): a jittered grid of 115x115 (70x70 mobile) for the floor, 0.8x for the ceiling, attribute aRand. Each point sits at the surface height + 0.12 + aRand*0.7 (floating just above). Size = (0.55 + aRand²*1.6)*(1 + (glow + rim*0.5)*1.2)*uPR*56/depth, clamped 1.5..20; rim = trail*(1-trail)*4. Twinkle = 0.5+0.5*sin(t*(0.8+aRand*1.6) + aRand*90). Soft round point (pow falloff 1.6), colour mix((1,.55,.16) → (1,.86,.6)), alpha = soft*fade*(0.35+0.65*twinkle)*(1+glow)*(1-trail*0.5); fade = (1 - smoothstep(14,70,depth))*smoothstep(1,5,depth)*sideMask. Additive.

FLOATING DUST: 1600 (700 mobile) points in a box (x ±25, y -1..8, z -60..20), wrapped along z with travel, slowly bobbing, apricot (1,.72,.42), visibility 0.3 → 1 as you dive in.

---- 5b. Cursor trail (the hover effect) ----
A 2D canvas texture (384px desktop, 256 mobile) per field mapped to field space, linear filtering. Each frame: fill with rgba(0,0,0, 1 - exp(-dt*0.6)) to fade it, then, while the mouse is over the page (or within 600ms of a tap), raycast the pointer onto a horizontal plane (floor: y = 0.4 + FLOOR_DROP*dive; ceiling: y = 6.2, only when looking up and the ceiling is visible) and paint soft white radial dabs (radius 3.6 field units; gradient alpha 0.1 → 0.055 at 50% → 0), interpolated along the segment from the previous point every 0.3 radius so fast moves draw a continuous stroke. Ignore jumps longer than 25% of the canvas.

---- 5c. Click ripples ----
pointerdown (not on a button/link; guard with e.target instanceof Element): if the ray points down, add a ripple on the floor at the hit point, otherwise on the ceiling. Round-robin the 6 uRip slots with the start time = current uTime. Also add 0.9 to the sculpture pulse (0.3 with reduced motion), spawn the DOM tap rings, and play the blip sound.

---- 5d. Camera and scroll ----
prog = scrollY/maxScroll, smoothed every frame (prog += (target - prog)*min(1, dt*4.2)); dt = clamp(Δt, 0, 0.05).
dive = smoothstep(0.06, 0.17, prog).
Hero camera position (0, 10.5, 24) looking at (0, 0.5, -14) → inside position (0, 3.7, 9) looking at (0, 3.5, -14), lerped by dive. Mouse parallax: pos.x += mouse.x*(1.2 - dive*0.4), pos.y += mouse.y*0.5, look.x += mouse.x*0.6 (smoothed mouse), plus a gentle bob sin(t*0.5)*0.15*dive.
uTravel = prog*120 + time*1.0 (0.2 with reduced motion), so the terrain flows toward you.
Floor uBase = FLOOR_DROP(-2.0)*dive; floor uCenter = dive (thins the middle when inside); ceiling uVis = dive*0.85.

Sections by prog: hero [0, .085), flower [.17, .31), ring [.31, .45), butterfly [.45, .59), gem [.59, .73), halo/outro [.79, 1]. Pad each range inward by .012 except the hero's start and the last section's end. Toggle .is-active and aria-hidden on the panels, update the rail and the status label.

---- 5e. Particle sculptures (centre of each section) ----
N = 7500 points (4200 mobile), a Points object inside a group parented to the camera at (0, rigY, -7.5); additive, depthTest false, renderOrder 10. Precompute a Float32Array of target positions for each shape; each frame move every particle toward the active target with its own speed (0.022..0.072 per 60fps frame), so particles fly between shapes.
- scatter (hero and gaps): random ring of radius 4..16 around the view, z from -7 to 3.
- flower: 6-petal rose r = 2.15*(0.18 + 0.82*|cos(3θ)|^0.75), filled toward the edge (r*rand^0.33), a cup lift z = (q/2.15)²*0.55 - 0.15; a 25% inner layer rotated by π/6 at 1.35 scale and lifted; a 10% seed head; 7% stamens. Base tilt x -0.35.
- ring: torus R 2.05, tube 0.26 (72%), a dashed outer orbit R 2.75 (18%), an inner hairline R 1.45; it lies in the XZ plane, tilt x 0.42, spins.
- butterfly: Temple Fay curve x = sin t·(e^cos t − 2cos 4t − sin⁵(t/12)), y = cos t·(same), t in 0..12π, scale 0.58, y −0.35, filled toward the edge; 8% body, 3% antennae. Wings flap: z += |x|*sin(t*2.4)*0.6*0.85, x *= 1 − |flap|*0.22. Tilt x 0.15.
- gem: 8-sided brilliant cut, girdle R 1.75 at y 0.3, table R 0.95 at y 0.95, culet at y -1.75; 45% of points snapped to the facet edges; tilt x 0.22, spins.
- halo (outro): 34% thin ring R 2.75 + the rest a flower at 0.95 scale.
Shader: per-point shimmer 0.025*sin noise; pulse pushes along normalize(p) by uPulse*(0.35+aRand)*0.9 (the pulse decays by pow(0.04, dt)).
Hover on the sculpture (soft bulge, NO hole or ring): d = (ndc - uMouse)*vec2(aspect, 1); g = exp(-dot(d,d)/0.045)*uHover; mv.xy += d*g*0.55*(-mv.z)*0.52. uMouse lerps toward the pointer with dt*5, uHover eases with dt*2.5 (0 on touch), so particles drift back smoothly.
Size (0.6 + aRand*1.4)*uPR*26/depth, clamped 1..22; colour mix((1,.38,.06) → (1,.88,.7) by aRand³ + g*0.25); alpha soft*twinkle(0.55..1)*uAlpha*0.55; uAlpha eases to 1 on a shape, 0.32 on scatter.
Group rotation: x eases to the shape's tilt − mouse.y*0.25; y = spin + sin(t*0.4)*0.3 + mouse.x*0.45 (spinning shapes add dt*0.45; others ease back to the nearest full turn).
Responsive rig: scale = portrait ? clamp(aspect*1.25, 0.52, 0.9) : 1 (×0.82 on the outro); rigY = (portrait ? 0.95 : 0.6) + (outro ? (portrait ? 0.35 : 0.2) : 0), both eased.

==================================================
6. SOUND (WebAudio, off by default)
==================================================
On the first toggle create an AudioContext: two sawtooth oscillators at 55 and 55.35 Hz (gain .16), a triangle at 82.4 (.1) and a sine at 110.6 (.08) → lowpass 420 Hz, Q 5, its frequency modulated by a 0.06 Hz LFO (depth 240) → master gain. Toggle fades the master to 0.14 or 0 (setTargetAtTime 0.5). Blip on click: a sine from 740–940 Hz down to 220 Hz over 0.6s, gain envelope 0.12 peak, 0.85s long.

==================================================
7. RESPONSIVE
==================================================
≤860px: hide the rail; hero becomes 1 column (gap 18px), the second h1 line indent is .5em, hero paragraph 14px; outro row stacks (column, left aligned).
≤560px: hide the status label and "Made by"; stat bottom = calc(84px + safe area); outro bottom = calc(72px + safe area); replay padding 10px 14px.
Landscape with height ≤560px: stat h2 clamp(32px, 6vw, 64px).
Particle counts and surface detail drop on small screens (see above). No horizontal scroll at 375px. Visible :focus-visible outline (1px apricot, offset 4px). Respect prefers-reduced-motion.

==================================================
8. PERFORMANCE AND ROBUSTNESS
==================================================
frustumCulled = false on all field and sculpture objects. Mark the sculpture position attribute DynamicDrawUsage and set needsUpdate every frame. Handle resize (renderer size, aspect, fov switch, sculpture aspect uniform). Wrap the share/clipboard calls in try/catch. Call scrollTo(0,0) when the loader finishes.

Deliver the complete, working index.html in one file.
[/hidecontent]
← 返回首页