feat: 新增认识时钟绘制
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user