本帖最后由 wzs0777 于 2025-12-13 20:35 编辑
[JavaScript] 纯文本查看 复制代码 <!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>
body {
margin: 0;
padding: 20px;
background: #e0e0e0;
font-family: sans-serif;
}
#canvas {
/* 道林纸米黄色背景 */
background: #f5f5dc;
/* 添加细微纹理(噪声图案)模拟纸张纤维 */
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='paper'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.04' result='noise'/%3E%3CfeDiffuseLighting in='noise' lighting-color='white' surfaceScale='1'%3E%3CfeDistantLight azimuth='45' elevation='60'/%3E%3C/feDiffuseLighting%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23paper)' opacity='0.15'/%3E%3C/svg%3E");
border: 2px solid #333;
cursor: crosshair;
display: block;
touch-action: none;
}
.controls {
margin: 10px 0;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
select, input, button {
padding: 8px 12px;
font-size: 14px;
}
.debug-info {
margin-top: 10px;
font-size: 12px;
color: #666;
font-family: monospace;
}
</style>
</head>
<body>
<h3>道林纸质感手写笔迹模拟</h3>
<div class="controls">
<label>笔刷:
<select id="penType">
<option value="fountain">钢笔</option>
<option value="brush">毛笔</option>
<option value="ballpoint">圆珠笔</option>
</select>
</label>
<label>颜色:
<input type="color" id="colorPicker" value="#000000">
</label>
<button>清空画布</button>
</div>
<canvas id="canvas" width="800" height="600"></canvas>
<div class="debug-info" id="debug">状态:准备就绪 | 提示:按住鼠标拖动绘制</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let isDrawing = false;
let points = [];
let lastPoint = null;
let strokeCount = 0;
// 获取笔刷配置
function getPenConfig(type) {
const configs = {
fountain: { minWidth: 2, maxWidth: 6, alpha: 0.95 },
brush: { minWidth: 8, maxWidth: 25, alpha: 0.8 },
ballpoint: { minWidth: 2, maxWidth: 4, alpha: 1 }
};
return configs[type] || configs.fountain;
}
// 基于速度的压力模拟
function getPressureByVelocity(current, last) {
if (!last) return 0.8;
const distance = Math.sqrt(
Math.pow(current.x - last.x, 2) + Math.pow(current.y - last.y, 2)
);
// 速度越快,线条越细
return Math.min(1, Math.max(0.3, 1 - distance / 30));
}
// 笔锋模拟
function simulatePenTip(points) {
const tipLength = Math.min(10, points.length);
for (let i = 0; i < points.length; i++) {
const remaining = points.length - i;
if (remaining < tipLength) {
points[i].pressure *= (remaining / tipLength) * 0.5 + 0.5;
}
}
}
// 核心绘制函数 - 道林纸质感版
function drawPath() {
if (points.length < 2) return;
const color = document.getElementById('colorPicker').value;
const penType = document.getElementById('penType').value;
const config = getPenConfig(penType);
// 应用笔锋效果
simulatePenTip(points);
ctx.save();
ctx.strokeStyle = color;
ctx.globalAlpha = config.alpha;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
// 添加墨水扩散效果
const alphaHex = Math.round(config.alpha * 85).toString(16).padStart(2, '0');
ctx.shadowColor = color + alphaHex;
ctx.shadowBlur = config.maxWidth * 0.3; // 根据笔刷宽度调整模糊
// 分段绘制,每段使用对应的压力宽度
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const current = points[i];
// 计算线宽
const width = config.minWidth + (current.pressure || 0.8) * (config.maxWidth - config.minWidth);
ctx.lineWidth = width;
// 绘制线段
ctx.beginPath();
ctx.moveTo(prev.x, prev.y);
ctx.lineTo(current.x, current.y);
ctx.stroke();
}
ctx.restore();
// 更新调试信息
const currentPoint = points[points.length - 1];
debug.textContent = `状态:绘制中 | 线段数:${points.length} | 压力:${(currentPoint.pressure || 0.8).toFixed(2)} | 颜色:${color}`;
}
// 鼠标事件处理
canvas.addEventListener('mousedown', (e) => {
isDrawing = true;
const rect = canvas.getBoundingClientRect();
const newPoint = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
pressure: 0.8
};
points = [newPoint];
lastPoint = newPoint;
strokeCount++;
debug.textContent = `状态:开始绘制 | 笔画:${strokeCount}`;
});
canvas.addEventListener('mousemove', (e) => {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
const currentPoint = {
x: currentX,
y: currentY,
pressure: getPressureByVelocity({x: currentX, y: currentY}, lastPoint)
};
points.push(currentPoint);
drawPath();
lastPoint = currentPoint;
});
canvas.addEventListener('mouseup', () => {
isDrawing = false;
points = [];
debug.textContent = `状态:绘制完成 | 总笔画:${strokeCount}`;
});
canvas.addEventListener('mouseleave', () => {
if (isDrawing) {
isDrawing = false;
points = [];
debug.textContent = `状态:绘制中断 | 总笔画:${strokeCount}`;
}
});
// 清空画布并重绘纸张纹理
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 重绘纸张纹理
const svgTexture = "data:image/svg+xml,%3Csvg width='100' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='paper'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.04' result='noise'/%3E%3CfeDiffuseLighting in='noise' lighting-color='white' surfaceScale='1'%3E%3CfeDistantLight azimuth='45' elevation='60'/%3E%3C/feDiffuseLighting%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23paper)' opacity='0.15'/%3E%3C/svg%3E";
const img = new Image();
img.onload = () => {
const pattern = ctx.createPattern(img, 'repeat');
ctx.fillStyle = pattern;
ctx.globalAlpha = 0.1;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1;
};
img.src = svgTexture;
points = [];
strokeCount = 0;
debug.textContent = '状态:画布已清空';
}
// 防止右键菜单
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// 初始化
debug.textContent = '状态:准备就绪 | 提示:按住鼠标左键拖动绘制';
</script>
</body>
</html> |