Skip to content

Instantly share code, notes, and snippets.

@djsnipa1
Created December 24, 2024 05:28
Show Gist options
  • Save djsnipa1/f7d61a98071f1b30c559aebdf17e9367 to your computer and use it in GitHub Desktop.
Save djsnipa1/f7d61a98071f1b30c559aebdf17e9367 to your computer and use it in GitHub Desktop.
<pixel-canvas> Web Component
<main>
<div class="card">
<pixel-canvas></pixel-canvas>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentcolor" viewBox="0 0 256 256">
<path d="M216,42H40A14,14,0,0,0,26,56V200a14,14,0,0,0,14,14H216a14,14,0,0,0,14-14V56A14,14,0,0,0,216,42ZM40,54H216a2,2,0,0,1,2,2V98H38V56A2,2,0,0,1,40,54ZM38,200V110H98v92H40A2,2,0,0,1,38,200Zm178,2H110V110H218v90A2,2,0,0,1,216,202Z"></path>
</svg>
<button>Layout</buton>
</div>
<div class="card" style="--active-color: #e0f2fe">
<pixel-canvas data-gap="10" data-speed="25" data-colors="#e0f2fe, #7dd3fc, #0ea5e9"></pixel-canvas>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentcolor" viewBox="0 0 256 256">
<path d="M67.84,92.61,25.37,128l42.47,35.39a6,6,0,1,1-7.68,9.22l-48-40a6,6,0,0,1,0-9.22l48-40a6,6,0,0,1,7.68,9.22Zm176,30.78-48-40a6,6,0,1,0-7.68,9.22L230.63,128l-42.47,35.39a6,6,0,1,0,7.68,9.22l48-40a6,6,0,0,0,0-9.22Zm-81.79-89A6,6,0,0,0,154.36,38l-64,176A6,6,0,0,0,94,221.64a6.15,6.15,0,0,0,2,.36,6,6,0,0,0,5.64-3.95l64-176A6,6,0,0,0,162.05,34.36Z"></path>
</svg>
<button>Code</buton>
</div>
<div class="card" style="--active-color: #fef08a">
<pixel-canvas data-gap="3" data-speed="20" data-colors="#fef08a, #fde047, #eab308"></pixel-canvas>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentcolor" viewBox="0 0 256 256">
<path d="M180,146H158V110h22a34,34,0,1,0-34-34V98H110V76a34,34,0,1,0-34,34H98v36H76a34,34,0,1,0,34,34V158h36v22a34,34,0,1,0,34-34ZM158,76a22,22,0,1,1,22,22H158ZM54,76a22,22,0,0,1,44,0V98H76A22,22,0,0,1,54,76ZM98,180a22,22,0,1,1-22-22H98Zm12-70h36v36H110Zm70,92a22,22,0,0,1-22-22V158h22a22,22,0,0,1,0,44Z"></path>
</svg>
<button>Command</buton>
</div>
<div class="card" style="--active-color: #fecdd3">
<pixel-canvas data-gap="6" data-speed="80" data-colors="#fecdd3, #fda4af, #e11d48" data-no-focus></pixel-canvas>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentcolor" viewBox="0 0 256 256">
<path d="M222,67.34a33.81,33.81,0,0,0-10.64-24.25C198.12,30.56,176.68,31,163.54,44.18L142.82,65l-.63-.63a22,22,0,0,0-31.11,0l-9,9a14,14,0,0,0,0,19.81l3.47,3.47L53.14,149.1a37.81,37.81,0,0,0-9.84,36.73l-8.31,19a11.68,11.68,0,0,0,2.46,13A13.91,13.91,0,0,0,47.32,222,14.15,14.15,0,0,0,53,220.82L71,212.92a37.92,37.92,0,0,0,35.84-10.07l52.44-52.46,3.47,3.48a14,14,0,0,0,19.8,0l9-9a22.06,22.06,0,0,0,0-31.13l-.66-.65L212,91.85A33.76,33.76,0,0,0,222,67.34Zm-123.61,127a26,26,0,0,1-26,6.47,6,6,0,0,0-4.17.24l-20,8.75a2,2,0,0,1-2.09-.31l9.12-20.9a5.94,5.94,0,0,0,.19-4.31A25.91,25.91,0,0,1,56,166h70.78ZM138.78,154H65.24l48.83-48.84,36.76,36.78Zm64.77-70.59L178.17,108.9a6,6,0,0,0,0,8.47l4.88,4.89a10,10,0,0,1,0,14.15l-9,9a2,2,0,0,1-2.82,0l-60.69-60.7a2,2,0,0,1,0-2.83l9-9a10,10,0,0,1,14.14,0l4.89,4.89a6,6,0,0,0,4.24,1.75h0a6,6,0,0,0,4.25-1.77L172,52.66c8.57-8.58,22.51-9,31.07-.85a22,22,0,0,1,.44,31.57Z"></path>
</svg>
<button>Dropper</buton>
</div>
</main>
class Pixel {
constructor(canvas, context, x, y, color, speed, delay) {
this.width = canvas.width;
this.height = canvas.height;
this.ctx = context;
this.x = x;
this.y = y;
this.color = color;
this.speed = this.getRandomValue(0.1, 0.9) * speed;
this.size = 0;
this.sizeStep = Math.random() * 0.4;
this.minSize = 0.5;
this.maxSizeInteger = 2;
this.maxSize = this.getRandomValue(this.minSize, this.maxSizeInteger);
this.delay = delay;
this.counter = 0;
this.counterStep = Math.random() * 4 + (this.width + this.height) * 0.01;
this.isIdle = false;
this.isReverse = false;
this.isShimmer = false;
}
getRandomValue(min, max) {
return Math.random() * (max - min) + min;
}
draw() {
const centerOffset = this.maxSizeInteger * 0.5 - this.size * 0.5;
this.ctx.fillStyle = this.color;
this.ctx.fillRect(
this.x + centerOffset,
this.y + centerOffset,
this.size,
this.size
);
}
appear() {
this.isIdle = false;
if (this.counter <= this.delay) {
this.counter += this.counterStep;
return;
}
if (this.size >= this.maxSize) {
this.isShimmer = true;
}
if (this.isShimmer) {
this.shimmer();
} else {
this.size += this.sizeStep;
}
this.draw();
}
disappear() {
this.isShimmer = false;
this.counter = 0;
if (this.size <= 0) {
this.isIdle = true;
return;
} else {
this.size -= 0.1;
}
this.draw();
}
shimmer() {
if (this.size >= this.maxSize) {
this.isReverse = true;
} else if (this.size <= this.minSize) {
this.isReverse = false;
}
if (this.isReverse) {
this.size -= this.speed;
} else {
this.size += this.speed;
}
}
}
class PixelCanvas extends HTMLElement {
static register(tag = "pixel-canvas") {
if ("customElements" in window) {
customElements.define(tag, this);
}
}
static css = `
:host {
display: grid;
inline-size: 100%;
block-size: 100%;
overflow: hidden;
}
`;
get colors() {
return this.dataset.colors?.split(",") || ["#f8fafc", "#f1f5f9", "#cbd5e1"];
}
get gap() {
const value = this.dataset.gap || 5;
const min = 4;
const max = 50;
if (value <= min) {
return min;
} else if (value >= max) {
return max;
} else {
return parseInt(value);
}
}
get speed() {
const value = this.dataset.speed || 35;
const min = 0;
const max = 100;
const throttle = 0.001;
if (value <= min || this.reducedMotion) {
return min;
} else if (value >= max) {
return max * throttle;
} else {
return parseInt(value) * throttle;
}
}
get noFocus() {
return this.hasAttribute("data-no-focus");
}
connectedCallback() {
const canvas = document.createElement("canvas");
const sheet = new CSSStyleSheet();
this._parent = this.parentNode;
this.shadowroot = this.attachShadow({ mode: "open" });
sheet.replaceSync(PixelCanvas.css);
this.shadowroot.adoptedStyleSheets = [sheet];
this.shadowroot.append(canvas);
this.canvas = this.shadowroot.querySelector("canvas");
this.ctx = this.canvas.getContext("2d");
this.timeInterval = 1000 / 60;
this.timePrevious = performance.now();
this.reducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
this.init();
this.resizeObserver = new ResizeObserver(() => this.init());
this.resizeObserver.observe(this);
this._parent.addEventListener("mouseenter", this);
this._parent.addEventListener("mouseleave", this);
if (!this.noFocus) {
this._parent.addEventListener("focusin", this);
this._parent.addEventListener("focusout", this);
}
}
disconnectedCallback() {
this.resizeObserver.disconnect();
this._parent.removeEventListener("mouseenter", this);
this._parent.removeEventListener("mouseleave", this);
if (!this.noFocus) {
this._parent.removeEventListener("focusin", this);
this._parent.removeEventListener("focusout", this);
}
delete this._parent;
}
handleEvent(event) {
this[`on${event.type}`](event);
}
onmouseenter() {
this.handleAnimation("appear");
}
onmouseleave() {
this.handleAnimation("disappear");
}
onfocusin(e) {
if (e.currentTarget.contains(e.relatedTarget)) return;
this.handleAnimation("appear");
}
onfocusout(e) {
if (e.currentTarget.contains(e.relatedTarget)) return;
this.handleAnimation("disappear");
}
handleAnimation(name) {
cancelAnimationFrame(this.animation);
this.animation = this.animate(name);
}
init() {
const rect = this.getBoundingClientRect();
const width = Math.floor(rect.width);
const height = Math.floor(rect.height);
this.pixels = [];
this.canvas.width = width;
this.canvas.height = height;
this.canvas.style.width = `${width}px`;
this.canvas.style.height = `${height}px`;
this.createPixels();
}
getDistanceToCanvasCenter(x, y) {
const dx = x - this.canvas.width / 2;
const dy = y - this.canvas.height / 2;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance;
}
createPixels() {
for (let x = 0; x < this.canvas.width; x += this.gap) {
for (let y = 0; y < this.canvas.height; y += this.gap) {
const color = this.colors[
Math.floor(Math.random() * this.colors.length)
];
const delay = this.reducedMotion
? 0
: this.getDistanceToCanvasCenter(x, y);
this.pixels.push(
new Pixel(this.canvas, this.ctx, x, y, color, this.speed, delay)
);
}
}
}
animate(fnName) {
this.animation = requestAnimationFrame(() => this.animate(fnName));
const timeNow = performance.now();
const timePassed = timeNow - this.timePrevious;
if (timePassed < this.timeInterval) return;
this.timePrevious = timeNow - (timePassed % this.timeInterval);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < this.pixels.length; i++) {
this.pixels[i][fnName]();
}
if (this.pixels.every((pixel) => pixel.isIdle)) {
cancelAnimationFrame(this.animation);
}
}
}
PixelCanvas.register();
:root {
--space: 1rem;
--bg: #09090b;
--fg: #e3e3e3;
--surface-1: #101012;
--surface-2: #27272a;
--surface-3: #52525b;
--ease-out: cubic-bezier(0.5, 1, 0.89, 1);
--ease-in-out: cubic-bezier(0.45, 0, 0.55, 1);
}
* {
box-sizing: border-box;
}
height,
body {
height: 100%;
}
body {
display: grid;
color: var(--fg);
background: var(--bg);
padding: var(--space);
min-height: 100vh;
}
main {
display: grid;
grid-template-columns: repeat(var(--count, 1), 1fr);
gap: var(--space);
margin: auto;
inline-size: min(var(--max, 15rem), 100%);
@media (min-width: 25rem) {
--count: 2;
--max: 30rem;
}
@media (min-width: 45rem) {
--count: 4;
--max: 60rem;
}
}
.card {
position: relative;
overflow: hidden;
display: grid;
grid-template-areas: "card";
place-items: center;
aspect-ratio: 4/5;
border: 1px solid var(--surface-2);
isolation: isolate;
transition: border-color 200ms var(--ease-out);
user-select: none;
&::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(
circle at bottom left,
transparent 55%,
var(--surface-1)
);
pointer-events: none;
box-shadow: var(--bg) -0.5cqi 0.5cqi 2.5cqi inset;
transition: opacity 900ms var(--ease-out);
}
&::after {
content: "";
position: absolute;
inset: 0;
margin: auto;
aspect-ratio: 1;
background: radial-gradient(circle, var(--bg), transparent 65%);
opacity: 0;
transition: opacity 800ms var(--ease-out);
}
> * {
grid-area: card;
}
svg {
position: relative;
z-index: 1;
width: 30%;
height: auto;
color: var(--surface-3);
transition: 300ms var(--ease-out);
transition-property: color, scale;
}
button {
opacity: 0;
}
&:focus-within {
outline: 5px auto Highlight;
outline: 5px auto -webkit-focus-ring-color;
}
&:where(:hover, :focus-within) {
border-color: var(--active-color, var(--fg));
transition: border-color 800ms var(--ease-in-out);
}
&:where(:hover, :focus-within) svg {
color: var(--active-color, var(--fg));
scale: 1.1;
transition: 300ms var(--ease-in-out);
}
&:where(:hover, :focus-within)::before {
opacity: 0;
}
&:where(:hover, :focus-within)::after {
opacity: 1;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment