Skip to content

Commit

Permalink
feat: support vertical-rl / vertical-lr
Browse files Browse the repository at this point in the history
  • Loading branch information
qq15725 committed Mar 19, 2024
1 parent b132f25 commit db99dbc
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 145 deletions.
16 changes: 0 additions & 16 deletions src/bounding-box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,6 @@ export class BoundingBox {
return this
}

flipVerticalLr(width: number, height: number) {
const { x, y } = this
this.x = y
this.y = x
this.width = width
this.height = height
}

flipVerticalRl(width: number, height: number, totalWidth: number) {
const { x, y } = this
this.x = totalWidth - y - width
this.y = x
this.width = width
this.height = height
}

clone() {
return new BoundingBox({
x: this.x,
Expand Down
259 changes: 145 additions & 114 deletions src/measure-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,121 +48,175 @@ function resolveStyle(style?: Partial<MeasureTextStyle>): MeasureTextStyle {
export function measureText(options: MeasureTextOptions) {
const { content } = options
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { width, height, ...style } = resolveStyle(options.style)
const { width: userWidth, height: userHeight, ...style } = resolveStyle(options.style)
let paragraphs = parseParagraphs(content, style)
paragraphs = wrapParagraphs(paragraphs, width, height)
paragraphs = wrapParagraphs(paragraphs, userWidth, userHeight)

const maxVerticalWidth = paragraphs.reduce((w, p) => w + p.maxCharWidth, 0)

let px = 0
let py = 0
paragraphs.forEach(p => {
const contentBoxes: Array<BoundingBox> = []
let highestF: Fragment | null = null
let fx = 0

p.fragments.forEach(f => {
let fx = px
let fy = py
p.fragments.forEach((f, i) => {
const fStyle = f.getComputedStyle()
const {
fontBoundingBoxAscent,
fontBoundingBoxDescent,
actualBoundingBoxAscent,
actualBoundingBoxDescent,
actualBoundingBoxLeft,
actualBoundingBoxRight,
width: fWidth,
} = canvasMeasureText(f.content, {
...fStyle,
textAlign: 'center',
verticalAlign: 'baseline',
})
const fHeight = fStyle.fontSize
f.inlineBox.x = fx
f.inlineBox.y = py
f.inlineBox.width = fWidth
f.inlineBox.height = fHeight * fStyle.lineHeight
f.contentBox.x = f.inlineBox.x
f.contentBox.y = f.inlineBox.y + (f.inlineBox.height - fHeight) / 2
f.contentBox.width = fWidth
f.contentBox.height = fHeight
f.baseline = f.inlineBox.y
+ (f.inlineBox.height - (fontBoundingBoxAscent + fontBoundingBoxDescent)) / 2
+ fontBoundingBoxAscent
f.glyphBox.x = f.contentBox.x
f.glyphBox.y = f.baseline - actualBoundingBoxAscent
f.glyphBox.width = actualBoundingBoxLeft + actualBoundingBoxRight
f.glyphBox.height = actualBoundingBoxAscent + actualBoundingBoxDescent
f.centerX = f.glyphBox.x + actualBoundingBoxLeft
fx += f.contentBox.width
contentBoxes.push(f.contentBox)
p.contentBox = BoundingBox.from(...contentBoxes)
if (p.contentBox.height < f.contentBox.height) highestF = f
switch (fStyle.writingMode) {
case 'vertical-rl':
fx = maxVerticalWidth - fx
// eslint-disable-next-line no-fallthrough
case 'vertical-lr': {
if (!i) fy = 0
const len = f.content.length
const fWidth = p.maxCharWidth
const fLineWidth = fWidth * fStyle.lineHeight
const fHeight = len * fStyle.fontSize + (len - 1) * fStyle.letterSpacing
f.contentBox.x = fx + (fLineWidth - fWidth) / 2
f.contentBox.y = fy
f.contentBox.width = fWidth
f.contentBox.height = fHeight
f.inlineBox.x = fx
f.inlineBox.y = fy
f.inlineBox.width = fLineWidth
f.inlineBox.height = fHeight
f.glyphBox.x = fx + (fLineWidth - fWidth) / 2
f.glyphBox.y = fy
f.glyphBox.width = fWidth
f.glyphBox.height = fHeight
f.baseline = 0
f.centerX = fx + fLineWidth / 2
fy += fHeight
break
}
case 'horizontal-tb': {
if (!i) fx = 0
const {
fontBoundingBoxAscent,
fontBoundingBoxDescent,
actualBoundingBoxAscent,
actualBoundingBoxDescent,
actualBoundingBoxLeft,
actualBoundingBoxRight,
width: fWidth,
} = canvasMeasureText(f.content, {
...fStyle,
textAlign: 'center',
verticalAlign: 'baseline',
})
const fHeight = fStyle.fontSize
const fLineHeight = fHeight * fStyle.lineHeight
const baseline = fy
+ (fLineHeight - (fontBoundingBoxAscent + fontBoundingBoxDescent)) / 2
+ fontBoundingBoxAscent
f.contentBox.x = fx
f.contentBox.y = fy + (fLineHeight - fHeight) / 2
f.contentBox.width = fWidth
f.contentBox.height = fHeight
f.inlineBox.x = fx
f.inlineBox.y = fy
f.inlineBox.width = fWidth
f.inlineBox.height = fLineHeight
f.glyphBox.x = fx
f.glyphBox.y = baseline - actualBoundingBoxAscent
f.glyphBox.width = actualBoundingBoxLeft + actualBoundingBoxRight
f.glyphBox.height = actualBoundingBoxAscent + actualBoundingBoxDescent
f.baseline = baseline
f.centerX = fx + actualBoundingBoxLeft
contentBoxes.push(f.contentBox)
p.contentBox = BoundingBox.from(...contentBoxes)
if (p.contentBox.height < f.contentBox.height) highestF = f
fx += fWidth
break
}
}
})

p.lineBox = BoundingBox.from(...p.fragments.map(f => f.inlineBox))
px += p.lineBox.width
py += p.lineBox.height
const xMetrics = canvasMeasureText('x', {
...(highestF ?? p).getComputedStyle(),
textAlign: 'left',
verticalAlign: 'baseline',
})
const xFontHeight = xMetrics.fontBoundingBoxAscent + xMetrics.fontBoundingBoxDescent
p.xHeight = xMetrics.actualBoundingBoxAscent

p.lineBox = BoundingBox.from(...p.fragments.map(f => f.inlineBox))
p.baseline = p.lineBox.y
+ (p.lineBox.height - xFontHeight) / 2
+ (p.lineBox.height - (xMetrics.fontBoundingBoxAscent + xMetrics.fontBoundingBoxDescent)) / 2
+ xMetrics.fontBoundingBoxAscent
py += p.lineBox.height
})

const box = BoundingBox.from(
...paragraphs.map(p => p.lineBox),
new BoundingBox({ width: userWidth, height: userHeight }),
)
const { width, height } = box

// align
paragraphs.forEach(p => {
p.contentBox = BoundingBox.from(...p.fragments.map(f => f.contentBox))
p.glyphBox = BoundingBox.from(...p.fragments.map(f => f.glyphBox))
p.fragments.forEach(f => {
const fStyle = f.getComputedStyle()
const oldX = f.inlineBox.x
const oldY = f.inlineBox.y

let newX
let newX = oldX
let newY = oldY
switch (fStyle.textAlign) {
case 'end':
case 'right':
newX = oldX + (p.lineBox.width - p.contentBox.width)
break
case 'center':
newX = oldX + (p.lineBox.width - p.contentBox.width) / 2
break
case 'start':
case 'left':
default:
newX = oldX + p.lineBox.x
break
}

switch (fStyle.verticalAlign) {
case 'top':
newY = oldY + (p.lineBox.y - f.inlineBox.y)
break
case 'middle':
newY = (p.baseline - p.xHeight / 2) - f.inlineBox.height / 2
break
case 'bottom':
newY = oldY + (p.lineBox.bottom - f.inlineBox.bottom)
break
case 'sub':
newY = oldY + (p.baseline - f.glyphBox.bottom)
break
case 'super':
newY = oldY + (p.baseline - f.glyphBox.y)
break
case 'text-top':
newY = oldY + (p.glyphBox.y - f.inlineBox.y)
break
case 'text-bottom':
newY = oldY + (p.glyphBox.bottom - f.inlineBox.bottom)
switch (fStyle.writingMode) {
case 'vertical-rl':
case 'vertical-lr':
switch (fStyle.textAlign) {
case 'end':
case 'right':
newY += height - p.contentBox.height
break
case 'center':
newY += (height - p.contentBox.height) / 2
break
}
break
case 'baseline':
default:
if (f.inlineBox.height < p.lineBox.height) {
newY = oldY + (p.baseline - f.baseline)
case 'horizontal-tb': {
switch (fStyle.textAlign) {
case 'end':
case 'right':
newX += (width - p.contentBox.width)
break
case 'center':
newX += (width - p.contentBox.width) / 2
break
}
switch (fStyle.verticalAlign) {
case 'top':
newY += p.lineBox.y - f.inlineBox.y
break
case 'middle':
newY = (p.baseline - p.xHeight / 2) - f.inlineBox.height / 2
break
case 'bottom':
newY += p.lineBox.bottom - f.inlineBox.bottom
break
case 'sub':
newY += p.baseline - f.glyphBox.bottom
break
case 'super':
newY += p.baseline - f.glyphBox.y
break
case 'text-top':
newY += p.glyphBox.y - f.inlineBox.y
break
case 'text-bottom':
newY += p.glyphBox.bottom - f.inlineBox.bottom
break
case 'baseline':
default:
if (f.inlineBox.height < p.lineBox.height) {
newY += p.baseline - f.baseline
}
break
}
break
}
}

const diffX = newX - oldX
Expand All @@ -173,39 +227,16 @@ export function measureText(options: MeasureTextOptions) {
f.baseline += diffY
f.centerX += diffX
})
})

const verticalRlWidth = paragraphs.reduce((w, p) => w + p.maxCharWidth * p.getComputedStyle().lineHeight, 0)

// vertical writing mode
paragraphs.forEach(p => {
p.fragments.forEach(f => {
const fStyle = f.getComputedStyle()
if (fStyle.writingMode === 'horizontal-tb') return
const vw = p.maxCharWidth * fStyle.lineHeight
const vh = f.content.length * fStyle.fontSize
+ (f.content.length - 1) * fStyle.letterSpacing
switch (fStyle.writingMode) {
case 'vertical-rl':
f.contentBox.flipVerticalRl(vw, vh, verticalRlWidth)
f.inlineBox.flipVerticalRl(vw, vh, verticalRlWidth)
f.glyphBox.flipVerticalRl(vw, vh, verticalRlWidth)
break
case 'vertical-lr':
f.contentBox.flipVerticalLr(vw, vh)
f.inlineBox.flipVerticalLr(vw, vh)
f.glyphBox.flipVerticalLr(vw, vh)
break
}
})
p.contentBox = BoundingBox.from(...p.fragments.map(f => f.contentBox))
p.lineBox = BoundingBox.from(...p.fragments.map(f => f.inlineBox))
p.glyphBox = BoundingBox.from(...p.fragments.map(f => f.glyphBox))
})

const contentBox = BoundingBox.from(...paragraphs.map(p => p.contentBox))

return {
actualContentBox: BoundingBox.from(...paragraphs.map(p => p.contentBox)),
contentBox: BoundingBox.from(...paragraphs.map(p => p.lineBox)),
box,
contentBox,
viewBox: BoundingBox.from(box, contentBox),
glyphBox: BoundingBox.from(...paragraphs.map(p => p.glyphBox)),
paragraphs,
}
Expand Down
31 changes: 16 additions & 15 deletions src/render-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,27 @@ export function renderText(options: RenderTextOptions) {
pixelRatio = 1,
} = options
const style = { ...rawStyle }
let { width = 0, height = 0 } = style
const { contentBox, paragraphs } = measureText(options)
if (!width) width = contentBox.width
if (!height) height = contentBox.height
const { box, viewBox, paragraphs } = measureText(options)
const { x, y, width, height } = viewBox
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
canvas.style.width = `${ width }px`
canvas.style.height = `${ height }px`
canvas.dataset.width = String(width)
canvas.dataset.height = String(height)
canvas.width = Math.max(1, Math.floor(width * pixelRatio))
canvas.height = Math.max(1, Math.floor(height * pixelRatio))
const canvasWidth = -x + width
const canvasHeight = -y + height
canvas.style.width = `${ canvasWidth }px`
canvas.style.height = `${ canvasHeight }px`
canvas.dataset.width = String(box.width)
canvas.dataset.height = String(box.height)
canvas.dataset.pixelRatio = String(pixelRatio)
canvas.width = Math.max(1, Math.floor(canvasWidth * pixelRatio))
canvas.height = Math.max(1, Math.floor(canvasHeight * pixelRatio))
ctx.scale(pixelRatio, pixelRatio)
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.translate(-x, -y)

const box = new BoundingBox({ width, height })

if (style?.color) style.color = parseColor(ctx, style.color, box)
if (style?.backgroundColor) style.backgroundColor = parseColor(ctx, style.backgroundColor, box)
if (style?.textStrokeColor) style.textStrokeColor = parseColor(ctx, style.textStrokeColor, box)
const colorBox = new BoundingBox({ width, height })
if (style?.color) style.color = parseColor(ctx, style.color, colorBox)
if (style?.backgroundColor) style.backgroundColor = parseColor(ctx, style.backgroundColor, colorBox)
if (style?.textStrokeColor) style.textStrokeColor = parseColor(ctx, style.textStrokeColor, colorBox)
if (style?.backgroundColor) {
ctx.fillStyle = style.backgroundColor
ctx.fillRect(0, 0, canvas.width, canvas.height)
Expand Down

0 comments on commit db99dbc

Please sign in to comment.