From ff928ab9f515a9cc64692d7366ea63aea4869b58 Mon Sep 17 00:00:00 2001 From: wxm <157215725@qq.com> Date: Thu, 21 Nov 2024 17:47:52 +0800 Subject: [PATCH] feat: revert highlightReferImage --- src/Text.ts | 6 +- src/canvas/drawPath.ts | 10 +- src/plugins/highlight.ts | 273 +++++++++++++-------------------- src/plugins/listStyle.ts | 54 ++----- src/types.ts | 10 +- src/utils.ts | 86 ++++++++++- test/fixtures/highlight.2.json | 5 +- 7 files changed, 219 insertions(+), 225 deletions(-) diff --git a/src/Text.ts b/src/Text.ts index cd90a0b..f736925 100644 --- a/src/Text.ts +++ b/src/Text.ts @@ -48,16 +48,16 @@ export const defaultTextStyles: TextStyle = { // listStyle listStyleType: 'none', listStyleImage: 'none', - listStyleImageColors: {}, + listStyleColormap: 'none', listStyleSize: 'cover', listStylePosition: 'outside', // highlight highlightImage: 'none', - highlightImageColors: {}, + highlightReferImage: 'none', + highlightColormap: 'none', highlightLine: 'none', highlightSize: 'cover', highlightThickness: '100%', - highlightOverflow: 'none', // shadow shadowColor: 'rgba(0, 0, 0, 0)', shadowOffsetX: 0, diff --git a/src/canvas/drawPath.ts b/src/canvas/drawPath.ts index bc31a3b..f9f60e6 100644 --- a/src/canvas/drawPath.ts +++ b/src/canvas/drawPath.ts @@ -1,15 +1,14 @@ -import type { BoundingBox, Path2D } from 'modern-path2d' +import type { Path2D } from 'modern-path2d' import type { TextStyle } from '../types' export interface DrawShapePathsOptions extends Partial { ctx: CanvasRenderingContext2D path: Path2D fontSize: number - clipRect?: BoundingBox } export function drawPath(options: DrawShapePathsOptions): void { - const { ctx, path, fontSize, clipRect } = options + const { ctx, path, fontSize } = options ctx.save() ctx.beginPath() @@ -26,11 +25,6 @@ export function drawPath(options: DrawShapePathsOptions): void { shadowBlur: (options.shadowBlur ?? 0) * fontSize, shadowColor: options.shadowColor, } - if (clipRect) { - ctx.rect(clipRect.left, clipRect.top, clipRect.width, clipRect.height) - ctx.clip() - ctx.beginPath() - } path.drawTo(ctx, style) ctx.restore() } diff --git a/src/plugins/highlight.ts b/src/plugins/highlight.ts index 0979a30..c419b2e 100644 --- a/src/plugins/highlight.ts +++ b/src/plugins/highlight.ts @@ -1,53 +1,13 @@ import type { Path2D } from 'modern-path2d' import type { Character } from '../content' -import type { HighlightLine, HighlightSize, HighlightThickness, TextPlugin, TextStyle } from '../types' +import type { HighlightLine, TextPlugin, TextStyle } from '../types' import { BoundingBox, getPathsBoundingBox, Matrix3, parseSvg, parseSvgToDom } from 'modern-path2d' import { drawPath } from '../canvas' import { definePlugin } from '../definePlugin' -import { hexToRgb, isEqualObject, isNone } from '../utils' - -function parseCharsPerRepeat(size: HighlightSize, fontSize: number, total: number): number { - if (size === 'cover') { - return 0 - } - else if (typeof size === 'string') { - if (size.endsWith('%')) { - const rate = Number(size.substring(0, size.length - 1)) / 100 - return Math.ceil(rate * total / fontSize) - } - else if (size.endsWith('rem')) { - return Number(size.substring(0, size.length - 3)) - } - else { - return Math.ceil(Number(size) / fontSize) - } - } - else { - return Math.ceil(size / fontSize) - } -} - -function parseThickness(thickness: HighlightThickness, fontSize: number, total: number): number { - if (typeof thickness === 'string') { - if (thickness.endsWith('%')) { - return Number(thickness.substring(0, thickness.length - 1)) / 100 - } - else if (thickness.endsWith('rem')) { - const value = Number(thickness.substring(0, thickness.length - 3)) - return (value * fontSize) / total - } - else { - return Number(thickness) / total - } - } - else { - return thickness / total - } -} +import { closestDivisor, isEqualValue, isNone, parseColormap, parseValueNumber } from '../utils' export function highlight(): TextPlugin { const paths: Path2D[] = [] - const clipRects: (BoundingBox | undefined)[] = [] const svgStringToSvgPaths = new Map() function getPaths(svg: string): { dom: SVGElement, paths: Path2D[] } { @@ -76,12 +36,11 @@ export function highlight(): TextPlugin { style.highlightSize !== '1rem' && ( !prevStyle || ( - prevStyle.highlightImage === style.highlightImage - && isEqualObject(prevStyle.highlightImageColors, style.highlightImageColors) - && prevStyle.highlightLine === style.highlightLine - && prevStyle.highlightSize === style.highlightSize - && prevStyle.highlightThickness === style.highlightThickness - && prevStyle.highlightOverflow === style.highlightOverflow + isEqualValue(prevStyle.highlightImage, style.highlightImage) + && isEqualValue(prevStyle.highlightColormap, style.highlightColormap) + && isEqualValue(prevStyle.highlightLine, style.highlightLine) + && isEqualValue(prevStyle.highlightSize, style.highlightSize) + && isEqualValue(prevStyle.highlightThickness, style.highlightThickness) ) ) && group?.length @@ -105,126 +64,117 @@ export function highlight(): TextPlugin { groups .filter(characters => characters.length) - .map((characters) => { + .forEach((characters) => { const char = characters[0]! - return { - char, - groupBox: BoundingBox.from(...characters.map(c => c.glyphBox!)), - } - }) - .forEach((group) => { - const { char, groupBox } = group + const groupBox = BoundingBox.from(...characters.map(c => c.glyphBox!)) const { computedStyle: style } = char const { fontSize, writingMode, - highlightThickness, - highlightSize, - highlightLine, - highlightOverflow, highlightImage, - highlightImageColors, + highlightReferImage, + highlightColormap, + highlightLine, + highlightSize, + highlightThickness, } = style const isVertical = writingMode.includes('vertical') - const thickness = parseThickness(highlightThickness, fontSize, groupBox.width) - const charsPerRepeat = parseCharsPerRepeat(highlightSize, fontSize, groupBox.width) - const overflow = isNone(highlightOverflow) - ? charsPerRepeat ? 'hidden' : 'visible' - : highlightOverflow - const colors = Object.keys(highlightImageColors).reduce((obj, key) => { - let value = highlightImageColors[key] - const keyRgb = hexToRgb(key) - const valueRgb = hexToRgb(value) - if (keyRgb) { - key = keyRgb - } - if (valueRgb) { - value = valueRgb - } - obj[key] = value - return obj - }, {} as Record) + const thickness = parseValueNumber(highlightThickness, { fontSize, total: groupBox.width }) / groupBox.width + const colormap = parseColormap(highlightColormap) const { paths: svgPaths, dom: svgDom } = getPaths(highlightImage) const aBox = getPathsBoundingBox(svgPaths, true)! const styleScale = fontSize / aBox.width * 2 const cBox = new BoundingBox().copy(groupBox) - cBox.width = charsPerRepeat - ? (fontSize * charsPerRepeat) - : isVertical ? groupBox.height : groupBox.width - cBox.height = isVertical ? groupBox.width : groupBox.height - const width = isVertical ? cBox.height : cBox.width + if (isVertical) { + cBox.width = groupBox.height + cBox.height = groupBox.width + cBox.left = groupBox.left + groupBox.width + } + const rawWidth = Math.floor(cBox.width) + let userWidth = rawWidth + if (highlightSize !== 'cover') { + userWidth = parseValueNumber(highlightSize, { fontSize, total: groupBox.width }) + userWidth = closestDivisor(rawWidth, userWidth) + cBox.width = userWidth + } - let line: Omit - if (isNone(highlightLine)) { - if (aBox.width / aBox.height > 4) { - line = 'underline' - const viewBox = svgDom.getAttribute('viewBox') - if (viewBox) { - const [_x, y, _w, h] = viewBox.split(' ').map(v => Number(v)) - const viewCenter = y + h / 2 - if (aBox.y < viewCenter && aBox.y + aBox.height > viewCenter) { - line = 'line-through' - } - else if (aBox.y + aBox.height < viewCenter) { - line = 'overline' - } - else { - line = 'underline' + if (!isNone(highlightReferImage) && isNone(highlightLine)) { + const bBox = getPathsBoundingBox(getPaths(highlightReferImage).paths, true)! + aBox.copy(bBox) + } + else { + let line: Omit + if (isNone(highlightLine)) { + if (aBox.width / aBox.height > 4) { + line = 'underline' + const viewBox = svgDom.getAttribute('viewBox') + if (viewBox) { + const [_x, y, _w, h] = viewBox.split(' ').map(v => Number(v)) + const viewCenter = y + h / 2 + if (aBox.y < viewCenter && aBox.y + aBox.height > viewCenter) { + line = 'line-through' + } + else if (aBox.y + aBox.height < viewCenter) { + line = 'overline' + } + else { + line = 'underline' + } } } + else { + line = 'outline' + } } else { - line = 'outline' + line = highlightLine } - } - else { - line = highlightLine - } - switch (line) { - case 'outline': { - const paddingX = cBox.width * 0.2 - const paddingY = cBox.height * 0.2 - cBox.width += paddingX - cBox.height += paddingY - if (isVertical) { - cBox.x -= paddingY / 2 - cBox.y -= paddingX / 2 - cBox.x += cBox.height - } - else { - cBox.x -= paddingX / 2 - cBox.y -= paddingY / 2 + switch (line) { + case 'outline': { + const paddingX = cBox.width * 0.2 + const paddingY = cBox.height * 0.2 + cBox.width += paddingX + cBox.height += paddingY + if (isVertical) { + cBox.x -= paddingY / 2 + cBox.y -= paddingX / 2 + cBox.x += cBox.height + } + else { + cBox.x -= paddingX / 2 + cBox.y -= paddingY / 2 + } + break } - break + case 'overline': + cBox.height = aBox.height * styleScale + if (isVertical) { + cBox.x = char.inlineBox.left + char.inlineBox.width + } + else { + cBox.y = char.inlineBox.top + } + break + case 'line-through': + cBox.height = aBox.height * styleScale + if (isVertical) { + cBox.x = char.inlineBox.left + char.inlineBox.width - char.strikeoutPosition + cBox.height / 2 + } + else { + cBox.y = char.inlineBox.top + char.strikeoutPosition - cBox.height / 2 + } + break + case 'underline': + cBox.height = aBox.height * styleScale + if (isVertical) { + cBox.x = char.inlineBox.left + char.inlineBox.width - char.underlinePosition + } + else { + cBox.y = char.inlineBox.top + char.underlinePosition + } + break } - case 'overline': - cBox.height = aBox.height * styleScale - if (isVertical) { - cBox.x = char.inlineBox.left + char.inlineBox.width - } - else { - cBox.y = char.inlineBox.top - } - break - case 'line-through': - cBox.height = aBox.height * styleScale - if (isVertical) { - cBox.x = char.inlineBox.left + char.inlineBox.width - char.strikeoutPosition + cBox.height / 2 - } - else { - cBox.y = char.inlineBox.top + char.strikeoutPosition - cBox.height / 2 - } - break - case 'underline': - cBox.height = aBox.height * styleScale - if (isVertical) { - cBox.x = char.inlineBox.left + char.inlineBox.width - char.underlinePosition - } - else { - cBox.y = char.inlineBox.top + char.underlinePosition - } - break } const transform = new Matrix3() @@ -235,8 +185,14 @@ export function highlight(): TextPlugin { } transform.translate(cBox.x, cBox.y) - for (let i = 0, len = Math.ceil(groupBox.width / width); i < len; i++) { - const _transform = transform.clone().translate(i * width, 0) + for (let i = 0, len = rawWidth / userWidth; i < len; i++) { + const _transform = transform.clone() + if (isVertical) { + _transform.translate(0, i * cBox.width) + } + else { + _transform.translate(i * cBox.width, 0) + } svgPaths.forEach((originalPath) => { const path = originalPath.clone().matrix(_transform) if (path.style.strokeWidth) { @@ -251,32 +207,23 @@ export function highlight(): TextPlugin { if (path.style.strokeDasharray) { path.style.strokeDasharray = path.style.strokeDasharray.map(v => v * styleScale) } - if (path.style.fill && (path.style.fill as string) in colors) { - path.style.fill = colors[path.style.fill as string] + if (path.style.fill && (path.style.fill as string) in colormap) { + path.style.fill = colormap[path.style.fill as string] } - if (path.style.stroke && (path.style.stroke as string) in colors) { - path.style.stroke = colors[path.style.stroke as string] + if (path.style.stroke && (path.style.stroke as string) in colormap) { + path.style.stroke = colormap[path.style.stroke as string] } paths.push(path) - clipRects[paths.length - 1] = overflow === 'hidden' - ? new BoundingBox( - groupBox.left, - groupBox.top - groupBox.height, - groupBox.width, - groupBox.height * 3, - ) - : undefined }) } }) }, renderOrder: -1, render: (ctx, text) => { - paths.forEach((path, index) => { + paths.forEach((path) => { drawPath({ ctx, path, - clipRect: clipRects[index], fontSize: text.computedStyle.fontSize, }) diff --git a/src/plugins/listStyle.ts b/src/plugins/listStyle.ts index ed1ca82..c88136a 100644 --- a/src/plugins/listStyle.ts +++ b/src/plugins/listStyle.ts @@ -1,29 +1,8 @@ import type { Path2D } from 'modern-path2d' -import type { ListStyleSize, TextPlugin } from '../types' +import type { TextPlugin } from '../types' import { getPathsBoundingBox, Matrix3, parseSvg } from 'modern-path2d' import { definePlugin } from '../definePlugin' -import { hexToRgb, isNone } from '../utils' - -function parseScale(size: ListStyleSize, fontSize: number, total: number): number { - if (size === 'cover') { - return 1 - } - else if (typeof size === 'string') { - if (size.endsWith('%')) { - return Number(size.substring(0, size.length - 1)) / 100 - } - else if (size.endsWith('rem')) { - const value = Number(size.substring(0, size.length - 3)) - return value * fontSize / total - } - else { - return Number(size) / total - } - } - else { - return size / total - } -} +import { isNone, parseColormap, parseValueNumber } from '../utils' export function listStyle(): TextPlugin { const paths: Path2D[] = [] @@ -36,20 +15,8 @@ export function listStyle(): TextPlugin { const padding = fontSize * 0.45 paragraphs.forEach((paragraph) => { const { computedStyle: style } = paragraph - const { listStyleImage, listStyleImageColors, listStyleSize, listStyleType, color } = style - const colors = Object.keys(listStyleImageColors).reduce((obj, key) => { - let value = listStyleImageColors[key] - const keyRgb = hexToRgb(key) - const valueRgb = hexToRgb(value) - if (keyRgb) { - key = keyRgb - } - if (valueRgb) { - value = valueRgb - } - obj[key] = value - return obj - }, {} as Record) + const { listStyleImage, listStyleColormap, listStyleSize, listStyleType, color } = style + const colormap = parseColormap(listStyleColormap) let size = listStyleSize let image: string | undefined if (!isNone(listStyleImage)) { @@ -74,9 +41,11 @@ export function listStyle(): TextPlugin { const box = paragraph.lineBox const fBox = paragraph.fragments[0].inlineBox if (fBox) { + const scale = size === 'cover' + ? 1 + : parseValueNumber(size, { total: fontSize, fontSize }) / fontSize const m = new Matrix3() if (isVertical) { - const scale = parseScale(size, fontSize, fontSize) const reScale = (fontSize / imageBox.height) * scale m.translate(-imageBox.left, -imageBox.top) m.rotate(Math.PI / 2) @@ -85,7 +54,6 @@ export function listStyle(): TextPlugin { m.translate(box.left + (box.width - fontSize) / 2, fBox.top - padding) } else { - const scale = parseScale(size, fontSize, fontSize) const reScale = (fontSize / imageBox.height) * scale m.translate(-imageBox.left, -imageBox.top) m.translate(-imageBox.width, 0) @@ -96,11 +64,11 @@ export function listStyle(): TextPlugin { paths.push(...imagePaths.map((p) => { const path = p.clone() path.matrix(m) - if (path.style.fill && (path.style.fill as string) in colors) { - path.style.fill = colors[path.style.fill as string] + if (path.style.fill && (path.style.fill as string) in colormap) { + path.style.fill = colormap[path.style.fill as string] } - if (path.style.stroke && (path.style.stroke as string) in colors) { - path.style.stroke = colors[path.style.stroke as string] + if (path.style.stroke && (path.style.stroke as string) in colormap) { + path.style.stroke = colormap[path.style.stroke as string] } return path })) diff --git a/src/types.ts b/src/types.ts index 6049b1b..0492448 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,13 +15,15 @@ export type TextTransform = 'none' | 'uppercase' | 'lowercase' export type TextDecorationLine = 'none' | 'underline' | 'line-through' | 'overline' export type ListStyleType = 'none' | 'disc' /* 'decimal' | 'circle' | 'square' | 'georgian' | 'trad-chinese-informal' | 'kannada' */ export type ListStyleImage = 'none' | string +export type ListStyleColormap = 'none' | Record export type ListStyleSize = 'cover' | Sizeable export type ListStylePosition = 'inside' | 'outside' export type HighlightLine = TextDecorationLine | 'outline' export type HighlightImage = 'none' | string +export type HighlightReferImage = 'none' | string +export type HighlightColormap = 'none' | Record export type HighlightSize = 'cover' | Sizeable export type HighlightThickness = Sizeable -export type HighlightOverflow = 'none' | 'visible' | 'hidden' export interface TextLineStyle { writingMode: WritingMode @@ -31,7 +33,7 @@ export interface TextLineStyle { lineHeight: number listStyleType: ListStyleType listStyleImage: ListStyleImage - listStyleImageColors: Record + listStyleColormap: ListStyleColormap listStyleSize: ListStyleSize listStylePosition: ListStylePosition } @@ -49,11 +51,11 @@ export interface TextInlineStyle { textDecoration: TextDecorationLine // extended part highlightImage: HighlightImage - highlightImageColors: Record + highlightReferImage: HighlightReferImage + highlightColormap: HighlightColormap highlightLine: HighlightLine highlightSize: HighlightSize highlightThickness: HighlightThickness - highlightOverflow: HighlightOverflow } export interface TextDrawStyle { diff --git a/src/utils.ts b/src/utils.ts index 9c8a367..9a31746 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,49 @@ -export function isNone(val: string | undefined): boolean { +interface ValueContext { + total: number + fontSize: number +} + +export function parseValueNumber(value: string | number, ctx: ValueContext): number { + if (typeof value === 'number') { + return value + } + else { + if (value.endsWith('%')) { + value = value.substring(0, value.length - 1) + return Math.ceil(Number(value) / 100 * ctx.total) + } + else if (value.endsWith('rem')) { + value = value.substring(0, value.length - 3) + return Number(value) * ctx.fontSize + } + else if (value.endsWith('em')) { + value = value.substring(0, value.length - 2) + return Number(value) * ctx.fontSize + } + else { + return Number(value) + } + } +} + +export function parseColormap(colormap: 'none' | Record): Record { + const _colormap = (isNone(colormap) ? {} : colormap) as Record + return Object.keys(_colormap).reduce((obj, key) => { + let value = _colormap[key] + const keyRgb = hexToRgb(key) + const valueRgb = hexToRgb(value) + if (keyRgb) { + key = keyRgb + } + if (valueRgb) { + value = valueRgb + } + obj[key] = value + return obj + }, {} as Record) +} + +export function isNone(val: any): boolean { return !val || val === 'none' } @@ -6,7 +51,19 @@ export function isEqualObject(obj1: Record, obj2: Record obj1[key] === obj2[key]) + return keys.every(key => isEqualValue(obj1[key], obj2[key])) +} + +export function isEqualValue(val1: any, val2: any): boolean { + const typeof1 = typeof val1 + const typeof2 = typeof val2 + if (typeof1 === typeof2) { + if (typeof1 === 'object') { + return isEqualObject(val1, val2) + } + return val1 === val2 + } + return false } export function hexToRgb(hex: string): string | null { @@ -34,3 +91,28 @@ export function filterEmpty(val: Record | undefined): Record