Skip to content

Instantly share code, notes, and snippets.

@dgerrells
Created February 23, 2025 18:56
Show Gist options
  • Save dgerrells/e35f16344fee8c58a92db45a5e83fa23 to your computer and use it in GitHub Desktop.
Save dgerrells/e35f16344fee8c58a92db45a5e83fa23 to your computer and use it in GitHub Desktop.
Break up image as divs client side
import { Html } from "@elysiajs/html";
export function ImgResolve({ imagePath }) {
const fullPath = `/public/${imagePath}`;
return (
<div
id="image-container"
style={{
position: "relative",
margin: "auto",
width: `${304 * 1.6}px`,
height: `${174 * 1.6}px`,
}}
>
<script>
{`
async function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
function downsampleImageData(imageData, scale) {
const width = Math.floor(imageData.width * scale);
const height = Math.floor(imageData.height * scale);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
const tempCanvas = document.createElement('canvas');
tempCanvas.width = imageData.width;
tempCanvas.height = imageData.height;
const tempContext = tempCanvas.getContext('2d');
tempContext.putImageData(imageData, 0, 0);
context.imageSmoothingEnabled = false;
context.drawImage(tempCanvas, 0, 0, width, height);
return context.getImageData(0, 0, width, height);
}
function greedyMesh(imageData, scaleFactor) {
const { width, height, data } = imageData;
const visited = new Array(width * height).fill(false);
const divData = [];
function getColor(x, y) {
const index = (y * width + x) * 4;
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
const a = data[index + 3] / 255;
return [r, g, b, a]; // Return as array
}
function colorsMatch(x1, y1, x2, y2) {
const index1 = (y1 * width + x1) * 4;
const index2 = (y2 * width + x2) * 4;
for (let i = 0; i < 4; i++) {
if (data[index1 + i] !== data[index2 + i]) return false;
}
return true;
}
let vId = 0;
let maxIds = 32;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (visited[y * width + x]) continue;
const color = getColor(x, y);
let maxX = x;
let maxY = y;
// Expand in x direction
while (
maxX + 1 < width &&
!visited[y * width + maxX + 1] &&
colorsMatch(x, y, maxX + 1, y)
) {
maxX++;
}
// Expand in y direction
let expandY = true;
while (expandY && maxY + 1 < height) {
for (let i = x; i <= maxX; i++) {
if (
visited[(maxY + 1) * width + i] ||
!colorsMatch(x, y, i, maxY + 1)
) {
expandY = false;
break;
}
}
if (expandY) maxY++;
}
// Mark visited
for (let i = x; i <= maxX; i++) {
for (let j = y; j <= maxY; j++) {
visited[j * width + i] = true;
}
}
divData.push({
position: "absolute",
width: (maxX - x + 1) * scaleFactor, // Store as number
height: (maxY - y + 1) * scaleFactor, // Store as number
color: getColor(x, y),
top: 0,
left: 0,
transform: { x: x * scaleFactor, y: y * scaleFactor }, // Store as object
});
}
}
divData.sort((a, b) => b.width * b.height - a.width * a.height);
while (vId < maxIds && vId < divData.length) {
divData[vId].viewTransitionName = \`block-\${vId++}\`;
}
return divData;
}
function tween(oldData, newData, progress, easingFunction) {
if (!oldData) return newData;
const easedProgress = easingFunction(progress);
return newData.map((newDiv, index) => {
const oldDiv = oldData[index] || newDiv;
const tweenedDiv = { ...newDiv };
tweenedDiv.width = oldDiv.width + (newDiv.width - oldDiv.width) * easedProgress;
tweenedDiv.height = oldDiv.height + (newDiv.height - oldDiv.height) * easedProgress;
const tweenedX = oldDiv.transform.x + (newDiv.transform.x - oldDiv.transform.x) * easedProgress;
const tweenedY = oldDiv.transform.y + (newDiv.transform.y - oldDiv.transform.y) * easedProgress;
tweenedDiv.transform = { x: tweenedX, y: tweenedY };
const [oldR, oldG, oldB, oldA] = oldDiv.color;
const [newR, newG, newB, newA] = newDiv.color;
const tweenedColor = [
oldR + (newR - oldR) * easedProgress,
oldG + (newG - oldG) * easedProgress,
oldB + (newB - oldB) * easedProgress,
oldA + (newA - oldA) * easedProgress,
];
tweenedDiv.color = tweenedColor;
return tweenedDiv;
});
}
async function processImage(imagePath) {
const img = await loadImage(imagePath);
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0);
const originalImageData = context.getImageData(0, 0, img.width, img.height);
const downsampledImageData = downsampleImageData(originalImageData, 0.4);
const divs = greedyMesh(downsampledImageData, 4);
return { divs, width: img.width * 1.6, height: img.height * 1.6 };
}
function saveToLocalStorage(data) {
localStorage.setItem('imageDivData', JSON.stringify(data));
}
function loadFromLocalStorage() {
const data = localStorage.getItem('imageDivData');
return data ? JSON.parse(data) : null;
}
async function init() {
const imagePath = '${fullPath}';
let oldDivData = loadFromLocalStorage();
const { divs: newDivData, width, height } = await processImage(imagePath);
if (!oldDivData) {
oldDivData = newDivData;
}
const container = document.getElementById('image-container');
container.style.width = width + 'px';
container.style.height = height + 'px';
const divs = [];
const maxLength = Math.max(oldDivData.length, newDivData.length);
for (let i = 0; i < maxLength; i++) {
const div = document.createElement('div');
container.appendChild(div);
divs.push(div);
}
for (let i = oldDivData.length; i < maxLength; i++) {
oldDivData.push({
width: 0,
height: 0,
transform: { x: Math.random()*width, y: Math.random()*height },
color: [0,0,0,0],
});
}
for (let i = newDivData.length; i < maxLength; i++) {
newDivData.push({
...oldDivData[i],
transform: { x: Math.random()*width, y: Math.random()*height },
color: [0,0,0,0],
});
}
let progress = 0;
let frameCount = 0;
const saveEveryNthFrame = 10;
const duration = 500;
let easingFunction = (t) => t * (2 - t);
function animate(timestamp) {
if (!start) start = timestamp;
const elapsed = timestamp - start;
progress = Math.min(elapsed / duration, 1);
const tweenedData = tween(oldDivData, newDivData, progress, easingFunction);
tweenedData.forEach(({ width, height, transform, color, ...styles }, index) => {
const div = divs[index];
const [r, g, b, a] = color;
Object.assign(div.style, styles, {
width: \`\${width}px\`,
height: \`\${height}px\`,
transform: \`translate(\${transform.x}px, \${transform.y}px)\`,
backgroundColor: \`rgba(\${r}, \${g}, \${b}, \${a})\`,
});
});
frameCount++;
if (frameCount % saveEveryNthFrame === 0) {
saveToLocalStorage(tweenedData);
}
if (progress < 1) {
requestAnimationFrame(animate);
}
}
let start;
requestAnimationFrame(animate);
}
window.onload = init;
`}
</script>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment