Created
February 23, 2025 03:13
-
-
Save dgerrells/9435b9aba6d907ae40ff28078a1931ac to your computer and use it in GitHub Desktop.
Non-trivial View Transitions
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"; | |
| 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