vue-confetti-explosion
Version:
Confetti explosion in Vue 3 🎉🎊
303 lines (238 loc) • 11.8 kB
JavaScript
import { ref, computed, watchEffect, onMounted, openBlock, createElementBlock, normalizeStyle, Fragment, renderList, createElementVNode, createCommentVNode } from 'vue';
const ROTATION_SPEED_MIN = 200; // minimum possible duration of single particle full rotation
const ROTATION_SPEED_MAX = 800; // maximum possible duration of single particle full rotation
const CRAZY_PARTICLES_FREQUENCY = 0.1; // 0-1 frequency of crazy curvy unpredictable particles
const CRAZY_PARTICLE_CRAZINESS = 0.3; // 0-1 how crazy these crazy particles are
const BEZIER_MEDIAN = 0.5; // utility for mid-point bezier curves, to ensure smooth motion paths
const FORCE = 0.5; // 0-1 roughly the vertical force at which particles initially explode
const SIZE = 12; // max height for particle rectangles, diameter for particle circles
const FLOOR_HEIGHT = 800; // pixels the particles will fall from initial explosion point
const FLOOR_WIDTH = 1600; // horizontal spread of particles in pixels
const PARTICLE_COUNT = 150;
const DURATION = 3500;
const COLORS = ["#FFC700", "#FF0000", "#2E3191", "#41BBC7"];
var script = {
props: {
particleCount: {
type: Number,
default: PARTICLE_COUNT
},
particleSize: {
type: Number,
default: SIZE
},
duration: {
type: Number,
default: DURATION
},
colors: {
type: Array,
default: COLORS
},
force: {
type: Number,
default: FORCE
},
stageHeight: {
type: Number,
default: FLOOR_HEIGHT
},
stageWidth: {
type: Number,
default: FLOOR_WIDTH
},
shouldDestroyAfterDone: {
type: Boolean,
default: true
}
},
setup(props) {
const isVisible = ref(true);
const setItemRef = (el, degree) => {
confettiStyles(el, {
degree
});
};
const particles = computed(() => createParticles(props.particleCount, props.colors));
watchEffect(() => {
props.particleCount > 300 && console.log("[VUE-CONFETTI-EXPLOSION] That's a lot of confetti, you sure about that? A lesser number" + " like 200 will still give off the party vibes while still not bricking the device 😉");
});
const isValid = computed(() => validate(props.particleCount, props.duration, props.colors, props.particleSize, props.force, props.stageHeight, props.stageWidth));
onMounted(async () => {
await waitFor(props.duration);
if (props.shouldDestroyAfterDone) {
isVisible.value = false;
}
});
const createParticles = (count, colors) => {
const increment = 360 / count;
return Array.from({
length: count
}, (_, i) => ({
color: colors[i % colors.length],
degree: i * increment
}));
};
const waitFor = ms => new Promise(resolve => setTimeout(resolve, ms)); // From here: https://stackoverflow.com/a/11832950
function round(num) {
let precision = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
return Math.round((num + Number.EPSILON) * 10 ** precision) / 10 ** precision;
}
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
return true;
}
const mapRange = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
const rotate = (degree, amount) => {
const result = degree + amount;
return result > 360 ? result - 360 : result;
};
const coinFlip = () => Math.random() > 0.5; // avoid this for circles, as it will have no visual effect
const zAxisRotation = [0, 0, 1];
const rotationTransforms = [// dual axis rotations (a bit more realistic)
[1, 1, 0], [1, 0, 1], [0, 1, 1], // single axis rotations (a bit dumber)
[1, 0, 0], [0, 1, 0], zAxisRotation];
const shouldBeCircle = rotationIndex => !arraysEqual(rotationTransforms[rotationIndex], zAxisRotation) && coinFlip();
const isUndefined = value => typeof value === "undefined";
const error = message => {
console.error(message);
};
function validate(particleCount, duration, colors, particleSize, force, floorHeight, floorWidth) {
const isSafeInteger = Number.isSafeInteger;
if (!isUndefined(particleCount) && isSafeInteger(particleCount) && particleCount < 0) {
error("particleCount must be a positive integer");
return false;
}
if (!isUndefined(duration) && isSafeInteger(duration) && duration < 0) {
error("duration must be a positive integer");
return false;
}
if (!isUndefined(colors) && !Array.isArray(colors)) {
error("colors must be an array of strings");
return false;
}
if (!isUndefined(particleSize) && isSafeInteger(particleSize) && particleSize < 0) {
error("particleSize must be a positive integer");
return false;
}
if (!isUndefined(force) && isSafeInteger(force) && (force < 0 || force > 1)) {
error("force must be a positive integer and should be within 0 and 1");
return false;
}
if (!isUndefined(floorHeight) && typeof floorHeight === "number" && isSafeInteger(floorHeight) && floorHeight < 0) {
error("floorHeight must be a positive integer");
return false;
}
if (!isUndefined(floorWidth) && typeof floorWidth === "number" && isSafeInteger(floorWidth) && floorWidth < 0) {
error("floorWidth must be a positive integer");
return false;
}
return true;
}
function confettiStyles(node, _ref) {
let {
degree
} = _ref;
// Get x landing point for it
const landingPoint = mapRange(Math.abs(rotate(degree, 90) - 180), 0, 180, -props.stageWidth / 2, props.stageWidth / 2); // Crazy calculations for generating styles
const rotation = Math.random() * (ROTATION_SPEED_MAX - ROTATION_SPEED_MIN) + ROTATION_SPEED_MIN;
const rotationIndex = Math.round(Math.random() * (rotationTransforms.length - 1));
const durationChaos = props.duration - Math.round(Math.random() * 1000);
const shouldBeCrazy = Math.random() < CRAZY_PARTICLES_FREQUENCY;
const isCircle = shouldBeCircle(rotationIndex); // x-axis disturbance, roughly the distance the particle will initially deviate from its target
const x1 = shouldBeCrazy ? round(Math.random() * CRAZY_PARTICLE_CRAZINESS, 2) : 0;
const x2 = x1 * -1;
const x3 = x1; // x-axis arc of explosion, so 90deg and 270deg particles have curve of 1, 0deg and 180deg have 0
const x4 = round(Math.abs(mapRange(Math.abs(rotate(degree, 90) - 180), 0, 180, -1, 1)), 4); // roughly how fast particle reaches end of its explosion curve
const y1 = round(Math.random() * BEZIER_MEDIAN, 4); // roughly maps to the distance particle goes before reaching free-fall
const y2 = round(Math.random() * props.force * (coinFlip() ? 1 : -1), 4); // roughly how soon the particle transitions from explosion to free-fall
const y3 = BEZIER_MEDIAN; // roughly the ease of free-fall
const y4 = round(Math.max(mapRange(Math.abs(degree - 180), 0, 180, props.force, -props.force), 0), 4);
const setCSSVar = (key, val) => node === null || node === void 0 ? void 0 : node.style.setProperty(key, val + "");
setCSSVar("--x-landing-point", `${landingPoint}px`);
setCSSVar("--duration-chaos", `${durationChaos}ms`);
setCSSVar("--x1", `${x1}`);
setCSSVar("--x2", `${x2}`);
setCSSVar("--x3", `${x3}`);
setCSSVar("--x4", `${x4}`);
setCSSVar("--y1", `${y1}`);
setCSSVar("--y2", `${y2}`);
setCSSVar("--y3", `${y3}`);
setCSSVar("--y4", `${y4}`); // set --width and --height here
setCSSVar("--width", `${isCircle ? props.particleSize : Math.round(Math.random() * 4) + props.particleSize / 2}px`);
setCSSVar("--height", (isCircle ? props.particleSize : Math.round(Math.random() * 2) + props.particleSize) + "px");
setCSSVar("--rotation", `${rotationTransforms[rotationIndex].join()}`);
setCSSVar("--rotation-duration", `${rotation}ms`);
setCSSVar("--border-radius", `${isCircle ? "50%" : "0"}`);
}
return {
isVisible,
isValid,
stageHeight: props.stageHeight,
particles,
setItemRef
};
}
};
function render(_ctx, _cache, $props, $setup, $data, $options) {
return $setup.isVisible && $setup.isValid ? (openBlock(), createElementBlock("div", {
key: 0,
class: "confetti-container",
style: normalizeStyle(`--floor-height: ${$setup.stageHeight}px;`)
}, [(openBlock(true), createElementBlock(Fragment, null, renderList($setup.particles, _ref => {
let {
color,
degree
} = _ref;
return openBlock(), createElementBlock("div", {
key: degree,
class: "particle",
ref: el => $setup.setItemRef(el, degree)
}, [createElementVNode("div", {
style: normalizeStyle(`--bgcolor: ${color};`)
}, null, 4)], 512);
}), 128))], 4)) : createCommentVNode("", true);
}
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = "\n@keyframes y-axis-4ff796ae {\nto {\n transform: translate3d(0, var(--floor-height), 0);\n}\n}\n@keyframes x-axis-4ff796ae {\nto {\n transform: translate3d(var(--x-landing-point), 0, 0);\n}\n}\n@keyframes rotation-4ff796ae {\nto {\n transform: rotate3d(var(--rotation), 360deg);\n}\n}\n.confetti-container[data-v-4ff796ae] {\n width: 0;\n height: 0;\n overflow: visible;\n position: relative;\n transform: translate3d(var(--x, 0), var(--y, 0), 0);\n z-index: 1200;\n}\n.confetti-container > .particle[data-v-4ff796ae] {\n animation: x-axis-4ff796ae var(--duration-chaos) forwards cubic-bezier(var(--x1), var(--x2), var(--x3), var(--x4));\n}\n.confetti-container > .particle div[data-v-4ff796ae] {\n position: absolute;\n top: 0;\n left: 0;\n animation: y-axis-4ff796ae var(--duration-chaos) forwards cubic-bezier(var(--y1), var(--y2), var(--y3), var(--y4));\n width: var(--width);\n height: var(--height);\n}\n.confetti-container > .particle div[data-v-4ff796ae]:before {\n display: block;\n height: 100%;\n width: 100%;\n content: \"\";\n background-color: var(--bgcolor);\n animation: rotation-4ff796ae var(--rotation-duration) infinite linear;\n border-radius: var(--border-radius);\n}\n";
styleInject(css_248z);
script.render = render;
script.__scopeId = "data-v-4ff796ae";
// Import vue component
// IIFE injects install function into component, allowing component
// to be registered via Vue.use() as well as Vue.component(),
var entry_esm = /*#__PURE__*/(() => {
// Assign InstallableComponent type
const installable = script; // Attach install function executed by Vue.use()
installable.install = app => {
app.component("ConfettiExplosion", installable);
};
return installable;
})(); // It's possible to expose named exports when writing components that can
// also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo';
// export const RollupDemoDirective = directive;
export { entry_esm as default };