Files
2026-06-25 12:46:43 +08:00

211 lines
5.6 KiB
TypeScript
Raw Permalink 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.
const CLOCK_GREEN = '#3D9E47';
const CLOCK_BLACK = '#333333';
const HOUR_HAND_RED = '#E53935';
export interface DrawAnalogClockOptions {
cx: number;
cy: number;
radius: number;
hour: number;
minute: number;
}
function toCanvasAngle(degFromTwelve: number): number {
return ((degFromTwelve - 90) * Math.PI) / 180;
}
function handPoint(
cx: number,
cy: number,
angle: number,
perp: number,
along: number,
perpOffset: number,
) {
return {
x: cx + Math.cos(angle) * along + Math.cos(perp) * perpOffset,
y: cy + Math.sin(angle) * along + Math.sin(perp) * perpOffset,
};
}
/** 指针:近圆心 80% 为矩形,远端 20% 收尖为三角形 */
function drawCompositeHand(
ctx: RenderingContext,
cx: number,
cy: number,
angleDeg: number,
length: number,
width: number,
color: string,
) {
const angle = toCanvasAngle(angleDeg);
const perp = angle + Math.PI / 2;
const halfW = width / 2;
const rectEnd = length * 0.8;
const tip = handPoint(cx, cy, angle, perp, length, 0);
const rectEndR = handPoint(cx, cy, angle, perp, rectEnd, halfW);
const baseR = handPoint(cx, cy, angle, perp, 0, halfW);
const baseL = handPoint(cx, cy, angle, perp, 0, -halfW);
const rectEndL = handPoint(cx, cy, angle, perp, rectEnd, -halfW);
ctx.save();
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(tip.x, tip.y);
ctx.lineTo(rectEndR.x, rectEndR.y);
ctx.lineTo(baseR.x, baseR.y);
ctx.lineTo(baseL.x, baseL.y);
ctx.lineTo(rectEndL.x, rectEndL.y);
ctx.closePath();
ctx.fill();
ctx.restore();
}
const HAND_WIDTH = 2.8;
/** 绘制模拟钟表:绿圈、刻度、数字、红时针、黑分针 */
export function drawAnalogClock(
ctx: RenderingContext,
options: DrawAnalogClockOptions,
): void {
const { cx, cy, radius, hour, minute } = options;
const faceRadius = radius - 4;
ctx.save();
// 外圈(线宽原 3.5 的 90%
ctx.strokeStyle = CLOCK_GREEN;
ctx.lineWidth = 3.15;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.stroke();
// 刻度(长度缩短为原来的 80%)
const tickOuter = faceRadius - 1;
const hourTickLen = 7 * 0.8;
const minuteTickLen = 3 * 0.8;
for (let i = 0; i < 60; i++) {
const angle = toCanvasAngle(i * 6);
const isHour = i % 5 === 0;
const tickLen = isHour ? hourTickLen : minuteTickLen;
const inner = tickOuter - tickLen;
ctx.strokeStyle = CLOCK_BLACK;
ctx.lineWidth = isHour ? 1.5 : 0.8;
ctx.beginPath();
ctx.moveTo(cx + Math.cos(angle) * inner, cy + Math.sin(angle) * inner);
ctx.lineTo(cx + Math.cos(angle) * tickOuter, cy + Math.sin(angle) * tickOuter);
ctx.stroke();
}
// 数字 112(原 13px 的 70%
ctx.fillStyle = CLOCK_BLACK;
ctx.font = 'bold 9px "Microsoft Yahei", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const numberRadius = faceRadius - 14;
for (let n = 1; n <= 12; n++) {
const angle = toCanvasAngle(n * 30);
const nx = cx + Math.cos(angle) * numberRadius;
const ny = cy + Math.sin(angle) * numberRadius;
ctx.fillText(String(n), nx, ny);
}
const minuteAngle = minute * 6;
const hourAngle = (hour % 12) * 30 + minute * 0.5;
// 指针:80% 矩形 + 20% 三角尖
const minuteHandLength = numberRadius - 3;
const hourHandLength = numberRadius * 0.67;
drawCompositeHand(
ctx,
cx,
cy,
minuteAngle,
minuteHandLength,
HAND_WIDTH,
CLOCK_BLACK,
);
drawCompositeHand(
ctx,
cx,
cy,
hourAngle,
hourHandLength,
HAND_WIDTH,
HOUR_HAND_RED,
);
// 中心点
ctx.fillStyle = CLOCK_BLACK;
ctx.beginPath();
ctx.arc(cx, cy, 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
const INPUT_GREEN = '#3D9E47';
function strokeRoundRect(
ctx: RenderingContext,
x: number,
y: number,
w: number,
h: number,
r: number,
) {
const radius = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + w - radius, y);
ctx.arcTo(x + w, y, x + w, y + radius, radius);
ctx.lineTo(x + w, y + h - radius);
ctx.arcTo(x + w, y + h, x + w - radius, y + h, radius);
ctx.lineTo(x + radius, y + h);
ctx.arcTo(x, y + h, x, y + h - radius, radius);
ctx.lineTo(x, y + radius);
ctx.arcTo(x, y, x + radius, y, radius);
ctx.closePath();
ctx.stroke();
}
/** 在矩形内水平、垂直居中绘制文字 */
function fillTextCentered(
ctx: RenderingContext,
text: string,
x: number,
y: number,
width: number,
height: number,
) {
ctx.textAlign = 'center';
ctx.textBaseline = 'alphabetic';
const metrics = ctx.measureText(text);
const ascent = metrics.actualBoundingBoxAscent ?? 10;
const descent = metrics.actualBoundingBoxDescent ?? 2;
const textH = ascent + descent;
const baselineY = y + (height - textH) / 2 + ascent;
ctx.fillText(text, x + width / 2, baselineY);
}
/** 绘制数字填写框(绿色圆角描边 + 居中冒号) */
export function drawTimeInputBox(
ctx: RenderingContext,
x: number,
y: number,
width: number,
height: number,
): void {
ctx.save();
ctx.strokeStyle = INPUT_GREEN;
ctx.lineWidth = 1.5;
strokeRoundRect(ctx, x, y, width, height, 6);
ctx.fillStyle = CLOCK_BLACK;
ctx.font = 'bold 16px "Microsoft Yahei", sans-serif';
fillTextCentered(ctx, ':', x, y, width, height);
ctx.restore();
}