File size: 4,310 Bytes
2413e0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
127
128
129
// utils/quote-generate/text-render.js
// Render phase: draw laid-out text to a canvas.
// Sync, no I/O. Takes a TextLayout + PreparedText and produces a canvas.

const { createCanvas } = require('canvas')
const { hexToRgb, normalizeColor } = require('./color')

/**
 * Render laid-out text to a canvas.
 */
function renderText (layout, prepared, fontColor) {
  const { lines, width, height } = layout
  const { segments, fontSize, emojiSize } = prepared

  if (lines.length === 0 || width === 0) {
    return createCanvas(1, 1)
  }

  // Create exact-size canvas — no oversized buffer
  const canvasWidth = Math.max(1, Math.ceil(width))
  const canvasHeight = Math.max(1, Math.ceil(height))
  const canvas = createCanvas(canvasWidth, canvasHeight)
  const ctx = canvas.getContext('2d')

  let currentFont = null
  let currentFillStyle = null

  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
    const line = lines[lineIdx]

    for (const layoutSeg of line.segments) {
      const seg = segments[layoutSeg.index]
      if (!seg) continue

      // Resolve fill style for this segment
      let fillStyle = fontColor
      if (seg.styles.includes('monospace')) fillStyle = '#5887a7'
      else if (seg.styles.includes('mention')) fillStyle = '#6ab7ec'
      else if (seg.styles.includes('spoiler')) {
        const rgb = hexToRgb(normalizeColor(fontColor))
        fillStyle = `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.15)`
      }

      // Set context state (only when changed)
      if (currentFont !== seg.font) {
        ctx.font = seg.font
        currentFont = seg.font
      }
      if (currentFillStyle !== fillStyle) {
        ctx.fillStyle = fillStyle
        currentFillStyle = fillStyle
      }

      // Compute x position (RTL mirrors relative to canvas width for right-alignment)
      let drawX = layoutSeg.x
      if (line.direction === 'rtl') {
        const segWidth = seg.kind === 'emoji' ? emojiSize : layoutSeg.width
        drawX = width - layoutSeg.x - segWidth
      }

      const drawY = line.y

      if (seg.kind === 'emoji' && seg.emojiImage) {
        // Draw emoji image at consistent size
        ctx.drawImage(
          seg.emojiImage,
          drawX,
          drawY - fontSize + (fontSize * 0.15),
          emojiSize,
          emojiSize
        )
      } else if (seg.kind === 'space' || seg.kind === 'break') {
        // Don't draw whitespace
      } else {
        // Determine text to draw (handle grapheme splits from overflow-wrap)
        let drawText = seg.text
        if (layoutSeg.graphemeStart !== undefined || layoutSeg.graphemeEnd !== undefined) {
          if (prepared.computeGraphemeWidths) {
            const gData = prepared.computeGraphemeWidths(seg)
            if (gData) {
              const start = layoutSeg.graphemeStart !== undefined ? layoutSeg.graphemeStart : 0
              const end = layoutSeg.graphemeEnd !== undefined ? layoutSeg.graphemeEnd : gData.texts.length
              drawText = gData.texts.slice(start, end).join('')
            }
          }
        }

        ctx.fillText(drawText, drawX, drawY)

        // Strikethrough decoration
        if (seg.styles.includes('strikethrough')) {
          const textWidth = ctx.measureText(drawText).width
          ctx.fillRect(drawX, drawY - fontSize / 2.8, textWidth, fontSize * 0.1)
        }

        // Underline decoration
        if (seg.styles.includes('underline')) {
          const textWidth = ctx.measureText(drawText).width
          ctx.fillRect(drawX, drawY + 2, textWidth, fontSize * 0.1)
        }
      }
    }

    // Handle truncation ellipsis on last line
    if (lineIdx === lines.length - 1 && line.truncated) {
      const ellipsis = '\u2026'
      if (currentFillStyle !== fontColor) {
        ctx.fillStyle = fontColor
        currentFillStyle = fontColor
      }

      const lastSeg = line.segments[line.segments.length - 1]
      if (lastSeg) {
        let ellipsisX = lastSeg.x + lastSeg.width
        // Apply RTL mirror to ellipsis position
        if (line.direction === 'rtl') {
          const eWidth = ctx.measureText(ellipsis).width
          ellipsisX = width - lastSeg.x - lastSeg.width - eWidth
        }
        ctx.fillText(ellipsis, ellipsisX, line.y)
      }
    }
  }

  return canvas
}

module.exports = { renderText }