diff --git a/log-viewer/src/features/timeline/__tests__/markers.test.ts b/log-viewer/src/features/timeline/__tests__/markers.test.ts index e9def0d2..127d18e7 100644 --- a/log-viewer/src/features/timeline/__tests__/markers.test.ts +++ b/log-viewer/src/features/timeline/__tests__/markers.test.ts @@ -19,6 +19,7 @@ class MockSprite { public width = 0; public height = 0; public tint = 0xffffff; + public alpha = 1; public visible = true; public parent: unknown = null; public _zIndex = 0; @@ -67,22 +68,11 @@ jest.mock('pixi.js', () => { }); import * as PIXI from 'pixi.js'; -import { blendWithBackground } from '../optimised/BucketColorResolver.js'; import { TimelineMarkerRenderer } from '../optimised/markers/TimelineMarkerRenderer.js'; import { TimelineViewport } from '../optimised/TimelineViewport.js'; import type { TimelineMarker } from '../types/flamechart.types.js'; import { MARKER_ALPHA, MARKER_COLORS } from '../types/flamechart.types.js'; -/** - * Pre-blended marker colors for testing (matches TimelineMarkerRenderer). - * These are computed once to match the expected render output. - */ -const MARKER_COLORS_BLENDED = { - error: blendWithBackground(MARKER_COLORS.error, MARKER_ALPHA), - skip: blendWithBackground(MARKER_COLORS.skip, MARKER_ALPHA), - unexpected: blendWithBackground(MARKER_COLORS.unexpected, MARKER_ALPHA), -}; - // Mock PIXI.Container class MockContainer { private children: unknown[] = []; @@ -125,7 +115,7 @@ describe('TimelineMarkerRenderer', () => { }); describe('T013: Color Accuracy Verification', () => { - it('should render error markers with pre-blended color via sprite tint', () => { + it('should render error markers with raw color and alpha via sprite tint', () => { const markers: TimelineMarker[] = [ { id: 'marker-error', type: 'error', startTime: 100_000, summary: 'Test error' }, ]; @@ -137,14 +127,15 @@ describe('TimelineMarkerRenderer', () => { ); renderer.render(); - // Find the sprite with error color + // Find the sprite with error color and alpha const errorSprites = createdMockSprites.filter( - (s) => s.visible && s.tint === MARKER_COLORS_BLENDED.error, + (s) => s.visible && s.tint === MARKER_COLORS.error, ); expect(errorSprites.length).toBeGreaterThanOrEqual(1); + expect(errorSprites[0]!.alpha).toBe(MARKER_ALPHA); }); - it('should render skip markers with pre-blended color via sprite tint', () => { + it('should render skip markers with raw color and alpha via sprite tint', () => { const markers: TimelineMarker[] = [ { id: 'marker-skip', type: 'skip', startTime: 100_000, summary: 'Test skip' }, ]; @@ -156,14 +147,15 @@ describe('TimelineMarkerRenderer', () => { ); renderer.render(); - // Find the sprite with skip color + // Find the sprite with skip color and alpha const skipSprites = createdMockSprites.filter( - (s) => s.visible && s.tint === MARKER_COLORS_BLENDED.skip, + (s) => s.visible && s.tint === MARKER_COLORS.skip, ); expect(skipSprites.length).toBeGreaterThanOrEqual(1); + expect(skipSprites[0]!.alpha).toBe(MARKER_ALPHA); }); - it('should render unexpected markers with pre-blended color via sprite tint', () => { + it('should render unexpected markers with raw color and alpha via sprite tint', () => { const markers: TimelineMarker[] = [ { id: 'marker-unexpected', @@ -180,11 +172,12 @@ describe('TimelineMarkerRenderer', () => { ); renderer.render(); - // Find the sprite with unexpected color + // Find the sprite with unexpected color and alpha const unexpectedSprites = createdMockSprites.filter( - (s) => s.visible && s.tint === MARKER_COLORS_BLENDED.unexpected, + (s) => s.visible && s.tint === MARKER_COLORS.unexpected, ); expect(unexpectedSprites.length).toBeGreaterThanOrEqual(1); + expect(unexpectedSprites[0]!.alpha).toBe(MARKER_ALPHA); }); }); @@ -300,7 +293,7 @@ describe('TimelineMarkerRenderer', () => { // First marker should be culled (ends at 200_000 < viewport start 250_000) // Second marker should be visible (200_000 to 1_000_000 overlaps 250_000) expect(visibleSprites.length).toBe(1); - expect(visibleSprites[0]!.tint).toBe(MARKER_COLORS_BLENDED.skip); + expect(visibleSprites[0]!.tint).toBe(MARKER_COLORS.skip); }); it('should cull markers entirely after viewport', () => { @@ -412,11 +405,11 @@ describe('TimelineMarkerRenderer', () => { expect(visibleSprites.length).toBe(3); // First sprite (skip at 100_000) should be leftmost - expect(visibleSprites[0]!.tint).toBe(MARKER_COLORS_BLENDED.skip); + expect(visibleSprites[0]!.tint).toBe(MARKER_COLORS.skip); // Second sprite (unexpected at 200_000) - expect(visibleSprites[1]!.tint).toBe(MARKER_COLORS_BLENDED.unexpected); + expect(visibleSprites[1]!.tint).toBe(MARKER_COLORS.unexpected); // Third sprite (error at 300_000) - expect(visibleSprites[2]!.tint).toBe(MARKER_COLORS_BLENDED.error); + expect(visibleSprites[2]!.tint).toBe(MARKER_COLORS.error); }); }); @@ -580,8 +573,8 @@ describe('TimelineMarkerRenderer', () => { expect(visibleSprites.length).toBe(2); // Verify the new markers are rendered with correct tints - const skipSprites = visibleSprites.filter((s) => s.tint === MARKER_COLORS_BLENDED.skip); - const errorSprites = visibleSprites.filter((s) => s.tint === MARKER_COLORS_BLENDED.error); + const skipSprites = visibleSprites.filter((s) => s.tint === MARKER_COLORS.skip); + const errorSprites = visibleSprites.filter((s) => s.tint === MARKER_COLORS.error); expect(skipSprites.length).toBe(1); expect(errorSprites.length).toBe(1); }); diff --git a/log-viewer/src/features/timeline/optimised/RectangleShader.ts b/log-viewer/src/features/timeline/optimised/RectangleShader.ts index 41de9be1..fc3c38af 100644 --- a/log-viewer/src/features/timeline/optimised/RectangleShader.ts +++ b/log-viewer/src/features/timeline/optimised/RectangleShader.ts @@ -41,8 +41,8 @@ void main() { /** * WebGL fragment shader (GLSL 300 ES) * - * Simply outputs the interpolated vertex color. - * Colors are pre-blended with background, so no alpha blending needed. + * Outputs premultiplied alpha color for correct blending with PixiJS 8's + * default blend mode (GL_ONE, GL_ONE_MINUS_SRC_ALPHA). */ const glslFragment = /* glsl */ `#version 300 es precision highp float; @@ -51,7 +51,7 @@ in vec4 vColor; out vec4 fragColor; void main() { - fragColor = vColor; + fragColor = vec4(vColor.rgb * vColor.a, vColor.a); } `; @@ -83,7 +83,7 @@ fn main(input: VertexInput) -> VertexOutput { /** * WebGPU fragment shader (WGSL) * - * Same functionality as GLSL version for WebGPU renderer. + * Outputs premultiplied alpha color for correct blending. */ const wgslFragment = /* wgsl */ ` struct FragmentInput { @@ -92,7 +92,7 @@ struct FragmentInput { @fragment fn main(input: FragmentInput) -> @location(0) vec4f { - return input.vColor; + return vec4f(input.vColor.rgb * input.vColor.a, input.vColor.a); } `; diff --git a/log-viewer/src/features/timeline/optimised/markers/MarkerProcessor.ts b/log-viewer/src/features/timeline/optimised/markers/MarkerProcessor.ts index 4874cb59..0fa99652 100644 --- a/log-viewer/src/features/timeline/optimised/markers/MarkerProcessor.ts +++ b/log-viewer/src/features/timeline/optimised/markers/MarkerProcessor.ts @@ -10,22 +10,8 @@ * used by both MeshMarkerRenderer and TimelineMarkerRenderer. */ -import type { MarkerType, TimelineMarker } from '../../types/flamechart.types.js'; -import { MARKER_ALPHA, MARKER_COLORS, SEVERITY_RANK } from '../../types/flamechart.types.js'; -import { blendWithBackground } from '../BucketColorResolver.js'; - -/** - * Pre-blended opaque marker colors (MARKER_COLORS blended at MARKER_ALPHA opacity). - * Computed once at module load time for performance. - * - * Using pre-blended colors avoids runtime alpha compositing on the GPU, - * which is more efficient for static opacity values. - */ -export const MARKER_COLORS_BLENDED: Record = { - error: blendWithBackground(MARKER_COLORS.error, MARKER_ALPHA), - skip: blendWithBackground(MARKER_COLORS.skip, MARKER_ALPHA), - unexpected: blendWithBackground(MARKER_COLORS.unexpected, MARKER_ALPHA), -}; +import type { TimelineMarker } from '../../types/flamechart.types.js'; +import { SEVERITY_RANK } from '../../types/flamechart.types.js'; /** * Sort markers by startTime, then by severity (higher severity first for stacking). diff --git a/log-viewer/src/features/timeline/optimised/markers/MeshMarkerRenderer.ts b/log-viewer/src/features/timeline/optimised/markers/MeshMarkerRenderer.ts index 724d3f72..5d7d1f49 100644 --- a/log-viewer/src/features/timeline/optimised/markers/MeshMarkerRenderer.ts +++ b/log-viewer/src/features/timeline/optimised/markers/MeshMarkerRenderer.ts @@ -12,16 +12,17 @@ * - Single Mesh draw call for all markers * - Direct buffer updates (no scene graph overhead) * - Clip-space coordinates (no uniform binding overhead) - * - Pre-blended opaque colors (no alpha blending) + * - True alpha transparency for theme-adaptive colors */ import { Container, Geometry, Mesh, Shader } from 'pixi.js'; import type { TimelineMarker } from '../../types/flamechart.types.js'; +import { MARKER_ALPHA, MARKER_COLORS } from '../../types/flamechart.types.js'; import { RectangleGeometry, type ViewportTransform } from '../RectangleGeometry.js'; import { createRectangleShader } from '../RectangleShader.js'; import type { TimelineViewport } from '../TimelineViewport.js'; import { hitTestMarkers, type MarkerIndicator } from './MarkerHitTest.js'; -import { MARKER_COLORS_BLENDED, sortMarkersByTimeAndSeverity } from './MarkerProcessor.js'; +import { sortMarkersByTimeAndSeverity } from './MarkerProcessor.js'; /** * Renders marker indicators as semi-transparent vertical bands using Mesh. @@ -136,14 +137,13 @@ export class MeshMarkerRenderer { continue; } - // Create indicator record with pre-blended opaque color const indicator: MarkerIndicator = { marker, resolvedEndTime, screenStartX: worldStartX, screenEndX: worldEndX, screenWidth: worldWidth, - color: MARKER_COLORS_BLENDED[marker.type], + color: MARKER_COLORS[marker.type], isVisible: true, }; @@ -193,6 +193,7 @@ export class MeshMarkerRenderer { viewportState.displayHeight, indicator.color, viewportTransform, + MARKER_ALPHA, ); rectIndex++; } diff --git a/log-viewer/src/features/timeline/optimised/markers/TimelineMarkerRenderer.ts b/log-viewer/src/features/timeline/optimised/markers/TimelineMarkerRenderer.ts index b4847807..c39a5b34 100644 --- a/log-viewer/src/features/timeline/optimised/markers/TimelineMarkerRenderer.ts +++ b/log-viewer/src/features/timeline/optimised/markers/TimelineMarkerRenderer.ts @@ -11,27 +11,16 @@ * Performance optimizations: * - Uses SpritePool with shared 1x1 white texture for automatic GPU batching * - Sprites are pooled and reused (no GC overhead after warmup) - * - Color applied via sprite.tint (pre-blended opaque colors) + * - Color applied via sprite.tint with true alpha transparency */ import type { Container } from 'pixi.js'; -import type { MarkerType, TimelineMarker } from '../../types/flamechart.types.js'; +import type { TimelineMarker } from '../../types/flamechart.types.js'; import { MARKER_ALPHA, MARKER_COLORS, SEVERITY_RANK } from '../../types/flamechart.types.js'; -import { blendWithBackground } from '../BucketColorResolver.js'; import { SpritePool } from '../SpritePool.js'; import type { TimelineViewport } from '../TimelineViewport.js'; import { hitTestMarkers, type MarkerIndicator } from './MarkerHitTest.js'; -/** - * Pre-blended opaque marker colors (MARKER_COLORS blended at MARKER_ALPHA opacity). - * Computed once at module load time for performance. - */ -const MARKER_COLORS_BLENDED: Record = { - error: blendWithBackground(MARKER_COLORS.error, MARKER_ALPHA), - skip: blendWithBackground(MARKER_COLORS.skip, MARKER_ALPHA), - unexpected: blendWithBackground(MARKER_COLORS.unexpected, MARKER_ALPHA), -}; - /** * Renders marker indicators as semi-transparent vertical bands using sprites. * @@ -130,14 +119,13 @@ export class TimelineMarkerRenderer { continue; } - // Create indicator record with pre-blended opaque color const indicator: MarkerIndicator = { marker, resolvedEndTime, screenStartX: worldStartX, screenEndX: worldEndX, screenWidth: worldWidth, - color: MARKER_COLORS_BLENDED[marker.type], + color: MARKER_COLORS[marker.type], isVisible: true, }; @@ -161,6 +149,7 @@ export class TimelineMarkerRenderer { sprite.width = gappedWidth; sprite.height = viewportState.displayHeight; sprite.tint = indicator.color; + sprite.alpha = MARKER_ALPHA; } } diff --git a/log-viewer/src/features/timeline/optimised/metric-strip/MetricStripRenderer.ts b/log-viewer/src/features/timeline/optimised/metric-strip/MetricStripRenderer.ts index 83439a4e..7ad37115 100644 --- a/log-viewer/src/features/timeline/optimised/metric-strip/MetricStripRenderer.ts +++ b/log-viewer/src/features/timeline/optimised/metric-strip/MetricStripRenderer.ts @@ -34,6 +34,7 @@ import type { TimelineMarker, ViewportState, } from '../../types/flamechart.types.js'; +import { MARKER_ALPHA, MARKER_COLORS } from '../../types/flamechart.types.js'; import { BREACH_AREA_OPACITY, DANGER_ZONE_OPACITY, @@ -41,8 +42,6 @@ import { getTrafficLightColor, METRIC_STRIP_HEIGHT, METRIC_STRIP_LINE_WIDTHS, - METRIC_STRIP_MARKER_COLORS_BLENDED, - METRIC_STRIP_MARKER_OPACITY, METRIC_STRIP_THRESHOLDS, METRIC_STRIP_TOGGLE_WIDTH, METRIC_STRIP_Y_MAX_PERCENT, @@ -402,7 +401,7 @@ export class MetricStripRenderer { continue; } - const color = METRIC_STRIP_MARKER_COLORS_BLENDED[marker.type]; + const color = MARKER_COLORS[marker.type]; if (color === undefined) { continue; } @@ -413,7 +412,7 @@ export class MetricStripRenderer { if (gappedWidth > 0) { g.rect(gappedStartX, 0, gappedWidth, this.height); - g.fill({ color, alpha: METRIC_STRIP_MARKER_OPACITY }); + g.fill({ color, alpha: MARKER_ALPHA }); } } } diff --git a/log-viewer/src/features/timeline/optimised/metric-strip/metric-strip-colors.ts b/log-viewer/src/features/timeline/optimised/metric-strip/metric-strip-colors.ts index 4980496c..bc2db591 100644 --- a/log-viewer/src/features/timeline/optimised/metric-strip/metric-strip-colors.ts +++ b/log-viewer/src/features/timeline/optimised/metric-strip/metric-strip-colors.ts @@ -9,8 +9,6 @@ * Colors are designed to be distinguishable on both light and dark backgrounds. */ -import type { MarkerType } from '../../types/flamechart.types.js'; - /** * MetricStrip color palette. */ @@ -264,22 +262,6 @@ export const METRIC_STRIP_TIME_GRID_COLOR = 0x808080; */ export const METRIC_STRIP_TIME_GRID_OPACITY = 0.3; -/** - * Marker colors pre-blended with background for metric strip background bands. - * These match the minimap marker colors for visual consistency. - */ -export const METRIC_STRIP_MARKER_COLORS_BLENDED: Record = { - error: 0xff8080, // Light red - skip: 0x1e80ff, // Light blue - unexpected: 0x8080ff, // Light purple -}; - -/** - * Marker band opacity in metric strip. - * Matches MARKER_ALPHA (0.2) from main timeline for visual consistency. - */ -export const METRIC_STRIP_MARKER_OPACITY = 0.2; - /** * Width of the expand/collapse toggle area on the left side. */ diff --git a/log-viewer/src/features/timeline/optimised/minimap/MinimapRenderer.ts b/log-viewer/src/features/timeline/optimised/minimap/MinimapRenderer.ts index cb9fb757..e4aa5214 100644 --- a/log-viewer/src/features/timeline/optimised/minimap/MinimapRenderer.ts +++ b/log-viewer/src/features/timeline/optimised/minimap/MinimapRenderer.ts @@ -27,9 +27,8 @@ import * as PIXI from 'pixi.js'; import { formatDuration, formatTimeRange } from '../../../../core/utility/Util.js'; -import type { MarkerType, TimelineMarker } from '../../types/flamechart.types.js'; +import type { TimelineMarker } from '../../types/flamechart.types.js'; import { MARKER_ALPHA, MARKER_COLORS } from '../../types/flamechart.types.js'; -import { blendWithBackground } from '../BucketColorResolver.js'; import { createRectangleShader } from '../RectangleShader.js'; import { MinimapAxisRenderer } from './MinimapAxisRenderer.js'; import { MinimapBarGeometry } from './MinimapBarGeometry.js'; @@ -43,16 +42,6 @@ const MIN_OPACITY = 0.5; const MAX_OPACITY = 1.0; const SATURATION_COUNT = 100; -/** - * Pre-blended opaque marker colors (MARKER_COLORS blended at MARKER_ALPHA opacity). - * Computed once at module load time for performance. - */ -const MINIMAP_MARKER_COLORS_BLENDED: Record = { - error: blendWithBackground(MARKER_COLORS.error, MARKER_ALPHA), - skip: blendWithBackground(MARKER_COLORS.skip, MARKER_ALPHA), - unexpected: blendWithBackground(MARKER_COLORS.unexpected, MARKER_ALPHA), -}; - /** * Curtain overlay opacity (outside viewport lens). */ @@ -589,7 +578,7 @@ export class MinimapRenderer { /** * Render markers as colored vertical bands. - * Uses pre-blended opaque colors and 1px gaps between adjacent markers. + * Uses MARKER_COLORS with MARKER_ALPHA and 1px gaps between adjacent markers. */ private renderMarkers( manager: MinimapViewport, @@ -621,8 +610,7 @@ export class MinimapRenderer { const endTime = nextMarker?.startTime ?? state.totalDuration; const endX = manager.timeToMinimapX(endTime); - // Get pre-blended opaque marker color - const color = MINIMAP_MARKER_COLORS_BLENDED[marker.type] ?? 0x808080; + const color = MARKER_COLORS[marker.type]; // Apply gap to create separation between adjacent markers const gappedStartX = startX + halfGap; @@ -631,7 +619,7 @@ export class MinimapRenderer { // Draw marker band (full height of chart area below axis) if (gappedWidth > 0) { this.markerGraphics.rect(gappedStartX, chartTop, gappedWidth, chartHeight); - this.markerGraphics.fill({ color, alpha: 1.0 }); + this.markerGraphics.fill({ color, alpha: MARKER_ALPHA }); } } }