Skip to main content
Glama
Confetti.vue17.2 kB
// adapted from https://codepen.io/iprodev/pen/azpWBr <template> <div v-if="active" ref="canvasParentRef" class="fixed inset-0 z-[1000] pointer-events-none" > <canvas id="confetti" ref="canvasRef" class="absolute w-full h-full left-0 top-0" /> </div> </template> <script setup lang="ts"> /* eslint-disable */ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"; const canvasRef = ref<HTMLCanvasElement>(); const canvasParentRef = ref<HTMLDivElement>(); // Math shorthands const PI = Math.PI; const sqrt = Math.sqrt; const round = Math.round; const random = Math.random; const cos = Math.cos; const sin = Math.sin; // Local WindowAnimationTiming interface let rAF = window.requestAnimationFrame; // @ts-ignore let cAF = window.cancelAnimationFrame || window.cancelRequestAnimationFrame; const _now = Date.now || (() => new Date().getTime()); // Local WindowAnimationTiming interface polyfill (function (w) { /** * Fallback implementation. */ let prev = _now(); function fallback(fn: any) { const curr = _now(); const ms = Math.max(0, 16 - (curr - prev)); const req = setTimeout(fn, ms); prev = curr; return req; } /** * Cancel. */ const cancel = // @ts-ignore w.cancelAnimationFrame || w.webkitCancelAnimationFrame || w.clearTimeout; // @ts-ignore rAF = w.requestAnimationFrame || w.webkitRequestAnimationFrame || fallback; cAF = function (id) { cancel.call(w, id); }; })(window); const confettiRibbonCount = 8; const ribbonPaperCount = 30; const ribbonPaperDist = 8.0; const ribbonPaperThick = 8.0; const confettiPaperCount = 150; const DEG_TO_RAD = PI / 180; const RAD_TO_DEG = 180 / PI; const colors = [ ["#df0049", "#660671"], ["#00e857", "#005291"], ["#2bebbc", "#05798a"], ["#ffd200", "#b06c00"], ]; class Vector2 { constructor(public x: number, public y: number) {} Length() { return sqrt(this.SqrLength()); }; SqrLength() { return this.x * this.x + this.y * this.y; }; Add(_vec: any) { this.x += _vec.x; this.y += _vec.y; }; Sub(_vec: any) { this.x -= _vec.x; this.y -= _vec.y; }; Div(_f: any) { this.x /= _f; this.y /= _f; }; Mul(_f: any) { this.x *= _f; this.y *= _f; }; Normalize() { const sqrLen = this.SqrLength(); if (sqrLen != 0) { const factor = 1.0 / sqrt(sqrLen); this.x *= factor; this.y *= factor; } }; Normalized() { const sqrLen = this.SqrLength(); if (sqrLen != 0) { const factor = 1.0 / sqrt(sqrLen); return new Vector2(this.x * factor, this.y * factor); } return new Vector2(0, 0); }; static Lerp(_vec0: Vector2, _vec1: Vector2, _t: number) { return new Vector2( (_vec1.x - _vec0.x) * _t + _vec0.x, (_vec1.y - _vec0.y) * _t + _vec0.y, ); }; static Distance(_vec0: Vector2, _vec1: Vector2) { return sqrt(Vector2.SqrDistance(_vec0, _vec1)); }; static SqrDistance(_vec0: Vector2, _vec1: Vector2) { const x = _vec0.x - _vec1.x; const y = _vec0.y - _vec1.y; return x * x + y * y/* + z * z*/; }; static Scale(_vec0: Vector2, _vec1: Vector2) { return new Vector2(_vec0.x * _vec1.x, _vec0.y * _vec1.y); }; static Min(_vec0: Vector2, _vec1: Vector2) { return new Vector2(Math.min(_vec0.x, _vec1.x), Math.min(_vec0.y, _vec1.y)); }; static Max(_vec0: Vector2, _vec1: Vector2) { return new Vector2(Math.max(_vec0.x, _vec1.x), Math.max(_vec0.y, _vec1.y)); }; static ClampMagnitude(_vec0: Vector2, _len: number) { const vecNorm = _vec0.Normalized(); return new Vector2(vecNorm.x * _len, vecNorm.y * _len); }; static Sub(_vec0: Vector2, _vec1: Vector2) { return new Vector2(_vec0.x - _vec1.x, _vec0.y - _vec1.y/*, _vec0.z - _vec1.z*/); }; } class EulerMass { public position: Vector2; public force: Vector2; public velocity: Vector2; constructor(_x: number, _y: number, public mass: number, public drag: number) { this.position = new Vector2(_x, _y); this.force = new Vector2(0, 0); this.velocity = new Vector2(0, 0); } AddForce(_f:Vector2) { this.force.Add(_f); }; Integrate(_dt:number) { const acc = this.CurrentForce(this.position); acc.Div(this.mass); const posDelta = new Vector2(this.velocity.x, this.velocity.y); posDelta.Mul(_dt); this.position.Add(posDelta); acc.Mul(_dt); this.velocity.Add(acc); this.force = new Vector2(0, 0); }; CurrentForce(_pos:Vector2/*, _vel:Vector2*/) { const totalForce = new Vector2(this.force.x, this.force.y); const speed = this.velocity.Length(); const dragVel = new Vector2(this.velocity.x, this.velocity.y); dragVel.Mul(this.drag * this.mass * speed); totalForce.Sub(dragVel); return totalForce; }; } class ConfettiPaper { public pos: Vector2; public rotationSpeed: number; public angle: number; public rotation: number; public cosA: number; public size: number; public oscillationSpeed: number; public xSpeed: number; public ySpeed: number; public corners: Vector2[]; public time: number; public frontColor: string; public backColor: string; public finished = false; constructor(_x:number, _y:number) { this.pos = new Vector2(_x, _y); this.rotationSpeed = random() * 600 + 800; this.angle = DEG_TO_RAD * random() * 360; this.rotation = DEG_TO_RAD * random() * 360; this.cosA = 1.0; this.size = 5.0; this.oscillationSpeed = random() * 1.5 + 0.5; this.xSpeed = 40.0; this.ySpeed = random() * 50 + canvasElHeight.value / 4; this.corners = []; this.time = random(); const ci = round(random() * (colors.length - 1)); this.frontColor = colors[ci][0]; this.backColor = colors[ci][1]; for (let i = 0; i < 4; i++) { const dx = cos(this.angle + DEG_TO_RAD * (i * 90 + 45)); const dy = sin(this.angle + DEG_TO_RAD * (i * 90 + 45)); this.corners[i] = new Vector2(dx, dy); } } Update(_dt:number) { if (this.pos.y > canvasElHeight.value) { this.finished = true; if (!props.noLoop) this.Reset(); return; } this.time += _dt; this.rotation += this.rotationSpeed * _dt; this.cosA = cos(DEG_TO_RAD * this.rotation); this.pos.x += cos(this.time * this.oscillationSpeed) * this.xSpeed * _dt; this.pos.y += this.ySpeed * _dt; } Reset() { this.finished = false; this.pos.x = random() * canvasElWidth.value; this.pos.y = 0; } Draw(_g: CanvasRenderingContext2D) { if (this.cosA > 0) { _g.fillStyle = this.frontColor; } else { _g.fillStyle = this.backColor; } _g.beginPath(); _g.moveTo( (this.pos.x + this.corners[0].x * this.size) * retinaRatio.value, (this.pos.y + this.corners[0].y * this.size * this.cosA) * retinaRatio.value, ); for (let i = 1; i < 4; i++) { _g.lineTo( (this.pos.x + this.corners[i].x * this.size) * retinaRatio.value, (this.pos.y + this.corners[i].y * this.size * this.cosA) * retinaRatio.value, ); } _g.closePath(); _g.fill(); }; } class ConfettiRibbon { public particles: EulerMass[]; public frontColor: string; public backColor: string; public xOff: number; public yOff: number; public position: Vector2; public prevPosition: Vector2; public velocityInherit: number; public time: number; public oscillationSpeed: number; public oscillationDistance: number; public ySpeed: number; public finished = false; constructor( _x: number, _y: number, public particleCount: number, public particleDist: number, _thickness: number, _angle: number, public particleMass: number, public particleDrag: number, ) { this.particles = []; const ci = round(random() * (colors.length - 1)); this.frontColor = colors[ci][0]; this.backColor = colors[ci][1]; this.xOff = cos(DEG_TO_RAD * _angle) * _thickness; this.yOff = sin(DEG_TO_RAD * _angle) * _thickness; this.position = new Vector2(_x, _y); this.prevPosition = new Vector2(_x, _y); this.velocityInherit = random() * 2 + 4; this.time = random() * 100; this.oscillationSpeed = random() * 2 + 8; this.oscillationDistance = random() * 40 + 50; this.ySpeed = random() * 40 + canvasElHeight.value / 4; for (let i = 0; i < this.particleCount; i++) { this.particles[i] = new EulerMass( _x, _y - i * this.particleDist, this.particleMass, this.particleDrag, ); } } Update(_dt: number) { if ( this.position.y > canvasElHeight.value + this.particleDist * this.particleCount ) { this.finished = true; if (!props.noLoop) this.Reset(); return; } let i = 0; this.time += _dt * this.oscillationSpeed; this.position.y += this.ySpeed * _dt; this.position.x += cos(this.time) * this.oscillationDistance * _dt; this.particles[0].position = this.position; const dX = this.prevPosition.x - this.position.x; const dY = this.prevPosition.y - this.position.y; const delta = sqrt(dX * dX + dY * dY); this.prevPosition = new Vector2(this.position.x, this.position.y); for (i = 1; i < this.particleCount; i++) { const dirP = Vector2.Sub( this.particles[i - 1].position, this.particles[i].position, ); dirP.Normalize(); dirP.Mul((delta / _dt) * this.velocityInherit); this.particles[i].AddForce(dirP); } for (i = 1; i < this.particleCount; i++) { this.particles[i].Integrate(_dt); } for (i = 1; i < this.particleCount; i++) { const rp2 = new Vector2( this.particles[i].position.x, this.particles[i].position.y, ); rp2.Sub(this.particles[i - 1].position); rp2.Normalize(); rp2.Mul(this.particleDist); rp2.Add(this.particles[i - 1].position); this.particles[i].position = rp2; } }; Reset() { this.finished = false; this.position.y = -random() * canvasElHeight.value; this.position.x = random() * canvasElWidth.value; this.prevPosition = new Vector2(this.position.x, this.position.y); this.velocityInherit = random() * 2 + 4; this.time = random() * 100; this.oscillationSpeed = random() * 2.0 + 1.5; this.oscillationDistance = random() * 40 + 40; this.ySpeed = random() * 40 + 400; const ci = round(random() * (colors.length - 1)); this.frontColor = colors[ci][0]; this.backColor = colors[ci][1]; this.particles = []; for (let i = 0; i < this.particleCount; i++) { this.particles[i] = new EulerMass( this.position.x, this.position.y - i * this.particleDist, this.particleMass, this.particleDrag, ); } }; Draw(_g: CanvasRenderingContext2D) { const retina = retinaRatio.value; for (let i = 0; i < this.particleCount - 1; i++) { const p0 = new Vector2( this.particles[i].position.x + this.xOff, this.particles[i].position.y + this.yOff, ); const p1 = new Vector2( this.particles[i + 1].position.x + this.xOff, this.particles[i + 1].position.y + this.yOff, ); if ( this.Side( this.particles[i].position.x, this.particles[i].position.y, this.particles[i + 1].position.x, this.particles[i + 1].position.y, p1.x, p1.y, ) < 0 ) { _g.fillStyle = this.frontColor; _g.strokeStyle = this.frontColor; } else { _g.fillStyle = this.backColor; _g.strokeStyle = this.backColor; } if (i == 0) { _g.beginPath(); _g.moveTo( this.particles[i].position.x * retina, this.particles[i].position.y * retina, ); _g.lineTo( this.particles[i + 1].position.x * retina, this.particles[i + 1].position.y * retina, ); _g.lineTo( (this.particles[i + 1].position.x + p1.x) * 0.5 * retina, (this.particles[i + 1].position.y + p1.y) * 0.5 * retina, ); _g.closePath(); _g.stroke(); _g.fill(); _g.beginPath(); _g.moveTo(p1.x * retina, p1.y * retina); _g.lineTo(p0.x * retina, p0.y * retina); _g.lineTo( (this.particles[i + 1].position.x + p1.x) * 0.5 * retina, (this.particles[i + 1].position.y + p1.y) * 0.5 * retina, ); _g.closePath(); _g.stroke(); _g.fill(); } else if (i == this.particleCount - 2) { _g.beginPath(); _g.moveTo( this.particles[i].position.x * retina, this.particles[i].position.y * retina, ); _g.lineTo( this.particles[i + 1].position.x * retina, this.particles[i + 1].position.y * retina, ); _g.lineTo( (this.particles[i].position.x + p0.x) * 0.5 * retina, (this.particles[i].position.y + p0.y) * 0.5 * retina, ); _g.closePath(); _g.stroke(); _g.fill(); _g.beginPath(); _g.moveTo(p1.x * retina, p1.y * retina); _g.lineTo(p0.x * retina, p0.y * retina); _g.lineTo( (this.particles[i].position.x + p0.x) * 0.5 * retina, (this.particles[i].position.y + p0.y) * 0.5 * retina, ); _g.closePath(); _g.stroke(); _g.fill(); } else { _g.beginPath(); _g.moveTo( this.particles[i].position.x * retina, this.particles[i].position.y * retina, ); _g.lineTo( this.particles[i + 1].position.x * retina, this.particles[i + 1].position.y * retina, ); _g.lineTo(p1.x * retina, p1.y * retina); _g.lineTo(p0.x * retina, p0.y * retina); _g.closePath(); _g.stroke(); _g.fill(); } } }; Side(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) { return (x1 - x2) * (y3 - y2) - (y1 - y2) * (x3 - x2); }; } // NOTE - some awkward stuff here adapting from pure js code to TS/vue const retinaRatio = ref(window.devicePixelRatio); const canvasElWidth = ref(1); const canvasElHeight = ref(1); const canvasWidth = computed(() => canvasElWidth.value * retinaRatio.value); const canvasHeight = computed(() => canvasElHeight.value * retinaRatio.value); let canvasContext: CanvasRenderingContext2D; let confettiRibbons: ConfettiRibbon[] = []; let confettiPapers: ConfettiPaper[] = []; function windowResizeHandler() { if (!canvasParentRef.value || !canvasRef.value) return; retinaRatio.value = window.devicePixelRatio; canvasElWidth.value = canvasParentRef.value.offsetWidth; canvasElHeight.value = canvasParentRef.value.offsetHeight; // set width/height on actual canvas el canvasRef.value.width = canvasWidth.value canvasRef.value.height = canvasHeight.value } let interval: number | null = null; let running = false; function reset() { if (!canvasRef.value) return; windowResizeHandler(); canvasContext = canvasRef.value.getContext("2d")!; let i = 0; for (i = 0; i < confettiRibbonCount; i++) { confettiRibbons[i] = new ConfettiRibbon( random() * canvasElWidth.value, -random() * canvasElHeight.value * .4, ribbonPaperCount, ribbonPaperDist, ribbonPaperThick, 45, 1, 0.05, ); } for (i = 0; i < confettiPaperCount; i++) { confettiPapers[i] = new ConfettiPaper( random() * canvasElWidth.value, props.startTop ? -random() * canvasElHeight.value / 2 : random() * canvasElHeight.value, ); } play(); } function play() { update(); } function pause() { cAF(interval!); } function stop() { cAF(interval!); running = false; } // const speed = computed(() => canvasHeight.value / 300); // const duration = computed(() => 1 / speed.value); let duration = 10/1000; let lastUpdateAt: Date; function update() { const now = new Date(); if (lastUpdateAt) { duration = (now.getTime() - lastUpdateAt.getTime())/1000; } lastUpdateAt = now canvasContext.clearRect(0, 0, canvasWidth.value, canvasHeight.value); let i; for (i = 0; i < confettiPaperCount; i++) { confettiPapers[i].Update(duration); confettiPapers[i].Draw(canvasContext); } for (i = 0; i < confettiRibbonCount; i++) { confettiRibbons[i].Update(duration); confettiRibbons[i].Draw(canvasContext); } if ( props.noLoop && confettiPapers.every((c) => c.finished) && confettiRibbons.every((c) => c.finished) ) { // not looping and all finished... // might want to do some cleanup? } else { interval = rAF(update); } } const props = defineProps({ active: Boolean, startTop: Boolean, noLoop: Boolean, }); watch( () => props.active, () => { if (props.active) { // use nextTick because the div/canvas are not in the dom yet nextTick(reset); } else { stop(); } }, ); onMounted(() => { windowResizeHandler(); window.addEventListener("resize", windowResizeHandler); if (props.active) reset(); }); onBeforeUnmount(() => { window.removeEventListener("resize", windowResizeHandler); }) </script>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server