|
4 | 4 | // All are concatenated into the same scope by the Python loader — no imports. |
5 | 5 |
|
6 | 6 | const SAMPLE_COUNT = 4; |
7 | | -const CLEAR_COLOR = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }; |
| 7 | +const LIGHT_CLEAR_COLOR = Object.freeze({ r: 1.0, g: 1.0, b: 1.0, a: 1.0 }); |
| 8 | +const DARK_CLEAR_COLOR = Object.freeze({ r: 0.68, g: 0.68, b: 0.68, a: 1.0 }); |
| 9 | +const LIGHT_CANVAS_BG = '#ffffff'; |
| 10 | +const DARK_CANVAS_BG = '#adadad'; |
8 | 11 | const DEPTH_FORMAT = 'depth24plus'; |
9 | 12 |
|
10 | 13 | const TRANSPARENT_BLEND = { |
@@ -297,6 +300,10 @@ class RenderEngine { |
297 | 300 | this._resizeObserver.observe(canvas); |
298 | 301 | } |
299 | 302 |
|
| 303 | + // --- Theme handling --- |
| 304 | + this._applyTheme(); |
| 305 | + this._setupThemeObserver(); |
| 306 | + |
300 | 307 | // --- First frame --- |
301 | 308 | // In live mode, the host decides when to render. Don't auto-render. |
302 | 309 | if (this.mode !== 'live') this.render(); |
@@ -741,7 +748,7 @@ class RenderEngine { |
741 | 748 | resolveTarget: canvasTexture.createView(), |
742 | 749 | loadOp: 'clear', |
743 | 750 | storeOp: 'store', |
744 | | - clearValue: CLEAR_COLOR, |
| 751 | + clearValue: this.clearColor || LIGHT_CLEAR_COLOR, |
745 | 752 | }], |
746 | 753 | depthStencilAttachment: { |
747 | 754 | view: this.depthTexture.createView(), |
@@ -864,8 +871,50 @@ class RenderEngine { |
864 | 871 | return out; |
865 | 872 | } |
866 | 873 |
|
| 874 | + _isDarkMode() { |
| 875 | + try { |
| 876 | + return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); |
| 877 | + } catch { |
| 878 | + return false; |
| 879 | + } |
| 880 | + } |
| 881 | + |
| 882 | + _applyTheme() { |
| 883 | + const dark = this._isDarkMode(); |
| 884 | + this.clearColor = dark ? DARK_CLEAR_COLOR : LIGHT_CLEAR_COLOR; |
| 885 | + if (this.canvas && this.canvas.style) { |
| 886 | + this.canvas.style.backgroundColor = dark ? DARK_CANVAS_BG : LIGHT_CANVAS_BG; |
| 887 | + } |
| 888 | + } |
| 889 | + |
| 890 | + _setupThemeObserver() { |
| 891 | + if (typeof window === 'undefined' || !window.matchMedia) return; |
| 892 | + this._themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); |
| 893 | + this._onThemeChange = () => { |
| 894 | + this._applyTheme(); |
| 895 | + this.render(); |
| 896 | + }; |
| 897 | + if (this._themeMediaQuery.addEventListener) { |
| 898 | + this._themeMediaQuery.addEventListener('change', this._onThemeChange); |
| 899 | + } else if (this._themeMediaQuery.addListener) { |
| 900 | + this._themeMediaQuery.addListener(this._onThemeChange); |
| 901 | + } |
| 902 | + } |
| 903 | + |
| 904 | + _teardownThemeObserver() { |
| 905 | + if (!this._themeMediaQuery || !this._onThemeChange) return; |
| 906 | + if (this._themeMediaQuery.removeEventListener) { |
| 907 | + this._themeMediaQuery.removeEventListener('change', this._onThemeChange); |
| 908 | + } else if (this._themeMediaQuery.removeListener) { |
| 909 | + this._themeMediaQuery.removeListener(this._onThemeChange); |
| 910 | + } |
| 911 | + this._themeMediaQuery = null; |
| 912 | + this._onThemeChange = null; |
| 913 | + } |
| 914 | + |
867 | 915 | dispose() { |
868 | 916 | if (this._resizeObserver) this._resizeObserver.disconnect(); |
| 917 | + this._teardownThemeObserver(); |
869 | 918 | if (this.input) this.input.dispose(); |
870 | 919 | if (this.interactions) this.interactions.dispose(); |
871 | 920 | if (this.depthTexture) this.depthTexture.destroy(); |
|
0 commit comments