NEW RELEASE
v2.2 - 40+ new elementsWhat's new
<!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>FluidCursor Configurator - BricksFusion</title>
  <style>
    :root {
  --background: #000;
  --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;
  --action-bar-height: 70px;
  --success: #28a745;
  --warning: #ffc107;
  --danger: #dc3545;
}

* {
  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;
  padding-bottom: var(--action-bar-height);
}

.action-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: var(--action-bar-height);
  background: linear-gradient(145deg, #1a1a1a, #0f0f0f);
  border-top: 1px solid var(--border);
  z-index: 1000;
  display: flex;
  align-items: center;
  padding: 0 1.5rem;
  gap: 1rem;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(10px);
}

.breadcrumb {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1;
}

.breadcrumb-item {
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: 500;
  text-decoration: none;
  transition: var(--transition);
  padding: 0.5rem 0.75rem;
  border-radius: 6px;
}

.breadcrumb-item:hover {
  color: var(--text-primary);
  background-color: rgba(255, 255, 255, 0.05);
}

.breadcrumb-item.active {
  color: var(--accent);
  background-color: rgba(239, 96, 19, 0.1);
}

.breadcrumb-separator {
  color: var(--text-secondary);
  font-size: var(--text-xs);
  opacity: 0.5;
}

.action-buttons {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.action-btn {
  padding: 0.6rem 1rem;
  background-color: var(--card-bg);
  color: var(--text-primary);
  font-family: var(--font);
  font-size: var(--text-xs);
  font-weight: 500;
  border: 1px solid var(--border);
  border-radius: var(--button-radius);
  cursor: pointer;
  transition: var(--transition);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  white-space: nowrap;
}

.action-btn:hover {
  background-color: var(--card-bg-hover);
  border-color: var(--accent);
  transform: translateY(-1px);
}

.action-btn.primary {
  background: linear-gradient(90deg, var(--accent), #ff8c51);
  border-color: var(--accent);
  color: white;
}

.action-btn.primary:hover {
  background: linear-gradient(90deg, var(--accent-hover), #e67a3f);
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(239, 96, 19, 0.3);
}

.data-attribute-display {
  background-color: rgba(50, 50, 50, 0.8);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.5rem 0.75rem;
  font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  font-size: var(--text-xs);
  color: #ff8c51;
  cursor: pointer;
  transition: var(--transition);
  user-select: all;
}

.data-attribute-display:hover {
  background-color: rgba(239, 96, 19, 0.2);
  border-color: var(--accent);
}

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

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

.page-title {
  font-size: 2.5rem;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 0.5rem;
  background: linear-gradient(90deg, var(--accent), #ff8c51);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.page-subtitle {
  font-size: var(--text-s);
  color: var(--text-secondary);
  font-weight: 500;
}

.instructions-toggle {
  margin-bottom: 2rem;
}

.instructions-card {
  background-color: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--card-radius);
  box-shadow: var(--shadow);
  overflow: hidden;
  transition: var(--transition);
}

.instructions-header {
  padding: 1rem 1.5rem;
  cursor: pointer;
  transition: var(--transition);
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid transparent;
}

.instructions-header:hover {
  background-color: var(--card-bg-hover);
}

.instructions-card.expanded .instructions-header {
  border-bottom-color: var(--border);
}

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

.toggle-icon {
  font-size: 1.2em;
  transition: transform 0.3s ease;
}

.toggle-icon.expanded {
  transform: rotate(180deg);
}

.instructions-content {
  padding: 0 1.5rem;
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease, padding 0.3s ease;
}

.instructions-content.show {
  max-height: 500px;
  padding: 1.5rem;
}

.instructions-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
}

.how-to-use ol {
  padding-left: 1.5rem;
}

.how-to-use li {
  margin-bottom: 0.75rem;
  font-size: var(--text-xs);
  color: var(--text-secondary);
  line-height: 1.6;
}

.how-to-use strong {
  color: var(--text-primary);
  font-weight: 600;
}

.how-to-use 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;
}

.content {
  display: grid;
  grid-template-columns: 1fr 500px;
  gap: 2rem;
  align-items: start;
}

.preview-section {
  position: sticky;
  top: 2rem;
}

.controls-section {
  max-width: 500px;
}

.card {
  background-color: var(--card-bg);
  border-radius: var(--card-radius);
  box-shadow: var(--shadow);
  overflow: hidden;
  margin-bottom: 1.5rem;
  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: 500px;
  width: 100%;
  position: relative;
  overflow: hidden;
  border-radius: var(--card-radius);
  background-color: #000000;
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  cursor: crosshair;
}

.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.5);
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  pointer-events: none;
  opacity: 0.7;
}

.preview-controls {
  position: absolute;
  top: 1rem;
  right: 1rem;
  display: flex;
  gap: 0.5rem;
  z-index: 10;
}

.preview-btn {
  padding: 0.5rem;
  background-color: rgba(0, 0, 0, 0.7);
  color: white;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 6px;
  cursor: pointer;
  transition: var(--transition);
  font-size: var(--text-xs);
  backdrop-filter: blur(5px);
}

.preview-btn:hover {
  background-color: var(--accent);
  border-color: var(--accent);
}

.card-heading {
  padding: 1rem 1.5rem;
  font-size: var(--text-s);
  font-weight: 600;
  border-bottom: 1px solid var(--border);
  letter-spacing: 0.3px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.card-actions {
  display: flex;
  gap: 0.5rem;
}

.card-action-btn {
  padding: 0.4rem 0.8rem;
  background-color: transparent;
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  font-size: var(--text-xs);
  transition: var(--transition);
}

.card-action-btn:hover {
  color: var(--text-primary);
  border-color: var(--accent);
  background-color: rgba(239, 96, 19, 0.1);
}

.card-content {
  padding: 1.5rem;
}

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

.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;
  letter-spacing: 0.2px;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.help-tooltip {
  cursor: help;
  opacity: 0.7;
  transition: var(--transition);
}

.help-tooltip:hover {
  opacity: 1;
  color: var(--accent);
}

.value-display {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.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;
  min-width: 45px;
  text-align: center;
}

.reset-btn {
  padding: 0.2rem 0.4rem;
  background-color: transparent;
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  font-size: 10px;
  transition: var(--transition);
}

.reset-btn:hover {
  color: var(--danger);
  border-color: var(--danger);
  background-color: rgba(220, 53, 69, 0.1);
}

input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 6px;
  background: var(--track);
  border-radius: 3px;
  outline: none;
  margin: 0.8rem 0;
  position: relative;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  background: var(--thumb);
  border-radius: 50%;
  cursor: pointer;
  transition: var(--transition);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}

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

.color-row {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 1rem 1.25rem;
  background-color: rgba(30, 30, 30, 0.7);
  border: 1px solid var(--border);
  border-radius: var(--input-radius);
  transition: var(--transition);
  margin-bottom: 1rem;
}

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

.color-picker-container {
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 8px;
  overflow: hidden;
  border: 2px solid var(--border);
  cursor: pointer;
  transition: var(--transition);
  flex-shrink: 0;
  background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%), 
              linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), 
              linear-gradient(45deg, transparent 75%, #f0f0f0 75%), 
              linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
  background-size: 8px 8px;
  background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
}

.color-picker-container::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--current-color, #ff6b35);
  border-radius: 6px;
  z-index: 1;
}

.color-picker-container:hover {
  border-color: var(--accent);
  transform: scale(1.05);
}

input[type="color"] {
  position: absolute;
  top: -2px;
  left: -2px;
  width: calc(100% + 4px);
  height: calc(100% + 4px);
  border: none;
  cursor: pointer;
  background: transparent;
  z-index: 2;
  opacity: 0;
}

.color-input-group {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.color-label {
  font-size: 10px;
  font-weight: 500;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-left: 0.25rem;
}

.color-input {
  padding: 0.5rem 0.75rem;
  background-color: rgba(0, 0, 0, 0.3);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-primary);
  font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  font-size: 12px;
  transition: var(--transition);
  min-width: 0;
}

.color-input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px rgba(239, 96, 19, 0.2);
  outline: none;
}

.color-input.invalid {
  border-color: var(--danger);
  box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.2);
}

.hex-input,
.hsl-input {
  width: 100%;
}

.color-input-group:nth-child(2) {
  flex: 0.3;
}

.color-input-group:nth-child(3) {
  flex: 0.7;
}

.switch-container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1.2rem;
  padding: 0.5rem 0;
}

.switch-label {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.2px;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.switch {
  position: relative;
  display: inline-block;
  width: 52px;
  height: 28px;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: var(--track);
  transition: var(--transition);
  border-radius: 34px;
}

.slider:before {
  position: absolute;
  content: "";
  height: 20px;
  width: 20px;
  left: 4px;
  bottom: 4px;
  background-color: #f2f2f7;
  transition: var(--transition);
  border-radius: 50%;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}

input:checked + .slider {
  background-color: var(--accent);
}

input:checked + .slider:before {
  transform: translateX(24px);
}

.touch-mode-selector {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin: 0.8rem 0;
}

.touch-mode-btn {
  padding: 0.6rem 0.8rem;
  background-color: var(--card-bg-hover);
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: var(--input-radius);
  font-family: var(--font);
  font-size: var(--text-xs);
  font-weight: 500;
  cursor: pointer;
  transition: var(--transition);
  letter-spacing: 0.2px;
  text-align: center;
}

.touch-mode-btn:hover {
  background-color: rgba(239, 96, 19, 0.1);
  border-color: var(--accent);
  color: var(--text-primary);
}

.touch-mode-btn.active {
  background-color: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: 600;
}

.mode-description {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  margin-top: 0.5rem;
  padding: 0.5rem;
  background-color: rgba(50, 50, 50, 0.3);
  border-radius: var(--input-radius);
  line-height: 1.4;
  border-left: 2px solid var(--accent);
}

.notification {
  position: fixed;
  bottom: calc(var(--action-bar-height) + 1rem);
  left: 50%;
  background-color: var(--success);
  color: white;
  padding: 0.75rem 1rem;
  border-radius: var(--input-radius);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  z-index: 1001;
  transform: translate(-50%, 200px);
  opacity: 0;
  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease;
  font-size: var(--text-xs);
  font-weight: 500;
  max-width: 320px;
  word-wrap: break-word;
  line-height: 1.4;
  text-align: center;
}

.notification.show {
  transform: translate(-50%, 0);
  opacity: 1;
}

@media (max-width: 1200px) {
  .content {
    grid-template-columns: 1fr;
    gap: 1.5rem;
  }
  
  .preview-section {
    position: static;
  }
  
  .controls-section {
    max-width: 100%;
  }
}

@media (max-width: 768px) {
  .action-bar {
    flex-direction: column;
    height: auto;
    min-height: var(--action-bar-height);
    padding: 0.75rem;
  }
  
  .breadcrumb {
    order: 1;
    width: 100%;
  }
  
  .action-buttons {
    order: 2;
    width: 100%;
    justify-content: center;
    flex-wrap: wrap;
  }
  
  body {
    padding-bottom: calc(var(--action-bar-height) + 20px);
  }
  
  .notification {
    bottom: calc(var(--action-bar-height) + 2rem);
    max-width: 280px;
    transform: translate(-50%, 250px);
  }
  
  .notification.show {
    transform: translate(-50%, 0);
    opacity: 1;
  }
  
  .color-row {
    flex-direction: column;
    align-items: stretch;
    gap: 1rem;
    padding: 1rem;
  }
  
  .color-picker-container {
    align-self: center;
    margin-bottom: 0.5rem;
  }
  
  .color-input-group {
    align-items: stretch;
  }
  
  .hex-input,
  .hsl-input {
    width: 100%;
  }
  
  .preview-container {
    height: 300px;
  }
  
  .data-attribute-display {
    font-size: 10px;
    padding: 0.4rem 0.6rem;
  }
  
  .action-btn {
    font-size: 11px;
    padding: 0.5rem 0.8rem;
  }
  
  .page-title {
    font-size: 2rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

button:focus-visible,
input:focus-visible,
.action-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

::-webkit-scrollbar {
  width: 8px;
}

::-webkit-scrollbar-track {
  background: var(--background);
}

::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--accent);
}

.loading {
  opacity: 0.6;
  pointer-events: none;
  position: relative;
}

.loading::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}
  </style>
</head>
<body>
  <div class="action-bar">
    <nav class="breadcrumb">
      <a href="https://bricksfusion.com" class="breadcrumb-item">Home</a>
      <span class="breadcrumb-separator">›</span>
      <a href="https://bricksfusion.com/corebackground/" class="breadcrumb-item">Core Backgrounds</a>
      <span class="breadcrumb-separator">›</span>
      <span class="breadcrumb-item active">FluidCursor</span>
    </nav>
    
    <div class="action-buttons">
      <div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
        data-fluid-cursor
      </div>
      <button class="action-btn primary" id="download-config" title="Copy JavaScript code (Ctrl+D)" data-protection-animation="true">
        <span>📋</span>
        Copy JS
      </button>
      <button class="action-btn" id="copy-full-section" title="Copy complete section JSON for Bricks Builder (Ctrl+S)" data-protection-animation="true">
        <span>📦</span>
        Copy Full Section
      </button>
    </div>
  </div>

  <div class="container">
    <div class="page-header">
      <h1 class="page-title">FluidCursor</h1>
      <p class="page-subtitle">Interactive fluid simulations for Bricks Builder</p>
    </div>

    <div class="instructions-toggle">
      <div class="instructions-card" id="instructions-card">
        <div class="instructions-header" id="instructions-toggle">
          <div class="instructions-title">
            How to Use & Code Information
          </div>
          <span class="toggle-icon">▼</span>
        </div>
        <div class="instructions-content" id="instructions-content">
          <div class="instructions-grid">
            <div class="how-to-use">
              <ol>
                <li>Design your FluidCursor simulation effect using the controls below</li>
                <li>Click <strong>Copy JS</strong> to copy the JavaScript code to clipboard</li>
                <li>In Bricks Builder, add a <strong>Code</strong> element</li>
                <li>Paste or upload the JavaScript code</li>
                <li>To add the effect to any section: go to <strong>Section → Style → Attributes</strong>, add <code>data-fluid-cursor</code> as attribute name (leave value empty)</li>
              </ol>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div class="content">
      <section class="preview-section">
        <div class="preview-container" id="fluid-preview" data-fluid-cursor>
          <div class="preview-content">Interactive FluidCursor Preview</div>
          <div class="preview-controls">
            <button class="preview-btn" id="randomize-fluid" title="Randomize (R)">🎲</button>
          </div>
        </div>
      </section>

      <section class="controls-section">
        <div class="card">
          <div class="card-heading">
            Quality & Performance
            <div class="card-actions">
              <button class="card-action-btn" id="reset-quality" title="Reset Quality Settings">↺</button>
            </div>
          </div>
          <div class="card-content">
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Simulation Quality
                  <span class="help-tooltip" title="Higher values provide better simulation quality but require more processing power">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="sim-resolution-value">128</span></span>
                  <button class="reset-btn" onclick="resetParameter('sim-resolution', 128)">↺</button>
                </div>
              </div>
              <input type="range" id="sim-resolution" min="64" max="512" value="128" step="32">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Render Quality
                  <span class="help-tooltip" title="Controls the visual fidelity of the fluid rendering">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="dye-resolution-value">1024</span></span>
                  <button class="reset-btn" onclick="resetParameter('dye-resolution', 1024)">↺</button>
                </div>
              </div>
              <input type="range" id="dye-resolution" min="256" max="2048" value="1024" step="128">
            </div>

            <div class="switch-container">
              <span class="switch-label">
                Enable Shading
                <span class="help-tooltip" title="Adds realistic lighting effects to the fluid">ℹ</span>
              </span>
              <label class="switch">
                <input type="checkbox" id="shading" checked>
                <span class="slider"></span>
              </label>
            </div>
            
            <div class="switch-container">
              <span class="switch-label">
                Transparency
                <span class="help-tooltip" title="Allows the fluid to blend with background content">ℹ</span>
              </span>
              <label class="switch">
                <input type="checkbox" id="transparent" checked>
                <span class="slider"></span>
              </label>
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-heading">
            Fluid Physics
            <div class="card-actions">
              <button class="card-action-btn" id="reset-physics" title="Reset Physics Settings">↺</button>
            </div>
          </div>
          <div class="card-content">
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Density Dissipation
                  <span class="help-tooltip" title="How quickly the fluid color fades over time">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="density-dissipation-value">0.8</span></span>
                  <button class="reset-btn" onclick="resetParameter('density-dissipation', 0.8)">↺</button>
                </div>
              </div>
              <input type="range" id="density-dissipation" min="0.1" max="10" value="0.8" step="0.1">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Velocity Dissipation
                  <span class="help-tooltip" title="How quickly the fluid motion slows down">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="velocity-dissipation-value">0.2</span></span>
                  <button class="reset-btn" onclick="resetParameter('velocity-dissipation', 0.2)">↺</button>
                </div>
              </div>
              <input type="range" id="velocity-dissipation" min="0.1" max="10" value="0.2" step="0.1">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Pressure
                  <span class="help-tooltip" title="Internal pressure of the fluid system">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="pressure-value">0.43</span></span>
                  <button class="reset-btn" onclick="resetParameter('pressure', 0.43)">↺</button>
                </div>
              </div>
              <input type="range" id="pressure" min="0.01" max="1" value="0.43" step="0.01">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Curl Strength
                  <span class="help-tooltip" title="Creates swirling vorticity in the fluid motion">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="curl-value">25</span></span>
                  <button class="reset-btn" onclick="resetParameter('curl', 25)">↺</button>
                </div>
              </div>
              <input type="range" id="curl" min="0" max="50" value="25" step="1">
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-heading">
            Interaction Settings
            <div class="card-actions">
              <button class="card-action-btn" id="reset-interaction" title="Reset Interaction Settings">↺</button>
            </div>
          </div>
          <div class="card-content">
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Splat Radius
                  <span class="help-tooltip" title="Size of the interaction area when clicking or moving mouse">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="splat-radius-value">0.45</span></span>
                  <button class="reset-btn" onclick="resetParameter('splat-radius', 0.45)">↺</button>
                </div>
              </div>
              <input type="range" id="splat-radius" min="0.1" max="1" value="0.45" step="0.05">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Splat Force
                  <span class="help-tooltip" title="Strength of the force applied when interacting">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="splat-force-value">13000</span></span>
                  <button class="reset-btn" onclick="resetParameter('splat-force', 13000)">↺</button>
                </div>
              </div>
              <input type="range" id="splat-force" min="1000" max="20000" value="13000" step="500">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Color Update Speed
                  <span class="help-tooltip" title="How quickly colors change over time">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="color-update-speed-value">10</span></span>
                  <button class="reset-btn" onclick="resetParameter('color-update-speed', 10)">↺</button>
                </div>
              </div>
              <input type="range" id="color-update-speed" min="1" max="50" value="10" step="1">
            </div>

            <div class="switch-container">
              <span class="switch-label">
                Random Colors
                <span class="help-tooltip" title="Use dynamic random colors instead of a fixed base color">ℹ</span>
              </span>
              <label class="switch">
                <input type="checkbox" id="random-colors" checked>
                <span class="slider"></span>
              </label>
            </div>

            <div class="control-group" id="color-control-group" style="opacity: 0.5; pointer-events: none;">
              <div class="control-label">
                <span class="label-text">Base Color</span>
              </div>
              <div class="color-row">
                <div class="color-picker-container">
                  <input type="color" id="base-color" value="#ff6b35">
                </div>
                <div class="color-input-group">
                  <span class="color-label">HEX</span>
                  <input type="text" class="color-input hex-input" id="base-color-hex" value="#ff6b35" placeholder="#FFFFFF">
                </div>
                <div class="color-input-group">
                  <span class="color-label">HSL</span>
                  <input type="text" class="color-input hsl-input" id="base-color-hsl" placeholder="hsl(0, 100%, 50%)">
                </div>
              </div>
            </div>

            <div class="control-group">
              <div class="control-label">
                <span class="label-text">Mobile Touch Mode</span>
              </div>
              <div class="touch-mode-selector">
                <button class="touch-mode-btn" data-mode="disabled">Disabled</button>
                <button class="touch-mode-btn active" data-mode="scroll">Scroll</button>
                <button class="touch-mode-btn" data-mode="gentle">Gentle</button>
                <button class="touch-mode-btn" data-mode="smooth">Smooth</button>
              </div>
              <div class="mode-description" id="mode-description">
                Perfect for mobile: Allows native scroll while adding gentle tap interactions without preventing scrolling.
              </div>
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-heading">
            Advanced Options
            <div class="card-actions">
              <button class="card-action-btn" id="reset-advanced" title="Reset Advanced Settings">↺</button>
            </div>
          </div>
          <div class="card-content">
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Pressure Iterations
                  <span class="help-tooltip" title="Number of pressure calculation iterations for fluid stability">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="pressure-iterations-value">26</span></span>
                  <button class="reset-btn" onclick="resetParameter('pressure-iterations', 26)">↺</button>
                </div>
              </div>
              <input type="range" id="pressure-iterations" min="5" max="50" value="26" step="1">
            </div>
          </div>
        </div>
      </section>
    </div>
  </div>

  <div class="notification" id="notification"></div>

  <script>
    (function() {
        function initConfigurator() {
      let fluidConfig = {
        simResolution: 128,
        dyeResolution: 1024,
        densityDissipation: 0.8,
        velocityDissipation: 0.2,
        pressure: 0.43,
        pressureIterations: 26,
        curl: 25,
        splatRadius: 0.45,
        splatForce: 13000,
        shading: true,
        colorUpdateSpeed: 10,
        transparent: true,
        baseColor: 'random',
        mobileTouch: 'scroll'
      };

      const defaultConfig = { ...fluidConfig };
      
      let activeFluid = null;
      
      const modeDescriptions = {
        disabled: 'No touch interaction. Mouse only. Perfect for desktop-only experiences.',
        scroll: 'Perfect for mobile: Allows native scroll while adding gentle tap interactions without preventing scrolling.',
        gentle: 'Moderate touch interaction with gentle effects. Disables native scroll for full touch control.',
        smooth: 'Full touch interaction with smooth, fluid effects. Disables native scroll for maximum interactivity.'
      };
      
      function updateModeDescription(mode) {
        const descElement = document.getElementById('mode-description');
        descElement.textContent = modeDescriptions[mode] || modeDescriptions.scroll;
      }
      
      function initFluidAnimation() {
        destroyExistingFluid();
        
        const container = document.getElementById('fluid-preview');
        
        const options = {
          simResolution: fluidConfig.simResolution,
          dyeResolution: fluidConfig.dyeResolution,
          densityDissipation: fluidConfig.densityDissipation,
          velocityDissipation: fluidConfig.velocityDissipation,
          pressure: fluidConfig.pressure,
          pressureIterations: fluidConfig.pressureIterations,
          curl: fluidConfig.curl,
          splatRadius: fluidConfig.splatRadius,
          splatForce: fluidConfig.splatForce,
          shading: fluidConfig.shading,
          colorUpdateSpeed: fluidConfig.colorUpdateSpeed,
          transparent: fluidConfig.transparent,
          baseColor: fluidConfig.baseColor,
          mobileTouch: fluidConfig.mobileTouch
        };
        
        activeFluid = new FluidCursor(container, options);
      }
      
      function destroyExistingFluid() {
        if (activeFluid) {
          activeFluid.destroy();
          activeFluid = null;
        }
      }

      class FluidCursor {
        constructor(container, options = {}) {
          this.container = container;
          this.canvas = document.createElement('canvas');
          
          this.config = {
            SIM_RESOLUTION: options.simResolution || 128,
            DYE_RESOLUTION: options.dyeResolution || 1024,
            DENSITY_DISSIPATION: options.densityDissipation || 3.5,
            VELOCITY_DISSIPATION: options.velocityDissipation || 2,
            PRESSURE: options.pressure || 0.1,
            PRESSURE_ITERATIONS: options.pressureIterations || 20,
            CURL: options.curl || 3,
            SPLAT_RADIUS: options.splatRadius || 0.2,
            SPLAT_FORCE: options.splatForce || 6000,
            SHADING: options.shading !== false,
            COLOR_UPDATE_SPEED: options.colorUpdateSpeed || 10,
            TRANSPARENT: options.transparent !== false,
            BASE_COLOR: options.baseColor || '#ff6b35',
            MOBILE_TOUCH: options.mobileTouch || 'scroll',
            PAUSED: false
          };

          this.pointers = [this.createPointer()];
          this.animationId = null;
          this.lastUpdateTime = Date.now();
          this.colorUpdateTimer = 0.0;
          this.lastTapTime = 0;
          this.tapCooldown = 100;
          
          this.setupCanvas();
          this.initWebGL();
        }

        createPointer() {
          return {
            id: -1,
            texcoordX: 0,
            texcoordY: 0,
            prevTexcoordX: 0,
            prevTexcoordY: 0,
            deltaX: 0,
            deltaY: 0,
            down: false,
            moved: false,
            color: [0, 0, 0]
          };
        }

        setupCanvas() {
          this.canvas.style.position = 'absolute';
          this.canvas.style.top = '0';
          this.canvas.style.left = '0';
          this.canvas.style.width = '100%';
          this.canvas.style.height = '100%';
          this.canvas.style.pointerEvents = 'none';
          this.canvas.style.zIndex = '1';

          this.container.style.position = this.container.style.position || 'relative';
          this.container.style.overflow = 'hidden';
          this.container.appendChild(this.canvas);
        }

        initWebGL() {
          const params = {
            alpha: true,
            depth: false,
            stencil: false,
            antialias: false,
            preserveDrawingBuffer: false,
          };

          this.gl = this.canvas.getContext("webgl2", params) ||
                   this.canvas.getContext("webgl", params) ||
                   this.canvas.getContext("experimental-webgl", params);

          if (!this.gl) {
            return;
          }

          this.isWebGL2 = !!this.canvas.getContext("webgl2", params);
          this.setupWebGLExtensions();
          this.createShaders();
          this.createPrograms();
          this.initFramebuffers();
          this.setupEventListeners();
          this.updateFrame();
        }

        setupWebGLExtensions() {
          let halfFloat, supportLinearFiltering;
          
          if (this.isWebGL2) {
            this.gl.getExtension("EXT_color_buffer_float");
            supportLinearFiltering = this.gl.getExtension("OES_texture_float_linear");
          } else {
            halfFloat = this.gl.getExtension("OES_texture_half_float");
            supportLinearFiltering = this.gl.getExtension("OES_texture_half_float_linear");
          }

          this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
          const halfFloatTexType = this.isWebGL2 ? this.gl.HALF_FLOAT : (halfFloat && halfFloat.HALF_FLOAT_OES);

          let formatRGBA, formatRG, formatR;

          if (this.isWebGL2) {
            formatRGBA = this.getSupportedFormat(this.gl.RGBA16F, this.gl.RGBA, halfFloatTexType);
            formatRG = this.getSupportedFormat(this.gl.RG16F, this.gl.RG, halfFloatTexType);
            formatR = this.getSupportedFormat(this.gl.R16F, this.gl.RED, halfFloatTexType);
          } else {
            formatRGBA = this.getSupportedFormat(this.gl.RGBA, this.gl.RGBA, halfFloatTexType);
            formatRG = this.getSupportedFormat(this.gl.RGBA, this.gl.RGBA, halfFloatTexType);
            formatR = this.getSupportedFormat(this.gl.RGBA, this.gl.RGBA, halfFloatTexType);
          }

          this.ext = {
            formatRGBA,
            formatRG,
            formatR,
            halfFloatTexType,
            supportLinearFiltering: !!supportLinearFiltering
          };

          if (!this.ext.supportLinearFiltering) {
            this.config.DYE_RESOLUTION = 256;
            this.config.SHADING = false;
          }
        }

        getSupportedFormat(internalFormat, format, type) {
          if (!this.supportRenderTextureFormat(internalFormat, format, type)) {
            switch (internalFormat) {
              case this.gl.R16F:
                return this.getSupportedFormat(this.gl.RG16F, this.gl.RG, type);
              case this.gl.RG16F:
                return this.getSupportedFormat(this.gl.RGBA16F, this.gl.RGBA, type);
              default:
                return null;
            }
          }
          return { internalFormat, format };
        }

        supportRenderTextureFormat(internalFormat, format, type) {
          const texture = this.gl.createTexture();
          this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
          this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
          this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
          this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
          this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
          this.gl.texImage2D(this.gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);
          
          const fbo = this.gl.createFramebuffer();
          this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fbo);
          this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
          const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
          return status === this.gl.FRAMEBUFFER_COMPLETE;
        }

        createShaders() {
          const baseVertexShaderSource = `
            precision highp float;
            attribute vec2 aPosition;
            varying vec2 vUv;
            varying vec2 vL;
            varying vec2 vR;
            varying vec2 vT;
            varying vec2 vB;
            uniform vec2 texelSize;
            void main () {
              vUv = aPosition * 0.5 + 0.5;
              vL = vUv - vec2(texelSize.x, 0.0);
              vR = vUv + vec2(texelSize.x, 0.0);
              vT = vUv + vec2(0.0, texelSize.y);
              vB = vUv - vec2(0.0, texelSize.y);
              gl_Position = vec4(aPosition, 0.0, 1.0);
            }
          `;

          this.baseVertexShader = this.compileShader(this.gl.VERTEX_SHADER, baseVertexShaderSource);
          this.createFragmentShaders();
        }

        createFragmentShaders() {
          this.copyShader = this.compileShader(this.gl.FRAGMENT_SHADER, `
            precision mediump float;
            precision mediump sampler2D;
            varying highp vec2 vUv;
            uniform sampler2D uTexture;
            void main () {
              gl_FragColor = texture2D(uTexture, vUv);
            }
          `);

          this.clearShader = this.compileShader(this.gl.FRAGMENT_SHADER, `
            precision mediump float;
            precision mediump sampler2D;
            varying highp vec2 vUv;
            uniform sampler2D uTexture;
            uniform float value;
            void main () {
              gl_FragColor = value * texture2D(uTexture, vUv);
            }
          `);

          this.splatShader = this.compileShader(this.gl.FRAGMENT_SHADER, `
            precision highp float;
            precision highp sampler2D;
            varying vec2 vUv;
            uniform sampler2D uTarget;
            uniform float aspectRatio;
            uniform vec3 color;
            uniform vec2 point;
            uniform float radius;
            void main () {
              vec2 p = vUv - point.xy;
              p.x *= aspectRatio;
              vec3 splat = exp(-dot(p, p) / radius) * color;
              vec3 base = texture2D(uTarget, vUv).xyz;
              gl_FragColor = vec4(base + splat, 1.0);
            }
          `);

          this.advectionShader = this.compileShader(this.gl.FRAGMENT_SHADER, `
            precision highp float;
            precision highp sampler2D;
            varying vec2 vUv;
            uniform sampler2D uVelocity;
            uniform sampler2D uSource;
            uniform vec2 texelSize;
            uniform vec2 dyeTexelSize;
            uniform float dt;
            uniform float dissipation;
            ${!this.ext.supportLinearFiltering ? `
            vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) {
              vec2 st = uv / tsize - 0.5;
              vec2 iuv = floor(st);
              vec2 fuv = fract(st);
              vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize);
              vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize);
              vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize);
              vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize);
              return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y);
            }
            ` : ''}
            void main () {
              ${!this.ext.supportLinearFiltering ? `
                vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize;
                vec4 result = bilerp(uSource, coord, dyeTexelSize);
              ` : `
                vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;
                vec4 result = texture2D(uSource, coord);
              `}
              float decay = 1.0 + dissipation * dt;
              gl_FragColor = result / decay;
            }
          `);

          this.displayShaderSource = `
            precision highp float;
            precision highp sampler2D;
            varying vec2 vUv;
            varying vec2 vL;
            varying vec2 vR;
            varying vec2 vT;
            varying vec2 vB;
            uniform sampler2D uTexture;
            uniform vec2 texelSize;
            ${this.config.SHADING ? `
            vec3 linearToGamma (vec3 color) {
              color = max(color, vec3(0));
              return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0));
            }
            ` : ''}
            void main () {
              vec3 c = texture2D(uTexture, vUv).rgb;
              ${this.config.SHADING ? `
                vec3 lc = texture2D(uTexture, vL).rgb;
                vec3 rc = texture2D(uTexture, vR).rgb;
                vec3 tc = texture2D(uTexture, vT).rgb;
                vec3 bc = texture2D(uTexture, vB).rgb;
                float dx = length(rc) - length(lc);
                float dy = length(tc) - length(bc);
                vec3 n = normalize(vec3(dx, dy, length(texelSize)));
                vec3 l = vec3(0.0, 0.0, 1.0);
                float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0);
                c *= diffuse;
              ` : ''}
              float a = max(c.r, max(c.g, c.b));
              gl_FragColor = vec4(c, a);
            }
          `;

          this.createSimulationShaders();
        }

        createSimulationShaders() {
          this.divergenceShader = this.compileShader(this.gl.FRAGMENT_SHADER, `
            precision mediump float;
            precision mediump sampler2D;
            varying highp vec2 vUv;
            varying highp vec2 vL;
            varying highp vec2 vR;
            varying highp vec2 vT;
            varying highp vec2 vB;
            uniform sampler2D uVelocity;
            void main () {
              float L = texture2D(uVelocity, vL).x;
              float R = texture2D(uVelocity, vR).x;
              float T = texture2D(uVelocity, vT).y;
              float B = texture2D(uVelocity, vB).y;
              vec2 C = texture2D(uVelocity, vUv).xy;
              if (vL.x < 0.0) { L = -C.x; }
              if (vR.x > 1.0) { R = -C.x; }
              if (vT.y > 1.0) { T = -C.y; }
              if (vB.y < 0.0) { B = -C.y; }
              float div = 0.5 * (R - L + T - B);
              gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
            }
          `);

          this.curlShader = this.compileShader(this.gl.FRAGMENT_SHADER, `
            precision mediump float;
            precision mediump sampler2D;
            varying highp vec2 vUv;
            varying highp vec2 vL;
            varying highp vec2 vR;
            varying highp vec2 vT;
            varying highp vec2 vB;
            uniform sampler2D uVelocity;
            void main () {
              float L = texture2D(uVelocity, vL).y;
              float R = texture2D(uVelocity, vR).y;
              float T = texture2D(uVelocity, vT).x;
              float B = texture2D(uVelocity, vB).x;
              float vorticity = R - L - T + B;
              gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
            }
          `);

          this.vorticityShader = this.compileShader(this.gl.FRAGMENT_SHADER, `
            precision highp float;
            precision highp sampler2D;
            varying vec2 vUv;
            varying vec2 vL;
            varying vec2 vR;
            varying vec2 vT;
            varying vec2 vB;
            uniform sampler2D uVelocity;
            uniform sampler2D uCurl;
            uniform float curl;
            uniform float dt;
            void main () {
              float L = texture2D(uCurl, vL).x;
              float R = texture2D(uCurl, vR).x;
              float T = texture2D(uCurl, vT).x;
              float B = texture2D(uCurl, vB).x;
              float C = texture2D(uCurl, vUv).x;
              vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
              force /= length(force) + 0.0001;
              force *= curl * C;
              force.y *= -1.0;
              vec2 velocity = texture2D(uVelocity, vUv).xy;
              velocity += force * dt;
              velocity = min(max(velocity, -1000.0), 1000.0);
              gl_FragColor = vec4(velocity, 0.0, 1.0);
            }
          `);

          this.pressureShader = this.compileShader(this.gl.FRAGMENT_SHADER, `
            precision mediump float;
            precision mediump sampler2D;
            varying highp vec2 vUv;
            varying highp vec2 vL;
            varying highp vec2 vR;
            varying highp vec2 vT;
            varying highp vec2 vB;
            uniform sampler2D uPressure;
            uniform sampler2D uDivergence;
            void main () {
              float L = texture2D(uPressure, vL).x;
              float R = texture2D(uPressure, vR).x;
              float T = texture2D(uPressure, vT).x;
              float B = texture2D(uPressure, vB).x;
              float C = texture2D(uPressure, vUv).x;
              float divergence = texture2D(uDivergence, vUv).x;
              float pressure = (L + R + B + T - divergence) * 0.25;
              gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
            }
          `);

          this.gradientSubtractShader = this.compileShader(this.gl.FRAGMENT_SHADER, `
            precision mediump float;
            precision mediump sampler2D;
            varying highp vec2 vUv;
            varying highp vec2 vL;
            varying highp vec2 vR;
            varying highp vec2 vT;
            varying highp vec2 vB;
            uniform sampler2D uPressure;
            uniform sampler2D uVelocity;
            void main () {
              float L = texture2D(uPressure, vL).x;
              float R = texture2D(uPressure, vR).x;
              float T = texture2D(uPressure, vT).x;
              float B = texture2D(uPressure, vB).x;
              vec2 velocity = texture2D(uVelocity, vUv).xy;
              velocity.xy -= vec2(R - L, T - B);
              gl_FragColor = vec4(velocity, 0.0, 1.0);
            }
          `);
        }

        compileShader(type, source) {
          const shader = this.gl.createShader(type);
          this.gl.shaderSource(shader, source);
          this.gl.compileShader(shader);
          if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
            this.gl.deleteShader(shader);
            return null;
          }
          return shader;
        }

        createProgram(vertexShader, fragmentShader) {
          const program = this.gl.createProgram();
          this.gl.attachShader(program, vertexShader);
          this.gl.attachShader(program, fragmentShader);
          this.gl.linkProgram(program);
          if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
            return null;
          }
          return program;
        }

        getUniforms(program) {
          const uniforms = {};
          const uniformCount = this.gl.getProgramParameter(program, this.gl.ACTIVE_UNIFORMS);
          for (let i = 0; i < uniformCount; i++) {
            const uniformName = this.gl.getActiveUniform(program, i).name;
            uniforms[uniformName] = this.gl.getUniformLocation(program, uniformName);
          }
          return uniforms;
        }

        createPrograms() {
          this.copyProgram = {
            program: this.createProgram(this.baseVertexShader, this.copyShader),
            uniforms: {}
          };
          this.copyProgram.uniforms = this.getUniforms(this.copyProgram.program);

          this.clearProgram = {
            program: this.createProgram(this.baseVertexShader, this.clearShader),
            uniforms: {}
          };
          this.clearProgram.uniforms = this.getUniforms(this.clearProgram.program);

          this.splatProgram = {
            program: this.createProgram(this.baseVertexShader, this.splatShader),
            uniforms: {}
          };
          this.splatProgram.uniforms = this.getUniforms(this.splatProgram.program);

          this.advectionProgram = {
            program: this.createProgram(this.baseVertexShader, this.advectionShader),
            uniforms: {}
          };
          this.advectionProgram.uniforms = this.getUniforms(this.advectionProgram.program);

          this.divergenceProgram = {
            program: this.createProgram(this.baseVertexShader, this.divergenceShader),
            uniforms: {}
          };
          this.divergenceProgram.uniforms = this.getUniforms(this.divergenceProgram.program);

          this.curlProgram = {
            program: this.createProgram(this.baseVertexShader, this.curlShader),
            uniforms: {}
          };
          this.curlProgram.uniforms = this.getUniforms(this.curlProgram.program);

          this.vorticityProgram = {
            program: this.createProgram(this.baseVertexShader, this.vorticityShader),
            uniforms: {}
          };
          this.vorticityProgram.uniforms = this.getUniforms(this.vorticityProgram.program);

          this.pressureProgram = {
            program: this.createProgram(this.baseVertexShader, this.pressureShader),
            uniforms: {}
          };
          this.pressureProgram.uniforms = this.getUniforms(this.pressureProgram.program);

          this.gradientSubtractProgram = {
            program: this.createProgram(this.baseVertexShader, this.gradientSubtractShader),
            uniforms: {}
          };
          this.gradientSubtractProgram.uniforms = this.getUniforms(this.gradientSubtractProgram.program);

          const displayShader = this.compileShader(this.gl.FRAGMENT_SHADER, this.displayShaderSource);
          this.displayProgram = {
            program: this.createProgram(this.baseVertexShader, displayShader),
            uniforms: {}
          };
          this.displayProgram.uniforms = this.getUniforms(this.displayProgram.program);

          this.setupGeometry();
        }

        setupGeometry() {
          this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.gl.createBuffer());
          this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), this.gl.STATIC_DRAW);
          this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.gl.createBuffer());
          this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), this.gl.STATIC_DRAW);
          this.gl.vertexAttribPointer(0, 2, this.gl.FLOAT, false, 0, 0);
          this.gl.enableVertexAttribArray(0);
        }

        blit(target, clear = false) {
          if (target == null) {
            this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);
            this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
          } else {
            this.gl.viewport(0, 0, target.width, target.height);
            this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, target.fbo);
          }
          if (clear) {
            this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
            this.gl.clear(this.gl.COLOR_BUFFER_BIT);
          }
          this.gl.drawElements(this.gl.TRIANGLES, 6, this.gl.UNSIGNED_SHORT, 0);
        }

        initFramebuffers() {
          const simRes = this.getResolution(this.config.SIM_RESOLUTION);
          const dyeRes = this.getResolution(this.config.DYE_RESOLUTION);
          const texType = this.ext.halfFloatTexType;
          const rgba = this.ext.formatRGBA;
          const rg = this.ext.formatRG;
          const r = this.ext.formatR;
          const filtering = this.ext.supportLinearFiltering ? this.gl.LINEAR : this.gl.NEAREST;

          this.gl.disable(this.gl.BLEND);

          this.dye = this.createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering);
          this.velocity = this.createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering);
          this.divergence = this.createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, this.gl.NEAREST);
          this.curl = this.createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, this.gl.NEAREST);
          this.pressure = this.createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, this.gl.NEAREST);
        }

        createFBO(w, h, internalFormat, format, type, param) {
          this.gl.activeTexture(this.gl.TEXTURE0);
          const texture = this.gl.createTexture();
          this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
          this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, param);
          this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, param);
          this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
          this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
          this.gl.texImage2D(this.gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);

          const fbo = this.gl.createFramebuffer();
          this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fbo);
          this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
          this.gl.viewport(0, 0, w, h);
          this.gl.clear(this.gl.COLOR_BUFFER_BIT);

          return {
            texture,
            fbo,
            width: w,
            height: h,
            texelSizeX: 1.0 / w,
            texelSizeY: 1.0 / h,
            attach: (id) => {
              this.gl.activeTexture(this.gl.TEXTURE0 + id);
              this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
              return id;
            }
          };
        }

        createDoubleFBO(w, h, internalFormat, format, type, param) {
          let fbo1 = this.createFBO(w, h, internalFormat, format, type, param);
          let fbo2 = this.createFBO(w, h, internalFormat, format, type, param);
          
          return {
            width: w,
            height: h,
            texelSizeX: fbo1.texelSizeX,
            texelSizeY: fbo1.texelSizeY,
            get read() { return fbo1; },
            set read(value) { fbo1 = value; },
            get write() { return fbo2; },
            set write(value) { fbo2 = value; },
            swap: () => {
              const temp = fbo1;
              fbo1 = fbo2;
              fbo2 = temp;
            }
          };
        }

        getResolution(resolution) {
          let aspectRatio = this.gl.drawingBufferWidth / this.gl.drawingBufferHeight;
          if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio;
          const min = Math.round(resolution);
          const max = Math.round(resolution * aspectRatio);
          if (this.gl.drawingBufferWidth > this.gl.drawingBufferHeight)
            return { width: max, height: min };
          else return { width: min, height: max };
        }

        setupEventListeners() {
          const getPointerData = (e) => {
            const rect = this.container.getBoundingClientRect();
            const scaleX = this.canvas.width / rect.width;
            const scaleY = this.canvas.height / rect.height;
            const posX = (e.clientX - rect.left) * scaleX;
            const posY = (e.clientY - rect.top) * scaleY;
            return { posX, posY };
          };

          const getTouchData = (e) => {
            const rect = this.container.getBoundingClientRect();
            const scaleX = this.canvas.width / rect.width;
            const scaleY = this.canvas.height / rect.height;
            const touch = e.touches[0] || e.changedTouches[0];
            const posX = (touch.clientX - rect.left) * scaleX;
            const posY = (touch.clientY - rect.top) * scaleY;
            return { posX, posY };
          };

          const handleMouseMove = (e) => {
            const { posX, posY } = getPointerData(e);
            this.updatePointerMoveData(this.pointers[0], posX, posY, this.pointers[0].color);
          };

          const handleMouseDown = (e) => {
            const now = Date.now();
            if (now - this.lastTapTime < this.tapCooldown) return;
            this.lastTapTime = now;
            
            const { posX, posY } = getPointerData(e);
            this.updatePointerDownData(this.pointers[0], -1, posX, posY);
            
            if (this.config.MOBILE_TOUCH === 'scroll') {
              this.scrollFriendlyTap(this.pointers[0]);
            } else if (this.config.MOBILE_TOUCH === 'smooth') {
              this.smoothTapInteraction(posX, posY);
            } else if (this.config.MOBILE_TOUCH === 'gentle') {
              this.gentleTapInteraction(this.pointers[0]);
            } else if (this.config.MOBILE_TOUCH !== 'disabled') {
              this.clickSplat(this.pointers[0]);
            }
          };

          const handleTouchStart = (e) => {
            if (this.config.MOBILE_TOUCH !== 'gentle' && this.config.MOBILE_TOUCH !== 'smooth') return;
            
            e.preventDefault();
            const now = Date.now();
            if (now - this.lastTapTime < this.tapCooldown) return;
            this.lastTapTime = now;
            
            const { posX, posY } = getTouchData(e);
            this.updatePointerDownData(this.pointers[0], -1, posX, posY);
            
            if (this.config.MOBILE_TOUCH === 'smooth') {
              this.smoothTapInteraction(posX, posY);
            } else if (this.config.MOBILE_TOUCH === 'gentle') {
              this.gentleTapInteraction(this.pointers[0]);
            }
          };

          const handleTouchMove = (e) => {
            if (this.config.MOBILE_TOUCH !== 'gentle' && this.config.MOBILE_TOUCH !== 'smooth') return;
            
            e.preventDefault();
            const { posX, posY } = getTouchData(e);
            this.updatePointerMoveData(this.pointers[0], posX, posY, this.pointers[0].color);
          };

          const handleTouchEnd = (e) => {
            if (this.config.MOBILE_TOUCH !== 'gentle' && this.config.MOBILE_TOUCH !== 'smooth') return;
            
            e.preventDefault();
            this.pointers[0].down = false;
          };

          const handleResize = () => {
            this.resizeCanvas();
          };

          this.container.addEventListener('mousemove', handleMouseMove);
          this.container.addEventListener('mousedown', handleMouseDown);

          if (this.config.MOBILE_TOUCH === 'gentle' || this.config.MOBILE_TOUCH === 'smooth') {
            this.container.addEventListener('touchstart', handleTouchStart, { passive: false });
            this.container.addEventListener('touchmove', handleTouchMove, { passive: false });
            this.container.addEventListener('touchend', handleTouchEnd, { passive: false });
          }

          window.addEventListener('resize', handleResize);

          this.cleanup = () => {
            this.container.removeEventListener('mousemove', handleMouseMove);
            this.container.removeEventListener('mousedown', handleMouseDown);
            if (this.config.MOBILE_TOUCH === 'gentle' || this.config.MOBILE_TOUCH === 'smooth') {
              this.container.removeEventListener('touchstart', handleTouchStart);
              this.container.removeEventListener('touchmove', handleTouchMove);
              this.container.removeEventListener('touchend', handleTouchEnd);
            }
            window.removeEventListener('resize', handleResize);
          };
        }

        scrollFriendlyTap(pointer) {
          const color = this.generateColor();
          color.r *= 1.5;
          color.g *= 1.5;
          color.b *= 1.5;
          const dx = 3 * (Math.random() - 0.5);
          const dy = 8 * (Math.random() - 0.5);
          this.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
        }

        smoothTapInteraction(x, y) {
          const steps = 5;
          const baseColor = this.generateColor();
          
          for (let i = 0; i < steps; i++) {
            setTimeout(() => {
              const progress = i / (steps - 1);
              const offsetX = (Math.random() - 0.5) * 0.02 * progress;
              const offsetY = (Math.random() - 0.5) * 0.02 * progress;
              
              const currentX = (x / this.canvas.width) + offsetX;
              const currentY = 1.0 - ((y / this.canvas.height) + offsetY);
              
              const dx = 3 * (Math.random() - 0.5) * (1 - progress * 0.5);
              const dy = 8 * (Math.random() - 0.5) * (1 - progress * 0.5);
              
              const intensity = 1.5 * (1 - progress * 0.3);
              const color = {
                r: baseColor.r * intensity,
                g: baseColor.g * intensity,
                b: baseColor.b * intensity
              };
              
              this.splat(currentX, currentY, dx, dy, color);
            }, i * 16);
          }
        }

        gentleTapInteraction(pointer) {
          const color = this.generateColor();
          color.r *= 4.0;
          color.g *= 4.0;
          color.b *= 4.0;
          const dx = 8 * (Math.random() - 0.5);
          const dy = 20 * (Math.random() - 0.5);
          this.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
        }

        resizeCanvas() {
          const rect = this.container.getBoundingClientRect();
          const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
          const width = Math.floor(rect.width * pixelRatio);
          const height = Math.floor(rect.height * pixelRatio);
          
          if (this.canvas.width !== width || this.canvas.height !== height) {
            this.canvas.width = width;
            this.canvas.height = height;
            this.initFramebuffers();
            return true;
          }
          return false;
        }

        updateFrame = () => {
          const dt = this.calcDeltaTime();
          if (this.resizeCanvas()) this.initFramebuffers();
          this.updateColors(dt);
          this.applyInputs();
          this.step(dt);
          this.render();
          this.animationId = requestAnimationFrame(this.updateFrame);
        };

        calcDeltaTime() {
          const now = Date.now();
          const dt = Math.min((now - this.lastUpdateTime) / 1000, 0.016666);
          this.lastUpdateTime = now;
          return dt;
        }

        updateColors(dt) {
          this.colorUpdateTimer += dt * this.config.COLOR_UPDATE_SPEED;
          if (this.colorUpdateTimer >= 1) {
            this.colorUpdateTimer = this.wrap(this.colorUpdateTimer, 0, 1);
            this.pointers.forEach(p => {
              p.color = this.generateColor();
            });
          }
        }

        applyInputs() {
          this.pointers.forEach(p => {
            if (p.moved) {
              p.moved = false;
              this.splatPointer(p);
            }
          });
        }

        step(dt) {
          this.gl.disable(this.gl.BLEND);

          this.gl.useProgram(this.curlProgram.program);
          this.gl.uniform2f(this.curlProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
          this.gl.uniform1i(this.curlProgram.uniforms.uVelocity, this.velocity.read.attach(0));
          this.blit(this.curl);

          this.gl.useProgram(this.vorticityProgram.program);
          this.gl.uniform2f(this.vorticityProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
          this.gl.uniform1i(this.vorticityProgram.uniforms.uVelocity, this.velocity.read.attach(0));
          this.gl.uniform1i(this.vorticityProgram.uniforms.uCurl, this.curl.attach(1));
          this.gl.uniform1f(this.vorticityProgram.uniforms.curl, this.config.CURL);
          this.gl.uniform1f(this.vorticityProgram.uniforms.dt, dt);
          this.blit(this.velocity.write);
          this.velocity.swap();

          this.gl.useProgram(this.divergenceProgram.program);
          this.gl.uniform2f(this.divergenceProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
          this.gl.uniform1i(this.divergenceProgram.uniforms.uVelocity, this.velocity.read.attach(0));
          this.blit(this.divergence);

          this.gl.useProgram(this.clearProgram.program);
          this.gl.uniform1i(this.clearProgram.uniforms.uTexture, this.pressure.read.attach(0));
          this.gl.uniform1f(this.clearProgram.uniforms.value, this.config.PRESSURE);
          this.blit(this.pressure.write);
          this.pressure.swap();

          this.gl.useProgram(this.pressureProgram.program);
          this.gl.uniform2f(this.pressureProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
          this.gl.uniform1i(this.pressureProgram.uniforms.uDivergence, this.divergence.attach(0));
          for (let i = 0; i < this.config.PRESSURE_ITERATIONS; i++) {
            this.gl.uniform1i(this.pressureProgram.uniforms.uPressure, this.pressure.read.attach(1));
            this.blit(this.pressure.write);
            this.pressure.swap();
          }

          this.gl.useProgram(this.gradientSubtractProgram.program);
          this.gl.uniform2f(this.gradientSubtractProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
          this.gl.uniform1i(this.gradientSubtractProgram.uniforms.uPressure, this.pressure.read.attach(0));
          this.gl.uniform1i(this.gradientSubtractProgram.uniforms.uVelocity, this.velocity.read.attach(1));
          this.blit(this.velocity.write);
          this.velocity.swap();

          this.gl.useProgram(this.advectionProgram.program);
          this.gl.uniform2f(this.advectionProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
          if (!this.ext.supportLinearFiltering)
            this.gl.uniform2f(this.advectionProgram.uniforms.dyeTexelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
          let velocityId = this.velocity.read.attach(0);
          this.gl.uniform1i(this.advectionProgram.uniforms.uVelocity, velocityId);
          this.gl.uniform1i(this.advectionProgram.uniforms.uSource, velocityId);
          this.gl.uniform1f(this.advectionProgram.uniforms.dt, dt);
          this.gl.uniform1f(this.advectionProgram.uniforms.dissipation, this.config.VELOCITY_DISSIPATION);
          this.blit(this.velocity.write);
          this.velocity.swap();

          if (!this.ext.supportLinearFiltering)
            this.gl.uniform2f(this.advectionProgram.uniforms.dyeTexelSize, this.dye.texelSizeX, this.dye.texelSizeY);
          this.gl.uniform1i(this.advectionProgram.uniforms.uVelocity, this.velocity.read.attach(0));
          this.gl.uniform1i(this.advectionProgram.uniforms.uSource, this.dye.read.attach(1));
          this.gl.uniform1f(this.advectionProgram.uniforms.dissipation, this.config.DENSITY_DISSIPATION);
          this.blit(this.dye.write);
          this.dye.swap();
        }

        render() {
          this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
          this.gl.enable(this.gl.BLEND);
          this.drawDisplay();
        }

        drawDisplay() {
          const width = this.gl.drawingBufferWidth;
          const height = this.gl.drawingBufferHeight;
          this.gl.useProgram(this.displayProgram.program);
          if (this.config.SHADING)
            this.gl.uniform2f(this.displayProgram.uniforms.texelSize, 1.0 / width, 1.0 / height);
          this.gl.uniform1i(this.displayProgram.uniforms.uTexture, this.dye.read.attach(0));
          this.blit(null);
        }

        splatPointer(pointer) {
          const dx = pointer.deltaX * this.config.SPLAT_FORCE;
          const dy = pointer.deltaY * this.config.SPLAT_FORCE;
          this.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
        }

        clickSplat(pointer) {
          const color = this.generateColor();
          color.r *= 10.0;
          color.g *= 10.0;
          color.b *= 10.0;
          const dx = 10 * (Math.random() - 0.5);
          const dy = 30 * (Math.random() - 0.5);
          this.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
        }

        splat(x, y, dx, dy, color) {
          this.gl.useProgram(this.splatProgram.program);
          this.gl.uniform1i(this.splatProgram.uniforms.uTarget, this.velocity.read.attach(0));
          this.gl.uniform1f(this.splatProgram.uniforms.aspectRatio, this.canvas.width / this.canvas.height);
          this.gl.uniform2f(this.splatProgram.uniforms.point, x, y);
          this.gl.uniform3f(this.splatProgram.uniforms.color, dx, dy, 0.0);
          this.gl.uniform1f(this.splatProgram.uniforms.radius, this.correctRadius(this.config.SPLAT_RADIUS / 100.0));
          this.blit(this.velocity.write);
          this.velocity.swap();

          this.gl.uniform1i(this.splatProgram.uniforms.uTarget, this.dye.read.attach(0));
          this.gl.uniform3f(this.splatProgram.uniforms.color, color.r, color.g, color.b);
          this.blit(this.dye.write);
          this.dye.swap();
        }

        correctRadius(radius) {
          const aspectRatio = this.canvas.width / this.canvas.height;
          if (aspectRatio > 1) radius *= aspectRatio;
          return radius;
        }

        updatePointerDownData(pointer, id, posX, posY) {
          pointer.id = id;
          pointer.down = true;
          pointer.moved = false;
          pointer.texcoordX = posX / this.canvas.width;
          pointer.texcoordY = 1.0 - posY / this.canvas.height;
          pointer.prevTexcoordX = pointer.texcoordX;
          pointer.prevTexcoordY = pointer.texcoordY;
          pointer.deltaX = 0;
          pointer.deltaY = 0;
          pointer.color = this.generateColor();
        }

        updatePointerMoveData(pointer, posX, posY, color) {
          pointer.prevTexcoordX = pointer.texcoordX;
          pointer.prevTexcoordY = pointer.texcoordY;
          pointer.texcoordX = posX / this.canvas.width;
          pointer.texcoordY = 1.0 - posY / this.canvas.height;
          pointer.deltaX = this.correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX);
          pointer.deltaY = this.correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY);
          pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
          pointer.color = color;
        }

        correctDeltaX(delta) {
          const aspectRatio = this.canvas.width / this.canvas.height;
          if (aspectRatio < 1) delta *= aspectRatio;
          return delta;
        }

        correctDeltaY(delta) {
          const aspectRatio = this.canvas.width / this.canvas.height;
          if (aspectRatio > 1) delta /= aspectRatio;
          return delta;
        }

        generateColor() {
          if (this.config.BASE_COLOR && this.config.BASE_COLOR !== 'random') {
            const baseColor = this.hexToRGB(this.config.BASE_COLOR);
            return {
              r: baseColor.r / 255 * 0.15,
              g: baseColor.g / 255 * 0.15,
              b: baseColor.b / 255 * 0.15
            };
          } else {
            const c = this.HSVtoRGB(Math.random(), 1.0, 1.0);
            c.r *= 0.15;
            c.g *= 0.15;
            c.b *= 0.15;
            return c;
          }
        }

        hexToRGB(hex) {
          const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
          return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
          } : { r: 255, g: 107, b: 53 };
        }

        HSVtoRGB(h, s, v) {
          let r, g, b, i, f, p, q, t;
          i = Math.floor(h * 6);
          f = h * 6 - i;
          p = v * (1 - s);
          q = v * (1 - f * s);
          t = v * (1 - (1 - f) * s);
          switch (i % 6) {
            case 0: r = v; g = t; b = p; break;
            case 1: r = q; g = v; b = p; break;
            case 2: r = p; g = v; b = t; break;
            case 3: r = p; g = q; b = v; break;
            case 4: r = t; g = p; b = v; break;
            case 5: r = v; g = p; b = q; break;
            default: break;
          }
          return { r, g, b };
        }

        wrap(value, min, max) {
          const range = max - min;
          if (range === 0) return min;
          return ((value - min) % range) + min;
        }

        updateBaseColor(newColor) {
          this.config.BASE_COLOR = newColor;
          this.pointers.forEach(pointer => {
            pointer.color = this.generateColor();
          });
        }

        destroy() {
          if (this.animationId) {
            cancelAnimationFrame(this.animationId);
          }
          if (this.cleanup) {
            this.cleanup();
          }
          if (this.canvas.parentNode) {
            this.canvas.parentNode.removeChild(this.canvas);
          }
        }
      }

      function hexToHsl(hex) {
        const r = parseInt(hex.slice(1, 3), 16) / 255;
        const g = parseInt(hex.slice(3, 5), 16) / 255;
        const b = parseInt(hex.slice(5, 7), 16) / 255;
        
        const max = Math.max(r, g, b);
        const min = Math.min(r, g, b);
        let h, s, l = (max + min) / 2;
        
        if (max === min) {
          h = s = 0;
        } else {
          const d = max - min;
          s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
          switch (max) {
            case r: h = (g - b) / d + (g < b ? 6 : 0); break;
            case g: h = (b - r) / d + 2; break;
            case b: h = (r - g) / d + 4; break;
          }
          h /= 6;
        }
        
        return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
      }

      function hslToHex(hsl) {
        const match = hsl.match(/hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/);
        if (!match) return null;
        
        let h = parseInt(match[1]) / 360;
        let s = parseInt(match[2]) / 100;
        let l = parseInt(match[3]) / 100;
        
        const hue2rgb = (p, q, t) => {
          if (t < 0) t += 1;
          if (t > 1) t -= 1;
          if (t < 1/6) return p + (q - p) * 6 * t;
          if (t < 1/2) return q;
          if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
          return p;
        };
        
        let r, g, b;
        if (s === 0) {
          r = g = b = l;
        } else {
          const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
          const p = 2 * l - q;
          r = hue2rgb(p, q, h + 1/3);
          g = hue2rgb(p, q, h);
          b = hue2rgb(p, q, h - 1/3);
        }
        
        const toHex = (c) => {
          const hex = Math.round(c * 255).toString(16);
          return hex.length === 1 ? '0' + hex : hex;
        };
        
        return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
      }

      function isValidHex(hex) {
        return /^#[0-9A-F]{6}$/i.test(hex);
      }

      function isValidHsl(hsl) {
        return /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/i.test(hsl);
      }

      function formatHex(value) {
        let hex = value.replace(/[^0-9A-Fa-f#]/g, '');
        
        if (!hex.startsWith('#')) {
          hex = '#' + hex;
        }
        
        if (hex.length > 7) {
          hex = hex.substring(0, 7);
        }
        
        return hex.toUpperCase();
      }

      function formatHsl(value) {
        const cleanValue = value.replace(/[^\d,\s]/g, '');
        const numbers = cleanValue.match(/\d+/g);
        
        if (!numbers || numbers.length < 3) {
          const partialMatch = value.match(/(\d+)/g);
          if (partialMatch && partialMatch.length >= 1) {
            const h = Math.min(360, Math.max(0, parseInt(partialMatch[0]) || 0));
            const s = Math.min(100, Math.max(0, parseInt(partialMatch[1]) || 50));
            const l = Math.min(100, Math.max(0, parseInt(partialMatch[2]) || 50));
            return `hsl(${h}, ${s}%, ${l}%)`;
          }
          return value;
        }
        
        let h = Math.min(360, Math.max(0, parseInt(numbers[0])));
        let s = Math.min(100, Math.max(0, parseInt(numbers[1])));
        let l = Math.min(100, Math.max(0, parseInt(numbers[2])));
        
        return `hsl(${h}, ${s}%, ${l}%)`;
      }

      function generateRandomColor() {
        return '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
      }

      function showNotification(message, type = 'success') {
        const notification = document.getElementById('notification');
        notification.textContent = message;
        notification.className = `notification ${type}`;
        
        notification.offsetHeight;
        
        notification.style.visibility = 'visible';
        notification.classList.add('show');
        
        setTimeout(() => {
          notification.classList.remove('show');
          
          setTimeout(() => {
            if (!notification.classList.contains('show')) {
              notification.style.visibility = 'hidden';
            }
          }, 400);
        }, 3000);
      }

      function copyToClipboard(text) {
  navigator.clipboard.writeText(text)
    .then(() => {
      showNotification('Copied to clipboard!');
    })
    .catch(err => {
      try {
        const textArea = document.createElement('textarea');
        textArea.value = text;
        textArea.style.position = 'fixed';
        textArea.style.opacity = '0';
        document.body.appendChild(textArea);
        textArea.select();
        document.execCommand('copy');
        document.body.removeChild(textArea);
        showNotification('Copied to clipboard!');
      } catch (fallbackErr) {
        showNotification('Failed to copy to clipboard', 'error');
      }
    });
}

      function generateUniqueId() {
        return Math.random().toString(36).substring(2, 8);
      }

      function generateFullSectionJSON() {
  // Generar IDs únicos para todos los elementos
  const sectionId = generateUniqueId();
  const containerId = generateUniqueId();
  const block1Id = generateUniqueId();
  const div1Id = generateUniqueId();
  const text1Id = generateUniqueId();
  const div2Id = generateUniqueId();
  const text2Id = generateUniqueId();
  const text3Id = generateUniqueId();
  const block2Id = generateUniqueId();
  const div3Id = generateUniqueId();
  const text4Id = generateUniqueId();
  const text5Id = generateUniqueId();
  const div4Id = generateUniqueId();
  const text6Id = generateUniqueId();
  const text7Id = generateUniqueId();
  const div5Id = generateUniqueId();
  const text8Id = generateUniqueId();
  const text9Id = generateUniqueId();
  const block3Id = generateUniqueId();
  const div6Id = generateUniqueId();
  const button1Id = generateUniqueId();
  const div7Id = generateUniqueId();
  const button2Id = generateUniqueId();
  const button3Id = generateUniqueId();
  const codeId = generateUniqueId();
  const attributeId = generateUniqueId();

  // Obtener el JavaScript actual con la configuración del usuario
  const jsCode = generateJavaScriptCode();

  // Crear el objeto JSON completo de Bricks Builder
  const bricksJSON = {
    "content": [
      {
        "id": sectionId,
        "name": "section",
        "parent": 0,
        "children": [containerId, codeId],
        "settings": {
          "_attributes": [{"id": attributeId, "name": "data-fluid-cursor"}],
          "_justifyContent": "center",
          "_background": {"color": {"hex": "#000000"}},
          "_padding": {"top": "50", "right": "20", "bottom": "50", "left": "20"},
          "_height:mobile_landscape": "auto"
        },
        "label": "CTA 011"
      },
      {
        "id": containerId,
        "name": "container",
        "parent": sectionId,
        "children": [block1Id, block2Id, block3Id],
        "settings": {"_rowGap": "50"}
      },
      {
        "id": block1Id,
        "name": "block",
        "parent": containerId,
        "children": [div1Id, div2Id],
        "settings": {"_rowGap": "20"}
      },
      {
        "id": div1Id,
        "name": "div",
        "parent": block1Id,
        "children": [text1Id],
        "settings": {
          "_padding": {"top": "0", "bottom": "0", "left": "10", "right": "10"},
          "_border": {
            "radius": {"top": "15", "right": "15", "bottom": "15", "left": "15"},
            "style": "solid",
            "width": {"top": "1", "right": "1", "bottom": "1", "left": "1"},
            "color": {"hex": "#00d17d", "rgb": "rgba(0, 209, 125, 0.3)", "hsl": "hsla(156, 100%, 41%, 0.3)"}
          }
        }
      },
      {
        "id": text1Id,
        "name": "text-basic",
        "parent": div1Id,
        "children": [],
        "settings": {
          "text": "INTERACTIVE ART\n",
          "tag": "span",
          "_typography": {"color": {"hex": "#00d17d"}, "font-size": "12", "font-weight": "400"}
        }
      },
      {
        "id": div2Id,
        "name": "div",
        "parent": block1Id,
        "children": [text2Id, text3Id],
        "settings": {
          "_display": "flex",
          "_direction": "column",
          "_rowGap": "20",
          "_width": "50%",
          "_width:tablet_portrait": "70%",
          "_width:mobile_landscape": "100%"
        }
      },
      {
        "id": text2Id,
        "name": "text-basic",
        "parent": div2Id,
        "children": [],
        "settings": {
          "text": "Paint with <span style=\"background:linear-gradient(90deg,orange,pink);-webkit-background-clip:text;color:transparent\">physics</span>\n",
          "tag": "span",
          "_typography": {"color": {"hex": "#ffffff"}, "font-weight": "300", "font-size": "40"}
        }
      },
      {
        "id": text3Id,
        "name": "text-basic",
        "parent": div2Id,
        "children": [],
        "settings": {
          "text": "Experience real-time fluid dynamics. Every movement creates ripples of color through space and time.\n",
          "tag": "p",
          "_typography": {"color": {"hex": "#ffffff"}, "font-weight": "300", "font-size": "18"}
        }
      },
      {
        "id": block2Id,
        "name": "block",
        "parent": containerId,
        "children": [div3Id, div4Id, div5Id],
        "settings": {"_direction": "row", "_columnGap": "50"}
      },
      {
        "id": div3Id,
        "name": "div",
        "parent": block2Id,
        "children": [text4Id, text5Id],
        "settings": {"_display": "flex", "_direction": "column", "_rowGap": "0"}
      },
      {
        "id": text4Id,
        "name": "text-basic",
        "parent": div3Id,
        "children": [],
        "settings": {
          "text": "60fps\n",
          "tag": "span",
          "_typography": {"color": {"hex": "#ffffff"}, "font-weight": "200", "font-size": "25"}
        }
      },
      {
        "id": text5Id,
        "name": "text-basic",
        "parent": div3Id,
        "children": [],
        "settings": {
          "text": "PERFORMANCE\n",
          "tag": "span",
          "_typography": {"color": {"hex": "#8f8f8f"}, "font-weight": "300", "font-size": "14"}
        }
      },
      {
        "id": div4Id,
        "name": "div",
        "parent": block2Id,
        "children": [text6Id, text7Id],
        "settings": {"_display": "flex", "_direction": "column", "_rowGap": "0"}
      },
      {
        "id": text6Id,
        "name": "text-basic",
        "parent": div4Id,
        "children": [],
        "settings": {
          "text": "∞\n",
          "tag": "span",
          "_typography": {"color": {"hex": "#ffffff"}, "font-weight": "200", "font-size": "25"}
        }
      },
      {
        "id": text7Id,
        "name": "text-basic",
        "parent": div4Id,
        "children": [],
        "settings": {
          "text": "POSSIBILITIES\n\n",
          "tag": "span",
          "_typography": {"color": {"hex": "#8f8f8f"}, "font-weight": "300", "font-size": "14"}
        }
      },
      {
        "id": div5Id,
        "name": "div",
        "parent": block2Id,
        "children": [text8Id, text9Id],
        "settings": {"_display": "flex", "_direction": "column", "_rowGap": "0"}
      },
      {
        "id": text8Id,
        "name": "text-basic",
        "parent": div5Id,
        "children": [],
        "settings": {
          "text": "2D\n",
          "tag": "span",
          "_typography": {"color": {"hex": "#ffffff"}, "font-weight": "200", "font-size": "25"}
        }
      },
      {
        "id": text9Id,
        "name": "text-basic",
        "parent": div5Id,
        "children": [],
        "settings": {
          "text": "SIMULATION\n",
          "tag": "span",
          "_typography": {"color": {"hex": "#8f8f8f"}, "font-weight": "300", "font-size": "14"}
        }
      },
      {
        "id": block3Id,
        "name": "block",
        "parent": containerId,
        "children": [div6Id, div7Id],
        "settings": {"_rowGap": "15"}
      },
      {
        "id": div6Id,
        "name": "div",
        "parent": block3Id,
        "children": [button1Id],
        "settings": {"_width": "100%"}
      },
      {
        "id": button1Id,
        "name": "button",
        "parent": div6Id,
        "children": [],
        "settings": {
          "text": "Experiencie Now",
          "_padding": {"top": "15", "right": "15", "bottom": "15", "left": "15"},
          "_width": "100%",
          "_typography": {"color": {"hex": "#000000"}, "font-size": "16", "font-weight": "500"},
          "_background": {"color": {"hex": "#ffffff"}},
          "_border": {"radius": {"top": "10", "right": "10", "bottom": "10", "left": "10"}}
        }
      },
      {
        "id": div7Id,
        "name": "div",
        "parent": block3Id,
        "children": [button2Id, button3Id],
        "settings": {
          "_width": "100%",
          "_display": "flex",
          "_direction": "row",
          "_columnGap": "15"
        }
      },
      {
        "id": button2Id,
        "name": "button",
        "parent": div7Id,
        "children": [],
        "settings": {
          "text": "Demo",
          "_padding": {"top": "15", "right": "15", "bottom": "15", "left": "15"},
          "_width": "100%",
          "_typography": {"color": {"hex": "#ffffff"}, "font-size": "15", "font-weight": "400"},
          "_background": {"color": {"hex": "#000000"}},
          "_border": {
            "radius": {"top": "10", "right": "10", "bottom": "10", "left": "10"},
            "color": {"hex": "#8f8f8f"},
            "style": "solid",
            "width": {"top": "1", "right": "1", "bottom": "1", "left": "1"}
          },
          "icon": {"library": "ionicons", "icon": "ion-ios-play"},
          "iconPosition": "left",
          "iconTypography": {"font-size": "25"}
        }
      },
      {
        "id": button3Id,
        "name": "button",
        "parent": div7Id,
        "children": [],
        "settings": {
          "_padding": {"top": "15", "right": "15", "bottom": "15", "left": "15"},
          "_width": "100%",
          "_typography": {"color": {"hex": "#ffffff"}, "font-size": "15", "font-weight": "400"},
          "_background": {"color": {"hex": "#000000"}},
          "_border": {
            "radius": {"top": "10", "right": "10", "bottom": "10", "left": "10"},
            "color": {"hex": "#8f8f8f"},
            "style": "solid",
            "width": {"top": "1", "right": "1", "bottom": "1", "left": "1"}
          },
          "icon": {"library": "ionicons", "icon": "ion-md-download"},
          "iconPosition": "left",
          "iconTypography": {"font-size": "25"},
          "text": "Code"
        }
      },
      {
        "id": codeId,
        "name": "code",
        "parent": sectionId,
        "children": [],
        "settings": {
          "executeCode": true,
          "_display": "none",
          "javascriptCode": jsCode
        },
        "label": "Fluid Cursor JS"
      }
    ],
    "source": "bricksCopiedElements",
    "sourceUrl": "https://test.bricksfusion.com",
    "version": "2.0.1",
    "globalClasses": [],
    "globalElements": []
  };

  return JSON.stringify(bricksJSON, null, 2);
}

function copyFullSectionToClipboard() {
  const fullSectionJSON = generateFullSectionJSON();
  
  navigator.clipboard.writeText(fullSectionJSON)
    .then(() => {
      showNotification('Full Bricks section JSON copied to clipboard!');
    })
    .catch(err => {
      try {
        const textArea = document.createElement('textarea');
        textArea.value = fullSectionJSON;
        textArea.style.position = 'fixed';
        textArea.style.opacity = '0';
        document.body.appendChild(textArea);
        textArea.select();
        document.execCommand('copy');
        document.body.removeChild(textArea);
        showNotification('Full Bricks section JSON copied to clipboard!');
      } catch (fallbackErr) {
        showNotification('Failed to copy to clipboard. Please try again.', 'error');
      }
    });
}

      // Replace this function in your HTML file (around line 2456)
function generateJavaScriptCode() {
  const config = fluidConfig;
  
  return `(function() {
  const defaultConfig = {
    simResolution: ${config.simResolution},
    dyeResolution: ${config.dyeResolution},
    densityDissipation: ${config.densityDissipation},
    velocityDissipation: ${config.velocityDissipation},
    pressure: ${config.pressure},
    pressureIterations: ${config.pressureIterations},
    curl: ${config.curl},
    splatRadius: ${config.splatRadius},
    splatForce: ${config.splatForce},
    shading: ${config.shading},
    colorUpdateSpeed: ${config.colorUpdateSpeed},
    transparent: ${config.transparent},
    baseColor: '${config.baseColor}',
    mobileTouch: '${config.mobileTouch}'
  };
  
  function initFluidCursor() {
    const sections = document.querySelectorAll('[data-fluid-cursor]:not([data-fluid-initialized="true"])');
    
    sections.forEach((section) => {
      const options = {
        simResolution: parseInt(section.getAttribute('data-sim-resolution')) || defaultConfig.simResolution,
        dyeResolution: parseInt(section.getAttribute('data-dye-resolution')) || defaultConfig.dyeResolution,
        densityDissipation: parseFloat(section.getAttribute('data-density-dissipation')) || defaultConfig.densityDissipation,
        velocityDissipation: parseFloat(section.getAttribute('data-velocity-dissipation')) || defaultConfig.velocityDissipation,
        pressure: parseFloat(section.getAttribute('data-pressure')) || defaultConfig.pressure,
        pressureIterations: parseInt(section.getAttribute('data-pressure-iterations')) || defaultConfig.pressureIterations,
        curl: parseInt(section.getAttribute('data-curl')) || defaultConfig.curl,
        splatRadius: parseFloat(section.getAttribute('data-splat-radius')) || defaultConfig.splatRadius,
        splatForce: parseInt(section.getAttribute('data-splat-force')) || defaultConfig.splatForce,
        shading: section.getAttribute('data-shading') !== 'false' && defaultConfig.shading,
        colorUpdateSpeed: parseInt(section.getAttribute('data-color-update-speed')) || defaultConfig.colorUpdateSpeed,
        transparent: section.getAttribute('data-transparent') !== 'false' && defaultConfig.transparent,
        baseColor: section.getAttribute('data-base-color') || defaultConfig.baseColor,
        mobileTouch: section.getAttribute('data-mobile-touch') || defaultConfig.mobileTouch
      };
      
      const fluidCursor = new FluidCursor(section, options);
      section.dataset.fluidInitialized = 'true';
    });
  }
  
  class FluidCursor {
    constructor(container, options = {}) {
      this.container = container;
      this.canvas = document.createElement('canvas');
      
      this.config = {
        SIM_RESOLUTION: options.simResolution || 128,
        DYE_RESOLUTION: options.dyeResolution || 1024,
        DENSITY_DISSIPATION: options.densityDissipation || 0.8,
        VELOCITY_DISSIPATION: options.velocityDissipation || 0.2,
        PRESSURE: options.pressure || 0.43,
        PRESSURE_ITERATIONS: options.pressureIterations || 26,
        CURL: options.curl || 25,
        SPLAT_RADIUS: options.splatRadius || 0.45,
        SPLAT_FORCE: options.splatForce || 13000,
        SHADING: options.shading !== false,
        COLOR_UPDATE_SPEED: options.colorUpdateSpeed || 10,
        TRANSPARENT: options.transparent !== false,
        BASE_COLOR: options.baseColor || 'random',
        MOBILE_TOUCH: options.mobileTouch || 'scroll',
        PAUSED: false
      };

      this.pointers = [this.createPointer()];
      this.animationId = null;
      this.lastUpdateTime = Date.now();
      this.colorUpdateTimer = 0.0;
      this.lastTapTime = 0;
      this.tapCooldown = 100;
      
      this.setupCanvas();
      this.initWebGL();
    }

    createPointer() {
      return {
        id: -1,
        texcoordX: 0,
        texcoordY: 0,
        prevTexcoordX: 0,
        prevTexcoordY: 0,
        deltaX: 0,
        deltaY: 0,
        down: false,
        moved: false,
        color: [0, 0, 0]
      };
    }

    setupCanvas() {
      this.canvas.style.position = 'absolute';
      this.canvas.style.top = '0';
      this.canvas.style.left = '0';
      this.canvas.style.width = '100%';
      this.canvas.style.height = '100%';
      this.canvas.style.pointerEvents = 'none';
      this.canvas.style.zIndex = '1';

      this.container.style.position = this.container.style.position || 'relative';
      this.container.style.overflow = 'hidden';
      this.container.appendChild(this.canvas);
    }

    initWebGL() {
      const params = {
        alpha: true,
        depth: false,
        stencil: false,
        antialias: false,
        preserveDrawingBuffer: false,
      };

      this.gl = this.canvas.getContext("webgl2", params) ||
               this.canvas.getContext("webgl", params) ||
               this.canvas.getContext("experimental-webgl", params);

      if (!this.gl) {
        return;
      }

      this.isWebGL2 = !!this.canvas.getContext("webgl2", params);
      this.setupWebGLExtensions();
      this.createShaders();
      this.createPrograms();
      this.initFramebuffers();
      this.setupEventListeners();
      this.updateFrame();
    }

    setupWebGLExtensions() {
      let halfFloat, supportLinearFiltering;
      
      if (this.isWebGL2) {
        this.gl.getExtension("EXT_color_buffer_float");
        supportLinearFiltering = this.gl.getExtension("OES_texture_float_linear");
      } else {
        halfFloat = this.gl.getExtension("OES_texture_half_float");
        supportLinearFiltering = this.gl.getExtension("OES_texture_half_float_linear");
      }

      this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
      const halfFloatTexType = this.isWebGL2 ? this.gl.HALF_FLOAT : (halfFloat && halfFloat.HALF_FLOAT_OES);

      let formatRGBA, formatRG, formatR;

      if (this.isWebGL2) {
        formatRGBA = this.getSupportedFormat(this.gl.RGBA16F, this.gl.RGBA, halfFloatTexType);
        formatRG = this.getSupportedFormat(this.gl.RG16F, this.gl.RG, halfFloatTexType);
        formatR = this.getSupportedFormat(this.gl.R16F, this.gl.RED, halfFloatTexType);
      } else {
        formatRGBA = this.getSupportedFormat(this.gl.RGBA, this.gl.RGBA, halfFloatTexType);
        formatRG = this.getSupportedFormat(this.gl.RGBA, this.gl.RGBA, halfFloatTexType);
        formatR = this.getSupportedFormat(this.gl.RGBA, this.gl.RGBA, halfFloatTexType);
      }

      this.ext = {
        formatRGBA,
        formatRG,
        formatR,
        halfFloatTexType,
        supportLinearFiltering: !!supportLinearFiltering
      };

      if (!this.ext.supportLinearFiltering) {
        this.config.DYE_RESOLUTION = 256;
        this.config.SHADING = false;
      }
    }

    getSupportedFormat(internalFormat, format, type) {
      if (!this.supportRenderTextureFormat(internalFormat, format, type)) {
        switch (internalFormat) {
          case this.gl.R16F:
            return this.getSupportedFormat(this.gl.RG16F, this.gl.RG, type);
          case this.gl.RG16F:
            return this.getSupportedFormat(this.gl.RGBA16F, this.gl.RGBA, type);
          default:
            return null;
        }
      }
      return { internalFormat, format };
    }

    supportRenderTextureFormat(internalFormat, format, type) {
      const texture = this.gl.createTexture();
      this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
      this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
      this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
      this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
      this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
      this.gl.texImage2D(this.gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null);
      
      const fbo = this.gl.createFramebuffer();
      this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fbo);
      this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
      const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
      return status === this.gl.FRAMEBUFFER_COMPLETE;
    }

    createShaders() {
      const baseVertexShaderSource = \`
        precision highp float;
        attribute vec2 aPosition;
        varying vec2 vUv;
        varying vec2 vL;
        varying vec2 vR;
        varying vec2 vT;
        varying vec2 vB;
        uniform vec2 texelSize;
        void main () {
          vUv = aPosition * 0.5 + 0.5;
          vL = vUv - vec2(texelSize.x, 0.0);
          vR = vUv + vec2(texelSize.x, 0.0);
          vT = vUv + vec2(0.0, texelSize.y);
          vB = vUv - vec2(0.0, texelSize.y);
          gl_Position = vec4(aPosition, 0.0, 1.0);
        }
      \`;

      this.baseVertexShader = this.compileShader(this.gl.VERTEX_SHADER, baseVertexShaderSource);
      this.createFragmentShaders();
    }

    createFragmentShaders() {
      this.copyShader = this.compileShader(this.gl.FRAGMENT_SHADER, \`
        precision mediump float;
        precision mediump sampler2D;
        varying highp vec2 vUv;
        uniform sampler2D uTexture;
        void main () {
          gl_FragColor = texture2D(uTexture, vUv);
        }
      \`);

      this.clearShader = this.compileShader(this.gl.FRAGMENT_SHADER, \`
        precision mediump float;
        precision mediump sampler2D;
        varying highp vec2 vUv;
        uniform sampler2D uTexture;
        uniform float value;
        void main () {
          gl_FragColor = value * texture2D(uTexture, vUv);
        }
      \`);

      this.splatShader = this.compileShader(this.gl.FRAGMENT_SHADER, \`
        precision highp float;
        precision highp sampler2D;
        varying vec2 vUv;
        uniform sampler2D uTarget;
        uniform float aspectRatio;
        uniform vec3 color;
        uniform vec2 point;
        uniform float radius;
        void main () {
          vec2 p = vUv - point.xy;
          p.x *= aspectRatio;
          vec3 splat = exp(-dot(p, p) / radius) * color;
          vec3 base = texture2D(uTarget, vUv).xyz;
          gl_FragColor = vec4(base + splat, 1.0);
        }
      \`);

      this.advectionShader = this.compileShader(this.gl.FRAGMENT_SHADER, \`
        precision highp float;
        precision highp sampler2D;
        varying vec2 vUv;
        uniform sampler2D uVelocity;
        uniform sampler2D uSource;
        uniform vec2 texelSize;
        uniform vec2 dyeTexelSize;
        uniform float dt;
        uniform float dissipation;
        \${!this.ext.supportLinearFiltering ? \`
        vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) {
          vec2 st = uv / tsize - 0.5;
          vec2 iuv = floor(st);
          vec2 fuv = fract(st);
          vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize);
          vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize);
          vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize);
          vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize);
          return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y);
        }
        \` : ''}
        void main () {
          \${!this.ext.supportLinearFiltering ? \`
            vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize;
            vec4 result = bilerp(uSource, coord, dyeTexelSize);
          \` : \`
            vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize;
            vec4 result = texture2D(uSource, coord);
          \`}
          float decay = 1.0 + dissipation * dt;
          gl_FragColor = result / decay;
        }
      \`);

      this.displayShaderSource = \`
        precision highp float;
        precision highp sampler2D;
        varying vec2 vUv;
        varying vec2 vL;
        varying vec2 vR;
        varying vec2 vT;
        varying vec2 vB;
        uniform sampler2D uTexture;
        uniform vec2 texelSize;
        \${this.config.SHADING ? \`
        vec3 linearToGamma (vec3 color) {
          color = max(color, vec3(0));
          return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0));
        }
        \` : ''}
        void main () {
          vec3 c = texture2D(uTexture, vUv).rgb;
          \${this.config.SHADING ? \`
            vec3 lc = texture2D(uTexture, vL).rgb;
            vec3 rc = texture2D(uTexture, vR).rgb;
            vec3 tc = texture2D(uTexture, vT).rgb;
            vec3 bc = texture2D(uTexture, vB).rgb;
            float dx = length(rc) - length(lc);
            float dy = length(tc) - length(bc);
            vec3 n = normalize(vec3(dx, dy, length(texelSize)));
            vec3 l = vec3(0.0, 0.0, 1.0);
            float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0);
            c *= diffuse;
          \` : ''}
          float a = max(c.r, max(c.g, c.b));
          gl_FragColor = vec4(c, a);
        }
      \`;

      this.createSimulationShaders();
    }

    createSimulationShaders() {
      this.divergenceShader = this.compileShader(this.gl.FRAGMENT_SHADER, \`
        precision mediump float;
        precision mediump sampler2D;
        varying highp vec2 vUv;
        varying highp vec2 vL;
        varying highp vec2 vR;
        varying highp vec2 vT;
        varying highp vec2 vB;
        uniform sampler2D uVelocity;
        void main () {
          float L = texture2D(uVelocity, vL).x;
          float R = texture2D(uVelocity, vR).x;
          float T = texture2D(uVelocity, vT).y;
          float B = texture2D(uVelocity, vB).y;
          vec2 C = texture2D(uVelocity, vUv).xy;
          if (vL.x < 0.0) { L = -C.x; }
          if (vR.x > 1.0) { R = -C.x; }
          if (vT.y > 1.0) { T = -C.y; }
          if (vB.y < 0.0) { B = -C.y; }
          float div = 0.5 * (R - L + T - B);
          gl_FragColor = vec4(div, 0.0, 0.0, 1.0);
        }
      \`);

      this.curlShader = this.compileShader(this.gl.FRAGMENT_SHADER, \`
        precision mediump float;
        precision mediump sampler2D;
        varying highp vec2 vUv;
        varying highp vec2 vL;
        varying highp vec2 vR;
        varying highp vec2 vT;
        varying highp vec2 vB;
        uniform sampler2D uVelocity;
        void main () {
          float L = texture2D(uVelocity, vL).y;
          float R = texture2D(uVelocity, vR).y;
          float T = texture2D(uVelocity, vT).x;
          float B = texture2D(uVelocity, vB).x;
          float vorticity = R - L - T + B;
          gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
        }
      \`);

      this.vorticityShader = this.compileShader(this.gl.FRAGMENT_SHADER, \`
        precision highp float;
        precision highp sampler2D;
        varying vec2 vUv;
        varying vec2 vL;
        varying vec2 vR;
        varying vec2 vT;
        varying vec2 vB;
        uniform sampler2D uVelocity;
        uniform sampler2D uCurl;
        uniform float curl;
        uniform float dt;
        void main () {
          float L = texture2D(uCurl, vL).x;
          float R = texture2D(uCurl, vR).x;
          float T = texture2D(uCurl, vT).x;
          float B = texture2D(uCurl, vB).x;
          float C = texture2D(uCurl, vUv).x;
          vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
          force /= length(force) + 0.0001;
          force *= curl * C;
          force.y *= -1.0;
          vec2 velocity = texture2D(uVelocity, vUv).xy;
          velocity += force * dt;
          velocity = min(max(velocity, -1000.0), 1000.0);
          gl_FragColor = vec4(velocity, 0.0, 1.0);
        }
      \`);

      this.pressureShader = this.compileShader(this.gl.FRAGMENT_SHADER, \`
        precision mediump float;
        precision mediump sampler2D;
        varying highp vec2 vUv;
        varying highp vec2 vL;
        varying highp vec2 vR;
        varying highp vec2 vT;
        varying highp vec2 vB;
        uniform sampler2D uPressure;
        uniform sampler2D uDivergence;
        void main () {
          float L = texture2D(uPressure, vL).x;
          float R = texture2D(uPressure, vR).x;
          float T = texture2D(uPressure, vT).x;
          float B = texture2D(uPressure, vB).x;
          float C = texture2D(uPressure, vUv).x;
          float divergence = texture2D(uDivergence, vUv).x;
          float pressure = (L + R + B + T - divergence) * 0.25;
          gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0);
        }
      \`);

      this.gradientSubtractShader = this.compileShader(this.gl.FRAGMENT_SHADER, \`
        precision mediump float;
        precision mediump sampler2D;
        varying highp vec2 vUv;
        varying highp vec2 vL;
        varying highp vec2 vR;
        varying highp vec2 vT;
        varying highp vec2 vB;
        uniform sampler2D uPressure;
        uniform sampler2D uVelocity;
        void main () {
          float L = texture2D(uPressure, vL).x;
          float R = texture2D(uPressure, vR).x;
          float T = texture2D(uPressure, vT).x;
          float B = texture2D(uPressure, vB).x;
          vec2 velocity = texture2D(uVelocity, vUv).xy;
          velocity.xy -= vec2(R - L, T - B);
          gl_FragColor = vec4(velocity, 0.0, 1.0);
        }
      \`);
    }

    compileShader(type, source) {
      const shader = this.gl.createShader(type);
      this.gl.shaderSource(shader, source);
      this.gl.compileShader(shader);
      if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
        this.gl.deleteShader(shader);
        return null;
      }
      return shader;
    }

    createProgram(vertexShader, fragmentShader) {
      const program = this.gl.createProgram();
      this.gl.attachShader(program, vertexShader);
      this.gl.attachShader(program, fragmentShader);
      this.gl.linkProgram(program);
      if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
        return null;
      }
      return program;
    }

    getUniforms(program) {
      const uniforms = {};
      const uniformCount = this.gl.getProgramParameter(program, this.gl.ACTIVE_UNIFORMS);
      for (let i = 0; i < uniformCount; i++) {
        const uniformName = this.gl.getActiveUniform(program, i).name;
        uniforms[uniformName] = this.gl.getUniformLocation(program, uniformName);
      }
      return uniforms;
    }

    createPrograms() {
      this.copyProgram = {
        program: this.createProgram(this.baseVertexShader, this.copyShader),
        uniforms: {}
      };
      this.copyProgram.uniforms = this.getUniforms(this.copyProgram.program);

      this.clearProgram = {
        program: this.createProgram(this.baseVertexShader, this.clearShader),
        uniforms: {}
      };
      this.clearProgram.uniforms = this.getUniforms(this.clearProgram.program);

      this.splatProgram = {
        program: this.createProgram(this.baseVertexShader, this.splatShader),
        uniforms: {}
      };
      this.splatProgram.uniforms = this.getUniforms(this.splatProgram.program);

      this.advectionProgram = {
        program: this.createProgram(this.baseVertexShader, this.advectionShader),
        uniforms: {}
      };
      this.advectionProgram.uniforms = this.getUniforms(this.advectionProgram.program);

      this.divergenceProgram = {
        program: this.createProgram(this.baseVertexShader, this.divergenceShader),
        uniforms: {}
      };
      this.divergenceProgram.uniforms = this.getUniforms(this.divergenceProgram.program);

      this.curlProgram = {
        program: this.createProgram(this.baseVertexShader, this.curlShader),
        uniforms: {}
      };
      this.curlProgram.uniforms = this.getUniforms(this.curlProgram.program);

      this.vorticityProgram = {
        program: this.createProgram(this.baseVertexShader, this.vorticityShader),
        uniforms: {}
      };
      this.vorticityProgram.uniforms = this.getUniforms(this.vorticityProgram.program);

      this.pressureProgram = {
        program: this.createProgram(this.baseVertexShader, this.pressureShader),
        uniforms: {}
      };
      this.pressureProgram.uniforms = this.getUniforms(this.pressureProgram.program);

      this.gradientSubtractProgram = {
        program: this.createProgram(this.baseVertexShader, this.gradientSubtractShader),
        uniforms: {}
      };
      this.gradientSubtractProgram.uniforms = this.getUniforms(this.gradientSubtractProgram.program);

      const displayShader = this.compileShader(this.gl.FRAGMENT_SHADER, this.displayShaderSource);
      this.displayProgram = {
        program: this.createProgram(this.baseVertexShader, displayShader),
        uniforms: {}
      };
      this.displayProgram.uniforms = this.getUniforms(this.displayProgram.program);

      this.setupGeometry();
    }

    setupGeometry() {
      this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.gl.createBuffer());
      this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), this.gl.STATIC_DRAW);
      this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.gl.createBuffer());
      this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), this.gl.STATIC_DRAW);
      this.gl.vertexAttribPointer(0, 2, this.gl.FLOAT, false, 0, 0);
      this.gl.enableVertexAttribArray(0);
    }

    blit(target, clear = false) {
      if (target == null) {
        this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);
        this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
      } else {
        this.gl.viewport(0, 0, target.width, target.height);
        this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, target.fbo);
      }
      if (clear) {
        this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
        this.gl.clear(this.gl.COLOR_BUFFER_BIT);
      }
      this.gl.drawElements(this.gl.TRIANGLES, 6, this.gl.UNSIGNED_SHORT, 0);
    }

    initFramebuffers() {
      const simRes = this.getResolution(this.config.SIM_RESOLUTION);
      const dyeRes = this.getResolution(this.config.DYE_RESOLUTION);
      const texType = this.ext.halfFloatTexType;
      const rgba = this.ext.formatRGBA;
      const rg = this.ext.formatRG;
      const r = this.ext.formatR;
      const filtering = this.ext.supportLinearFiltering ? this.gl.LINEAR : this.gl.NEAREST;

      this.gl.disable(this.gl.BLEND);

      this.dye = this.createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering);
      this.velocity = this.createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering);
      this.divergence = this.createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, this.gl.NEAREST);
      this.curl = this.createFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, this.gl.NEAREST);
      this.pressure = this.createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, this.gl.NEAREST);
    }

    createFBO(w, h, internalFormat, format, type, param) {
      this.gl.activeTexture(this.gl.TEXTURE0);
      const texture = this.gl.createTexture();
      this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
      this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, param);
      this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, param);
      this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
      this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
      this.gl.texImage2D(this.gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null);

      const fbo = this.gl.createFramebuffer();
      this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fbo);
      this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
      this.gl.viewport(0, 0, w, h);
      this.gl.clear(this.gl.COLOR_BUFFER_BIT);

      return {
        texture,
        fbo,
        width: w,
        height: h,
        texelSizeX: 1.0 / w,
        texelSizeY: 1.0 / h,
        attach: (id) => {
          this.gl.activeTexture(this.gl.TEXTURE0 + id);
          this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
          return id;
        }
      };
    }

    createDoubleFBO(w, h, internalFormat, format, type, param) {
      let fbo1 = this.createFBO(w, h, internalFormat, format, type, param);
      let fbo2 = this.createFBO(w, h, internalFormat, format, type, param);
      
      return {
        width: w,
        height: h,
        texelSizeX: fbo1.texelSizeX,
        texelSizeY: fbo1.texelSizeY,
        get read() { return fbo1; },
        set read(value) { fbo1 = value; },
        get write() { return fbo2; },
        set write(value) { fbo2 = value; },
        swap: () => {
          const temp = fbo1;
          fbo1 = fbo2;
          fbo2 = temp;
        }
      };
    }

    getResolution(resolution) {
      let aspectRatio = this.gl.drawingBufferWidth / this.gl.drawingBufferHeight;
      if (aspectRatio < 1) aspectRatio = 1.0 / aspectRatio;
      const min = Math.round(resolution);
      const max = Math.round(resolution * aspectRatio);
      if (this.gl.drawingBufferWidth > this.gl.drawingBufferHeight)
        return { width: max, height: min };
      else return { width: min, height: max };
    }

    setupEventListeners() {
      const getPointerData = (e) => {
        const rect = this.container.getBoundingClientRect();
        const scaleX = this.canvas.width / rect.width;
        const scaleY = this.canvas.height / rect.height;
        const posX = (e.clientX - rect.left) * scaleX;
        const posY = (e.clientY - rect.top) * scaleY;
        return { posX, posY };
      };

      const getTouchData = (e) => {
        const rect = this.container.getBoundingClientRect();
        const scaleX = this.canvas.width / rect.width;
        const scaleY = this.canvas.height / rect.height;
        const touch = e.touches[0] || e.changedTouches[0];
        const posX = (touch.clientX - rect.left) * scaleX;
        const posY = (touch.clientY - rect.top) * scaleY;
        return { posX, posY };
      };

      const handleMouseMove = (e) => {
        const { posX, posY } = getPointerData(e);
        this.updatePointerMoveData(this.pointers[0], posX, posY, this.pointers[0].color);
      };

      const handleMouseDown = (e) => {
        const now = Date.now();
        if (now - this.lastTapTime < this.tapCooldown) return;
        this.lastTapTime = now;
        
        const { posX, posY } = getPointerData(e);
        this.updatePointerDownData(this.pointers[0], -1, posX, posY);
        
        if (this.config.MOBILE_TOUCH === 'scroll') {
          this.scrollFriendlyTap(this.pointers[0]);
        } else if (this.config.MOBILE_TOUCH === 'smooth') {
          this.smoothTapInteraction(posX, posY);
        } else if (this.config.MOBILE_TOUCH === 'gentle') {
          this.gentleTapInteraction(this.pointers[0]);
        } else if (this.config.MOBILE_TOUCH !== 'disabled') {
          this.clickSplat(this.pointers[0]);
        }
      };

      const handleTouchStart = (e) => {
        if (this.config.MOBILE_TOUCH !== 'gentle' && this.config.MOBILE_TOUCH !== 'smooth') return;
        
        e.preventDefault();
        const now = Date.now();
        if (now - this.lastTapTime < this.tapCooldown) return;
        this.lastTapTime = now;
        
        const { posX, posY } = getTouchData(e);
        this.updatePointerDownData(this.pointers[0], -1, posX, posY);
        
        if (this.config.MOBILE_TOUCH === 'smooth') {
          this.smoothTapInteraction(posX, posY);
        } else if (this.config.MOBILE_TOUCH === 'gentle') {
          this.gentleTapInteraction(this.pointers[0]);
        }
      };

      const handleTouchMove = (e) => {
        if (this.config.MOBILE_TOUCH !== 'gentle' && this.config.MOBILE_TOUCH !== 'smooth') return;
        
        e.preventDefault();
        const { posX, posY } = getTouchData(e);
        this.updatePointerMoveData(this.pointers[0], posX, posY, this.pointers[0].color);
      };

      const handleTouchEnd = (e) => {
        if (this.config.MOBILE_TOUCH !== 'gentle' && this.config.MOBILE_TOUCH !== 'smooth') return;
        
        e.preventDefault();
        this.pointers[0].down = false;
      };

      const handleResize = () => {
        this.resizeCanvas();
      };

      this.container.addEventListener('mousemove', handleMouseMove);
      this.container.addEventListener('mousedown', handleMouseDown);

      if (this.config.MOBILE_TOUCH === 'gentle' || this.config.MOBILE_TOUCH === 'smooth') {
        this.container.addEventListener('touchstart', handleTouchStart, { passive: false });
        this.container.addEventListener('touchmove', handleTouchMove, { passive: false });
        this.container.addEventListener('touchend', handleTouchEnd, { passive: false });
      }

      window.addEventListener('resize', handleResize);

      this.cleanup = () => {
        this.container.removeEventListener('mousemove', handleMouseMove);
        this.container.removeEventListener('mousedown', handleMouseDown);
        if (this.config.MOBILE_TOUCH === 'gentle' || this.config.MOBILE_TOUCH === 'smooth') {
          this.container.removeEventListener('touchstart', handleTouchStart);
          this.container.removeEventListener('touchmove', handleTouchMove);
          this.container.removeEventListener('touchend', handleTouchEnd);
        }
        window.removeEventListener('resize', handleResize);
      };
    }

    scrollFriendlyTap(pointer) {
      const color = this.generateColor();
      color.r *= 1.5;
      color.g *= 1.5;
      color.b *= 1.5;
      const dx = 3 * (Math.random() - 0.5);
      const dy = 8 * (Math.random() - 0.5);
      this.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
    }

    smoothTapInteraction(x, y) {
      const steps = 5;
      const baseColor = this.generateColor();
      
      for (let i = 0; i < steps; i++) {
        setTimeout(() => {
          const progress = i / (steps - 1);
          const offsetX = (Math.random() - 0.5) * 0.02 * progress;
          const offsetY = (Math.random() - 0.5) * 0.02 * progress;
          
          const currentX = (x / this.canvas.width) + offsetX;
          const currentY = 1.0 - ((y / this.canvas.height) + offsetY);
          
          const dx = 3 * (Math.random() - 0.5) * (1 - progress * 0.5);
          const dy = 8 * (Math.random() - 0.5) * (1 - progress * 0.5);
          
          const intensity = 1.5 * (1 - progress * 0.3);
          const color = {
            r: baseColor.r * intensity,
            g: baseColor.g * intensity,
            b: baseColor.b * intensity
          };
          
          this.splat(currentX, currentY, dx, dy, color);
        }, i * 16);
      }
    }

    gentleTapInteraction(pointer) {
      const color = this.generateColor();
      color.r *= 4.0;
      color.g *= 4.0;
      color.b *= 4.0;
      const dx = 8 * (Math.random() - 0.5);
      const dy = 20 * (Math.random() - 0.5);
      this.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
    }

    resizeCanvas() {
      const rect = this.container.getBoundingClientRect();
      const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
      const width = Math.floor(rect.width * pixelRatio);
      const height = Math.floor(rect.height * pixelRatio);
      
      if (this.canvas.width !== width || this.canvas.height !== height) {
        this.canvas.width = width;
        this.canvas.height = height;
        this.initFramebuffers();
        return true;
      }
      return false;
    }

    updateFrame = () => {
      const dt = this.calcDeltaTime();
      if (this.resizeCanvas()) this.initFramebuffers();
      this.updateColors(dt);
      this.applyInputs();
      this.step(dt);
      this.render();
      this.animationId = requestAnimationFrame(this.updateFrame);
    };

    calcDeltaTime() {
      const now = Date.now();
      const dt = Math.min((now - this.lastUpdateTime) / 1000, 0.016666);
      this.lastUpdateTime = now;
      return dt;
    }

    updateColors(dt) {
      this.colorUpdateTimer += dt * this.config.COLOR_UPDATE_SPEED;
      if (this.colorUpdateTimer >= 1) {
        this.colorUpdateTimer = this.wrap(this.colorUpdateTimer, 0, 1);
        this.pointers.forEach(p => {
          p.color = this.generateColor();
        });
      }
    }

    applyInputs() {
      this.pointers.forEach(p => {
        if (p.moved) {
          p.moved = false;
          this.splatPointer(p);
        }
      });
    }

    step(dt) {
      this.gl.disable(this.gl.BLEND);

      this.gl.useProgram(this.curlProgram.program);
      this.gl.uniform2f(this.curlProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
      this.gl.uniform1i(this.curlProgram.uniforms.uVelocity, this.velocity.read.attach(0));
      this.blit(this.curl);

      this.gl.useProgram(this.vorticityProgram.program);
      this.gl.uniform2f(this.vorticityProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
      this.gl.uniform1i(this.vorticityProgram.uniforms.uVelocity, this.velocity.read.attach(0));
      this.gl.uniform1i(this.vorticityProgram.uniforms.uCurl, this.curl.attach(1));
      this.gl.uniform1f(this.vorticityProgram.uniforms.curl, this.config.CURL);
      this.gl.uniform1f(this.vorticityProgram.uniforms.dt, dt);
      this.blit(this.velocity.write);
      this.velocity.swap();

      this.gl.useProgram(this.divergenceProgram.program);
      this.gl.uniform2f(this.divergenceProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
      this.gl.uniform1i(this.divergenceProgram.uniforms.uVelocity, this.velocity.read.attach(0));
      this.blit(this.divergence);

      this.gl.useProgram(this.clearProgram.program);
      this.gl.uniform1i(this.clearProgram.uniforms.uTexture, this.pressure.read.attach(0));
      this.gl.uniform1f(this.clearProgram.uniforms.value, this.config.PRESSURE);
      this.blit(this.pressure.write);
      this.pressure.swap();

      this.gl.useProgram(this.pressureProgram.program);
      this.gl.uniform2f(this.pressureProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
      this.gl.uniform1i(this.pressureProgram.uniforms.uDivergence, this.divergence.attach(0));
      for (let i = 0; i < this.config.PRESSURE_ITERATIONS; i++) {
        this.gl.uniform1i(this.pressureProgram.uniforms.uPressure, this.pressure.read.attach(1));
        this.blit(this.pressure.write);
        this.pressure.swap();
      }

      this.gl.useProgram(this.gradientSubtractProgram.program);
      this.gl.uniform2f(this.gradientSubtractProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
      this.gl.uniform1i(this.gradientSubtractProgram.uniforms.uPressure, this.pressure.read.attach(0));
      this.gl.uniform1i(this.gradientSubtractProgram.uniforms.uVelocity, this.velocity.read.attach(1));
      this.blit(this.velocity.write);
      this.velocity.swap();

      this.gl.useProgram(this.advectionProgram.program);
      this.gl.uniform2f(this.advectionProgram.uniforms.texelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
      if (!this.ext.supportLinearFiltering)
        this.gl.uniform2f(this.advectionProgram.uniforms.dyeTexelSize, this.velocity.texelSizeX, this.velocity.texelSizeY);
      let velocityId = this.velocity.read.attach(0);
      this.gl.uniform1i(this.advectionProgram.uniforms.uVelocity, velocityId);
      this.gl.uniform1i(this.advectionProgram.uniforms.uSource, velocityId);
      this.gl.uniform1f(this.advectionProgram.uniforms.dt, dt);
      this.gl.uniform1f(this.advectionProgram.uniforms.dissipation, this.config.VELOCITY_DISSIPATION);
      this.blit(this.velocity.write);
      this.velocity.swap();

      if (!this.ext.supportLinearFiltering)
        this.gl.uniform2f(this.advectionProgram.uniforms.dyeTexelSize, this.dye.texelSizeX, this.dye.texelSizeY);
      this.gl.uniform1i(this.advectionProgram.uniforms.uVelocity, this.velocity.read.attach(0));
      this.gl.uniform1i(this.advectionProgram.uniforms.uSource, this.dye.read.attach(1));
      this.gl.uniform1f(this.advectionProgram.uniforms.dissipation, this.config.DENSITY_DISSIPATION);
      this.blit(this.dye.write);
      this.dye.swap();
    }

    render() {
      this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
      this.gl.enable(this.gl.BLEND);
      this.drawDisplay();
    }

    drawDisplay() {
      const width = this.gl.drawingBufferWidth;
      const height = this.gl.drawingBufferHeight;
      this.gl.useProgram(this.displayProgram.program);
      if (this.config.SHADING)
        this.gl.uniform2f(this.displayProgram.uniforms.texelSize, 1.0 / width, 1.0 / height);
      this.gl.uniform1i(this.displayProgram.uniforms.uTexture, this.dye.read.attach(0));
      this.blit(null);
    }

    splatPointer(pointer) {
      const dx = pointer.deltaX * this.config.SPLAT_FORCE;
      const dy = pointer.deltaY * this.config.SPLAT_FORCE;
      this.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
    }

    clickSplat(pointer) {
      const color = this.generateColor();
      color.r *= 10.0;
      color.g *= 10.0;
      color.b *= 10.0;
      const dx = 10 * (Math.random() - 0.5);
      const dy = 30 * (Math.random() - 0.5);
      this.splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
    }

    splat(x, y, dx, dy, color) {
      this.gl.useProgram(this.splatProgram.program);
      this.gl.uniform1i(this.splatProgram.uniforms.uTarget, this.velocity.read.attach(0));
      this.gl.uniform1f(this.splatProgram.uniforms.aspectRatio, this.canvas.width / this.canvas.height);
      this.gl.uniform2f(this.splatProgram.uniforms.point, x, y);
      this.gl.uniform3f(this.splatProgram.uniforms.color, dx, dy, 0.0);
      this.gl.uniform1f(this.splatProgram.uniforms.radius, this.correctRadius(this.config.SPLAT_RADIUS / 100.0));
      this.blit(this.velocity.write);
      this.velocity.swap();

      this.gl.uniform1i(this.splatProgram.uniforms.uTarget, this.dye.read.attach(0));
      this.gl.uniform3f(this.splatProgram.uniforms.color, color.r, color.g, color.b);
      this.blit(this.dye.write);
      this.dye.swap();
    }

    correctRadius(radius) {
      const aspectRatio = this.canvas.width / this.canvas.height;
      if (aspectRatio > 1) radius *= aspectRatio;
      return radius;
    }

    updatePointerDownData(pointer, id, posX, posY) {
      pointer.id = id;
      pointer.down = true;
      pointer.moved = false;
      pointer.texcoordX = posX / this.canvas.width;
      pointer.texcoordY = 1.0 - posY / this.canvas.height;
      pointer.prevTexcoordX = pointer.texcoordX;
      pointer.prevTexcoordY = pointer.texcoordY;
      pointer.deltaX = 0;
      pointer.deltaY = 0;
      pointer.color = this.generateColor();
    }

    updatePointerMoveData(pointer, posX, posY, color) {
      pointer.prevTexcoordX = pointer.texcoordX;
      pointer.prevTexcoordY = pointer.texcoordY;
      pointer.texcoordX = posX / this.canvas.width;
      pointer.texcoordY = 1.0 - posY / this.canvas.height;
      pointer.deltaX = this.correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX);
      pointer.deltaY = this.correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY);
      pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0;
      pointer.color = color;
    }

    correctDeltaX(delta) {
      const aspectRatio = this.canvas.width / this.canvas.height;
      if (aspectRatio < 1) delta *= aspectRatio;
      return delta;
    }

    correctDeltaY(delta) {
      const aspectRatio = this.canvas.width / this.canvas.height;
      if (aspectRatio > 1) delta /= aspectRatio;
      return delta;
    }

    generateColor() {
      if (this.config.BASE_COLOR && this.config.BASE_COLOR !== 'random') {
        const baseColor = this.hexToRGB(this.config.BASE_COLOR);
        return {
          r: baseColor.r / 255 * 0.15,
          g: baseColor.g / 255 * 0.15,
          b: baseColor.b / 255 * 0.15
        };
      } else {
        const c = this.HSVtoRGB(Math.random(), 1.0, 1.0);
        c.r *= 0.15;
        c.g *= 0.15;
        c.b *= 0.15;
        return c;
      }
    }

    hexToRGB(hex) {
      const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : { r: 255, g: 107, b: 53 };
    }

    HSVtoRGB(h, s, v) {
      let r, g, b, i, f, p, q, t;
      i = Math.floor(h * 6);
      f = h * 6 - i;
      p = v * (1 - s);
      q = v * (1 - f * s);
      t = v * (1 - (1 - f) * s);
      switch (i % 6) {
        case 0: r = v; g = t; b = p; break;
        case 1: r = q; g = v; b = p; break;
        case 2: r = p; g = v; b = t; break;
        case 3: r = p; g = q; b = v; break;
        case 4: r = t; g = p; b = v; break;
        case 5: r = v; g = p; b = q; break;
        default: break;
      }
      return { r, g, b };
    }

    wrap(value, min, max) {
      const range = max - min;
      if (range === 0) return min;
      return ((value - min) % range) + min;
    }

    updateBaseColor(newColor) {
      this.config.BASE_COLOR = newColor;
      this.pointers.forEach(pointer => {
        pointer.color = this.generateColor();
      });
    }

    destroy() {
      if (this.animationId) {
        cancelAnimationFrame(this.animationId);
      }
      if (this.cleanup) {
        this.cleanup();
      }
      if (this.canvas.parentNode) {
        this.canvas.parentNode.removeChild(this.canvas);
      }
    }
  }
  
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initFluidCursor);
  } else {
    initFluidCursor();
  }
})();`;
}

      function copyJsToClipboard() {
        const jsCode = generateJavaScriptCode();
        
        navigator.clipboard.writeText(jsCode)
          .then(() => {
            showNotification('JavaScript code copied to clipboard!');
          })
          .catch(err => {
            try {
              const textArea = document.createElement('textarea');
              textArea.value = jsCode;
              textArea.style.position = 'fixed';
              textArea.style.opacity = '0';
              document.body.appendChild(textArea);
              textArea.select();
              document.execCommand('copy');
              document.body.removeChild(textArea);
              showNotification('JavaScript code copied to clipboard!');
            } catch (fallbackErr) {
              showNotification('Failed to copy to clipboard. Please try again.', 'error');
            }
          });
      }

      function generateRandomFluid() {
        fluidConfig.simResolution = [64, 96, 128, 192, 256][Math.floor(Math.random() * 5)];
        fluidConfig.dyeResolution = [512, 768, 1024, 1536, 2048][Math.floor(Math.random() * 5)];
        fluidConfig.densityDissipation = Math.round((Math.random() * 9.9 + 0.1) * 10) / 10;
        fluidConfig.velocityDissipation = Math.round((Math.random() * 9.9 + 0.1) * 10) / 10;
        fluidConfig.pressure = Math.round((Math.random() * 0.99 + 0.01) * 100) / 100;
        fluidConfig.curl = Math.floor(Math.random() * 50);
        fluidConfig.splatRadius = Math.round((Math.random() * 0.9 + 0.1) * 20) / 20;
        fluidConfig.splatForce = Math.floor(Math.random() * 19000) + 1000;
        fluidConfig.colorUpdateSpeed = Math.floor(Math.random() * 49) + 1;
        fluidConfig.pressureIterations = Math.floor(Math.random() * 45) + 5;
        
        const isRandomColors = document.getElementById('random-colors').checked;
        if (!isRandomColors) {
          const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
          fluidConfig.baseColor = randomColor;
        }
        
        document.getElementById('sim-resolution').value = fluidConfig.simResolution;
        document.getElementById('dye-resolution').value = fluidConfig.dyeResolution;
        document.getElementById('density-dissipation').value = fluidConfig.densityDissipation;
        document.getElementById('velocity-dissipation').value = fluidConfig.velocityDissipation;
        document.getElementById('pressure').value = fluidConfig.pressure;
        document.getElementById('curl').value = fluidConfig.curl;
        document.getElementById('splat-radius').value = fluidConfig.splatRadius;
        document.getElementById('splat-force').value = fluidConfig.splatForce;
        document.getElementById('color-update-speed').value = fluidConfig.colorUpdateSpeed;
        document.getElementById('pressure-iterations').value = fluidConfig.pressureIterations;
        
        if (!isRandomColors) {
          document.getElementById('base-color').value = fluidConfig.baseColor;
          document.getElementById('base-color-hex').value = fluidConfig.baseColor;
          document.getElementById('base-color-hsl').value = hexToHsl(fluidConfig.baseColor);
          
          const container = document.getElementById('base-color').closest('.color-picker-container');
          if (container) {
            container.style.setProperty('--current-color', fluidConfig.baseColor);
          }
        }
        
        document.getElementById('sim-resolution-value').textContent = fluidConfig.simResolution;
        document.getElementById('dye-resolution-value').textContent = fluidConfig.dyeResolution;
        document.getElementById('density-dissipation-value').textContent = fluidConfig.densityDissipation;
        document.getElementById('velocity-dissipation-value').textContent = fluidConfig.velocityDissipation;
        document.getElementById('pressure-value').textContent = fluidConfig.pressure;
        document.getElementById('curl-value').textContent = fluidConfig.curl;
        document.getElementById('splat-radius-value').textContent = fluidConfig.splatRadius;
        document.getElementById('splat-force-value').textContent = fluidConfig.splatForce;
        document.getElementById('color-update-speed-value').textContent = fluidConfig.colorUpdateSpeed;
        document.getElementById('pressure-iterations-value').textContent = fluidConfig.pressureIterations;
        
        initFluidAnimation();
        showNotification('Random fluid configuration generated!');
      }

      function toggleColorControls(disabled) {
        const colorGroup = document.getElementById('color-control-group');
        if (disabled) {
          colorGroup.style.opacity = '0.5';
          colorGroup.style.pointerEvents = 'none';
        } else {
          colorGroup.style.opacity = '1';
          colorGroup.style.pointerEvents = 'auto';
        }
      }

      function initializeUI() {
        const instructionsToggle = document.getElementById('instructions-toggle');
        const instructionsContent = document.getElementById('instructions-content');
        const instructionsCard = document.getElementById('instructions-card');
        const toggleIcon = instructionsToggle.querySelector('.toggle-icon');
        
        instructionsToggle.addEventListener('click', () => {
          const isVisible = instructionsContent.classList.contains('show');
          
          if (isVisible) {
            instructionsContent.classList.remove('show');
            instructionsCard.classList.remove('expanded');
            toggleIcon.classList.remove('expanded');
          } else {
            instructionsContent.classList.add('show');
            instructionsCard.classList.add('expanded');
            toggleIcon.classList.add('expanded');
          }
        });

        document.getElementById('quick-attribute').addEventListener('click', () => {
          copyToClipboard('data-fluid-cursor');
        });

        document.getElementById('download-config').addEventListener('click', () => {
          copyJsToClipboard();
        });

        document.getElementById('copy-full-section').addEventListener('click', () => {
          copyFullSectionToClipboard();
        });

        document.getElementById('randomize-fluid').addEventListener('click', () => {
          generateRandomFluid();
        });

        document.getElementById('reset-quality').addEventListener('click', () => {
          fluidConfig.simResolution = defaultConfig.simResolution;
          fluidConfig.dyeResolution = defaultConfig.dyeResolution;
          fluidConfig.shading = defaultConfig.shading;
          fluidConfig.transparent = defaultConfig.transparent;
          
          document.getElementById('sim-resolution').value = defaultConfig.simResolution;
          document.getElementById('dye-resolution').value = defaultConfig.dyeResolution;
          document.getElementById('shading').checked = defaultConfig.shading;
          document.getElementById('transparent').checked = defaultConfig.transparent;
          
          document.getElementById('sim-resolution-value').textContent = defaultConfig.simResolution;
          document.getElementById('dye-resolution-value').textContent = defaultConfig.dyeResolution;
          
          initFluidAnimation();
          showNotification('Quality settings reset');
        });

        document.getElementById('reset-physics').addEventListener('click', () => {
          fluidConfig.densityDissipation = defaultConfig.densityDissipation;
          fluidConfig.velocityDissipation = defaultConfig.velocityDissipation;
          fluidConfig.pressure = defaultConfig.pressure;
          fluidConfig.curl = defaultConfig.curl;
          
          document.getElementById('density-dissipation').value = defaultConfig.densityDissipation;
          document.getElementById('velocity-dissipation').value = defaultConfig.velocityDissipation;
          document.getElementById('pressure').value = defaultConfig.pressure;
          document.getElementById('curl').value = defaultConfig.curl;
          
          document.getElementById('density-dissipation-value').textContent = defaultConfig.densityDissipation;
          document.getElementById('velocity-dissipation-value').textContent = defaultConfig.velocityDissipation;
          document.getElementById('pressure-value').textContent = defaultConfig.pressure;
          document.getElementById('curl-value').textContent = defaultConfig.curl;
          
          initFluidAnimation();
          showNotification('Physics settings reset');
        });

        document.getElementById('reset-interaction').addEventListener('click', () => {
          fluidConfig.splatRadius = defaultConfig.splatRadius;
          fluidConfig.splatForce = defaultConfig.splatForce;
          fluidConfig.colorUpdateSpeed = defaultConfig.colorUpdateSpeed;
          fluidConfig.baseColor = defaultConfig.baseColor;
          fluidConfig.mobileTouch = defaultConfig.mobileTouch;
          
          document.getElementById('splat-radius').value = defaultConfig.splatRadius;
          document.getElementById('splat-force').value = defaultConfig.splatForce;
          document.getElementById('color-update-speed').value = defaultConfig.colorUpdateSpeed;
          document.getElementById('random-colors').checked = defaultConfig.baseColor === 'random';
          
          document.getElementById('splat-radius-value').textContent = defaultConfig.splatRadius;
          document.getElementById('splat-force-value').textContent = defaultConfig.splatForce;
          document.getElementById('color-update-speed-value').textContent = defaultConfig.colorUpdateSpeed;
          
          document.querySelectorAll('.touch-mode-btn').forEach(btn => btn.classList.remove('active'));
          document.querySelector(`[data-mode="${defaultConfig.mobileTouch}"]`).classList.add('active');
          updateModeDescription(defaultConfig.mobileTouch);
          
          toggleColorControls(defaultConfig.baseColor === 'random');
          initFluidAnimation();
          showNotification('Interaction settings reset');
        });

        document.getElementById('reset-advanced').addEventListener('click', () => {
          fluidConfig.pressureIterations = defaultConfig.pressureIterations;
          
          document.getElementById('pressure-iterations').value = defaultConfig.pressureIterations;
          document.getElementById('pressure-iterations-value').textContent = defaultConfig.pressureIterations;
          
          initFluidAnimation();
          showNotification('Advanced settings reset');
        });

        const rangeInputs = document.querySelectorAll('input[type="range"]');
        rangeInputs.forEach(input => {
          const valueElement = document.getElementById(`${input.id}-value`);
          
          if (valueElement) {
            valueElement.textContent = input.value;
          }
          
          input.addEventListener('input', () => {
            if (valueElement) {
              valueElement.textContent = input.value;
            }
            
            switch (input.id) {
              case 'sim-resolution':
                fluidConfig.simResolution = parseInt(input.value);
                break;
              case 'dye-resolution':
                fluidConfig.dyeResolution = parseInt(input.value);
                break;
              case 'density-dissipation':
                fluidConfig.densityDissipation = parseFloat(input.value);
                break;
              case 'velocity-dissipation':
                fluidConfig.velocityDissipation = parseFloat(input.value);
                break;
              case 'pressure':
                fluidConfig.pressure = parseFloat(input.value);
                break;
              case 'curl':
                fluidConfig.curl = parseInt(input.value);
                break;
              case 'splat-radius':
                fluidConfig.splatRadius = parseFloat(input.value);
                break;
              case 'splat-force':
                fluidConfig.splatForce = parseInt(input.value);
                break;
              case 'color-update-speed':
                fluidConfig.colorUpdateSpeed = parseInt(input.value);
                break;
              case 'pressure-iterations':
                fluidConfig.pressureIterations = parseInt(input.value);
                break;
            }
            
            initFluidAnimation();
          });
        });

        document.getElementById('shading').addEventListener('change', function() {
          fluidConfig.shading = this.checked;
          initFluidAnimation();
        });
        
        document.getElementById('transparent').addEventListener('change', function() {
          fluidConfig.transparent = this.checked;
          initFluidAnimation();
        });

        document.getElementById('random-colors').addEventListener('change', function() {
          fluidConfig.baseColor = this.checked ? 'random' : document.getElementById('base-color').value;
          toggleColorControls(this.checked);
          initFluidAnimation();
        });

        const baseColorInput = document.getElementById('base-color');
        const baseColorHex = document.getElementById('base-color-hex');
        const baseColorHsl = document.getElementById('base-color-hsl');
        
        baseColorHsl.value = hexToHsl(baseColorInput.value);
        
        const container = baseColorInput.closest('.color-picker-container');
        if (container) {
          container.style.setProperty('--current-color', baseColorInput.value);
        }
        
        baseColorInput.addEventListener('input', () => {
          const color = baseColorInput.value;
          baseColorHex.value = color;
          baseColorHsl.value = hexToHsl(color);
          baseColorHex.classList.remove('invalid');
          baseColorHsl.classList.remove('invalid');
          
          const container = baseColorInput.closest('.color-picker-container');
          if (container) {
            container.style.setProperty('--current-color', color);
          }
          
          if (!document.getElementById('random-colors').checked) {
            fluidConfig.baseColor = color;
            if (activeFluid) {
              activeFluid.updateBaseColor(color);
            }
          }
        });
        
        baseColorHex.addEventListener('input', (e) => {
          let hex = e.target.value;
          
          hex = formatHex(hex);
          e.target.value = hex;
          
          if (isValidHex(hex)) {
            baseColorInput.value = hex;
            baseColorHsl.value = hexToHsl(hex);
            
            const container = baseColorInput.closest('.color-picker-container');
            if (container) {
              container.style.setProperty('--current-color', hex);
            }
            
            if (!document.getElementById('random-colors').checked) {
              fluidConfig.baseColor = hex;
              if (activeFluid) {
                activeFluid.updateBaseColor(hex);
              }
            }
            e.target.classList.remove('invalid');
            baseColorHsl.classList.remove('invalid');
          } else {
            e.target.classList.add('invalid');
          }
        });

        baseColorHex.addEventListener('blur', (e) => {
          if (!isValidHex(e.target.value)) {
            e.target.value = baseColorInput.value;
            e.target.classList.remove('invalid');
          }
        });
        
        baseColorHsl.addEventListener('input', (e) => {
          let hsl = e.target.value;
          
          if (isValidHsl(hsl)) {
            const hex = hslToHex(hsl);
            if (hex) {
              baseColorInput.value = hex;
              baseColorHex.value = hex;
              
              const container = baseColorInput.closest('.color-picker-container');
              if (container) {
                container.style.setProperty('--current-color', hex);
              }
              
              if (!document.getElementById('random-colors').checked) {
                fluidConfig.baseColor = hex;
                if (activeFluid) {
                  activeFluid.updateBaseColor(hex);
                }
              }
              e.target.classList.remove('invalid');
              baseColorHex.classList.remove('invalid');
              return;
            }
          }
          
          e.target.classList.add('invalid');
        });
        
        baseColorHsl.addEventListener('blur', (e) => {
          let hsl = e.target.value;
          
          if (!isValidHsl(hsl) && hsl.trim()) {
            const formatted = formatHsl(hsl);
            if (isValidHsl(formatted)) {
              e.target.value = formatted;
              const hex = hslToHex(formatted);
              if (hex) {
                baseColorInput.value = hex;
                baseColorHex.value = hex;
                
                const container = baseColorInput.closest('.color-picker-container');
                if (container) {
                  container.style.setProperty('--current-color', hex);
                }
                
                if (!document.getElementById('random-colors').checked) {
                  fluidConfig.baseColor = hex;
                  if (activeFluid) {
                    activeFluid.updateBaseColor(hex);
                  }
                }
                e.target.classList.remove('invalid');
                baseColorHex.classList.remove('invalid');
                return;
              }
            }
          }
          
          if (!isValidHsl(e.target.value)) {
            e.target.value = hexToHsl(baseColorInput.value);
            e.target.classList.remove('invalid');
          }
        });

        document.querySelectorAll('.touch-mode-btn').forEach(btn => {
          btn.addEventListener('click', () => {
            document.querySelectorAll('.touch-mode-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            
            const mode = btn.dataset.mode;
            fluidConfig.mobileTouch = mode;
            updateModeDescription(mode);
            initFluidAnimation();
          });
        });

        document.addEventListener('keydown', (e) => {
          if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
            return;
          }
          
          if (e.ctrlKey || e.metaKey) {
            switch (e.key.toLowerCase()) {
              case 'd':
                e.preventDefault();
                const downloadBtn = document.getElementById('download-config');
                if (downloadBtn && downloadBtn.hasAttribute('data-protection-animation')) {
                  downloadBtn.click();
                } else {
                  copyJsToClipboard();
                }
                break;
              case 's':
                e.preventDefault();
                const fullSectionBtn = document.getElementById('copy-full-section');
                if (fullSectionBtn && fullSectionBtn.hasAttribute('data-protection-animation')) {
                  fullSectionBtn.click();
                } else {
                  copyFullSectionToClipboard();
                }
                break;
            }
          } else {
            switch (e.key.toLowerCase()) {
              case 'r':
                generateRandomFluid();
                break;
            }
          }
        });

        setTimeout(() => {
          showNotification('BricksFusion FluidCursor Configurator loaded!');
        }, 500);

        function saveConfiguration() {
          try {
            localStorage.setItem('bricksfusion-fluid-config', JSON.stringify(fluidConfig));
          } catch (e) {
          }
        }

        function loadConfiguration() {
          try {
            const saved = localStorage.getItem('bricksfusion-fluid-config');
            if (saved) {
              const savedConfig = JSON.parse(saved);
              Object.assign(fluidConfig, savedConfig);
              
              document.getElementById('sim-resolution').value = savedConfig.simResolution;
              document.getElementById('dye-resolution').value = savedConfig.dyeResolution;
              document.getElementById('density-dissipation').value = savedConfig.densityDissipation;
              document.getElementById('velocity-dissipation').value = savedConfig.velocityDissipation;
              document.getElementById('pressure').value = savedConfig.pressure;
              document.getElementById('curl').value = savedConfig.curl;
              document.getElementById('splat-radius').value = savedConfig.splatRadius;
              document.getElementById('splat-force').value = savedConfig.splatForce;
              document.getElementById('color-update-speed').value = savedConfig.colorUpdateSpeed;
              document.getElementById('pressure-iterations').value = savedConfig.pressureIterations;
              document.getElementById('shading').checked = savedConfig.shading;
              document.getElementById('transparent').checked = savedConfig.transparent;
              document.getElementById('random-colors').checked = savedConfig.baseColor === 'random';
              
              if (savedConfig.baseColor !== 'random') {
                document.getElementById('base-color').value = savedConfig.baseColor;
                document.getElementById('base-color-hex').value = savedConfig.baseColor;
                document.getElementById('base-color-hsl').value = hexToHsl(savedConfig.baseColor);
              }
              
              document.getElementById('sim-resolution-value').textContent = savedConfig.simResolution;
              document.getElementById('dye-resolution-value').textContent = savedConfig.dyeResolution;
              document.getElementById('density-dissipation-value').textContent = savedConfig.densityDissipation;
              document.getElementById('velocity-dissipation-value').textContent = savedConfig.velocityDissipation;
              document.getElementById('pressure-value').textContent = savedConfig.pressure;
              document.getElementById('curl-value').textContent = savedConfig.curl;
              document.getElementById('splat-radius-value').textContent = savedConfig.splatRadius;
              document.getElementById('splat-force-value').textContent = savedConfig.splatForce;
              document.getElementById('color-update-speed-value').textContent = savedConfig.colorUpdateSpeed;
              document.getElementById('pressure-iterations-value').textContent = savedConfig.pressureIterations;
              
              document.querySelectorAll('.touch-mode-btn').forEach(btn => btn.classList.remove('active'));
              document.querySelector(`[data-mode="${savedConfig.mobileTouch}"]`).classList.add('active');
              updateModeDescription(savedConfig.mobileTouch);
              
              toggleColorControls(savedConfig.baseColor === 'random');
              initFluidAnimation();
            }
          } catch (e) {
          }
        }

        const originalInitFluidAnimation = initFluidAnimation;
        initFluidAnimation = function() {
          originalInitFluidAnimation();
          saveConfiguration();
        };

        loadConfiguration();
      }
      
      initializeUI();
      initFluidAnimation();
    }
  
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initConfigurator);
  } else {
    initConfigurator();
  }
})();
  </script>
</body>
</html>
Fluid Cursor - Bricksfusion
HEAVY

Fluid Cursor

Creates realistic fluid simulation that follows cursor movement. Uses WebGL shaders to simulate velocity, pressure, vorticity, and dye dissipation in real-time. Perfect for immersive backgrounds, interactive hero sections, or adding liquid magic to any design.

Fluid Cursor

Move your cursor to create fluid simulations.

Resolution

Simulation Resolution 32-512

Detail level of fluid simulation physics. Lower is faster but less detailed, higher is more realistic but intensive.

Default: 128

Dye Resolution 256-2048

Quality of rendered colors and textures. Higher creates sharper, more vibrant fluid trails.

Default: 1024

Dissipation

Density Dissipation 0.0-1.0

How quickly colors fade away. Lower makes trails disappear fast, higher keeps colors longer.

Default: 0.8

Velocity Dissipation 0.0-1.0

How quickly fluid motion slows down. Lower creates short-lived movements, higher maintains flow longer.

Default: 0.2

Physics

Pressure 0.0-1.0

Base pressure in fluid system. Affects how fluid spreads and flows naturally.

Default: 0.43

Pressure Iterations 10-50

Accuracy of pressure calculations. Higher is more realistic but slower. Affects fluid incompressibility.

Default: 26

Curl 0-50

Strength of vorticity confinement. Creates swirling, turbulent motion. Higher adds more chaos and detail.

Default: 25

Splat

Splat Radius 0.1-1.0

Size of cursor interaction area. Larger radius creates broader fluid strokes.

Default: 0.45

Splat Force 1000-30000

Intensity of cursor impact on fluid. Higher creates more dramatic, explosive interactions.

Default: 13000

Appearance

Shading on/off

Enable 3D-like shading effects. Adds depth with lighting calculations based on fluid gradients.

Default: On

Color Update Speed 1-50

How quickly colors cycle through spectrum. Higher changes colors faster for more variety.

Default: 10

Transparent on/off

Enable alpha transparency. Allows fluid to blend with background content underneath.

Default: On

Base Color hex or 'random'

Set specific color or use 'random' for rainbow spectrum. Fixed colors create cohesive branding.

Default: random

Effects

Bloom on/off

Enable bloom glow effect. Creates luminous halos around bright fluid areas for a dreamy, ethereal look.

Default: Off

Bloom Intensity 0.0-1.0

Strength of bloom glow. Higher values create more intense, radiant effects.

Default: 0.8

Bloom Threshold 0.0-1.0

Brightness level needed to trigger bloom. Lower values bloom more areas, higher limits to brightest spots.

Default: 0.6

Bloom Iterations 1-20

Quality of bloom blur passes. Higher creates smoother, more diffused glow but impacts performance.

Default: 8

Sunrays on/off

Enable volumetric light rays effect. Creates radial god rays emanating from bright fluid centers.

Default: Off

Sunrays Weight 0.0-2.0

Intensity of light ray effect. Higher creates more dramatic, visible rays.

Default: 1.0

Mobile

Mobile Touch 4 modes

Touch behavior on mobile. Scroll allows normal scrolling with subtle effects, smooth creates gradual taps, gentle makes dramatic splashes, disabled turns off mobile interaction.

Default: scroll

Performance Warning

This element uses WebGL2/WebGL with complex fragment shaders for fluid dynamics. Simulates Navier-Stokes equations with velocity advection, pressure projection, vorticity confinement, and dye transport. Runs multiple shader passes per frame including curl, divergence, pressure (26 iterations), gradient subtraction, and advection. Extremely GPU-intensive - not suitable for low-end devices or multiple instances.