Skip to content

Commit

Permalink
feat: render emoji with esdt
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoiver committed Jan 13, 2025
1 parent 88fd963 commit 64bc1cb
Show file tree
Hide file tree
Showing 13 changed files with 83 additions and 55 deletions.
23 changes: 21 additions & 2 deletions packages/core/src/drawcalls/SDFText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
SDF_SCALE,
BitmapFont,
GlyphPositions,
containsEmoji,
} from '../utils';

export class SDFText extends Drawcall {
Expand Down Expand Up @@ -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[] = [];
Expand All @@ -86,6 +97,7 @@ export class SDFText extends Drawcall {
allText,
this.device,
esdt,
hasEmoji ? (fill as string) : '',
);
}

Expand Down Expand Up @@ -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();
}

Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/shaders/sdf_text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/utils/emoji.ts
Original file line number Diff line number Diff line change
@@ -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);
}
31 changes: 9 additions & 22 deletions packages/core/src/utils/glyph/glyph-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -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<string, TinySDF> = {};

private textMetricsCache: Record<string, Record<string, number>> = {};

private glyphAtlas: GlyphAtlas;
private glyphMap: Record<string, Record<string, StyleGlyph>> = {};
private glyphAtlasTexture: Texture;
Expand Down Expand Up @@ -160,6 +155,7 @@ export class GlyphManager {
text: string,
device: Device,
esdt: boolean,
fill: string,
) {
let newChars: string[] = [];
if (!this.glyphMap[fontStack]) {
Expand All @@ -185,6 +181,7 @@ export class GlyphManager {
fontStyle,
char,
esdt,
fill,
);
})
.reduce((prev, cur) => {
Expand Down Expand Up @@ -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,
Expand All @@ -266,7 +253,7 @@ export class GlyphManager {
glyphLeft,
glyphTop,
glyphAdvance,
} = sdfGenerator.draw(char, esdt);
} = sdfGenerator.draw(char, esdt, !!fill);

return {
id: char,
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/utils/glyph/sdf-esdt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@ 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++) {
const a = getData(x, y);
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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/utils/glyph/symbol-quad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/utils/glyph/tiny-sdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class TinySDF {
fontFamily = 'sans-serif',
fontWeight = 'normal',
fontStyle = 'normal',
fill = 'black',
} = {}) {
this.buffer = buffer;
this.cutoff = cutoff;
Expand All @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -161,7 +162,7 @@ export class TinySDF {
if (esdt) {
({ data, width, height } = glyphToESDT(
imageData.data,
null,
color ? imageData.data : null,
w,
h,
pad,
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from './rough';
export * from './font';
export * from './glyph';
export * from './bitmap-font';
export * from './emoji';
8 changes: 4 additions & 4 deletions packages/site/docs/components/BitmapFont.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup>
import { Text, Rect, loadBitmapFont } from '@infinite-canvas-tutorial/core';
import '@infinite-canvas-tutorial/ui';
import { useTemplateRef, onMounted } from 'vue';
import { ref, onMounted } from 'vue';
import Stats from 'stats.js';
let canvas;
Expand All @@ -13,10 +13,10 @@ $stats.style.position = 'absolute';
$stats.style.left = '0px';
$stats.style.top = '0px';
const canvasRef = useTemplateRef('canvas');
const wrapper = ref(null);
onMounted(() => {
const $canvas = canvasRef.value;
const $canvas = wrapper.value;
if (!$canvas) return;
Expand Down Expand Up @@ -90,6 +90,6 @@ onMounted(() => {

<template>
<div style="position: relative">
<ic-canvas ref="canvas"></ic-canvas>
<ic-canvas ref="wrapper"></ic-canvas>
</div>
</template>
10 changes: 5 additions & 5 deletions packages/site/docs/components/Emoji.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup>
import { Text } from '@infinite-canvas-tutorial/core';
import '@infinite-canvas-tutorial/ui';
import { useTemplateRef, onMounted } from 'vue';
import { ref, onMounted } from 'vue';
import Stats from 'stats.js';
let canvas;
Expand All @@ -13,10 +13,10 @@ $stats.style.position = 'absolute';
$stats.style.left = '0px';
$stats.style.top = '0px';
const canvasRef = useTemplateRef('canvas');
const wrapper = ref(null);
onMounted(() => {
const $canvas = canvasRef.value;
const $canvas = wrapper.value;
if (!$canvas) return;
Expand All @@ -28,7 +28,7 @@ onMounted(() => {
const text = new Text({
x: 50,
y: 100,
content: '🌹🌍🌞🌛',
content: 'Hello, world! \n🌹🌍🌞🌛',
fontSize: 30,
fill: '#F67676',
});
Expand All @@ -43,6 +43,6 @@ onMounted(() => {

<template>
<div style="position: relative">
<ic-canvas ref="canvas" style="height: 200px"></ic-canvas>
<ic-canvas ref="wrapper" style="height: 200px"></ic-canvas>
</div>
</template>
8 changes: 4 additions & 4 deletions packages/site/docs/components/MSDFText.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup>
import { Text, Rect, loadBitmapFont } from '@infinite-canvas-tutorial/core';
import '@infinite-canvas-tutorial/ui';
import { useTemplateRef, onMounted } from 'vue';
import { ref, onMounted } from 'vue';
import Stats from 'stats.js';
let canvas;
Expand All @@ -13,10 +13,10 @@ $stats.style.position = 'absolute';
$stats.style.left = '0px';
$stats.style.top = '0px';
const canvasRef = useTemplateRef('canvas');
const wrapper = ref(null);
onMounted(() => {
const $canvas = canvasRef.value;
const $canvas = wrapper.value;
if (!$canvas) return;
Expand Down Expand Up @@ -63,6 +63,6 @@ onMounted(() => {

<template>
<div style="position: relative;">
<ic-canvas ref="canvas" style="height: 200px" zoom="250"></ic-canvas>
<ic-canvas ref="wrapper" style="height: 200px" zoom="250"></ic-canvas>
</div>
</template>
8 changes: 4 additions & 4 deletions packages/site/docs/components/SDFText.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup>
import { Text } from '@infinite-canvas-tutorial/core';
import '@infinite-canvas-tutorial/ui';
import { useTemplateRef, onMounted } from 'vue';
import { ref, onMounted } from 'vue';
import Stats from 'stats.js';
let canvas;
Expand All @@ -13,10 +13,10 @@ $stats.style.position = 'absolute';
$stats.style.left = '0px';
$stats.style.top = '0px';
const canvasRef = useTemplateRef('canvas');
const wrapper = ref(null);
onMounted(() => {
const $canvas = canvasRef.value;
const $canvas = wrapper.value;
if (!$canvas) return;
Expand Down Expand Up @@ -55,6 +55,6 @@ onMounted(() => {

<template>
<div style="position: relative">
<ic-canvas ref="canvas"></ic-canvas>
<ic-canvas ref="wrapper"></ic-canvas>
</div>
</template>
Loading

0 comments on commit 64bc1cb

Please sign in to comment.