类型:Site模板 · 收录:2026-10-17 12:02:18 · 源码:会员可见
Build a single standalone index.html (no build step, no local assets, everything inline) for a full-viewport game hero section called "Agehana — The Jade Feather". A looping background video of a jungle temple with falling petals fills the page; in front of it a 3D character bust loaded from a remote GLB turns her head to follow the mouse cursor. Follow every value below exactly.
=== HEAD ===
- <html lang="en">, charset UTF-8, viewport "width=device-width, initial-scale=1.0"
- <title>Agehana — The Jade Feather</title>
- Preconnect to https://fonts.googleapis.com and https://fonts.gstatic.com (crossorigin)
- Google Fonts stylesheet: https://fonts.googleapis.com/css2?family=Instrument+Serif&family=Syncopate:wght@400;700&family=Jost:wght@400;500&display=swap
=== REMOTE ASSETS (load directly from these URLs, do not download) ===
Background video (1080p, 16:9, 8s loop):
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261013_180631_365651e0-2a29-4de7-82ec-4c8d10432911.mp4
Video poster / fallback still:
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261013_172235_a951e4c4-f405-40a8-902d-eaf76cbcb132.png
3D character model (GLB):
https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261013_162603_e9883db5-cf87-47ae-8476-0a2b862c89f0.glb
=== CSS ===
:root variables:
--rose: #b9655e; --rose-light: #c98278; --rose-deep: #a4544f;
--cream: #fbe9dc; --jade: #1f6a49; --jade-hover: #27805a;
Reset: * { box-sizing: border-box; margin: 0; padding: 0 } html, body { height: 100% }
body: min-height 100vh; overflow hidden; color var(--cream); font-family 'Jost', sans-serif;
background (fallback behind the video, two layers):
radial-gradient(ellipse 60% 70% at 68% 38%, var(--rose-light) 0%, transparent 70%),
linear-gradient(180deg, var(--rose) 0%, var(--rose-deep) 100%);
background-color var(--rose).
body::after (film grain): content ''; position fixed; inset 0; z-index 3; pointer-events none; opacity .07; mix-blend-mode overlay;
background-image is an inline SVG data URI: a 200x200 svg containing <filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/></filter> and a <rect width='100%' height='100%' filter='url(%23n)'/>.
#bg (background video): position fixed; inset 0; z-index 0; width 100%; height 100%; object-fit cover; object-position 62% 50%; filter contrast(1.08) saturate(1.08).
.shade (darkening overlay above the video, below the canvas): position fixed; inset 0; z-index 0; pointer-events none; background (two layers):
linear-gradient(90deg, rgba(38,10,14,.72) 0%, rgba(38,10,14,.38) 34%, rgba(38,10,14,0) 62%),
linear-gradient(180deg, rgba(38,10,14,.35) 0%, rgba(38,10,14,0) 22%, rgba(38,10,14,0) 62%, rgba(38,10,14,.6) 100%).
#stage (the 3D canvas): position fixed; inset 0; z-index 1; width 100%; height 100%; display block; opacity 0; transition opacity 1.2s ease. #stage.ready { opacity: 1 }
header: position fixed; top 0; left 0; right 0; z-index 4; display flex; align-items center; justify-content center; padding 18px 18px 0.
.logo: position absolute; left 18px; top 12px; font-family 'Instrument Serif', serif; font-size 30px; letter-spacing -.01em; transform scaleX(.82); transform-origin left center; color var(--cream); text-decoration none.
nav: display flex; gap 26px.
nav a: font-family 'Syncopate', sans-serif; font-size 11.5px; letter-spacing .14em; color var(--cream); text-decoration none; opacity .95; transition opacity .2s. nav a:hover { opacity: .6 }
.hero: position fixed; left 40px; top 50%; z-index 4; transform translateY(-8%); max-width 520px.
.hero h1: font-family 'Instrument Serif', serif; font-weight 400; font-size clamp(56px, 7.4vw, 104px); line-height .95; letter-spacing -.03em; white-space nowrap; transform scaleX(.8); transform-origin left center; color var(--cream).
.hero p: margin-top 18px; font-family 'Syncopate', sans-serif; font-size clamp(9.5px, .86vw, 11.5px); line-height 1.6; letter-spacing .2em; text-transform uppercase; max-width 420px.
.cta: display inline-block; margin-top 26px; padding 13px 36px; border-radius 3px; background var(--jade); color var(--cream); font-family 'Syncopate', sans-serif; font-size 11.5px; letter-spacing .14em; text-decoration none; box-shadow 0 6px 18px rgba(40,10,10,.18); transition background .25s, transform .25s.
.cta:hover: background var(--jade-hover); transform translateY(-2px).
#loader: position fixed; right 26px; bottom 22px; z-index 4; font-family 'Syncopate', sans-serif; font-size 10px; letter-spacing .2em; opacity .8; transition opacity .6s. #loader.hidden { opacity: 0 }
Mobile, @media (max-width: 760px):
.hero { left: 22px; top: auto; bottom: 44px; transform: none }
.hero h1 { font-size: 58px }
nav { gap: 16px; margin-left: 110px }
=== HTML (in this order inside body) ===
<video id="bg" src="[background video URL]" poster="[poster URL]" autoplay muted loop playsinline preload="auto"></video>
<div class="shade"></div>
<canvas id="stage"></canvas>
<header> <a class="logo" href="#">AGEHANA</a> <nav> <a href="#">BUY</a> <a href="#">MEDIA</a> <a href="#">FAN ART</a> </nav> </header>
<section class="hero"> <h1>The Jade Feather</h1> <p>Chosen by the sun, she guards the temple at the edge of the world.</p> <a class="cta" href="#">BEGIN JOURNEY</a> </section>
<div id="loader">SUMMONING 0%</div>
=== SCRIPTS ===
Import map: "three" -> https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js and "three/addons/" -> https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/
One <script type="module"> importing THREE, GLTFLoader (three/addons/loaders/GLTFLoader.js) and RoomEnvironment (three/addons/environments/RoomEnvironment.js).
Background video playback (keeps it running if autoplay is held back):
bg = #bg. playBg = () => { if (bg.paused && document.visibilityState === 'visible') bg.play().catch(() => {}) }.
Call playBg on: bg 'canplay'; bg 'pause' (via setTimeout(playBg, 150)); window 'pointerdown'; window 'pointermove' (once); document 'visibilitychange'; once immediately; and again when the 3D model finishes loading.
Constants:
NECK_LOW = 0.525, NECK_HIGH = 0.578, PIVOT_Y = 0.55, PIVOT_Z = -0.01 (fractions of model height, 0 = bottom, 1 = top)
MAX_YAW = 42deg, MAX_PITCH = 22deg, BODY_YAW = 7deg, REST_YAW = -14deg (all converted to radians)
Renderer: WebGLRenderer on #stage with antialias true and alpha true (transparent, the video shows through); pixel ratio min(devicePixelRatio, 2); outputColorSpace SRGBColorSpace; toneMapping ACESFilmicToneMapping; toneMappingExposure 1.18.
Scene: environment = PMREMGenerator(renderer).fromScene(new RoomEnvironment(), 0.04).texture. No scene background.
Camera: PerspectiveCamera(fov 22, aspect 1, near 0.1, far 50).
Lights: key DirectionalLight 0xfff1e2 intensity 2.2 at (-1.5, 2.2, 3); rim DirectionalLight 0xffb6a4 intensity 1.4 at (2.5, 1.5, -2.5); AmbientLight 0xffe6dc intensity 0.35.
Add an empty Group called rig to the scene; the model goes inside it.
Shared head uniforms object used by every material: uYaw (float 0), uPitch (float 0), uPivot (Vector3), uNeck (Vector2).
Head-turn shader, applied to every material of the model via material.onBeforeCompile (then material.needsUpdate = true). Assign the shared uniforms into shader.uniforms and patch shader.vertexShader with three string replacements:
1) After '#include <common>' add: uniform float uYaw; uniform float uPitch; uniform vec3 uPivot; uniform vec2 uNeck; and a function
mat3 headRotation(float y) { float w = smoothstep(uNeck.x, uNeck.y, y); float cy = cos(uYaw*w), sy = sin(uYaw*w); float cp = cos(uPitch*w), sp = sin(uPitch*w); mat3 yaw = mat3(cy,0.0,-sy, 0.0,1.0,0.0, sy,0.0,cy); mat3 pitch = mat3(1.0,0.0,0.0, 0.0,cp,sp, 0.0,-sp,cp); return yaw * pitch; }
2) After '#include <beginnormal_vertex>' add: mat3 headMat = headRotation(position.y); objectNormal = headMat * objectNormal;
3) After '#include <begin_vertex>' add: transformed = headMat * (transformed - uPivot) + uPivot;
Effect: vertices above the neck rotate around the neck pivot, vertices below stay still, with a smooth blend across the neck band.
Loading (GLTFLoader.load of the model URL):
onLoad:
- gltf.scene.updateMatrixWorld(true). Create a new Group "model". Collect every mesh in gltf.scene; for each: geometry.applyMatrix4(mesh.matrixWorld), reset its position to 0, quaternion to identity, scale to 1, frustumCulled = false, apply the head-turn patch to each of its materials, and add it to "model" (this bakes all node transforms so every mesh shares one space).
- Compute Box3 of model: size, centre. s = 1 / size.y. model.scale.setScalar(s); model.position.set(-centre.x*s, -box.min.y*s, -centre.z*s). modelHeight = 1.
- uNeck = (box.min.y + NECK_LOW*size.y, box.min.y + NECK_HIGH*size.y). uPivot = (centre.x, box.min.y + PIVOT_Y*size.y, centre.z + PIVOT_Z*size.y).
- rig.add(model); call layout(); add class "ready" to the canvas; add class "hidden" to #loader; call playBg().
onProgress: if e.total, set loader text to "SUMMONING {round(loaded/total*100)}%".
onError: console.error and set loader text to "COULD NOT LOAD CHARACTER".
layout() (run once at start and on window resize):
w = innerWidth, h = innerHeight; renderer.setSize(w, h, false); camera.aspect = w/h.
narrow = w < 760. visible = narrow ? 0.8 : 0.75 (portion of model height on screen).
dist = (visible/2) / tan(radians(camera.fov/2)). top = modelHeight + 0.012. lookY = top - visible/2.
camera.position.set(0, lookY, dist); camera.lookAt(0, lookY, 0); camera.updateProjectionMatrix().
rig.position.x = narrow ? 0 : visible * camera.aspect * 0.17 (character sits right of centre on desktop, centred on mobile; the bottom of the bust runs off the bottom edge).
Pointer tracking:
pointer = Vector2 in NDC: x = clientX/innerWidth*2-1, y = -(clientY/innerHeight*2-1); set hasPointer = true.
Listen on window for 'pointermove' and for 'touchmove' (passive, first touch). On document 'pointerleave' set hasPointer = false.
Animation loop (requestAnimationFrame, THREE.Clock, dt clamped to 0.05), state yaw = 0, pitch = 0, bodyYaw = REST_YAW:
If hasPointer:
faceNdc = Vector3(rig.position.x, modelHeight*0.68, 0.05).project(camera)
dx = clamp((pointer.x - faceNdc.x) * 1.9, -1, 1); dy = clamp((pointer.y - faceNdc.y) * 1.6, -1, 1)
targetYaw = dx*MAX_YAW - REST_YAW*0.9; targetPitch = -dy*MAX_PITCH; targetBody = REST_YAW + dx*BODY_YAW
Else (idle look-around):
targetYaw = sin(t*0.5)*0.18 - REST_YAW*0.6; targetPitch = sin(t*0.37)*0.05; targetBody = REST_YAW
Clamp targetYaw to [-MAX_YAW, MAX_YAW].
Smoothing: k = 1 - exp(-dt*7); yaw += (targetYaw - yaw)*k; pitch += (targetPitch - pitch)*k; bodyYaw += (targetBody - bodyYaw) * (1 - exp(-dt*3)).
Apply: uYaw = yaw; uPitch = pitch; rig.rotation.y = bodyYaw; rig.position.y = sin(t*1.4)*0.0025 (breathing). Render.
=== LAYER ORDER (back to front) ===
body gradient fallback -> #bg video (z 0) -> .shade overlay (z 0) -> #stage 3D canvas (z 1) -> film grain (z 3) -> header, hero text, loader (z 4).
=== RESULT ===
A looping dusk jungle-temple video in deep rose and crimson with wind-blown petals and leaves fills the page, darkened on the left and bottom for legibility, with subtle film grain. "AGEHANA" logo top-left, BUY / MEDIA / FAN ART centred at top; large condensed serif title "The Jade Feather", uppercase tagline and a jade-green "BEGIN JOURNEY" button on the left; the 3D warrior bust on the right fades in after load and smoothly turns her head toward the cursor in all directions while the body only leans slightly. On screens under 760px the character is centred and the text block moves to the bottom-left.[/hidecontent]