gx
chenyc
2025-06-12 7b72ac13a83764a662159d4a49b7fffb90476ecb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/* eslint-disable max-classes-per-file */
import { IDimensions, IPoint } from '../classes/index';
import { getContext2dOrThrow } from '../dom/getContext2dOrThrow';
import { resolveInput } from '../dom/resolveInput';
 
// eslint-disable-next-line no-shadow
export enum AnchorPosition {
  // eslint-disable-next-line no-unused-vars
  TOP_LEFT = 'TOP_LEFT',
  // eslint-disable-next-line no-unused-vars
  TOP_RIGHT = 'TOP_RIGHT',
  // eslint-disable-next-line no-unused-vars
  BOTTOM_LEFT = 'BOTTOM_LEFT',
  // eslint-disable-next-line no-unused-vars
  BOTTOM_RIGHT = 'BOTTOM_RIGHT'
}
 
export interface IDrawTextFieldOptions {
  anchorPosition?: AnchorPosition
  backgroundColor?: string
  fontColor?: string
  fontSize?: number
  fontStyle?: string
  padding?: number
}
 
export class DrawTextFieldOptions implements IDrawTextFieldOptions {
  public anchorPosition: AnchorPosition;
 
  public backgroundColor: string;
 
  public fontColor: string;
 
  public fontSize: number;
 
  public fontStyle: string;
 
  public padding: number;
 
  constructor(options: IDrawTextFieldOptions = {}) {
    const {
      anchorPosition, backgroundColor, fontColor, fontSize, fontStyle, padding,
    } = options;
    this.anchorPosition = anchorPosition || AnchorPosition.TOP_LEFT;
    this.backgroundColor = backgroundColor || 'rgba(0, 0, 0, 0.5)';
    this.fontColor = fontColor || 'rgba(255, 255, 255, 1)';
    this.fontSize = fontSize || 14;
    this.fontStyle = fontStyle || 'Georgia';
    this.padding = padding || 4;
  }
}
 
export class DrawTextField {
  public text: string[];
 
  public anchor : IPoint;
 
  public options: DrawTextFieldOptions;
 
  constructor(
    text: string | string[] | DrawTextField,
    anchor: IPoint,
    options: IDrawTextFieldOptions = {},
  ) {
    // eslint-disable-next-line no-nested-ternary
    this.text = typeof text === 'string'
      ? [text]
      : (text instanceof DrawTextField ? text.text : text);
    this.anchor = anchor;
    this.options = new DrawTextFieldOptions(options);
  }
 
  measureWidth(ctx: CanvasRenderingContext2D): number {
    const { padding } = this.options;
    return this.text.map((l) => ctx.measureText(l).width).reduce((w0, w1) => (w0 < w1 ? w1 : w0), 0) + (2 * padding);
  }
 
  measureHeight(): number {
    const { fontSize, padding } = this.options;
    return this.text.length * fontSize + (2 * padding);
  }
 
  getUpperLeft(ctx: CanvasRenderingContext2D, canvasDims?: IDimensions): IPoint {
    const { anchorPosition } = this.options;
    const isShiftLeft = anchorPosition === AnchorPosition.BOTTOM_RIGHT || anchorPosition === AnchorPosition.TOP_RIGHT;
    const isShiftTop = anchorPosition === AnchorPosition.BOTTOM_LEFT || anchorPosition === AnchorPosition.BOTTOM_RIGHT;
 
    const textFieldWidth = this.measureWidth(ctx);
    const textFieldHeight = this.measureHeight();
    const x = (isShiftLeft ? this.anchor.x - textFieldWidth : this.anchor.x);
    const y = isShiftTop ? this.anchor.y - textFieldHeight : this.anchor.y;
 
    // adjust anchor if text box exceeds canvas borders
    if (canvasDims) {
      const { width, height } = canvasDims;
      const newX = Math.max(Math.min(x, width - textFieldWidth), 0);
      const newY = Math.max(Math.min(y, height - textFieldHeight), 0);
      return { x: newX, y: newY };
    }
    return { x, y };
  }
 
  draw(canvasArg: string | HTMLCanvasElement | CanvasRenderingContext2D) {
    const canvas = resolveInput(canvasArg);
    const ctx = getContext2dOrThrow(canvas);
 
    const {
      backgroundColor, fontColor, fontSize, fontStyle, padding,
    } = this.options;
 
    ctx.font = `${fontSize}px ${fontStyle}`;
    const maxTextWidth = this.measureWidth(ctx);
    const textHeight = this.measureHeight();
 
    ctx.fillStyle = backgroundColor;
    const upperLeft = this.getUpperLeft(ctx, canvas);
    ctx.fillRect(upperLeft.x, upperLeft.y, maxTextWidth, textHeight);
 
    ctx.fillStyle = fontColor;
    this.text.forEach((textLine, i) => {
      const x = padding + upperLeft.x;
      const y = padding + upperLeft.y + ((i + 1) * fontSize);
      ctx.fillText(textLine, x, y);
    });
  }
}