-
Notifications
You must be signed in to change notification settings - Fork 16
Open
Labels
enhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomershacktoberfesthelp wantedExtra attention is neededExtra attention is needed
Description
Feature Description
Implement comprehensive touch gesture support for mobile and tablet users, including pinch-to-zoom, two-finger pan, and multi-touch drawing capabilities.
Problem Statement
Current mobile experience is limited:
- No pinch-to-zoom support
- Difficult to navigate large canvases on mobile
- Single-touch only (no multi-finger gestures)
- Touch precision issues
- No palm rejection
- Zoom controls require buttons instead of gestures
Proposed Features
1. Touch Gesture Handler
// frontend/src/utils/TouchGestureHandler.js
export class TouchGestureHandler {
constructor(canvas) {
this.canvas = canvas;
this.touches = new Map();
this.initialPinchDistance = null;
this.initialZoom = 1;
this.isPanning = false;
this.isDrawing = false;
}
handleTouchStart(e) {
e.preventDefault();
const touches = Array.from(e.touches);
if (touches.length === 1) {
// Single touch - drawing
this.startDrawing(touches[0]);
} else if (touches.length === 2) {
// Two fingers - zoom/pan
this.startPinchZoom(touches);
}
}
handleTouchMove(e) {
e.preventDefault();
const touches = Array.from(e.touches);
if (touches.length === 1 && this.isDrawing) {
this.continueDrawing(touches[0]);
} else if (touches.length === 2) {
this.updatePinchZoom(touches);
}
}
handleTouchEnd(e) {
e.preventDefault();
if (e.touches.length === 0) {
this.endDrawing();
this.endPinchZoom();
}
}
startDrawing(touch) {
this.isDrawing = true;
const point = this.getTouchPoint(touch);
this.canvas.startStroke(point.x, point.y);
}
continueDrawing(touch) {
const point = this.getTouchPoint(touch);
this.canvas.continueStroke(point.x, point.y);
}
endDrawing() {
if (this.isDrawing) {
this.canvas.endStroke();
this.isDrawing = false;
}
}
startPinchZoom(touches) {
this.initialPinchDistance = this.getDistance(touches[0], touches[1]);
this.initialZoom = this.canvas.zoom;
this.isPanning = true;
}
updatePinchZoom(touches) {
const currentDistance = this.getDistance(touches[0], touches[1]);
const scale = currentDistance / this.initialPinchDistance;
// Update zoom
const newZoom = this.initialZoom * scale;
this.canvas.setZoom(newZoom);
// Calculate pan based on midpoint
const midpoint = this.getMidpoint(touches[0], touches[1]);
this.canvas.panTo(midpoint.x, midpoint.y);
}
endPinchZoom() {
this.initialPinchDistance = null;
this.isPanning = false;
}
getDistance(touch1, touch2) {
const dx = touch2.clientX - touch1.clientX;
const dy = touch2.clientY - touch1.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
getMidpoint(touch1, touch2) {
return {
x: (touch1.clientX + touch2.clientX) / 2,
y: (touch1.clientY + touch2.clientY) / 2
};
}
getTouchPoint(touch) {
const rect = this.canvas.element.getBoundingClientRect();
return {
x: (touch.clientX - rect.left) / this.canvas.zoom,
y: (touch.clientY - rect.top) / this.canvas.zoom
};
}
}2. Palm Rejection
// frontend/src/utils/PalmRejection.js
export class PalmRejection {
constructor() {
this.minTouchRadius = 5; // Minimum radius for valid touch (stylus)
this.maxTouchRadius = 50; // Maximum radius (palm)
}
isPalmTouch(touch) {
// Detect palm based on touch radius
const radius = touch.radiusX || touch.radiusY || 0;
if (radius > this.maxTouchRadius) {
return true; // Likely palm
}
// Check for stylus support
if (touch.touchType === 'stylus') {
return false; // Definitely stylus
}
// Check for Apple Pencil or similar
if (touch.force && touch.force > 0) {
return false; // Pressure-sensitive stylus
}
return false;
}
filterPalmTouches(touches) {
}
}3. Responsive Zoom Controls
// frontend/src/components/MobileZoomControls.jsx
export function MobileZoomControls({ zoom, onZoomChange, onFitToScreen }) {
const isMobile = useMediaQuery('(max-width: 768px)');
return (
<Box
sx={{
position: 'fixed',
bottom: 80,
right: 16,
display: 'flex',
flexDirection: 'column',
gap: 1
}}
>
<Fab size="small" onClick={() => onZoomChange(zoom * 1.2)}>
<ZoomInIcon />
</Fab>
<Fab size="small" onClick={() => onZoomChange(zoom / 1.2)}>
<ZoomOutIcon />
</Fab>
<Fab size="small" onClick={onFitToScreen}>
<FitScreenIcon />
</Fab>
<Chip
label={`${Math.round(zoom * 100)}%`}
size="small"
sx={{ mt: 1 }}
/>
</Box>
);
}4. Touch-Optimized Toolbar
// frontend/src/components/MobileToolbar.jsx
export function MobileToolbar({ currentTool, onToolChange }) {
const [drawerOpen, setDrawerOpen] = useState(false);
const isMobile = useMediaQuery('(max-width: 768px)');
return (
<>
<Fab
color="primary"
sx={{ position: 'fixed', bottom: 16, right: 16 }}
onClick={() => setDrawerOpen(true)}
>
<PaletteIcon />
</Fab>
<Drawer
anchor="bottom"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
>
<Box p={2}>
<Typography variant="h6" gutterBottom>
Drawing Tools
</Typography>
<Grid container spacing={2}>
{tools.map(tool => (
<Grid item xs={3} key={tool.id}>
<Button
variant={currentTool === tool.id ? 'contained' : 'outlined'}
fullWidth
onClick={() => {
onToolChange(tool.id);
setDrawerOpen(false);
}}
sx={{ height: 80, flexDirection: 'column' }}
>
{tool.icon}
<Typography variant="caption">{tool.label}</Typography>
</Button>
</Grid>
))}
</Grid>
<Divider sx={{ my: 2 }} />
{/* Color picker */}
<Typography variant="subtitle2" gutterBottom>
Color
</Typography>
<ColorPicker />
{/* Brush size */}
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
Brush Size
</Typography>
<Slider
min={1}
max={20}
defaultValue={5}
marks
valueLabelDisplay="auto"
/>
</Box>
</Drawer>
</>
);
}5. Multi-Touch Drawing Support
// frontend/src/components/Canvas.js (modifications)
class Canvas {
enableMultiTouch() {
this.activeStrokes = new Map(); // touchId -> stroke
this.canvas.addEventListener('touchstart', (e) => {
for (const touch of e.changedTouches) {
this.startStrokeForTouch(touch);
}
}
});
this.canvas.addEventListener('touchmove', (e) => {
for (const touch of e.changedTouches) {
if (this.activeStrokes.has(touch.identifier)) {
this.continueStrokeForTouch(touch);
}
}
});
this.canvas.addEventListener('touchend', (e) => {
for (const touch of e.changedTouches) {
this.endStrokeForTouch(touch);
}
});
}
startStrokeForTouch(touch) {
const point = this.getTouchPoint(touch);
const stroke = {
id: generateId(),
points: [point],
color: this.currentColor,
width: this.currentWidth,
touchId: touch.identifier
};
this.activeStrokes.set(touch.identifier, stroke);
}
continueStrokeForTouch(touch) {
const stroke = this.activeStrokes.get(touch.identifier);
if (stroke) {
const point = this.getTouchPoint(touch);
stroke.points.push(point);
this.renderStroke(stroke);
}
}
endStrokeForTouch(touch) {
const stroke = this.activeStrokes.get(touch.identifier);
if (stroke) {
this.commitStroke(stroke);
this.activeStrokes.delete(touch.identifier);
}
}
}6. Smooth Zoom Animation
// frontend/src/utils/SmoothZoom.js
export class SmoothZoom {
constructor(canvas) {
this.canvas = canvas;
this.targetZoom = 1;
this.currentZoom = 1;
this.animating = false;
}
setZoom(targetZoom, animate = true) {
this.targetZoom = Math.max(0.1, Math.min(10, targetZoom));
if (animate) {
this.startAnimation();
} else {
this.currentZoom = this.targetZoom;
this.canvas.zoom = this.currentZoom;
}
}
startAnimation() {
if (this.animating) return;
this.animating = true;
this.animate();
}
animate() {
const diff = this.targetZoom - this.currentZoom;
if (Math.abs(diff) < 0.01) {
this.currentZoom = this.targetZoom;
this.canvas.zoom = this.currentZoom;
this.animating = false;
return;
}
this.currentZoom += diff * 0.2; // Easing
this.canvas.zoom = this.currentZoom;
requestAnimationFrame(() => this.animate());
}
}Mobile-Specific Optimizations
1. Reduce Rendering Quality on Mobile
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const renderQuality = isMobile ? 'low' : 'high';2. Touch-Friendly Hit Targets
- Minimum 44x44px touch targets
- Increased spacing between toolbar buttons
- Larger color swatches
3. Prevent Overscroll
body {
overscroll-behavior: none;
touch-action: none;
}Files to Create/Modify
Frontend:
frontend/src/utils/TouchGestureHandler.js⭐ (NEW)frontend/src/utils/PalmRejection.js⭐ (NEW)frontend/src/utils/SmoothZoom.js⭐ (NEW)frontend/src/components/MobileZoomControls.jsx⭐ (NEW)frontend/src/components/MobileToolbar.jsx⭐ (NEW)frontend/src/components/Canvas.js(MODIFY - add touch support)
Benefits
- Native mobile app feel
- Intuitive gesture controls
- Better tablet/stylus support
- Improved accessibility
- Professional mobile UX
- Competitive with native apps
Testing Requirements
- Test on iOS Safari
- Test on Android Chrome
- Test with iPad + Apple Pencil
- Test with Samsung tablets + S Pen
- Verify palm rejection accuracy
- Test multi-user mobile sessions
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomershacktoberfesthelp wantedExtra attention is neededExtra attention is needed