Skip to content

Commit 3a6d1a9

Browse files
koule
1 parent fb6afa9 commit 3a6d1a9

File tree

2 files changed

+216
-97
lines changed

2 files changed

+216
-97
lines changed

static/kvh/app.js

Lines changed: 199 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ function loadVideo(src) {
1313
const physicsData = [];
1414
const main = document.querySelector('main');
1515

16+
// Drag and drop state management
17+
let dragState = {
18+
isDragging: false,
19+
draggedBallIndex: -1,
20+
dragOffset: { x: 0, y: 0 },
21+
lastMousePos: { x: 0, y: 0 },
22+
dragStartTime: 0
23+
};
24+
1625
// Get actual viewport dimensions accounting for mobile quirks
1726
function getViewportDimensions() {
1827
const rect = main.getBoundingClientRect();
@@ -22,23 +31,38 @@ function getViewportDimensions() {
2231
};
2332
}
2433

34+
// Helper function to find which ball is at a given position
35+
function findBallAtPosition(x, y) {
36+
for (let i = 0; i < physicsData.length; i++) {
37+
const data = physicsData[i];
38+
const ballCenterX = data.x + data.radius;
39+
const ballCenterY = data.y + data.radius;
40+
const distance = Math.sqrt(
41+
Math.pow(x - ballCenterX, 2) +
42+
Math.pow(y - ballCenterY, 2)
43+
);
44+
45+
// Check if click/touch is within the ball's radius
46+
if (distance <= data.radius) {
47+
return i;
48+
}
49+
}
50+
return -1;
51+
}
52+
53+
// Helper function to get mouse/touch position relative to main element
54+
function getRelativePosition(clientX, clientY) {
55+
const rect = main.getBoundingClientRect();
56+
return {
57+
x: clientX - rect.left,
58+
y: clientY - rect.top
59+
};
60+
}
61+
2562
// Centralized ball size management
2663
function getBallRadius() {
2764
const viewport = getViewportDimensions();
28-
const minDimension = Math.min(viewport.width, viewport.height);
29-
30-
// Better scaling for mobile devices
31-
if (minDimension < 400) {
32-
// Small mobile screens
33-
return Math.max(25, minDimension / 12);
34-
} else if (minDimension < 800) {
35-
// Larger mobile screens and small tablets
36-
return Math.max(30, minDimension / 15);
37-
} else {
38-
// Desktop and large tablets
39-
const k = Math.round(viewport.width / 200);
40-
return viewport.width / (2 * k);
41-
}
65+
return Math.min(viewport.width, viewport.height) / 7.5; // Simplified - 2x bigger everywhere
4266
}
4367

4468
function setBallSize(video, radius) {
@@ -93,13 +117,18 @@ function updatePhysics() {
93117
const video = main.children[i];
94118
const data = physicsData[i];
95119

120+
// Skip physics updates for the ball being dragged
121+
if (dragState.isDragging && dragState.draggedBallIndex === i) {
122+
continue;
123+
}
124+
96125
data.vy += 0.2;
97126

98127
data.x += data.vx;
99128
data.y += data.vy;
100129
data.r += data.vr;
101130

102-
const maxSpin = 1;
131+
const maxSpin = 3; // Increased from 1 to allow more dramatic spinning
103132
if (data.vr > maxSpin) data.vr = maxSpin;
104133
if (data.vr < -maxSpin) data.vr = -maxSpin;
105134

@@ -109,18 +138,18 @@ function updatePhysics() {
109138
const high_update = 1.05;
110139
const low_update = 1.0;
111140

112-
// Use viewport dimensions instead of window dimensions
141+
// Simple wall collision with spin effects
113142
if (centerX - data.radius <= 0 || centerX + data.radius >= viewport.width) {
114-
data.vx *= -high_update;
115-
data.vy *= low_update;
116-
data.vr *= low_update; // trencle
143+
data.vx *= -1.1;
144+
data.vy += data.vr * 0.5; // Spin affects bounce direction
145+
data.vr *= 0.8; // Lose some spin
117146
data.x = Math.max(0, Math.min(viewport.width - data.radius * 2, data.x));
118147
}
119148

120149
if (centerY - data.radius <= 0 || centerY + data.radius >= viewport.height) {
121-
data.vy *= -high_update;
122-
data.vx *= low_update;
123-
data.vr *= low_update; // slipy
150+
data.vy *= -1.1;
151+
data.vx += data.vr * 0.5; // Spin affects bounce direction
152+
data.vr *= 0.8; // Lose some spin
124153
data.y = Math.max(0, Math.min(viewport.height - data.radius * 2, data.y));
125154
}
126155

@@ -131,6 +160,11 @@ function updatePhysics() {
131160

132161
for (let i = 0; i < physicsData.length; i++) {
133162
for (let j = i + 1; j < physicsData.length; j++) {
163+
// Skip collision detection if either ball is being dragged
164+
if (dragState.isDragging && (dragState.draggedBallIndex === i || dragState.draggedBallIndex === j)) {
165+
continue;
166+
}
167+
134168
const ball1 = physicsData[i];
135169
const ball2 = physicsData[j];
136170

@@ -143,59 +177,34 @@ function updatePhysics() {
143177
const dy = center2Y - center1Y;
144178
const distance = Math.sqrt(dx * dx + dy * dy);
145179

146-
const collisionDistance = ball1.radius + ball2.radius;
147-
if (distance < collisionDistance) {
148-
const overlap = collisionDistance - distance;
180+
// Simple collision detection and response
181+
if (distance < ball1.radius + ball2.radius) {
182+
const overlap = ball1.radius + ball2.radius - distance;
149183
const nx = dx / distance;
150184
const ny = dy / distance;
151-
const separationX = nx * overlap * 0.5;
152-
const separationY = ny * overlap * 0.5;
153-
ball1.x -= separationX;
154-
ball1.y -= separationY;
155-
ball2.x += separationX;
156-
ball2.y += separationY;
157-
158-
const speed1 = Math.sqrt(ball1.vx * ball1.vx + ball1.vy * ball1.vy);
159-
const speed2 = Math.sqrt(ball2.vx * ball2.vx + ball2.vy * ball2.vy);
160-
const areTouching = speed1 < 1 && speed2 < 1;
161-
162-
if (areTouching) {
163-
const avgSpin = (ball1.vr + ball2.vr) * 1.05;
164-
ball1.vr = avgSpin;
165-
ball2.vr = avgSpin;
166-
167-
const separationForce = overlap * 0.05;
168-
ball1.vx -= nx * separationForce;
169-
ball1.vy -= ny * separationForce;
170-
ball2.vx += nx * separationForce;
171-
ball2.vy += ny * separationForce;
172-
173-
// Apply upward velocity to the upper ball
174-
const upwardForce = overlap * 0.1;
175-
if (ball1.y <= ball2.y) {
176-
// ball1 is higher
177-
ball1.vy -= upwardForce;
178-
} else {
179-
// ball2 is higher
180-
ball2.vy -= upwardForce;
181-
}
182-
} else {
183-
// At least one ball moving fast - collision physics
184-
const dvx = ball2.vx - ball1.vx;
185-
const dvy = ball2.vy - ball1.vy;
186-
const dvn = dvx * nx + dvy * ny;
187-
188-
const impulse = -dvn * 0.8;
189-
ball1.vx -= impulse * nx;
190-
ball1.vy -= impulse * ny;
191-
ball2.vx += impulse * nx;
192-
ball2.vy += impulse * ny;
193-
194-
// Rotational effects
195-
const tangentialImpulse = impulse * 0.2;
196-
ball1.vr += tangentialImpulse / ball1.radius;
197-
ball2.vr -= tangentialImpulse / ball2.radius;
198-
}
185+
186+
// Separate balls
187+
const separation = overlap * 0.5;
188+
ball1.x -= nx * separation;
189+
ball1.y -= ny * separation;
190+
ball2.x += nx * separation;
191+
ball2.y += ny * separation;
192+
193+
// Simple velocity exchange
194+
const dvx = ball2.vx - ball1.vx;
195+
const dvy = ball2.vy - ball1.vy;
196+
const dvn = dvx * nx + dvy * ny;
197+
198+
const impulse = dvn * 0.8;
199+
ball1.vx += impulse * nx;
200+
ball1.vy += impulse * ny;
201+
ball2.vx -= impulse * nx;
202+
ball2.vy -= impulse * ny;
203+
204+
// Exchange some spin
205+
const spinExchange = impulse * 0.3;
206+
ball1.vr += spinExchange;
207+
ball2.vr -= spinExchange;
199208
}
200209
}
201210
}
@@ -221,47 +230,81 @@ globalThis.addEventListener('orientationchange', function() {
221230
setTimeout(updateBallRadius, 100);
222231
});
223232

224-
// Add touch event handling for mobile interaction
233+
// Simple touch interaction
225234
function addTouchInteraction() {
226235
let touchStartTime = 0;
227236

228237
main.addEventListener('touchstart', function(e) {
229-
e.preventDefault(); // Prevent scrolling and zooming
238+
e.preventDefault();
230239
touchStartTime = Date.now();
240+
241+
if (e.touches.length === 1) {
242+
const touch = e.touches[0];
243+
const pos = getRelativePosition(touch.clientX, touch.clientY);
244+
const ballIndex = findBallAtPosition(pos.x, pos.y);
245+
246+
if (ballIndex !== -1) {
247+
dragState.isDragging = true;
248+
dragState.draggedBallIndex = ballIndex;
249+
dragState.dragOffset = {
250+
x: pos.x - physicsData[ballIndex].x,
251+
y: pos.y - physicsData[ballIndex].y
252+
};
253+
254+
physicsData[ballIndex].vx = 0;
255+
physicsData[ballIndex].vy = 0;
256+
physicsData[ballIndex].vr = 0;
257+
main.children[ballIndex].classList.add('dragging');
258+
}
259+
}
231260
}, { passive: false });
232261

233262
main.addEventListener('touchmove', function(e) {
234-
e.preventDefault(); // Prevent scrolling
263+
e.preventDefault();
264+
265+
if (dragState.isDragging && e.touches.length === 1) {
266+
const touch = e.touches[0];
267+
const pos = getRelativePosition(touch.clientX, touch.clientY);
268+
const data = physicsData[dragState.draggedBallIndex];
269+
const video = main.children[dragState.draggedBallIndex];
270+
271+
data.x = pos.x - dragState.dragOffset.x;
272+
data.y = pos.y - dragState.dragOffset.y;
273+
274+
const viewport = getViewportDimensions();
275+
data.x = Math.max(0, Math.min(viewport.width - data.radius * 2, data.x));
276+
data.y = Math.max(0, Math.min(viewport.height - data.radius * 2, data.y));
277+
278+
video.style.left = data.x + 'px';
279+
video.style.top = data.y + 'px';
280+
}
235281
}, { passive: false });
236282

237283
main.addEventListener('touchend', function(e) {
238284
e.preventDefault();
239-
const touchDuration = Date.now() - touchStartTime;
240285

241-
// If it's a quick tap (less than 200ms), add some energy to nearby balls
242-
if (touchDuration < 200 && e.changedTouches.length > 0) {
286+
if (dragState.isDragging) {
287+
const video = main.children[dragState.draggedBallIndex];
288+
video.classList.remove('dragging');
289+
dragState.isDragging = false;
290+
dragState.draggedBallIndex = -1;
291+
} else if (Date.now() - touchStartTime < 200) {
292+
// Quick tap - push nearby balls
243293
const touch = e.changedTouches[0];
244-
const rect = main.getBoundingClientRect();
245-
const touchX = touch.clientX - rect.left;
246-
const touchY = touch.clientY - rect.top;
294+
const pos = getRelativePosition(touch.clientX, touch.clientY);
247295

248-
// Find balls near the touch point and give them a little push
249296
for (let i = 0; i < physicsData.length; i++) {
250297
const data = physicsData[i];
251-
const ballCenterX = data.x + data.radius;
252-
const ballCenterY = data.y + data.radius;
253-
const distance = Math.sqrt(
254-
Math.pow(touchX - ballCenterX, 2) +
255-
Math.pow(touchY - ballCenterY, 2)
256-
);
257-
258-
// If touch is within 100px of ball center, give it a push
298+
const dx = pos.x - (data.x + data.radius);
299+
const dy = pos.y - (data.y + data.radius);
300+
const distance = Math.sqrt(dx * dx + dy * dy);
301+
259302
if (distance < 100) {
260-
const pushStrength = Math.max(0.5, (100 - distance) / 100 * 3);
261-
const angle = Math.atan2(ballCenterY - touchY, ballCenterX - touchX);
262-
data.vx += Math.cos(angle) * pushStrength;
263-
data.vy += Math.sin(angle) * pushStrength;
264-
data.vr += (Math.random() - 0.5) * 0.2;
303+
const force = (100 - distance) / 100 * 3;
304+
const angle = Math.atan2(dy, dx);
305+
data.vx -= Math.cos(angle) * force;
306+
data.vy -= Math.sin(angle) * force;
307+
data.vr += (Math.random() - 0.5) * 0.3;
265308
}
266309
}
267310
}
@@ -276,6 +319,66 @@ main.addEventListener('contextmenu', function(e) {
276319
// Initialize touch interaction
277320
addTouchInteraction();
278321

322+
// Simple mouse interaction
323+
function addMouseInteraction() {
324+
main.addEventListener('mousedown', function(e) {
325+
e.preventDefault();
326+
327+
const pos = getRelativePosition(e.clientX, e.clientY);
328+
const ballIndex = findBallAtPosition(pos.x, pos.y);
329+
330+
if (ballIndex !== -1) {
331+
dragState.isDragging = true;
332+
dragState.draggedBallIndex = ballIndex;
333+
dragState.dragOffset = {
334+
x: pos.x - physicsData[ballIndex].x,
335+
y: pos.y - physicsData[ballIndex].y
336+
};
337+
338+
physicsData[ballIndex].vx = 0;
339+
physicsData[ballIndex].vy = 0;
340+
physicsData[ballIndex].vr = 0;
341+
main.children[ballIndex].classList.add('dragging');
342+
document.body.style.cursor = 'grabbing';
343+
}
344+
});
345+
346+
main.addEventListener('mousemove', function(e) {
347+
if (dragState.isDragging) {
348+
const pos = getRelativePosition(e.clientX, e.clientY);
349+
const data = physicsData[dragState.draggedBallIndex];
350+
const video = main.children[dragState.draggedBallIndex];
351+
352+
data.x = pos.x - dragState.dragOffset.x;
353+
data.y = pos.y - dragState.dragOffset.y;
354+
355+
const viewport = getViewportDimensions();
356+
data.x = Math.max(0, Math.min(viewport.width - data.radius * 2, data.x));
357+
data.y = Math.max(0, Math.min(viewport.height - data.radius * 2, data.y));
358+
359+
video.style.left = data.x + 'px';
360+
video.style.top = data.y + 'px';
361+
} else {
362+
const pos = getRelativePosition(e.clientX, e.clientY);
363+
const ballIndex = findBallAtPosition(pos.x, pos.y);
364+
document.body.style.cursor = ballIndex !== -1 ? 'grab' : 'default';
365+
}
366+
});
367+
368+
main.addEventListener('mouseup', function(e) {
369+
if (dragState.isDragging) {
370+
const video = main.children[dragState.draggedBallIndex];
371+
video.classList.remove('dragging');
372+
dragState.isDragging = false;
373+
dragState.draggedBallIndex = -1;
374+
document.body.style.cursor = 'default';
375+
}
376+
});
377+
}
378+
379+
// Initialize mouse interaction
380+
addMouseInteraction();
381+
279382
// Handle mobile viewport changes and prevent scrolling
280383
function initializeMobileHandling() {
281384
// Prevent pull-to-refresh on mobile

0 commit comments

Comments
 (0)