Skip to content

Commit 6cb0fb2

Browse files
committed
fix: the brush cursor was disappearing because browsers have a limitation on cursor sizes (128 for chrome), to fix this, the brush cursor is now drawn on an overlay canvas. This explains why other web-based drawing tools have inertia (we now have) on the brush cursor.
1 parent b8e1b6e commit 6cb0fb2

File tree

2 files changed

+76
-46
lines changed

2 files changed

+76
-46
lines changed

src/rendered_data_panel.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ export abstract class RenderedDataPanel extends RenderedPanel {
165165
*/
166166
pickRequestPending = false;
167167

168+
private overlay_canvas: HTMLCanvasElement;
169+
private overlay_context: CanvasRenderingContext2D;
170+
168171
private mouseStateForcer = () => this.blockOnPickRequest();
169172
protected isMovingToMousePosition: boolean = false;
170173

@@ -812,6 +815,46 @@ export abstract class RenderedDataPanel extends RenderedPanel {
812815
}
813816
},
814817
);
818+
819+
this.overlay_canvas = document.createElement('canvas');
820+
this.overlay_canvas.style.position = 'absolute';
821+
this.overlay_canvas.style.top = '0';
822+
this.overlay_canvas.style.left = '0';
823+
this.overlay_canvas.style.width = '100%';
824+
this.overlay_canvas.style.height = '100%';
825+
this.overlay_canvas.style.pointerEvents = 'none';
826+
this.overlay_canvas.style.zIndex = '10';
827+
this.element.appendChild(this.overlay_canvas);
828+
this.overlay_context = this.overlay_canvas.getContext('2d')!;
829+
830+
this.boundsUpdated.add(() => {
831+
this.overlay_canvas.width = this.renderViewport.logicalWidth;
832+
this.overlay_canvas.height = this.renderViewport.logicalHeight;
833+
});
834+
}
835+
836+
drawBrushCursor(x: number, y: number, radius: number) {
837+
const ctx = this.overlay_context;
838+
const { logicalWidth, logicalHeight } = this.renderViewport;
839+
840+
ctx.clearRect(0, 0, logicalWidth, logicalHeight);
841+
842+
if (radius > 0) {
843+
ctx.beginPath();
844+
ctx.arc(x, y, radius, 0, 2 * Math.PI);
845+
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
846+
ctx.fill();
847+
ctx.strokeStyle = 'rgba(255, 255, 255, 1)';
848+
ctx.lineWidth = 3;
849+
ctx.stroke();
850+
ctx.strokeStyle = 'rgba(0, 0, 0, 1)';
851+
ctx.lineWidth = 1.5;
852+
ctx.stroke();
853+
}
854+
}
855+
856+
clearOverlay() {
857+
this.overlay_context.clearRect(0, 0, this.overlay_canvas.width, this.overlay_canvas.height);
815858
}
816859

817860
abstract translateDataPointByViewportPixels(

src/ui/voxel_annotations.ts

Lines changed: 33 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@ import type {
1919
UserLayerWithVoxelEditing,
2020
VoxelEditingContext,
2121
} from "#src/layer/vox/index.js";
22-
import type { RenderedDataPanel } from "#src/rendered_data_panel.js";
22+
import { RenderedDataPanel } from "#src/rendered_data_panel.js";
2323
import { StatusMessage } from "#src/status.js";
2424
import { LayerTool, registerTool, type ToolActivation } from "#src/ui/tool.js";
2525
import { vec3 } from "#src/util/geom.js";
2626
import { EventActionMap } from "#src/util/mouse_bindings.js";
2727
import { startRelativeMouseDrag } from "#src/util/mouse_drag.js";
28-
import { NullarySignal } from "#src/util/signal.js";
2928
import { BrushShape } from "#src/voxel_annotation/base.js";
3029

3130
export const BRUSH_TOOL_ID = "vox-brush";
@@ -138,53 +137,41 @@ export class VoxelBrushTool extends BaseVoxelTool {
138137

139138
activate(activation: ToolActivation<this>) {
140139
super.activate(activation);
141-
const getZoom = () => {
142-
const panels = Array.from(
143-
this.layer.manager.root.display.panels,
144-
) as RenderedDataPanel[];
145-
if (panels.length > 0) {
146-
return panels[0].navigationState.zoomFactor.value;
147-
}
148-
return 1.0;
149-
};
150-
151-
const getZoomChangedSignal = () => {
152-
const panels = Array.from(
153-
this.layer.manager.root.display.panels,
154-
) as RenderedDataPanel[];
155-
return panels.length > 0
156-
? panels[0].navigationState.zoomFactor.changed
157-
: new NullarySignal();
158-
};
159-
160-
const updateCursor = () => {
161-
const radiusInVoxels = this.layer.voxBrushRadius.value;
162-
const zoom = getZoom();
163-
const radiusInPixels = Math.max(1, radiusInVoxels / zoom);
164-
const svgSize = 2 * radiusInPixels + 4;
165-
const svgCenter = svgSize / 2;
166-
167-
const svgString = `
168-
<svg xmlns="http://www.w3.org/2000/svg" width="${svgSize}" height="${svgSize}">
169-
<circle cx="${svgCenter}" cy="${svgCenter}" r="${radiusInPixels}"
170-
stroke="white" stroke-width="3" fill="rgba(255, 255, 255, 0.2)" />
171-
<circle cx="${svgCenter}" cy="${svgCenter}" r="${radiusInPixels}"
172-
stroke="black" stroke-width="1.5" fill="rgba(255, 255, 255, 0)" />
173-
</svg>
174-
`.replace(/\s\s+/g, " ");
175-
176-
const cursorURL = `url('data:image/svg+xml;utf8,${encodeURIComponent(svgString)}')`;
177-
this.setCursor(`${cursorURL} ${svgCenter} ${svgCenter}, crosshair`);
178-
};
179-
180-
updateCursor();
181-
activation.registerDisposer(
182-
this.layer.voxBrushRadius.changed.add(updateCursor),
183-
);
184-
activation.registerDisposer(getZoomChangedSignal().add(updateCursor));
140+
185141
activation.registerDisposer(() => {
142+
this.getActivePanel()?.clearOverlay();
186143
this.resetCursor();
187144
});
145+
activation.registerDisposer(
146+
this.mouseState.changed.add(() => {
147+
this.updateBrushOutline();
148+
})
149+
);
150+
}
151+
152+
private getActivePanel(): RenderedDataPanel | undefined {
153+
let activePanel: RenderedDataPanel | undefined;
154+
for (const panel of this.layer.manager.root.display.panels) {
155+
if (panel instanceof RenderedDataPanel) {
156+
if (panel.mouseX !== -1) {
157+
activePanel = panel;
158+
} else {
159+
panel.clearOverlay();
160+
}
161+
}
162+
}
163+
return activePanel;
164+
}
165+
166+
private updateBrushOutline() {
167+
const panel = this.getActivePanel();
168+
if (!panel) return;
169+
170+
const zoom = panel.navigationState.zoomFactor.value;
171+
const radiusInVoxels = this.layer.voxBrushRadius.value;
172+
const radiusInPixels = radiusInVoxels / zoom;
173+
174+
panel.drawBrushCursor(panel.mouseX, panel.mouseY, radiusInPixels);
188175
}
189176

190177
activationCallback(_activation: ToolActivation<this>): void {

0 commit comments

Comments
 (0)