mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-24 02:36:02 +10:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
238
website/crt-bulge.js
Normal file
238
website/crt-bulge.js
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* CRT Bulge Coordinate Transformer
|
||||
* Intercepts mouse events and applies inverse distortion to match the visual bulge effect
|
||||
*/
|
||||
class CRTBulgeTransformer {
|
||||
constructor(containerSelector, strength = 0.15) {
|
||||
this.container = document.querySelector(containerSelector);
|
||||
this.strength = strength;
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply inverse barrel distortion to transform visual coords to DOM coords
|
||||
* @param {number} x - Visual X coordinate relative to container
|
||||
* @param {number} y - Visual Y coordinate relative to container
|
||||
* @param {number} width - Container width
|
||||
* @param {number} height - Container height
|
||||
* @returns {{x: number, y: number}} - Transformed coordinates
|
||||
*/
|
||||
inverseDistort(x, y, width, height) {
|
||||
// Normalize to -1 to 1 (center is 0,0)
|
||||
const nx = (x / width) * 2 - 1;
|
||||
const ny = (y / height) * 2 - 1;
|
||||
|
||||
// The visual filter pushes pixels outward based on distance squared
|
||||
// To invert: we need to find where this pixel came FROM
|
||||
// Using Newton-Raphson iteration to solve the inverse
|
||||
|
||||
let ux = nx;
|
||||
let uy = ny;
|
||||
|
||||
// Iterate to find the original position
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const distSq = ux * ux + uy * uy;
|
||||
const factor = 1 + this.strength * distSq;
|
||||
|
||||
// Forward distortion: visual = original * factor
|
||||
// Inverse: original = visual / factor (approximately)
|
||||
ux = nx / factor;
|
||||
uy = ny / factor;
|
||||
}
|
||||
|
||||
// Convert back to pixel coordinates
|
||||
const newX = ((ux + 1) / 2) * width;
|
||||
const newY = ((uy + 1) / 2) * height;
|
||||
|
||||
return { x: newX, y: newY };
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform mouse event coordinates
|
||||
* @param {MouseEvent} e - Original mouse event
|
||||
* @returns {{x: number, y: number}} - Transformed page coordinates
|
||||
*/
|
||||
transformEvent(e) {
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
|
||||
// Get position relative to container
|
||||
const relX = e.clientX - rect.left;
|
||||
const relY = e.clientY - rect.top;
|
||||
|
||||
// Apply inverse distortion
|
||||
const transformed = this.inverseDistort(relX, relY, rect.width, rect.height);
|
||||
|
||||
// Convert back to page coordinates
|
||||
return {
|
||||
x: rect.left + transformed.x,
|
||||
y: rect.top + transformed.y
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners for mouse interaction
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Create invisible overlay to capture events
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'crt-event-overlay';
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
cursor: inherit;
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Track currently hovered element for hover states
|
||||
let lastHoveredElement = null;
|
||||
|
||||
// Handle mouse movement
|
||||
overlay.addEventListener('mousemove', (e) => {
|
||||
const transformed = this.transformEvent(e);
|
||||
|
||||
// Temporarily hide overlay to find element underneath
|
||||
overlay.style.pointerEvents = 'none';
|
||||
const elementBelow = document.elementFromPoint(transformed.x, transformed.y);
|
||||
overlay.style.pointerEvents = 'auto';
|
||||
|
||||
// Update cursor based on element below
|
||||
if (elementBelow) {
|
||||
const computedStyle = window.getComputedStyle(elementBelow);
|
||||
overlay.style.cursor = computedStyle.cursor;
|
||||
|
||||
// Handle hover state changes
|
||||
if (elementBelow !== lastHoveredElement) {
|
||||
if (lastHoveredElement) {
|
||||
lastHoveredElement.dispatchEvent(new MouseEvent('mouseleave', {
|
||||
bubbles: true,
|
||||
clientX: transformed.x,
|
||||
clientY: transformed.y
|
||||
}));
|
||||
}
|
||||
elementBelow.dispatchEvent(new MouseEvent('mouseenter', {
|
||||
bubbles: true,
|
||||
clientX: transformed.x,
|
||||
clientY: transformed.y
|
||||
}));
|
||||
lastHoveredElement = elementBelow;
|
||||
}
|
||||
|
||||
// Dispatch mousemove to element
|
||||
elementBelow.dispatchEvent(new MouseEvent('mousemove', {
|
||||
bubbles: true,
|
||||
clientX: transformed.x,
|
||||
clientY: transformed.y
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle clicks
|
||||
overlay.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const transformed = this.transformEvent(e);
|
||||
|
||||
overlay.style.pointerEvents = 'none';
|
||||
const elementBelow = document.elementFromPoint(transformed.x, transformed.y);
|
||||
overlay.style.pointerEvents = 'auto';
|
||||
|
||||
if (elementBelow) {
|
||||
elementBelow.dispatchEvent(new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
clientX: transformed.x,
|
||||
clientY: transformed.y
|
||||
}));
|
||||
|
||||
// Focus if it's an interactive element
|
||||
if (elementBelow.tagName === 'INPUT' || elementBelow.tagName === 'TEXTAREA' || elementBelow.tabIndex >= 0) {
|
||||
elementBelow.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle mousedown
|
||||
overlay.addEventListener('mousedown', (e) => {
|
||||
const transformed = this.transformEvent(e);
|
||||
|
||||
overlay.style.pointerEvents = 'none';
|
||||
const elementBelow = document.elementFromPoint(transformed.x, transformed.y);
|
||||
overlay.style.pointerEvents = 'auto';
|
||||
|
||||
if (elementBelow) {
|
||||
elementBelow.dispatchEvent(new MouseEvent('mousedown', {
|
||||
bubbles: true,
|
||||
clientX: transformed.x,
|
||||
clientY: transformed.y,
|
||||
button: e.button
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle mouseup
|
||||
overlay.addEventListener('mouseup', (e) => {
|
||||
const transformed = this.transformEvent(e);
|
||||
|
||||
overlay.style.pointerEvents = 'none';
|
||||
const elementBelow = document.elementFromPoint(transformed.x, transformed.y);
|
||||
overlay.style.pointerEvents = 'auto';
|
||||
|
||||
if (elementBelow) {
|
||||
elementBelow.dispatchEvent(new MouseEvent('mouseup', {
|
||||
bubbles: true,
|
||||
clientX: transformed.x,
|
||||
clientY: transformed.y,
|
||||
button: e.button
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle wheel/scroll
|
||||
overlay.addEventListener('wheel', (e) => {
|
||||
const transformed = this.transformEvent(e);
|
||||
|
||||
overlay.style.pointerEvents = 'none';
|
||||
const elementBelow = document.elementFromPoint(transformed.x, transformed.y);
|
||||
overlay.style.pointerEvents = 'auto';
|
||||
|
||||
if (elementBelow) {
|
||||
elementBelow.dispatchEvent(new WheelEvent('wheel', {
|
||||
bubbles: true,
|
||||
clientX: transformed.x,
|
||||
clientY: transformed.y,
|
||||
deltaX: e.deltaX,
|
||||
deltaY: e.deltaY,
|
||||
deltaMode: e.deltaMode
|
||||
}));
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Handle context menu
|
||||
overlay.addEventListener('contextmenu', (e) => {
|
||||
const transformed = this.transformEvent(e);
|
||||
|
||||
overlay.style.pointerEvents = 'none';
|
||||
const elementBelow = document.elementFromPoint(transformed.x, transformed.y);
|
||||
overlay.style.pointerEvents = 'auto';
|
||||
|
||||
if (elementBelow) {
|
||||
const event = new MouseEvent('contextmenu', {
|
||||
bubbles: true,
|
||||
clientX: transformed.x,
|
||||
clientY: transformed.y
|
||||
});
|
||||
if (!elementBelow.dispatchEvent(event)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Match the strength value from create-bulge-map.js
|
||||
new CRTBulgeTransformer('.container', 0.15);
|
||||
});
|
||||
Reference in New Issue
Block a user