From 64bc1cb30a8c3be44cdcc0179b65c1ecd7b88dc7 Mon Sep 17 00:00:00 2001 From: xiaoiver Date: Mon, 13 Jan 2025 17:35:34 +0800 Subject: [PATCH] feat: render emoji with esdt --- packages/core/src/drawcalls/SDFText.ts | 23 ++++++++++++-- packages/core/src/shaders/sdf_text.ts | 5 ++- packages/core/src/utils/emoji.ts | 11 +++++++ .../core/src/utils/glyph/glyph-manager.ts | 31 ++++++------------- packages/core/src/utils/glyph/sdf-esdt.ts | 10 +++--- packages/core/src/utils/glyph/symbol-quad.ts | 6 ++-- packages/core/src/utils/glyph/tiny-sdf.ts | 9 ++++-- packages/core/src/utils/index.ts | 1 + packages/site/docs/components/BitmapFont.vue | 8 ++--- packages/site/docs/components/Emoji.vue | 10 +++--- packages/site/docs/components/MSDFText.vue | 8 ++--- packages/site/docs/components/SDFText.vue | 8 ++--- .../site/docs/components/WikipediaDatamap.vue | 8 ++--- 13 files changed, 83 insertions(+), 55 deletions(-) create mode 100644 packages/core/src/utils/emoji.ts diff --git a/packages/core/src/drawcalls/SDFText.ts b/packages/core/src/drawcalls/SDFText.ts index b015e0a..bb17ee4 100644 --- a/packages/core/src/drawcalls/SDFText.ts +++ b/packages/core/src/drawcalls/SDFText.ts @@ -28,6 +28,7 @@ import { SDF_SCALE, BitmapFont, GlyphPositions, + containsEmoji, } from '../utils'; export class SDFText extends Drawcall { @@ -62,8 +63,18 @@ export class SDFText extends Drawcall { } createGeometry(): void { - const { metrics, fontFamily, fontWeight, fontStyle, bitmapFont, esdt } = - this.shapes[0] as Text; + const { + metrics, + fontFamily, + fontWeight, + fontStyle, + bitmapFont, + esdt, + content, + fill, + } = this.shapes[0] as Text; + + const hasEmoji = containsEmoji(content); const indices: number[] = []; const positions: number[] = []; @@ -86,6 +97,7 @@ export class SDFText extends Drawcall { allText, this.device, esdt, + hasEmoji ? (fill as string) : '', ); } @@ -201,6 +213,13 @@ export class SDFText extends Drawcall { } } else { defines += '#define USE_SDF\n'; + + const { content } = this.shapes[0] as Text; + const hasEmoji = containsEmoji(content); + if (hasEmoji) { + defines += '#define USE_EMOJI\n'; + } + glyphAtlasTexture = this.#glyphManager.getAtlasTexture(); } diff --git a/packages/core/src/shaders/sdf_text.ts b/packages/core/src/shaders/sdf_text.ts index b724ca6..1da242c 100644 --- a/packages/core/src/shaders/sdf_text.ts +++ b/packages/core/src/shaders/sdf_text.ts @@ -110,7 +110,9 @@ void main() { float dist; lowp float buff; #ifdef USE_SDF - // fillColor = texture(SAMPLER_2D(u_Texture), v_Uv); + #ifdef USE_EMOJI + fillColor = texture(SAMPLER_2D(u_Texture), v_Uv); + #endif dist = texture(SAMPLER_2D(u_Texture), v_Uv).a; buff = (256.0 - 64.0) / 256.0; #endif @@ -132,6 +134,7 @@ void main() { highp float gamma_scaled = fwidth(dist); highp float alpha = smoothstep(buff - gamma_scaled, buff + gamma_scaled, dist); + // alpha = dist; opacity *= alpha; outputColor = fillColor; diff --git a/packages/core/src/utils/emoji.ts b/packages/core/src/utils/emoji.ts new file mode 100644 index 0000000..ba6bf42 --- /dev/null +++ b/packages/core/src/utils/emoji.ts @@ -0,0 +1,11 @@ +/** + * 检测字符串是否包含emoji + * @param str 需要检测的字符串 + * @returns boolean 是否包含emoji + */ +export function containsEmoji(str: string): boolean { + // 匹配emoji的正则表达式 + const emojiRegex = + /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F100}-\u{1F1FF}]|[\u{1F200}-\u{1F2FF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{2B00}-\u{2BFF}]|[\u{2900}-\u{297F}]|[\u{2B00}-\u{2BFF}]|[\u{1F000}-\u{1F02F}]|[\u{1F0A0}-\u{1F0FF}]|[\u{1F100}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F910}-\u{1F96B}]|[\u{1F980}-\u{1F9E0}]/u; + return emojiRegex.test(str); +} diff --git a/packages/core/src/utils/glyph/glyph-manager.ts b/packages/core/src/utils/glyph/glyph-manager.ts index 48bb195..44e7000 100644 --- a/packages/core/src/utils/glyph/glyph-manager.ts +++ b/packages/core/src/utils/glyph/glyph-manager.ts @@ -43,7 +43,7 @@ export type PositionedGlyph = { export const SDF_SCALE = 1; export const BASE_FONT_WIDTH = 24 * SDF_SCALE; export const BASE_FONT_BUFFER = 3 * SDF_SCALE; -export const radius = 8 * SDF_SCALE; +export const RADIUS = 8 * SDF_SCALE; export function getDefaultCharacterSet(): string[] { const charSet = []; @@ -53,14 +53,9 @@ export function getDefaultCharacterSet(): string[] { return charSet; } -/** - * TODO: use one atlas for all fontstacks, each fontstack has one texture now - */ export class GlyphManager { private sdfGeneratorCache: Record = {}; - private textMetricsCache: Record> = {}; - private glyphAtlas: GlyphAtlas; private glyphMap: Record> = {}; private glyphAtlasTexture: Texture; @@ -160,6 +155,7 @@ export class GlyphManager { text: string, device: Device, esdt: boolean, + fill: string, ) { let newChars: string[] = []; if (!this.glyphMap[fontStack]) { @@ -185,6 +181,7 @@ export class GlyphManager { fontStyle, char, esdt, + fill, ); }) .reduce((prev, cur) => { @@ -231,31 +228,21 @@ export class GlyphManager { fontStyle: string, char: string, esdt: boolean, + fill: string, ): StyleGlyph { - let sdfGenerator = this.sdfGeneratorCache[fontStack]; + let sdfGenerator = this.sdfGeneratorCache[fontStack + fill]; if (!sdfGenerator) { - sdfGenerator = this.sdfGeneratorCache[fontStack] = new TinySDF({ + sdfGenerator = this.sdfGeneratorCache[fontStack + fill] = new TinySDF({ fontSize: BASE_FONT_WIDTH, fontFamily, fontWeight, fontStyle, buffer: BASE_FONT_BUFFER, - radius, + radius: RADIUS, + fill, }); } - if (!this.textMetricsCache[fontStack]) { - this.textMetricsCache[fontStack] = {}; - } - - if (!this.textMetricsCache[fontStack][char]) { - // 使用 mapbox/tiny-sdf 中的 context - // @see https://stackoverflow.com/questions/46126565/how-to-get-font-glyphs-metrics-details-in-javascript - this.textMetricsCache[fontStack][char] = - // @ts-ignore - sdfGenerator.ctx.measureText(char).width; - } - // use sdf 2.x @see https://github.com/mapbox/tiny-sdf const { data, @@ -266,7 +253,7 @@ export class GlyphManager { glyphLeft, glyphTop, glyphAdvance, - } = sdfGenerator.draw(char, esdt); + } = sdfGenerator.draw(char, esdt, !!fill); return { id: char, diff --git a/packages/core/src/utils/glyph/sdf-esdt.ts b/packages/core/src/utils/glyph/sdf-esdt.ts index 727f9ac..73610d5 100644 --- a/packages/core/src/utils/glyph/sdf-esdt.ts +++ b/packages/core/src/utils/glyph/sdf-esdt.ts @@ -31,7 +31,9 @@ export const paintIntoStage = ( inner.fill(0, 0, np); // const getData = (x: number, y: number) => data[y * w + x] ?? 0; - const getData = (x: number, y: number) => data[4 * (y * w + x) + 3] ?? 0; + // const getData = (x: number, y: number) => data[4 * (y * w + x) + 3] ?? 0; + const getData = (x: number, y: number) => + (data[4 * (y * w + x) + 3] ?? 0) / 255; for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { @@ -39,7 +41,7 @@ export const paintIntoStage = ( if (!a) continue; const i = (y + pad) * wp + x + pad; - if (a >= 254) { + if (a >= 254 / 255) { // Fix for bad rasterizer rounding data[4 * (y * w + x) + 3] = 255; @@ -417,7 +419,7 @@ export const relaxSubpixelOffsets = ( // Paint original color data into final RGBA (emoji) export const paintIntoRGB = ( image: Uint8Array, - color: Uint8Array | number[], + color: Uint8ClampedArray, xs: Float32Array, ys: Float32Array, w: number, @@ -590,7 +592,7 @@ export const esdt = ( // Convert grayscale or color glyph to SDF using subpixel distance transform export const glyphToESDT = ( data: Uint8ClampedArray, - color: Uint8Array | null, + color: Uint8ClampedArray | null, w: number, h: number, pad: number = 4, diff --git a/packages/core/src/utils/glyph/symbol-quad.ts b/packages/core/src/utils/glyph/symbol-quad.ts index 71a1c0b..12e25f6 100644 --- a/packages/core/src/utils/glyph/symbol-quad.ts +++ b/packages/core/src/utils/glyph/symbol-quad.ts @@ -46,9 +46,11 @@ export function getGlyphQuads( const pixelRatio = 1; const paddedWidth = - (rect.w * positionedGlyph.scale) / (pixelRatio * SDF_SCALE); + (rect.w * positionedGlyph.scale) / + (pixelRatio * (useMSDF ? 1 : SDF_SCALE)); const paddedHeight = - (rect.h * positionedGlyph.scale) / (pixelRatio * SDF_SCALE); + (rect.h * positionedGlyph.scale) / + (pixelRatio * (useMSDF ? 1 : SDF_SCALE)); const x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - diff --git a/packages/core/src/utils/glyph/tiny-sdf.ts b/packages/core/src/utils/glyph/tiny-sdf.ts index 14cb66d..bb3a562 100644 --- a/packages/core/src/utils/glyph/tiny-sdf.ts +++ b/packages/core/src/utils/glyph/tiny-sdf.ts @@ -81,6 +81,7 @@ export class TinySDF { fontFamily = 'sans-serif', fontWeight = 'normal', fontStyle = 'normal', + fill = 'black', } = {}) { this.buffer = buffer; this.cutoff = cutoff; @@ -98,7 +99,7 @@ export class TinySDF { ctx.textBaseline = 'alphabetic'; ctx.textAlign = 'left'; // Necessary so that RTL text doesn't have different alignment - ctx.fillStyle = 'black'; + ctx.fillStyle = fill; } _createCanvas(size: number) { @@ -107,7 +108,7 @@ export class TinySDF { return canvas; } - draw(char: string, esdt = false) { + draw(char: string, esdt = false, color = false) { const { width: glyphAdvance, actualBoundingBoxAscent, @@ -161,7 +162,7 @@ export class TinySDF { if (esdt) { ({ data, width, height } = glyphToESDT( imageData.data, - null, + color ? imageData.data : null, w, h, pad, @@ -257,6 +258,8 @@ export const edt = ( // Helpers export const isBlack = (x: number) => !x; export const isWhite = (x: number) => x === 1; +// export const isBlack = (x: number) => x === 1; +// export const isWhite = (x: number) => !x; export const isSolid = (x: number) => !(x && 1 - x); export const sqr = (x: number) => x * x; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index dec633c..90882f1 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -14,3 +14,4 @@ export * from './rough'; export * from './font'; export * from './glyph'; export * from './bitmap-font'; +export * from './emoji'; diff --git a/packages/site/docs/components/BitmapFont.vue b/packages/site/docs/components/BitmapFont.vue index 53b0f07..10e8c28 100644 --- a/packages/site/docs/components/BitmapFont.vue +++ b/packages/site/docs/components/BitmapFont.vue @@ -1,7 +1,7 @@