feat(skill-tree): add touch support and improve modal handling
This commit is contained in:
1 parent
f091bf6d55
commit
7f0f86d14e
1 file changed
+62
-13
@@ -18,26 +18,35 @@
|
|||||||
let windowWidth = 0;
|
let windowWidth = 0;
|
||||||
let windowHeight = 0;
|
let windowHeight = 0;
|
||||||
|
|
||||||
|
let modalOpenedAt = 0;
|
||||||
|
|
||||||
function openModal(node: TreeNode) {
|
function openModal(node: TreeNode) {
|
||||||
selectedNode = node;
|
selectedNode = node;
|
||||||
modalVisible = true;
|
modalVisible = true;
|
||||||
|
modalOpenedAt = Date.now();
|
||||||
// Hide tooltip when modal opens
|
// Hide tooltip when modal opens
|
||||||
hoveredNode = null;
|
hoveredNode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
modalVisible = false;
|
// Prevent closing if modal just opened (within 300ms)
|
||||||
setTimeout(() => {
|
// This fixes touch events "bleeding through" on mobile
|
||||||
selectedNode = null;
|
if (Date.now() - modalOpenedAt < 300) {
|
||||||
}, 200);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modalVisible = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
selectedNode = null;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
function getCategoryColorHex(category: TreeNode['category']): string {
|
function getCategoryColorHex(category: TreeNode['category']): string {
|
||||||
const colorMap: Record<TreeNode['category'], string> = {
|
const colorMap: Record<TreeNode['category'], string> = {
|
||||||
origin: '#ffd700',
|
origin: '#ffd700',
|
||||||
experience: '#4ecdc4',
|
experience: '#4ecdc4',
|
||||||
projects: '#ff6b6b',
|
projects: '#ff6b6b',
|
||||||
skills: '#95e1d3',
|
skills: '#f9a825',
|
||||||
education: '#dda0dd',
|
education: '#dda0dd',
|
||||||
};
|
};
|
||||||
return colorMap[category];
|
return colorMap[category];
|
||||||
@@ -54,6 +63,8 @@
|
|||||||
handleResize();
|
handleResize();
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
|
||||||
const app = new Application();
|
const app = new Application();
|
||||||
|
|
||||||
await app.init({
|
await app.init({
|
||||||
@@ -245,16 +256,45 @@
|
|||||||
}
|
}
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
|
||||||
|
// ========== TOUCH EVENTS FOR PANNING & ZOOMING ==========
|
||||||
|
let touchStartedOnNode = false;
|
||||||
|
|
||||||
|
app.canvas.addEventListener('touchstart', (e: TouchEvent) => {
|
||||||
|
// Check if touch started on a node by seeing if the target is interactive
|
||||||
|
touchStartedOnNode = false;
|
||||||
|
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
// Single touch - prepare for potential panning
|
||||||
|
isDragging = true;
|
||||||
|
hasMoved = false;
|
||||||
|
isTouchZooming = false;
|
||||||
|
dragStartX = e.touches[0].clientX;
|
||||||
|
dragStartY = e.touches[0].clientY;
|
||||||
|
containerStartX = treeContainer.x;
|
||||||
|
containerStartY = treeContainer.y;
|
||||||
|
} else if (e.touches.length === 2) {
|
||||||
|
// Two touches - start pinch zoom
|
||||||
|
isDragging = false;
|
||||||
|
isTouchZooming = true;
|
||||||
|
hasMoved = true; // Pinch zoom counts as movement
|
||||||
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
lastTouchDistance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
app.canvas.addEventListener('touchmove', (e: TouchEvent) => {
|
app.canvas.addEventListener('touchmove', (e: TouchEvent) => {
|
||||||
if (e.touches.length === 1 && isDragging && !isTouchZooming) {
|
if (e.touches.length === 1 && isDragging && !isTouchZooming) {
|
||||||
// Single touch - panning
|
// Single touch - panning
|
||||||
const dx = e.touches[0].clientX - dragStartX;
|
const dx = e.touches[0].clientX - dragStartX;
|
||||||
const dy = e.touches[0].clientY - dragStartY;
|
const dy = e.touches[0].clientY - dragStartY;
|
||||||
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
|
|
||||||
|
// Only count as moved if we've moved more than a small threshold
|
||||||
|
if (Math.abs(dx) > 10 || Math.abs(dy) > 10) {
|
||||||
hasMoved = true;
|
hasMoved = true;
|
||||||
|
treeContainer.x = containerStartX + dx;
|
||||||
|
treeContainer.y = containerStartY + dy;
|
||||||
}
|
}
|
||||||
treeContainer.x = containerStartX + dx;
|
|
||||||
treeContainer.y = containerStartY + dy;
|
|
||||||
} else if (e.touches.length === 2 && isTouchZooming) {
|
} else if (e.touches.length === 2 && isTouchZooming) {
|
||||||
// Two touches - pinch zoom
|
// Two touches - pinch zoom
|
||||||
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
@@ -296,6 +336,7 @@
|
|||||||
// Went from 2 touches to 1 - reset pan start
|
// Went from 2 touches to 1 - reset pan start
|
||||||
isTouchZooming = false;
|
isTouchZooming = false;
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
|
hasMoved = true; // Consider this movement since we were zooming
|
||||||
dragStartX = e.touches[0].clientX;
|
dragStartX = e.touches[0].clientX;
|
||||||
dragStartY = e.touches[0].clientY;
|
dragStartY = e.touches[0].clientY;
|
||||||
containerStartX = treeContainer.x;
|
containerStartX = treeContainer.x;
|
||||||
@@ -332,7 +373,7 @@
|
|||||||
case 'large':
|
case 'large':
|
||||||
return 30 * mobileFactor;
|
return 30 * mobileFactor;
|
||||||
case 'medium':
|
case 'medium':
|
||||||
return 20 * mobileFactor;
|
return 25 * mobileFactor;
|
||||||
case 'small':
|
case 'small':
|
||||||
return 20 * mobileFactor;
|
return 20 * mobileFactor;
|
||||||
default:
|
default:
|
||||||
@@ -426,6 +467,10 @@
|
|||||||
profileContainer.eventMode = 'static';
|
profileContainer.eventMode = 'static';
|
||||||
profileContainer.cursor = 'pointer';
|
profileContainer.cursor = 'pointer';
|
||||||
|
|
||||||
|
profileContainer.on('pointerdown', () => {
|
||||||
|
hasMoved = false;
|
||||||
|
});
|
||||||
|
|
||||||
profileContainer.on('pointerup', () => {
|
profileContainer.on('pointerup', () => {
|
||||||
if (!hasMoved) {
|
if (!hasMoved) {
|
||||||
openModal(node);
|
openModal(node);
|
||||||
@@ -435,13 +480,13 @@
|
|||||||
profileContainer.on('pointerover', () => {
|
profileContainer.on('pointerover', () => {
|
||||||
gsap.to(profileContainer.scale, { x: 1.2, y: 1.2, duration: 0.2 });
|
gsap.to(profileContainer.scale, { x: 1.2, y: 1.2, duration: 0.2 });
|
||||||
gsap.to(glowGraphics, { alpha: 1.5, duration: 0.2 });
|
gsap.to(glowGraphics, { alpha: 1.5, duration: 0.2 });
|
||||||
if (!isMobile) hoveredNode = node;
|
if (!isTouchDevice) hoveredNode = node;
|
||||||
});
|
});
|
||||||
|
|
||||||
profileContainer.on('pointerout', () => {
|
profileContainer.on('pointerout', () => {
|
||||||
gsap.to(profileContainer.scale, { x: 1, y: 1, duration: 0.2 });
|
gsap.to(profileContainer.scale, { x: 1, y: 1, duration: 0.2 });
|
||||||
gsap.to(glowGraphics, { alpha: 1, duration: 0.2 });
|
gsap.to(glowGraphics, { alpha: 1, duration: 0.2 });
|
||||||
hoveredNode = null;
|
if (!isTouchDevice) hoveredNode = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
treeContainer.addChild(profileContainer);
|
treeContainer.addChild(profileContainer);
|
||||||
@@ -488,6 +533,10 @@
|
|||||||
nodeContainer.eventMode = 'static';
|
nodeContainer.eventMode = 'static';
|
||||||
nodeContainer.cursor = 'pointer';
|
nodeContainer.cursor = 'pointer';
|
||||||
|
|
||||||
|
nodeContainer.on('pointerdown', () => {
|
||||||
|
hasMoved = false;
|
||||||
|
});
|
||||||
|
|
||||||
nodeContainer.on('pointerup', () => {
|
nodeContainer.on('pointerup', () => {
|
||||||
if (!hasMoved) {
|
if (!hasMoved) {
|
||||||
openModal(node);
|
openModal(node);
|
||||||
@@ -497,13 +546,13 @@
|
|||||||
nodeContainer.on('pointerover', () => {
|
nodeContainer.on('pointerover', () => {
|
||||||
gsap.to(nodeContainer.scale, { x: 1.2, y: 1.2, duration: 0.2 });
|
gsap.to(nodeContainer.scale, { x: 1.2, y: 1.2, duration: 0.2 });
|
||||||
gsap.to(glowGraphics, { alpha: 1.5, duration: 0.2 });
|
gsap.to(glowGraphics, { alpha: 1.5, duration: 0.2 });
|
||||||
if (!isMobile) hoveredNode = node;
|
if (!isTouchDevice) hoveredNode = node;
|
||||||
});
|
});
|
||||||
|
|
||||||
nodeContainer.on('pointerout', () => {
|
nodeContainer.on('pointerout', () => {
|
||||||
gsap.to(nodeContainer.scale, { x: 1, y: 1, duration: 0.2 });
|
gsap.to(nodeContainer.scale, { x: 1, y: 1, duration: 0.2 });
|
||||||
gsap.to(glowGraphics, { alpha: 1, duration: 0.2 });
|
gsap.to(glowGraphics, { alpha: 1, duration: 0.2 });
|
||||||
hoveredNode = null;
|
if (!isTouchDevice) hoveredNode = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
treeContainer.addChild(nodeContainer);
|
treeContainer.addChild(nodeContainer);
|
||||||
|
|||||||
Reference in new issue
Block a user