599 lines
23 KiB
HTML
599 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Niagara Falls 瀑布效果 - Three.js</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
overflow: hidden;
|
|
background: #0a0a15;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
#canvas-container {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
#stats {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 20px;
|
|
color: #fff;
|
|
background: rgba(0, 20, 40, 0.8);
|
|
padding: 15px 20px;
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
line-height: 1.8;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(100, 180, 255, 0.3);
|
|
box-shadow: 0 4px 20px rgba(0, 100, 200, 0.3);
|
|
z-index: 100;
|
|
}
|
|
#stats .label {
|
|
color: #88ccff;
|
|
font-weight: bold;
|
|
}
|
|
#stats .value {
|
|
color: #fff;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
#title {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
color: #fff;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
text-shadow: 0 0 20px rgba(100, 180, 255, 0.8);
|
|
z-index: 100;
|
|
}
|
|
.fps-good { color: #4eff4e !important; }
|
|
.fps-ok { color: #ffff4e !important; }
|
|
.fps-bad { color: #ff4e4e !important; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="canvas-container"></div>
|
|
<div id="stats">
|
|
<div><span class="label">FPS:</span> <span class="value" id="fps">--</span></div>
|
|
<div><span class="label">总粒子数:</span> <span class="value" id="particle-count">--</span></div>
|
|
<div><span class="label">瀑布粒子:</span> <span class="value" id="fall-count">--</span></div>
|
|
<div><span class="label">水花粒子:</span> <span class="value" id="splash-count">--</span></div>
|
|
<div><span class="label">帧时间:</span> <span class="value" id="frame-time">--</span> ms</div>
|
|
</div>
|
|
<div id="title">🌊 Niagara Falls 瀑布模拟</div>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
|
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
// 配置参数
|
|
const CONFIG = {
|
|
FALL_PARTICLES: 6000, // 瀑布粒子数
|
|
SPLASH_PARTICLES: 4000, // 水花粒子数
|
|
FALL_HEIGHT: 100, // 瀑布高度 (米)
|
|
FALL_WIDTH: 60, // 瀑布宽度 (米)
|
|
FALL_DEPTH: 5, // 瀑布厚度
|
|
GRAVITY: 9.8, // 重力加速度
|
|
GROUND_Y: -10, // 地面高度
|
|
SPAWN_RATE: 30, // 每帧生成粒子数
|
|
};
|
|
|
|
// 场景设置
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x0a0a15);
|
|
scene.fog = new THREE.FogExp2(0x0a0a15, 0.008);
|
|
|
|
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
camera.position.set(0, 30, 120);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
document.getElementById('canvas-container').appendChild(renderer.domElement);
|
|
|
|
const controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.maxPolarAngle = Math.PI / 2 - 0.05;
|
|
controls.target.set(0, 20, 0);
|
|
|
|
// 创建粒子纹理
|
|
function createWaterTexture() {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 64;
|
|
canvas.height = 64;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// 径向渐变创建圆形水滴
|
|
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
|
|
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
|
|
gradient.addColorStop(0.3, 'rgba(200, 230, 255, 0.8)');
|
|
gradient.addColorStop(0.6, 'rgba(100, 180, 255, 0.4)');
|
|
gradient.addColorStop(1, 'rgba(0, 100, 200, 0)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, 64, 64);
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
return texture;
|
|
}
|
|
|
|
const waterTexture = createWaterTexture();
|
|
|
|
// 瀑布粒子类
|
|
class WaterfallParticles {
|
|
constructor(count) {
|
|
this.count = count;
|
|
this.geometry = new THREE.BufferGeometry();
|
|
|
|
// 位置数组
|
|
this.positions = new Float32Array(count * 3);
|
|
this.velocities = new Float32Array(count * 3);
|
|
this.life = new Float32Array(count);
|
|
this.colors = new Float32Array(count * 3);
|
|
this.sizes = new Float32Array(count);
|
|
|
|
// 初始化
|
|
for (let i = 0; i < count; i++) {
|
|
this.resetParticle(i, true);
|
|
}
|
|
|
|
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
|
|
this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
|
|
this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
|
|
|
|
// 创建材质
|
|
const material = new THREE.PointsMaterial({
|
|
size: 1,
|
|
map: waterTexture,
|
|
transparent: true,
|
|
opacity: 0.8,
|
|
vertexColors: true,
|
|
blending: THREE.AdditiveBlending,
|
|
depthWrite: false,
|
|
sizeAttenuation: true
|
|
});
|
|
|
|
this.mesh = new THREE.Points(this.geometry, material);
|
|
scene.add(this.mesh);
|
|
}
|
|
|
|
resetParticle(i, initial = false) {
|
|
const x = (Math.random() - 0.5) * CONFIG.FALL_WIDTH;
|
|
const z = (Math.random() - 0.5) * CONFIG.FALL_DEPTH;
|
|
const y = initial ? Math.random() * CONFIG.FALL_HEIGHT + CONFIG.GROUND_Y : CONFIG.FALL_HEIGHT;
|
|
|
|
this.positions[i * 3] = x;
|
|
this.positions[i * 3 + 1] = y;
|
|
this.positions[i * 3 + 2] = z;
|
|
|
|
// 初始速度 - 向下加速
|
|
this.velocities[i * 3] = (Math.random() - 0.5) * 2;
|
|
this.velocities[i * 3 + 1] = -Math.random() * 5 - 5;
|
|
this.velocities[i * 3 + 2] = (Math.random() - 0.5) * 2;
|
|
|
|
this.life[i] = 1.0;
|
|
|
|
// 大小变化
|
|
this.sizes[i] = Math.random() * 2 + 1;
|
|
|
|
this.updateColor(i, y);
|
|
}
|
|
|
|
updateColor(i, y) {
|
|
const height = y - CONFIG.GROUND_Y;
|
|
const ratio = Math.max(0, Math.min(1, height / CONFIG.FALL_HEIGHT));
|
|
|
|
// 颜色渐变:白(顶部) → 浅蓝(中部) → 深蓝(底部)
|
|
let r, g, b;
|
|
|
|
if (ratio > 0.6) {
|
|
// 顶部:白色带蓝
|
|
const t = (ratio - 0.6) / 0.4;
|
|
r = 0.9 + t * 0.1;
|
|
g = 0.95;
|
|
b = 1.0;
|
|
} else if (ratio > 0.3) {
|
|
// 中部:浅蓝色
|
|
const t = (ratio - 0.3) / 0.3;
|
|
r = 0.4 + t * 0.5;
|
|
g = 0.7 + t * 0.25;
|
|
b = 0.9 + t * 0.1;
|
|
} else {
|
|
// 底部:深蓝色
|
|
const t = ratio / 0.3;
|
|
r = 0.1 + t * 0.3;
|
|
g = 0.3 + t * 0.4;
|
|
b = 0.6 + t * 0.3;
|
|
}
|
|
|
|
this.colors[i * 3] = r;
|
|
this.colors[i * 3 + 1] = g;
|
|
this.colors[i * 3 + 2] = b;
|
|
}
|
|
|
|
update(deltaTime) {
|
|
const positions = this.positions;
|
|
const velocities = this.velocities;
|
|
const colors = this.colors;
|
|
|
|
for (let i = 0; i < this.count; i++) {
|
|
// 更新速度(重力)
|
|
velocities[i * 3 + 1] -= CONFIG.GRAVITY * deltaTime;
|
|
|
|
// 更新位置
|
|
positions[i * 3] += velocities[i * 3] * deltaTime;
|
|
positions[i * 3 + 1] += velocities[i * 3 + 1] * deltaTime;
|
|
positions[i * 3 + 2] += velocities[i * 3 + 2] * deltaTime;
|
|
|
|
// 更新颜色
|
|
this.updateColor(i, positions[i * 3 + 1]);
|
|
|
|
// 检查是否撞击地面
|
|
if (positions[i * 3 + 1] <= CONFIG.GROUND_Y) {
|
|
// 产生水花
|
|
splashParticles.createSplash(
|
|
positions[i * 3],
|
|
CONFIG.GROUND_Y,
|
|
positions[i * 3 + 2],
|
|
Math.abs(velocities[i * 3 + 1]) * 0.3
|
|
);
|
|
|
|
// 重置粒子
|
|
this.resetParticle(i);
|
|
}
|
|
|
|
// 随机扰动
|
|
velocities[i * 3] += (Math.random() - 0.5) * 2 * deltaTime;
|
|
velocities[i * 3 + 2] += (Math.random() - 0.5) * 2 * deltaTime;
|
|
}
|
|
|
|
this.geometry.attributes.position.needsUpdate = true;
|
|
this.geometry.attributes.color.needsUpdate = true;
|
|
}
|
|
}
|
|
|
|
// 水花粒子类
|
|
class SplashParticles {
|
|
constructor(count) {
|
|
this.count = count;
|
|
this.geometry = new THREE.BufferGeometry();
|
|
|
|
this.positions = new Float32Array(count * 3);
|
|
this.velocities = new Float32Array(count * 3);
|
|
this.life = new Float32Array(count);
|
|
this.maxLife = new Float32Array(count);
|
|
this.colors = new Float32Array(count * 3);
|
|
this.sizes = new Float32Array(count);
|
|
this.active = new Uint8Array(count);
|
|
|
|
// 初始化所有为非激活
|
|
for (let i = 0; i < count; i++) {
|
|
this.active[i] = 0;
|
|
this.positions[i * 3] = 0;
|
|
this.positions[i * 3 + 1] = -1000;
|
|
this.positions[i * 3 + 2] = 0;
|
|
}
|
|
|
|
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
|
|
this.geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
|
|
this.geometry.setAttribute('size', new THREE.BufferAttribute(this.sizes, 1));
|
|
|
|
const material = new THREE.PointsMaterial({
|
|
size: 1.5,
|
|
map: waterTexture,
|
|
transparent: true,
|
|
opacity: 0.9,
|
|
vertexColors: true,
|
|
blending: THREE.AdditiveBlending,
|
|
depthWrite: false,
|
|
sizeAttenuation: true
|
|
});
|
|
|
|
this.mesh = new THREE.Points(this.geometry, material);
|
|
scene.add(this.mesh);
|
|
|
|
this.nextIndex = 0;
|
|
}
|
|
|
|
createSplash(x, y, z, impactSpeed) {
|
|
const splashCount = Math.min(8, Math.floor(impactSpeed / 2) + 2);
|
|
|
|
for (let s = 0; s < splashCount; s++) {
|
|
let idx = this.nextIndex;
|
|
let attempts = 0;
|
|
|
|
// 寻找非激活粒子
|
|
while (this.active[idx] && attempts < this.count) {
|
|
idx = (idx + 1) % this.count;
|
|
attempts++;
|
|
}
|
|
|
|
if (attempts >= this.count) return; // 没有可用粒子
|
|
|
|
this.nextIndex = (idx + 1) % this.count;
|
|
|
|
// 设置初始位置
|
|
this.positions[idx * 3] = x + (Math.random() - 0.5) * 2;
|
|
this.positions[idx * 3 + 1] = y;
|
|
this.positions[idx * 3 + 2] = z + (Math.random() - 0.5) * 2;
|
|
|
|
// 向四周溅射的速度
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const spread = Math.random() * 0.5 + 0.2;
|
|
const upwardSpeed = impactSpeed * (Math.random() * 0.4 + 0.3);
|
|
const horizontalSpeed = impactSpeed * spread;
|
|
|
|
this.velocities[idx * 3] = Math.cos(angle) * horizontalSpeed;
|
|
this.velocities[idx * 3 + 1] = upwardSpeed;
|
|
this.velocities[idx * 3 + 2] = Math.sin(angle) * horizontalSpeed;
|
|
|
|
// 生命周期
|
|
this.maxLife[idx] = Math.random() * 1.5 + 0.5;
|
|
this.life[idx] = this.maxLife[idx];
|
|
this.active[idx] = 1;
|
|
this.sizes[idx] = Math.random() * 1.5 + 0.8;
|
|
|
|
// 初始颜色 - 白色水花
|
|
this.colors[idx * 3] = 0.95;
|
|
this.colors[idx * 3 + 1] = 0.98;
|
|
this.colors[idx * 3 + 2] = 1.0;
|
|
}
|
|
}
|
|
|
|
update(deltaTime) {
|
|
const positions = this.positions;
|
|
const velocities = this.velocities;
|
|
const colors = this.colors;
|
|
|
|
let activeCount = 0;
|
|
|
|
for (let i = 0; i < this.count; i++) {
|
|
if (!this.active[i]) {
|
|
positions[i * 3 + 1] = -1000;
|
|
continue;
|
|
}
|
|
|
|
activeCount++;
|
|
|
|
// 更新生命
|
|
this.life[i] -= deltaTime;
|
|
|
|
if (this.life[i] <= 0) {
|
|
this.active[i] = 0;
|
|
positions[i * 3 + 1] = -1000;
|
|
continue;
|
|
}
|
|
|
|
// 重力
|
|
velocities[i * 3 + 1] -= CONFIG.GRAVITY * 2 * deltaTime;
|
|
|
|
// 空气阻力
|
|
velocities[i * 3] *= 0.98;
|
|
velocities[i * 3 + 2] *= 0.98;
|
|
|
|
// 更新位置
|
|
positions[i * 3] += velocities[i * 3] * deltaTime;
|
|
positions[i * 3 + 1] += velocities[i * 3 + 1] * deltaTime;
|
|
positions[i * 3 + 2] += velocities[i * 3 + 2] * deltaTime;
|
|
|
|
// 地面碰撞
|
|
if (positions[i * 3 + 1] < CONFIG.GROUND_Y) {
|
|
positions[i * 3 + 1] = CONFIG.GROUND_Y;
|
|
velocities[i * 3 + 1] *= -0.3;
|
|
velocities[i * 3] *= 0.7;
|
|
velocities[i * 3 + 2] *= 0.7;
|
|
}
|
|
|
|
// 更新颜色和大小
|
|
const lifeRatio = this.life[i] / this.maxLife[i];
|
|
colors[i * 3] = 0.9 * lifeRatio;
|
|
colors[i * 3 + 1] = 0.95 * lifeRatio;
|
|
colors[i * 3 + 2] = 1.0;
|
|
this.sizes[i] = (Math.random() * 0.5 + 0.5) * lifeRatio * 2;
|
|
}
|
|
|
|
this.geometry.attributes.position.needsUpdate = true;
|
|
this.geometry.attributes.color.needsUpdate = true;
|
|
this.geometry.attributes.size.needsUpdate = true;
|
|
|
|
return activeCount;
|
|
}
|
|
}
|
|
|
|
// 创建环境
|
|
function createEnvironment() {
|
|
// 地面
|
|
const groundGeometry = new THREE.PlaneGeometry(300, 300, 50, 50);
|
|
|
|
// 添加波浪
|
|
const positions = groundGeometry.attributes.position.array;
|
|
for (let i = 0; i < positions.length; i += 3) {
|
|
const x = positions[i];
|
|
const z = positions[i + 1];
|
|
positions[i + 2] = Math.sin(x * 0.1) * Math.cos(z * 0.1) * 2;
|
|
}
|
|
groundGeometry.computeVertexNormals();
|
|
|
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x0d1a33,
|
|
roughness: 0.3,
|
|
metalness: 0.6,
|
|
emissive: 0x051020,
|
|
emissiveIntensity: 0.3
|
|
});
|
|
|
|
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
|
|
ground.rotation.x = -Math.PI / 2;
|
|
ground.position.y = CONFIG.GROUND_Y - 0.5;
|
|
scene.add(ground);
|
|
|
|
// 水面反光层
|
|
const waterGeometry = new THREE.PlaneGeometry(200, 100);
|
|
const waterMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0x1a3a5c,
|
|
transparent: true,
|
|
opacity: 0.4
|
|
});
|
|
const water = new THREE.Mesh(waterGeometry, waterMaterial);
|
|
water.rotation.x = -Math.PI / 2;
|
|
water.position.y = CONFIG.GROUND_Y + 0.1;
|
|
scene.add(water);
|
|
|
|
// 瀑布源头的悬崖
|
|
const cliffGeometry = new THREE.BoxGeometry(CONFIG.FALL_WIDTH + 20, 10, 30);
|
|
const cliffMaterial = new THREE.MeshStandardMaterial({
|
|
color: 0x1a1a2e,
|
|
roughness: 0.9
|
|
});
|
|
const cliff = new THREE.Mesh(cliffGeometry, cliffMaterial);
|
|
cliff.position.set(0, CONFIG.FALL_HEIGHT + 5, -20);
|
|
scene.add(cliff);
|
|
|
|
// 添加一些雾效粒子
|
|
const fogParticles = new THREE.BufferGeometry();
|
|
const fogPositions = [];
|
|
const fogCount = 500;
|
|
|
|
for (let i = 0; i < fogCount; i++) {
|
|
fogPositions.push(
|
|
(Math.random() - 0.5) * 150,
|
|
Math.random() * 40 + CONFIG.GROUND_Y,
|
|
(Math.random() - 0.5) * 80 + 20
|
|
);
|
|
}
|
|
|
|
fogParticles.setAttribute('position', new THREE.Float32BufferAttribute(fogPositions, 3));
|
|
|
|
const fogMaterial = new THREE.PointsMaterial({
|
|
color: 0x88ccff,
|
|
size: 8,
|
|
transparent: true,
|
|
opacity: 0.15,
|
|
map: waterTexture,
|
|
blending: THREE.AdditiveBlending,
|
|
depthWrite: false
|
|
});
|
|
|
|
const fog = new THREE.Points(fogParticles, fogMaterial);
|
|
scene.add(fog);
|
|
|
|
// 雾效动画
|
|
return { fog, fogPositions };
|
|
}
|
|
|
|
// 灯光
|
|
function setupLighting() {
|
|
const ambientLight = new THREE.AmbientLight(0x224466, 0.5);
|
|
scene.add(ambientLight);
|
|
|
|
const moonLight = new THREE.DirectionalLight(0x88ccff, 0.8);
|
|
moonLight.position.set(50, 100, 50);
|
|
scene.add(moonLight);
|
|
|
|
// 瀑布底部发光
|
|
const bottomLight = new THREE.PointLight(0x4488ff, 2, 100);
|
|
bottomLight.position.set(0, CONFIG.GROUND_Y + 10, 0);
|
|
scene.add(bottomLight);
|
|
|
|
// 顶部光源
|
|
const topLight = new THREE.PointLight(0xaaddff, 1, 80);
|
|
topLight.position.set(0, CONFIG.FALL_HEIGHT - 10, 0);
|
|
scene.add(topLight);
|
|
}
|
|
|
|
// 初始化
|
|
setupLighting();
|
|
const { fog, fogPositions } = createEnvironment();
|
|
|
|
const waterfallParticles = new WaterfallParticles(CONFIG.FALL_PARTICLES);
|
|
const splashParticles = new SplashParticles(CONFIG.SPLASH_PARTICLES);
|
|
|
|
// 性能统计
|
|
let lastTime = performance.now();
|
|
let frameCount = 0;
|
|
let fps = 0;
|
|
let lastFpsTime = lastTime;
|
|
|
|
function updateStats(frameTime, splashActive) {
|
|
const now = performance.now();
|
|
frameCount++;
|
|
|
|
if (now - lastFpsTime >= 1000) {
|
|
fps = Math.round(frameCount * 1000 / (now - lastFpsTime));
|
|
frameCount = 0;
|
|
lastFpsTime = now;
|
|
}
|
|
|
|
const fpsEl = document.getElementById('fps');
|
|
fpsEl.textContent = fps;
|
|
fpsEl.className = fps >= 50 ? 'fps-good' : fps >= 30 ? 'fps-ok' : 'fps-bad';
|
|
|
|
document.getElementById('particle-count').textContent =
|
|
(CONFIG.FALL_PARTICLES + splashActive).toLocaleString();
|
|
document.getElementById('fall-count').textContent = CONFIG.FALL_PARTICLES.toLocaleString();
|
|
document.getElementById('splash-count').textContent = splashActive.toLocaleString();
|
|
document.getElementById('frame-time').textContent = frameTime.toFixed(2);
|
|
}
|
|
|
|
// 动画循环
|
|
let fogTime = 0;
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const currentTime = performance.now();
|
|
const deltaTime = Math.min((currentTime - lastTime) / 1000, 0.1);
|
|
lastTime = currentTime;
|
|
|
|
// 更新粒子
|
|
waterfallParticles.update(deltaTime);
|
|
const splashActive = splashParticles.update(deltaTime);
|
|
|
|
// 雾效动画
|
|
fogTime += deltaTime;
|
|
const positions = fog.geometry.attributes.position.array;
|
|
for (let i = 0; i < positions.length; i += 3) {
|
|
positions[i] += Math.sin(fogTime + i) * 0.1;
|
|
positions[i + 1] += Math.cos(fogTime * 0.5 + i) * 0.05;
|
|
}
|
|
fog.geometry.attributes.position.needsUpdate = true;
|
|
|
|
// 更新控制
|
|
controls.update();
|
|
|
|
// 渲染
|
|
renderer.render(scene, camera);
|
|
|
|
// 更新统计
|
|
updateStats(deltaTime * 1000, splashActive);
|
|
}
|
|
|
|
// 窗口调整
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
// 开始动画
|
|
animate();
|
|
</script>
|
|
</body>
|
|
</html>
|