<script>
var images = {
Mario: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAMAAABcgh8DAAAADFBMVEUAAAD/MRjGYwD/lFpY2Im6AAAAAXRSTlMAQObYZgAAAExJREFUeNptzgkKwCAQQ1Enuf+dm8wYbIsfF56CuC6VO0pDACS4tij1DN8WLLUmTltA+Spq5gSmwphEsTRANgXHphXnz7Pk5e929l8Pl0sBHu8kwiYAAAAASUVORK5CYII=",
Mushroom: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAADFBMVEVHcEzmnCG1MSD//v88rhxeAAAAAXRSTlMAQObYZgAAAENJREFUeNp9jEESACAIAhP//+cManA8tBdlR1kiCm3OwMw28QScZdSjVdDo2CKaOGMIlOgdFAOVJuEHyQuDRXzEankDhM8BIzKiB5cAAAAASUVORK5CYII=",
Star: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVHcEzmnCG1MSDkqOjHAAAAAXRSTlMAQObYZgAAADlJREFUeNo1xCEBADAIRcGHIAJ9iIDgCwLQv8LEthMHlnDz+kVDSE3MNj5b2GyCxM2kfHUknlY41AGBagqh/yM9HQAAAABJRU5ErkJggg==",
Block: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAK0lEQVR4AWP4f+4oSYhhjhcWBAS4xEnTQA7AbyqmOB00kAxoEqyj8UAyAAAI6bN914aCXQAAAABJRU5ErkJggg==",
"Special Block": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEVHcEycSgDmnCEAAABTL0nuAAAAAXRSTlMAQObYZgAAAEBJREFUeAFjEA0NDWHIWrVqNUPeqlW7GbKmRq1myIq/BiTipsOIfeFAYmU9iKgCEqt+gVhZYC5I26/dEAP+AwEAS28lRmY43mwAAAAASUVORK5CYII="
};
</script>
.row {
display: flex;
font-size: 4vmin;
gap: 1px;
margin: 1px;
input {
transform: translateZ(0);
display: block;
line-height: 1;
width: 1em;
height: 1em;
margin: 0;
}
}
html {
height: 100%;
}
body {
min-height: 100%;
display: grid;
place-items: center;
}
.imageSelector {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 4px;
justify-content: center;
img {
image-rendering: pixelated;
width: 32px;
cursor: pointer;
padding: 4px;
&[data-selected="true"] {
border: solid 2px;
}
}
}
View Compiled
console.clear();
import gsap from "https://cdn.skypack.dev/gsap@3.10.4";
import React, {
useCallback,
useEffect,
useState,
useRef
} from "https://cdn.skypack.dev/react@17";
import ReactDOM from "https://cdn.skypack.dev/react-dom@17";
function makeRows(image) {
const { width, height, data } = image;
const rows = [];
for (var y = 0; y < height; y++) {
let row = [];
rows.push(row);
let rowi = y * width * 4;
for (var x = 0; x < width; x++) {
let i = x * 4 + rowi;
let checked = data[i + 3] > 0.5;
let color = `rgba(${data[i]},${data[i + 1]},${data[i + 2]},${
data[i + 3]
})`;
row.push({ checked, color });
}
}
return rows;
}
function CheckboxImage({ image }) {
const rows = makeRows(image);
const ref = useRef();
console.log({ image, rows });
const animateCheckboxes = useCallback(
({ index, from, to, ...options }) => {
const checkboxes = ref.current.querySelectorAll("input");
const config = {
duration: 0.3,
ease: "elastic.out(2, 0.5)",
stagger: {
// wrap advanced options in an object
each: 0.05,
from: index,
grid: "auto",
ease: "linear.easeNone"
}
};
if (to) {
return gsap.fromTo(checkboxes, from, { ...config, ...options, ...to });
}
return gsap.from(checkboxes, {
...config,
...options,
...from
});
},
[ref]
);
function animateClick(index) {
var tl = gsap.timeline();
tl.set("body", { "pointer-events": "none" });
tl.add(
animateCheckboxes({
index,
from: { scale: 1 },
to: { scale: 0.5, checked: false },
scale: 1,
repeat: 1,
yoyo: true
})
);
tl.set("body", { "pointer-events": "" });
}
useEffect(() => {
var tl = gsap.timeline();
tl.add([
animateCheckboxes({ index: "center", from: { checked: false } }),
animateCheckboxes({
index: "center",
from: { scale: 0.5 },
to: { scale: 1 }
})
]);
}, []);
let i = 0;
return (
<div className="container" ref={ref}>
{rows.map((row, rowIndex) => (
<div key={rowIndex} className="row">
{row.map(({ checked, color }) => {
const index = i++;
return (
<input
key={index}
type="checkbox"
checked={checked ? "checked" : null}
onClick={() => animateClick(index)}
style={{ "accent-color": color }}
/>
);
})}
</div>
))}
</div>
);
}
let defaultImage = images.Mario;
function App() {
const [image, setImage] = useState("Mario");
const [imageData, setImageData] = useState();
useEffect(() => {
setImageData(null);
makeImg(images[image], function (img) {
const data = parseImg(img);
setImageData(data);
});
}, [image]);
return (
<div>
<div className="imageSelector">
{Object.entries(images).map(([key, value]) => {
return (
<img
src={value}
key={key}
data-selected={image === key}
onClick={() => setImage(key)}
/>
);
})}
</div>
{imageData && <CheckboxImage key={image} image={imageData} />}
</div>
);
}
const elApp = document.createElement("div");
document.body.appendChild(elApp);
ReactDOM.render(<App />, elApp);
function makeImg(src, callback) {
const img = new Image();
img.onload = () => callback(img);
img.src = src.target ? src.target.result : src;
return img;
}
async function loadImg(src) {
const img = new Image();
// img.crossOrigin = "Anonymous";
const imgLoaded = new Promise((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
});
img.src = src.target ? src.target.result : src;
await imgLoaded;
return img;
}
function parseImg(img) {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
console.log({ img });
let biggest = Math.max(img.width, img.height);
let ratio = biggest > 32 ? 32 / biggest : 1;
canvas.width = img.width * ratio;
canvas.height = img.height * ratio;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
return ctx.getImageData(0, 0, canvas.width, canvas.height);
}
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.