<!DOCTYPE html>
<html>
<head>
<title>圆柱刻度图</title>
<style>
body {
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
font-family: Arial, sans-serif;
}
.chart-container {
position: relative;
width: 300px;
text-align: center;
}
canvas {
display: block;
margin: 0 auto;
}
.chart-title {
margin-top: 10px;
font-size: 18px;
font-weight: bold;
}
.chart-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
font-weight: bold;
color: #333;
}
</style>
</head>
<body>
<div class="chart-container">
<canvas id="cylinderGauge" width="200" height="300"></canvas>
<div class="chart-value" id="gaugeValue">75</div>
</div>
<script>
const canvas = document.getElementById('cylinderGauge');
const ctx = canvas.getContext('2d');
const valueDisplay = document.getElementById('gaugeValue');
// 图表配置
const config = {
width: canvas.width,
height: canvas.height,
centerX: canvas.width / 2,
centerY: canvas.height / 2,
cylinderRadius: 60,
cylinderHeight: 240,
radiusRate: 0.25, // 椭圆扁平度,越小越扁
segments: 12,
value: 75, // 当前值(百分比)
maxValue: 350,
animationDuration: 1000, // 动画持续时间(毫秒)
tickGap: 50, // 刻度值
colors: {
empty: '#DFE6F1',
fill: '#36C5F7',
tick: '#7f8c8d',
border: '#34495e',
highlight: '#5dade2'
}
};
// 绘制圆柱刻度图
function drawCylinderGauge() {
// 清除画布
ctx.clearRect(0, 0, config.width, config.height);
// 绘制背景圆柱
drawCylinderBg(0, config.cylinderHeight, config.colors.empty);
// 绘制填充部分
const fillHeight = (config.value / config.maxValue) * config.cylinderHeight;
drawCylinderVal(config.cylinderHeight - fillHeight, config.cylinderHeight, config.colors.fill);
// 绘制刻度线
drawTicks();
// 绘制顶部椭圆
drawTopEllipse();
// 绘制3D效果边框
// drawBorder();
}
// 绘制圆柱背景部分
function drawCylinderBg(startY, endY, color) {
const segmentAngle = (Math.PI * 2) / config.segments;
const topY = config.centerY - config.cylinderHeight / 2;
ctx.fillStyle = color;
// ctx.globalCompositeOperation = 'darken';
for (let i = 0; i < config.segments; i++) {
const angle1 = i * segmentAngle;
const angle2 = (i + 1) * segmentAngle;
ctx.beginPath();
// 顶部两点
const x1 = config.centerX + Math.cos(angle1) * config.cylinderRadius;
const y1 = topY + startY + Math.sin(angle1) * config.cylinderRadius * config.radiusRate;
const x2 = config.centerX + Math.cos(angle2) * config.cylinderRadius;
const y2 = topY + startY + Math.sin(angle2) * config.cylinderRadius * config.radiusRate;
// 底部两点
const x3 = x2;
const y3 = topY + endY + Math.sin(angle2) * config.cylinderRadius * config.radiusRate;
const x4 = x1;
const y4 = topY + endY + Math.sin(angle1) * config.cylinderRadius * config.radiusRate;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.lineTo(x4, y4);
ctx.closePath();
ctx.fill();
}
}
function drawCylinderVal(startY, endY, color) {
const topY = config.centerY - config.cylinderHeight / 2;
ctx.fillStyle = color;
//下椭圆
const bottomY = config.cylinderRadius / 2 + config.cylinderHeight;
ctx.beginPath();
ctx.ellipse(
config.centerX,
bottomY,
config.cylinderRadius,
config.cylinderRadius * config.radiusRate,
0,
0,
Math.PI * 2
);
ctx.fill();
//中间矩形
ctx.beginPath();
ctx.fillStyle = color;
// 顶部两点
const x1 = config.centerX - config.cylinderRadius;
const y1 = topY + startY;
const x2 = config.centerX + config.cylinderRadius;
const y2 = topY + startY;
// 底部两点
const x3 = x2;
const y3 = topY + endY;
const x4 = x1;
const y4 = topY + endY;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
ctx.lineTo(x4, y4);
ctx.closePath();
ctx.fill();
//上椭圆
ctx.beginPath();
ctx.fillStyle = "#39ACE7";
if (topY + startY <= 0) ctx.fillStyle = color;
ctx.ellipse(
config.centerX,
topY + startY,
config.cylinderRadius,
config.cylinderRadius * config.radiusRate,
0,
0,
Math.PI * 2
);
ctx.fill();
}
// 绘制顶部椭圆
function drawTopEllipse() {
const topY = config.centerY - config.cylinderHeight / 2;
ctx.fillStyle = "#CFDAE8";
ctx.beginPath();
ctx.ellipse(
config.centerX,
topY,
config.cylinderRadius,
config.cylinderRadius * config.radiusRate,
0,
0,
Math.PI * 2
);
ctx.fill();
// 绘制边缘线
// ctx.strokeStyle = config.colors.border;
// ctx.lineWidth = 1;
// ctx.stroke();
}
// 绘制刻度线
function drawTicks() {
const topY = config.centerY - config.cylinderHeight / 2;
const tickLength = 8;
ctx.strokeStyle = config.colors.tick;
ctx.lineWidth = 1;
ctx.font = '10px Arial';
ctx.fillStyle = config.colors.tick;
ctx.textAlign = 'right';
for (let i = 0; i <= config.maxValue; i += 10) {
const value = i;
const y = topY + config.cylinderHeight - (value / config.maxValue) * config.cylinderHeight;
let start_x = config.centerX - config.cylinderRadius - tickLength / 2;
const end_x = config.centerX - config.cylinderRadius;
if (i % config.tickGap == 0) {
start_x = config.centerX - config.cylinderRadius - tickLength;
}
// 绘制刻度线
ctx.beginPath();
ctx.moveTo(start_x, y);
ctx.lineTo(end_x, y);
ctx.stroke();
if (i % config.tickGap == 0) {
ctx.fillStyle = "#000000";
// 绘制刻度值
ctx.fillText(value, config.centerX - config.cylinderRadius - tickLength - 5, y + 3);
}
}
}
// 绘制3D效果边框
function drawBorder() {
const topY = config.centerY - config.cylinderHeight / 2;
const bottomY = config.centerY + config.cylinderHeight / 2;
// 左侧边框(阴影)
ctx.strokeStyle = darkenColor(config.colors.border, config.radiusRate);
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(config.centerX - config.cylinderRadius, topY);
ctx.lineTo(config.centerX - config.cylinderRadius, bottomY);
ctx.stroke();
// 右侧边框(高光)
ctx.strokeStyle = lightenColor(config.colors.border, config.radiusRate);
ctx.beginPath();
ctx.moveTo(config.centerX + config.cylinderRadius, topY);
ctx.lineTo(config.centerX + config.cylinderRadius, bottomY);
ctx.stroke();
// 顶部椭圆边框
ctx.strokeStyle = config.colors.border;
ctx.beginPath();
ctx.ellipse(
config.centerX,
topY,
config.cylinderRadius,
config.cylinderRadius * config.radiusRate,
0,
0,
Math.PI * 2
);
ctx.stroke();
// 底部椭圆边框
ctx.beginPath();
ctx.ellipse(
config.centerX,
bottomY,
config.cylinderRadius,
config.cylinderRadius * config.radiusRate,
0,
0,
Math.PI * 2
);
ctx.stroke();
}
// 颜色辅助函数
function adjustColorBrightness(color, amount) {
const hex = color => parseInt(color.slice(1), 16);
const r = (hex(color) >> 16) & 255;
const g = (hex(color) >> 8) & 255;
const b = hex(color) & 255;
const adjust = value => Math.max(0, Math.min(255, value + Math.round(255 * amount)));
return `rgb(${adjust(r)}, ${adjust(g)}, ${adjust(b)})`;
}
function interpolateColor(color1, color2, factor) {
const hex = color => parseInt(color.slice(1), 16);
const r1 = (hex(color1) >> 16) & 255;
const g1 = (hex(color1) >> 8) & 255;
const b1 = hex(color1) & 255;
const r2 = (hex(color2) >> 16) & 255;
const g2 = (hex(color2) >> 8) & 255;
const b2 = hex(color2) & 255;
const r = Math.round(r1 + (r2 - r1) * factor);
const g = Math.round(g1 + (g2 - g1) * factor);
const b = Math.round(b1 + (b2 - b1) * factor);
return `rgb(${r}, ${g}, ${b})`;
}
function lightenColor(color, amount) {
return adjustColorBrightness(color, amount);
}
function darkenColor(color, amount) {
return adjustColorBrightness(color, -amount);
}
// 动画更新数值
function updateValue(newValue) {
const startTime = Date.now();
const startValue = config.value;
const change = newValue - startValue;
function animate() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / config.animationDuration, 1);
// 缓动函数
const easedProgress = easeOutQuad(progress);
config.value = startValue + change * easedProgress;
valueDisplay.textContent = Math.round(config.value);
drawCylinderGauge();
if (progress < 1) {
requestAnimationFrame(animate);
}
}
animate();
}
// 缓动函数
function easeOutQuad(t) {
return t * (2 - t);
}
// 初始化绘制
drawCylinderGauge();
// 示例:点击更新数值
canvas.addEventListener('click', () => {
const newValue = Math.floor(Math.random() * 300);
updateValue(newValue);
});
</script>
</body>
</html>
index.html
index.html