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(); } // 数字 1–12(原 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(); }