UNPKG

dropflow

Version:

A small CSS2 document renderer built from specifications

177 lines (176 loc) 7.58 kB
import { prevCluster, nextCluster, nextGrapheme, prevGrapheme } from './layout-text.js'; import { G_ID, G_CL, G_AX, G_AY, G_DX, G_DY, G_FL, G_SZ } from './text-harfbuzz.js'; function findGlyph(item, offset) { let index = item.attrs.level & 1 ? item.glyphs.length - G_SZ : 0; while (index >= 0 && index < item.glyphs.length && item.glyphs[index + G_CL] < offset) { index += item.attrs.level & 1 ? -G_SZ : G_SZ; } return index; } function glyphsWidth(item, glyphStart, glyphEnd) { let ax = 0; for (let i = glyphStart; i < glyphEnd; i += G_SZ) ax += item.glyphs[i + G_AX]; return ax / item.face.hbface.upem * item.attrs.style.fontSize; } // Solve for: // textStart..textEnd: largest safe-boundaried string inside totalTextStart..totalTextEnd // startGlyphStart...startGlyphEnd: chain of glyphs inside totalTextStart..textStart // endGlyphStart...endGlyphEnd: chain of glyphs inside textEnd...totalTextEnd // TODO not well tested. this took me days to figure out function fastGlyphBoundaries(item, totalTextStart, totalTextEnd) { const glyphs = item.glyphs; let startGlyphStart = findGlyph(item, totalTextStart); let endGlyphEnd = findGlyph(item, totalTextEnd); // TODO findGlyphFromEnd? let textStart = nextGrapheme(item.paragraph.string, totalTextStart); let startGlyphEnd = findGlyph(item, textStart); let textEnd = Math.max(textStart, prevGrapheme(item.paragraph.string, totalTextEnd)); let endGlyphStart = findGlyph(item, textEnd); if (item.attrs.level & 1) { while (startGlyphEnd > endGlyphStart && glyphs[startGlyphEnd + G_FL] & 1) { startGlyphEnd = prevCluster(glyphs, startGlyphEnd); } textStart = glyphs[startGlyphEnd + G_CL] ?? textEnd; while (endGlyphStart < startGlyphEnd && glyphs[endGlyphStart + G_FL] & 1) { endGlyphStart = nextCluster(glyphs, endGlyphStart); } textEnd = glyphs[endGlyphStart + G_CL] ?? textEnd; } else { while (startGlyphEnd < endGlyphStart && glyphs[startGlyphEnd + G_FL] & 1) { startGlyphEnd = nextCluster(glyphs, startGlyphEnd); } textStart = glyphs[startGlyphEnd + G_CL] ?? textEnd; while (endGlyphStart > startGlyphEnd && glyphs[endGlyphStart + G_FL] & 1) { endGlyphStart = prevCluster(glyphs, endGlyphStart); } textEnd = glyphs[endGlyphStart + G_CL] ?? textEnd; } if (item.attrs.level & 1) { [startGlyphStart, startGlyphEnd] = [startGlyphEnd + G_SZ, startGlyphStart + G_SZ]; [endGlyphStart, endGlyphEnd] = [endGlyphEnd + G_SZ, endGlyphStart + G_SZ]; } return { startGlyphStart, startGlyphEnd, textStart, textEnd, endGlyphStart, endGlyphEnd }; } export default class CanvasPaintBackend { fillColor; strokeColor; lineWidth; direction; font; fontSize; ctx; constructor(ctx) { this.fillColor = { r: 0, g: 0, b: 0, a: 0 }; this.strokeColor = { r: 0, g: 0, b: 0, a: 0 }; this.lineWidth = 0; this.direction = 'ltr'; this.font = undefined; this.fontSize = 8; this.ctx = ctx; } // TODO: pass in amount of each side that's shared with another border so they can divide // TODO: pass in border-radius edge(x, y, length, side) { const { r, g, b, a } = this.strokeColor; this.ctx.beginPath(); this.ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${a})`; this.ctx.lineWidth = this.lineWidth; this.ctx.moveTo(x, y); this.ctx.lineTo(side === 'top' || side === 'bottom' ? x + length : x, side === 'left' || side === 'right' ? y + length : y); this.ctx.stroke(); } fastText(x, y, item, textStart, textEnd) { const text = item.paragraph.slice(textStart, textEnd); const { r, g, b, a } = this.fillColor; this.ctx.save(); this.ctx.direction = item.attrs.level & 1 ? 'rtl' : 'ltr'; this.ctx.textAlign = 'left'; // TODO: PR to node-canvas to make this the default. I see no issues with // drawing glyphs, and it's way way way faster, and the correct way to do it if ('textDrawingMode' in this.ctx) { this.ctx.textDrawingMode = 'glyph'; } this.ctx.font = this.font?.toFontString(this.fontSize) || ''; this.ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`; this.ctx.fillText(text, x, y); this.ctx.restore(); } correctText(x, y, item, glyphStart, glyphEnd) { const { r, g, b, a } = this.fillColor; const scale = 1 / item.face.hbface.upem * this.fontSize; let sx = 0; let sy = 0; this.ctx.save(); this.ctx.translate(x, y); this.ctx.scale(scale, -scale); this.ctx.beginPath(); this.ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`; for (let i = glyphStart; i < glyphEnd; i += G_SZ) { const x = sx + item.glyphs[i + G_DX]; const y = sy + item.glyphs[i + G_DY]; this.ctx.translate(x, y); item.face.hbfont.drawGlyph(item.glyphs[i + G_ID], this.ctx); this.ctx.translate(-x, -y); sx += item.glyphs[i + G_AX]; sy += item.glyphs[i + G_AY]; } this.ctx.fill(); this.ctx.restore(); } text(x, y, item, totalTextStart, totalTextEnd, isColorBoundary) { if (isColorBoundary) { const { startGlyphStart, startGlyphEnd, textStart, textEnd, endGlyphStart, endGlyphEnd } = fastGlyphBoundaries(item, totalTextStart, totalTextEnd); if (item.attrs.level & 1) { if (endGlyphStart !== endGlyphEnd) { this.correctText(x, y, item, endGlyphStart, endGlyphEnd); x += glyphsWidth(item, endGlyphStart, endGlyphEnd); } if (textStart !== textEnd) { this.fastText(x, y, item, textStart, textEnd); x += glyphsWidth(item, startGlyphEnd, endGlyphStart); } if (startGlyphStart !== startGlyphEnd) { this.correctText(x, y, item, startGlyphStart, startGlyphEnd); } } else { if (startGlyphStart !== startGlyphEnd) { this.correctText(x, y, item, startGlyphStart, startGlyphEnd); x += glyphsWidth(item, startGlyphStart, startGlyphEnd); } if (textStart !== textEnd) { this.fastText(x, y, item, textStart, textEnd); x += glyphsWidth(item, startGlyphEnd, endGlyphStart); } if (endGlyphStart !== endGlyphEnd) { this.correctText(x, y, item, endGlyphStart, endGlyphEnd); } } } else { this.fastText(x, y, item, totalTextStart, totalTextEnd); } } rect(x, y, w, h) { const { r, g, b, a } = this.fillColor; this.ctx.beginPath(); this.ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`; this.ctx.fillRect(x, y, w, h); } pushClip(x, y, w, h) { this.ctx.save(); this.ctx.beginPath(); this.ctx.rect(x, y, w, h); this.ctx.clip(); } popClip() { this.ctx.restore(); } image(x, y, w, h, image) { if (!image.decoded) { throw new Error('Image handle missing. Did you call flow.loadSync instead of flow.load?'); } this.ctx.drawImage(image.decoded, x, y, w, h); } }