init
This commit is contained in:
+208
@@ -0,0 +1,208 @@
|
||||
import { buildCanvasFont, hexToRgba, type IdDrawStyle } from "./id-config";
|
||||
|
||||
export type TextMetricsLike = {
|
||||
width: number;
|
||||
actualBoundingBoxAscent?: number;
|
||||
actualBoundingBoxDescent?: number;
|
||||
};
|
||||
|
||||
export type Canvas2D = {
|
||||
font: string;
|
||||
fillStyle: string | CanvasGradient | CanvasPattern;
|
||||
strokeStyle: string | CanvasGradient | CanvasPattern;
|
||||
lineWidth: number;
|
||||
textAlign: CanvasTextAlign;
|
||||
textBaseline: CanvasTextBaseline;
|
||||
shadowColor: string;
|
||||
shadowBlur: number;
|
||||
shadowOffsetX: number;
|
||||
shadowOffsetY: number;
|
||||
lineJoin: CanvasLineJoin;
|
||||
miterLimit: number;
|
||||
fillText(text: string, x: number, y: number): void;
|
||||
strokeText(text: string, x: number, y: number): void;
|
||||
measureText(text: string): TextMetricsLike;
|
||||
save(): void;
|
||||
restore(): void;
|
||||
setLineDash(segments: number[]): void;
|
||||
strokeRect(x: number, y: number, w: number, h: number): void;
|
||||
};
|
||||
|
||||
export type TextBox = {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type GlyphMetrics = {
|
||||
width: number;
|
||||
ascent: number;
|
||||
descent: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export function positionFromPercent(
|
||||
xPercent: number,
|
||||
yPercent: number,
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
): { x: number; y: number } {
|
||||
return {
|
||||
x: (xPercent / 100) * imageWidth,
|
||||
y: (yPercent / 100) * imageHeight,
|
||||
};
|
||||
}
|
||||
|
||||
function applyFont(ctx: Canvas2D, style: IdDrawStyle): void {
|
||||
ctx.font = buildCanvasFont(style);
|
||||
ctx.textAlign = style.align;
|
||||
ctx.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
export function measureGlyphs(
|
||||
ctx: Canvas2D,
|
||||
text: string,
|
||||
style: IdDrawStyle,
|
||||
): GlyphMetrics {
|
||||
applyFont(ctx, style);
|
||||
const metrics = ctx.measureText(text);
|
||||
const ascent = metrics.actualBoundingBoxAscent ?? style.fontSize * 0.8;
|
||||
const descent = metrics.actualBoundingBoxDescent ?? style.fontSize * 0.2;
|
||||
return {
|
||||
width: metrics.width,
|
||||
ascent,
|
||||
descent,
|
||||
height: ascent + descent,
|
||||
};
|
||||
}
|
||||
|
||||
export function alphabeticBaselineY(
|
||||
visualCenterY: number,
|
||||
glyphs: GlyphMetrics,
|
||||
): number {
|
||||
return visualCenterY + (glyphs.ascent - glyphs.descent) / 2;
|
||||
}
|
||||
|
||||
export function applyIdStyle(ctx: Canvas2D, style: IdDrawStyle): void {
|
||||
applyFont(ctx, style);
|
||||
ctx.fillStyle = style.color;
|
||||
}
|
||||
|
||||
function applyShadow(ctx: Canvas2D, style: IdDrawStyle): void {
|
||||
if (!style.shadowEnabled) {
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowOffsetX = 0;
|
||||
ctx.shadowOffsetY = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.shadowColor = hexToRgba(style.shadowColor, style.shadowOpacity);
|
||||
ctx.shadowBlur = style.shadowBlur;
|
||||
ctx.shadowOffsetX = style.shadowOffsetX;
|
||||
ctx.shadowOffsetY = style.shadowOffsetY;
|
||||
}
|
||||
|
||||
export function drawIdText(
|
||||
ctx: Canvas2D,
|
||||
text: string,
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
style: IdDrawStyle,
|
||||
): void {
|
||||
const { x, y } = positionFromPercent(
|
||||
style.xPercent,
|
||||
style.yPercent,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
);
|
||||
const glyphs = measureGlyphs(ctx, text, style);
|
||||
const baselineY = alphabeticBaselineY(y, glyphs);
|
||||
|
||||
ctx.save();
|
||||
applyIdStyle(ctx, style);
|
||||
applyShadow(ctx, style);
|
||||
|
||||
if (style.outlineEnabled && style.outlineWidth > 0) {
|
||||
ctx.strokeStyle = hexToRgba(style.outlineColor, style.outlineOpacity);
|
||||
ctx.lineWidth = style.outlineWidth;
|
||||
ctx.lineJoin = "round";
|
||||
ctx.miterLimit = 2;
|
||||
ctx.strokeText(text, x, baselineY);
|
||||
ctx.shadowColor = "transparent";
|
||||
}
|
||||
|
||||
ctx.fillText(text, x, baselineY);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function measureIdTextBox(
|
||||
ctx: Canvas2D,
|
||||
text: string,
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
style: IdDrawStyle,
|
||||
): TextBox {
|
||||
const glyphs = measureGlyphs(ctx, text, style);
|
||||
const { x, y } = positionFromPercent(
|
||||
style.xPercent,
|
||||
style.yPercent,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
);
|
||||
|
||||
let left = x;
|
||||
if (style.align === "center") left = x - glyphs.width / 2;
|
||||
if (style.align === "right") left = x - glyphs.width;
|
||||
|
||||
const outlinePad =
|
||||
style.outlineEnabled && style.outlineWidth > 0
|
||||
? style.outlineWidth / 2
|
||||
: 0;
|
||||
|
||||
return {
|
||||
left: left - outlinePad,
|
||||
top: y - glyphs.height / 2 - outlinePad,
|
||||
width: glyphs.width + outlinePad * 2,
|
||||
height: glyphs.height + outlinePad * 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function drawIdSelection(
|
||||
ctx: Canvas2D,
|
||||
box: TextBox,
|
||||
imageWidth: number,
|
||||
): void {
|
||||
const pad = Math.max(6, imageWidth / 400);
|
||||
ctx.save();
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.strokeStyle = "rgba(99, 102, 241, 0.95)";
|
||||
ctx.lineWidth = Math.max(1, imageWidth / 700);
|
||||
ctx.setLineDash([
|
||||
Math.max(4, imageWidth / 180),
|
||||
Math.max(3, imageWidth / 220),
|
||||
]);
|
||||
ctx.strokeRect(
|
||||
box.left - pad,
|
||||
box.top - pad,
|
||||
box.width + pad * 2,
|
||||
box.height + pad * 2,
|
||||
);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function hitTestTextBox(
|
||||
box: TextBox,
|
||||
x: number,
|
||||
y: number,
|
||||
imageWidth: number,
|
||||
): boolean {
|
||||
const pad = Math.max(10, imageWidth / 80);
|
||||
return (
|
||||
x >= box.left - pad &&
|
||||
x <= box.left + box.width + pad &&
|
||||
y >= box.top - pad &&
|
||||
y <= box.top + box.height + pad
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user