Skip to content

Instantly share code, notes, and snippets.

@dgerrells
Created February 23, 2025 03:13
Show Gist options
  • Select an option

  • Save dgerrells/9435b9aba6d907ae40ff28078a1931ac to your computer and use it in GitHub Desktop.

Select an option

Save dgerrells/9435b9aba6d907ae40ff28078a1931ac to your computer and use it in GitHub Desktop.
Non-trivial View Transitions
import { Html } from "@elysiajs/html";
import { createCanvas, ImageData, loadImage } from "canvas";
async function downsampleImageData(imageData: ImageData, scale: number) {
const width = Math.floor(imageData.width * scale);
const height = Math.floor(imageData.height * scale);
const canvas = createCanvas(width, height);
const context = canvas.getContext("2d");
// Create a temporary canvas to draw the original image data
const tempCanvas = createCanvas(imageData.width, imageData.height);
const tempContext = tempCanvas.getContext("2d");
tempContext.putImageData(imageData, 0, 0);
// Draw the downsampled image using nearest neighbor
context.imageSmoothingEnabled = false;
context.drawImage(tempCanvas, 0, 0, width, height);
return context.getImageData(0, 0, width, height);
}
function greedyMesh(imageData: ImageData, scaleFactor: number) {
const { width, height, data } = imageData;
const visited = new Array(width * height).fill(false);
const divData = [];
function getColor(x: number, y: number) {
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 `rgba(${r}, ${g}, ${b}, ${a})`;
}
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: Math.floor((maxX - x + 1) * scaleFactor),
height: Math.floor((maxY - y + 1) * scaleFactor),
backgroundColor: color,
transform: `translate(${x * scaleFactor}px, ${y * scaleFactor}px)`,
});
}
}
divData.sort((a, b) => b.width * b.height - a.width * a.height);
while (vId < maxIds && vId < divData.length) {
divData[vId].viewTransitionName = `block-${vId++}`;
}
// while (vId < width * height) {
// vId++;
// divData.push({
// position: "absolute",
// display: "none",
// });
// }
return divData.map(({ width, height, ...styles }) => (
<div style={{ ...styles, width: `${width}px`, height: `${height}px` }} />
));
}
export async function ImgDissolveGreed({ imagePath }): Promise<JSX.Element> {
const rootPath = process.cwd();
const fullPath = `${rootPath}/public/${imagePath}`;
const img = await loadImage(fullPath);
const canvas = createCanvas(img.width, img.height);
const context = canvas.getContext("2d");
context.drawImage(img, 0, 0);
const originalImageData = context.getImageData(0, 0, img.width, img.height);
const downsampledImageData = await downsampleImageData(
originalImageData,
0.5
);
const divs = greedyMesh(downsampledImageData, 4);
return (
<div
style={{
position: "relative",
width: `${img.width * 2}px`,
height: `${img.height * 2}px`,
margin: "auto",
}}
>
{divs}
<style>{`
html::view-transition-group(*) {
animation-duration: .3s;
}`}</style>
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment