diff --git a/chromakey.js b/chromakey.js new file mode 100644 index 0000000..8b869fb --- /dev/null +++ b/chromakey.js @@ -0,0 +1,53 @@ +const fs = require('fs'); +const path = require('path'); + +// Using Jimp for image processing +const { Jimp } = require('jimp'); + +async function chromakeyImage() { + try { + const inputPath = path.join(__dirname, 'website', 'screenborder.jpg'); + const outputPath = path.join(__dirname, 'website', 'screenborder.png'); + + console.log('Loading image...'); + const image = await Jimp.read(inputPath); + + console.log('Removing green screen...'); + + // Scan through each pixel + image.scan(0, 0, image.bitmap.width, image.bitmap.height, function(x, y, idx) { + const red = this.bitmap.data[idx + 0]; + const green = this.bitmap.data[idx + 1]; + const blue = this.bitmap.data[idx + 2]; + const alpha = this.bitmap.data[idx + 3]; + + // Very aggressive green detection + const isGreen = green > red * 1.05 && green > blue * 1.05 && green > 30; + + // Catch any bright greens + const isBrightGreen = green > 120 && green > red && green > blue; + + if (isGreen || isBrightGreen) { + // Make pixel transparent + this.bitmap.data[idx + 3] = 0; + } else if (green > red || green > blue) { + // Aggressively reduce alpha for any greenish tint + const greenness = Math.max( + (green - red) / 255, + (green - blue) / 255 + ); + this.bitmap.data[idx + 3] = Math.floor(alpha * (1 - greenness)); + } + }); + + console.log('Saving PNG...'); + await image.write(outputPath); + + console.log('✓ Chromakey complete! Saved to website/screenborder.png'); + } catch (error) { + console.error('✗ Chromakey failed:', error); + process.exit(1); + } +} + +chromakeyImage(); diff --git a/create-bulge-map.js b/create-bulge-map.js new file mode 100644 index 0000000..8e7cd60 --- /dev/null +++ b/create-bulge-map.js @@ -0,0 +1,46 @@ +const { Jimp } = require('jimp'); +const path = require('path'); + +async function createDisplacementMap() { + const size = 512; + const image = new Jimp({ width: size, height: size }); + + // Create a gentle CRT bulge displacement map + // Center appears to bulge outward (magnified) + // R channel = horizontal displacement + // G channel = vertical displacement + // 128 = no displacement, <128 = pull toward 0, >128 = push toward max + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + // Normalize to -1 to 1 range (center is 0,0) + const nx = (x / (size - 1)) * 2 - 1; + const ny = (y / (size - 1)) * 2 - 1; + + // Use smooth cubic falloff for gentle curve + // Pull edges slightly toward center (pincushion) + const distSq = nx * nx + ny * ny; + const strength = distSq * 0.15; // Gentle effect + + // Direction away from center (positive = away from center) + const dx = nx * strength; + const dy = ny * strength; + + // Convert to 0-255 range (128 = no displacement) + const r = Math.floor(128 + dx * 127); + const g = Math.floor(128 + dy * 127); + + const idx = (y * size + x) * 4; + image.bitmap.data[idx + 0] = Math.max(0, Math.min(255, r)); + image.bitmap.data[idx + 1] = Math.max(0, Math.min(255, g)); + image.bitmap.data[idx + 2] = 128; + image.bitmap.data[idx + 3] = 255; + } + } + + const outputPath = path.join(__dirname, 'website', 'bulge-map.png'); + await image.write(outputPath); + console.log('✓ Displacement map created: website/bulge-map.png'); +} + +createDisplacementMap(); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b4a24a1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,904 @@ +{ + "name": "lit.ruv.wtf", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lit.ruv.wtf", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "jimp": "^1.6.0" + }, + "devDependencies": {} + }, + "node_modules/@jimp/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.0.tgz", + "integrity": "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w==", + "license": "MIT", + "dependencies": { + "@jimp/file-ops": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "await-to-js": "^3.0.0", + "exif-parser": "^0.1.12", + "file-type": "^16.0.0", + "mime": "3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/diff": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.0.tgz", + "integrity": "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw==", + "license": "MIT", + "dependencies": { + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "pixelmatch": "^5.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/file-ops": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.0.tgz", + "integrity": "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-bmp": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.0.tgz", + "integrity": "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "bmp-ts": "^1.0.9" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-gif": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.0.tgz", + "integrity": "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "gifwrap": "^0.10.1", + "omggif": "^1.0.10" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-jpeg": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.0.tgz", + "integrity": "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "jpeg-js": "^0.4.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-png": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.0.tgz", + "integrity": "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "pngjs": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/js-tiff": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.0.tgz", + "integrity": "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "utif2": "^4.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.0.tgz", + "integrity": "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.0.tgz", + "integrity": "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/utils": "1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.0.tgz", + "integrity": "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.0.tgz", + "integrity": "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "tinycolor2": "^1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.0.tgz", + "integrity": "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/plugin-blit": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.0.tgz", + "integrity": "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/plugin-crop": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.0.tgz", + "integrity": "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.0.tgz", + "integrity": "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.0.tgz", + "integrity": "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.0.tgz", + "integrity": "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.0.tgz", + "integrity": "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-hash": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.0.tgz", + "integrity": "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/js-bmp": "1.6.0", + "@jimp/js-jpeg": "1.6.0", + "@jimp/js-png": "1.6.0", + "@jimp/js-tiff": "1.6.0", + "@jimp/plugin-color": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "any-base": "^1.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.0.tgz", + "integrity": "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.0.tgz", + "integrity": "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/js-jpeg": "1.6.0", + "@jimp/js-png": "1.6.0", + "@jimp/plugin-blit": "1.6.0", + "@jimp/types": "1.6.0", + "parse-bmfont-ascii": "^1.0.6", + "parse-bmfont-binary": "^1.0.6", + "parse-bmfont-xml": "^1.1.6", + "simple-xml-to-json": "^1.2.2", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-quantize": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.0.tgz", + "integrity": "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.0.tgz", + "integrity": "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/types": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.0.tgz", + "integrity": "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/plugin-crop": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.0.tgz", + "integrity": "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/plugin-color": "1.6.0", + "@jimp/plugin-hash": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/types": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.0.tgz", + "integrity": "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg==", + "license": "MIT", + "dependencies": { + "zod": "^3.23.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jimp/utils": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA==", + "license": "MIT", + "dependencies": { + "@jimp/types": "1.6.0", + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "license": "MIT" + }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bmp-ts": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz", + "integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==" + }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/gifwrap": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz", + "integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/jimp": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.0.tgz", + "integrity": "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==", + "license": "MIT", + "dependencies": { + "@jimp/core": "1.6.0", + "@jimp/diff": "1.6.0", + "@jimp/js-bmp": "1.6.0", + "@jimp/js-gif": "1.6.0", + "@jimp/js-jpeg": "1.6.0", + "@jimp/js-png": "1.6.0", + "@jimp/js-tiff": "1.6.0", + "@jimp/plugin-blit": "1.6.0", + "@jimp/plugin-blur": "1.6.0", + "@jimp/plugin-circle": "1.6.0", + "@jimp/plugin-color": "1.6.0", + "@jimp/plugin-contain": "1.6.0", + "@jimp/plugin-cover": "1.6.0", + "@jimp/plugin-crop": "1.6.0", + "@jimp/plugin-displace": "1.6.0", + "@jimp/plugin-dither": "1.6.0", + "@jimp/plugin-fisheye": "1.6.0", + "@jimp/plugin-flip": "1.6.0", + "@jimp/plugin-hash": "1.6.0", + "@jimp/plugin-mask": "1.6.0", + "@jimp/plugin-print": "1.6.0", + "@jimp/plugin-quantize": "1.6.0", + "@jimp/plugin-resize": "1.6.0", + "@jimp/plugin-rotate": "1.6.0", + "@jimp/plugin-threshold": "1.6.0", + "@jimp/types": "1.6.0", + "@jimp/utils": "1.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "license": "BSD-3-Clause" + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pixelmatch": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz", + "integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==", + "license": "ISC", + "dependencies": { + "pngjs": "^6.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/simple-xml-to-json": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.3.tgz", + "integrity": "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==", + "license": "MIT", + "engines": { + "node": ">=20.12.2" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/utif2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", + "integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.11" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index 5311ec7..91f5be8 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,9 @@ ], "author": "Max Litruv Boonzaayer", "license": "MIT", - "dependencies": {}, - "devDependencies": {}, + "dependencies": { + "jimp": "^1.6.0" + }, "repository": { "type": "git", "url": "https://github.com/litruv/lit.ruv.wtf" diff --git a/website/bulge-map.png b/website/bulge-map.png new file mode 100644 index 0000000..0c298c2 Binary files /dev/null and b/website/bulge-map.png differ diff --git a/website/crt-bulge.js b/website/crt-bulge.js new file mode 100644 index 0000000..619bb33 --- /dev/null +++ b/website/crt-bulge.js @@ -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); +}); diff --git a/website/crt-canvas.js b/website/crt-canvas.js new file mode 100644 index 0000000..4bf0cb5 --- /dev/null +++ b/website/crt-canvas.js @@ -0,0 +1,459 @@ +/** + * CRT Canvas Renderer + * Renders content to a canvas with WebGL barrel distortion shader + * Handles input coordinate transformation automatically + */ +class CRTCanvasRenderer { + /** + * @param {HTMLElement} sourceElement - Element to render (will be hidden) + * @param {Object} options - Configuration options + */ + constructor(sourceElement, options = {}) { + this.source = sourceElement; + this.options = { + bulgeStrength: options.bulgeStrength ?? 0.08, + scanlineIntensity: options.scanlineIntensity ?? 0.1, + vignetteStrength: options.vignetteStrength ?? 0.2, + fps: options.fps ?? 60, + ...options + }; + + this.canvas = null; + this.gl = null; + this.program = null; + this.animationId = null; + + this.init(); + } + + /** + * Initialize the canvas and WebGL context + */ + init() { + // Create canvas + this.canvas = document.createElement('canvas'); + this.canvas.id = 'crt-canvas'; + this.canvas.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + `; + + // Insert canvas before source + this.source.parentNode.insertBefore(this.canvas, this.source); + + // Hide source but keep it functional + this.source.style.position = 'absolute'; + this.source.style.opacity = '0'; + this.source.style.pointerEvents = 'none'; + + // Get WebGL context + this.gl = this.canvas.getContext('webgl', { + alpha: true, + antialias: false, + preserveDrawingBuffer: true + }); + + if (!this.gl) { + console.error('WebGL not supported, falling back to source element'); + this.source.style.opacity = '1'; + this.source.style.pointerEvents = 'auto'; + this.canvas.remove(); + return; + } + + this.setupShaders(); + this.setupGeometry(); + this.setupTexture(); + this.setupEventListeners(); + this.resize(); + + window.addEventListener('resize', () => this.resize()); + + this.startRenderLoop(); + } + + /** + * Create and compile WebGL shaders + */ + setupShaders() { + const gl = this.gl; + + // Vertex shader + const vertexSource = ` + attribute vec2 a_position; + attribute vec2 a_texCoord; + varying vec2 v_texCoord; + + void main() { + gl_Position = vec4(a_position, 0.0, 1.0); + v_texCoord = a_texCoord; + } + `; + + // Fragment shader with barrel distortion + const fragmentSource = ` + precision mediump float; + + uniform sampler2D u_texture; + uniform vec2 u_resolution; + uniform float u_bulgeStrength; + uniform float u_scanlineIntensity; + uniform float u_vignetteStrength; + uniform float u_time; + + varying vec2 v_texCoord; + + vec2 barrelDistort(vec2 uv) { + // Center UV around origin + vec2 centered = uv * 2.0 - 1.0; + + // Calculate distance from center + float distSq = dot(centered, centered); + + // Apply barrel distortion + float distortion = 1.0 + u_bulgeStrength * distSq; + centered *= distortion; + + // Convert back to 0-1 range + return centered * 0.5 + 0.5; + } + + void main() { + // Apply barrel distortion + vec2 distortedUV = barrelDistort(v_texCoord); + + // Check if UV is out of bounds + if (distortedUV.x < 0.0 || distortedUV.x > 1.0 || + distortedUV.y < 0.0 || distortedUV.y > 1.0) { + gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + + // Sample texture + vec4 color = texture2D(u_texture, distortedUV); + + // Scanlines + float scanline = sin(distortedUV.y * u_resolution.y * 1.5) * 0.5 + 0.5; + color.rgb *= 1.0 - (u_scanlineIntensity * (1.0 - scanline)); + + // Subtle RGB shift for chromatic aberration + float shift = u_bulgeStrength * 0.002; + float r = texture2D(u_texture, distortedUV + vec2(shift, 0.0)).r; + float b = texture2D(u_texture, distortedUV - vec2(shift, 0.0)).b; + color.r = mix(color.r, r, 0.5); + color.b = mix(color.b, b, 0.5); + + // Vignette + vec2 vignetteUV = v_texCoord * 2.0 - 1.0; + float vignette = 1.0 - dot(vignetteUV, vignetteUV) * u_vignetteStrength; + color.rgb *= vignette; + + gl_FragColor = color; + } + `; + + // Compile shaders + const vertexShader = this.compileShader(gl.VERTEX_SHADER, vertexSource); + const fragmentShader = this.compileShader(gl.FRAGMENT_SHADER, fragmentSource); + + // Create program + this.program = gl.createProgram(); + gl.attachShader(this.program, vertexShader); + gl.attachShader(this.program, fragmentShader); + gl.linkProgram(this.program); + + if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) { + console.error('Shader program failed to link:', gl.getProgramInfoLog(this.program)); + } + + gl.useProgram(this.program); + + // Get uniform locations + this.uniforms = { + texture: gl.getUniformLocation(this.program, 'u_texture'), + resolution: gl.getUniformLocation(this.program, 'u_resolution'), + bulgeStrength: gl.getUniformLocation(this.program, 'u_bulgeStrength'), + scanlineIntensity: gl.getUniformLocation(this.program, 'u_scanlineIntensity'), + vignetteStrength: gl.getUniformLocation(this.program, 'u_vignetteStrength'), + time: gl.getUniformLocation(this.program, 'u_time') + }; + } + + /** + * Compile a shader + */ + compileShader(type, source) { + const gl = this.gl; + const shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error('Shader compile error:', gl.getShaderInfoLog(shader)); + gl.deleteShader(shader); + return null; + } + + return shader; + } + + /** + * Set up geometry (full-screen quad) + */ + setupGeometry() { + const gl = this.gl; + + // Position attribute + const positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1, -1, 1, -1, -1, 1, + -1, 1, 1, -1, 1, 1 + ]), gl.STATIC_DRAW); + + const positionLoc = gl.getAttribLocation(this.program, 'a_position'); + gl.enableVertexAttribArray(positionLoc); + gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0); + + // Texture coordinate attribute + const texCoordBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + 0, 1, 1, 1, 0, 0, + 0, 0, 1, 1, 1, 0 + ]), gl.STATIC_DRAW); + + const texCoordLoc = gl.getAttribLocation(this.program, 'a_texCoord'); + gl.enableVertexAttribArray(texCoordLoc); + gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0); + } + + /** + * Set up texture for rendering source element + */ + setupTexture() { + const gl = this.gl; + + this.texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + + // Set texture parameters + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + } + + /** + * Handle canvas resize + */ + resize() { + const rect = this.source.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + + this.canvas.width = rect.width * dpr; + this.canvas.height = rect.height * dpr; + this.canvas.style.width = rect.width + 'px'; + this.canvas.style.height = rect.height + 'px'; + + this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); + } + + /** + * Apply inverse barrel distortion to get source coordinates from canvas coordinates + */ + inverseDistort(canvasX, canvasY) { + const rect = this.canvas.getBoundingClientRect(); + + // Normalize to 0-1 + let u = canvasX / rect.width; + let v = canvasY / rect.height; + + // Center around 0 + let x = u * 2 - 1; + let y = v * 2 - 1; + + // Iteratively solve inverse (Newton-Raphson) + for (let i = 0; i < 10; i++) { + const distSq = x * x + y * y; + const distortion = 1 + this.options.bulgeStrength * distSq; + + // The forward transform is: distorted = original * distortion + // So inverse is approximately: original = distorted / distortion + x = (u * 2 - 1) / distortion; + y = (v * 2 - 1) / distortion; + } + + // Convert back to pixel coordinates + const sourceX = ((x + 1) / 2) * rect.width; + const sourceY = ((y + 1) / 2) * rect.height; + + return { x: sourceX, y: sourceY }; + } + + /** + * Set up event listeners for mouse/touch input + */ + setupEventListeners() { + const forwardEvent = (e, type) => { + const rect = this.canvas.getBoundingClientRect(); + const canvasX = e.clientX - rect.left; + const canvasY = e.clientY - rect.top; + + const sourceCoords = this.inverseDistort(canvasX, canvasY); + const sourceX = rect.left + sourceCoords.x; + const sourceY = rect.top + sourceCoords.y; + + // Find element at transformed position in source + this.source.style.pointerEvents = 'auto'; + this.source.style.opacity = '0.001'; // Nearly invisible but still there + const element = document.elementFromPoint(sourceX, sourceY); + this.source.style.pointerEvents = 'none'; + this.source.style.opacity = '0'; + + if (element && this.source.contains(element)) { + const newEvent = new MouseEvent(type, { + bubbles: true, + cancelable: true, + clientX: sourceX, + clientY: sourceY, + button: e.button, + buttons: e.buttons + }); + element.dispatchEvent(newEvent); + + // Update cursor + this.canvas.style.cursor = window.getComputedStyle(element).cursor; + } + }; + + // Mouse events + this.canvas.addEventListener('click', (e) => forwardEvent(e, 'click')); + this.canvas.addEventListener('mousedown', (e) => forwardEvent(e, 'mousedown')); + this.canvas.addEventListener('mouseup', (e) => forwardEvent(e, 'mouseup')); + this.canvas.addEventListener('mousemove', (e) => forwardEvent(e, 'mousemove')); + this.canvas.addEventListener('dblclick', (e) => forwardEvent(e, 'dblclick')); + this.canvas.addEventListener('contextmenu', (e) => { + e.preventDefault(); + forwardEvent(e, 'contextmenu'); + }); + + // Wheel events + this.canvas.addEventListener('wheel', (e) => { + const rect = this.canvas.getBoundingClientRect(); + const canvasX = e.clientX - rect.left; + const canvasY = e.clientY - rect.top; + + const sourceCoords = this.inverseDistort(canvasX, canvasY); + const sourceX = rect.left + sourceCoords.x; + const sourceY = rect.top + sourceCoords.y; + + this.source.style.pointerEvents = 'auto'; + const element = document.elementFromPoint(sourceX, sourceY); + this.source.style.pointerEvents = 'none'; + + if (element && this.source.contains(element)) { + element.dispatchEvent(new WheelEvent('wheel', { + bubbles: true, + clientX: sourceX, + clientY: sourceY, + deltaX: e.deltaX, + deltaY: e.deltaY, + deltaMode: e.deltaMode + })); + } + }, { passive: true }); + + // Keyboard events should go to focused elements naturally + // Focus management + this.canvas.addEventListener('mousedown', () => { + // Focus the terminal when clicking canvas + const terminal = this.source.querySelector('.xterm-helper-textarea'); + if (terminal) { + terminal.focus(); + } + }); + } + + /** + * Capture source element to texture using html2canvas + */ + async captureSource() { + const gl = this.gl; + + // Use html2canvas to render the source element + try { + const canvas = await html2canvas(this.source, { + backgroundColor: null, + scale: window.devicePixelRatio || 1, + logging: false, + useCORS: true + }); + + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas); + } catch (err) { + console.error('Failed to capture source:', err); + } + } + + /** + * Render frame + */ + render(time) { + const gl = this.gl; + + gl.clear(gl.COLOR_BUFFER_BIT); + + // Set uniforms + gl.uniform1i(this.uniforms.texture, 0); + gl.uniform2f(this.uniforms.resolution, this.canvas.width, this.canvas.height); + gl.uniform1f(this.uniforms.bulgeStrength, this.options.bulgeStrength); + gl.uniform1f(this.uniforms.scanlineIntensity, this.options.scanlineIntensity); + gl.uniform1f(this.uniforms.vignetteStrength, this.options.vignetteStrength); + gl.uniform1f(this.uniforms.time, time * 0.001); + + // Draw + gl.drawArrays(gl.TRIANGLES, 0, 6); + } + + /** + * Start the render loop + */ + startRenderLoop() { + const frameInterval = 1000 / this.options.fps; + let lastCapture = 0; + + const loop = async (time) => { + // Capture at reduced rate to save performance + if (time - lastCapture > frameInterval) { + await this.captureSource(); + lastCapture = time; + } + + this.render(time); + this.animationId = requestAnimationFrame(loop); + }; + + this.animationId = requestAnimationFrame(loop); + } + + /** + * Stop rendering + */ + destroy() { + if (this.animationId) { + cancelAnimationFrame(this.animationId); + } + this.canvas.remove(); + this.source.style.opacity = '1'; + this.source.style.pointerEvents = 'auto'; + } +} + +// Export for use +window.CRTCanvasRenderer = CRTCanvasRenderer; diff --git a/website/index.html b/website/index.html index 34bde8d..39a45cc 100644 --- a/website/index.html +++ b/website/index.html @@ -30,6 +30,7 @@
+