Created
February 23, 2025 18:56
-
-
Save dgerrells/e35f16344fee8c58a92db45a5e83fa23 to your computer and use it in GitHub Desktop.
Break up image as divs client side
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
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