|
2478 | 2478 | return xStart + distortedT * totalWidth; |
2479 | 2479 | }, |
2480 | 2480 |
|
2481 | | - // Fisheye distortion function that keeps the focus point fixed |
2482 | | - // Simple formula: multiply distance by distortion factor near focus |
| 2481 | + // Fisheye distortion function that preserves monotonicity |
| 2482 | + // Maps normalized time [0,1] to distorted normalized position [0,1] |
| 2483 | + // Ensures order is preserved: if t1 < t2, then distorted(t1) < distorted(t2) |
2483 | 2484 | fisheyeDistortion: function(t, focus, distortion) { |
2484 | 2485 | if (distortion <= 1) return t; |
2485 | 2486 |
|
2486 | | - // Distance from focus point |
2487 | | - const delta = t - focus; |
2488 | | - const distance = Math.abs(delta); |
2489 | | - const sign = delta < 0 ? -1 : 1; |
2490 | | - |
2491 | | - if (distance < 0.0001) { |
2492 | | - // At the focus point, no distortion |
2493 | | - return t; |
2494 | | - } |
2495 | | - |
2496 | | - // Fisheye effect: points near focus expand, far points compress |
2497 | | - // Use a smooth falloff based on distance |
2498 | | - const effectRadius = 0.5; // Half the range is affected |
2499 | | - |
2500 | | - // Calculate how much to magnify based on distance from focus |
2501 | | - // Close to focus: multiply by distortion (expand) |
2502 | | - // Far from focus: divide by distortion (compress) |
2503 | | - let scale; |
2504 | | - if (distance < effectRadius) { |
2505 | | - // Inside radius: interpolate from distortion (at focus) to 1 (at radius edge) |
2506 | | - const normalized = distance / effectRadius; |
2507 | | - // Use cosine for smooth interpolation |
2508 | | - const blend = (1 - Math.cos(normalized * Math.PI)) / 2; |
2509 | | - scale = distortion - (distortion - 1) * blend; |
| 2487 | + // Clamp input to valid range |
| 2488 | + t = Math.max(0, Math.min(1, t)); |
| 2489 | + focus = Math.max(0, Math.min(1, focus)); |
| 2490 | + |
| 2491 | + // Effect radius around focus (fraction of total range) |
| 2492 | + const effectRadius = 0.15; // 15% on each side of focus = 30% total magnified region |
| 2493 | + |
| 2494 | + // Define regions: [0, focusLeft], [focusLeft, focusRight], [focusRight, 1] |
| 2495 | + const focusLeft = Math.max(0, focus - effectRadius); |
| 2496 | + const focusRight = Math.min(1, focus + effectRadius); |
| 2497 | + |
| 2498 | + // Calculate how much space each region should occupy after distortion |
| 2499 | + // The magnified region expands, other regions compress to compensate |
| 2500 | + const magnifiedWidth = focusRight - focusLeft; |
| 2501 | + const leftWidth = focusLeft; |
| 2502 | + const rightWidth = 1 - focusRight; |
| 2503 | + |
| 2504 | + // Total "virtual" width if we expand magnified region by distortion factor |
| 2505 | + const virtualWidth = leftWidth + magnifiedWidth * distortion + rightWidth; |
| 2506 | + |
| 2507 | + // Normalize back to [0,1] range - each region gets proportional space |
| 2508 | + const leftTargetWidth = leftWidth / virtualWidth; |
| 2509 | + const magnifiedTargetWidth = (magnifiedWidth * distortion) / virtualWidth; |
| 2510 | + const rightTargetWidth = rightWidth / virtualWidth; |
| 2511 | + |
| 2512 | + // Map t to output position based on which region it's in |
| 2513 | + if (t <= focusLeft) { |
| 2514 | + // Left region: compress linearly |
| 2515 | + if (leftWidth === 0) return 0; |
| 2516 | + const localT = t / leftWidth; // Normalize to [0,1] within region |
| 2517 | + return localT * leftTargetWidth; |
| 2518 | + } else if (t <= focusRight) { |
| 2519 | + // Magnified region: expand linearly |
| 2520 | + if (magnifiedWidth === 0) return leftTargetWidth; |
| 2521 | + const localT = (t - focusLeft) / magnifiedWidth; // Normalize to [0,1] within region |
| 2522 | + return leftTargetWidth + localT * magnifiedTargetWidth; |
2510 | 2523 | } else { |
2511 | | - // Outside radius: compress more as we go further |
2512 | | - const excessDistance = distance - effectRadius; |
2513 | | - const compressionFactor = 1 / distortion; |
2514 | | - scale = 1 - (1 - compressionFactor) * Math.min(1, excessDistance / (1 - effectRadius)); |
| 2524 | + // Right region: compress linearly |
| 2525 | + if (rightWidth === 0) return leftTargetWidth + magnifiedTargetWidth; |
| 2526 | + const localT = (t - focusRight) / rightWidth; // Normalize to [0,1] within region |
| 2527 | + return leftTargetWidth + magnifiedTargetWidth + localT * rightTargetWidth; |
2515 | 2528 | } |
2516 | | - |
2517 | | - // Apply the scale to the distance |
2518 | | - const distorted = focus + sign * distance * scale; |
2519 | | - return Math.max(0, Math.min(1, distorted)); |
2520 | 2529 | } |
2521 | 2530 | }; |
2522 | 2531 |
|
|
0 commit comments