Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

561 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Winter Snowfall</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: linear-gradient(180deg, #0f172a 0%, #1e293b 50%, #334155 100%);
font-family: 'Space Mono', monospace;
}
#canvas {
display: block;
width: 100vw;
height: 100vh;
}
.ui-overlay {
position: fixed;
top: 24px;
left: 24px;
color: rgba(255, 255, 255, 0.85);
z-index: 100;
pointer-events: none;
}
.ui-stats {
display: flex;
flex-direction: column;
gap: 8px;
}
.stat-item {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
letter-spacing: 0.05em;
}
.stat-label {
color: rgba(148, 163, 184, 0.8);
text-transform: uppercase;
font-size: 11px;
}
.stat-value {
color: #e2e8f0;
font-weight: 700;
min-width: 60px;
}
.fps-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
margin-right: 8px;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(0.9); }
}
.title {
position: fixed;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.4);
font-size: 12px;
letter-spacing: 0.3em;
text-transform: uppercase;
z-index: 100;
pointer-events: none;
}
.ground-fade {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 120px;
background: linear-gradient(to top, rgba(241, 245, 249, 0.15) 0%, transparent 100%);
pointer-events: none;
z-index: 10;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="ground-fade"></div>
<div class="ui-overlay">
<div class="ui-stats">
<div class="stat-item">
<span class="fps-indicator"></span>
<span class="stat-label">FPS</span>
<span class="stat-value" id="fps">60</span>
</div>
<div class="stat-item">
<span class="stat-label">Snowflakes</span>
<span class="stat-value" id="count">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Wind</span>
<span class="stat-value" id="wind">0.0</span>
</div>
</div>
</div>
<div class="title">Winter Snowfall</div>
<script type="module">
import * as THREE from 'https://cdn.skypack.dev/three@0.136.0';
// Configuration
const CONFIG = {
snowflakeCount: 1200,
minSize: 2,
maxSize: 8,
minSpeed: 0.3,
maxSpeed: 1.8,
windStrength: 0.3,
rotationSpeed: 0.02
};
// State
let scene, camera, renderer;
let snowSystem, snowGeometry, snowMaterial;
let snowData = [];
let frameCount = 0;
let lastTime = performance.now();
let windOffset = 0;
// UI Elements
const fpsEl = document.getElementById('fps');
const countEl = document.getElementById('count');
const windEl = document.getElementById('wind');
// Generate snowflake textures
function createSnowflakeTexture(type) {
const canvas = document.createElement('canvas');
const size = 64;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
const centerX = size / 2;
const centerY = size / 2;
ctx.translate(centerX, centerY);
// Create gradient for glow effect
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, size / 2);
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
gradient.addColorStop(0.3, 'rgba(200, 230, 255, 0.9)');
gradient.addColorStop(0.6, 'rgba(150, 200, 255, 0.5)');
gradient.addColorStop(1, 'rgba(100, 150, 255, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(0, 0, size / 2 - 2, 0, Math.PI * 2);
ctx.fill();
// Draw snowflake shape based on type
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
ctx.lineWidth = 1.5;
ctx.lineCap = 'round';
if (type === 'hexagon') {
// Six-pointed star
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate((i * 60 * Math.PI) / 180);
// Main arm
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, size * 0.35);
ctx.stroke();
// Side branches
ctx.beginPath();
ctx.moveTo(0, size * 0.15);
ctx.lineTo(size * 0.1, size * 0.22);
ctx.moveTo(0, size * 0.15);
ctx.lineTo(-size * 0.1, size * 0.22);
ctx.stroke();
ctx.restore();
}
} else if (type === 'star') {
// Eight-pointed star with cross pattern
for (let i = 0; i < 8; i++) {
ctx.save();
ctx.rotate((i * 45 * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, size * 0.3);
ctx.stroke();
if (i % 2 === 0) {
ctx.beginPath();
ctx.moveTo(0, size * 0.12);
ctx.lineTo(size * 0.08, size * 0.18);
ctx.moveTo(0, size * 0.12);
ctx.lineTo(-size * 0.08, size * 0.18);
ctx.stroke();
}
ctx.restore();
}
} else {
// Crystal pattern
for (let i = 0; i < 6; i++) {
ctx.save();
ctx.rotate((i * 60 * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(0, size * 0.4);
ctx.stroke();
// Multiple branches
for (let j = 1; j <= 3; j++) {
const y = size * 0.1 * j;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(size * 0.06, y + size * 0.05);
ctx.moveTo(0, y);
ctx.lineTo(-size * 0.06, y + size * 0.05);
ctx.stroke();
}
ctx.restore();
}
}
// Center dot
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
ctx.beginPath();
ctx.arc(0, 0, 2, 0, Math.PI * 2);
ctx.fill();
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
// Initialize scene
function init() {
const canvas = document.getElementById('canvas');
// Scene setup
scene = new THREE.Scene();
// Fog for depth and atmosphere
scene.fog = new THREE.FogExp2(0x1e293b, 0.002);
// Camera
camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 100;
camera.position.y = 20;
camera.lookAt(0, -20, 0);
// Renderer
renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Create snowflake textures
const textures = [
createSnowflakeTexture('hexagon'),
createSnowflakeTexture('star'),
createSnowflakeTexture('crystal')
];
// Create snow particle system
createSnowSystem(textures);
// Update UI
countEl.textContent = CONFIG.snowflakeCount;
// Event listeners
window.addEventListener('resize', onWindowResize, false);
// Start animation
animate();
}
function createSnowSystem(textures) {
snowGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(CONFIG.snowflakeCount * 3);
const colors = new Float32Array(CONFIG.snowflakeCount * 3);
const sizes = new Float32Array(CONFIG.snowflakeCount);
const rotations = new Float32Array(CONFIG.snowflakeCount);
const rotationSpeeds = new Float32Array(CONFIG.snowflakeCount);
const speeds = new Float32Array(CONFIG.snowflakeCount);
const textureIndices = new Float32Array(CONFIG.snowflakeCount);
const opacities = new Float32Array(CONFIG.snowflakeCount);
const wobble = new Float32Array(CONFIG.snowflakeCount);
const color1 = new THREE.Color(0xffffff); // White
const color2 = new THREE.Color(0xb8e6ff); // Light blue
const color3 = new THREE.Color(0xe0f2fe); // Very light blue
for (let i = 0; i < CONFIG.snowflakeCount; i++) {
// Random position across the view
positions[i * 3] = (Math.random() - 0.5) * 200; // x
positions[i * 3 + 1] = Math.random() * 150 - 20; // y
positions[i * 3 + 2] = (Math.random() - 0.5) * 100; // z
// Random size
sizes[i] = CONFIG.minSize + Math.random() * (CONFIG.maxSize - CONFIG.minSize);
// Random rotation and speed
rotations[i] = Math.random() * Math.PI * 2;
rotationSpeeds[i] = (Math.random() - 0.5) * CONFIG.rotationSpeed * 2;
speeds[i] = CONFIG.minSpeed + Math.random() * (CONFIG.maxSpeed - CONFIG.minSpeed);
// Random texture
textureIndices[i] = Math.floor(Math.random() * textures.length);
// Random wobble for wind effect
wobble[i] = Math.random() * Math.PI * 2;
// Color gradient from white to light blue
const colorMix = Math.random();
let finalColor;
if (colorMix < 0.5) {
finalColor = color1.clone().lerp(color3, colorMix * 2);
} else {
finalColor = color3.clone().lerp(color2, (colorMix - 0.5) * 2);
}
colors[i * 3] = finalColor.r;
colors[i * 3 + 1] = finalColor.g;
colors[i * 3 + 2] = finalColor.b;
// Initial opacity
opacities[i] = 1.0;
// Store particle data for animation
snowData.push({
originalY: positions[i * 3 + 1],
originalSize: sizes[i],
speed: speeds[i],
wobblePhase: wobble[i],
wobbleSpeed: 0.5 + Math.random() * 1.5
});
}
snowGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
snowGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
snowGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
snowGeometry.setAttribute('rotation', new THREE.BufferAttribute(rotations, 1));
snowGeometry.setAttribute('opacity', new THREE.BufferAttribute(opacities, 1));
// Shader material for advanced rendering
const vertexShader = `
attribute float size;
attribute float rotation;
attribute float opacity;
varying float vRotation;
varying float vOpacity;
varying vec3 vColor;
void main() {
vRotation = rotation;
vOpacity = opacity;
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`;
const fragmentShader = `
uniform sampler2D texture1;
uniform sampler2D texture2;
uniform sampler2D texture3;
uniform float textureIndex;
varying float vRotation;
varying float vOpacity;
varying vec3 vColor;
void main() {
// Rotate texture coordinates
vec2 center = vec2(0.5, 0.5);
vec2 uv = gl_PointCoord - center;
float s = sin(vRotation);
float c = cos(vRotation);
vec2 rotatedUv = vec2(
uv.x * c - uv.y * s,
uv.x * s + uv.y * c
) + center;
// Sample from texture atlas or use default circle
vec4 texColor = texture2D(texture1, rotatedUv);
if (texColor.a < 0.1) discard;
gl_FragColor = vec4(vColor * texColor.rgb, texColor.a * vOpacity);
}
`;
// Use PointsMaterial for simplicity and better performance
snowMaterial = new THREE.PointsMaterial({
size: 1,
vertexColors: true,
map: textures[0],
transparent: true,
opacity: 0.9,
blending: THREE.AdditiveBlending,
depthWrite: false,
sizeAttenuation: true
});
snowSystem = new THREE.Points(snowGeometry, snowMaterial);
scene.add(snowSystem);
}
function updateSnow(deltaTime) {
const positions = snowGeometry.attributes.position.array;
const rotations = snowGeometry.attributes.rotation.array;
const sizes = snowGeometry.attributes.size.array;
const opacities = snowGeometry.attributes.opacity.array;
// Update wind
windOffset += deltaTime * 0.5;
const currentWind = Math.sin(windOffset * 0.3) * CONFIG.windStrength +
Math.sin(windOffset * 0.7) * CONFIG.windStrength * 0.5;
// Update UI
windEl.textContent = currentWind.toFixed(2);
const groundLevel = -50;
const fadeStart = -30;
for (let i = 0; i < CONFIG.snowflakeCount; i++) {
const data = snowData[i];
// Apply gravity
positions[i * 3 + 1] -= data.speed * deltaTime * 30;
// Apply wind with wobble
data.wobblePhase += data.wobbleSpeed * deltaTime;
const wobbleOffset = Math.sin(data.wobblePhase) * 0.3;
positions[i * 3] += (currentWind + wobbleOffset) * deltaTime * 20;
// Rotate
rotations[i] += data.rotationSpeed * deltaTime * 2;
// Ground accumulation effect - fade out when near ground
const y = positions[i * 3 + 1];
if (y < fadeStart) {
const fadeProgress = (y - groundLevel) / (fadeStart - groundLevel);
opacities[i] = Math.max(0, Math.min(1, fadeProgress));
// Scale down near ground
const scale = Math.max(0.3, fadeProgress);
sizes[i] = data.originalSize * scale;
} else {
opacities[i] = 1;
sizes[i] = data.originalSize;
}
// Reset if below ground
if (y < groundLevel) {
positions[i * 3 + 1] = 100 + Math.random() * 50;
positions[i * 3] = (Math.random() - 0.5) * 200;
positions[i * 3 + 2] = (Math.random() - 0.5) * 100;
opacities[i] = 1;
sizes[i] = data.originalSize;
}
// Wrap around horizontally
if (positions[i * 3] > 100) {
positions[i * 3] = -100;
} else if (positions[i * 3] < -100) {
positions[i * 3] = 100;
}
}
snowGeometry.attributes.position.needsUpdate = true;
snowGeometry.attributes.rotation.needsUpdate = true;
snowGeometry.attributes.size.needsUpdate = true;
snowGeometry.attributes.opacity.needsUpdate = true;
}
function updateFPS() {
frameCount++;
const currentTime = performance.now();
if (currentTime - lastTime >= 1000) {
fpsEl.textContent = frameCount;
frameCount = 0;
lastTime = currentTime;
// Update FPS indicator color
const indicator = document.querySelector('.fps-indicator');
if (frameCount >= 55) {
indicator.style.background = '#22c55e'; // Green
} else if (frameCount >= 30) {
indicator.style.background = '#eab308'; // Yellow
} else {
indicator.style.background = '#ef4444'; // Red
}
}
}
function animate() {
requestAnimationFrame(animate);
const deltaTime = 0.016; // Approximate 60fps delta
updateSnow(deltaTime);
updateFPS();
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Initialize
init();
</script>
</body>
</html>