Created
January 11, 2025 05:40
-
-
Save zuriby/4fc57e67c48624fe2d6217566ab15b50 to your computer and use it in GitHub Desktop.
Sort image by its pixels colors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Pixel Sorter</title> | |
<style> | |
body { | |
font-family: system-ui, -apple-system, sans-serif; | |
max-width: 1200px; | |
margin: 0 auto; | |
padding: 20px; | |
background: #f5f5f5; | |
} | |
.container { | |
background: white; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
h1 { | |
color: #333; | |
margin-bottom: 20px; | |
} | |
.input-container { | |
margin-bottom: 20px; | |
} | |
.canvas-container { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 20px; | |
margin-top: 20px; | |
} | |
.canvas-wrapper { | |
border: 1px solid #ddd; | |
padding: 10px; | |
border-radius: 4px; | |
} | |
canvas { | |
max-width: 100%; | |
height: auto; | |
} | |
h2 { | |
color: #666; | |
font-size: 1.2em; | |
margin-bottom: 10px; | |
} | |
input[type="file"] { | |
display: block; | |
margin: 10px 0; | |
padding: 10px; | |
border: 1px solid #ddd; | |
border-radius: 4px; | |
width: 100%; | |
box-sizing: border-box; | |
} | |
.sort-options { | |
margin: 10px 0; | |
} | |
select { | |
padding: 8px; | |
border-radius: 4px; | |
border: 1px solid #ddd; | |
margin-right: 10px; | |
} | |
button { | |
padding: 8px 16px; | |
background: #4a90e2; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
} | |
button:hover { | |
background: #357abd; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Pixel Sorter</h1> | |
<div class="input-container"> | |
<input type="file" id="imageInput" accept="image/*"> | |
<div class="sort-options"> | |
<select id="sortMode"> | |
<option value="luminance">Sort by Luminance</option> | |
<option value="hue">Sort by Hue</option> | |
<option value="both">Sort by Both</option> | |
</select> | |
<button id="sortButton">Sort Pixels</button> | |
</div> | |
</div> | |
<div class="canvas-container"> | |
<div class="canvas-wrapper"> | |
<h2>Original Image</h2> | |
<canvas id="originalCanvas"></canvas> | |
</div> | |
<div class="canvas-wrapper"> | |
<h2>Sorted Pixels</h2> | |
<canvas id="sortedCanvas"></canvas> | |
</div> | |
</div> | |
</div> | |
<script> | |
const imageInput = document.getElementById('imageInput'); | |
const sortMode = document.getElementById('sortMode'); | |
const sortButton = document.getElementById('sortButton'); | |
const originalCanvas = document.getElementById('originalCanvas'); | |
const sortedCanvas = document.getElementById('sortedCanvas'); | |
const originalCtx = originalCanvas.getContext('2d'); | |
const sortedCtx = sortedCanvas.getContext('2d'); | |
let currentImageData = null; | |
function rgbToHsl(r, g, b) { | |
r /= 255; | |
g /= 255; | |
b /= 255; | |
const max = Math.max(r, g, b); | |
const min = Math.min(r, g, b); | |
let h, s, l = (max + min) / 2; | |
if (max === min) { | |
h = s = 0; | |
} else { | |
const d = max - min; | |
s = l > 0.5 ? d / (2 - max - min) : d / (max + min); | |
switch (max) { | |
case r: h = (g - b) / d + (g < b ? 6 : 0); break; | |
case g: h = (b - r) / d + 2; break; | |
case b: h = (r - g) / d + 4; break; | |
} | |
h /= 6; | |
} | |
return [h * 360, s * 100, l * 100]; | |
} | |
function getLuminance(r, g, b) { | |
return 0.299 * r + 0.587 * g + 0.114 * b; | |
} | |
function sortPixels(imageData, mode) { | |
const pixels = []; | |
const { data, width, height } = imageData; | |
// Convert ImageData to array of pixels | |
for (let i = 0; i < data.length; i += 4) { | |
pixels.push({ | |
r: data[i], | |
g: data[i + 1], | |
b: data[i + 2], | |
a: data[i + 3], | |
index: i | |
}); | |
} | |
// Sort pixels based on selected mode | |
pixels.sort((a, b) => { | |
const [h1, s1, l1] = rgbToHsl(a.r, a.g, a.b); | |
const [h2, s2, l2] = rgbToHsl(b.r, b.g, b.b); | |
switch (mode) { | |
case 'luminance': | |
return getLuminance(b.r, b.g, b.b) - getLuminance(a.r, a.g, a.b); | |
case 'hue': | |
return h1 - h2; | |
case 'both': | |
const lumDiff = getLuminance(b.r, b.g, b.b) - getLuminance(a.r, a.g, a.b); | |
return Math.abs(lumDiff) > 1 ? lumDiff : h1 - h2; | |
default: | |
return 0; | |
} | |
}); | |
// Create new ImageData with sorted pixels | |
const sortedData = new Uint8ClampedArray(data.length); | |
pixels.forEach((pixel, i) => { | |
const offset = i * 4; | |
sortedData[offset] = pixel.r; | |
sortedData[offset + 1] = pixel.g; | |
sortedData[offset + 2] = pixel.b; | |
sortedData[offset + 3] = pixel.a; | |
}); | |
return new ImageData(sortedData, width, height); | |
} | |
imageInput.addEventListener('change', (e) => { | |
const file = e.target.files[0]; | |
if (file) { | |
const reader = new FileReader(); | |
reader.onload = (event) => { | |
const img = new Image(); | |
img.onload = () => { | |
// Set canvas dimensions | |
originalCanvas.width = img.width; | |
originalCanvas.height = img.height; | |
sortedCanvas.width = img.width; | |
sortedCanvas.height = img.height; | |
// Draw original image | |
originalCtx.drawImage(img, 0, 0); | |
currentImageData = originalCtx.getImageData(0, 0, img.width, img.height); | |
}; | |
img.src = event.target.result; | |
}; | |
reader.readAsDataURL(file); | |
} | |
}); | |
sortButton.addEventListener('click', () => { | |
if (currentImageData) { | |
const mode = sortMode.value; | |
const sortedImageData = sortPixels(currentImageData, mode); | |
sortedCtx.putImageData(sortedImageData, 0, 0); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment