267 lines
10 KiB
HTML
267 lines
10 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>水墨山水</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
overflow: hidden;
|
||
background: #c9b896;
|
||
font-family: "Microsoft YaHei", serif;
|
||
}
|
||
canvas { display: block; }
|
||
#ui {
|
||
position: fixed;
|
||
top: 0; left: 0; width: 100%; height: 100%;
|
||
pointer-events: none; z-index: 10;
|
||
}
|
||
#title {
|
||
position: absolute;
|
||
top: 25px; left: 30px;
|
||
color: #0a0a0a;
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
letter-spacing: 10px;
|
||
writing-mode: vertical-rl;
|
||
}
|
||
#fps {
|
||
position: absolute;
|
||
top: 20px; right: 25px;
|
||
color: #333;
|
||
font-size: 12px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="canvas"></canvas>
|
||
<div id="ui">
|
||
<div id="title">水墨山水</div>
|
||
<div id="fps">FPS: 60</div>
|
||
</div>
|
||
|
||
<script>
|
||
const canvas = document.getElementById('canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
function resize() {
|
||
canvas.width = window.innerWidth;
|
||
canvas.height = window.innerHeight;
|
||
}
|
||
resize();
|
||
window.addEventListener('resize', resize);
|
||
|
||
// 静态水墨场景 - 一次性绘制
|
||
function drawScene() {
|
||
const W = canvas.width;
|
||
const H = canvas.height;
|
||
|
||
// 宣纸底色
|
||
ctx.fillStyle = '#c9b896';
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
// ========== 远山(淡墨)==========
|
||
for (let i = 0; i < 200; i++) {
|
||
const x = Math.random() * W;
|
||
const y = H * 0.45 + Math.random() * 80;
|
||
const r = 15 + Math.random() * 25;
|
||
|
||
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||
g.addColorStop(0, 'rgba(100, 95, 85, 0.4)');
|
||
g.addColorStop(0.5, 'rgba(120, 115, 100, 0.2)');
|
||
g.addColorStop(1, 'rgba(140, 135, 120, 0)');
|
||
|
||
ctx.fillStyle = g;
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// ========== 主峰(浓墨)==========
|
||
// 左峰
|
||
for (let i = 0; i < 400; i++) {
|
||
const t = Math.random();
|
||
const x = W * 0.15 + (Math.random() - 0.5) * 180 * t;
|
||
const y = H * 0.85 - t * 300 + Math.abs(x - W * 0.15) * 0.3;
|
||
const r = 12 + Math.random() * 20;
|
||
const dark = 0.7 + Math.random() * 0.3;
|
||
|
||
const gray = Math.floor(40 + (1 - dark) * 40);
|
||
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||
g.addColorStop(0, `rgba(${gray-10}, ${gray-10}, ${gray-10}, ${dark})`);
|
||
g.addColorStop(0.4, `rgba(${gray}, ${gray}, ${gray}, ${dark * 0.5})`);
|
||
g.addColorStop(1, `rgba(${gray+20}, ${gray+20}, ${gray+20}, 0)`);
|
||
|
||
ctx.fillStyle = g;
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// 中峰(最大最浓)
|
||
for (let i = 0; i < 500; i++) {
|
||
const t = Math.random();
|
||
const x = W * 0.45 + (Math.random() - 0.5) * 250 * t;
|
||
const y = H * 0.9 - t * 380 + Math.abs(x - W * 0.45) * 0.35;
|
||
const r = 15 + Math.random() * 25;
|
||
const dark = 0.8 + Math.random() * 0.2;
|
||
|
||
const gray = Math.floor(25 + (1 - dark) * 30);
|
||
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||
g.addColorStop(0, `rgba(${gray}, ${gray}, ${gray}, ${dark})`);
|
||
g.addColorStop(0.3, `rgba(${gray+15}, ${gray+15}, ${gray+15}, ${dark * 0.6})`);
|
||
g.addColorStop(0.7, `rgba(${gray+35}, ${gray+35}, ${gray+35}, ${dark * 0.2})`);
|
||
g.addColorStop(1, `rgba(${gray+50}, ${gray+50}, ${gray+50}, 0)`);
|
||
|
||
ctx.fillStyle = g;
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// 右峰
|
||
for (let i = 0; i < 350; i++) {
|
||
const t = Math.random();
|
||
const x = W * 0.78 + (Math.random() - 0.5) * 160 * t;
|
||
const y = H * 0.88 - t * 280 + Math.abs(x - W * 0.78) * 0.4;
|
||
const r = 12 + Math.random() * 18;
|
||
const dark = 0.65 + Math.random() * 0.35;
|
||
|
||
const gray = Math.floor(35 + (1 - dark) * 40);
|
||
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||
g.addColorStop(0, `rgba(${gray}, ${gray}, ${gray}, ${dark})`);
|
||
g.addColorStop(0.5, `rgba(${gray+20}, ${gray+20}, ${gray+20}, ${dark * 0.4})`);
|
||
g.addColorStop(1, `rgba(${gray+40}, ${gray+40}, ${gray+40}, 0)`);
|
||
|
||
ctx.fillStyle = g;
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// ========== 云雾 ==========
|
||
for (let i = 0; i < 150; i++) {
|
||
const x = Math.random() * W;
|
||
const y = H * 0.75 + Math.random() * 60;
|
||
const r = 30 + Math.random() * 50;
|
||
|
||
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
|
||
g.addColorStop(0, 'rgba(160, 150, 130, 0.25)');
|
||
g.addColorStop(0.5, 'rgba(180, 170, 150, 0.12)');
|
||
g.addColorStop(1, 'rgba(200, 190, 170, 0)');
|
||
|
||
ctx.fillStyle = g;
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, r, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// ========== 泼墨点 ==========
|
||
// 中心墨滴
|
||
for (let ring = 0; ring < 5; ring++) {
|
||
const count = 30 + ring * 20;
|
||
const radius = 20 + ring * 25;
|
||
const alpha = 0.7 - ring * 0.12;
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const angle = (i / count) * Math.PI * 2 + Math.random() * 0.5;
|
||
const r = radius + Math.random() * 30;
|
||
const x = W / 2 + Math.cos(angle) * r;
|
||
const y = H / 2 + Math.sin(angle) * r;
|
||
const size = 8 + Math.random() * 15;
|
||
|
||
const gray = 30 + ring * 15;
|
||
const g = ctx.createRadialGradient(x, y, 0, x, y, size);
|
||
g.addColorStop(0, `rgba(${gray}, ${gray}, ${gray}, ${alpha})`);
|
||
g.addColorStop(1, `rgba(${gray+30}, ${gray+30}, ${gray+30}, 0)`);
|
||
|
||
ctx.fillStyle = g;
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, size, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 动画循环(仅更新FPS)
|
||
let frames = 0;
|
||
let lastTime = performance.now();
|
||
const fpsEl = document.getElementById('fps');
|
||
|
||
let sceneDrawn = false;
|
||
function animate() {
|
||
if (!sceneDrawn) {
|
||
drawScene();
|
||
sceneDrawn = true;
|
||
}
|
||
|
||
frames++;
|
||
const now = performance.now();
|
||
if (now - lastTime >= 1000) {
|
||
fpsEl.textContent = 'FPS: ' + frames;
|
||
frames = 0;
|
||
lastTime = now;
|
||
}
|
||
|
||
requestAnimationFrame(animate);
|
||
}
|
||
|
||
animate();
|
||
|
||
// 绘制墨滴效果函数
|
||
function drawInkDrop(cx, cy, power) {
|
||
// 中心浓墨(纯黑)
|
||
for (let i = 0; i < 30 * power; i++) {
|
||
const angle = Math.random() * Math.PI * 2;
|
||
const r = Math.random() * 25 * power;
|
||
const x = cx + Math.cos(angle) * r;
|
||
const y = cy + Math.sin(angle) * r;
|
||
const size = 10 + Math.random() * 18;
|
||
|
||
const g = ctx.createRadialGradient(x, y, 0, x, y, size);
|
||
g.addColorStop(0, 'rgba(15, 15, 15, 0.95)');
|
||
g.addColorStop(0.4, 'rgba(40, 40, 40, 0.6)');
|
||
g.addColorStop(1, 'rgba(80, 80, 80, 0)');
|
||
|
||
ctx.fillStyle = g;
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, size, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// 外围飞白
|
||
for (let i = 0; i < 20 * power; i++) {
|
||
const angle = Math.random() * Math.PI * 2;
|
||
const r = 25 + Math.random() * 40 * power;
|
||
const x = cx + Math.cos(angle) * r;
|
||
const y = cy + Math.sin(angle) * r;
|
||
const size = 8 + Math.random() * 15;
|
||
|
||
const g = ctx.createRadialGradient(x, y, 0, x, y, size);
|
||
g.addColorStop(0, 'rgba(50, 50, 50, 0.5)');
|
||
g.addColorStop(1, 'rgba(100, 100, 100, 0)');
|
||
|
||
ctx.fillStyle = g;
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, size, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
}
|
||
|
||
// 点击添加墨滴
|
||
canvas.addEventListener('mousedown', (e) => {
|
||
e.preventDefault();
|
||
drawInkDrop(e.clientX, e.clientY, 1.5);
|
||
});
|
||
|
||
// 触摸支持
|
||
canvas.addEventListener('touchstart', (e) => {
|
||
e.preventDefault();
|
||
const t = e.touches[0];
|
||
drawInkDrop(t.clientX, t.clientY, 1.5);
|
||
}, { passive: false });
|
||
</script>
|
||
</body>
|
||
</html>
|