| const { createCanvas } = require('canvas') |
|
|
| |
| |
| |
| |
| function bubblePath (ctx, w, h, r, tailSize) { |
| let { tl, tr, br, bl } = typeof r === 'number' ? { tl: r, tr: r, br: r, bl: r } : r |
| const cap = (v) => Math.min(v, w / 2, h / 2) |
| tl = cap(tl) |
| tr = cap(tr) |
| br = cap(br) |
| bl = cap(bl) |
|
|
| ctx.beginPath() |
| ctx.moveTo(tl, 0) |
| |
| ctx.arcTo(w, 0, w, h, tr) |
| |
| ctx.arcTo(w, h, 0, h, br) |
|
|
| if (tailSize > 0) { |
| const t = tailSize |
|
|
| |
| ctx.lineTo(-t, h) |
|
|
| |
| |
| ctx.bezierCurveTo( |
| -t * 0.4, h, |
| 0, h - bl * 0.3, |
| 0, h - bl |
| ) |
| } else { |
| |
| ctx.arcTo(0, h, 0, 0, bl) |
| } |
|
|
| |
| ctx.arcTo(0, 0, w, 0, tl) |
| ctx.closePath() |
| } |
|
|
| |
| |
| |
| function paintGlass (ctx, w, h, r, tailSize, lw) { |
| ctx.save() |
| bubblePath(ctx, w, h, r, tailSize) |
| ctx.clip() |
|
|
| |
| |
| bubblePath(ctx, w, h, r, tailSize) |
| ctx.lineWidth = lw * 2 |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.07)' |
| ctx.stroke() |
|
|
| |
| const grad = ctx.createLinearGradient(0, 0, 0, h * 0.4) |
| grad.addColorStop(0, 'rgba(255, 255, 255, 0.16)') |
| grad.addColorStop(1, 'rgba(255, 255, 255, 0)') |
| ctx.lineWidth = lw * 2.6 |
| ctx.strokeStyle = grad |
| ctx.stroke() |
| ctx.restore() |
| } |
|
|
| function drawRoundRect (color, w, h, r, tailSize = 0, glassLw = 0) { |
| const extraLeft = tailSize > 0 ? Math.ceil(tailSize * 0.8) : 0 |
| const canvas = createCanvas(w + extraLeft, h) |
| const ctx = canvas.getContext('2d') |
| ctx.translate(extraLeft, 0) |
| ctx.fillStyle = color |
| bubblePath(ctx, w, h, r, tailSize) |
| ctx.fill() |
| if (glassLw > 0) paintGlass(ctx, w, h, r, tailSize, glassLw) |
| canvas._tailOffset = extraLeft |
| return canvas |
| } |
|
|
| function drawGradientRoundRect (colorOne, colorTwo, w, h, r, tailSize = 0, glassLw = 0) { |
| const extraLeft = tailSize > 0 ? Math.ceil(tailSize * 0.8) : 0 |
| const canvas = createCanvas(w + extraLeft, h) |
| const ctx = canvas.getContext('2d') |
| ctx.translate(extraLeft, 0) |
| const gradient = ctx.createLinearGradient(0, 0, w, h) |
| gradient.addColorStop(0, colorOne) |
| gradient.addColorStop(1, colorTwo) |
| ctx.fillStyle = gradient |
| bubblePath(ctx, w, h, r, tailSize) |
| ctx.fill() |
| if (glassLw > 0) paintGlass(ctx, w, h, r, tailSize, glassLw) |
| canvas._tailOffset = extraLeft |
| return canvas |
| } |
|
|
| |
| |
| |
| function roundImage (image, r) { |
| const w = image.width |
| const h = image.height |
| const canvas = createCanvas(w, h) |
| const ctx = canvas.getContext('2d') |
| ctx.imageSmoothingEnabled = true |
| ctx.imageSmoothingQuality = 'high' |
| let { tl, tr, br, bl } = typeof r === 'number' ? { tl: r, tr: r, br: r, bl: r } : r |
| const cap = (v) => Math.min(v, w / 2, h / 2) |
| tl = cap(tl) |
| tr = cap(tr) |
| br = cap(br) |
| bl = cap(bl) |
| ctx.beginPath() |
| ctx.moveTo(tl, 0) |
| ctx.arcTo(w, 0, w, h, tr) |
| ctx.arcTo(w, h, 0, h, br) |
| ctx.arcTo(0, h, 0, 0, bl) |
| ctx.arcTo(0, 0, w, 0, tl) |
| ctx.save() |
| ctx.clip() |
| ctx.closePath() |
| ctx.drawImage(image, 0, 0) |
| ctx.restore() |
| return canvas |
| } |
|
|
| function drawReplyLine (lineWidth, height, color) { |
| const r = lineWidth |
| const w = lineWidth |
| const h = height + lineWidth |
| const x = 5 |
| const canvas = createCanvas(w + 10, h) |
| const ctx = canvas.getContext('2d') |
|
|
| ctx.fillStyle = color |
| ctx.beginPath() |
| |
| ctx.moveTo(x, r) |
| ctx.arcTo(x, 0, x + w, 0, r) |
| |
| ctx.lineTo(x + w, 0) |
| |
| ctx.lineTo(x + w, h) |
| |
| ctx.lineTo(x + r, h) |
| |
| ctx.arcTo(x, h, x, 0, r) |
| ctx.closePath() |
| ctx.fill() |
|
|
| return canvas |
| } |
|
|
| function drawQuoteIcon (size, color, alpha = 0.15) { |
| const canvas = createCanvas(size, size) |
| const ctx = canvas.getContext('2d') |
|
|
| |
| ctx.fillStyle = color |
| ctx.globalAlpha = alpha |
|
|
| const s = size |
| const r = s * 0.09 |
| const gapX = s * 0.36 |
|
|
| |
| ctx.beginPath() |
| addQuoteMarkPath(ctx, s * 0.08, s * 0.15, r, s) |
| addQuoteMarkPath(ctx, s * 0.08 + gapX, s * 0.15, r, s) |
| ctx.fill() |
|
|
| return canvas |
| } |
|
|
| function addQuoteMarkPath (ctx, x, y, r, s) { |
| |
| const cx = x + r |
| const cy = y + r |
| |
| ctx.moveTo(cx + r, cy) |
| ctx.arc(cx, cy, r, 0, Math.PI * 2) |
| |
| ctx.moveTo(x + r * 2, cy) |
| ctx.quadraticCurveTo(x + r * 2.2, y + s * 0.35, x + r * 0.3, y + s * 0.4) |
| ctx.lineTo(x + r * 0.3, y + s * 0.32) |
| ctx.quadraticCurveTo(x + r * 1.5, y + s * 0.27, x + r * 0.8, cy) |
| ctx.closePath() |
| } |
|
|
| |
| |
| |
| |
| |
| function inkBounds (canvas) { |
| const w = canvas.width |
| const h = canvas.height |
| |
| |
| if (w < 1 || h < 1 || typeof canvas.getContext !== 'function') return null |
| const data = canvas.getContext('2d').getImageData(0, 0, w, h).data |
| const rowHasInk = (y) => { |
| const off = y * w * 4 + 3 |
| for (let x = 0; x < w; x++) if (data[off + x * 4] > 8) return true |
| return false |
| } |
| |
| let top = -1 |
| for (let y = 0; y < h; y++) if (rowHasInk(y)) { top = y; break } |
| if (top === -1) return null |
| let bottom = top |
| for (let y = h - 1; y > top; y--) if (rowHasInk(y)) { bottom = y; break } |
| return { top, bottom } |
| } |
|
|
| |
| |
| |
| function drawLabel (text, fontSize, color, opts = {}) { |
| |
| const { fontMetrics } = require('./text-prepare') |
| const { ascent, descent } = fontMetrics(fontSize) |
| const font = `${opts.bold ? 'bold ' : ''}${fontSize}px NotoSans` |
| const measure = createCanvas(1, 1).getContext('2d') |
| measure.font = font |
| const m = measure.measureText(text) |
|
|
| const canvas = createCanvas(Math.max(1, Math.ceil(m.width)), Math.max(1, Math.ceil(ascent + descent))) |
| const ctx = canvas.getContext('2d') |
| ctx.font = font |
| ctx.fillStyle = color |
| if (opts.alpha !== undefined) ctx.globalAlpha = opts.alpha |
| ctx.fillText(text, 0, ascent) |
| return canvas |
| } |
|
|
| function drawForwardLabel (text, fontSize, color) { |
| return drawLabel(text, fontSize, color, { bold: true }) |
| } |
|
|
| module.exports = { drawRoundRect, drawGradientRoundRect, roundImage, drawReplyLine, drawQuoteIcon, drawLabel, drawForwardLabel, inkBounds } |
|
|