Skip to content

Commit 02fae1e

Browse files
committed
refactor: move common color logic to color utils
1 parent 717d743 commit 02fae1e

10 files changed

Lines changed: 87 additions & 266 deletions

File tree

log-viewer/src/features/timeline/optimised/RectangleGeometry.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,9 @@ export class RectangleGeometry {
189189
* @param worldY - Top edge Y in world coordinates
190190
* @param worldWidth - Rectangle width in world coordinates
191191
* @param worldHeight - Rectangle height in world coordinates
192-
* @param color - PixiJS color (0xRRGGBB), will be converted to RGBA
192+
* @param color - PixiJS color (0xRRGGBB), will be converted to ABGR
193193
* @param viewport - Viewport transform for coordinate conversion
194+
* @param alpha - Alpha value 0.0–1.0 (default: 1.0, fully opaque)
194195
*/
195196
public writeRectangle(
196197
rectIndex: number,
@@ -200,6 +201,7 @@ export class RectangleGeometry {
200201
worldHeight: number,
201202
color: number,
202203
viewport: ViewportTransform,
204+
alpha: number = 1.0,
203205
): void {
204206
// Calculate buffer offsets (6 vertices per rect)
205207
const positionOffset = rectIndex * VERTICES_PER_RECT * FLOATS_PER_POSITION;
@@ -264,11 +266,11 @@ export class RectangleGeometry {
264266
this.positionData[positionOffset + 11] = clipY1;
265267

266268
// Convert 0xRRGGBB to 0xAABBGGRR (ABGR for little-endian systems)
267-
// Alpha is always 255 (fully opaque) since colors are pre-blended
268269
const r = (color >> 16) & 0xff;
269270
const g = (color >> 8) & 0xff;
270271
const b = color & 0xff;
271-
const packedColor = 0xff000000 | (b << 16) | (g << 8) | r; // ABGR format
272+
const a = (Math.round(alpha * 255) & 0xff) << 24;
273+
const packedColor = a | (b << 16) | (g << 8) | r; // ABGR format
272274

273275
// Write color data (same color for all 6 vertices)
274276
this.colorData[colorOffset] = packedColor;

log-viewer/src/features/timeline/optimised/TextLabelRenderer.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { BitmapFont, BitmapText, Container } from 'pixi.js';
2626
import type { RenderBatch, ViewportState } from '../types/flamechart.types.js';
2727
import { TEXT_LABEL_CONSTANTS, TIMELINE_CONSTANTS } from '../types/flamechart.types.js';
2828
import type { PrecomputedRect } from './RectangleCache.js';
29+
import { isLightBackground } from './rendering/ColorUtils.js';
2930

3031
/**
3132
* TextLabelRenderer
@@ -254,27 +255,12 @@ export class TextLabelRenderer {
254255

255256
/**
256257
* Calculate contrasting text color based on background luminance.
257-
* Uses W3C relative luminance formula for accessibility compliance.
258258
*
259259
* @param bgColor - Background color in PixiJS format (0xRRGGBB)
260260
* @returns Dark text color for light backgrounds, light text color for dark backgrounds
261261
*/
262262
private getContrastingTextColor(bgColor: number): number {
263-
const r = ((bgColor >> 16) & 0xff) / 255;
264-
const g = ((bgColor >> 8) & 0xff) / 255;
265-
const b = (bgColor & 0xff) / 255;
266-
267-
// Apply gamma correction for sRGB
268-
const rLin = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
269-
const gLin = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
270-
const bLin = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
271-
272-
// W3C relative luminance formula
273-
const luminance = 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
274-
275-
// Use dark text for light backgrounds, light text for dark backgrounds
276-
// Threshold of 0.179 corresponds to ~50% perceived brightness
277-
return luminance > 0.179
263+
return isLightBackground(bgColor)
278264
? TEXT_LABEL_CONSTANTS.FONT.LIGHT_THEME_COLOR
279265
: TEXT_LABEL_CONSTANTS.FONT.DARK_THEME_COLOR;
280266
}

log-viewer/src/features/timeline/optimised/minimap/MinimapAxisRenderer.ts

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import { BitmapText, Container, Graphics } from 'pixi.js';
2525

2626
import { formatDuration, TEXT_LABEL_CONSTANTS } from '../../types/flamechart.types.js';
27+
import { parseColorToHex } from '../rendering/ColorUtils.js';
2728
import type { MinimapViewport } from './MinimapViewport.js';
2829

2930
/**
@@ -205,51 +206,18 @@ export class MinimapAxisRenderer {
205206
// Update tick color
206207
const tickColorStr =
207208
computedStyle.getPropertyValue('--vscode-editorLineNumber-foreground').trim() || '#808080';
208-
this.config.tickColor = this.parseColorToHex(tickColorStr);
209+
this.config.tickColor = parseColorToHex(tickColorStr, 0x808080);
209210
this.strokeOptions.color = this.config.tickColor;
210211

211212
// Update label tint color
212-
this.config.labelTint = this.parseColorToHex(tickColorStr);
213+
this.config.labelTint = parseColorToHex(tickColorStr, 0x808080);
213214

214215
// Update existing labels with new tint (BitmapText uses tint for color)
215216
for (const label of this.labelPool) {
216217
label.tint = this.config.labelTint;
217218
}
218219
}
219220

220-
/**
221-
* Parse CSS color string to numeric hex.
222-
*/
223-
private parseColorToHex(cssColor: string): number {
224-
if (!cssColor) {
225-
return 0x808080;
226-
}
227-
228-
if (cssColor.startsWith('#')) {
229-
const hex = cssColor.slice(1);
230-
if (hex.length === 6) {
231-
return parseInt(hex, 16);
232-
}
233-
if (hex.length === 3) {
234-
const r = hex[0]!;
235-
const g = hex[1]!;
236-
const b = hex[2]!;
237-
return parseInt(r + r + g + g + b + b, 16);
238-
}
239-
}
240-
241-
// rgba() fallback
242-
const rgba = cssColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
243-
if (rgba) {
244-
const r = parseInt(rgba[1]!, 10);
245-
const g = parseInt(rgba[2]!, 10);
246-
const b = parseInt(rgba[3]!, 10);
247-
return (r << 16) | (g << 8) | b;
248-
}
249-
250-
return 0x808080;
251-
}
252-
253221
/**
254222
* Destroy renderer and cleanup resources.
255223
*/

log-viewer/src/features/timeline/optimised/rendering/ColorUtils.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
/**
66
* ColorUtils
77
*
8-
* Shared utilities for parsing CSS color strings to PixiJS numeric hex format.
9-
* Used by renderers that need to extract colors from CSS variables.
8+
* Shared color conversion utilities for the timeline renderer.
9+
* Consolidates CSS-to-PixiJS parsing, luminance calculations, greyscale
10+
* conversion, and GPU color format packing into a single module.
1011
*/
1112

1213
/**
@@ -58,3 +59,60 @@ export function parseColorToHex(cssColor: string, defaultColor: number = 0x1e1e1
5859

5960
return defaultColor;
6061
}
62+
63+
/**
64+
* Convert a color to greyscale based on perceived luminance.
65+
* Uses standard luminance formula: 0.299*R + 0.587*G + 0.114*B
66+
* Then applies a dimming factor to match Chrome DevTools appearance.
67+
*
68+
* @param color - PixiJS color (0xRRGGBB)
69+
* @param dimFactor - Dimming multiplier applied after greyscale conversion (default: 0.7)
70+
* @returns Greyscale color (0xRRGGBB)
71+
*/
72+
export function colorToGreyscale(color: number, dimFactor: number = 0.7): number {
73+
const r = (color >> 16) & 0xff;
74+
const g = (color >> 8) & 0xff;
75+
const b = color & 0xff;
76+
77+
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
78+
const dimmed = Math.floor(luminance * dimFactor);
79+
80+
return (dimmed << 16) | (dimmed << 8) | dimmed;
81+
}
82+
83+
/**
84+
* Determine if a background color is "light" using W3C relative luminance.
85+
* Uses sRGB gamma correction for accurate perceptual brightness.
86+
*
87+
* @param bgColor - Background color in PixiJS format (0xRRGGBB)
88+
* @returns true if the background is light (luminance > 0.179)
89+
*/
90+
export function isLightBackground(bgColor: number): boolean {
91+
const r = ((bgColor >> 16) & 0xff) / 255;
92+
const g = ((bgColor >> 8) & 0xff) / 255;
93+
const b = (bgColor & 0xff) / 255;
94+
95+
const rLin = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
96+
const gLin = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
97+
const bLin = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
98+
99+
const luminance = 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
100+
101+
return luminance > 0.179;
102+
}
103+
104+
/**
105+
* Convert 0xRRGGBB color to ABGR packed uint32 for GPU vertex buffers.
106+
* Little-endian systems expect ABGR byte order for correct rendering.
107+
*
108+
* @param color - Color in 0xRRGGBB format
109+
* @param alpha - Alpha value 0.0–1.0 (default: 1.0, fully opaque)
110+
* @returns Packed ABGR uint32
111+
*/
112+
export function rgbToABGR(color: number, alpha: number = 1.0): number {
113+
const r = (color >> 16) & 0xff;
114+
const g = (color >> 8) & 0xff;
115+
const b = color & 0xff;
116+
const a = Math.round(alpha * 255) & 0xff;
117+
return (a << 24) | (b << 16) | (g << 8) | r;
118+
}

log-viewer/src/features/timeline/optimised/rendering/CursorLineRenderer.ts

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import * as PIXI from 'pixi.js';
2020
import type { ViewportState } from '../../types/flamechart.types.js';
21+
import { parseColorToHex } from './ColorUtils.js';
2122

2223
/**
2324
* Cursor line width in pixels.
@@ -113,40 +114,7 @@ export class CursorLineRenderer {
113114
computedStyle.getPropertyValue('--vscode-focusBorder').trim() ||
114115
'#ffffff';
115116

116-
return this.parseColorToHex(colorStr);
117-
}
118-
119-
/**
120-
* Parse CSS color string to numeric hex.
121-
*/
122-
private parseColorToHex(cssColor: string): number {
123-
if (!cssColor) {
124-
return DEFAULT_CURSOR_COLOR;
125-
}
126-
127-
if (cssColor.startsWith('#')) {
128-
const hex = cssColor.slice(1);
129-
if (hex.length === 6) {
130-
return parseInt(hex, 16);
131-
}
132-
if (hex.length === 3) {
133-
const r = hex[0]!;
134-
const g = hex[1]!;
135-
const b = hex[2]!;
136-
return parseInt(r + r + g + g + b + b, 16);
137-
}
138-
}
139-
140-
// rgba() fallback
141-
const rgba = cssColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
142-
if (rgba) {
143-
const r = parseInt(rgba[1]!, 10);
144-
const g = parseInt(rgba[2]!, 10);
145-
const b = parseInt(rgba[3]!, 10);
146-
return (r << 16) | (g << 8) | b;
147-
}
148-
149-
return DEFAULT_CURSOR_COLOR;
117+
return parseColorToHex(colorStr, DEFAULT_CURSOR_COLOR);
150118
}
151119

152120
/**

log-viewer/src/features/timeline/optimised/rendering/HighlightRenderer.ts

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import * as PIXI from 'pixi.js';
1414
import { TIMELINE_CONSTANTS, type ViewportState } from '../../types/flamechart.types.js';
15+
import { parseColorToHex } from './ColorUtils.js';
1516

1617
/**
1718
* Highlight colors with alpha values for true transparency.
@@ -115,53 +116,6 @@ export function extractHighlightColors(): HighlightColors {
115116
computedStyle.getPropertyValue('--vscode-editor-findMatchBackground').trim() || '#ff9632';
116117

117118
return {
118-
sourceColor: parseColorToHex(colorStr),
119+
sourceColor: parseColorToHex(colorStr, 0xea5c00),
119120
};
120121
}
121-
122-
/**
123-
* Parse CSS color string to numeric hex color (RGB only, ignoring alpha).
124-
*
125-
* @param cssColor - CSS color string
126-
* @returns Numeric color (0xRRGGBB)
127-
*/
128-
function parseColorToHex(cssColor: string): number {
129-
if (!cssColor) {
130-
return 0xea5c00; // Default orange
131-
}
132-
133-
if (cssColor.startsWith('#')) {
134-
const hex = cssColor.slice(1);
135-
if (hex.length === 8) {
136-
// #RRGGBBAA - extract RGB, ignore alpha
137-
return parseInt(hex.slice(0, 6), 16);
138-
}
139-
if (hex.length === 6) {
140-
return parseInt(hex, 16);
141-
}
142-
if (hex.length === 4) {
143-
// #RGBA - extract RGB, ignore alpha
144-
const r = hex[0]!;
145-
const g = hex[1]!;
146-
const b = hex[2]!;
147-
return parseInt(r + r + g + g + b + b, 16);
148-
}
149-
if (hex.length === 3) {
150-
const r = hex[0]!;
151-
const g = hex[1]!;
152-
const b = hex[2]!;
153-
return parseInt(r + r + g + g + b + b, 16);
154-
}
155-
}
156-
157-
// rgba() fallback - ignore alpha
158-
const rgba = cssColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d*(?:\.\d+)?))?\)/);
159-
if (rgba) {
160-
const r = parseInt(rgba[1]!, 10);
161-
const g = parseInt(rgba[2]!, 10);
162-
const b = parseInt(rgba[3]!, 10);
163-
return (r << 16) | (g << 8) | b;
164-
}
165-
166-
return 0xea5c00; // Default orange
167-
}

log-viewer/src/features/timeline/optimised/search/MeshSearchStyleRenderer.ts

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { resolveColor } from '../BucketColorResolver.js';
3838
import type { PrecomputedRect } from '../RectangleCache.js';
3939
import { RectangleGeometry, type ViewportTransform } from '../RectangleGeometry.js';
4040
import { createRectangleShader } from '../RectangleShader.js';
41+
import { colorToGreyscale } from '../rendering/ColorUtils.js';
4142

4243
/**
4344
* MeshSearchStyleRenderer
@@ -148,7 +149,7 @@ export class MeshSearchStyleRenderer {
148149
}
149150

150151
const originalColor = batch.color;
151-
const greyColor = this.colorToGreyscale(originalColor);
152+
const greyColor = colorToGreyscale(originalColor);
152153

153154
for (const rect of rectangles) {
154155
// Use original color for matched events, greyscale for non-matched
@@ -275,7 +276,7 @@ export class MeshSearchStyleRenderer {
275276
}).color;
276277
} else {
277278
// No matches - desaturate the bucket's pre-blended color
278-
displayColor = this.colorToGreyscale(bucket.color);
279+
displayColor = colorToGreyscale(bucket.color);
279280
}
280281

281282
this.geometry.writeRectangle(
@@ -293,28 +294,4 @@ export class MeshSearchStyleRenderer {
293294

294295
return rectIndex;
295296
}
296-
297-
/**
298-
* Convert a color to greyscale based on luminance.
299-
* Uses standard luminance formula: 0.299*R + 0.587*G + 0.114*B
300-
* Then applies slight dimming to match Chrome DevTools appearance.
301-
*
302-
* @param color - PixiJS color (0xRRGGBB)
303-
* @returns Greyscale color (0xRRGGBB)
304-
*/
305-
private colorToGreyscale(color: number): number {
306-
// Extract RGB components
307-
const r = (color >> 16) & 0xff;
308-
const g = (color >> 8) & 0xff;
309-
const b = color & 0xff;
310-
311-
// Calculate luminance (perceived brightness)
312-
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
313-
314-
// Apply dimming factor to match Chrome DevTools
315-
const dimmed = Math.floor(luminance * 0.7);
316-
317-
// Create greyscale color (same value for R, G, B)
318-
return (dimmed << 16) | (dimmed << 8) | dimmed;
319-
}
320297
}

0 commit comments

Comments
 (0)