From 2af3cad3b710672f4bb8f7b44ec4a8ea2c510d1d Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Fri, 8 May 2026 12:08:01 -0400 Subject: [PATCH] fix: bake node color into canvas text texture to preserve emoji colors CanvasTextRenderer hardcoded fillStyle='white', causing the GPU shader to multiply the texture by node.color as a tint. This crushed native emoji colors to near-black when text color was dark (e.g. light theme). Bake node.color directly into the canvas fillStyle so text glyphs render in the correct color and emoji glyphs retain their native colors. Force the shader tint to white (with worldAlpha) so the texture passes through untinted. Re-render canvas text when color changes. Closes solid-tv/renderer#1 Co-Authored-By: Claude Opus 4.6 --- src/core/CoreTextNode.ts | 23 +++++++++++++++++-- src/core/text-rendering/CanvasTextRenderer.ts | 7 +++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index 26fb968..0e9a1d2 100644 --- a/src/core/CoreTextNode.ts +++ b/src/core/CoreTextNode.ts @@ -6,7 +6,7 @@ import type { TextRenderInfo, SdfVertexCache, } from './text-rendering/TextRenderer.js'; -import { USE_RTT } from '../utils.js'; +import { USE_RTT, premultiplyColorABGR } from '../utils.js'; import { CoreNode, CoreNodeRenderState, @@ -300,8 +300,15 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { return; } - // Canvas renderer: use standard texture rendering via CoreNode + // Canvas renderer: color is baked into the texture to preserve + // native emoji colors. Use white tint with worldAlpha only. if (this._type === 'canvas') { + const white = premultiplyColorABGR(0xffffffff, this.worldAlpha); + this.premultipliedColorTl = + this.premultipliedColorTr = + this.premultipliedColorBl = + this.premultipliedColorBr = + white; super.renderQuads(renderer); return; } @@ -558,6 +565,18 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { } } + override get color(): number { + return this.props.color; + } + + override set color(value: number) { + super.color = value; + if (this._type === 'canvas') { + this._layoutGenerated = false; + this.setUpdateType(UpdateType.Local); + } + } + get forceLoad() { return this.textProps.forceLoad; } diff --git a/src/core/text-rendering/CanvasTextRenderer.ts b/src/core/text-rendering/CanvasTextRenderer.ts index f45c40c..42df42f 100644 --- a/src/core/text-rendering/CanvasTextRenderer.ts +++ b/src/core/text-rendering/CanvasTextRenderer.ts @@ -130,7 +130,12 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => { canvas.width = canvasW; canvas.height = canvasH; - context.fillStyle = 'white'; + const color = props.color ?? 0xffffffff; + const r = (color >>> 24) & 0xff; + const g = (color >>> 16) & 0xff; + const b = (color >>> 8) & 0xff; + const a = color & 0xff; + context.fillStyle = `rgba(${r},${g},${b},${a / 255})`; context.font = font; context.textBaseline = 'hanging';