| // Copyright (C) 2026 The Android Open Source Project |
| // |
| // 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. |
| |
| // WebGL renderer with an immediate-mode style API using instanced rendering. |
| // Uses two separate pipelines: |
| // 1. Rects pipeline - plain/hatched rectangles |
| // 2. Markers pipeline - SDF-based shapes like chevrons |
| |
| import { |
| Renderer, |
| MarkerRenderFunc, |
| MarkerBuffers, |
| StepAreaBuffers, |
| SliceBuffers, |
| RowLayout, |
| } from './../renderer'; |
| import {DisposableStack} from './../disposable_stack'; |
| import {SliceBatch} from './slices'; |
| import {ChevronBatch} from './chevrons'; |
| import {StepAreaBatch} from './step_area'; |
| import {Color} from './../color'; |
| import {Transform1D, Transform2D} from '../geom'; |
| |
| export class WebGLRenderer implements Renderer { |
| private readonly c2d: CanvasRenderingContext2D; |
| readonly gl: WebGL2RenderingContext; |
| private readonly slices: SliceBatch; |
| private readonly markers: ChevronBatch; |
| private readonly stepArea: StepAreaBatch; |
| private transform = Transform2D.Identity; |
| private clipRect: |
| | {left: number; top: number; right: number; bottom: number} |
| | undefined; |
| |
| constructor(c2d: CanvasRenderingContext2D, gl: WebGL2RenderingContext) { |
| this.c2d = c2d; |
| this.gl = gl; |
| this.slices = new SliceBatch(gl); |
| this.markers = new ChevronBatch(gl); |
| this.stepArea = new StepAreaBatch(gl); |
| } |
| |
| pushTransform(transform: Partial<Transform2D>): Disposable { |
| const trash = new DisposableStack(); |
| trash.use(this.pushWebGLTransform(transform)); |
| trash.use(this.pushCanvas2DTransform(transform)); |
| return trash; |
| } |
| |
| pushWebGLTransform(transform: Partial<Transform2D>): Disposable { |
| const previousTransform = this.transform; |
| this.transform = Transform2D.compose(this.transform, transform); |
| return { |
| [Symbol.dispose]: () => { |
| this.transform = previousTransform; |
| }, |
| }; |
| } |
| |
| pushCanvas2DTransform({ |
| offsetX = 0, |
| offsetY = 0, |
| scaleX = 1, |
| scaleY = 1, |
| }: Partial<Transform2D>): Disposable { |
| const ctx = this.c2d; |
| ctx.save(); |
| ctx.translate(offsetX, offsetY); |
| ctx.scale(scaleX, scaleY); |
| return { |
| [Symbol.dispose]: () => { |
| ctx.restore(); |
| }, |
| }; |
| } |
| |
| drawMarkers( |
| buffers: MarkerBuffers, |
| rowLayout: RowLayout, |
| markerWidth: number, |
| xTransform: Transform1D, |
| _render: MarkerRenderFunc, |
| ): void { |
| this.markers.draw( |
| buffers, |
| rowLayout, |
| markerWidth, |
| xTransform, |
| this.transform, |
| ); |
| } |
| |
| drawSlices( |
| buffers: SliceBuffers, |
| rowLayout: RowLayout, |
| xTransform: Transform1D, |
| ): void { |
| // Use current clip rect, or full canvas if no clip is active |
| const clipRect = this.clipRect ?? { |
| left: 0, |
| top: 0, |
| right: this.gl.canvas.width, |
| bottom: this.gl.canvas.height, |
| }; |
| this.slices.draw(buffers, rowLayout, xTransform, this.transform, clipRect); |
| } |
| |
| drawStepArea( |
| buffers: StepAreaBuffers, |
| dataTransform: Transform2D, |
| color: Color, |
| top: number, |
| bottom: number, |
| ): void { |
| this.stepArea.draw( |
| buffers, |
| dataTransform, |
| this.transform, |
| top, |
| bottom, |
| color.rgba, |
| ); |
| } |
| |
| resetTransform(): void { |
| this.transform = Transform2D.Identity; |
| this.c2d.resetTransform(); |
| } |
| |
| clear(): void { |
| const gl = this.gl; |
| gl.clearColor(0, 0, 0, 0); |
| gl.clear(gl.COLOR_BUFFER_BIT); |
| |
| const ctx = this.c2d; |
| const canvas = ctx.canvas; |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| } |
| |
| clip(x: number, y: number, w: number, h: number): Disposable { |
| const gl = this.gl; |
| const ctx = this.c2d; |
| |
| // Apply transform: physPos = offset + pos * scale |
| const physX = this.transform.offsetX + x * this.transform.scaleX; |
| const physY = this.transform.offsetY + y * this.transform.scaleY; |
| const physW = w * this.transform.scaleX; |
| const physH = h * this.transform.scaleY; |
| |
| gl.enable(gl.SCISSOR_TEST); |
| const canvasHeight = gl.canvas.height; |
| gl.scissor( |
| Math.round(physX), |
| Math.round(canvasHeight - (physY + physH)), |
| Math.round(physW), |
| Math.round(physH), |
| ); |
| |
| ctx.save(); |
| ctx.beginPath(); |
| ctx.rect(x, y, w, h); |
| ctx.clip(); |
| |
| // Store clip rect in screen space for shader-based vertex clamping |
| const previousClipRect = this.clipRect; |
| this.clipRect = { |
| left: physX, |
| top: physY, |
| right: physX + physW, |
| bottom: physY + physH, |
| }; |
| |
| return { |
| [Symbol.dispose]: () => { |
| ctx.restore(); |
| gl.disable(gl.SCISSOR_TEST); |
| this.clipRect = previousClipRect; |
| }, |
| }; |
| } |
| } |