Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions examples/tests/text-bbcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2025 Comcast Cable Communications Management, LLC.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { ExampleSettings } from '../common/ExampleSettings.js';

export async function automation(settings: ExampleSettings) {
await test(settings);
await settings.snapshot();
}

export default async function test({ renderer, testRoot }: ExampleSettings) {
const fontSize = 28;
const fontFamily = 'Ubuntu';

// Main container
const view = renderer.createNode({
x: 0,
y: 0,
w: 1250,
h: 600,
color: 0xf8f8f8ff,
parent: testRoot,
});

// Title
renderer.createTextNode({
text: 'BBCode Formatting Test - SDF Renderer',
x: 20,
y: 30,
fontSize: 24,
fontFamily,
color: 0x000000ff,
parent: view,
});

// Subtitle
renderer.createTextNode({
text: 'Demonstrating color and style BBCode tags',
x: 20,
y: 60,
fontSize: 16,
fontFamily,
color: 0x666666ff,
parent: view,
});

const examples = [
{
title: 'Color Tags',
text: '[color=red]Red text[/color] [u]and[/u] [color=#00FF00][u]green[/u] text[/color]',
description: 'Named and hex colors',
},
{
title: 'Style Tags',
text: '[b]Bold[/b] [i]Italic[/i] [u]Underline[/u] text',
description: 'Bold, italic, underline',
},
{
title: 'Strikethrough',
text: '[s]Strikethrough text[/s] and normal text',
description: 'Strikethrough style',
},
{
title: 'Nested Tags',
text: '[color=purple][u]Purple Bold[/u][/color] [color=orange][s]Orange Italic[/s][/color]',
description: 'Combined formatting',
},
{
title: 'Mixed Decorations',
text: '[u][s]Under[color=red]lin[/color]e[/s][/u] and [s]strikethrough[/s] text',
description: 'Different text decorations',
},
{
title: 'Complex Mix',
text: 'Hello [color=red]World[/color] [color=blue][s]Strike[/s][/color] [u]Underline[/u]!',
description: 'Multiple mixed tags',
},
];

examples.forEach((example, index) => {
const rowY = 80 + index * 50;

// Create container for BBCode text
const textContainer = renderer.createNode({
x: 30,
y: rowY,
w: 600,
h: 45,
color: 0x0066cc10,
parent: view,
});

// Add a subtle border
renderer.createNode({
x: 0,
y: 0,
color: 0x0066cc40,
w: textContainer.w,
h: 2,
parent: textContainer,
});
renderer.createNode({
x: 0,
y: textContainer.h - 2,
w: textContainer.w,
h: 2,
color: 0x0066cc40,
parent: textContainer,
});

// Title label
renderer.createTextNode({
text: example.title,
x: 650,
y: rowY,
fontSize: 14,
fontFamily,
fontStyle: 'normal',
color: 0x0066ccff,
parent: view,
});

// Description label
renderer.createTextNode({
text: example.description,
x: 650,
y: rowY + 25,
fontSize: 12,
fontFamily,
color: 0x888888ff,
parent: view,
});

// BBCode formatted text
renderer.createTextNode({
text: example.text,
x: 10,
y: 10,
fontSize,
fontFamily,
textRendererOverride: 'sdf',
color: 0x000000ff, // Default text color
parent: textContainer,
});

// // Raw BBCode display (what the user typed)
renderer.createTextNode({
text: `Raw: ${example.text}`,
x: 850,
y: rowY + 45,
fontSize: 10,
fontFamily: 'monospace',
color: 0x666666ff,
maxWidth: 350,
parent: view,
});
});

// Legend section
const legendY = 80 + examples.length * 50 + 20;

renderer.createTextNode({
text: 'Supported BBCode Tags:',
x: 30,
y: legendY,
fontSize: 18,
fontFamily,
color: 0x000000ff,
parent: view,
});

const tags = [
'[color=name] or [color=#hex] - Text color',
'[b] - Bold text (TODO)',
'[i] - Italic text (TODO)',
'[u] - Underlined text',
'[s] - Strikethrough text',
];

tags.forEach((tag, index) => {
renderer.createTextNode({
text: `* ${tag}`,
x: 50,
y: legendY + 20 + index * 22,
fontSize: 15,
color: 0x000000ff,
parent: view,
});
});
}
26 changes: 24 additions & 2 deletions src/core/shaders/webgl/SdfShader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const Sdf: WebGlShaderType<SdfShaderProps> = {
// It will receive data from a buffer
attribute vec2 a_position;
attribute vec2 a_textureCoords;
attribute vec4 a_color;

uniform vec2 u_resolution;
uniform mat3 u_transform;
Expand All @@ -85,6 +86,7 @@ export const Sdf: WebGlShaderType<SdfShaderProps> = {
uniform float u_size;

varying vec2 v_texcoord;
varying vec4 v_color;

void main() {
vec2 scrolledPosition = a_position * u_size - vec2(0, u_scrollY);
Expand All @@ -95,6 +97,7 @@ export const Sdf: WebGlShaderType<SdfShaderProps> = {

gl_Position = vec4(screenSpace, 0.0, 1.0);
v_texcoord = a_textureCoords;
v_color = a_color;

}
`,
Expand All @@ -111,24 +114,43 @@ export const Sdf: WebGlShaderType<SdfShaderProps> = {
uniform int u_debug;

varying vec2 v_texcoord;
varying vec4 v_color;

float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}

void main() {
// Check if this is an underline/strikethrough quad (UV coordinates are very close to 0,0)
if (length(v_texcoord) < 0.001) {
// Render as solid color for underlines/strikethroughs, use uniform color
gl_FragColor = vec4(u_color.r, u_color.g, u_color.b, u_color.a);
return;
}

vec3 sample = texture2D(u_texture, v_texcoord).rgb;
if (u_debug == 1) {
gl_FragColor = vec4(sample.r, sample.g, sample.b, 1.0);
return;
}
float scaledDistRange = u_distanceRange * u_pixelRatio;
float sigDist = scaledDistRange * (median(sample.r, sample.g, sample.b) - 0.5);
float opacity = clamp(sigDist + 0.5, 0.0, 1.0) * u_color.a;
float opacity = clamp(sigDist + 0.5, 0.0, 1.0);

// Check if we should use uniform color or per-vertex color
vec4 finalColor;
if (v_color.r < 0.0) {
finalColor = u_color;
} else {
// Use per-vertex color from BBCode
finalColor = v_color;
}

opacity *= finalColor.a;

// Build the final color.
// IMPORTANT: We must premultiply the color by the alpha value before returning it.
gl_FragColor = vec4(u_color.r * opacity, u_color.g * opacity, u_color.b * opacity, opacity);
gl_FragColor = vec4(finalColor.r * opacity, finalColor.g * opacity, finalColor.b * opacity, opacity);
}
`,
};
15 changes: 11 additions & 4 deletions src/core/text-rendering/CanvasFontHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const fontFamilies: Record<string, FontFace> = {};
const loadedFonts = new Set<string>();
const fontLoadPromises = new Map<string, Promise<void>>();
const normalizedMetrics = new Map<string, NormalizedFontMetrics>();
const nodesWaitingForFont: Record<string, CoreTextNode[]> = Object.create(null);
const nodesWaitingForFont: Record<string, Record<number, CoreTextNode>> = {};
let initialized = false;
let context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;

Expand Down Expand Up @@ -89,7 +89,8 @@ export const loadFont = async (
return existingPromise;
}

const nwff: CoreTextNode[] = (nodesWaitingForFont[fontFamily] = []);
const nwff: Record<number, CoreTextNode> = (nodesWaitingForFont[fontFamily] =
{});
// Create and store the loading promise
const loadPromise = new FontFace(fontFamily, `url(${fontUrl})`)
.load()
Expand All @@ -102,7 +103,10 @@ export const loadFont = async (
setFontMetrics(fontFamily, normalizeMetrics(metrics));
}
for (let key in nwff) {
nwff[key]!.setUpdateType(UpdateType.Local);
const node = nwff[key];
if (node) {
node.setUpdateType(UpdateType.Local);
}
}
delete nodesWaitingForFont[fontFamily];
})
Expand Down Expand Up @@ -169,7 +173,10 @@ export const isFontLoaded = (fontFamily: string): boolean => {
* @param node
*/
export const waitingForFont = (fontFamily: string, node: CoreTextNode) => {
nodesWaitingForFont[fontFamily]![node.id] = node;
if (!nodesWaitingForFont[fontFamily]) {
nodesWaitingForFont[fontFamily] = {};
}
nodesWaitingForFont[fontFamily][node.id] = node;
};

/**
Expand Down
Loading