• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 27441416196

12 Jun 2026 08:32PM UTC coverage: 70.567% (-0.1%) from 70.693%
27441416196

Pull #2674

github

web-flow
Merge 7750e5219 into 530fcafa4
Pull Request #2674: POC: HTMLTexture via HTML-in-Canvas

9543 of 15250 branches covered (62.58%)

Branch coverage included in aggregate %.

38 of 95 new or added lines in 5 files covered. (40.0%)

47 existing lines in 2 files now uncovered.

19546 of 25972 relevant lines covered (75.26%)

4126.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.85
/modules/engine/src/dynamic-texture/html-texture.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {Device} from '@luma.gl/core';
6
import {Texture} from '@luma.gl/core';
7
import {DynamicTexture, type DynamicTextureProps} from './dynamic-texture';
8

9
const HTML_CANVAS_LAYOUT_SUBTREE_ATTRIBUTE = 'layoutsubtree';
123✔
10

11
type PaintableHTMLCanvasElement = HTMLCanvasElement & {
12
  layoutSubtree?: boolean;
13
  requestPaint?: () => void;
14
};
15

16
type ElementTextureSource = Element;
17

18
export type HTMLTextureProps = Omit<
19
  DynamicTextureProps,
20
  'data' | 'dimension' | 'height' | 'mipmaps' | 'width'
21
> & {
22
  /** Canvas participating in the HTML-in-Canvas paint cycle. */
23
  canvas: HTMLCanvasElement;
24
  /** DOM subtree copied into this GPU texture. */
25
  element: ElementTextureSource;
26
  /** Texture width in pixels. */
27
  width: number;
28
  /** Texture height in pixels. */
29
  height: number;
30
  /** Request a repaint when the subtree mutates. */
31
  autoUpdate?: boolean;
32
  /** Request a repaint when the subtree size changes. */
33
  observeResize?: boolean;
34
};
35

36
/**
37
 * Dynamic texture backed by an HTML-in-Canvas DOM subtree.
38
 *
39
 * The browser owns layout, paint invalidation, accessibility, and pointer routing for the DOM
40
 * subtree. luma.gl owns only the GPU texture lifetime and the DOM-to-texture copy issued on paint.
41
 */
42
export class HTMLTexture extends DynamicTexture {
43
  readonly canvas: PaintableHTMLCanvasElement;
44
  readonly element: ElementTextureSource;
45
  readonly autoUpdate: boolean;
46
  readonly observeResize: boolean;
47

48
  private pendingPaint = false;
1✔
49
  private mutationObserver: MutationObserver | null = null;
1✔
50
  private resizeObserver: ResizeObserver | null = null;
1✔
51
  private readonly handlePaint = () => this.uploadElementImage();
1✔
52

53
  constructor(device: Device, props: HTMLTextureProps) {
54
    const {
55
      autoUpdate = false,
1✔
56
      canvas,
57
      element,
58
      height,
59
      observeResize = false,
1✔
60
      width,
61
      ...textureProps
62
    } = props;
1✔
63
    const resolvedTextureProps = {
1✔
64
      ...textureProps,
65
      data: null,
66
      dimension: '2d',
67
      height,
68
      mipmaps: false,
69
      usage: textureProps.usage ?? Texture.SAMPLE | Texture.COPY_DST,
2✔
70
      width
71
    } satisfies DynamicTextureProps;
72

73
    super(device, resolvedTextureProps);
1✔
74

75
    this.canvas = canvas;
1✔
76
    this.element = element;
1✔
77
    this.autoUpdate = autoUpdate;
1✔
78
    this.observeResize = observeResize;
1✔
79

80
    HTMLTexture.configureCanvas(canvas);
1✔
81
    this.canvas.addEventListener('paint', this.handlePaint);
1✔
82
    this.startObservers();
1✔
83
    void this.ready.then(() => {
1✔
84
      if (this.pendingPaint) {
1!
NEW
85
        this.pendingPaint = false;
×
NEW
86
        this.uploadElementImage();
×
87
      }
88
    });
89
    this.requestUpdate();
1✔
90
  }
91

92
  static configureCanvas(canvas: HTMLCanvasElement): void {
93
    (canvas as PaintableHTMLCanvasElement).layoutSubtree = true;
1✔
94
    if (!canvas.hasAttribute(HTML_CANVAS_LAYOUT_SUBTREE_ATTRIBUTE)) {
1!
95
      canvas.setAttribute(HTML_CANVAS_LAYOUT_SUBTREE_ATTRIBUTE, '');
1✔
96
    }
97
  }
98

99
  static isSupported(device: Device, canvas?: HTMLCanvasElement | null): boolean {
NEW
100
    if (canvas) {
×
NEW
101
      HTMLTexture.configureCanvas(canvas);
×
102
    }
NEW
103
    if (!canvas || typeof (canvas as PaintableHTMLCanvasElement).requestPaint !== 'function') {
×
NEW
104
      return false;
×
105
    }
106

NEW
107
    switch (device.type) {
×
108
      case 'webgpu':
NEW
109
        return (
×
110
          typeof (
111
            device as Device & {
112
              handle?: {queue?: {copyElementImageToTexture?: unknown}};
113
            }
114
          ).handle?.queue?.copyElementImageToTexture === 'function'
115
        );
116
      case 'webgl':
NEW
117
        return (
×
118
          typeof (
119
            device as Device & {
120
              gl?: {texElementImage2D?: unknown};
121
            }
122
          ).gl?.texElementImage2D === 'function'
123
        );
124
      default:
NEW
125
        return false;
×
126
    }
127
  }
128

129
  requestUpdate(): void {
130
    const requestPaint = this.canvas.requestPaint;
2✔
131
    if (typeof requestPaint !== 'function') {
2!
NEW
132
      throw new Error(`${this} canvas.requestPaint() is not available`);
×
133
    }
134
    requestPaint.call(this.canvas);
2✔
135
  }
136

137
  override resize(size: {width: number; height: number}): boolean {
NEW
138
    const resized = super.resize(size);
×
NEW
139
    if (resized) {
×
NEW
140
      this.requestUpdate();
×
141
    }
NEW
142
    return resized;
×
143
  }
144

145
  override destroy(): void {
146
    this.canvas.removeEventListener('paint', this.handlePaint);
1✔
147
    this.mutationObserver?.disconnect();
1✔
148
    this.mutationObserver = null;
1✔
149
    this.resizeObserver?.disconnect();
1✔
150
    this.resizeObserver = null;
1✔
151
    super.destroy();
1✔
152
  }
153

154
  private startObservers(): void {
155
    if (this.autoUpdate && typeof MutationObserver !== 'undefined') {
1!
NEW
156
      this.mutationObserver = new MutationObserver(() => this.requestUpdate());
×
NEW
157
      this.mutationObserver.observe(this.element, {
×
158
        attributes: true,
159
        characterData: true,
160
        childList: true,
161
        subtree: true
162
      });
163
    }
164

165
    if (this.observeResize && typeof ResizeObserver !== 'undefined') {
1!
NEW
166
      this.resizeObserver = new ResizeObserver(() => this.requestUpdate());
×
NEW
167
      this.resizeObserver.observe(this.element);
×
168
    }
169
  }
170

171
  private uploadElementImage(): void {
172
    if (this.destroyed) {
1!
NEW
173
      return;
×
174
    }
175
    if (!this.isReady) {
1!
NEW
176
      this.pendingPaint = true;
×
NEW
177
      return;
×
178
    }
179

180
    this.texture.copyElementImage({
1✔
181
      element: this.element,
182
      height: this.texture.height,
183
      width: this.texture.width
184
    });
185
    this._touch();
1✔
186
  }
187
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc