diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000..afc80de
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+confetti.js.org
diff --git a/css/main.css b/css/main.css
new file mode 100644
index 0000000..c653a5e
--- /dev/null
+++ b/css/main.css
@@ -0,0 +1,124 @@
+body {
+ min-height: 100vh;
+ background: #232323;
+ padding-top: 4.5rem;
+}
+
+main {
+ display: flex;
+ flex-wrap: nowrap;
+ height: 100%;
+ max-height: 100%;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.b-example-divider {
+ flex-shrink: 0;
+ width: 1.5rem;
+ height: 100vh;
+ background-color: rgba(0, 0, 0, 0.1);
+ border: solid rgba(0, 0, 0, 0.15);
+ border-width: 1px 0;
+ box-shadow: inset 0 0.5em 1.5em rgba(0, 0, 0, 0.1),
+ inset 0 0.125em 0.5em rgba(0, 0, 0, 0.15);
+}
+
+.bi {
+ vertical-align: -0.125em;
+ pointer-events: none;
+ fill: currentColor;
+}
+
+.dropdown-toggle {
+ outline: 0;
+}
+
+.nav-flush .nav-link {
+ border-radius: 0;
+}
+
+.btn-toggle {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.5rem;
+ font-weight: 600;
+ color: rgba(0, 0, 0, 0.65);
+ background-color: transparent;
+ border: 0;
+}
+.btn-toggle:hover,
+.btn-toggle:focus {
+ color: rgba(0, 0, 0, 0.85);
+ background-color: #d2f4ea;
+}
+
+.btn-toggle::before {
+ width: 1.25em;
+ line-height: 0;
+ content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
+ transition: transform 0.35s ease;
+ transform-origin: 0.5em 50%;
+}
+
+.btn-toggle[aria-expanded="true"] {
+ color: rgba(0, 0, 0, 0.85);
+}
+.btn-toggle[aria-expanded="true"]::before {
+ transform: rotate(90deg);
+}
+
+.btn-toggle-nav a {
+ display: inline-flex;
+ padding: 0.1875rem 0.5rem;
+ margin-top: 0.125rem;
+ margin-left: 1.25rem;
+ text-decoration: none;
+}
+.btn-toggle-nav a:hover,
+.btn-toggle-nav a:focus {
+ background-color: #d2f4ea;
+}
+
+.scrollarea {
+ overflow-y: auto;
+}
+
+.fw-semibold {
+ font-weight: 600;
+}
+.lh-tight {
+ line-height: 1.25;
+}
+
+#code {
+ left: 0;
+ position: fixed;
+ width: 100%;
+ height: calc(100% - 3.5rem);
+ top: 3.5rem;
+ margin: 0;
+ padding: 0;
+ z-index: 10;
+ overflow: auto;
+}
+
+#code pre {
+ height: 100%;
+}
+
+.hljs {
+ background: #282c34cc !important;
+ height: 100%;
+}
+
+#ads {
+ position: fixed;
+ top: 3.5rem;
+ right: 0;
+ max-width: 330px;
+ height: 100px;
+ z-index: 0;
+ background: #232323;
+ border-bottom: 1px solid #343a40;
+}
\ No newline at end of file
diff --git a/css/more.css b/css/more.css
new file mode 100644
index 0000000..2dc4562
--- /dev/null
+++ b/css/more.css
@@ -0,0 +1,296 @@
+:root {
+ --primary-color: #eeeeee;
+ --secondary-color: #363636;
+ --secondary-variant-color: #272727;
+ --background-color: #212121;
+ --inner-color: #ffffff;
+ --border-color: #555651;
+
+ /* icons by Google - Material Design
+ * https://material.io/resources/icons/?style=baseline
+ */
+ --switch-moon-white: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath fill='%23eeeeee' d='M20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-.89 0-1.74-.2-2.5-.55C11.56 16.5 13 14.42 13 12s-1.44-4.5-3.5-5.45C10.26 6.2 11.11 6 12 6c3.31 0 6 2.69 6 6s-2.69 6-6 6z'%3E%3C/path%3E%3C/svg%3E");
+ --switch-sun-black: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath fill='%23212121' d='M20 15.31L23.31 12 20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6z'%3E%3C/path%3E%3C/svg%3E");
+ --switch-auto-white: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath fill='%23eeeeee' d='M10.85 12.65h2.3L12 9l-1.15 3.65zM20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM14.3 16l-.7-2h-3.2l-.7 2H7.8L11 7h2l3.2 9h-1.9z'%3E%3C/path%3E%3C/svg%3E");
+ --switch-auto-black: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath fill='%23212121' d='M10.85 12.65h2.3L12 9l-1.15 3.65zM20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM14.3 16l-.7-2h-3.2l-.7 2H7.8L11 7h2l3.2 9h-1.9z'%3E%3C/path%3E%3C/svg%3E");
+
+ --theme-switch: var(--switch-moon-white);
+}
+
+[data-theme="light"] {
+ --primary-color: #212121;
+ --secondary-color: #ffffff;
+ --background-color: #f0f0f0;
+ --inner-color: #363636;
+
+ --theme-switch: var(--switch-sun-black);
+}
+
+[auto-theme] {
+ --theme-switch: var(--switch-auto-white);
+}
+
+[data-theme="light"][auto-theme] {
+ --theme-switch: var(--switch-auto-black);
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ background: var(--background-color);
+ color: var(--primary-color);
+ font-size: 1em;
+ font-family: 'Noto Sans', sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+.sprite {
+ display: none;
+}
+
+header {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ width: 100%;
+ height: 64px;
+}
+
+.theme {
+ --size: 28px;
+ position: relative;
+ display: inline-block;
+ width: var(--size);
+ height: var(--size);
+ background: none;
+ border: none;
+ outline: none;
+ margin-right: 12px;
+ cursor: pointer;
+}
+
+.theme:after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ content: "";
+ width: var(--size);
+ height: var(--size);
+ background-repeat: no-repeat;
+ background-position: center;
+ background: var(--theme-switch);
+}
+
+.github-icon {
+ --size: 36px;
+ position: relative;
+ display: block;
+ width: var(--size);
+ height: var(--size);
+ margin-right: 12px;
+}
+
+.github-icon svg.icon {
+ fill: var(--primary-color);
+}
+
+h1, h2, .center {
+ text-align: center;
+}
+
+h1 {
+ margin-top: 64px;
+}
+
+h2 {
+ padding: 0;
+ margin: 0.25em;
+}
+
+p {
+ margin: 0.5em;
+}
+
+.container {
+ position: relative;
+ max-width: 1000px;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.group {
+ position: relative;
+ width: 100%;
+ margin: 40px 0;
+ padding-top: 16px;
+
+ border-top: 1px solid var(--border-color);
+ border-radius: 20px;
+}
+
+.run {
+ padding: 10px 6px;
+ margin: 0.75em auto;
+ max-width: 200px;
+ width: 100%;
+ display: inline-block;
+
+ background: var(--secondary-color);
+ border: none;
+ outline: none;
+
+ color: var(--inner-color);
+ font-weight: bold;
+ cursor: pointer;
+ user-select: none;
+
+ opacity: 0.8;
+ transition: opacity 100ms ease;
+}
+
+.group .run:hover {
+ opacity: 1;
+}
+
+.editor {
+ position: relative;
+ min-height: 100px;
+ width: 100%;
+}
+
+.editor.ace_dark.ace_editor {
+ background-color: var(--secondary-color);
+}
+.editor.ace_dark .ace_gutter {
+ background: var(--secondary-variant-color);
+}
+.editor.ace_dark .ace_gutter .ace_gutter-cell {
+ color: var(--inner-color);
+ opacity: 0.6;
+}
+
+.flex-rows {
+ position: relative;
+ display: block;
+ width: 100%;
+ padding-bottom: 1em;
+}
+
+.description {
+ width: 94%;
+ margin: 10px auto;
+ padding: 0;
+
+ align-items: center;
+ line-height: 1.5;
+}
+
+.left {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+a.anchor {
+ position: relative;
+ color: currentColor;
+ text-decoration: none;
+}
+
+a.anchor:hover::before {
+ content: "🔗";
+ color: currentColor;
+ position: absolute;
+ left: -2rem;
+ top: 0;
+ transform: scale(0.75, 0.75);
+}
+
+footer {
+ font-size: 0.9rem;
+ text-align: center;
+ line-height: 2;
+
+ background: var(--secondary-color);
+}
+
+footer span {
+ vertical-align: middle;
+}
+
+span.icon {
+ position: relative;
+ display: inline-block;
+ height: 1em;
+ width: 1em;
+}
+svg.icon {
+ position: absolute;
+ pointer-events: none;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ fill: var(--inner-color);
+}
+
+footer a {
+ text-decoration: none;
+ color: var(--inner-color);
+ opacity: 0.85;
+ will-change: opacity;
+}
+
+footer a:hover {
+ opacity: 1;
+}
+
+.custom-canvas {
+ margin-top: 30px;
+ background: var(--secondary-color);
+}
+
+@media (min-width: 44em) {
+ .container {
+ width: 95%;
+ }
+
+ .flex-rows {
+ display: flex;
+ flex-direction: row;
+ }
+
+ .description {
+ width: 66%;
+ padding: 0 0 0 1em;
+ }
+}
+
+#ads {
+ max-width: 330px;
+ height: 100px;
+ z-index: 0;
+ background: #232323;
+ border-bottom: 1px solid #343a40;
+ margin: 0 auto;
+ color: #000;
+}
+
+.dh-banner {
+ margin-top: 64px !important;
+}
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..4cb339c
--- /dev/null
+++ b/index.html
@@ -0,0 +1,360 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tsParticles 🎉🎊 | JavaScript Confetti, Particles and Fireworks animations for
+ your website
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/bottom.js b/js/bottom.js
new file mode 100644
index 0000000..5eb2e52
--- /dev/null
+++ b/js/bottom.js
@@ -0,0 +1,162 @@
+import {
+ animationState,
+ animationStateButtons,
+ updateAnimationState,
+ getParticlesShapes,
+} from "./state.js";
+import { optionsToCode } from "./utils.js";
+
+const config = () => {
+ return {
+ fullScreen: {
+ zIndex: 1
+ },
+ emitters: {
+ position: {
+ x: 50,
+ y: 100,
+ },
+ rate: {
+ quantity: 5,
+ delay: 0.15,
+ },
+ },
+ particles: {
+ color: {
+ value: [ "#1E00FF", "#FF0061", "#E1FF00", "#00FF9E" ],
+ },
+ move: {
+ decay: 0.05,
+ direction: "top",
+ enable: true,
+ gravity: {
+ enable: true,
+ },
+ outModes: {
+ top: "none",
+ default: "destroy",
+ },
+ speed: { min: 50, max: 100 },
+ },
+ number: {
+ value: 0,
+ },
+ opacity: {
+ value: 1,
+ },
+ rotate: {
+ value: {
+ min: 0,
+ max: 360,
+ },
+ direction: "random",
+ animation: {
+ enable: true,
+ speed: 30,
+ },
+ },
+ tilt: {
+ direction: "random",
+ enable: true,
+ value: {
+ min: 0,
+ max: 360,
+ },
+ animation: {
+ enable: true,
+ speed: 30,
+ },
+ },
+ size: {
+ value: 3,
+ animation: {
+ enable: true,
+ startValue: "min",
+ count: 1,
+ speed: 16,
+ sync: true,
+ },
+ },
+ roll: {
+ darken: {
+ enable: true,
+ value: 25,
+ },
+ enlighten: {
+ enable: true,
+ value: 25,
+ },
+ enable: true,
+ speed: {
+ min: 5,
+ max: 15,
+ },
+ },
+ wobble: {
+ distance: 30,
+ enable: true,
+ speed: {
+ min: -7,
+ max: 7,
+ },
+ },
+ shape: getParticlesShapes(),
+ },
+ responsive: [
+ {
+ maxWidth: 1024,
+ options: {
+ particles: {
+ move: {
+ speed: { min: 33, max: 66 },
+ }
+ }
+ }
+ }
+ ]
+ };
+};
+
+const refreshBottomConfetti = () => {
+ const options = config();
+
+ console.log(JSON.stringify(options));
+
+ tsParticles
+ .load({
+ id: "tsparticles",
+ options: options
+ })
+ .then(optionsToCode);
+};
+
+const toggleBottomConfetti = (status) => {
+ if (status) {
+ refreshBottomConfetti();
+ }
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ const updateBottom = () => {
+ updateAnimationState({
+ bottom: !animationState.bottom,
+ explosions: false,
+ side: false,
+ falling: false,
+ singleExplosion: false,
+ });
+ };
+
+ const btnBottomConfetti = document.getElementById("btnBottomConfetti");
+
+ animationStateButtons.push({
+ status: () => animationState.bottom,
+ button: btnBottomConfetti,
+ toggle: toggleBottomConfetti,
+ refresh: refreshBottomConfetti,
+ });
+
+ btnBottomConfetti.addEventListener("click", () => {
+ updateBottom();
+ });
+});
diff --git a/js/confetti-modes.js b/js/confetti-modes.js
new file mode 100644
index 0000000..ddf1088
--- /dev/null
+++ b/js/confetti-modes.js
@@ -0,0 +1,49 @@
+(function() {
+ var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
+templates['confetti-modes'] = template({"1":function(container,depth0,helpers,partials,data) {
+ var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
+ if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
+ return parent[propertyName];
+ }
+ return undefined
+ };
+
+ return " \n
\n
\n
\n
\n
\n
\n
\n"
+ + ((stack1 = lookupProperty(helpers,"each").call(alias1,(depth0 != null ? lookupProperty(depth0,"description") : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":15,"column":10},"end":{"line":19,"column":19}}})) != null ? stack1 : "")
+ + "
\n
\n
\n"
+ + ((stack1 = lookupProperty(helpers,"if").call(alias1,lookupProperty(helpers,"isCustom").call(alias1,(depth0 != null ? lookupProperty(depth0,"id") : depth0),{"name":"isCustom","hash":{},"data":data,"loc":{"start":{"line":23,"column":12},"end":{"line":23,"column":25}}}),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":23,"column":6},"end":{"line":31,"column":13}}})) != null ? stack1 : "")
+ + "
\n
\n";
+},"2":function(container,depth0,helpers,partials,data) {
+ var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
+ if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
+ return parent[propertyName];
+ }
+ return undefined
+ };
+
+ return " \n "
+ + alias4(((helper = (helper = lookupProperty(helpers,"text") || (depth0 != null ? lookupProperty(depth0,"text") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"text","hash":{},"data":data,"loc":{"start":{"line":17,"column":14},"end":{"line":17,"column":22}}}) : helper)))
+ + "\n
\n";
+},"4":function(container,depth0,helpers,partials,data) {
+ return " \n \n
\n";
+},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
+ var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
+ if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
+ return parent[propertyName];
+ }
+ return undefined
+ };
+
+ return ((stack1 = lookupProperty(helpers,"each").call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? lookupProperty(depth0,"modes") : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":34,"column":9}}})) != null ? stack1 : "");
+},"useData":true});
+})();
\ No newline at end of file
diff --git a/js/explosions.js b/js/explosions.js
new file mode 100644
index 0000000..352bc9e
--- /dev/null
+++ b/js/explosions.js
@@ -0,0 +1,167 @@
+import {
+ animationState,
+ animationStateButtons,
+ getParticlesShapes,
+ updateAnimationState,
+} from "./state.js";
+import { optionsToCode } from "./utils.js";
+
+const config = () => {
+ return {
+ fullScreen: {
+ zIndex: 1
+ },
+ particles: {
+ number: {
+ value: 0,
+ },
+ color: {
+ value: [ "#00FFFC", "#FC00FF", "#fffc00" ],
+ },
+ shape: getParticlesShapes(),
+ opacity: {
+ value: { min: 0, max: 1 },
+ animation: {
+ enable: true,
+ speed: 2,
+ startValue: "max",
+ destroy: "min",
+ },
+ },
+ size: {
+ value: { min: 2, max: 4 },
+ },
+ links: {
+ enable: false,
+ },
+ life: {
+ duration: {
+ sync: true,
+ value: 5,
+ },
+ count: 1,
+ },
+ move: {
+ enable: true,
+ gravity: {
+ enable: true,
+ acceleration: 10,
+ },
+ speed: { min: 10, max: 20 },
+ decay: 0.1,
+ direction: "none",
+ straight: false,
+ outModes: {
+ default: "destroy",
+ top: "none",
+ },
+ },
+ rotate: {
+ value: {
+ min: 0,
+ max: 360,
+ },
+ direction: "random",
+ move: true,
+ animation: {
+ enable: true,
+ speed: 60,
+ },
+ },
+ tilt: {
+ direction: "random",
+ enable: true,
+ move: true,
+ value: {
+ min: 0,
+ max: 360,
+ },
+ animation: {
+ enable: true,
+ speed: 60,
+ },
+ },
+ roll: {
+ darken: {
+ enable: true,
+ value: 25,
+ },
+ enable: true,
+ speed: {
+ min: 15,
+ max: 25,
+ },
+ },
+ wobble: {
+ distance: 30,
+ enable: true,
+ move: true,
+ speed: {
+ min: -15,
+ max: 15,
+ },
+ },
+ },
+ emitters: {
+ life: {
+ count: 0,
+ duration: 0.1,
+ delay: 0.4,
+ },
+ rate: {
+ delay: 0.1,
+ quantity: 150,
+ },
+ size: {
+ width: 0,
+ height: 0,
+ },
+ },
+ };
+};
+
+const refreshConfettiExplosions = () => {
+ const options = config();
+
+ console.log(JSON.stringify(options));
+
+ tsParticles
+ .load({
+ id: "tsparticles",
+ options: options
+ })
+ .then(optionsToCode);
+};
+
+const toggleConfettiExplosions = (status) => {
+ if (status) {
+ refreshConfettiExplosions();
+ }
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ const updateExplosions = () => {
+ updateAnimationState({
+ bottom: false,
+ explosions: !animationState.explosions,
+ side: false,
+ falling: false,
+ singleExplosion: false,
+ });
+ };
+
+ const btnConfettiExplosions = document.getElementById(
+ "btnConfettiExplosions"
+ );
+
+ animationStateButtons.push({
+ status: () => animationState.explosions,
+ button: btnConfettiExplosions,
+ toggle: toggleConfettiExplosions,
+ refresh: refreshConfettiExplosions,
+ });
+
+ btnConfettiExplosions.addEventListener("click", () => {
+ updateExplosions();
+ });
+});
diff --git a/js/falling.js b/js/falling.js
new file mode 100644
index 0000000..2415bef
--- /dev/null
+++ b/js/falling.js
@@ -0,0 +1,149 @@
+import {
+ animationState,
+ animationStateButtons,
+ getParticlesShapes,
+ updateAnimationState,
+} from "./state.js";
+import { optionsToCode } from "./utils.js";
+
+const config = () => {
+ return {
+ fullScreen: {
+ zIndex: 1
+ },
+ particles: {
+ color: {
+ value: [ "#FFFFFF", "#FFd700" ]
+ },
+ move: {
+ direction: "bottom",
+ enable: true,
+ outModes: {
+ default: "out",
+ },
+ size: true,
+ speed: {
+ min: 1,
+ max: 3,
+ },
+ },
+ number: {
+ value: 500,
+ density: {
+ enable: true,
+ area: 800,
+ },
+ },
+ opacity: {
+ value: 1,
+ animation: {
+ enable: false,
+ startValue: "max",
+ destroy: "min",
+ speed: 0.3,
+ sync: true,
+ },
+ },
+ rotate: {
+ value: {
+ min: 0,
+ max: 360,
+ },
+ direction: "random",
+ move: true,
+ animation: {
+ enable: true,
+ speed: 60,
+ },
+ },
+ tilt: {
+ direction: "random",
+ enable: true,
+ move: true,
+ value: {
+ min: 0,
+ max: 360,
+ },
+ animation: {
+ enable: true,
+ speed: 60,
+ },
+ },
+ shape: getParticlesShapes(),
+ size: {
+ value: {
+ min: 2,
+ max: 4,
+ },
+ },
+ roll: {
+ darken: {
+ enable: true,
+ value: 30,
+ },
+ enlighten: {
+ enable: true,
+ value: 30,
+ },
+ enable: true,
+ speed: {
+ min: 15,
+ max: 25,
+ },
+ },
+ wobble: {
+ distance: 30,
+ enable: true,
+ move: true,
+ speed: {
+ min: -15,
+ max: 15,
+ },
+ },
+ },
+ };
+};
+
+const refreshFallingConfetti = () => {
+ const options = config();
+
+ console.log(JSON.stringify(options));
+
+ tsParticles
+ .load({
+ id: "tsparticles",
+ options: options
+ })
+ .then(optionsToCode);
+};
+
+const toggleFallingConfetti = (status) => {
+ if (status) {
+ refreshFallingConfetti();
+ }
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ const updateFalling = () => {
+ updateAnimationState({
+ bottom: false,
+ explosions: false,
+ side: false,
+ falling: !animationState.falling,
+ singleExplosion: false,
+ });
+ };
+
+ const btnFallingConfetti = document.getElementById("btnFallingConfetti");
+
+ animationStateButtons.push({
+ status: () => animationState.falling,
+ button: btnFallingConfetti,
+ toggle: toggleFallingConfetti,
+ refresh: refreshFallingConfetti,
+ });
+
+ btnFallingConfetti.addEventListener("click", () => {
+ updateFalling();
+ });
+});
diff --git a/js/main.js b/js/main.js
new file mode 100644
index 0000000..39b0c3f
--- /dev/null
+++ b/js/main.js
@@ -0,0 +1,25 @@
+import { appState, updateAnimationState, updateState } from "./state.js";
+import { initShapes } from "./shapes.js";
+import "./bottom.js";
+import "./explosions.js";
+import "./falling.js";
+import "./side.js";
+import "./single.js";
+
+document.addEventListener("DOMContentLoaded", () => {
+ updateAnimationState({
+ bottom: true,
+ });
+
+ const btnToggleCode = document.getElementById("btnToggleCode");
+
+ btnToggleCode.addEventListener("click", () => {
+ updateState({
+ code: !appState.code,
+ });
+ });
+
+ initShapes();
+
+ hljs.highlightAll();
+});
diff --git a/js/more.js b/js/more.js
new file mode 100644
index 0000000..78ca6c3
--- /dev/null
+++ b/js/more.js
@@ -0,0 +1,735 @@
+const editors = [];
+
+let activeTheme = "dark";
+let currentStep =
+ parseInt(localStorage.getItem("tsparticles-confetti/theme"), 10) || 0;
+
+const prefersLightTheme =
+ window.matchMedia && window.matchMedia("(prefers-color-scheme: light)");
+const themes = {
+ light: "ace/theme/xcode",
+ dark: "ace/theme/monokai",
+};
+
+const getPreferedTheme = function () {
+ return prefersLightTheme
+ ? prefersLightTheme.matches
+ ? "light"
+ : "dark"
+ : "dark";
+};
+
+const setTheme = function (isAuto, theme) {
+ if (isAuto) {
+ document.body.setAttribute("auto-theme", true);
+
+ activeTheme = getPreferedTheme();
+ } else {
+ document.body.removeAttribute("auto-theme");
+
+ activeTheme = theme;
+ }
+
+ document.body.setAttribute("data-theme", activeTheme);
+
+ editors.forEach(function (editor) {
+ editor.setTheme(themes[activeTheme]);
+ });
+};
+
+const updateTheme = function (step) {
+ currentStep = step;
+
+ switch (step) {
+ case 0:
+ setTheme(true);
+
+ prefersLightTheme &&
+ prefersLightTheme.addEventListener("change", setTheme);
+
+ break;
+
+ case 1:
+ case 2:
+ setTheme(false, step === 1 ? "dark" : "light");
+
+ prefersLightTheme && prefersLightTheme.removeListener(setTheme);
+ break;
+ }
+
+ localStorage.setItem("tsparticles-confetti/theme", currentStep);
+};
+
+updateTheme(currentStep);
+
+document
+ .getElementById("themeToggle")
+ .addEventListener("click", function (event) {
+ updateTheme(++currentStep % 3);
+ });
+
+const modes = [
+ {
+ id: "cannon",
+ name: "Basic Cannon",
+ description: [
+ {
+ cssClass: "",
+ text: "The default mode... just your regular basic average blast of confetti. But it's still a little cool, right?",
+ },
+ ],
+ fn: function () {
+ confetti({
+ particleCount: 100,
+ spread: 70,
+ origin: { y: 0.6 },
+ });
+ },
+ },
+
+ {
+ id: "random",
+ name: "Random Direction",
+ description: [
+ {
+ cssClass: "",
+ text: "Go crazy with some randomness. Shoot a random amount of confetti in random directions. (Go ahead... you know you want to click that button more than once.)",
+ },
+ ],
+ fn: function () {
+ function randomInRange(min, max) {
+ return Math.random() * (max - min) + min;
+ }
+
+ confetti({
+ angle: randomInRange(55, 125),
+ spread: randomInRange(50, 70),
+ particleCount: randomInRange(50, 100),
+ origin: { y: 0.6 },
+ });
+ },
+ },
+
+ {
+ id: "realistic",
+ name: "Realistic Look",
+ description: [
+ {
+ cssClass: "",
+ text: 'If you happened to get curious and changed the particle count to 400 or so, you saw something disappointing. An even "flattened cone" look to the confetti, making it look way too perfect and ruining the illusion. We can fix that by mixing a few effects together.',
+ },
+ ],
+ fn: function () {
+ const count = 200,
+ defaults = {
+ origin: { y: 0.7 },
+ };
+
+ function fire(particleRatio, opts) {
+ confetti(
+ Object.assign({}, defaults, opts, {
+ particleCount: Math.floor(count * particleRatio),
+ })
+ );
+ }
+
+ fire(0.25, {
+ spread: 26,
+ startVelocity: 55,
+ });
+
+ fire(0.2, {
+ spread: 60,
+ });
+
+ fire(0.35, {
+ spread: 100,
+ decay: 0.91,
+ scalar: 0.8,
+ });
+
+ fire(0.1, {
+ spread: 120,
+ startVelocity: 25,
+ decay: 0.92,
+ scalar: 1.2,
+ });
+
+ fire(0.1, {
+ spread: 120,
+ startVelocity: 45,
+ });
+ },
+ },
+
+ {
+ id: "hearts",
+ name: "Valentine's Day",
+ description: [
+ {
+ cssClass: "",
+ text: "You can create beautiful Valentine's Day effects with the heart shape. Spread the love with some heart shaped confetti.",
+ },
+ {
+ cssClass: "center",
+ text: "❤️ Happy Valentine's Day! ❤️",
+ },
+ ],
+ fn: function () {
+ const defaults = {
+ spread: 360,
+ ticks: 100,
+ gravity: 0,
+ decay: 0.94,
+ startVelocity: 30,
+ shapes: ["heart"],
+ colors: ["FFC0CB", "FF69B4", "FF1493", "C71585"],
+ };
+
+ confetti({
+ ...defaults,
+ particleCount: 50,
+ scalar: 2,
+ });
+
+ confetti({
+ ...defaults,
+ particleCount: 25,
+ scalar: 3,
+ });
+
+ confetti({
+ ...defaults,
+ particleCount: 10,
+ scalar: 4,
+ });
+ },
+ },
+
+ {
+ id: "stars",
+ name: "Stars",
+ description: [
+ {
+ cssClass: "",
+ text: "You can combine multiple calls to confetti with any settings in order to create a more complex effect. Go ahead, combine different shapes, sizes, etc. Stagger them for an extra boost of excitement.",
+ },
+ {
+ cssClass: "center",
+ text: "✨ Celebrate with a burst of stars! ✨",
+ },
+ ],
+ fn: function () {
+ const defaults = {
+ spread: 360,
+ ticks: 50,
+ gravity: 0,
+ decay: 0.94,
+ startVelocity: 30,
+ shapes: ["star"],
+ colors: ["FFE400", "FFBD00", "E89400", "FFCA6C", "FDFFB8"],
+ };
+
+ function shoot() {
+ confetti({
+ ...defaults,
+ particleCount: 40,
+ scalar: 1.2,
+ shapes: ["star"],
+ });
+
+ confetti({
+ ...defaults,
+ particleCount: 10,
+ scalar: 0.75,
+ shapes: ["circle"],
+ });
+ }
+
+ setTimeout(shoot, 0);
+ setTimeout(shoot, 100);
+ setTimeout(shoot, 200);
+ },
+ },
+
+ {
+ id: "emoji",
+ name: "Emoji and Unicorns",
+ description: [
+ {
+ cssClass: "",
+ text: "You can create a beautiful rainbow effect combined with some emoji unicorns. Unicorns already love this. 🦄 ",
+ },
+ {
+ cssClass: "center",
+ text: "🦄 Unicorns loves rainbows! 🦄",
+ },
+ ],
+ fn: function () {
+ const defaults = {
+ spread: 360,
+ ticks: 100,
+ gravity: 0,
+ decay: 0.94,
+ startVelocity: 30,
+ };
+
+ function shoot() {
+ confetti({
+ ...defaults,
+ particleCount: 30,
+ scalar: 1.2,
+ shapes: ["circle", "square"],
+ colors: ["#a864fd", "#29cdff", "#78ff44", "#ff718d", "#fdff6a"],
+ });
+
+ confetti({
+ ...defaults,
+ particleCount: 20,
+ scalar: 2,
+ shapes: ["emoji"],
+ shapeOptions: {
+ emoji: {
+ value: ["🦄", "🌈"],
+ },
+ },
+ });
+ }
+
+ setTimeout(shoot, 0);
+ setTimeout(shoot, 100);
+ setTimeout(shoot, 200);
+ },
+ },
+
+ {
+ id: "images",
+ name: "Images",
+ description: [
+ {
+ cssClass: "",
+ text: "You can create beautiful effects using all your favorite images. Just make sure they're the right size for being used as a confetti shape.",
+ },
+ {
+ cssClass: "center",
+ text: "🍎🥑🍌🍉🍍🍓 Aren't these fruits just the cutest? 🍒🍑🍈🍇🍊🍋",
+ },
+ ],
+ fn: function () {
+ confetti({
+ spread: 360,
+ ticks: 200,
+ gravity: 1,
+ decay: 0.94,
+ startVelocity: 30,
+ particleCount: 100,
+ scalar: 3,
+ shapes: ["image"],
+ shapeOptions: {
+ image: [
+ {
+ src: "https://particles.js.org/images/fruits/apple.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/avocado.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/banana.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/berries.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/cherry.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/grapes.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/lemon.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/orange.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/peach.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/pear.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/pepper.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/plum.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/star.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/strawberry.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/watermelon.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/watermelon_slice.png",
+ width: 32,
+ height: 32,
+ },
+ ],
+ },
+ });
+ },
+ },
+
+ {
+ id: "fireworks",
+ name: "Fireworks",
+ description: [
+ {
+ cssClass: "",
+ text: "Why click a button repeatedly when you can have code do it for you? Shoot some firework of confetti from the sides of page so you can still read the content in the center.",
+ },
+ ],
+ fn: function () {
+ const duration = 15 * 1000,
+ animationEnd = Date.now() + duration,
+ defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
+
+ function randomInRange(min, max) {
+ return Math.random() * (max - min) + min;
+ }
+
+ const interval = setInterval(function () {
+ const timeLeft = animationEnd - Date.now();
+
+ if (timeLeft <= 0) {
+ return clearInterval(interval);
+ }
+
+ const particleCount = 50 * (timeLeft / duration);
+
+ // since particles fall down, start a bit higher than random
+ confetti(
+ Object.assign({}, defaults, {
+ particleCount,
+ origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
+ })
+ );
+ confetti(
+ Object.assign({}, defaults, {
+ particleCount,
+ origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
+ })
+ );
+ }, 250);
+ },
+ },
+
+ {
+ id: "snow",
+ name: "Snow",
+ description: [
+ {
+ cssClass: "",
+ text: "The effect is not limited to crazy rapid fire of confetti though. You can create a wintery mood with gently falling particles across the entire page.",
+ },
+ ],
+ fn: function () {
+ const duration = 15 * 1000,
+ animationEnd = Date.now() + duration;
+
+ let skew = 1;
+
+ function randomInRange(min, max) {
+ return Math.random() * (max - min) + min;
+ }
+
+ (function frame() {
+ const timeLeft = animationEnd - Date.now(),
+ ticks = Math.max(200, 500 * (timeLeft / duration));
+
+ skew = Math.max(0.8, skew - 0.001);
+
+ confetti({
+ particleCount: 1,
+ startVelocity: 0,
+ ticks: ticks,
+ origin: {
+ x: Math.random(),
+ // since particles fall down, skew start toward the top
+ y: Math.random() * skew - 0.2,
+ },
+ colors: ["#ffffff"],
+ shapes: ["circle"],
+ gravity: randomInRange(0.4, 0.6),
+ scalar: randomInRange(0.4, 1),
+ drift: randomInRange(-0.4, 0.4),
+ });
+
+ if (timeLeft > 0) {
+ requestAnimationFrame(frame);
+ }
+ })();
+ },
+ },
+
+ {
+ id: "continuous",
+ name: "School Pride",
+ description: [
+ {
+ cssClass: "",
+ text: "But if you are into crazy rapid fire of confetti, what could be a better use than to show everyone what you are all about? Tell people where you are from with two confetti cannons from either side of the page.",
+ },
+ {
+ cssClass: "center",
+ text: "🌰 Go Buckeyes! 🌰",
+ },
+ ],
+ fn: function () {
+ const end = Date.now() + 15 * 1000;
+
+ // go Buckeyes!
+ const colors = ["#bb0000", "#ffffff"];
+
+ (function frame() {
+ confetti({
+ particleCount: 2,
+ angle: 60,
+ spread: 55,
+ origin: { x: 0 },
+ colors: colors,
+ });
+
+ confetti({
+ particleCount: 2,
+ angle: 120,
+ spread: 55,
+ origin: { x: 1 },
+ colors: colors,
+ });
+
+ if (Date.now() < end) {
+ requestAnimationFrame(frame);
+ }
+ })();
+ },
+ },
+
+ {
+ id: "customShapes",
+ name: "Custom Shapes",
+ description: [
+ {
+ cssClass: "",
+ text: "Celebrate some holidays with holiday-appropriate shapes! You can use any SVG path to make a confetti out of it. Go wild!",
+ },
+ {
+ cssClass: "center",
+ text: "🎃🎄💜",
+ },
+ ],
+ fn: function () {
+ var defaults = {
+ scalar: 2,
+ spread: 270,
+ particleCount: 25,
+ origin: { y: 0.4 },
+ startVelocity: 35,
+ };
+
+ confetti({
+ ...defaults,
+ shapes: ["image"],
+ shapeOptions: {
+ image: {
+ src: "https://particles.js.org/images/pumpkin.svg",
+ replaceColor: true,
+ width: 32,
+ height: 40,
+ },
+ },
+ colors: ["#ff9a00", "#ff7400", "#ff4d00"],
+ });
+ confetti({
+ ...defaults,
+ shapes: ["image"],
+ shapeOptions: {
+ image: {
+ src: "https://particles.js.org/images/pine-tree.svg",
+ replaceColor: true,
+ width: 271,
+ height: 351.5,
+ },
+ },
+ colors: ["#8d960f", "#be0f10", "#445404"],
+ });
+ confetti({
+ ...defaults,
+ shapes: ["heart"],
+ colors: ["#f93963", "#a10864", "#ee0b93"],
+ });
+ },
+ },
+
+ {
+ id: "custom",
+ name: "Custom Canvas",
+ description: [
+ {
+ cssClass: "",
+ text: "But if you just hate confetti all over the place, there's something here for you as well. You can limit where the confetti appear by providing your own canvas element.",
+ },
+ ],
+ fn: function () {
+ (async () => {
+ const canvas = document.getElementById("my-canvas");
+
+ // you should only initialize a canvas once, so save this function
+ // we'll save it to the canvas itself for the purpose of this demo
+ canvas.confetti =
+ canvas.confetti || (await confetti.create(canvas, { resize: true }));
+
+ canvas.confetti({
+ spread: 70,
+ origin: { y: 1.2 },
+ });
+ })();
+ },
+ },
+];
+
+Handlebars.registerHelper("isCustom", function (value) {
+ return value === "custom";
+});
+
+function pretty(val) {
+ return js_beautify(val, { indent_size: 2, brace_style: "preserve-inline" });
+}
+
+function getCode(name) {
+ // pretty-print the code, since we will use minified code in production
+ const mode = modes.find((t) => t.id === name);
+
+ let code = pretty(mode.fn.toString());
+
+ // take out the function wrapper, trim all whitespace
+ code = code
+ .split("\n")
+ .slice(1)
+ .slice(0, -1)
+ .map(function (s) {
+ return s.trim();
+ })
+ .join("\n");
+
+ // pretty-print again
+ return pretty(code);
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ Array.from(document.querySelectorAll(".html-group")).forEach(function (
+ group
+ ) {
+ const name = group.getAttribute("data-name"),
+ codeElem = group.querySelector(".editor"),
+ editor = ace.edit(codeElem);
+
+ editor.setTheme(themes[activeTheme]);
+
+ editor.session.setMode("ace/mode/html");
+ editor.session.setUseSoftTabs(true);
+ editor.session.setTabSize(2);
+
+ const count = editor.session.getLength();
+
+ // set height so that all code is visible
+ codeElem.style.minHeight = 14 * count + 1 + "px";
+ codeElem.style.height = count + "rem";
+
+ editors.push(editor);
+ });
+
+ const template = Handlebars.templates["confetti-modes"];
+
+ document.getElementById("confetti-modes").innerHTML = template({ modes });
+
+ Array.from(document.querySelectorAll(".group")).forEach(function (group) {
+ const name = group.getAttribute("data-name"),
+ button = group.querySelector(".run"),
+ codeElem = group.querySelector(".editor"),
+ editor = ace.edit(codeElem);
+
+ editor.setTheme(themes[activeTheme]);
+
+ editor.session.on("changeMode", function (e, session) {
+ if ("ace/mode/javascript" === session.getMode().$id) {
+ if (!!session.$worker) {
+ session.$worker.send("setOptions", [
+ {
+ esversion: 9,
+ esnext: false,
+ },
+ ]);
+ }
+ }
+ });
+
+ editor.session.setMode("ace/mode/javascript");
+ editor.session.setUseSoftTabs(true);
+ editor.session.setTabSize(2);
+ editor.session.setValue(getCode(name));
+
+ const count = editor.session.getLength();
+
+ // set height so that all code is visible
+ codeElem.style.minHeight = 14 * count + 1 + "px";
+ codeElem.style.height = count + "rem";
+
+ button.addEventListener("click", (ev) => {
+ // stop mobile browsers from zooming when clicking
+ // buttons repeatedly really fast
+ ev.preventDefault();
+
+ try {
+ eval(editor.getValue());
+ } catch (e) {
+ console.error(e);
+ }
+ });
+
+ editors.push(editor);
+ });
+});
diff --git a/js/shapes.js b/js/shapes.js
new file mode 100644
index 0000000..cfa89b1
--- /dev/null
+++ b/js/shapes.js
@@ -0,0 +1,119 @@
+import { confettiTypes, updateShapesState } from "./state.js";
+
+export const initShapes = () => {
+ const btnCircle = document.getElementById("btnCircleConfetti");
+ const btnSquare = document.getElementById("btnSquareConfetti");
+ const btnTriangle = document.getElementById("btnTriangleConfetti");
+ const btnPolygon = document.getElementById("btnPolygonConfetti");
+ const btnEmoji = document.getElementById("btnEmojiConfetti");
+ const btnImage = document.getElementById("btnImageConfetti");
+
+ const updateCircleBtn = () => {
+ if (confettiTypes.circle) {
+ btnCircle.classList.add("active");
+ } else {
+ btnCircle.classList.remove("active");
+ }
+ };
+
+ const updateSquareBtn = () => {
+ if (confettiTypes.square) {
+ btnSquare.classList.add("active");
+ } else {
+ btnSquare.classList.remove("active");
+ }
+ };
+
+ const updateTriangleBtn = () => {
+ if (confettiTypes.triangle) {
+ btnTriangle.classList.add("active");
+ } else {
+ btnTriangle.classList.remove("active");
+ }
+ };
+
+ const updatePolygonBtn = () => {
+ if (confettiTypes.polygon.enable) {
+ btnPolygon.classList.add("active");
+ } else {
+ btnPolygon.classList.remove("active");
+ }
+ };
+
+ const updateEmojiBtn = () => {
+ if (confettiTypes.emoji.enable) {
+ btnEmoji.classList.add("active");
+ } else {
+ btnEmoji.classList.remove("active");
+ }
+ };
+
+ const updateImageBtn = () => {
+ if (confettiTypes.image.enable) {
+ btnImage.classList.add("active");
+ } else {
+ btnImage.classList.remove("active");
+ }
+ };
+
+ btnCircle.addEventListener("click", () => {
+ updateShapesState({
+ circle: !confettiTypes.circle,
+ });
+
+ updateCircleBtn();
+ });
+
+ btnSquare.addEventListener("click", () => {
+ updateShapesState({
+ square: !confettiTypes.square,
+ });
+
+ updateSquareBtn();
+ });
+
+ btnTriangle.addEventListener("click", () => {
+ updateShapesState({
+ triangle: !confettiTypes.triangle,
+ });
+
+ updateTriangleBtn();
+ });
+
+ btnPolygon.addEventListener("click", () => {
+ updateShapesState({
+ polygon: {
+ enable: !confettiTypes.polygon.enable,
+ },
+ });
+
+ updatePolygonBtn();
+ });
+
+ btnEmoji.addEventListener("click", () => {
+ updateShapesState({
+ emoji: {
+ enable: !confettiTypes.emoji.enable,
+ },
+ });
+
+ updateEmojiBtn();
+ });
+
+ btnImage.addEventListener("click", () => {
+ updateShapesState({
+ image: {
+ enable: !confettiTypes.image.enable,
+ },
+ });
+
+ updateImageBtn();
+ });
+
+ updateCircleBtn();
+ updateSquareBtn();
+ updateTriangleBtn();
+ updatePolygonBtn();
+ updateEmojiBtn();
+ updateImageBtn();
+};
diff --git a/js/side.js b/js/side.js
new file mode 100644
index 0000000..7caf9e1
--- /dev/null
+++ b/js/side.js
@@ -0,0 +1,178 @@
+import {
+ animationState,
+ animationStateButtons,
+ getParticlesShapes,
+ updateAnimationState,
+} from "./state.js";
+import { optionsToCode } from "./utils.js";
+
+const config = () => {
+ return {
+ fullScreen: {
+ zIndex: 1
+ },
+ emitters: [
+ {
+ position: {
+ x: 0,
+ y: 30,
+ },
+ rate: {
+ quantity: 5,
+ delay: 0.15,
+ },
+ particles: {
+ move: {
+ direction: "top-right",
+ outModes: {
+ top: "none",
+ left: "none",
+ default: "destroy",
+ },
+ },
+ },
+ },
+ {
+ position: {
+ x: 100,
+ y: 30,
+ },
+ rate: {
+ quantity: 5,
+ delay: 0.15,
+ },
+ particles: {
+ move: {
+ direction: "top-left",
+ outModes: {
+ top: "none",
+ right: "none",
+ default: "destroy",
+ },
+ },
+ },
+ },
+ ],
+ particles: {
+ color: {
+ value: [ "#ffffff", "#FF0000" ],
+ },
+ move: {
+ decay: 0.05,
+ direction: "top",
+ enable: true,
+ gravity: {
+ enable: true,
+ },
+ outModes: {
+ top: "none",
+ default: "destroy",
+ },
+ speed: { min: 10, max: 50 },
+ },
+ number: {
+ value: 0,
+ },
+ opacity: {
+ value: 1,
+ },
+ rotate: {
+ value: {
+ min: 0,
+ max: 360,
+ },
+ direction: "random",
+ animation: {
+ enable: true,
+ speed: 30,
+ },
+ },
+ tilt: {
+ direction: "random",
+ enable: true,
+ value: {
+ min: 0,
+ max: 360,
+ },
+ animation: {
+ enable: true,
+ speed: 30,
+ },
+ },
+ size: {
+ value: { min: 0, max: 2 },
+ animation: {
+ enable: true,
+ startValue: "min",
+ count: 1,
+ speed: 16,
+ sync: true,
+ },
+ },
+ roll: {
+ darken: {
+ enable: true,
+ value: 25,
+ },
+ enable: true,
+ speed: {
+ min: 5,
+ max: 15,
+ },
+ },
+ wobble: {
+ distance: 30,
+ enable: true,
+ speed: {
+ min: -7,
+ max: 7,
+ },
+ },
+ shape: getParticlesShapes(),
+ },
+ };
+};
+
+const refreshSideConfetti = () => {
+ const options = config();
+
+ console.log(JSON.stringify(options));
+
+ tsParticles
+ .load({
+ id: "tsparticles",
+ options: options
+ })
+ .then(optionsToCode);
+};
+
+const toggleSideConfetti = (status) => {
+ if (status) {
+ refreshSideConfetti();
+ }
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ const updateSide = () => {
+ updateAnimationState({
+ bottom: false,
+ explosions: false,
+ side: !animationState.side,
+ falling: false,
+ singleExplosion: false,
+ });
+ };
+
+ const btnSideConfetti = document.getElementById("btnSideConfetti");
+
+ animationStateButtons.push({
+ status: () => animationState.side,
+ button: btnSideConfetti,
+ toggle: toggleSideConfetti,
+ refresh: refreshSideConfetti,
+ });
+
+ btnSideConfetti.addEventListener("click", () => {
+ updateSide();
+ });
+});
diff --git a/js/single.js b/js/single.js
new file mode 100644
index 0000000..832b13c
--- /dev/null
+++ b/js/single.js
@@ -0,0 +1,165 @@
+import {
+ animationState,
+ animationStateButtons,
+ getParticlesShapes,
+ updateAnimationState,
+} from "./state.js";
+import { optionsToCode } from "./utils.js";
+
+const config = () => {
+ return {
+ fullScreen: {
+ zIndex: 1
+ },
+ particles: {
+ number: {
+ value: 0,
+ },
+ color: {
+ value: [ "#00FFFC", "#FC00FF", "#fffc00" ],
+ },
+ shape: getParticlesShapes(),
+ opacity: {
+ value: { min: 0, max: 1 },
+ animation: {
+ enable: true,
+ speed: 2,
+ startValue: "max",
+ destroy: "min",
+ },
+ },
+ size: {
+ value: { min: 2, max: 4 },
+ },
+ links: {
+ enable: false,
+ },
+ life: {
+ duration: {
+ sync: true,
+ value: 5,
+ },
+ count: 1,
+ },
+ move: {
+ enable: true,
+ gravity: {
+ enable: true,
+ acceleration: 10,
+ },
+ speed: { min: 10, max: 20 },
+ decay: 0.1,
+ direction: "none",
+ straight: false,
+ outModes: {
+ default: "destroy",
+ top: "none",
+ },
+ },
+ rotate: {
+ value: {
+ min: 0,
+ max: 360,
+ },
+ direction: "random",
+ move: true,
+ animation: {
+ enable: true,
+ speed: 60,
+ },
+ },
+ tilt: {
+ direction: "random",
+ enable: true,
+ move: true,
+ value: {
+ min: 0,
+ max: 360,
+ },
+ animation: {
+ enable: true,
+ speed: 60,
+ },
+ },
+ roll: {
+ darken: {
+ enable: true,
+ value: 25,
+ },
+ enable: true,
+ speed: {
+ min: 15,
+ max: 25,
+ },
+ },
+ wobble: {
+ distance: 30,
+ enable: true,
+ move: true,
+ speed: {
+ min: -15,
+ max: 15,
+ },
+ },
+ },
+ emitters: {
+ life: {
+ count: 1,
+ duration: 0.1,
+ delay: 0.4,
+ },
+ rate: {
+ delay: 0.1,
+ quantity: 150,
+ },
+ size: {
+ width: 0,
+ height: 0,
+ },
+ },
+ };
+};
+
+const refreshSingleConfetti = () => {
+ const options = config();
+
+ console.log(JSON.stringify(options));
+
+ tsParticles
+ .load({
+ id: "tsparticles",
+ options: options
+ })
+ .then(optionsToCode);
+};
+
+const toggleSingleConfetti = (status) => {
+ if (status) {
+ refreshSingleConfetti();
+ }
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ const updateSingle = (status) => {
+ updateAnimationState({
+ bottom: false,
+ explosions: false,
+ side: false,
+ falling: false,
+ singleExplosion: !status,
+ });
+ };
+
+ const btnSingleConfetti = document.getElementById("btnSingleConfetti");
+
+ animationStateButtons.push({
+ status: () => animationState.singleExplosion,
+ button: btnSingleConfetti,
+ toggle: toggleSingleConfetti,
+ refresh: refreshSingleConfetti,
+ });
+
+ btnSingleConfetti.addEventListener("click", () => {
+ updateSingle(false);
+ });
+});
diff --git a/js/state.js b/js/state.js
new file mode 100644
index 0000000..50e5ede
--- /dev/null
+++ b/js/state.js
@@ -0,0 +1,247 @@
+export const animationState = {
+ bottom: false,
+ explosions: false,
+ side: false,
+ falling: false,
+ singleExplosion: false,
+};
+
+export const appState = {
+ singleTimeout: null,
+ code: false,
+};
+
+export const confettiTypes = {
+ circle: true,
+ square: true,
+ triangle: false,
+ polygon: {
+ enable: false,
+ shapes: [
+ {
+ sides: 5,
+ },
+ {
+ sides: 6,
+ },
+ ],
+ },
+ emoji: {
+ enable: false,
+ value: ["💩", "🤡", "🍀", "🍙", "🦄", "⭐️"],
+ },
+ image: {
+ enable: false,
+ sources: [
+ {
+ src: "https://particles.js.org/images/fruits/apple.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/avocado.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/banana.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/berries.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/cherry.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/grapes.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/lemon.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/orange.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/peach.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/pear.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/pepper.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/plum.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/star.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/strawberry.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/watermelon.png",
+ width: 32,
+ height: 32,
+ },
+ {
+ src: "https://particles.js.org/images/fruits/watermelon_slice.png",
+ width: 32,
+ height: 32,
+ },
+ ],
+ },
+};
+
+export const animationStateButtons = [];
+
+export const updateAnimationState = (newAnimationState) => {
+ _.merge(animationState, newAnimationState);
+
+ if (
+ Object.values(animationState).every((t) =>
+ typeof t === "boolean" ? !t : true
+ )
+ ) {
+ const container = tsParticles.domItem(0);
+
+ if (container) {
+ container.destroy();
+ }
+ }
+
+ for (const stateButton of animationStateButtons) {
+ stateButton.toggle(stateButton.status());
+
+ if (stateButton.status()) {
+ stateButton.button.classList.add("active");
+ } else {
+ stateButton.button.classList.remove("active");
+ }
+ }
+
+ if (appState.singleTimeout) {
+ clearTimeout(appState.singleTimeout);
+
+ appState.singleTimeout = null;
+ }
+
+ if (animationState.singleExplosion) {
+ appState.singleTimeout = setTimeout(() => {
+ updateAnimationState({
+ bottom: false,
+ explosions: false,
+ side: false,
+ falling: false,
+ singleExplosion: false,
+ });
+ }, 5000);
+ }
+};
+
+export const updateState = (newState) => {
+ _.merge(appState, newState);
+
+ const codeEl = document.getElementById("code");
+
+ if (appState.code) {
+ codeEl.classList.remove("d-none");
+ } else {
+ codeEl.classList.add("d-none");
+ }
+};
+
+export const updateCode = (newCode) => {
+ const codeEl = document.getElementById("code-text");
+
+ codeEl.innerHTML = newCode;
+};
+
+export const getParticlesShapes = () => {
+ const type = [];
+ const options = {};
+
+ if (confettiTypes.circle) {
+ type.push("circle");
+ }
+
+ if (confettiTypes.square) {
+ type.push("square");
+ }
+
+ if (confettiTypes.triangle) {
+ type.push("triangle");
+ }
+
+ if (confettiTypes.polygon.enable) {
+ type.push("polygon");
+
+ options.polygon = confettiTypes.polygon.shapes;
+ }
+
+ if (confettiTypes.emoji.enable) {
+ type.push("emoji");
+
+ if (!options.emoji) {
+ options.emoji = {
+ particles: {
+ size: {
+ value: 8,
+ },
+ },
+ };
+ }
+
+ options.emoji.value = confettiTypes.emoji.value;
+ }
+
+ if (confettiTypes.image.enable) {
+ type.push("image");
+
+ options.image = confettiTypes.image.sources.map((t) => {
+ return {
+ ...t,
+ particles: {
+ size: {
+ value: Math.min(t.width, t.height) / 2,
+ },
+ },
+ };
+ });
+ }
+
+ return {
+ type: type.length === 1 ? type[0] : type,
+ options,
+ };
+};
+
+export const updateShapesState = (newShapeState) => {
+ _.merge(confettiTypes, newShapeState);
+
+ updateAnimationState(appState);
+};
diff --git a/js/utils.js b/js/utils.js
new file mode 100644
index 0000000..1cd60c2
--- /dev/null
+++ b/js/utils.js
@@ -0,0 +1,16 @@
+import { updateCode } from "./state.js";
+
+export const optionsToCode = (container) => {
+ updateCode(
+ `tsParticles.load({
+ id: "tsparticles",
+ options: ${JSON.stringify(
+ container.sourceOptions,
+ undefined,
+ 2
+ )}
+ });`
+ );
+
+ hljs.highlightAll();
+};
diff --git a/more.html b/more.html
new file mode 100644
index 0000000..e0254a4
--- /dev/null
+++ b/more.html
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ tsParticles 🎉🎊 | JavaScript Confetti, Particles and Fireworks animations
+ for your website
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tsParticles Confetti
+
+
+
+
+
+
+
+
+
+
+
+
+ First of all include the script in your page, only if using in
+ plain HTML/JS pages:
+
+
+
+
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sitemap.xml b/sitemap.xml
new file mode 100644
index 0000000..f9ae01b
--- /dev/null
+++ b/sitemap.xml
@@ -0,0 +1,15 @@
+
+
+
+ https://confetti.js.org/
+ 2023-02-13
+ monthly
+ 1
+
+
+ https://confetti.js.org/more.html
+ 2023-02-13
+ monthly
+ 0.9
+
+
\ No newline at end of file