Skip to content

Instantly share code, notes, and snippets.

@wisniewski94
Created November 10, 2022 21:02
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wisniewski94/d28c1e8905c44b8b131c3bc472891e55 to your computer and use it in GitHub Desktop.
Save wisniewski94/d28c1e8905c44b8b131c3bc472891e55 to your computer and use it in GitHub Desktop.
Declarative Shadow DOM Experiment
const range = (min, max) => (min - max) * Math.random() + max;
class Particle {
constructor(x, y, canvas) {
this.setPhysics();
this.setDimensions();
this.setGraphics();
this.x = x;
this.y = y;
this.canvas = canvas;
}
applyForces() {
this.velocity.x -= this.velocity.x * (this.DRAG * this.velocity.y * 0.2);
this.velocity.x += (Math.random() > 0.5 ? Math.random() : -Math.random()) / 4;
this.velocity.y = Math.min(this.velocity.y + this.GRAVITY, this.TERMINAL_VELOCITY);
}
applyFade() {
this.opacity -= 0.01;
context.fillStyle = `rgba(${this.color.join(',')}, ${this.opacity})`;
}
setPhysics() {
this.COLORS = [[85, 71, 106], [174, 61, 99], [219, 56, 83], [244, 92, 68], [248, 182, 70]];
this.DRAG = range(0.01, 0.05);
this.GRAVITY = 0.3;
this.TERMINAL_VELOCITY = range(1, 3);
this.velocity = {
x: 0,
y: -5,
};
}
setDimensions() {
this.dimensions = {
w: range(7, 10),
h: range(7, 10),
};
this.scale = {
x: 1,
y: 1,
};
this.rotation = Math.random() * Math.PI;
}
setPosition() {
this.x += this.velocity.x;
this.y += this.velocity.y;
}
setGraphics() {
this.color = this.COLORS[~~range(1, 5)];
this.opacity = 1;
}
loop() {
if (this.x > this.canvas.width) {
this.x = 0;
} else if (this.x < 0) {
this.x = this.canvas.width;
}
}
spin() {
this.scale.y = Math.cos(this.y * 0.1);
}
draw(context) {
const width = this.dimensions.w * this.scale.x;
const height = this.dimensions.h * this.scale.y;
context.translate(this.x, this.y);
context.rotate(this.rotation);
this.applyForces();
this.setPosition();
this.loop();
this.spin();
this.opacity -= 0.01;
context.fillStyle = `rgba(${this.color.join(',')}, ${this.opacity})`;
context.fillRect(width / 2, height / 2, width, height);
context.setTransform(1, 0, 0, 1, 0, 0);
}
}
class ConfettiButton extends HTMLElement {
constructor() {
super();
this.MIN_COUNT = 10
this.MAX_COUNT = this.MIN_COUNT + range(-10, 5);
this.attachListeners();
this.isRunning = false;
this.initializeParticles();
this.prepareAnimation();
this.startX = 0;
this.startY = 0;
this.isDisabled = this.hasAttribute('disabled');
}
connectedCallback() {
this.canvas = this.shadowRoot.querySelector('canvas');
this.context = this.canvas.getContext('2d');
this.canvas.height = 500;
}
attachListeners() {
this.addEventListener('click', (e) => {
const rect = e.target.getBoundingClientRect();
const buttonCenter = [
rect.width / 2,
rect.height / 2
]
const canvasCenter = [
this.canvas.width / 2,
this.canvas.height / 2
]
this.startX = (e.clientX - rect.left) - buttonCenter[0] + canvasCenter[0]; //x position within the element.
this.startY = (e.clientY - rect.top) - buttonCenter[1] + canvasCenter[1]; //y position within the element.
if (e.clientX - rect.left < 0) { // if is a click other than mouseclick
this.startX = canvasCenter[0];
this.startY = canvasCenter[1] - buttonCenter[1];
}
if (!this.isRunning && !this.isDisabled) {
this.dropConfetti();
this.isRunning = true;
} else {
this.MAX_COUNT += this.MIN_COUNT + range(-9, 5);
}
})
}
initializeParticles() {
this.particles = [];
}
prepareAnimation() {
if (this.rafId) {
cancelAnimationFrame(this.rafId);
}
this.count = 0;
this.rafId = null;
}
draw(self) {
self.context.clearRect(0, 0, self.canvas.width, self.canvas.height);
if (self.count < self.MAX_COUNT) {
self.particles.push(new Particle(self.startX, self.startY, self.canvas));
self.count += 1;
}
self.particles.forEach((particle) => {
particle.draw(self.context);
if (particle.y > self.canvas.height) {
self.particles.splice(self.particles.indexOf(particle), 1);
if (self.particles.length === 0) {
cancelAnimationFrame(self.rafId);
self.isRunning = false;
}
}
});
if (self.isRunning) {
self.rafId = requestAnimationFrame(() => self.draw(self));
}
}
dropConfetti() {
this.rafId = requestAnimationFrame(() => {
this.draw(this);
});
}
}
if(customElements.get('confetti-button') === undefined) customElements.define('confetti-button', ConfettiButton)
<confetti-button>
<template shadowroot="open">
<style>
button {
box-shadow: 0 3px 0 0 #d9a65b, 0 1px 1px rgb(139 93 19 / 15%), 0 2px 2px rgb(139 93 19 / 15%), 0 4px 4px rgb(139 93 19 / 11%), 0 6px 8px rgb(139 93 19 / 11%), 0 8px 16px rgb(139 93 19 / 11%);
background-color: #f5c129;
background-image: linear-gradient(-45deg, #ffc46e, #f5c129);
color: #694e26;
margin: 5px;
display: inline-block;
padding: 8px 30px;
font-weight: 600;
border-style: solid;
border-radius: 3px;
cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
user-select: none;
vertical-align: bottom;
transition: box-shadow 0.2s, transform 0.2s, color 0.2s;
border: 2px solid transparent;
}
:host(:not([disabled])) button:hover {
background-image: linear-gradient(-45deg, #fdcd75, #f5c536);
color: #6f552e;
box-shadow: 0 3px 0 0 #ddaa60, 0 1px 1px rgb(139 93 19 / 15%), 0 2px 2px rgb(139 93 19 / 15%), 0 4px 4px rgb(139 93 19 / 11%), 0 6px 8px rgb(139 93 19 / 11%), 0 8px 16px rgb(139 93 19 / 11%);
}
:host(:active) button:active, :host([disabled]) button {
color: #694e26;
transition: all 0.05s;
box-shadow: initial;
transform: translateY(2px);
}
:host([disabled]) button {
background-image: linear-gradient(-45deg, #e9e9e9, #e5e5e5);
color: #6f6f6f;
cursor: not-allowed;
}
canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
</style>
<button>
<slot></slot>
</button>
<canvas>
</canvas>
</template>
🚀 Click Me
</confetti-button>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment