May 30th

Animation Configurator — Customize our animation library with ease! Animation Configurator!

Notify me

Documentation

v1.9

Premium element

Core Background

Celestial Flow

A premium particle animation system that brings elegant, dynamic motion to your designs. Built exclusively for Bricks Builder.

Quick Setup

data-celestial-flow-animation

Required

Add this attribute to enable the animation effect

Available Presets

data-celestial-color="preset-name"

Required

red

blue

green

gold

purple

orange

pink

teal

multicolor

silver

cosmic

We're here to help

Choose how you'd like to connect with our support team

Contact Support

Get help from our team of experts

Response within 24 hours

<!DOCTYPE html>
<html lang="en">
<head>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">

  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Celestial Flow Configurator</title>
  <style>
    :root {
  --background: #121212;
  --card-bg: #1e1e1e;
  --card-bg-hover: #252525;
  --text-primary: #f2f2f7;
  --text-secondary: #8e8e93;
  --accent: #ef6013;
  --accent-hover: #c64c0c;
  --border: #2c2c2e;
  --shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  --track: #2c2c2e;
  --thumb: #ef6013;
  --card-radius: 16px;
  --input-radius: 8px;
  --button-radius: 12px;
  --transition: all 0.25s ease;
  --font: 'Inter', BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Arial, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #121212;
    --card-bg: #1e1e1e;
    --text-primary: #f2f2f7;
    --text-secondary: #8e8e93;
    --border: #2c2c2e;
    --shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
    --track: #2c2c2e;
  }
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: var(--font);
  background-color: var(--background);
  color: var(--text-primary);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.container {
  max-width: 100%;
  margin: 0 auto;
  padding: 2rem 1rem;
}

header {
  text-align: center;
  margin-bottom: 2rem;
}

h1 {
  font-size: var(--text-m);
  font-weight: 600;
  margin-bottom: 0.5rem;
  background: linear-gradient(90deg, #ef6013, #ff8c51);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

h2 {
  font-size: var(--text-s);
  font-weight: 400;
  color: var(--text-secondary);
  margin-bottom: 1.5rem;
}

.content {
  display: flex;
  flex-wrap: wrap;
  gap: 2rem;
}

.preview-section {
  flex: 1;
  min-width: 300px;
}

.controls-section {
  flex: 1;
  min-width: 300px;
  max-width: 500px;
}

.card {
  background-color: var(--card-bg);
  border-radius: var(--card-radius);
  box-shadow: var(--shadow);
  overflow: hidden;
  margin-bottom: 2rem;
  border: 1px solid var(--border);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.card:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}

.preview-container {
  height: 300px;
  width: 100%;
  position: relative;
  overflow: hidden;
  border-radius: var(--card-radius);
  background-color: #252525;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}

.preview-content {
  color: white;
  text-align: center;
  font-weight: bold;
  font-size: var(--text-s);
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}

.card-heading {
  padding: 1.25rem 1.5rem;
  font-size: var(--text-s);
  font-weight: 600;
  border-bottom: 1px solid var(--border);
}

.card-content {
  padding: 1.5rem;
}

.control-group {
  margin-bottom: 1.5rem;
}

.control-group:last-child {
  margin-bottom: 0;
}

.control-label {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.75rem;
}

.label-text {
  font-size: var(--text-xs);
  font-weight: 500;
}

.value-text {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  background-color: rgba(50, 50, 50, 0.5);
  padding: 2px 8px;
  border-radius: 4px;
}

input[type="range"] {
  -webkit-appearance: none;
  width: 100%;
  height: 4px;
  background: var(--track);
  border-radius: 2px;
  outline: none;
  margin: 0.5rem 0;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  background: var(--thumb);
  border-radius: 50%;
  cursor: pointer;
  transition: var(--transition);
}

input[type="range"]::-webkit-slider-thumb:hover {
  transform: scale(1.1);
  box-shadow: 0 0 8px rgba(239, 96, 19, 0.5);
}

input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  background: var(--thumb);
  border: none;
  border-radius: 50%;
  cursor: pointer;
  transition: var(--transition);
}

input[type="range"]::-moz-range-thumb:hover {
  transform: scale(1.1);
  box-shadow: 0 0 8px rgba(239, 96, 19, 0.5);
}

input[type="text"],
input[type="number"],
select {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--input-radius);
  font-family: var(--font);
  font-size: var(--text-xs);
  color: var(--text-primary);
  background-color: var(--card-bg);
  margin-bottom: 0.75rem;
  outline: none;
  transition: var(--transition);
}

input[type="text"]:focus,
input[type="number"]:focus,
select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}

.color-input-group {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--input-radius);
  padding: 0.4rem;
  background-color: rgba(30, 30, 30, 0.7);
  transition: var(--transition);
  margin-bottom: 1rem;
}

.color-input-group:hover {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px rgba(239, 96, 19, 0.1);
}

.color-preview {
  position: absolute;
  top: 0.75rem;
  left: 0.75rem;
  width: 24px;
  height: 24px;
  border-radius: 4px;
  pointer-events: none;
}

input[type="color"] {
  -webkit-appearance: none;
  border: none;
  width: 100%;
  height: 24px;
  cursor: pointer;
  background-color: transparent;
}

input[type="color"]::-webkit-color-swatch-wrapper {
  padding: 0;
}

input[type="color"]::-webkit-color-swatch {
  border: none;
  border-radius: 4px;
}

input[type="color"]::-moz-color-swatch {
  border: none;
  border-radius: 4px;
}

.color-text {
  padding-left: 2.5rem;
}

.btn {
  display: block;
  width: 100%;
  padding: 0.9rem 1.5rem;
  background-color: var(--accent);
  color: white;
  font-family: var(--font);
  font-size: var(--text-xs);
  font-weight: 500;
  text-align: center;
  border: none;
  border-radius: var(--button-radius);
  cursor: pointer;
  transition: var(--transition);
}

.btn:hover {
  background-color: var(--accent-hover);
  transform: translateY(-2px);
  box-shadow: 0 5px 15px rgba(239, 96, 19, 0.3);
}

.btn:active {
  transform: translateY(0);
  box-shadow: 0 2px 8px rgba(239, 96, 19, 0.2);
}

.instructions {
  background-color: var(--card-bg);
  padding: 1.5rem;
  border-radius: var(--card-radius);
  margin-bottom: 1.5rem;
  box-shadow: var(--shadow);
}

.instructions h3 {
  font-size: var(--text-s);
  margin-bottom: 0.75rem;
  color: var(--text-primary);
  font-weight: 600;
}

.instructions ol {
  padding-left: 1.25rem;
}

.instructions li {
  margin-bottom: 0.5rem;
  font-size: var(--text-xs);
  color: var(--text-secondary);
  line-height: 1.5;
}

.instructions code {
  background-color: rgba(50, 50, 50, 0.5);
  padding: 0.2rem 0.5rem;
  border-radius: 4px;
  font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  font-size: var(--text-xs);
  color: #ff8c51;
}

.instructions strong {
  font-weight: 500;
  color: var(--text-primary);
}

.notice {
  background-color: rgba(239, 96, 19, 0.08);
  color: #ff8c51;
  padding: 1rem 1.2rem;
  border-radius: var(--input-radius);
  font-size: var(--text-xs);
  margin-top: 1.5rem;
  border-left: 3px solid var(--accent);
  letter-spacing: 0.2px;
  line-height: 1.6;
}

@media (prefers-color-scheme: dark) {
  .notice {
    background-color: rgba(239, 96, 19, 0.08);
    color: #ff8c51;
  }
}

.code-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.8);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  visibility: hidden;
  opacity: 0;
  transition: all 0.3s ease;
  backdrop-filter: blur(5px);
}

.code-modal.active {
  visibility: visible;
  opacity: 1;
}

.modal-content {
  width: 90%;
  max-width: 1000px;
  max-height: 90vh;
  background-color: var(--card-bg);
  border-radius: var(--card-radius);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  overflow: hidden;
  transform: translateY(20px);
  transition: all 0.3s ease;
  border: 1px solid var(--border);
}

.code-modal.active .modal-content {
  transform: translateY(0);
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1.25rem 1.5rem;
  border-bottom: 1px solid var(--border);
}

.modal-title {
  font-size: var(--text-s);
  font-weight: 600;
}

.close-modal {
  background: none;
  border: none;
  font-size: var(--text-s);
  cursor: pointer;
  color: var(--text-secondary);
  transition: var(--transition);
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.close-modal:hover {
  color: var(--text-primary);
  background-color: rgba(255, 255, 255, 0.1);
}

.modal-body {
  padding: 1.5rem;
  overflow-y: auto;
  max-height: calc(90vh - 130px);
}

.code-display {
  background-color: #161616;
  color: #e4e4e4;
  border-radius: var(--card-radius);
  padding: 1.5rem;
  overflow-x: auto;
  font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  font-size: var(--text-xs);
  line-height: 1.6;
  white-space: pre;
  margin-bottom: 1rem;
  max-height: 400px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid #252525;
}

.modal-footer {
    padding: 1rem 1.5rem;
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: flex-end;
  gap: 1rem;
}

.copy-btn,
.download-file-btn {
  padding: 0.7rem 1.2rem;
  background-color: var(--accent);
  color: white;
  font-family: var(--font);
  font-size: var(--text-xs);
  font-weight: 500;
  border: none;
  border-radius: var(--button-radius);
  cursor: pointer;
  transition: var(--transition);
}

.copy-btn:hover,
.download-file-btn:hover {
  background-color: var(--accent-hover);
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(239, 96, 19, 0.3);
}

.copy-btn:active,
.download-file-btn:active {
  transform: scale(0.98);
  box-shadow: 0 2px 8px rgba(239, 96, 19, 0.2);
}

@media (max-width: 768px) {
  .content {
    flex-direction: column;
  }
  
  .preview-container {
    height: 250px;
  }

  h1 {
    font-size: var(--text-s);
  }
}

.premium-badge {
  display: inline-block;
  background: linear-gradient(90deg, #ef6013, #ff8c51);
  color: white;
  font-size: var(--text-xs);
  padding: 0.3rem 0.7rem;
  border-radius: 6px;
  margin-right: 0.75rem;
  font-weight: 600;
  vertical-align: middle;
  letter-spacing: 0.3px;
  box-shadow: 0 2px 8px rgba(239, 96, 19, 0.3);
}

.premium-description {
  color: var(--text-secondary);
  font-size: var(--text-xs);
  margin-bottom: 1.5rem;
  line-height: 1.7;
  letter-spacing: 0.2px;
}

.feature-list {
  list-style: none;
  padding: 0;
  margin: 0 0 1.75rem 0;
}

.feature-list li {
  position: relative;
  padding-left: 1.75rem;
  font-size: var(--text-xs);
  color: var(--text-secondary);
  margin-bottom: 0.9rem;
  letter-spacing: 0.2px;
  line-height: 1.5;
}

.feature-list li:before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--accent);
  font-weight: bold;
}

.premium-btn {
  background: linear-gradient(90deg, #ef6013, #ff8c51);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  letter-spacing: 0.3px;
}

.premium-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 20px rgba(239, 96, 19, 0.4);
}

.btn-icon {
  font-size: 1.1em;
}

.copy-attribute {
  display: inline-flex;
  align-items: center;
  background-color: rgba(50, 50, 50, 0.5);
  padding: 0.2rem 0.5rem;
  border-radius: 4px;
  font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  font-size: var(--text-xs);
  color: #ff8c51;
  cursor: pointer;
  transition: var(--transition);
  margin: 0 0.2rem;
}

.copy-attribute:hover {
  background-color: rgba(239, 96, 19, 0.2);
}

.copy-icon {
  margin-left: 0.5rem;
  font-size: 0.9em;
  opacity: 0.7;
}

.copy-attribute:hover .copy-icon {
  opacity: 1;
}

.premium-card {
  margin-top: 2rem;
  border: 1px solid var(--border);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  background: linear-gradient(145deg, #212121, #1e1e1e);
}

.premium-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
  border-color: rgba(239, 96, 19, 0.3);
}
  </style>
</head>
<body>
  
    <div class="instructions">
      <h3>How to Use in Bricks Builder</h3>
      <ol>
        <li>Customize your celestial flow animation using the controls below</li>
        <li>Click the <strong>Get Code</strong> button and copy the JavaScript</li>
        <li>In Bricks Builder, add a <strong>Code</strong> element and paste the JavaScript</li>
        <li>Add the attribute <div class="copy-attribute" data-attribute="data-celestial-flow-animation">data-celestial-flow-animation<span class="copy-icon">📋</span></div> to any container element</li>
      </ol>
      <div class="notice">This tool creates a custom script with your settings built-in. You only need to add the data attribute to see your configuration!</div>
    </div>

    <div class="content">
      <section class="preview-section">
        <div class="preview-container" id="celestial-preview">
          <div id="celestial-flow-container" data-celestial-flow-animation="true">
          </div>
        </div>
        
        <div class="card premium-card">
          <div class="card-heading">
            <span class="premium-badge">BricksFusion Exclusive</span>
            Get Your Custom Celestial Flow Animation
          </div>
          <div class="card-content">
            <p class="premium-description">Add elegant and mesmerizing celestial particle animations to your website with this custom-crafted JavaScript snippet.</p>
            <ul class="feature-list">
              <li>Easy implementation with simple data attributes</li>
              <li>Smooth animated particles with natural flow</li>
              <li>Customizable colors and animation speed</li>
              <li>Multiple color themes to match your brand</li>
            </ul>
            <button class="btn premium-btn" id="download-btn" data-protection-animation="true">
              <span class="btn-icon">⚡</span>
              Get Instant Code
            </button>
          </div>
        </div>
      </section>

      <section class="controls-section">
        <div class="card">
          <div class="card-heading">Animation Settings</div>
          <div class="card-content">
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">Particle Count</span>
                <span class="value-text"><span id="particle-count-value">50</span></span>
              </div>
              <input type="range" id="particle-count" min="10" max="100" value="50">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">Particle Size</span>
                <span class="value-text"><span id="particle-size-value">1.5</span></span>
              </div>
              <input type="range" id="particle-size" min="0.5" max="3" step="0.1" value="1.5">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">Animation Speed</span>
                <span class="value-text"><span id="animation-speed-value">1</span>x</span>
              </div>
              <input type="range" id="animation-speed" min="0.5" max="2" step="0.1" value="1">
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-heading">Visual Settings</div>
          <div class="card-content">
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">Color Preset</span>
              </div>
              <select id="color-preset">
                <option value="blue">Blue</option>
                <option value="red">Red</option>
                <option value="green">Green</option>
                <option value="purple">Purple</option>
                <option value="orange">Orange</option>
                <option value="teal">Teal</option>
                <option value="pink">Pink</option>
                <option value="gold">Gold</option>
                <option value="silver">Silver</option>
                <option value="cosmic">Cosmic</option>
                <option value="multicolor">Multicolor</option>
              </select>
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">Particle Opacity</span>
                <span class="value-text"><span id="opacity-value">0.7</span></span>
              </div>
              <input type="range" id="opacity" min="0.2" max="1" step="0.05" value="0.7">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">Trail Length</span>
                <span class="value-text"><span id="trail-length-value">15</span>px</span>
              </div>
              <input type="range" id="trail-length" min="5" max="30" value="15">
            </div>
          </div>
        </div>
        
        <div class="card">
          <div class="card-heading">Advanced Options</div>
          <div class="card-content">
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">Direction</span>
              </div>
              <select id="flow-direction">
                <option value="diagonal">Diagonal (Default)</option>
                <option value="upward">Upward</option>
                <option value="downward">Downward</option>
                <option value="random">Random</option>
              </select>
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">Frame Rate</span>
                <span class="value-text"><span id="fps-value">60</span> FPS</span>
              </div>
              <input type="range" id="fps" min="30" max="60" step="10" value="60">
            </div>
          </div>
        </div>
      </section>
    </div>
  
    <div class="code-modal" id="code-modal">
      <div class="modal-content">
        <div class="modal-header">
          <h3 class="modal-title">JavaScript Code</h3>
          <button class="close-modal" id="close-modal">×</button>
        </div>
        <div class="modal-body">
          <div class="code-display" id="js-code"></div>
        </div>
        <div class="modal-footer">
          <button class="copy-btn" id="copy-btn">Copy to Clipboard</button>
          <button class="download-file-btn" id="download-file-btn">Download JS File</button>
        </div>
      </div>
    </div>
  
    <script>
      document.addEventListener('DOMContentLoaded', function() {
        let celestialConfig = {
          particleCount: 50,
          particleSize: 1.5,
          animationSpeed: 1,
          colorPreset: 'blue',
          opacity: 0.7,
          trailLength: 15,
          flowDirection: 'diagonal',
          fps: 60
        };
        
        // Initialize the UI with default values
        function initializeUI() {
          // Setup range sliders
          setupRangeSlider('particle-count', updatePreview);
          setupRangeSlider('particle-size', updatePreview);
          setupRangeSlider('animation-speed', updatePreview);
          setupRangeSlider('opacity', updatePreview);
          setupRangeSlider('trail-length', updatePreview);
          setupRangeSlider('fps', updatePreview);
          
          // Setup selects
          document.getElementById('color-preset').addEventListener('change', updatePreview);
          document.getElementById('flow-direction').addEventListener('change', updatePreview);
          
          // Setup download button and modal
          const downloadBtn = document.getElementById('download-btn');
          const codeModal = document.getElementById('code-modal');
          const closeModal = document.getElementById('close-modal');
          const copyBtn = document.getElementById('copy-btn');
          const downloadFileBtn = document.getElementById('download-file-btn');
          
          downloadBtn.addEventListener('click', function() {
            const jsCode = generateJavaScriptCode();
            document.getElementById('js-code').textContent = jsCode;
            codeModal.classList.add('active');
          });
          
          closeModal.addEventListener('click', function() {
            codeModal.classList.remove('active');
          });
          
          copyBtn.addEventListener('click', function() {
            const jsCode = document.getElementById('js-code').textContent;
            copyToClipboard(jsCode);
            showCopiedTooltip(this);
          });
          
          downloadFileBtn.addEventListener('click', downloadJsFile);
          
          codeModal.addEventListener('click', function(e) {
            if (e.target === codeModal) {
              codeModal.classList.remove('active');
            }
          });
          
          // Setup copy attribute functionality
          setupCopyAttributes();
          
          // Initialize preview
          initializePreview();
          updatePreview();
        }
        
        function setupRangeSlider(id, callback) {
          const slider = document.getElementById(id);
          const valueDisplay = document.getElementById(`${id}-value`);
          
          slider.addEventListener('input', function() {
            valueDisplay.textContent = this.value;
            if (callback) callback();
          });
        }
        
        function setupCopyAttributes() {
          document.querySelectorAll('.copy-attribute').forEach(el => {
            el.addEventListener('click', function(e) {
              e.preventDefault();
              const attribute = this.getAttribute('data-attribute');
              copyToClipboard(attribute);
              showCopiedTooltip(this);
            });
          });
        }
        
        function initializePreview() {
          const container = document.getElementById('celestial-flow-container');
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d');
          
          container.style.position = 'relative';
          container.style.width = '100%';
          container.style.height = '100%';
          container.appendChild(canvas);
          
          Object.assign(canvas.style, {
            position: 'absolute',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
          });
          
          canvas.width = container.clientWidth;
          canvas.height = container.clientHeight;
          
          container.celestialCanvas = canvas;
          container.celestialCtx = ctx;
          container.celestialParticles = [];
          container.animationFrame = null;
        }
        
        function updatePreview() {
          // Get current values from UI
          celestialConfig.particleCount = parseInt(document.getElementById('particle-count').value);
          celestialConfig.particleSize = parseFloat(document.getElementById('particle-size').value);
          celestialConfig.animationSpeed = parseFloat(document.getElementById('animation-speed').value);
          celestialConfig.colorPreset = document.getElementById('color-preset').value;
          celestialConfig.opacity = parseFloat(document.getElementById('opacity').value);
          celestialConfig.trailLength = parseInt(document.getElementById('trail-length').value);
          celestialConfig.flowDirection = document.getElementById('flow-direction').value;
          celestialConfig.fps = parseInt(document.getElementById('fps').value);
          
          // Update the preview
          renderCelestialPreview();
        }
        
        function renderCelestialPreview() {
          const container = document.getElementById('celestial-flow-container');
          const canvas = container.celestialCanvas;
          const ctx = container.celestialCtx;
          
          // Clear any existing animation
          if (container.animationFrame) {
            cancelAnimationFrame(container.animationFrame);
          }
          
          // Reset or create particles
          container.celestialParticles = [];
          for (let i = 0; i < celestialConfig.particleCount; i++) {
            container.celestialParticles.push(createParticle(canvas.width, canvas.height));
          }
          
          // Start the animation
          let lastTime = 0;
          const fpsInterval = 1000 / celestialConfig.fps;
          
          function animate(currentTime) {
            container.animationFrame = requestAnimationFrame(animate);
            
            const deltaTime = currentTime - lastTime;
            if (deltaTime < fpsInterval) return;
            
            lastTime = currentTime - (deltaTime % fpsInterval);
            
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            
            container.celestialParticles.forEach(particle => {
              updateParticle(particle, canvas.width, canvas.height, deltaTime);
              drawParticle(particle, ctx);
            });
          }
          
          container.animationFrame = requestAnimationFrame(animate);
        }
        
        function createParticle(width, height) {
          return {
            x: Math.random() * width,
            y: height + Math.random() * 50,
            radius: Math.random() * celestialConfig.particleSize + 0.5,
            speed: (Math.random() * 1.5 + 0.5) * celestialConfig.animationSpeed,
            life: 0,
            maxLife: Math.random() * 150 + 200,
            hue: getColorFromPreset(),
            saturation: '100%',
            lightness: '60%'
          };
        }
        
        function getColorFromPreset() {
          switch (celestialConfig.colorPreset) {
            case 'blue': return 220;
            case 'red': return 0;
            case 'green': return 120;
            case 'purple': return 270;
            case 'orange': return 30;
            case 'teal': return 180;
            case 'pink': return 330;
            case 'gold': return 45;
            case 'silver': return 0; // With 0% saturation
            case 'cosmic': return Math.random() * 60 + 210;
            case 'multicolor': return Math.random() * 360;
            default: return 220;
          }
        }
        
        function updateParticle(particle, width, height, deltaTime) {
          // Convert deltaTime to a factor (normalized against 16ms frame)
          const factor = (deltaTime / 16) * celestialConfig.animationSpeed;
          
          // Update based on direction
          switch (celestialConfig.flowDirection) {
            case 'upward':
              particle.y -= particle.speed * 2 * factor;
              break;
            case 'downward':
              particle.x -= particle.speed * 0.5 * factor;
              particle.y += particle.speed * 2 * factor;
              break;
            case 'random':
              particle.x += Math.cos(particle.life * 0.05) * particle.speed * factor;
              particle.y -= particle.speed * 1.2 * factor;
              break;
            default: // diagonal
              particle.x -= particle.speed * factor;
              particle.y -= particle.speed * 1.5 * factor;
          }
          
          particle.life += factor;
          
          // Reset particle if it's off-screen or has lived its life
          if (particle.life >= particle.maxLife || particle.x < -50 || particle.y < -50 || particle.x > width + 50) {
            resetParticle(particle, width, height);
          }
        }
        
        function resetParticle(particle, width, height) {
          // Para la dirección 'downward', coloca las partículas en la parte superior
          if (celestialConfig.flowDirection === 'downward') {
            particle.x = Math.random() * width;
            particle.y = -Math.random() * 50; // Empieza arriba de la pantalla
          } else {
            // Para otras direcciones, mantén el comportamiento original
            particle.x = Math.random() * width;
            particle.y = height + Math.random() * 50;
          }
          
          particle.radius = Math.random() * celestialConfig.particleSize + 0.5;
          particle.speed = (Math.random() * 1.5 + 0.5) * celestialConfig.animationSpeed;
          particle.life = 0;
          particle.maxLife = Math.random() * 150 + 200;
          particle.hue = getColorFromPreset();
        }
        
        function drawParticle(particle, ctx) {
          const alpha = Math.sin((particle.life / particle.maxLife) * Math.PI) * celestialConfig.opacity;
          
          // Draw particle
          ctx.beginPath();
          ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
          
          const saturation = celestialConfig.colorPreset === 'silver' ? '0%' : '100%';
          ctx.fillStyle = "hsla(" + particle.hue + ", " + saturation + ", " + particle.lightness + ", " + alpha + ")";
          ctx.fill();
          
          // Draw trail
          const trailLength = celestialConfig.trailLength * particle.speed;
          
          ctx.beginPath();
          ctx.moveTo(particle.x, particle.y);
          
          // Calculate trail end point based on direction
          let trailX, trailY;
          
          switch (celestialConfig.flowDirection) {
            case 'upward':
              trailX = particle.x;
              trailY = particle.y + trailLength;
              break;
            case 'downward':
              trailX = particle.x + trailLength * 0.3;
              trailY = particle.y - trailLength * 1.2;
              break;
            case 'random':
              trailX = particle.x - Math.cos(particle.life * 0.05) * trailLength;
              trailY = particle.y + trailLength;
              break;
            default: // diagonal
              trailX = particle.x + trailLength * 0.7;
              trailY = particle.y + trailLength;
          }
          
          ctx.lineTo(trailX, trailY);
          ctx.strokeStyle = "hsla(" + particle.hue + ", " + saturation + ", " + particle.lightness + ", " + (alpha * 0.5) + ")";
          ctx.lineWidth = particle.radius * 0.5;
          ctx.stroke();
        }
        
        function generateJavaScriptCode() {
          return "(function(window) {\n" +
  "  const EnhancedCelestialFlowAnimation = {\n" +
  "    instances: [],\n" +
  "    config: {\n" +
  "      particleCount: " + celestialConfig.particleCount + ",\n" +
  "      particleSize: " + celestialConfig.particleSize + ",\n" +
  "      animationSpeed: " + celestialConfig.animationSpeed + ",\n" +
  "      colorPreset: \"" + celestialConfig.colorPreset + "\",\n" +
  "      opacity: " + celestialConfig.opacity + ",\n" +
  "      trailLength: " + celestialConfig.trailLength + ",\n" +
  "      flowDirection: \"" + celestialConfig.flowDirection + "\",\n" +
  "      fps: " + celestialConfig.fps + "\n" +
  "    },\n" +
  "    init: function(options = {}) {\n" +
  "      const defaultOptions = {\n" +
  "        selector: '[data-celestial-flow-animation]',\n" +
  "        colorAttr: 'data-celestial-color'\n" +
  "      };\n" +
  "      const config = { ...defaultOptions, ...options };\n" +
  "\n" +
  "      const initInstances = () => {\n" +
  "        document.querySelectorAll(config.selector).forEach(element => {\n" +
  "          if (!element.hasAttribute('data-celestial-initialized')) {\n" +
  "            this.createInstance(element, config);\n" +
  "            element.setAttribute('data-celestial-initialized', 'true');\n" +
  "          }\n" +
  "        });\n" +
  "      };\n" +
  "\n" +
  "      initInstances();\n" +
  "      setTimeout(initInstances, 100);\n" +
  "      window.addEventListener('load', initInstances);\n" +
  "\n" +
  "      const observer = new MutationObserver((mutations) => {\n" +
  "        mutations.forEach((mutation) => {\n" +
  "          if (mutation.type === 'childList') {\n" +
  "            initInstances();\n" +
  "          }\n" +
  "        });\n" +
  "      });\n" +
  "\n" +
  "      observer.observe(document.body, { childList: true, subtree: true });\n" +
  "    },\n" +
  "\n" +
  "    createInstance: function(element, config) {\n" +
  "      const canvas = document.createElement('canvas');\n" +
  "      const ctx = canvas.getContext('2d');\n" +
  "      element.appendChild(canvas);\n" +
  "\n" +
  "      if (window.getComputedStyle(element).position === 'static') {\n" +
  "        element.style.position = 'relative';\n" +
  "      }\n" +
  "\n" +
  "      Object.assign(canvas.style, {\n" +
  "        position: 'absolute',\n" +
  "        top: '0',\n" +
  "        left: '0',\n" +
  "        width: '100%',\n" +
  "        height: '100%',\n" +
  "        pointerEvents: 'none'\n" +
  "      });\n" +
  "\n" +
  "      const instance = {\n" +
  "        element,\n" +
  "        canvas,\n" +
  "        ctx,\n" +
  "        width: 0,\n" +
  "        height: 0,\n" +
  "        celestialFlows: [],\n" +
  "        celestialFlowCount: this.config.particleCount,\n" +
  "        colorPreset: element.getAttribute(config.colorAttr) || this.config.colorPreset,\n" +
  "        lastTime: 0,\n" +
  "        fpsInterval: 1000 / this.config.fps,\n" +
  "\n" +
  "        resizeCanvas: function() {\n" +
  "          this.width = this.element.clientWidth;\n" +
  "          this.height = this.element.clientHeight;\n" +
  "          this.canvas.width = this.width;\n" +
  "          this.canvas.height = this.height;\n" +
  "        },\n" +
  "\n" +
  "        createCelestialFlows: function() {\n" +
  "          this.celestialFlows = [];\n" +
  "          for (let i = 0; i < this.celestialFlowCount; i++) {\n" +
  "            this.celestialFlows.push(new CelestialFlow(this));\n" +
  "          }\n" +
  "        },\n" +
  "\n" +
  "        animate: function(currentTime) {\n" +
  "          requestAnimationFrame(this.animate.bind(this));\n" +
  "          const deltaTime = currentTime - this.lastTime;\n" +
  "          if (deltaTime < this.fpsInterval) return;\n" +
  "          this.lastTime = currentTime - (deltaTime % this.fpsInterval);\n" +
  "\n" +
  "          this.ctx.clearRect(0, 0, this.width, this.height);\n" +
  "          this.celestialFlows.forEach(celestialFlow => {\n" +
  "            celestialFlow.update(deltaTime);\n" +
  "            celestialFlow.draw();\n" +
  "          });\n" +
  "        },\n" +
  "\n" +
  "        resize: function() {\n" +
  "          this.resizeCanvas();\n" +
  "          this.createCelestialFlows();\n" +
  "        },\n" +
  "\n" +
  "        changeColorPreset: function(preset) {\n" +
  "          this.colorPreset = preset;\n" +
  "          this.celestialFlows.forEach(celestialFlow => celestialFlow.updateColor());\n" +
  "        }\n" +
  "      };\n" +
  "\n" +
  "      class CelestialFlow {\n" +
  "        constructor(instance) {\n" +
  "          this.instance = instance;\n" +
  "          this.reset();\n" +
  "          this.updateColor();\n" +
  "        }\n" +
  "\n" +
  "        reset() {\n" +
  "          // Para la dirección 'downward', coloca las partículas en la parte superior\n" +
  "          if (EnhancedCelestialFlowAnimation.config.flowDirection === 'downward') {\n" +
  "            this.x = Math.random() * this.instance.width;\n" +
  "            this.y = -Math.random() * 50; // Empieza arriba de la pantalla\n" +
  "          } else {\n" +
  "            // Para otras direcciones, mantén el comportamiento original\n" +
  "            this.x = Math.random() * this.instance.width;\n" +
  "            this.y = this.instance.height + Math.random() * 50;\n" +
  "          }\n" +
  "          this.speed = (Math.random() * 1.5 + 0.5) * EnhancedCelestialFlowAnimation.config.animationSpeed;\n" +
  "          this.radius = Math.random() * EnhancedCelestialFlowAnimation.config.particleSize + 0.5;\n" +
  "          this.life = 0;\n" +
  "          this.maxLife = Math.random() * 150 + 200;\n" +
  "        }\n" +
  "\n" +
  "        updateColor() {\n" +
  "          const colorPresets = {\n" +
  "            blue: { hue: 220, saturation: '100%', lightness: '60%' },\n" +
  "            red: { hue: 0, saturation: '100%', lightness: '60%' },\n" +
  "            green: { hue: 120, saturation: '100%', lightness: '50%' },\n" +
  "            purple: { hue: 270, saturation: '100%', lightness: '70%' },\n" +
  "            orange: { hue: 30, saturation: '100%', lightness: '50%' },\n" +
  "            multicolor: { hue: () => Math.random() * 360, saturation: '100%', lightness: '50%' },\n" +
  "            teal: { hue: 180, saturation: '100%', lightness: '45%' },\n" +
  "            pink: { hue: 330, saturation: '100%', lightness: '70%' },\n" +
  "            gold: { hue: 45, saturation: '100%', lightness: '55%' },\n" +
  "            silver: { hue: 0, saturation: '0%', lightness: '75%' },\n" +
  "            cosmic: { hue: () => Math.random() * 60 + 210, saturation: '100%', lightness: '60%' }\n" +
  "          };\n" +
  "\n" +
  "          const preset = colorPresets[this.instance.colorPreset] || colorPresets.blue;\n" +
  "          this.hue = typeof preset.hue === 'function' ? preset.hue() : preset.hue;\n" +
  "          this.saturation = preset.saturation;\n" +
  "          this.lightness = preset.lightness;\n" +
  "        }\n" +
  "\n" +
  "        update(deltaTime) {\n" +
  "          const factor = (deltaTime / 16) * EnhancedCelestialFlowAnimation.config.animationSpeed;\n" +
  "          \n" +
  "          // Update based on direction\n" +
  "          switch (EnhancedCelestialFlowAnimation.config.flowDirection) {\n" +
  "            case 'upward':\n" +
  "              this.y -= this.speed * 2 * factor;\n" +
  "              break;\n" +
  "            case 'downward':\n" +
  "              this.x -= this.speed * 0.5 * factor;\n" +
  "              this.y += this.speed * 2 * factor;\n" +
  "              break;\n" +
  "            case 'random':\n" +
  "              this.x += Math.cos(this.life * 0.05) * this.speed * factor;\n" +
  "              this.y -= this.speed * 1.2 * factor;\n" +
  "              break;\n" +
  "            default: // diagonal\n" +
  "              this.x -= this.speed * factor;\n" +
  "              this.y -= this.speed * 1.5 * factor;\n" +
  "          }\n" +
  "          \n" +
  "          this.life += factor;\n" +
  "          \n" +
  "          if (this.life >= this.maxLife || this.x < -50 || this.y < -50 || this.x > this.instance.width + 50) {\n" +
  "            this.reset();\n" +
  "            this.updateColor();\n" +
  "          }\n" +
  "        }\n" +
  "\n" +
  "        draw() {\n" +
  "          const alpha = Math.sin((this.life / this.maxLife) * Math.PI) * EnhancedCelestialFlowAnimation.config.opacity;\n" +
  "          const { ctx } = this.instance;\n" +
  "\n" +
  "          ctx.beginPath();\n" +
  "          ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);\n" +
  "          ctx.fillStyle = \"hsla(\" + this.hue + \", \" + this.saturation + \", \" + this.lightness + \", \" + alpha + \")\";\n" +
  "          ctx.fill();\n" +
  "\n" +
  "          // Draw trail\n" +
  "          const trailLength = EnhancedCelestialFlowAnimation.config.trailLength * this.speed;\n" +
  "          let trailX, trailY;\n" +
  "          \n" +
  "          ctx.beginPath();\n" +
  "          ctx.moveTo(this.x, this.y);\n" +
  "          \n" +
  "          switch (EnhancedCelestialFlowAnimation.config.flowDirection) {\n" +
  "            case 'upward':\n" +
  "              trailX = this.x;\n" +
  "              trailY = this.y + trailLength;\n" +
  "              break;\n" +
  "            case 'downward':\n" +
  "              trailX = this.x + trailLength * 0.3;\n" +
  "              trailY = this.y - trailLength * 1.2;\n" +
  "              break;\n" +
  "            case 'random':\n" +
  "              trailX = this.x - Math.cos(this.life * 0.05) * trailLength;\n" +
  "              trailY = this.y + trailLength;\n" +
  "              break;\n" +
  "            default: // diagonal\n" +
  "              trailX = this.x + trailLength * 0.7;\n" +
  "              trailY = this.y + trailLength;\n" +
  "          }\n" +
  "          \n" +
  "          ctx.lineTo(trailX, trailY);\n" +
  "          ctx.strokeStyle = \"hsla(\" + this.hue + \", \" + this.saturation + \", \" + this.lightness + \", \" + (alpha * 0.5) + \")\";\n" +
  "          ctx.lineWidth = this.radius * 0.5;\n" +
  "          ctx.stroke();\n" +
  "        }\n" +
  "      }\n" +
  "\n" +
  "      instance.resizeCanvas();\n" +
  "      instance.createCelestialFlows();\n" +
  "      instance.animate(0);\n" +
  "\n" +
  "      let resizeTimeout;\n" +
  "      window.addEventListener('resize', () => {\n" +
  "        clearTimeout(resizeTimeout);\n" +
  "        resizeTimeout = setTimeout(() => instance.resize(), 250);\n" +
  "      });\n" +
  "\n" +
  "      this.instances.push(instance);\n" +
  "    },\n" +
  "\n" +
  "    changeColorPreset: function(element, preset) {\n" +
  "      const instance = this.instances.find(inst => inst.element === element);\n" +
  "      if (instance) {\n" +
  "        instance.changeColorPreset(preset);\n" +
  "      }\n" +
  "    }\n" +
  "  };\n" +
  "\n" +
  "  window.EnhancedCelestialFlowAnimation = EnhancedCelestialFlowAnimation;\n" +
  "  EnhancedCelestialFlowAnimation.init();\n" +
  "})(window);";
        }
        
        function downloadJsFile() {
          const jsCode = generateJavaScriptCode();
          const blob = new Blob([jsCode], { type: 'application/javascript' });
          const url = URL.createObjectURL(blob);
          
          const a = document.createElement('a');
          a.href = url;
          a.download = 'celestial-flow-animation.js';
          a.click();
          
          URL.revokeObjectURL(url);
        }
        
        function copyToClipboard(text) {
          navigator.clipboard.writeText(text)
            .catch(err => {
              console.error('Error copying to clipboard:', err);
            });
        }
        
        function showCopiedTooltip(element) {
          const tooltip = document.createElement('div');
          tooltip.textContent = 'Copied to clipboard!';
          tooltip.style.cssText = 'position:fixed;padding:8px 16px;background:#333;color:white;border-radius:6px;font-size:14px;z-index:9999;transition:all 0.3s ease;opacity:0;transform:translateY(10px);box-shadow:0 4px 12px rgba(0,0,0,0.2);';
          document.body.appendChild(tooltip);
          
          // Position near element
          const rect = element.getBoundingClientRect();
          tooltip.style.top = (rect.top - 40) + 'px';
          tooltip.style.left = (rect.left + rect.width/2 - 80) + 'px';
          
          // Show with animation
          setTimeout(() => {
            tooltip.style.opacity = '1';
            tooltip.style.transform = 'translateY(0)';
            
            // Hide with animation
            setTimeout(() => {
              tooltip.style.opacity = '0';
              tooltip.style.transform = 'translateY(-10px)';
              
              // Remove from DOM
              setTimeout(() => {
                document.body.removeChild(tooltip);
              }, 300);
            }, 1500);
          }, 10);
        }
        
        initializeUI();
      });
    </script>
</body>
</html>