@tableslayer/ui 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/package.json +2 -13
  2. package/src/lib/components/Avatar/Avatar.svelte +82 -0
  3. package/src/lib/components/Avatar/AvatarFileInput.svelte +85 -0
  4. package/src/lib/components/Avatar/AvatarPopover.svelte +34 -0
  5. package/src/lib/components/Avatar/index.ts +4 -0
  6. package/src/lib/components/Avatar/types.ts +24 -0
  7. package/src/lib/components/BrushSizeSlider/BrushSizeSlider.svelte +174 -0
  8. package/src/lib/components/BrushSizeSlider/index.ts +1 -0
  9. package/src/lib/components/Button/Button.svelte +182 -0
  10. package/src/lib/components/Button/ConfirmActionButton.svelte +98 -0
  11. package/src/lib/components/Button/IconButton.svelte +121 -0
  12. package/src/lib/components/Button/RadioButton.svelte +93 -0
  13. package/src/lib/components/Button/index.ts +5 -0
  14. package/src/lib/components/Button/types.ts +54 -0
  15. package/src/lib/components/CardFan/CardFan.svelte +165 -0
  16. package/src/lib/components/CardFan/index.ts +2 -0
  17. package/src/lib/components/CardFan/types.ts +6 -0
  18. package/src/lib/components/CodeBlock/Code.svelte +7 -0
  19. package/src/lib/components/CodeBlock/CodeBlock.svelte +102 -0
  20. package/src/lib/components/CodeBlock/index.ts +3 -0
  21. package/src/lib/components/CodeBlock/types.ts +10 -0
  22. package/src/lib/components/ColorMode/ColorMode.svelte +8 -0
  23. package/src/lib/components/ColorMode/index.ts +2 -0
  24. package/src/lib/components/ColorMode/types.ts +12 -0
  25. package/src/lib/components/ColorPicker/ColorPicker.svelte +838 -0
  26. package/src/lib/components/ColorPicker/ColorPickerSwatch.svelte +32 -0
  27. package/src/lib/components/ColorPicker/index.ts +3 -0
  28. package/src/lib/components/ColorPicker/types.ts +51 -0
  29. package/src/lib/components/ContextMenu/ContextMenu.svelte +86 -0
  30. package/src/lib/components/ContextMenu/index.ts +2 -0
  31. package/src/lib/components/ContextMenu/types.ts +15 -0
  32. package/src/lib/components/DrawingSliders/DrawingSliders.svelte +379 -0
  33. package/src/lib/components/DrawingSliders/index.ts +1 -0
  34. package/src/lib/components/Editor/Editor.svelte +825 -0
  35. package/src/lib/components/Editor/index.ts +1 -0
  36. package/src/lib/components/FogSliders/FogSliders.svelte +33 -0
  37. package/src/lib/components/FogSliders/index.ts +1 -0
  38. package/src/lib/components/Hr/Hr.svelte +15 -0
  39. package/src/lib/components/Hr/index.ts +1 -0
  40. package/src/lib/components/Icon/Icon.svelte +6 -0
  41. package/src/lib/components/Icon/index.ts +2 -0
  42. package/src/lib/components/Icon/types.ts +20 -0
  43. package/src/lib/components/Input/DualInputSlider.svelte +126 -0
  44. package/src/lib/components/Input/FileInput.svelte +176 -0
  45. package/src/lib/components/Input/FormControl.svelte +150 -0
  46. package/src/lib/components/Input/FormError.svelte +37 -0
  47. package/src/lib/components/Input/Input.svelte +56 -0
  48. package/src/lib/components/Input/InputCheckbox.svelte +99 -0
  49. package/src/lib/components/Input/InputSlider.svelte +86 -0
  50. package/src/lib/components/Input/Label.svelte +19 -0
  51. package/src/lib/components/Input/index.ts +9 -0
  52. package/src/lib/components/Input/types.ts +39 -0
  53. package/src/lib/components/Link/Link.svelte +41 -0
  54. package/src/lib/components/Link/LinkBox.svelte +20 -0
  55. package/src/lib/components/Link/LinkOverlay.svelte +23 -0
  56. package/src/lib/components/Link/index.ts +4 -0
  57. package/src/lib/components/Link/types.ts +17 -0
  58. package/src/lib/components/Loading/Loader.svelte +60 -0
  59. package/src/lib/components/Loading/Skeleton.svelte +9 -0
  60. package/src/lib/components/Loading/index.ts +2 -0
  61. package/src/lib/components/Logo/Logo.svelte +16 -0
  62. package/src/lib/components/Logo/index.ts +1 -0
  63. package/src/lib/components/MarkerTooltip/MarkerTooltip.svelte +435 -0
  64. package/src/lib/components/MarkerTooltip/index.ts +1 -0
  65. package/src/lib/components/Menu/SelectorMenu.svelte +280 -0
  66. package/src/lib/components/Menu/index.ts +2 -0
  67. package/src/lib/components/Menu/types.ts +17 -0
  68. package/src/lib/components/MyCounterButton.svelte +11 -0
  69. package/src/lib/components/Panel/index.ts +2 -0
  70. package/src/lib/components/Panel/panel.svelte +18 -0
  71. package/src/lib/components/Panel/types.ts +8 -0
  72. package/src/lib/components/PersistButton/PersistButton.svelte +100 -0
  73. package/src/lib/components/PersistButton/index.ts +1 -0
  74. package/src/lib/components/Popover/Popover.svelte +81 -0
  75. package/src/lib/components/Popover/index.ts +2 -0
  76. package/src/lib/components/Popover/types.ts +19 -0
  77. package/src/lib/components/PropsTable/PropsTable.svelte +107 -0
  78. package/src/lib/components/RadialMenu/EffectPreview.svelte +36 -0
  79. package/src/lib/components/RadialMenu/EffectPreviewScene.svelte +194 -0
  80. package/src/lib/components/RadialMenu/RadialMenu.svelte +503 -0
  81. package/src/lib/components/RadialMenu/RadialMenuItem.svelte +176 -0
  82. package/src/lib/components/RadialMenu/index.ts +2 -0
  83. package/src/lib/components/RadialMenu/types.ts +35 -0
  84. package/src/lib/components/Select/Select.svelte +342 -0
  85. package/src/lib/components/Select/index.ts +2 -0
  86. package/src/lib/components/Select/types.ts +22 -0
  87. package/src/lib/components/Spacer/Spacer.svelte +14 -0
  88. package/src/lib/components/Spacer/index.ts +2 -0
  89. package/src/lib/components/Spacer/types.ts +5 -0
  90. package/src/lib/components/Stage/components/AnnotationLayer/AnnotationLayer.svelte +445 -0
  91. package/src/lib/components/Stage/components/AnnotationLayer/AnnotationMaterial.svelte +167 -0
  92. package/src/lib/components/Stage/components/AnnotationLayer/types.ts +196 -0
  93. package/src/lib/components/Stage/components/CursorLayer/CursorLayer.svelte +148 -0
  94. package/src/lib/components/Stage/components/CursorLayer/cursor.svg +26 -0
  95. package/src/lib/components/Stage/components/CursorLayer/index.ts +2 -0
  96. package/src/lib/components/Stage/components/CursorLayer/types.ts +23 -0
  97. package/src/lib/components/Stage/components/DrawingLayer/DrawingMaterial.svelte +364 -0
  98. package/src/lib/components/Stage/components/DrawingLayer/types.ts +65 -0
  99. package/src/lib/components/Stage/components/EdgeOverlayLayer/EdgeOverlayLayer.svelte +72 -0
  100. package/src/lib/components/Stage/components/EdgeOverlayLayer/types.ts +34 -0
  101. package/src/lib/components/Stage/components/FogLayer/FogLayer.svelte +75 -0
  102. package/src/lib/components/Stage/components/FogLayer/types.ts +51 -0
  103. package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarLayer.svelte +249 -0
  104. package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarMaterial.svelte +200 -0
  105. package/src/lib/components/Stage/components/FogOfWarLayer/types.ts +116 -0
  106. package/src/lib/components/Stage/components/GridLayer/GridLayer.svelte +20 -0
  107. package/src/lib/components/Stage/components/GridLayer/GridMaterial.svelte +69 -0
  108. package/src/lib/components/Stage/components/GridLayer/types.ts +79 -0
  109. package/src/lib/components/Stage/components/LayerInput/LayerInput.svelte +300 -0
  110. package/src/lib/components/Stage/components/MapLayer/MapLayer.svelte +196 -0
  111. package/src/lib/components/Stage/components/MapLayer/dataSources/GifDataSource.ts +265 -0
  112. package/src/lib/components/Stage/components/MapLayer/dataSources/IMapDataSource.ts +55 -0
  113. package/src/lib/components/Stage/components/MapLayer/dataSources/ImageDataSource.ts +87 -0
  114. package/src/lib/components/Stage/components/MapLayer/dataSources/VideoDataSource.ts +150 -0
  115. package/src/lib/components/Stage/components/MapLayer/dataSources/dataSourceFactory.ts +48 -0
  116. package/src/lib/components/Stage/components/MapLayer/dataSources/index.ts +16 -0
  117. package/src/lib/components/Stage/components/MapLayer/types.ts +58 -0
  118. package/src/lib/components/Stage/components/MarkerLayer/MarkerLayer.svelte +398 -0
  119. package/src/lib/components/Stage/components/MarkerLayer/MarkerToken.svelte +262 -0
  120. package/src/lib/components/Stage/components/MarkerLayer/types.ts +126 -0
  121. package/src/lib/components/Stage/components/MeasurementLayer/MeasurementLayer.svelte +364 -0
  122. package/src/lib/components/Stage/components/MeasurementLayer/MeasurementManager.svelte +473 -0
  123. package/src/lib/components/Stage/components/MeasurementLayer/measurements/BaseMeasurement.ts +427 -0
  124. package/src/lib/components/Stage/components/MeasurementLayer/measurements/BeamMeasurement.ts +105 -0
  125. package/src/lib/components/Stage/components/MeasurementLayer/measurements/CircleMeasurement.ts +98 -0
  126. package/src/lib/components/Stage/components/MeasurementLayer/measurements/ConeMeasurement.ts +163 -0
  127. package/src/lib/components/Stage/components/MeasurementLayer/measurements/LineMeasurement.ts +102 -0
  128. package/src/lib/components/Stage/components/MeasurementLayer/measurements/RectangleMeasurement.ts +120 -0
  129. package/src/lib/components/Stage/components/MeasurementLayer/measurements/index.ts +7 -0
  130. package/src/lib/components/Stage/components/MeasurementLayer/types.ts +94 -0
  131. package/src/lib/components/Stage/components/MeasurementLayer/utils/canvasDrawing.ts +357 -0
  132. package/src/lib/components/Stage/components/MeasurementLayer/utils/distanceCalculations.ts +170 -0
  133. package/src/lib/components/Stage/components/ParticleSystem/ParticleSystem.svelte +220 -0
  134. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/ash.png +0 -0
  135. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/leaves.png +0 -0
  136. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/rain.png +0 -0
  137. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/snow.png +0 -0
  138. package/src/lib/components/Stage/components/ParticleSystem/rng.js +20 -0
  139. package/src/lib/components/Stage/components/ParticleSystem/types.ts +95 -0
  140. package/src/lib/components/Stage/components/PerformanceDebugger/PerformanceDebugger.svelte +144 -0
  141. package/src/lib/components/Stage/components/PerformanceDebugger/index.ts +1 -0
  142. package/src/lib/components/Stage/components/PerformanceOverlay/PerformanceOverlay.svelte +208 -0
  143. package/src/lib/components/Stage/components/PerformanceOverlay/index.ts +1 -0
  144. package/src/lib/components/Stage/components/PointerInputManager/PointerInputManager.svelte +201 -0
  145. package/src/lib/components/Stage/components/Scene/Scene.svelte +651 -0
  146. package/src/lib/components/Stage/components/Scene/luts.ts +24 -0
  147. package/src/lib/components/Stage/components/Scene/types.ts +225 -0
  148. package/src/lib/components/Stage/components/Stage/Stage.svelte +332 -0
  149. package/src/lib/components/Stage/components/Stage/types.ts +136 -0
  150. package/src/lib/components/Stage/components/WeatherLayer/WeatherLayer.svelte +135 -0
  151. package/src/lib/components/Stage/components/WeatherLayer/presets/AshPreset.ts +71 -0
  152. package/src/lib/components/Stage/components/WeatherLayer/presets/LeavesPreset.ts +70 -0
  153. package/src/lib/components/Stage/components/WeatherLayer/presets/RainPreset.ts +68 -0
  154. package/src/lib/components/Stage/components/WeatherLayer/presets/SnowPreset.ts +70 -0
  155. package/src/lib/components/Stage/components/WeatherLayer/presets/index.ts +6 -0
  156. package/src/lib/components/Stage/components/WeatherLayer/types.ts +35 -0
  157. package/src/lib/components/Stage/helpers/clippingPlaneStore.svelte.ts +28 -0
  158. package/src/lib/components/Stage/helpers/debugState.svelte.ts +18 -0
  159. package/src/lib/components/Stage/helpers/grid.ts +548 -0
  160. package/src/lib/components/Stage/helpers/lazyBrush.ts +171 -0
  161. package/src/lib/components/Stage/helpers/performanceMetrics.svelte.ts +220 -0
  162. package/src/lib/components/Stage/helpers/utils.ts +21 -0
  163. package/src/lib/components/Stage/index.ts +49 -0
  164. package/src/lib/components/Stage/shaders/AnnotationEffects.frag +1070 -0
  165. package/src/lib/components/Stage/shaders/Annotations.frag +29 -0
  166. package/src/lib/components/Stage/shaders/Drawing.frag +83 -0
  167. package/src/lib/components/Stage/shaders/Drawing.vert +5 -0
  168. package/src/lib/components/Stage/shaders/Fog.frag +147 -0
  169. package/src/lib/components/Stage/shaders/FractalNoise.frag +96 -0
  170. package/src/lib/components/Stage/shaders/GridShader.frag +174 -0
  171. package/src/lib/components/Stage/shaders/Overlay.frag +23 -0
  172. package/src/lib/components/Stage/shaders/Overlay.vert +0 -0
  173. package/src/lib/components/Stage/shaders/Particles.frag +27 -0
  174. package/src/lib/components/Stage/shaders/Particles.vert +51 -0
  175. package/src/lib/components/Stage/shaders/ToolOutline.frag +59 -0
  176. package/src/lib/components/Stage/shaders/default.vert +8 -0
  177. package/src/lib/components/Stage/types.ts +4 -0
  178. package/src/lib/components/Table/Table.svelte +16 -0
  179. package/src/lib/components/Table/Td.svelte +17 -0
  180. package/src/lib/components/Table/Th.svelte +18 -0
  181. package/src/lib/components/Table/index.ts +4 -0
  182. package/src/lib/components/Table/types.ts +14 -0
  183. package/src/lib/components/Text/Text.svelte +23 -0
  184. package/src/lib/components/Text/index.ts +2 -0
  185. package/src/lib/components/Text/types.ts +12 -0
  186. package/src/lib/components/Title/Title.svelte +54 -0
  187. package/src/lib/components/Title/index.ts +2 -0
  188. package/src/lib/components/Title/types.ts +9 -0
  189. package/src/lib/components/Toast/Toast.svelte +155 -0
  190. package/src/lib/components/Toast/index.ts +5 -0
  191. package/src/lib/components/Toast/toastCookie.ts +24 -0
  192. package/src/lib/components/Toast/types.ts +6 -0
  193. package/src/lib/components/ToolTip/ToolTip.svelte +70 -0
  194. package/src/lib/components/ToolTip/index.ts +2 -0
  195. package/src/lib/components/ToolTip/types.ts +14 -0
  196. package/src/lib/components/index.ts +32 -0
  197. package/src/lib/components/types.ts +0 -0
  198. package/src/lib/index.ts +2 -0
  199. package/src/lib/styles/globals.css +108 -0
  200. package/src/lib/styles/normalize.css +9 -0
  201. package/src/lib/styles/reset.css +133 -0
  202. package/src/lib/styles/utilities.css +179 -0
  203. package/src/lib/styles/vars.css +1103 -0
  204. package/src/lib/types/awareness.ts +17 -0
  205. package/src/lib/utils/rle.ts +217 -0
@@ -0,0 +1,445 @@
1
+ <script lang="ts">
2
+ import * as THREE from 'three';
3
+ import { getContext, onDestroy } from 'svelte';
4
+ import { T } from '@threlte/core';
5
+ import { type AnnotationLayerData, type AnnotationsLayerProps, AnnotationEffect } from './types';
6
+ import { StageMode, type Callbacks, type DisplayProps } from '../Stage/types';
7
+ import LayerInput from '../LayerInput/LayerInput.svelte';
8
+ import { SceneLayer, SceneLayerOrder } from '../Scene/types';
9
+ import AnnotationMaterial from './AnnotationMaterial.svelte';
10
+ import { LazyBrushManager } from '../../helpers/lazyBrush';
11
+ import { ToolType } from '../DrawingLayer/types';
12
+ import toolOutlineVertexShader from '../../shaders/default.vert?raw';
13
+ import toolOutlineFragmentShader from '../../shaders/ToolOutline.frag?raw';
14
+
15
+ interface Props {
16
+ props: AnnotationsLayerProps;
17
+ mode: StageMode;
18
+ isActive: boolean;
19
+ display: DisplayProps;
20
+ sceneZoom: number;
21
+ }
22
+
23
+ const { props, mode, isActive, display, sceneZoom }: Props = $props();
24
+
25
+ const onAnnotationUpdate = getContext<Callbacks>('callbacks').onAnnotationUpdate;
26
+
27
+ // Outline styling (same as fog of war)
28
+ const OUTLINE_COLOR = '#FFFFFF';
29
+ const OUTLINE_OPACITY = 1;
30
+ const OUTLINE_THICKNESS = 2;
31
+
32
+ // Pre-allocated vector for display center offset to avoid GC pressure
33
+ const displayCenterOffset = new THREE.Vector2();
34
+
35
+ // Convert percentage-based lineWidth to texture pixels for outline
36
+ const lineWidthPixels = $derived.by(() => {
37
+ const textureSize = Math.min(display.resolution.x, display.resolution.y);
38
+ return Math.round(textureSize * ((props.lineWidth ?? 2.0) / 100));
39
+ });
40
+
41
+ let mesh: THREE.Mesh = $state(new THREE.Mesh());
42
+ let outlineMesh: THREE.Mesh = $state(new THREE.Mesh());
43
+ let drawing = false;
44
+
45
+ // Export drawing state so parent can check it
46
+ export function isDrawing() {
47
+ return drawing;
48
+ }
49
+
50
+ // If mouse leaves the drawing area, we need to reset the start position
51
+ // when it re-enters the drawing area to prevent the drawing from "jumping"
52
+ // to the new point
53
+ let lastPos: THREE.Vector2 | null = null;
54
+
55
+ // Track the last screen position for the persist button
56
+ let lastScreenPos: { x: number; y: number } | null = null;
57
+
58
+ // Track if cursors are hidden to avoid redundant resets
59
+ let cursorsHidden = false;
60
+
61
+ // Initialize lazy brush for smooth drawing
62
+ // Base values for zoom level 1.0
63
+ const BASE_RADIUS = 20;
64
+ const BASE_FRICTION = 0.05;
65
+
66
+ // Smoothing disabled by default for more responsive drawing
67
+ const smoothingEnabled = $derived(props.smoothingEnabled ?? false);
68
+
69
+ const lazyBrush = new LazyBrushManager({
70
+ radius: BASE_RADIUS,
71
+ enabled: smoothingEnabled,
72
+ friction: BASE_FRICTION
73
+ });
74
+
75
+ // Outline material for brush cursor (same as fog of war)
76
+ const outlineMaterial = new THREE.ShaderMaterial({
77
+ uniforms: {
78
+ uStart: { value: new THREE.Vector2(Infinity, Infinity) },
79
+ uEnd: { value: new THREE.Vector2(Infinity, Infinity) },
80
+ uBrushSize: { value: lineWidthPixels },
81
+ uTextureSize: { value: new THREE.Vector2(display.resolution.x, display.resolution.y) },
82
+ uShapeType: { value: ToolType.Brush },
83
+ uOutlineColor: { value: new THREE.Color(OUTLINE_COLOR) },
84
+ uOutlineOpacity: { value: OUTLINE_OPACITY },
85
+ uOutlineThickness: { value: OUTLINE_THICKNESS }
86
+ },
87
+ vertexShader: toolOutlineVertexShader,
88
+ fragmentShader: toolOutlineFragmentShader,
89
+ transparent: true,
90
+ depthTest: false
91
+ });
92
+
93
+ onDestroy(() => {
94
+ outlineMaterial.dispose();
95
+ });
96
+
97
+ // Adjust lazy brush settings based on zoom level and smoothing toggle
98
+ $effect(() => {
99
+ // Scale radius inversely with zoom - less smoothing when zoomed in
100
+ // At zoom 2x, radius is 25 (half)
101
+ // At zoom 0.5x, radius is 100 (double)
102
+ const adjustedRadius = BASE_RADIUS / sceneZoom;
103
+
104
+ // Adjust friction based on zoom - more responsive when zoomed in
105
+ // At high zoom (>2), reduce friction for more immediate response
106
+ // At low zoom (<0.5), increase friction for smoother lines
107
+ const adjustedFriction =
108
+ sceneZoom > 2 ? BASE_FRICTION * 0.5 : sceneZoom < 0.5 ? BASE_FRICTION * 1.5 : BASE_FRICTION;
109
+
110
+ lazyBrush.updateConfig({
111
+ radius: Math.max(5, Math.min(100, adjustedRadius)), // Clamp between 5 and 100
112
+ friction: adjustedFriction,
113
+ enabled: smoothingEnabled
114
+ });
115
+ });
116
+
117
+ // Update outline material uniforms and center offset when props change
118
+ $effect(() => {
119
+ // Use .set() to avoid allocating new objects
120
+ outlineMaterial.uniforms.uTextureSize.value.set(display.resolution.x, display.resolution.y);
121
+ outlineMaterial.uniforms.uBrushSize.value = lineWidthPixels;
122
+ displayCenterOffset.set(display.resolution.x / 2, display.resolution.y / 2);
123
+ });
124
+
125
+ // Hide outline when tool is not active
126
+ $effect(() => {
127
+ if (!isActive || !props.activeLayer) {
128
+ outlineMesh.visible = false;
129
+ outlineMaterial.uniforms.uStart.value.set(Infinity, Infinity);
130
+ outlineMaterial.uniforms.uEnd.value.set(Infinity, Infinity);
131
+ }
132
+ });
133
+
134
+ // Reference to the child layers
135
+ let layers: AnnotationMaterial[] = $state([]);
136
+
137
+ // Get the currently active layer
138
+ let activeLayer = $derived(
139
+ layers.find((layer) => {
140
+ if (!layer) return false;
141
+ return layer.getId() === props.activeLayer;
142
+ })
143
+ );
144
+
145
+ // Whenever the tool type changes, we need to reset the drawing state
146
+ $effect(() => {
147
+ if (!isActive) {
148
+ lastPos = null;
149
+ drawing = false;
150
+ lazyBrush.reset();
151
+ // Reset cursor for all layers to ensure no ghosting
152
+ if (!cursorsHidden) {
153
+ layers.forEach((layer) => {
154
+ if (layer) {
155
+ layer.revertChanges();
156
+ layer.resetCursor();
157
+ }
158
+ });
159
+ cursorsHidden = true;
160
+ }
161
+ } else {
162
+ // Tool is active again
163
+ cursorsHidden = false;
164
+ }
165
+ });
166
+
167
+ // Also reset cursor when active layer becomes null
168
+ $effect(() => {
169
+ if (!props.activeLayer && layers.length > 0) {
170
+ // No active layer selected, reset all cursors and revert changes
171
+ lastPos = null; // Reset last position to prevent cursor from appearing
172
+ if (!cursorsHidden) {
173
+ layers.forEach((layer) => {
174
+ if (layer) {
175
+ layer.revertChanges();
176
+ layer.resetCursor();
177
+ }
178
+ });
179
+ cursorsHidden = true;
180
+ }
181
+ } else if (props.activeLayer) {
182
+ // Active layer selected again
183
+ cursorsHidden = false;
184
+ }
185
+ });
186
+
187
+ function onMouseDown(e: Event, p: THREE.Vector2 | null) {
188
+ e.preventDefault();
189
+ lastPos = p;
190
+ drawing = true;
191
+
192
+ // Track screen position for persist button
193
+ if (e instanceof MouseEvent) {
194
+ lastScreenPos = { x: e.clientX, y: e.clientY };
195
+ } else if (e instanceof TouchEvent && e.touches[0]) {
196
+ lastScreenPos = { x: e.touches[0].clientX, y: e.touches[0].clientY };
197
+ }
198
+
199
+ // Start a new stroke with lazy brush
200
+ if (p) {
201
+ // Need to adjust for display offset before starting stroke
202
+ const adjustedP = p.clone();
203
+ adjustedP.add(displayCenterOffset);
204
+ lazyBrush.startStroke(adjustedP);
205
+ }
206
+
207
+ draw(e, p);
208
+ }
209
+
210
+ function onMouseUp() {
211
+ // If we have just finished drawing, save the annotation
212
+ if (props.activeLayer && drawing) {
213
+ onAnnotationUpdate(props.activeLayer, toPng(), lastScreenPos || undefined);
214
+ }
215
+
216
+ // Reset the drawing state
217
+ drawing = false;
218
+ lazyBrush.endStroke();
219
+ // Don't reset lastPos here to prevent cursor jumping
220
+ }
221
+
222
+ function onMouseLeave() {
223
+ lastPos = null;
224
+ drawing = false;
225
+ lazyBrush.reset();
226
+
227
+ // Revert changes and hide cursor
228
+ if (activeLayer && !cursorsHidden) {
229
+ activeLayer.revertChanges();
230
+ activeLayer.resetCursor();
231
+ }
232
+
233
+ // Hide outline
234
+ outlineMesh.visible = false;
235
+ outlineMaterial.uniforms.uStart.value.set(Infinity, Infinity);
236
+ outlineMaterial.uniforms.uEnd.value.set(Infinity, Infinity);
237
+ }
238
+
239
+ function draw(e: Event, p: THREE.Vector2 | null) {
240
+ // Track screen position during drawing for persist button
241
+ if (drawing) {
242
+ if (e instanceof MouseEvent) {
243
+ lastScreenPos = { x: e.clientX, y: e.clientY };
244
+ } else if (e instanceof TouchEvent && e.touches[0]) {
245
+ lastScreenPos = { x: e.touches[0].clientX, y: e.touches[0].clientY };
246
+ }
247
+ }
248
+
249
+ // If the mouse is not within the drawing area, hide cursor and outline
250
+ if (!p) {
251
+ // Reset cursor for all layers when mouse leaves (only if not already hidden)
252
+ if (!cursorsHidden) {
253
+ layers.forEach((layer) => {
254
+ if (layer) {
255
+ layer.resetCursor();
256
+ }
257
+ });
258
+ }
259
+ // Hide outline
260
+ outlineMesh.visible = false;
261
+ outlineMaterial.uniforms.uStart.value.set(Infinity, Infinity);
262
+ outlineMaterial.uniforms.uEnd.value.set(Infinity, Infinity);
263
+ return;
264
+ }
265
+
266
+ // Only process if we have an active layer and the annotation tool is active
267
+ if (!activeLayer || !isActive || !props.activeLayer) {
268
+ // Make sure all cursors are hidden when not active (only if not already hidden)
269
+ if (!cursorsHidden) {
270
+ layers.forEach((layer) => {
271
+ if (layer) {
272
+ layer.resetCursor();
273
+ }
274
+ });
275
+ cursorsHidden = true;
276
+ }
277
+ // Hide outline
278
+ outlineMesh.visible = false;
279
+ outlineMaterial.uniforms.uStart.value.set(Infinity, Infinity);
280
+ outlineMaterial.uniforms.uEnd.value.set(Infinity, Infinity);
281
+ return;
282
+ }
283
+
284
+ // Tool is active and we have a valid position
285
+ cursorsHidden = false;
286
+
287
+ p.add(displayCenterOffset);
288
+
289
+ // Show outline at cursor position
290
+ outlineMesh.visible = true;
291
+ outlineMaterial.uniforms.uStart.value.copy(p);
292
+ outlineMaterial.uniforms.uEnd.value.copy(p);
293
+
294
+ // Use lazy brush for smooth drawing when drawing is active
295
+ if (drawing && lazyBrush.enabled) {
296
+ // Update lazy brush and get smoothed points
297
+ const segments = lazyBrush.updateStroke(p);
298
+
299
+ // Draw each segment (segments come as pairs: [lastPoint, newPoint])
300
+ for (let i = 0; i < segments.length; i += 2) {
301
+ if (segments[i] && segments[i + 1]) {
302
+ // Draw from last point to new point
303
+ activeLayer.drawPath(segments[i + 1], segments[i], true);
304
+ }
305
+ }
306
+
307
+ // Update cursor to show brush position
308
+ const brushPos = lazyBrush.getBrushPosition();
309
+ if (brushPos) {
310
+ activeLayer.drawPath(brushPos, brushPos, false);
311
+ // Update outline to follow lazy brush position
312
+ outlineMaterial.uniforms.uStart.value.copy(brushPos);
313
+ outlineMaterial.uniforms.uEnd.value.copy(brushPos);
314
+ } else {
315
+ // Show cursor at actual position if no brush position
316
+ activeLayer.drawPath(p, p, false);
317
+ }
318
+ } else if (drawing && !lazyBrush.enabled) {
319
+ // Drawing but lazy brush is disabled
320
+ if (!lastPos) {
321
+ lastPos = p.clone();
322
+ }
323
+ activeLayer.drawPath(p, lastPos, true);
324
+ lastPos = p.clone();
325
+ } else {
326
+ // Just hovering, show cursor
327
+ activeLayer.drawPath(p, p, false);
328
+ }
329
+ }
330
+
331
+ function isVisible(layer: AnnotationLayerData) {
332
+ // Don't show DM layers to players
333
+ return !(mode === StageMode.Player && layer.visibility === StageMode.DM);
334
+ }
335
+
336
+ function hasEffect(layer: AnnotationLayerData) {
337
+ // Check if the layer has an effect (not None)
338
+ return layer.effect?.type !== undefined && layer.effect.type !== AnnotationEffect.None;
339
+ }
340
+
341
+ /**
342
+ * Clears the annotation layer
343
+ */
344
+ export function clear(layerId: string) {
345
+ if (layerId) {
346
+ const layer = layers.find((layer) => layer.getId() === layerId);
347
+ if (layer) {
348
+ layer.clear();
349
+ onAnnotationUpdate(layerId, toPng());
350
+ }
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Serializes the annotation layer image data into a binary buffer
356
+ * @return A binary buffer
357
+ */
358
+ export async function toPng(): Promise<Blob> {
359
+ // For now, return the active layer's PNG or an empty blob
360
+ return (await activeLayer?.toPng()) ?? new Blob();
361
+ }
362
+
363
+ /**
364
+ * Exports the active annotation layer state as RLE-encoded data
365
+ * @returns RLE encoded Uint8Array
366
+ */
367
+ export async function toRLE(): Promise<Uint8Array> {
368
+ return (await activeLayer?.toRLE()) ?? new Uint8Array();
369
+ }
370
+
371
+ /**
372
+ * Loads RLE-encoded data into the active annotation layer
373
+ * @param rleData RLE encoded data
374
+ * @param width Image width
375
+ * @param height Image height
376
+ */
377
+ export async function fromRLE(rleData: Uint8Array, width: number, height: number) {
378
+ return activeLayer?.fromRLE(rleData, width, height);
379
+ }
380
+
381
+ /**
382
+ * Loads RLE-encoded data into a specific annotation layer by ID
383
+ * @param layerId The ID of the annotation layer
384
+ * @param rleData RLE encoded data
385
+ */
386
+ export async function loadMask(layerId: string, rleData: Uint8Array) {
387
+ // Filter out null entries that may exist before components are rendered
388
+ const layer = layers.find((layer) => layer && layer.getId() === layerId);
389
+ if (layer) {
390
+ return layer.fromRLE(rleData, 1024, 1024);
391
+ }
392
+ }
393
+ </script>
394
+
395
+ <LayerInput
396
+ id="annotation"
397
+ {isActive}
398
+ layerSize={{ width: 1, height: 1 }}
399
+ target={mesh}
400
+ {onMouseDown}
401
+ onMouseMove={draw}
402
+ {onMouseUp}
403
+ {onMouseLeave}
404
+ />
405
+
406
+ <!--
407
+ Invisible mesh used for input detection.
408
+ The plane geometry is larger than the map size to allow cursor
409
+ events to be detected outside of the fog of war layer.
410
+ -->
411
+ <T.Mesh bind:ref={mesh} name="annotationInput" layers={isActive ? [SceneLayer.Input] : undefined}>
412
+ <T.MeshBasicMaterial visible={false} />
413
+ <T.PlaneGeometry args={[2 * display.resolution.x, 2 * display.resolution.y]} />
414
+ </T.Mesh>
415
+
416
+ <!-- Brush outline for annotation tool -->
417
+ <T.Mesh
418
+ bind:ref={outlineMesh}
419
+ name="annotationToolOutline"
420
+ layers={[SceneLayer.Overlay]}
421
+ scale={[display.resolution.x, display.resolution.y, 1]}
422
+ renderOrder={SceneLayerOrder.Cursor}
423
+ >
424
+ <T is={outlineMaterial} transparent={true} depthTest={false} />
425
+ <T.PlaneGeometry />
426
+ </T.Mesh>
427
+
428
+ <!--
429
+ Effect annotations render on Main layer (under fog, with post-processing).
430
+ Color annotations render on Overlay layer (over fog, no post-processing).
431
+ -->
432
+ <T.Mesh name="annotationLayer" scale={[display.resolution.x, display.resolution.y, 1]}>
433
+ {#each props.layers as layer, index (layer.id)}
434
+ <T.Mesh
435
+ name={layer.id}
436
+ visible={isVisible(layer)}
437
+ position.z={(props.layers.length - index) * 0.001}
438
+ layers={hasEffect(layer) ? [SceneLayer.Main] : [SceneLayer.Overlay]}
439
+ renderOrder={hasEffect(layer) ? SceneLayerOrder.EffectAnnotation : SceneLayerOrder.Annotation}
440
+ >
441
+ <AnnotationMaterial bind:this={layers[index]} props={layer} {display} lineWidth={props.lineWidth} />
442
+ <T.PlaneGeometry />
443
+ </T.Mesh>
444
+ {/each}
445
+ </T.Mesh>
@@ -0,0 +1,167 @@
1
+ <script lang="ts">
2
+ import * as THREE from 'three';
3
+ import { T, useTask } from '@threlte/core';
4
+ import { onDestroy } from 'svelte';
5
+ import chroma from 'chroma-js';
6
+ import DrawingMaterial from '../DrawingLayer/DrawingMaterial.svelte';
7
+ import { type AnnotationLayerData, AnnotationEffect } from './types';
8
+ import { clippingPlaneStore } from '../../helpers/clippingPlaneStore.svelte';
9
+ import { DrawMode, ToolType, InitialState } from '../DrawingLayer/types';
10
+ import type { DisplayProps } from '../Stage/types';
11
+
12
+ import annotationEffectsFragmentShader from '../../shaders/AnnotationEffects.frag?raw';
13
+ import annotationVertexShader from '../../shaders/default.vert?raw';
14
+
15
+ interface Props {
16
+ props: AnnotationLayerData;
17
+ display: DisplayProps;
18
+ lineWidth?: number;
19
+ }
20
+
21
+ const { props, display, lineWidth = 2.0 }: Props = $props();
22
+
23
+ const lineWidthPixels = $derived.by(() => {
24
+ const textureSize = Math.min(display.resolution.x, display.resolution.y);
25
+ return Math.round(textureSize * (lineWidth / 100));
26
+ });
27
+
28
+ let size = $derived({ width: display.resolution.x, height: display.resolution.y });
29
+
30
+ let drawMaterial: DrawingMaterial;
31
+
32
+ const hexToRGB = (hex: string): THREE.Vector3 => {
33
+ const [r, g, b] = chroma(hex).gl();
34
+ return new THREE.Vector3(r, g, b);
35
+ };
36
+
37
+ // Hardcoded color for Magic effect (saturated purple)
38
+ const MAGIC_EFFECT_COLOR = '#9333ea';
39
+
40
+ // Use hardcoded color for Magic effect, otherwise use layer color
41
+ let colorUniform = $derived(
42
+ props.effect?.type === AnnotationEffect.Magic ? hexToRGB(MAGIC_EFFECT_COLOR) : hexToRGB(props.color)
43
+ );
44
+
45
+ const getEffectType = () => props.effect?.type ?? AnnotationEffect.None;
46
+ const getEffectSpeed = () => props.effect?.speed ?? 1.0;
47
+ const getEffectIntensity = () => props.effect?.intensity ?? 1.0;
48
+ const getEffectSoftness = () => props.effect?.softness ?? 0.5;
49
+ const getEffectBorder = () => props.effect?.border ?? 0.5;
50
+ const getEffectRoughness = () => props.effect?.roughness ?? 0.0;
51
+
52
+ let material = new THREE.ShaderMaterial({
53
+ defines: {
54
+ NUM_CLIPPING_PLANES: 4
55
+ },
56
+ uniforms: {
57
+ uMaskTexture: { value: null },
58
+ uTime: { value: 0.0 },
59
+ uEffectType: { value: getEffectType() },
60
+ uBaseColor: { value: hexToRGB(props.color) },
61
+ uOpacity: { value: props.opacity },
62
+ uSpeed: { value: getEffectSpeed() },
63
+ uIntensity: { value: getEffectIntensity() },
64
+ uSoftness: { value: getEffectSoftness() },
65
+ uBorder: { value: getEffectBorder() },
66
+ uRoughness: { value: getEffectRoughness() },
67
+ uEdgeMinMipMapLevel: { value: 0 },
68
+ uEdgeMaxMipMapLevel: { value: 4 },
69
+ uClippingPlanes: new THREE.Uniform(
70
+ clippingPlaneStore.value.map((p) => new THREE.Vector4(p.normal.x, p.normal.y, p.normal.z, p.constant))
71
+ )
72
+ },
73
+ transparent: true,
74
+ depthWrite: false, // Prevent transparent overlay from affecting depth buffer
75
+ fragmentShader: annotationEffectsFragmentShader,
76
+ vertexShader: annotationVertexShader
77
+ });
78
+
79
+ $effect(() => {
80
+ material.uniforms.uBaseColor.value.copy(colorUniform);
81
+ material.uniforms.uOpacity.value = props.opacity;
82
+ material.uniforms.uEffectType.value = getEffectType();
83
+ material.uniforms.uSpeed.value = getEffectSpeed();
84
+ material.uniforms.uIntensity.value = getEffectIntensity();
85
+ material.uniforms.uSoftness.value = getEffectSoftness();
86
+ material.uniforms.uBorder.value = getEffectBorder();
87
+ material.uniforms.uRoughness.value = getEffectRoughness();
88
+ // Update clipping planes in place to avoid allocating new Vector4 objects
89
+ const planes = clippingPlaneStore.value;
90
+ for (let i = 0; i < planes.length; i++) {
91
+ const p = planes[i];
92
+ material.uniforms.uClippingPlanes.value[i].set(p.normal.x, p.normal.y, p.normal.z, p.constant);
93
+ }
94
+ material.uniformsNeedUpdate = true;
95
+ });
96
+
97
+ useTask((delta) => {
98
+ material.uniforms.uTime.value += delta;
99
+ });
100
+
101
+ export const getId = () => props.id;
102
+
103
+ export const revertChanges = () => {
104
+ drawMaterial.revert();
105
+ };
106
+
107
+ export const resetCursor = () => {
108
+ if (drawMaterial) {
109
+ drawMaterial.resetCursor();
110
+ }
111
+ };
112
+
113
+ export const clear = () => {
114
+ drawMaterial.clear();
115
+ };
116
+
117
+ export const drawPath = (start: THREE.Vector2, last: THREE.Vector2 | null = null, persist: boolean = false) => {
118
+ drawMaterial.drawPath(start, last, persist);
119
+ };
120
+
121
+ export const toPng = async (): Promise<Blob> => {
122
+ return drawMaterial.toPng();
123
+ };
124
+
125
+ export const toRLE = async (): Promise<Uint8Array> => {
126
+ return drawMaterial.toRLE();
127
+ };
128
+
129
+ export const fromRLE = async (rleData: Uint8Array, width: number, height: number) => {
130
+ return drawMaterial.fromRLE(rleData, width, height);
131
+ };
132
+
133
+ onDestroy(() => {
134
+ material.dispose();
135
+ });
136
+ </script>
137
+
138
+ <DrawingMaterial
139
+ bind:this={drawMaterial}
140
+ props={{
141
+ url: props.url,
142
+ opacity: {
143
+ dm: props.opacity,
144
+ player: props.opacity
145
+ },
146
+ tool: {
147
+ mode: DrawMode.Draw,
148
+ size: lineWidthPixels,
149
+ type: ToolType.Brush
150
+ }
151
+ }}
152
+ initialState={InitialState.Clear}
153
+ {size}
154
+ onRender={(texture) => {
155
+ material.uniforms.uBaseColor.value.copy(colorUniform);
156
+ material.uniforms.uMaskTexture.value = texture;
157
+ material.uniformsNeedUpdate = true;
158
+ }}
159
+ />
160
+
161
+ {#snippet attachMaterial()}
162
+ {material}
163
+ {/snippet}
164
+
165
+ <T is={material}>
166
+ {@render attachMaterial()}
167
+ </T>