dropflow
Version:
A small CSS2 document renderer built from specifications
86 lines (85 loc) • 3.2 kB
JavaScript
import { getMetrics } from './layout-text.js';
function encode(s) {
return s.replaceAll('&', '&').replaceAll('<', '<');
}
function camelToKebab(camel) {
return camel.replace(/[A-Z]/g, s => '-' + s.toLowerCase());
}
export default class HtmlPaintBackend {
s;
fillColor;
strokeColor;
lineWidth;
direction;
font;
fontSize;
constructor() {
this.s = '';
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 = 0;
}
style(style) {
return Object.entries(style).map(([prop, value]) => {
return `${camelToKebab(prop)}: ${value}`;
}).join('; ');
}
attrs(attrs) {
return Object.entries(attrs).map(([name, value]) => {
return `${name}="${value}"`;
}).join(' ');
}
// 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;
const sw = this.lineWidth;
const left = (side === 'left' ? x - sw / 2 : side === 'right' ? x - sw / 2 : x) + 'px';
const top = (side === 'top' ? y - sw / 2 : side === 'bottom' ? y - sw / 2 : y) + 'px';
const width = side === 'top' || side === 'bottom' ? length + 'px' : sw + 'px';
const height = side === 'left' || side === 'right' ? length + 'px' : sw + 'px';
const position = 'absolute';
const backgroundColor = `rgba(${r}, ${g}, ${b}, ${a})`;
const style = this.style({ position, left, top, width, height, backgroundColor });
this.s += `<div style="${style}"></div>`;
}
text(x, y, item, textStart, textEnd) {
const { ascenderBox, descenderBox } = getMetrics(item.attrs.style, item.face);
const text = item.paragraph.string.slice(textStart, textEnd);
const { r, g, b, a } = this.fillColor;
const style = this.style({
position: 'absolute',
left: '0',
top: '0',
transform: `translate(${x}px, ${y - (ascenderBox - (ascenderBox + descenderBox) / 2)}px)`,
font: this.font?.toFontString(this.fontSize) || "",
lineHeight: '0',
whiteSpace: 'pre',
direction: this.direction,
unicodeBidi: 'bidi-override',
color: `rgba(${r}, ${g}, ${b}, ${a})`
});
this.s += `<div style="${style}">${encode(text)}</div>`;
}
rect(x, y, w, h) {
const { r, g, b, a } = this.fillColor;
const style = this.style({
position: 'absolute',
left: x + 'px',
top: y + 'px',
width: w + 'px',
height: h + 'px',
backgroundColor: `rgba(${r}, ${g}, ${b}, ${a})`
});
this.s += `<div style="${style}"></div>`;
}
pushClip(x, y, width, height) {
this.s += `<div style="position: absolute; clip: rect(${y}px, ${x + width}px, ${y + height}px, ${x}px);">`;
}
popClip() {
this.s += '</div>';
}
}