FinSmith
aadmin2026-10-26 发布未分类2 阅读

FinSmith

类型: · 收录:2026-10-26 23:39:21 · 源码:会员可见

[hidecontent type="vip1"]
Build a single standalone index.html (inline <style> and one inline <script type="module">; no build step, no local assets, everything by URL) containing ONE full-viewport hero section: a lacquer-red page with a giant cream headline and a real 3D koi fish that swims freely over it, casts a soft shadow, follows the cursor/finger, and comes to wherever the user clicks or taps. Must be mobile responsive down to 375px. Follow every value below exactly.

=== EXTERNAL URLS (use exactly) ===
3D koi model (GLB, CORS-enabled, ~17.8 MB):
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261013_132630_0605aa00-d02f-4d71-9db5-e0d1a8232667.glb
Fonts: https://fonts.googleapis.com/css2?family=Anton&family=Outfit:wght@300;600&family=Poppins:wght@700&display=swap
Import map:
  "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js"
  "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
Imports: THREE, GLTFLoader (three/addons/loaders/GLTFLoader.js), RoomEnvironment (three/addons/environments/RoomEnvironment.js).
Add <link rel="preconnect"> for fonts.googleapis.com, fonts.gstatic.com (crossorigin), d8j0ntlcm91z4.cloudfront.net (crossorigin), cdn.jsdelivr.net (crossorigin).

=== HEAD ===
lang="en"; <title>Finsmith — Craft</title>
meta description: "Branding shaped to endure and evolve with your ambition."
meta viewport: width=device-width, initial-scale=1, viewport-fit=cover; meta theme-color #b0060e.

=== MARKUP (in this order inside <main class="hero" id="hero">) ===
1. <div class="sky" id="sky" aria-hidden="true"> (sparkles injected by JS)
2. <h1 class="hero__title">Craft</h1>
3. <canvas class="koi" id="koi" aria-hidden="true">
4. <nav class="nav" aria-label="Primary">: <button class="nav__menu" type="button" aria-label="Show menu"> containing an inline SVG hamburger (viewBox 0 0 24 14, stroke currentColor, stroke-width 2, round caps, path "M1 1h22M1 7h22M1 13h22"); then <span class="nav__rule" aria-hidden="true">; then <a class="nav__link" href="#services">Services</a>
5. <div class="hero__foot">: <p class="hero__lede">Branding shaped to endure and evolve<br>with your ambition</p> and <a class="logo" href="#" aria-label="Finsmith, home">FINSMITH<sup aria-hidden="true">®</sup></a>
6. <p class="status" id="status" role="status">Calling koi</p>

=== CSS TOKENS (:root) ===
--red:#b0060e; --red-lift:#c20b14; --red-deep:#8f030a; --shadow:74 0 6; --cream:#f7f2e8; --ink:#17110f;
--gutter:clamp(20px,3vw,60px);
--display:"Poppins","Arial Black",sans-serif; --body:"Outfit","Helvetica Neue",Arial,sans-serif; --logo:"Anton","Arial Narrow",Impact,sans-serif;

=== CSS ===
* { box-sizing:border-box }
html,body { margin:0; height:100%; background:var(--red); color:var(--cream); font-family:var(--body); -webkit-font-smoothing:antialiased; overflow:hidden }
.hero { position:relative; height:100vh; height:100svh; min-height:420px; overflow:hidden; isolation:isolate; touch-action:pan-y;
  background: radial-gradient(120% 90% at 58% 46%, var(--red-lift) 0%, var(--red) 46%, var(--red-deep) 100%) }
.sky { position:absolute; inset:0; z-index:0; pointer-events:none }
.star { position:absolute; width:var(--s); height:var(--s); translate:-50% -50%; fill:#fff3d6;
  filter:drop-shadow(0 0 calc(var(--s) * .35) rgb(255 214 150 / .75)); animation:twinkle var(--d) ease-in-out var(--o) infinite }
.star.dot { border-radius:50%; background:#ffe9c4; filter:none }
@keyframes twinkle { 0%,100% { opacity:.95; scale:1 } 50% { opacity:.25; scale:.55 } }
.hero__title { position:absolute; z-index:1; left:50%; top:44%; translate:-50% -50%; margin:0; font-family:var(--display); font-weight:700;
  font-size:min(23.6vw,62vh); line-height:.8; letter-spacing:-.035em; color:var(--cream); white-space:nowrap; user-select:none }
.koi { position:absolute; inset:0; z-index:2; width:100%; height:100%; display:block; pointer-events:none; opacity:0; transition:opacity 1.1s ease;
  filter:drop-shadow(var(--sx,-10px) var(--sy,26px) var(--sb,22px) rgb(var(--shadow) / .5)) }
.koi.is-ready { opacity:1 }
.nav { position:absolute; z-index:3; top:calc(var(--gutter) * .8 + env(safe-area-inset-top)); right:calc(var(--gutter) * .88 + env(safe-area-inset-right));
  display:flex; align-items:center; height:clamp(46px,3.3vw,66px); padding:0 clamp(6px,.5vw,10px); background:var(--cream); border-radius:999px; color:var(--ink) }
.nav__menu,.nav__link { display:grid; place-items:center; height:100%; border:0; background:none; color:inherit; font:inherit; cursor:pointer; text-decoration:none; border-radius:999px; transition:opacity .2s ease }
.nav__menu { width:clamp(44px,3.2vw,62px); padding:0 }  .nav__menu svg { width:clamp(18px,1.2vw,24px); height:auto }
.nav__link { padding:0 clamp(14px,1.2vw,24px); font-weight:600; font-size:clamp(12px,.78vw,15.5px); letter-spacing:.01em; text-transform:uppercase }
.nav__rule { width:1px; height:44%; background:rgb(23 17 15 / .55) }
.nav__menu:hover,.nav__link:hover { opacity:.6 }
.hero__foot { position:absolute; z-index:3; left:calc(var(--gutter) + env(safe-area-inset-left)); bottom:calc(var(--gutter) * 1.25 + env(safe-area-inset-bottom)); pointer-events:none }
.hero__lede { margin:0 0 clamp(18px,2.3vw,46px); font-weight:300; font-size:clamp(16px,1.27vw,26px); line-height:1.2; letter-spacing:-.005em }
.logo { display:inline-flex; align-items:flex-start; font-family:var(--logo); font-size:clamp(34px,3.05vw,62px); line-height:.9; letter-spacing:-.012em; color:var(--cream); text-decoration:none; pointer-events:auto }
.logo sup { font-family:var(--body); font-weight:600; font-size:.36em; line-height:1; margin:-.05em 0 0 .18em }
.status { position:absolute; z-index:3; right:calc(var(--gutter) + env(safe-area-inset-right)); bottom:calc(var(--gutter) * 1.25 + env(safe-area-inset-bottom)); margin:0;
  font-size:12px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:rgb(247 242 232 / .8); transition:opacity .6s ease }
.status[hidden] { display:block; opacity:0 }
:focus-visible { outline:2px solid var(--cream); outline-offset:4px }   .nav :focus-visible { outline-color:var(--ink); outline-offset:-4px }
@media (orientation:portrait) { .hero__title { font-size:27.5vw; top:40% } }
@media (max-width:600px) { .koi { filter:drop-shadow(var(--sx,-6px) var(--sy,14px) var(--sb,12px) rgb(var(--shadow) / .5)) } }
@media (max-height:480px) and (orientation:landscape) { .hero__lede { margin-bottom:12px; font-size:14px } .logo { font-size:30px } }
@media (prefers-reduced-motion:reduce) { .star { animation:none } .koi { transition:none } }

=== SPARKLES (JS, into #sky) ===
Six four-point SVG stars (viewBox 0 0 24 24, path "M12 0c.7 7.2 4.8 11.3 12 12-7.2.7-11.3 4.8-12 12-.7-7.2-4.8-11.3-12-12 7.2-.7 11.3-4.8 12-12z", class "star") at [left%, top%, size px]:
[21.6,13.4,34] [83.6,76.6,36] [31,23,13] [30.2,67.4,11] [50.2,81,9] [74.9,30.3,9].
Plus 16 dots (<span class="star dot">) from a seeded PRNG: seed=7; rand = () => (seed = seed*16807 % 2147483647) / 2147483647; left = 4+rand()*92 %, top = 5+rand()*88 %, size = 2.5+rand()*2.5 px.
Every star sets inline: left, top, --s (size), --d = 2.4+rand()*3.6 s, --o = -rand()*6 s.

=== THREE.JS SCENE ===
WebGLRenderer({canvas, alpha:true, antialias:true, powerPreference:'high-performance'}), clear color 0x000000 alpha 0, toneMapping NeutralToneMapping, exposure 1.0.
scene.environment = PMREMGenerator.fromScene(new RoomEnvironment(), 0.04).texture; scene.environmentIntensity = 0.85.
PerspectiveCamera(fov 35, near 0.1, far 50) at (0,0,10) looking down -Z.
Lights: DirectionalLight 0xfff0dc intensity 2.4 at (-3,5,8); DirectionalLight 0xffb48a intensity 0.9 at (5,-3,4); AmbientLight 0xffffff 0.35.
resize() (run via ResizeObserver on #hero; return early if width or height is 0): pixelRatio = min(devicePixelRatio, width<700 ? 1.75 : 2); setSize(w,h,false); update aspect;
  view.h = 2*10*tan(17.5°); view.w = view.h*aspect;
  view.len (fish length in world units) = landscape ? min(view.w*0.66, view.h*1.15) : min(view.w*1.2, view.h*0.62);
  view.bx = (view.w/view.len)*0.5*0.8; view.by = (view.h/view.len)*0.5*0.74; uLen = view.len.

=== MODEL BAKE ===
The GLB is one mesh: nose at +Z, dorsal (back) at +X, Y lateral, length along Z. After load, bake each mesh's geometry so x = 0 (nose) → 1 (tail tip), y = lateral, z = dorsal (toward camera), all divided by the model length:
  len = box.max.z - box.min.z
  bake = Scale(1/len) · Matrix4.set(0,0,-1,0, 0,1,0,0, 1,0,0,0, 0,0,0,1) · Translate(0,0,-box.max.z)
  geometry.clone().applyMatrix4(bake · mesh.matrixWorld)
Texture anisotropy = min(8, max). mesh.frustumCulled = false. Call renderer.compile, then hide the status text.
Loader progress text: `Calling koi ${percent}%`. On error: console.error and show "Our koi is unable to appear".

=== KOI MATERIAL (clone the GLB material; transparent=true, depthWrite=true; patch with onBeforeCompile) ===
Uniforms: uSpine (Float32Array of 32 vec4: xyz world position, w roll), uLen, uTime, uSweep, uFlutter. NS = 32.
Vertex — insert after #include <common>:
  #define NS 32
  uniform vec4 uSpine[NS]; uniform float uLen, uTime, uSweep, uFlutter; varying float vFin;
  vec3 koiP, koiX, koiY, koiZ, koiLocal;
  vec4 koiAt(int i){ return uSpine[clamp(i,0,NS-1)]; }
  void koiSolve(){
    vec3 lp = position; float al = abs(lp.y);
    float fin = smoothstep(0.07,0.17,al); float tail = smoothstep(0.68,0.98,lp.x); float side = fin*(1.0-tail);
    float sweep = uSweep + 0.09*sin(uTime*1.7 + lp.x*4.0);
    lp.x += side*(al-0.07)*sweep*1.3;
    lp.z += side*sin(uTime*2.6 - al*16.0 + lp.x*5.0)*0.02*(al/0.2)*uFlutter;
    lp.z += tail*sin(uTime*2.2 - lp.x*14.0 + lp.y*9.0)*0.024*uFlutter;
    lp.y *= 1.0 + tail*0.1*sin(uTime*1.3 + lp.x*6.0)*uFlutter;
    vFin = max(fin, smoothstep(0.71,0.8,position.x)); koiLocal = lp;
    float f = clamp(lp.x,0.0,1.0)*float(NS-1); float fi = min(floor(f), float(NS-2)); float t = f-fi; int i = int(fi);
    vec4 p0=koiAt(i-1), p1=koiAt(i), p2=koiAt(i+1), p3=koiAt(i+2);
    vec3 a = -p0.xyz+p2.xyz; vec3 b = 2.0*p0.xyz-5.0*p1.xyz+4.0*p2.xyz-p3.xyz; vec3 c = -p0.xyz+3.0*p1.xyz-3.0*p2.xyz+p3.xyz;
    koiP = 0.5*(2.0*p1.xyz + a*t + b*t*t + c*t*t*t);
    vec3 tangent = normalize(a + 2.0*b*t + 3.0*c*t*t);
    float roll = mix(p1.w,p2.w,t); float cr=cos(roll), sr=sin(roll);
    vec3 flatY = normalize(cross(vec3(0.0,0.0,1.0), tangent)); vec3 flatZ = cross(tangent, flatY);
    koiX = tangent; koiY = flatY*cr + flatZ*sr; koiZ = flatZ*cr - flatY*sr;
  }
Replace #include <beginnormal_vertex> with: koiSolve(); vec3 objectNormal = normalize(koiX*normal.x + koiY*normal.y + koiZ*normal.z);
Replace #include <begin_vertex> with: vec3 transformed = koiP + (koiY*koiLocal.y + koiZ*koiLocal.z)*uLen;
Fragment — declare varying float vFin; after #include <map_fragment> add:
  diffuseColor.rgb = max(mix(vec3(dot(diffuseColor.rgb, vec3(0.2126,0.7152,0.0722))), diffuseColor.rgb, 1.22), 0.0);
  diffuseColor.a *= mix(1.0, 0.84, vFin);
after #include <emissivemap_fragment> add: totalEmissiveRadiance += diffuseColor.rgb * vFin * 0.22;

=== SWIM SIMULATION (all in "fish lengths"; world = value × view.len) ===
Constants: CRUISE = 0.3 lengths/s; HIST_STEP = 0.012.
State: fish {x,y,z,heading,omega,speed,roll,phase,sweep,dartUntil,wake}; trail = array of {x,y,z,r,d} (d = cumulative distance; last entry is the live head); pointer {x,y,seen,down}.
Opening pose (seedPose): Catmull-Rom (centripetal, open) through these points, each offset by cx = (view.w/view.len)*0.055 on x:
  [0.52,-0.02] [0.4,0.107] [0.267,0.22] [0.1,0.293] [-0.1,0.287] [-0.227,0.153] [-0.18,0] [-0.033,-0.107] [0.12,-0.2]
  Sample it at HIST_STEP spacing into trail; the last point is the head; heading = direction of the last few samples; omega = 0.5; speed = 0.
steer(t,dt):
  darting = t < dartUntil; chasing = pointer.down || t - pointer.seen < 2.6
  omega = 0.42*sin(t*0.31+1.3) + 0.3*sin(t*0.53+4.1) + 0.36*sin(t*0.17)
  speed = CRUISE*(1 + 0.28*sin(t*0.37+2.0))
  if chasing: dist to pointer; if dist > 0.16 { omega = omega*0.25 + angDiff(atan2(dy,dx), heading)*2.4; speed = CRUISE*0.8 + min(dist,1.3)*(darting ? 1.1 : 0.62) } else speed = CRUISE*0.55
  soft walls: look-ahead point = head + direction*0.28; out = hypot(ax/view.bx, ay/view.by) - 1; if out > 0 { w = min(1,out*2.5); omega += (angDiff(atan2(-ay,-ax), heading)*1.9 - omega)*w }
  maxTurn = 1.5 + speed*2.2; clamp omega; fish.omega += (omega - fish.omega)*min(1,(darting?5:3.2)*dt)
  fish.speed += (speed - fish.speed)*min(1,(speed > fish.speed ? 2.6 : 0.9)*dt)
advance(t,dt):
  sn = speed/CRUISE; phase += dt*2π*(0.5 + 0.48*sn); v = speed*(1 + 0.14*sin(phase*2))
  heading += omega*dt; x += cos(heading)*v*dt; y += sin(heading)*v*dt
  depth target = 0.1*sin(t*0.23) + 0.07*sin(t*0.41+2.0); z += (target - z)*min(1,1.5*dt)
  roll += (clamp(omega*0.26,-0.6,0.6) - roll)*min(1,4*dt)
  sweep += (clamp((sn-1)*0.22,-0.05,0.5) - sweep)*min(1,3*dt)
  update the live head in trail; push a new point every HIST_STEP of travel; drop points older than 1.25 lengths behind the head.
writeSpine(): for i in 0..31, s = i/31: sample the trail at distance (head.d - s) (linear interpolation of x,y,z,roll). Then add a travelling wave along the in-plane normal:
  amp = (0.45 + 0.55*min(sn,2.2))*wake; wave = (0.008 + 0.085*s^1.7)*amp*sin(phase - s*5.4)
  write (x - ty*wave)*len, (y + tx*wave)*len, z*len, roll*(1 - s*0.6) into uSpine, where (tx,ty) is the unit tangent from neighbouring samples.
Frame loop: dt = min(elapsed, 1/20). If not prefers-reduced-motion: clock += dt; wake = smoothstep(clock - born, 0.7, 2.6) (holds the opening pose, then eases in); if wake > 0 advance(clock, dt*(0.25 + 0.75*wake)); uTime = clock; uSweep = fish.sweep; uFlutter = 0.35 + 0.65*wake. If reduced motion: no advance, uFlutter = 0. Then writeSpine, update shadow, render.
Only seed the pose and add class "is-ready" to the canvas once the model is loaded AND #hero has non-zero size.
Dynamic shadow each frame (CSS variables on the canvas): lift = 1 + (fish.z + 0.17)*3.2; unit = min(heroWidth, heroHeight*1.6)/100;
  --sx = -0.55*unit*lift px; --sy = 1.25*unit*lift px; --sb = 0.95*unit*(0.6 + lift*0.5) px.

=== POINTER ===
Convert pointer to pond coords: x = ((clientX-left)/width - 0.5)*view.w/view.len; y = (0.5 - (clientY-top)/height)*view.h/view.len.
pointermove on #hero: set pointer x,y and pointer.seen = clock (the fish follows for 2.6 s after the last movement).
pointerdown on #hero (ignore if the target is inside a link or button): set pointer x,y; pointer.down = (pointerType !== 'mouse'); pointer.seen = clock; fish.dartUntil = clock + 1.2 — the fish swims quickly TO the clicked point. It must never flee or leave the screen.
pointerup / pointercancel on window: pointer.down = false.

=== ACCEPTANCE ===
- On load the fish fades in already curled in an S-curve over the middle of the headline, holds about 0.7 s, then swims off and wanders the whole hero, turning back at the edges.
- The body bends along the path the head took; tail beats faster when it speeds up; fins ripple and fold back at speed; it banks into turns; the shadow shifts with depth.
- Fins are slightly translucent so the headline shows through them.
- Works at 375×812 portrait: headline 27.5vw at top 40%, fish sized to the narrow width, nav pill top-right, tagline and logo bottom-left.
- No console errors; no local files referenced.
[/hidecontent]
← 返回首页