Files
nanocode/particle-lab/22-ink-wash/index.html
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

267 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>