Skip to content

Instantly share code, notes, and snippets.

@twobob
Last active October 18, 2025 00:13
Show Gist options
  • Select an option

  • Save twobob/6499fe27243cef72103ec85e893cf63e to your computer and use it in GitHub Desktop.

Select an option

Save twobob/6499fe27243cef72103ec85e893cf63e to your computer and use it in GitHub Desktop.
do chroma for cheapa
export interface ChromaKeyOptions {
keyColor?: [number, number, number] // RGB color to key out (default: magenta)
tolerance?: number // Tolerance range for color matching (0-100)
softness?: number // Edge softness (0-100)
spill?: number // Spill suppression strength (0-100)
preserveEdges?: boolean // Preserve edge detail
}
export interface ImageData {
data: Uint8ClampedArray
width: number
height: number
}
export function detectMagentaColor(imageData: ImageData): [number, number, number] {
const { data } = imageData
// Look for pixels that are highly magenta (high R+B, low G)
const magentaPixels: [number, number, number][] = []
for (let i = 0; i < data.length; i += 4) {
const r = data[i]
const g = data[i + 1]
const b = data[i + 2]
// Check if pixel is magenta-like (R+B > G, and reasonably bright)
const magentaness = (r + b) - g
const brightness = (r + g + b) / 3
if (magentaness > 50 && brightness > 50) {
magentaPixels.push([r, g, b])
}
}
if (magentaPixels.length === 0) {
// Default to pure magenta if no magenta pixels found
return [255, 0, 255]
}
// Calculate average of magenta pixels
const sum = magentaPixels.reduce(
(acc, pixel) => [acc[0] + pixel[0], acc[1] + pixel[1], acc[2] + pixel[2]],
[0, 0, 0]
)
return [
Math.round(sum[0] / magentaPixels.length),
Math.round(sum[1] / magentaPixels.length),
Math.round(sum[2] / magentaPixels.length)
]
}
function colorDistance(c1: [number, number, number], c2: [number, number, number]): number {
// Euclidean distance in RGB space
const dr = c1[0] - c2[0]
const dg = c1[1] - c2[1]
const db = c1[2] - c2[2]
return Math.sqrt(dr * dr + dg * dg + db * db)
}
function colorDistanceHSV(c1: [number, number, number], c2: [number, number, number]): number {
// Convert RGB to HSV for better color matching
const hsv1 = rgbToHsv(c1)
const hsv2 = rgbToHsv(c2)
// Weight hue difference more heavily for chroma keying
const dh = Math.min(Math.abs(hsv1[0] - hsv2[0]), 360 - Math.abs(hsv1[0] - hsv2[0]))
const ds = Math.abs(hsv1[1] - hsv2[1])
const dv = Math.abs(hsv1[2] - hsv2[2])
return Math.sqrt(dh * dh * 4 + ds * ds + dv * dv)
}
function rgbToHsv(rgb: [number, number, number]): [number, number, number] {
const r = rgb[0] / 255
const g = rgb[1] / 255
const b = rgb[2] / 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const diff = max - min
let h = 0
if (diff !== 0) {
if (max === r) h = 60 * (((g - b) / diff) % 6)
else if (max === g) h = 60 * ((b - r) / diff + 2)
else h = 60 * ((r - g) / diff + 4)
}
if (h < 0) h += 360
const s = max === 0 ? 0 : diff / max
const v = max
return [h, s * 100, v * 100]
}
export function chromaKeyPixelData(
imageData: ImageData,
options: ChromaKeyOptions = {}
): ImageData {
const {
keyColor = [255, 0, 255], // Default magenta
tolerance = 20,
softness = 5,
spill = 30,
preserveEdges = true
} = options
// Create new pixel data array with alpha channel
const d = new Uint8ClampedArray(imageData.data)
const toleranceRange = (tolerance / 100) * 255
const softnessRange = (softness / 100) * 255
const spillStrength = spill / 100
for (let i = 0; i < d.length; i += 4) {
const pixel: [number, number, number] = [d[i], d[i + 1], d[i + 2]]
// Calculate distance to key color using HSV-based matching
const distance = colorDistanceHSV(pixel, keyColor)
const rgbDistance = colorDistance(pixel, keyColor)
// Use the better of the two distance measures
const finalDistance = Math.min(distance * 2, rgbDistance)
if (finalDistance <= toleranceRange) {
// Pixel is within key color range - make transparent
if (finalDistance <= toleranceRange - softnessRange) {
// Fully transparent
d[i + 3] = 0
} else {
// Soft edge - partial transparency
const alpha = (finalDistance - (toleranceRange - softnessRange)) / softnessRange
d[i + 3] = Math.round(alpha * 255)
}
} else if (spillStrength > 0) {
// Apply spill suppression to nearby colors
const spillDistance = finalDistance - toleranceRange
const spillRange = toleranceRange * 2
if (spillDistance < spillRange) {
const spillFactor = 1 - (spillDistance / spillRange)
const spillAmount = spillFactor * spillStrength
// Reduce the key color components
if (keyColor[0] > keyColor[1] && keyColor[0] > keyColor[2]) {
// Key color is red-dominant
d[i] = Math.round(d[i] * (1 - spillAmount * 0.5))
}
if (keyColor[1] > keyColor[0] && keyColor[1] > keyColor[2]) {
// Key color is green-dominant
d[i + 1] = Math.round(d[i + 1] * (1 - spillAmount * 0.5))
}
if (keyColor[2] > keyColor[0] && keyColor[2] > keyColor[1]) {
// Key color is blue-dominant
d[i + 2] = Math.round(d[i + 2] * (1 - spillAmount * 0.5))
}
// For magenta (high R+B, low G), reduce both R and B
if (keyColor[0] > 200 && keyColor[2] > 200 && keyColor[1] < 100) {
d[i] = Math.round(d[i] * (1 - spillAmount * 0.3))
d[i + 2] = Math.round(d[i + 2] * (1 - spillAmount * 0.3))
}
}
}
}
return {
data: d,
width: imageData.width,
height: imageData.height
}
}
export function createMagentaBackground(width: number, height: number, magentaColor: [number, number, number] = [255, 0, 255]): ImageData {
const data = new Uint8ClampedArray(width * height * 4)
for (let i = 0; i < data.length; i += 4) {
data[i] = magentaColor[0] // R
data[i + 1] = magentaColor[1] // G
data[i + 2] = magentaColor[2] // B
data[i + 3] = 255 // A (fully opaque)
}
return {
data,
width,
height
}
}
export function compositeImages(foreground: ImageData, background: ImageData): ImageData {
if (foreground.width !== background.width || foreground.height !== background.height) {
throw new Error('Images must have the same dimensions for compositing')
}
const result = new Uint8ClampedArray(foreground.data)
for (let i = 0; i < result.length; i += 4) {
const fgAlpha = result[i + 3] / 255
const bgAlpha = background.data[i + 3] / 255
if (fgAlpha === 0) {
// Foreground is transparent, use background
result[i] = background.data[i]
result[i + 1] = background.data[i + 1]
result[i + 2] = background.data[i + 2]
result[i + 3] = background.data[i + 3]
} else if (fgAlpha < 1) {
// Alpha blend
const invAlpha = 1 - fgAlpha
result[i] = Math.round(result[i] * fgAlpha + background.data[i] * invAlpha)
result[i + 1] = Math.round(result[i + 1] * fgAlpha + background.data[i + 1] * invAlpha)
result[i + 2] = Math.round(result[i + 2] * fgAlpha + background.data[i + 2] * invAlpha)
result[i + 3] = Math.round((fgAlpha + bgAlpha * invAlpha) * 255)
}
// If fgAlpha === 1, keep foreground pixel as-is
}
return {
data: result,
width: foreground.width,
height: foreground.height
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeSepAI ChromaKey Test</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
}
h1 {
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.upload-section {
background: rgba(255, 255, 255, 0.1);
border: 2px dashed rgba(255, 255, 255, 0.3);
border-radius: 15px;
padding: 20px;
text-align: center;
margin-bottom: 20px;
transition: all 0.3s ease;
}
.upload-section:hover {
border-color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.15);
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.control-group {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
}
.control-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
.control-group input, .control-group select {
width: 100%;
padding: 8px;
border: none;
border-radius: 5px;
background: rgba(255, 255, 255, 0.9);
color: #333;
}
.slider-container {
display: flex;
align-items: center;
gap: 10px;
}
.slider-container input[type="range"] {
flex: 1;
}
.slider-value {
min-width: 40px;
text-align: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 5px;
padding: 5px;
}
.color-picker-container {
display: flex;
align-items: center;
gap: 10px;
}
.color-picker-container input[type="color"] {
width: 50px;
height: 30px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.preset-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.preset-btn {
padding: 10px 20px;
border: none;
border-radius: 25px;
background: rgba(255, 255, 255, 0.2);
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-weight: bold;
}
.preset-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.action-buttons {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.btn {
padding: 12px 25px;
border: none;
border-radius: 25px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
font-size: 16px;
}
.btn-primary {
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
}
.btn-secondary {
background: linear-gradient(45deg, #2196F3, #1976D2);
color: white;
}
.btn-detect {
background: linear-gradient(45deg, #FF9800, #F57C00);
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.results {
display: none;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.result-item {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
text-align: center;
}
.result-item h3 {
margin-top: 0;
margin-bottom: 15px;
}
.result-item img {
max-width: 100%;
max-height: 300px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect width="10" height="10" fill="%23ccc"/><rect x="10" y="10" width="10" height="10" fill="%23ccc"/><rect x="10" width="10" height="10" fill="white"/><rect y="10" width="10" height="10" fill="white"/></svg>');
background-size: 20px 20px;
}
.message {
padding: 15px;
border-radius: 10px;
margin: 10px 0;
font-weight: bold;
}
.message.info {
background: rgba(33, 150, 243, 0.2);
border: 1px solid rgba(33, 150, 243, 0.5);
}
.message.error {
background: rgba(244, 67, 54, 0.2);
border: 1px solid rgba(244, 67, 54, 0.5);
}
.message.success {
background: rgba(76, 175, 80, 0.2);
border: 1px solid rgba(76, 175, 80, 0.5);
}
.background-options {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.background-options input[type="color"] {
width: 40px;
height: 40px;
}
.processing {
opacity: 0.7;
pointer-events: none;
}
/* Toast Notification System */
.toast-container {
position: fixed !important;
top: 20px !important;
right: 20px !important;
z-index: 10000 !important;
display: flex;
flex-direction: column;
gap: 10px;
max-width: 400px;
pointer-events: none;
}
/* Responsive positioning for smaller screens */
@media (max-width: 480px) {
.toast-container {
top: 10px !important;
right: 10px !important;
left: 10px !important;
max-width: none;
}
}
.toast {
padding: 16px 20px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
color: white;
font-weight: 500;
font-size: 14px;
line-height: 1.4;
border: 1px solid rgba(255, 255, 255, 0.2);
transform: translateX(100%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: auto;
cursor: pointer;
position: relative;
overflow: hidden;
}
.toast.show {
transform: translateX(0);
opacity: 1;
}
.toast.hide {
transform: translateX(100%);
opacity: 0;
}
.toast.info {
background: linear-gradient(135deg, rgba(33, 150, 243, 0.9), rgba(33, 150, 243, 0.7));
border-color: rgba(33, 150, 243, 0.5);
}
.toast.error {
background: linear-gradient(135deg, rgba(244, 67, 54, 0.9), rgba(244, 67, 54, 0.7));
border-color: rgba(244, 67, 54, 0.5);
}
.toast.success {
background: linear-gradient(135deg, rgba(76, 175, 80, 0.9), rgba(76, 175, 80, 0.7));
border-color: rgba(76, 175, 80, 0.5);
}
.toast::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 3px;
background: rgba(255, 255, 255, 0.8);
border-radius: 0 0 12px 12px;
animation: toast-progress 3s linear forwards;
}
.toast.info::before {
background: rgba(33, 150, 243, 1);
}
.toast.error::before {
background: rgba(244, 67, 54, 1);
}
.toast.success::before {
background: rgba(76, 175, 80, 1);
}
@keyframes toast-progress {
from { width: 100%; }
to { width: 0%; }
}
.toast:hover::before {
animation-play-state: paused;
}
.toast-close {
position: absolute;
top: 8px;
right: 10px;
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
font-size: 18px;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
}
.toast-close:hover {
color: rgba(255, 255, 255, 1);
}
.magnifier-container {
position: relative;
display: inline-block;
}
.magnifier {
position: absolute;
width: 120px;
height: 120px;
border: 3px solid #fff;
border-radius: 50%;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
pointer-events: none;
z-index: 1000;
display: none;
background: #000;
overflow: hidden;
}
.magnifier canvas {
width: 100%;
height: 100%;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
}
.magnifier-crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: 2px;
background: red;
transform: translate(-50%, -50%);
z-index: 1001;
}
.magnifier-crosshair::before,
.magnifier-crosshair::after {
content: '';
position: absolute;
background: red;
}
.magnifier-crosshair::before {
width: 20px;
height: 1px;
top: 0.5px;
left: -9px;
}
.magnifier-crosshair::after {
width: 1px;
height: 20px;
top: -9px;
left: 0.5px;
}
.color-preview {
position: absolute;
bottom: -30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
}
</style>
</head>
<body>
<div class="container">
<h1>DeSepAI ChromaKey Tool</h1>
<div class="upload-section">
<h3>Select Image with Magenta Background</h3>
<input type="file" id="fileInput" accept="image/*" style="margin: 10px;">
<p>A default magenta.png image is loaded automatically. Choose a different image with magenta background or process the default one.</p>
</div>
<div class="preset-buttons">
<button class="preset-btn" onclick="setPreset('default')">Default Magenta</button>
<button class="preset-btn" onclick="setPreset('strict')">Strict Keying</button>
<button class="preset-btn" onclick="setPreset('soft')">Soft Keying</button>
<button class="preset-btn" onclick="setPreset('green')">Green Screen</button>
<button class="preset-btn" onclick="setPreset('blue')">Blue Screen</button>
</div>
<div class="controls">
<div class="control-group">
<label>Key Colour:</label>
<div class="color-picker-container">
<input type="color" id="keyColor" value="#ff00ff">
<span id="keyColorLabel">Magenta (#FF00FF)</span>
</div>
</div>
<div class="control-group">
<label>Tolerance:</label>
<div class="slider-container">
<input type="range" id="tolerance" min="0" max="100" value="20">
<span class="slider-value" id="toleranceValue">20</span>
</div>
</div>
<div class="control-group">
<label>Softness:</label>
<div class="slider-container">
<input type="range" id="softness" min="0" max="50" value="5">
<span class="slider-value" id="softnessValue">5</span>
</div>
</div>
<div class="control-group">
<label>Spill Suppression:</label>
<div class="slider-container">
<input type="range" id="spill" min="0" max="100" value="30">
<span class="slider-value" id="spillValue">30</span>
</div>
</div>
<div class="control-group">
<label>Background:</label>
<div class="background-options">
<select id="backgroundType">
<option value="transparent">Transparent</option>
<option value="color">Solid Colour</option>
<option value="image">Custom Image</option>
</select>
<input type="color" id="backgroundColor" value="#ffffff" style="display: none;">
<input type="file" id="backgroundImage" accept="image/*" style="display: none;">
</div>
</div>
<div class="control-group">
<label>
<input type="checkbox" id="preserveEdges" checked>
Preserve Edge Detail
</label>
</div>
</div>
<div class="action-buttons">
<button class="btn btn-detect" onclick="detectMagentaFromImage()">Auto-Detect Key Colour</button>
<button class="btn btn-primary" id="processBtn" onclick="processImage()" disabled>Apply ChromaKey</button>
<button class="btn btn-secondary" onclick="downloadResult()" id="downloadBtn" style="display: none;">Download Result</button>
</div>
<div class="results" id="results">
<div class="result-item">
<h3>Original Image (Click to pick chroma colour)</h3>
<div class="magnifier-container">
<img id="originalImage" src="#" alt="Original" />
<div class="magnifier" id="magnifier">
<canvas id="magnifierCanvas" width="120" height="120"></canvas>
<div class="magnifier-crosshair"></div>
<div class="color-preview" id="colorPreview">RGB(0, 0, 0)</div>
</div>
</div>
</div>
<div class="result-item">
<h3>ChromaKey Result</h3>
<img id="resultImage" src="#" alt="Result" />
</div>
</div>
</div>
<!-- Toast Container (positioned to follow viewport) -->
<div class="toast-container" id="toastContainer"></div>
<script>
let currentFile = null;
let resultBase64 = null;
// ChromaKey Core Functions (converted from TypeScript)
function detectMagentaColor(imageData) {
const { data } = imageData;
const magentaPixels = [];
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const magentaness = (r + b) - g;
const brightness = (r + g + b) / 3;
if (magentaness > 50 && brightness > 50) {
magentaPixels.push([r, g, b]);
}
}
if (magentaPixels.length === 0) {
return [255, 0, 255]; // Default magenta
}
const sum = magentaPixels.reduce(
(acc, pixel) => [acc[0] + pixel[0], acc[1] + pixel[1], acc[2] + pixel[2]],
[0, 0, 0]
);
return [
Math.round(sum[0] / magentaPixels.length),
Math.round(sum[1] / magentaPixels.length),
Math.round(sum[2] / magentaPixels.length)
];
}
function colorDistanceHSV(c1, c2) {
const hsv1 = rgbToHsv(c1);
const hsv2 = rgbToHsv(c2);
const dh = Math.min(Math.abs(hsv1[0] - hsv2[0]), 360 - Math.abs(hsv1[0] - hsv2[0]));
const ds = Math.abs(hsv1[1] - hsv2[1]);
const dv = Math.abs(hsv1[2] - hsv2[2]);
return Math.sqrt(dh * dh * 4 + ds * ds + dv * dv);
}
function colorDistance(c1, c2) {
const dr = c1[0] - c2[0];
const dg = c1[1] - c2[1];
const db = c1[2] - c2[2];
return Math.sqrt(dr * dr + dg * dg + db * db);
}
function rgbToHsv(rgb) {
const r = rgb[0] / 255;
const g = rgb[1] / 255;
const b = rgb[2] / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const diff = max - min;
let h = 0;
if (diff !== 0) {
if (max === r) h = 60 * (((g - b) / diff) % 6);
else if (max === g) h = 60 * ((b - r) / diff + 2);
else h = 60 * ((r - g) / diff + 4);
}
if (h < 0) h += 360;
const s = max === 0 ? 0 : diff / max;
const v = max;
return [h, s * 100, v * 100];
}
function chromaKeyPixelData(imageData, options = {}) {
const {
keyColor = [255, 0, 255],
tolerance = 20,
softness = 5,
spill = 30,
preserveEdges = true
} = options;
const d = new Uint8ClampedArray(imageData.data);
const toleranceRange = (tolerance / 100) * 255;
const softnessRange = (softness / 100) * 255;
const spillStrength = spill / 100;
for (let i = 0; i < d.length; i += 4) {
const pixel = [d[i], d[i + 1], d[i + 2]];
const distance = colorDistanceHSV(pixel, keyColor);
const rgbDistance = colorDistance(pixel, keyColor);
const finalDistance = Math.min(distance * 2, rgbDistance);
if (finalDistance <= toleranceRange) {
if (finalDistance <= toleranceRange - softnessRange) {
d[i + 3] = 0; // Fully transparent
} else {
const alpha = (finalDistance - (toleranceRange - softnessRange)) / softnessRange;
d[i + 3] = Math.round(alpha * 255);
}
} else if (spillStrength > 0) {
const spillDistance = finalDistance - toleranceRange;
const spillRange = toleranceRange * 2;
if (spillDistance < spillRange) {
const spillFactor = 1 - (spillDistance / spillRange);
const spillAmount = spillFactor * spillStrength;
if (keyColor[0] > 200 && keyColor[2] > 200 && keyColor[1] < 100) {
d[i] = Math.round(d[i] * (1 - spillAmount * 0.3));
d[i + 2] = Math.round(d[i + 2] * (1 - spillAmount * 0.3));
}
}
}
}
return {
data: d,
width: imageData.width,
height: imageData.height
};
}
// UI Helper Functions
async function getImageDataFromBase64(base64) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
resolve(imageData);
};
img.onerror = () => reject(new Error("Failed to load image"));
img.src = base64;
});
}
function imageDataToBase64(imageData) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = imageData.width;
canvas.height = imageData.height;
const canvasImageData = ctx.createImageData(imageData.width, imageData.height);
canvasImageData.data.set(imageData.data);
ctx.putImageData(canvasImageData, 0, 0);
return canvas.toDataURL("image/png");
}
async function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error("Failed to read file"));
reader.readAsDataURL(file);
});
}
function hexToRgb(hex) {
hex = hex.replace('#', '');
return [
parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16)
];
}
function rgbToHex(rgb) {
return '#' + rgb.map(c => Math.round(c).toString(16).padStart(2, '0')).join('');
}
function createSolidBackground(width, height, color) {
const data = new Uint8ClampedArray(width * height * 4);
for (let i = 0; i < data.length; i += 4) {
data[i] = color[0]; // R
data[i + 1] = color[1]; // G
data[i + 2] = color[2]; // B
data[i + 3] = 255; // A (fully opaque)
}
return {
data,
width,
height
};
}
function compositeImages(foreground, background) {
if (foreground.width !== background.width || foreground.height !== background.height) {
// Resize background to match foreground
background = resizeImageData(background, foreground.width, foreground.height);
}
const result = new Uint8ClampedArray(foreground.data);
for (let i = 0; i < result.length; i += 4) {
const fgAlpha = result[i + 3] / 255;
if (fgAlpha === 0) {
// Foreground is transparent, use background
result[i] = background.data[i];
result[i + 1] = background.data[i + 1];
result[i + 2] = background.data[i + 2];
result[i + 3] = background.data[i + 3];
} else if (fgAlpha < 1) {
// Alpha blend
const invAlpha = 1 - fgAlpha;
result[i] = Math.round(result[i] * fgAlpha + background.data[i] * invAlpha);
result[i + 1] = Math.round(result[i + 1] * fgAlpha + background.data[i + 1] * invAlpha);
result[i + 2] = Math.round(result[i + 2] * fgAlpha + background.data[i + 2] * invAlpha);
result[i + 3] = 255; // Result is opaque after compositing
}
// If fgAlpha === 1, keep foreground pixel as-is
}
return {
data: result,
width: foreground.width,
height: foreground.height
};
}
function resizeImageData(imageData, newWidth, newHeight) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Create canvas with original size
canvas.width = imageData.width;
canvas.height = imageData.height;
// Put original image data
const canvasImageData = ctx.createImageData(imageData.width, imageData.height);
canvasImageData.data.set(imageData.data);
ctx.putImageData(canvasImageData, 0, 0);
// Create new canvas with target size
const resizedCanvas = document.createElement('canvas');
const resizedCtx = resizedCanvas.getContext('2d');
resizedCanvas.width = newWidth;
resizedCanvas.height = newHeight;
// Draw resized image
resizedCtx.drawImage(canvas, 0, 0, newWidth, newHeight);
// Return new image data
return resizedCtx.getImageData(0, 0, newWidth, newHeight);
}
// UI Event Handlers
function setupEventListeners() {
const fileInput = document.getElementById('fileInput');
const sliders = ['tolerance', 'softness', 'spill'];
const keyColorPicker = document.getElementById('keyColor');
fileInput.addEventListener('change', function(e) {
if (e.target.files && e.target.files[0]) {
currentFile = e.target.files[0];
document.getElementById('processBtn').disabled = false;
// Show original image
const reader = new FileReader();
reader.onload = function(e) {
const originalImg = document.getElementById('originalImage');
originalImg.src = e.target.result;
document.getElementById('results').style.display = 'grid';
// Update magnifier image data when new image loads
originalImg.onload = function() {
updateImageData();
};
};
reader.readAsDataURL(currentFile);
clearMessages();
showInfo('Image loaded. Ready to apply ChromaKey processing.');
}
});
// Setup slider event listeners
sliders.forEach(sliderId => {
const slider = document.getElementById(sliderId);
const valueSpan = document.getElementById(sliderId + 'Value');
slider.addEventListener('input', function(e) {
valueSpan.textContent = e.target.value;
});
});
// Key color picker change listener
keyColorPicker.addEventListener('change', function(e) {
const hexColour = e.target.value;
const rgbColour = hexToRgb(hexColour);
updateKeyColorLabel(rgbColour, hexColour);
});
// Background type selector
document.getElementById('backgroundType').addEventListener('change', function(e) {
const colorPicker = document.getElementById('backgroundColor');
const imagePicker = document.getElementById('backgroundImage');
colorPicker.style.display = e.target.value === 'color' ? 'inline' : 'none';
imagePicker.style.display = e.target.value === 'image' ? 'inline' : 'none';
});
}
function setPreset(preset) {
const keyColor = document.getElementById('keyColor');
const tolerance = document.getElementById('tolerance');
const softness = document.getElementById('softness');
const spill = document.getElementById('spill');
switch(preset) {
case 'default':
keyColor.value = '#ff00ff'; // Magenta
tolerance.value = 20;
softness.value = 5;
spill.value = 30;
break;
case 'strict':
keyColor.value = '#ff00ff';
tolerance.value = 10;
softness.value = 2;
spill.value = 50;
break;
case 'soft':
keyColor.value = '#ff00ff';
tolerance.value = 35;
softness.value = 15;
spill.value = 20;
break;
case 'green':
keyColor.value = '#00ff00'; // Green
tolerance.value = 25;
softness.value = 8;
spill.value = 40;
break;
case 'blue':
keyColor.value = '#0000ff'; // Blue
tolerance.value = 25;
softness.value = 8;
spill.value = 40;
break;
}
// Update value displays
document.getElementById('toleranceValue').textContent = tolerance.value;
document.getElementById('softnessValue').textContent = softness.value;
document.getElementById('spillValue').textContent = spill.value;
// Update colour label
const rgbColour = hexToRgb(keyColor.value);
updateKeyColorLabel(rgbColour, keyColor.value);
showInfo(`Preset applied: ${preset}`);
}
async function detectMagentaFromImage() {
if (!currentFile) {
showError('Please select an image first');
return;
}
try {
showInfo('Detecting key colour from image...');
const inputBase64 = await fileToBase64(currentFile);
const imageData = await getImageDataFromBase64(inputBase64);
const detectedColour = detectMagentaColor(imageData);
const hexColour = rgbToHex(detectedColour);
document.getElementById('keyColor').value = hexColour;
// Update the colour label
updateKeyColorLabel(detectedColour, hexColour);
showSuccess(`Auto-detected key colour: RGB(${detectedColour.join(', ')})`);
} catch (error) {
showError('Auto-detection failed: ' + error.message);
}
}
async function processImage() {
if (!currentFile) {
showError('Please select an image first');
return;
}
try {
showInfo('Applying ChromaKey processing...');
document.body.classList.add('processing');
const inputBase64 = await fileToBase64(currentFile);
const imageData = await getImageDataFromBase64(inputBase64);
const options = {
keyColor: hexToRgb(document.getElementById('keyColor').value),
tolerance: parseInt(document.getElementById('tolerance').value),
softness: parseInt(document.getElementById('softness').value),
spill: parseInt(document.getElementById('spill').value),
preserveEdges: document.getElementById('preserveEdges').checked
};
// Apply chromakey to create transparency
const processedImageData = chromaKeyPixelData(imageData, options);
// Handle background composition
const backgroundType = document.getElementById('backgroundType').value;
let finalImageData = processedImageData;
if (backgroundType === 'color') {
const backgroundColor = hexToRgb(document.getElementById('backgroundColor').value);
const backgroundImageData = createSolidBackground(
processedImageData.width,
processedImageData.height,
backgroundColor
);
finalImageData = compositeImages(processedImageData, backgroundImageData);
showInfo('Composited with solid colour background');
} else if (backgroundType === 'image') {
const backgroundFileInput = document.getElementById('backgroundImage');
if (backgroundFileInput.files && backgroundFileInput.files[0]) {
const backgroundBase64 = await fileToBase64(backgroundFileInput.files[0]);
const backgroundImageData = await getImageDataFromBase64(backgroundBase64);
finalImageData = compositeImages(processedImageData, backgroundImageData);
showInfo('Composited with custom background image');
} else {
showInfo('No background image selected, using transparent background');
}
} else {
showInfo('Using transparent background');
}
resultBase64 = imageDataToBase64(finalImageData);
document.getElementById('resultImage').src = resultBase64;
document.getElementById('downloadBtn').style.display = 'inline-block';
showSuccess('ChromaKey processing completed successfully!');
} catch (error) {
showError('Processing failed: ' + error.message);
} finally {
document.body.classList.remove('processing');
}
}
function downloadResult() {
if (!resultBase64) {
showError('No processed image to download');
return;
}
const link = document.createElement('a');
link.download = 'chromakey-result.png';
link.href = resultBase64;
link.click();
showInfo('Download started');
}
// Toast Notification System
let toastId = 0;
function createToast(message, type = 'info', duration = 3000) {
const toastContainer = document.getElementById('toastContainer');
if (!toastContainer) return;
const id = ++toastId;
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.id = `toast-${id}`;
// Create close button
const closeBtn = document.createElement('button');
closeBtn.className = 'toast-close';
closeBtn.innerHTML = '×';
closeBtn.onclick = () => dismissToast(id);
// Set message content
const messageSpan = document.createElement('span');
messageSpan.textContent = message;
toast.appendChild(messageSpan);
toast.appendChild(closeBtn);
// Add click to dismiss
toast.onclick = (e) => {
if (e.target !== closeBtn) {
dismissToast(id);
}
};
toastContainer.appendChild(toast);
// Trigger show animation
setTimeout(() => {
toast.classList.add('show');
}, 10);
// Auto dismiss
setTimeout(() => {
dismissToast(id);
}, duration);
return id;
}
function dismissToast(id) {
const toast = document.getElementById(`toast-${id}`);
if (!toast) return;
toast.classList.remove('show');
toast.classList.add('hide');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}
function showInfo(message) {
createToast(message, 'info', 3000);
}
function showError(message) {
createToast(message, 'error', 5000);
}
function showSuccess(message) {
createToast(message, 'success', 4000);
}
function clearMessages() {
const toastContainer = document.getElementById('toastContainer');
if (toastContainer) {
toastContainer.innerHTML = '';
}
}
function updateKeyColorLabel(rgbColor, hexColor) {
const label = document.getElementById('keyColorLabel');
const colourName = getColourName(rgbColor);
label.textContent = `${colourName} (${hexColor.toUpperCase()})`;
}
function getColourName(rgb) {
const [r, g, b] = rgb;
// Pure colours with tight tolerances first (most specific)
if (r > 250 && g < 10 && b > 250) return 'Fuchsia-ish'; // #FF00FF
if (r > 250 && g < 10 && b < 10) return 'Red-ish'; // #FF0000
if (r < 10 && g > 250 && b < 10) return 'Lime-ish'; // #00FF00
if (r < 10 && g < 10 && b > 250) return 'Blue-ish'; // #0000FF
if (r > 250 && g > 250 && b < 10) return 'Yellow-ish'; // #FFFF00
if (r < 10 && g > 250 && b > 250) return 'Aqua-ish'; // #00FFFF
if (r > 250 && g > 250 && b > 250) return 'White-ish'; // #FFFFFF
if (r < 10 && g < 10 && b < 10) return 'Black-ish'; // #000000
// Specific named colours with medium tolerances
if (r > 220 && r < 255 && g > 20 && g < 70 && b > 120 && b < 170) return 'DeepPink-ish'; // #FF1493
if (r > 200 && r < 255 && g > 90 && g < 140 && b > 170 && b < 220) return 'HotPink-ish'; // #FF69B4
if (r > 230 && r < 255 && g > 160 && g < 210 && b > 190 && b < 240) return 'Pink-ish'; // #FFC0CB
if (r > 110 && r < 160 && g > 20 && g < 70 && b > 110 && b < 160) return 'Purple-ish'; // #800080
if (r > 70 && r < 120 && g < 30 && b > 100 && b < 150) return 'Indigo-ish'; // #4B0082
if (r > 230 && r < 255 && g > 120 && g < 170 && b < 30) return 'Orange-ish'; // #FFA500
if (r > 240 && r < 255 && g > 35 && g < 85 && b < 30) return 'OrangeRed-ish'; // #FF4500
if (r > 220 && r < 255 && g > 10 && g < 60 && b > 130 && b < 180) return 'Crimson-ish'; // #DC143C
if (r > 170 && r < 220 && g > 30 && g < 80 && b > 40 && b < 90) return 'Brown-ish'; // #A52A2A
if (r > 120 && r < 170 && g > 40 && g < 90 && b > 10 && b < 60) return 'SaddleBrown-ish'; // #8B4513
if (r > 160 && r < 210 && g > 80 && g < 130 && b > 30 && b < 80) return 'Chocolate-ish'; // #D2691E
if (r > 200 && r < 240 && g > 170 && g < 210 && b > 100 && b < 150) return 'Tan-ish'; // #D2B48C
if (r > 230 && r < 255 && g > 220 && g < 255 && b > 160 && b < 210) return 'Beige-ish'; // #F5F5DC
if (r > 240 && r < 255 && g > 240 && g < 255 && b > 220 && b < 255) return 'Ivory-ish'; // #FFFFF0
if (r > 90 && r < 140 && g > 100 && g < 150 && b > 100 && b < 150) return 'Grey-ish'; // #808080
if (r > 160 && r < 210 && g > 160 && g < 210 && b > 160 && b < 210) return 'Silver-ish'; // #C0C0C0
if (r > 30 && r < 80 && g > 70 && g < 120 && b > 30 && b < 80) return 'DarkOliveGreen-ish'; // #556B2F
if (r > 100 && r < 150 && g > 140 && g < 190 && b > 40 && b < 90) return 'Olive-ish'; // #808000
if (r > 140 && r < 190 && g > 200 && g < 250 && b > 30 && b < 80) return 'YellowGreen-ish'; // #9ACD32
if (r > 120 && r < 170 && g > 190 && g < 240 && b > 120 && b < 170) return 'LightGreen-ish'; // #90EE90
if (r > 30 && r < 80 && g > 90 && g < 140 && b > 30 && b < 80) return 'ForestGreen-ish'; // #228B22
if (r > 20 && r < 70 && g > 90 && g < 140 && b > 70 && b < 120) return 'SeaGreen-ish'; // #2E8B57
if (r > 60 && r < 110 && g > 150 && g < 200 && b > 150 && b < 200) return 'MediumSeaGreen-ish'; // #3CB371
if (r > 0 && r < 50 && g > 180 && g < 230 && b > 140 && b < 190) return 'SpringGreen-ish'; // #00FF7F
if (r > 90 && r < 140 && g > 200 && g < 250 && b > 200 && b < 250) return 'Turquoise-ish'; // #40E0D0
if (r > 140 && r < 190 && g > 230 && g < 255 && b > 230 && b < 255) return 'LightCyan-ish'; // #E0FFFF
if (r > 90 && r < 140 && g > 150 && g < 200 && b > 200 && b < 250) return 'SkyBlue-ish'; // #87CEEB
if (r > 160 && r < 210 && g > 210 && g < 255 && b > 230 && b < 255) return 'LightBlue-ish'; // #ADD8E6
if (r > 90 && r < 140 && g > 90 && g < 140 && b > 220 && b < 255) return 'RoyalBlue-ish'; // #4169E1
if (r > 20 && r < 70 && g > 20 && g < 70 && b > 130 && b < 180) return 'DarkBlue-ish'; // #00008B
if (r < 30 && g < 30 && b > 120 && b < 170) return 'Navy-ish'; // #000080
if (r > 120 && r < 170 && g > 100 && g < 150 && b > 210 && b < 255) return 'SlateBlue-ish'; // #6A5ACD
if (r > 140 && r < 190 && g > 60 && g < 110 && b > 200 && b < 250) return 'MediumSlateBlue-ish'; // #7B68EE
if (r > 210 && r < 255 && g > 150 && g < 200 && b > 230 && b < 255) return 'Lavender-ish'; // #E6E6FA
if (r > 210 && r < 255 && g > 160 && g < 210 && b > 240 && b < 255) return 'Thistle-ish'; // #D8BFD8
if (r > 180 && r < 230 && g > 120 && g < 170 && b > 200 && b < 250) return 'Plum-ish'; // #DDA0DD
if (r > 190 && r < 240 && g > 90 && g < 140 && b > 190 && b < 240) return 'Violet-ish'; // #EE82EE
if (r > 210 && r < 255 && g > 100 && g < 150 && b > 210 && b < 255) return 'Orchid-ish'; // #DA70D6
if (r > 180 && r < 230 && g > 50 && g < 100 && b > 180 && b < 230) return 'MediumOrchid-ish'; // #BA55D3
if (r > 140 && r < 190 && g > 30 && g < 80 && b > 190 && b < 240) return 'BlueViolet-ish'; // #8A2BE2
// Broader colour categories with wider tolerances (less specific)
if (r > 200 && g < 100 && b > 150) return 'Magenta-ish';
if (r < 100 && g > 200 && b < 100) return 'Green-ish';
if (r < 100 && g < 100 && b > 200) return 'Blue-ish';
if (r > 200 && g < 100 && b < 100) return 'Red-ish';
if (r > 200 && g > 200 && b < 100) return 'Yellow-ish';
if (r < 100 && g > 200 && b > 200) return 'Cyan-ish';
if (r > 150 && g > 100 && b < 100) return 'Orange-ish';
if (r > 100 && g < 100 && b > 150) return 'Purple-ish';
if (r > 180 && g > 180 && b > 180) return 'Light-ish';
if (r < 80 && g < 80 && b < 80) return 'Dark-ish';
// Final fallback based on dominant component
const max = Math.max(r, g, b);
const secondMax = Math.max(...[r, g, b].filter(x => x !== max));
if (max - secondMax < 30) {
// Similar values - mixed colour
if (r === max && g === secondMax) return 'Orange-ish';
if (r === max && b === secondMax) return 'Pink-ish';
if (g === max && b === secondMax) return 'Teal-ish';
if (g === max && r === secondMax) return 'Chartreuse-ish';
if (b === max && r === secondMax) return 'Violet-ish';
if (b === max && g === secondMax) return 'Azure-ish';
}
// Pure dominant component
if (r === max) return 'Red-ish';
if (g === max) return 'Green-ish';
if (b === max) return 'Blue-ish';
return 'Custom Colour';
}
// Magnifier functionality
let magnifierCanvas = null;
let magnifierCtx = null;
let originalImageElement = null;
let magnifierElement = null;
let colorPreviewElement = null;
let imageCanvas = null;
let imageCtx = null;
let imageData = null;
function initializeMagnifier() {
magnifierCanvas = document.getElementById('magnifierCanvas');
magnifierCtx = magnifierCanvas.getContext('2d');
originalImageElement = document.getElementById('originalImage');
magnifierElement = document.getElementById('magnifier');
colorPreviewElement = document.getElementById('colorPreview');
// Create off-screen canvas for pixel data access
imageCanvas = document.createElement('canvas');
imageCtx = imageCanvas.getContext('2d');
// Set up event listeners
originalImageElement.addEventListener('mouseenter', showMagnifier);
originalImageElement.addEventListener('mouseleave', hideMagnifier);
originalImageElement.addEventListener('mousemove', updateMagnifier);
originalImageElement.addEventListener('click', pickColor);
// Update image data when image loads
originalImageElement.addEventListener('load', updateImageData);
}
function updateImageData() {
if (!originalImageElement.complete || !originalImageElement.naturalWidth) return;
imageCanvas.width = originalImageElement.naturalWidth;
imageCanvas.height = originalImageElement.naturalHeight;
imageCtx.drawImage(originalImageElement, 0, 0);
imageData = imageCtx.getImageData(0, 0, imageCanvas.width, imageCanvas.height);
}
function showMagnifier() {
if (magnifierElement) {
magnifierElement.style.display = 'block';
}
}
function hideMagnifier() {
if (magnifierElement) {
magnifierElement.style.display = 'none';
}
}
function updateMagnifier(e) {
if (!imageData || !magnifierElement || !magnifierCanvas) return;
const rect = originalImageElement.getBoundingClientRect();
const scaleX = originalImageElement.naturalWidth / rect.width;
const scaleY = originalImageElement.naturalHeight / rect.height;
// Get mouse position relative to image
const mouseX = (e.clientX - rect.left) * scaleX;
const mouseY = (e.clientY - rect.top) * scaleY;
// Position magnifier above and slightly to the right of cursor
const magnifierX = e.clientX - rect.left + 20;
const magnifierY = e.clientY - rect.top - 140;
magnifierElement.style.left = magnifierX + 'px';
magnifierElement.style.top = magnifierY + 'px';
// Get color at exact mouse position
const pixelX = Math.floor(mouseX);
const pixelY = Math.floor(mouseY);
const centerColor = getPixelColor(pixelX, pixelY);
// Update color preview
if (colorPreviewElement && centerColor) {
colorPreviewElement.textContent = `RGB(${centerColor[0]}, ${centerColor[1]}, ${centerColor[2]})`;
colorPreviewElement.style.backgroundColor = `rgb(${centerColor[0]}, ${centerColor[1]}, ${centerColor[2]})`;
colorPreviewElement.style.color = (centerColor[0] + centerColor[1] + centerColor[2]) / 3 > 128 ? 'black' : 'white';
}
// Draw magnified area (16x16 pixels around mouse)
magnifierCtx.imageSmoothingEnabled = false;
magnifierCtx.clearRect(0, 0, 120, 120);
const radius = 8; // 16x16 area (8 pixels in each direction)
const magnification = 120 / 16; // Scale to fit 120px canvas
for (let y = -radius; y < radius; y++) {
for (let x = -radius; x < radius; x++) {
const srcX = Math.floor(mouseX) + x;
const srcY = Math.floor(mouseY) + y;
const color = getPixelColor(srcX, srcY);
if (color) {
magnifierCtx.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
magnifierCtx.fillRect(
(x + radius) * magnification,
(y + radius) * magnification,
magnification,
magnification
);
}
}
}
}
function getPixelColor(x, y) {
if (!imageData || x < 0 || y < 0 || x >= imageData.width || y >= imageData.height) {
return null;
}
const index = (Math.floor(y) * imageData.width + Math.floor(x)) * 4;
return [
imageData.data[index], // R
imageData.data[index + 1], // G
imageData.data[index + 2] // B
];
}
function pickColor(e) {
if (!imageData) return;
const rect = originalImageElement.getBoundingClientRect();
const scaleX = originalImageElement.naturalWidth / rect.width;
const scaleY = originalImageElement.naturalHeight / rect.height;
const mouseX = Math.floor((e.clientX - rect.left) * scaleX);
const mouseY = Math.floor((e.clientY - rect.top) * scaleY);
const color = getPixelColor(mouseX, mouseY);
if (color) {
const hexColor = rgbToHex(color);
document.getElementById('keyColor').value = hexColor;
updateKeyColorLabel(color, hexColor);
showSuccess(`Picked colour: RGB(${color[0]}, ${color[1]}, ${color[2]}) - ${getColourName(color)}`);
// Hide magnifier after picking
hideMagnifier();
// Re-show after a brief delay
setTimeout(() => {
if (originalImageElement.matches(':hover')) {
showMagnifier();
}
}, 100);
}
}
// Load default magenta.png image
async function loadDefaultImage() {
try {
console.log('Loading default magenta.png image...');
console.log('Current working directory:', window.location.href);
// Fetch magenta.png and convert to File object
const response = await fetch('./magenta.png');
console.log('Fetch response status:', response.status, response.statusText);
if (!response.ok) {
throw new Error(`Failed to fetch magenta.png: ${response.status} ${response.statusText}`);
}
const blob = await response.blob();
console.log('Blob created, size:', blob.size, 'type:', blob.type);
const file = new File([blob], 'magenta.png', { type: 'image/png' });
console.log('File object created:', file.name, file.size, file.type);
// Set as current file
currentFile = file;
console.log('currentFile set to:', currentFile.name);
// Show original image
const base64 = await fileToBase64(file);
console.log('Base64 conversion successful, length:', base64.length);
const originalImg = document.getElementById('originalImage');
const resultsDiv = document.getElementById('results');
const processBtn = document.getElementById('processBtn');
if (!originalImg || !resultsDiv || !processBtn) {
throw new Error('Required DOM elements not found');
}
originalImg.src = base64;
resultsDiv.style.display = 'grid';
processBtn.disabled = false;
console.log('Default magenta.png image loaded successfully');
showInfo('Magenta.png loaded as default image. Ready to apply ChromaKey!');
} catch (error) {
console.error('Failed to load default image:', error);
showError('Failed to load default magenta.png image: ' + error.message);
showInfo('Select an image file with magenta background to begin processing');
}
}
// Initialise
document.addEventListener('DOMContentLoaded', function() {
setupEventListeners();
initializeMagnifier();
loadDefaultImage();
showInfo('ChromaKey tool loaded. Loading default magenta.png image...');
});
</script>
</body>
</html>
@twobob
Copy link
Author

twobob commented Oct 17, 2025

magenta

The html file is just to show the kind of thing the .ts file can do, but doesn't actually call it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment