Skip to content

Commit c6b1bb4

Browse files
vinc3m1claude
andcommitted
Improve color wheel drag behavior to track outside circle bounds
Enhanced the color picker to allow dragging beyond the circle while maintaining color selection at the circle's edge. The picker now follows the mouse direction when dragged outside, snapping to the nearest point on the circle's perimeter. Key changes: - Added document-level mouse listeners during drag operations - Removed distance boundary check that stopped tracking outside circle - Only allows drag initiation from clicks inside the circle - Refactored event handling to use stable callback references 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 23b26c7 commit c6b1bb4

1 file changed

Lines changed: 56 additions & 37 deletions

File tree

src/components/ColorWheel.tsx

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState } from 'react';
1+
import { useCallback, useEffect, useRef, useState } from 'react';
22
import { hsvToRgb, rgbToHsv } from '../utils/colorConversion';
33

44
interface ColorWheelProps {
@@ -12,6 +12,56 @@ export function ColorWheel({ color, onChange, size = 200 }: ColorWheelProps) {
1212
const [isDragging, setIsDragging] = useState(false);
1313
const hsv = rgbToHsv(color.r, color.g, color.b);
1414

15+
const handleInteraction = useCallback((clientX: number, clientY: number) => {
16+
const canvas = canvasRef.current;
17+
if (!canvas) return;
18+
19+
const rect = canvas.getBoundingClientRect();
20+
const x = clientX - rect.left;
21+
const y = clientY - rect.top;
22+
23+
const centerX = size / 2;
24+
const centerY = size / 2;
25+
const radius = size / 2 - 10;
26+
27+
// Calculate distance from center
28+
const dx = x - centerX;
29+
const dy = y - centerY;
30+
const distance = Math.sqrt(dx * dx + dy * dy);
31+
32+
// Calculate hue from angle
33+
let angle = Math.atan2(dy, dx) * (180 / Math.PI) + 90;
34+
if (angle < 0) angle += 360;
35+
36+
// Calculate saturation from distance, clamped to radius
37+
const saturation = Math.min(distance / radius, 1);
38+
39+
// Convert to RGB and update
40+
const newColor = hsvToRgb(angle, saturation, 1);
41+
onChange(newColor);
42+
}, [onChange, size]);
43+
44+
// Add document-level mouse listeners when dragging
45+
useEffect(() => {
46+
if (!isDragging) return;
47+
48+
const handleDocumentMouseMove = (e: MouseEvent) => {
49+
handleInteraction(e.clientX, e.clientY);
50+
};
51+
52+
const handleDocumentMouseUp = () => {
53+
setIsDragging(false);
54+
};
55+
56+
document.addEventListener('mousemove', handleDocumentMouseMove);
57+
document.addEventListener('mouseup', handleDocumentMouseUp);
58+
59+
return () => {
60+
document.removeEventListener('mousemove', handleDocumentMouseMove);
61+
document.removeEventListener('mouseup', handleDocumentMouseUp);
62+
};
63+
}, [isDragging, handleInteraction]);
64+
1565
// Draw the color wheel and indicator
1666
useEffect(() => {
1767
const canvas = canvasRef.current;
@@ -66,14 +116,13 @@ export function ColorWheel({ color, onChange, size = 200 }: ColorWheelProps) {
66116
ctx.stroke();
67117
}, [hsv, size]);
68118

69-
const handleInteraction = (e: React.MouseEvent<HTMLCanvasElement>) => {
119+
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
70120
const canvas = canvasRef.current;
71121
if (!canvas) return;
72122

73123
const rect = canvas.getBoundingClientRect();
74124
const x = e.clientX - rect.left;
75125
const y = e.clientY - rect.top;
76-
77126
const centerX = size / 2;
78127
const centerY = size / 2;
79128
const radius = size / 2 - 10;
@@ -83,50 +132,20 @@ export function ColorWheel({ color, onChange, size = 200 }: ColorWheelProps) {
83132
const dy = y - centerY;
84133
const distance = Math.sqrt(dx * dx + dy * dy);
85134

86-
// Clamp to wheel radius
87-
if (distance > radius) return;
88-
89-
// Calculate hue from angle
90-
let angle = Math.atan2(dy, dx) * (180 / Math.PI) + 90;
91-
if (angle < 0) angle += 360;
92-
93-
// Calculate saturation from distance
94-
const saturation = Math.min(distance / radius, 1);
95-
96-
// Convert to RGB and update
97-
const newColor = hsvToRgb(angle, saturation, 1);
98-
onChange(newColor);
99-
};
100-
101-
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
102-
setIsDragging(true);
103-
handleInteraction(e);
104-
};
105-
106-
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
107-
if (isDragging) {
108-
handleInteraction(e);
135+
// Only start dragging if click is inside the circle
136+
if (distance <= radius) {
137+
setIsDragging(true);
138+
handleInteraction(e.clientX, e.clientY);
109139
}
110140
};
111141

112-
const handleMouseUp = () => {
113-
setIsDragging(false);
114-
};
115-
116-
const handleMouseLeave = () => {
117-
setIsDragging(false);
118-
};
119-
120142
return (
121143
<canvas
122144
ref={canvasRef}
123145
width={size}
124146
height={size}
125147
className="cursor-crosshair"
126148
onMouseDown={handleMouseDown}
127-
onMouseMove={handleMouseMove}
128-
onMouseUp={handleMouseUp}
129-
onMouseLeave={handleMouseLeave}
130149
/>
131150
);
132151
}

0 commit comments

Comments
 (0)