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">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>

  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Grid3D 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: 400px;
  width: 100%;
  position: relative;
  overflow: hidden;
  border-radius: var(--card-radius);
  background-color: #252525;
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}

.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;
}

.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);
}

.preview-btn svg {
  width: 18px;
  height: 18px;
  stroke: currentColor;
}

.background-selector-wrapper {
  position: relative;
  display: inline-block;
}

.background-selector-btn {
  position: relative;
}

.background-selector-btn:hover {
  background-color: rgba(239, 96, 19, 0.2);
  border-color: var(--accent);
  box-shadow: 0 0 8px rgba(239, 96, 19, 0.3);
}

.hidden-color-input {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}

.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;
  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-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.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);
}

.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: var(--card-bg);
  display: flex;
  align-items: center;
  justify-content: center;
  --selected-color: #081316;
}

.color-picker-container:hover {
  border-color: var(--accent);
  transform: scale(1.05);
  box-shadow: 0 0 12px rgba(239, 96, 19, 0.3);
}

.color-picker-container::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--selected-color, #081316);
  border-radius: 6px;
  transition: var(--transition);
}

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

.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;
}

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

select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}

.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/visual-effects/" class="breadcrumb-item">Visual effects</a>
      <span class="breadcrumb-separator">›</span>
      <span class="breadcrumb-item active">Grid3D Effect</span>
    </nav>
    
    <div class="action-buttons">
      <div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
        data-grid3d
      </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">Grid3D Effect</h1>
      <p class="page-subtitle">Interactive 3D grid animations 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>Customize your 3D grid animation 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-grid3d</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="grid-preview" data-grid3d="true">
          <div class="preview-content">Interactive 3D Grid Preview</div>
          <div class="preview-controls">
            <div class="background-selector-wrapper">
              <button class="preview-btn background-selector-btn" id="background-selector">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                  <polygon points="12,2 2,7 12,12 22,7"/>
                  <polyline points="2,17 12,22 22,17"/>
                  <polyline points="2,12 12,17 22,12"/>
                </svg>
              </button>
              <input type="color" id="preview-background-picker" class="hidden-color-input" value="#252525" title="Change Preview Background (B)">
            </div>
          </div>
        </div>
      </section>

      <section class="controls-section">
        <div class="card">
          <div class="card-heading">
            Grid Settings
            <div class="card-actions">
              <button class="card-action-btn" id="reset-grid" title="Reset Grid Settings">↺</button>
            </div>
          </div>
          <div class="card-content">
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Grid Size
                  <span class="help-tooltip" title="Size of the 3D grid (NxN)">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="grid-size-value">10</span>x<span id="grid-size-value2">10</span></span>
                  <button class="reset-btn" onclick="resetParameter('grid-size', 10)">↺</button>
                </div>
              </div>
              <input type="range" id="grid-size" min="5" max="15" step="1" value="10">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Box Size
                  <span class="help-tooltip" title="Scale of individual grid boxes">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="box-size-value">1.0</span></span>
                  <button class="reset-btn" onclick="resetParameter('box-size', 1.0)">↺</button>
                </div>
              </div>
              <input type="range" id="box-size" min="0.5" max="2.0" step="0.1" value="1.0">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Ripple Intensity
                  <span class="help-tooltip" title="Strength of the hover ripple effect">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="ripple-intensity-value">2.5</span></span>
                  <button class="reset-btn" onclick="resetParameter('ripple-intensity', 2.5)">↺</button>
                </div>
              </div>
              <input type="range" id="ripple-intensity" min="1.0" max="5.0" step="0.1" value="2.5">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Ripple Radius
                  <span class="help-tooltip" title="Size of the ripple effect area">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="ripple-radius-value">2.0</span></span>
                  <button class="reset-btn" onclick="resetParameter('ripple-radius', 2.0)">↺</button>
                </div>
              </div>
              <input type="range" id="ripple-radius" min="1.0" max="4.0" step="0.1" value="2.0">
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-heading">
            Visual Settings
            <div class="card-actions">
              <button class="card-action-btn" id="reset-visual" title="Reset Visual Settings">↺</button>
            </div>
          </div>
          <div class="card-content">
            <div class="color-list">
              <div class="color-row">
                <div class="color-picker-container">
                  <input type="color" id="grid-color" value="#081316">
                </div>
                <div class="color-input-group">
                  <span class="color-label">HEX</span>
                  <input type="text" class="color-input hex-input" id="grid-color-hex" value="#081316" placeholder="#FFFFFF">
                </div>
                <div class="color-input-group">
                  <span class="color-label">HSL</span>
                  <input type="text" class="color-input hsl-input" id="grid-color-hsl" placeholder="hsl(0, 100%, 50%)">
                </div>
              </div>
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Metalness
                  <span class="help-tooltip" title="Metallic property of the material">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="metalness-value">1.0</span></span>
                  <button class="reset-btn" onclick="resetParameter('metalness', 1.0)">↺</button>
                </div>
              </div>
              <input type="range" id="metalness" min="0.0" max="1.0" step="0.1" value="1.0">
            </div>
            
            <div class="control-group">
              <div class="control-label">
                <span class="label-text">
                  Roughness
                  <span class="help-tooltip" title="Surface roughness of the material">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="roughness-value">0.5</span></span>
                  <button class="reset-btn" onclick="resetParameter('roughness', 0.5)">↺</button>
                </div>
              </div>
              <input type="range" id="roughness" min="0.0" max="1.0" step="0.1" value="0.5">
            </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">
                  Animation Speed
                  <span class="help-tooltip" title="Speed multiplier for hover animations">ℹ</span>
                </span>
                <div class="value-display">
                  <span class="value-text"><span id="animation-speed-value">1.0</span>x</span>
                  <button class="reset-btn" onclick="resetParameter('animation-speed', 1.0)">↺</button>
                </div>
              </div>
              <input type="range" id="animation-speed" min="0.1" max="3.0" step="0.1" value="1.0">
            </div>
          </div>
        </div>
      </section>
    </div>
  </div>

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

  <script>
    document.addEventListener('DOMContentLoaded', function() {
      let grid3DConfig = {
        gridSize: 10,
        boxSize: 1.0,
        rippleIntensity: 2.5,
        rippleRadius: 2.0,
        animationSpeed: 1.0,
        gridColor: "#081316",
        metalness: 1.0,
        roughness: 0.5
      };

      const defaultConfig = { ...grid3DConfig };

      let activeGrid3D = null;
      
      class Grid3D {
        constructor(container, options = {}) {
          this.container = container;
          this.scene = null;
          this.camera = null;
          this.renderer = null;
          this.raycaster = null;
          this.mouse = new THREE.Vector2();
          this.boxes = [];
          this.animationId = null;
          this.isDestroyed = false;
          this.hoveredBox = null;
          
          this.gridSize = options.gridSize || 10;
          this.boxWidth = 4 * (options.boxSize || 1.0);
          this.boxLength = 4 * (options.boxSize || 1.0);
          this.gap = 0.05;
          this.spacingX = this.boxWidth + this.gap;
          this.spacingZ = this.boxLength + this.gap;
          this.rippleScale = options.rippleIntensity || 2.5;
          this.rippleRadius = options.rippleRadius || 2.0;
          this.animationSpeed = options.animationSpeed || 1.0;
          this.gridColor = options.gridColor || "#081316";
          this.metalness = options.metalness || 1.0;
          this.roughness = options.roughness || 0.5;
          
          this.init();
        }
        
        init() {
          this.createScene();
          this.createCamera();
          this.createRenderer();
          this.createLights();
          this.createGrid();
          this.setupEventListeners();
          this.animate();
        }
        
        createScene() {
          this.scene = new THREE.Scene();
          this.raycaster = new THREE.Raycaster();
        }
        
        createCamera() {
          this.camera = new THREE.PerspectiveCamera(
            35,
            this.container.offsetWidth / this.container.offsetHeight,
            0.1,
            1000
          );
          this.camera.position.set(-9.31, 12, 24.72);
          this.camera.rotation.set(-0.65, -0.2, -0.13);
        }
        
        createRenderer() {
          this.renderer = new THREE.WebGLRenderer({ 
            antialias: true,
            alpha: true
          });
          this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight);
          this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
          this.renderer.shadowMap.enabled = true;
          this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
          this.renderer.setClearColor(0x000000, 0);
          
          this.renderer.domElement.style.position = 'absolute';
          this.renderer.domElement.style.top = '0';
          this.renderer.domElement.style.left = '0';
          this.renderer.domElement.style.width = '100%';
          this.renderer.domElement.style.height = '100%';
          this.renderer.domElement.style.zIndex = '1';
          this.renderer.domElement.style.pointerEvents = 'none';
          
          this.container.style.position = 'relative';
          this.container.appendChild(this.renderer.domElement);
        }
        
        createLights() {
          const ambientLight = new THREE.AmbientLight(0xffffff, 1);
          this.scene.add(ambientLight);
          
          const directionalLight1 = new THREE.DirectionalLight(0xffffff, 10);
          directionalLight1.position.set(10, 15, 10);
          directionalLight1.castShadow = true;
          this.scene.add(directionalLight1);
          
          const directionalLight2 = new THREE.DirectionalLight(0xffffff, 10);
          directionalLight2.position.set(-10, 10, -5);
          this.scene.add(directionalLight2);
          
          const directionalLight3 = new THREE.DirectionalLight(0xf0f8ff, 5);
          directionalLight3.position.set(5, -10, 15);
          this.scene.add(directionalLight3);
          
          const pointLight1 = new THREE.PointLight(0xffffff, 2, 50);
          pointLight1.position.set(0, 20, 3);
          this.scene.add(pointLight1);
          
          const pointLight2 = new THREE.PointLight(0xffffff, 1.5, 40);
          pointLight2.position.set(15, 5, 15);
          this.scene.add(pointLight2);
        }
        
        createRoundedBoxGeometry(width, length, cornerRadius) {
          const shape = new THREE.Shape();
          const angleStep = Math.PI * 0.5;
          const radius = cornerRadius;
          
          const halfWidth = width / 2;
          const halfLength = length / 2;

          shape.absarc(halfWidth - radius, halfLength - radius, radius, angleStep * 0, angleStep * 1);
          shape.absarc(-halfWidth + radius, halfLength - radius, radius, angleStep * 1, angleStep * 2);
          shape.absarc(-halfWidth + radius, -halfLength + radius, radius, angleStep * 2, angleStep * 3);
          shape.absarc(halfWidth - radius, -halfLength + radius, radius, angleStep * 3, angleStep * 4);

          const extrudeSettings = {
            depth: 0.3,
            bevelEnabled: true,
            bevelThickness: 0.05,
            bevelSize: 0.05,
            bevelSegments: 20,
            curveSegments: 20
          };

          const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
          geometry.center();
          
          return geometry;
        }
        
        createGrid() {
          this.boxes.forEach(box => {
            this.scene.remove(box);
            if (box.geometry) box.geometry.dispose();
            if (box.material) box.material.dispose();
          });
          this.boxes = [];
          
          const geometry = this.createRoundedBoxGeometry(this.boxWidth, this.boxLength, 0.8);
          
          for (let x = 0; x < this.gridSize; x++) {
            for (let z = 0; z < this.gridSize; z++) {
              const posX = (x - (this.gridSize - 1) / 2) * this.spacingX;
              const posZ = (z - (this.gridSize - 1) / 2) * this.spacingZ;
              
              const material = new THREE.MeshPhysicalMaterial({
                color: this.gridColor,
                roughness: this.roughness,
                metalness: this.metalness,
                clearcoat: 1,
                clearcoatRoughness: 0
              });
              
              const mesh = new THREE.Mesh(geometry, material);
              mesh.position.set(posX, -0.85, posZ);
              mesh.rotation.set(Math.PI / 2, 0, 0);
              mesh.userData = {
                gridPosition: [x, z],
                currentScale: 1,
                targetScale: 1
              };
              
              this.scene.add(mesh);
              this.boxes.push(mesh);
            }
          }
        }
        
        setupEventListeners() {
          this.onMouseMove = this.onMouseMove.bind(this);
          this.onResize = this.onResize.bind(this);
          
          this.container.addEventListener('mousemove', this.onMouseMove);
          this.container.addEventListener('mouseleave', () => {
            this.hoveredBox = null;
          });
          window.addEventListener('resize', this.onResize);
        }
        
        onMouseMove(event) {
          const rect = this.container.getBoundingClientRect();
          this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
          this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
          
          this.detectHover();
        }
        
        detectHover() {
          this.raycaster.setFromCamera(this.mouse, this.camera);
          const intersects = this.raycaster.intersectObjects(this.boxes);
          
          if (intersects.length > 0) {
            const mesh = intersects[0].object;
            if (mesh.userData && mesh.userData.gridPosition) {
              this.hoveredBox = mesh.userData.gridPosition;
              return;
            }
          }
          
          this.hoveredBox = null;
        }
        
        updateBoxScales() {
          this.boxes.forEach(box => {
            const gridPosition = box.userData.gridPosition;
            let targetScale = 1;
            
            const isThisBoxHovered = this.hoveredBox && 
              gridPosition[0] === this.hoveredBox[0] && 
              gridPosition[1] === this.hoveredBox[1];
            
            if (isThisBoxHovered) {
              targetScale = 5;
            } else if (this.hoveredBox) {
              const dx = gridPosition[0] - this.hoveredBox[0];
              const dz = gridPosition[1] - this.hoveredBox[1];
              const distance = Math.sqrt(dx * dx + dz * dz);
              
              if (distance <= this.rippleRadius && distance > 0) {
                const falloff = Math.max(0, 1 - (distance / this.rippleRadius));
                const rippleEffect = falloff * this.rippleScale;
                targetScale = 1 + (rippleEffect * 3);
              }
            }
            
            box.userData.targetScale = targetScale;
            
            const lerpFactor = 0.1 * this.animationSpeed;
            const newScale = box.userData.currentScale + (targetScale - box.userData.currentScale) * lerpFactor;
            box.userData.currentScale = newScale;
            
            box.scale.z = newScale;
          });
        }
        
        onResize() {
          if (this.isDestroyed) return;
          
          const width = this.container.offsetWidth;
          const height = this.container.offsetHeight;
          
          this.camera.aspect = width / height;
          this.camera.updateProjectionMatrix();
          this.renderer.setSize(width, height);
        }
        
        animate() {
          if (this.isDestroyed) return;
          
          this.animationId = requestAnimationFrame(() => this.animate());
          
          this.updateBoxScales();
          this.renderer.render(this.scene, this.camera);
        }
        
        updateConfig(options) {
          if (options.gridSize && options.gridSize !== this.gridSize) {
            this.gridSize = options.gridSize;
            this.boxWidth = 4 * this.boxSize;
            this.boxLength = 4 * this.boxSize;
            this.spacingX = this.boxWidth + this.gap;
            this.spacingZ = this.boxLength + this.gap;
            this.createGrid();
          }
          
          if (options.boxSize) {
            this.boxSize = options.boxSize;
            this.boxWidth = 4 * options.boxSize;
            this.boxLength = 4 * options.boxSize;
            this.spacingX = this.boxWidth + this.gap;
            this.spacingZ = this.boxLength + this.gap;
            this.createGrid();
          }
          
          if (options.rippleIntensity) {
            this.rippleScale = options.rippleIntensity;
          }
          
          if (options.rippleRadius) {
            this.rippleRadius = options.rippleRadius;
          }
          
          if (options.animationSpeed) {
            this.animationSpeed = options.animationSpeed;
          }
          
          if (options.gridColor) {
            this.gridColor = options.gridColor;
            this.boxes.forEach(box => {
              if (box.material) {
                box.material.color.setHex(parseInt(options.gridColor.replace('#', '0x')));
              }
            });
          }
          
          if (options.metalness !== undefined) {
            this.metalness = options.metalness;
            this.boxes.forEach(box => {
              if (box.material) {
                box.material.metalness = options.metalness;
              }
            });
          }
          
          if (options.roughness !== undefined) {
            this.roughness = options.roughness;
            this.boxes.forEach(box => {
              if (box.material) {
                box.material.roughness = options.roughness;
              }
            });
          }
        }
        
        destroy() {
          this.isDestroyed = true;
          
          if (this.animationId) {
            cancelAnimationFrame(this.animationId);
          }
          
          this.container.removeEventListener('mousemove', this.onMouseMove);
          window.removeEventListener('resize', this.onResize);
          
          this.boxes.forEach(box => {
            if (box.geometry) box.geometry.dispose();
            if (box.material) box.material.dispose();
          });
          
          if (this.renderer) {
            this.renderer.dispose();
            if (this.renderer.domElement.parentNode) {
              this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
            }
          }
          
          this.scene = null;
          this.camera = null;
          this.renderer = null;
          this.boxes = [];
        }
      }
      
      function initGrid3DFlow() {
        const sections = document.querySelectorAll('[data-grid3d]:not([data-grid3d-initialized="true"])');
        
        sections.forEach((section) => {
          const gridSize = section.hasAttribute('data-grid3d-size') 
            ? parseInt(section.getAttribute('data-grid3d-size')) 
            : grid3DConfig.gridSize;
            
          const boxSize = section.hasAttribute('data-grid3d-box-size') 
            ? parseFloat(section.getAttribute('data-grid3d-box-size')) 
            : grid3DConfig.boxSize;
            
          const rippleIntensity = section.hasAttribute('data-grid3d-ripple-intensity') 
            ? parseFloat(section.getAttribute('data-grid3d-ripple-intensity')) 
            : grid3DConfig.rippleIntensity;
            
          const rippleRadius = section.hasAttribute('data-grid3d-ripple-radius') 
            ? parseFloat(section.getAttribute('data-grid3d-ripple-radius')) 
            : grid3DConfig.rippleRadius;
            
          const animationSpeed = section.hasAttribute('data-grid3d-animation-speed') 
            ? parseFloat(section.getAttribute('data-grid3d-animation-speed')) 
            : grid3DConfig.animationSpeed;
            
          const gridColor = section.hasAttribute('data-grid3d-color') 
            ? section.getAttribute('data-grid3d-color') 
            : grid3DConfig.gridColor;
            
          const metalness = section.hasAttribute('data-grid3d-metalness') 
            ? parseFloat(section.getAttribute('data-grid3d-metalness')) 
            : grid3DConfig.metalness;
            
          const roughness = section.hasAttribute('data-grid3d-roughness') 
            ? parseFloat(section.getAttribute('data-grid3d-roughness')) 
            : grid3DConfig.roughness;
            
          const options = {
            gridSize,
            boxSize,
            rippleIntensity,
            rippleRadius,
            animationSpeed,
            gridColor,
            metalness,
            roughness
          };
          
          if (section.offsetHeight === 0) {
            section.style.minHeight = '400px';
          }
          
          const grid3DInstance = new Grid3D(section, options);
          section.grid3DInstance = grid3DInstance;
          section.dataset.grid3dInitialized = 'true';
          
          if (section.id === 'grid-preview') {
            activeGrid3D = grid3DInstance;
            grid3DConfig = {
              gridSize: options.gridSize,
              boxSize: options.boxSize,
              rippleIntensity: options.rippleIntensity,
              rippleRadius: options.rippleRadius,
              animationSpeed: options.animationSpeed,
              gridColor: options.gridColor,
              metalness: options.metalness,
              roughness: options.roughness
            };
          }
        });
      }
      
      function updateGrid3DPreview() {
        const previews = ['grid-preview'];
        
        previews.forEach(previewId => {
          const preview = document.getElementById(previewId);
          if (!preview) return;
          
          if (preview.grid3DInstance) {
            preview.grid3DInstance.updateConfig(grid3DConfig);
          }
        });
      }



      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 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 codeId = generateUniqueId();
  const attributeId1 = generateUniqueId();
  const attributeId2 = 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": {
          "_height": "500",
          "_justifyContent": "center",
          "_background": {"color": {"hex": "#000000"}},
          "_attributes": [{"id": attributeId1, "name": "data-grid3d"}]
        },
        "label": "Grid 3D Section"
      },
      {
        "id": containerId,
        "name": "container",
        "parent": sectionId,
        "children": [],
        "settings": {
          "_alignItems": "center",
          "_width": "500",
          "_height": "300",
          "_attributes": [{"id": attributeId2, "name": "data-grid-flip"}],
          "_overflow": "hidden"
        }
      },
      {
        "id": codeId,
        "name": "code",
        "parent": sectionId,
        "children": [],
        "settings": {
          "javascriptCode": jsCode,
          "executeCode": true,
          "_display": "none"
        },
        "label": "Grid 3D JS"
      }
    ],
    "source": "bricksCopiedElements",
    "sourceUrl": "https://test.bricksfusion.com",
    "version": "2.0.1",
    "globalClasses": [],
    "globalElements": []
  };

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

      function generateJavaScriptCode() {
        return `(function(window) {
  const EnhancedGrid3DAnimation = {
    instances: [],
    config: {
      gridSize: ${grid3DConfig.gridSize},
      boxSize: ${grid3DConfig.boxSize},
      rippleIntensity: ${grid3DConfig.rippleIntensity},
      rippleRadius: ${grid3DConfig.rippleRadius},
      animationSpeed: ${grid3DConfig.animationSpeed},
      gridColor: "${grid3DConfig.gridColor}",
      metalness: ${grid3DConfig.metalness},
      roughness: ${grid3DConfig.roughness}
    },
    init: function(options = {}) {
      const defaultOptions = {
        selector: '[data-grid3d]',
        colorAttr: 'data-grid3d-color'
      };
      const config = { ...defaultOptions, ...options };

      function loadThreeJS(callback) {
        if (typeof THREE !== 'undefined') {
          callback();
          return;
        }
        
        const script = document.createElement('script');
        script.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
        script.onload = callback;
        script.onerror = function() {
          console.error('Failed to load Three.js');
        };
        document.head.appendChild(script);
      }

      const initInstances = () => {
        loadThreeJS(() => {
          document.querySelectorAll(config.selector).forEach(element => {
            if (!element.hasAttribute('data-grid3d-initialized')) {
              this.createInstance(element, config);
              element.setAttribute('data-grid3d-initialized', 'true');
            }
          });
        });
      };

      initInstances();
      setTimeout(initInstances, 100);
      window.addEventListener('load', initInstances);

      const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (mutation.type === 'childList') {
            initInstances();
          }
        });
      });

      observer.observe(document.body, { childList: true, subtree: true });
    },

    createInstance: function(element, config) {
      if (typeof THREE === 'undefined') {
        console.error('Three.js is required for Grid3D Animation');
        return;
      }

      if (element.offsetHeight === 0) {
        element.style.minHeight = '400px';
      }

      const instance = new Grid3D(element, {
        gridSize: this.config.gridSize,
        boxSize: this.config.boxSize,
        rippleIntensity: this.config.rippleIntensity,
        rippleRadius: this.config.rippleRadius,
        animationSpeed: this.config.animationSpeed,
        gridColor: element.getAttribute(config.colorAttr) || this.config.gridColor,
        metalness: this.config.metalness,
        roughness: this.config.roughness
      });

      this.instances.push(instance);
    }
  };

  class Grid3D {
    constructor(container, options = {}) {
      this.container = container;
      this.scene = null;
      this.camera = null;
      this.renderer = null;
      this.raycaster = null;
      this.mouse = new THREE.Vector2();
      this.boxes = [];
      this.animationId = null;
      this.isDestroyed = false;
      this.hoveredBox = null;
      
      this.gridSize = options.gridSize || ${grid3DConfig.gridSize};
      this.boxWidth = 4 * (options.boxSize || ${grid3DConfig.boxSize});
      this.boxLength = 4 * (options.boxSize || ${grid3DConfig.boxSize});
      this.gap = 0.05;
      this.spacingX = this.boxWidth + this.gap;
      this.spacingZ = this.boxLength + this.gap;
      this.rippleScale = options.rippleIntensity || ${grid3DConfig.rippleIntensity};
      this.rippleRadius = options.rippleRadius || ${grid3DConfig.rippleRadius};
      this.animationSpeed = options.animationSpeed || ${grid3DConfig.animationSpeed};
      this.gridColor = options.gridColor || "${grid3DConfig.gridColor}";
      this.metalness = options.metalness || ${grid3DConfig.metalness};
      this.roughness = options.roughness || ${grid3DConfig.roughness};
      
      this.init();
    }
    
    init() {
      this.createScene();
      this.createCamera();
      this.createRenderer();
      this.createLights();
      this.createGrid();
      this.setupEventListeners();
      this.animate();
    }
    
    createScene() {
      this.scene = new THREE.Scene();
      this.raycaster = new THREE.Raycaster();
    }
    
    createCamera() {
      this.camera = new THREE.PerspectiveCamera(
        35,
        this.container.offsetWidth / this.container.offsetHeight,
        0.1,
        1000
      );
      this.camera.position.set(-9.31, 12, 24.72);
      this.camera.rotation.set(-0.65, -0.2, -0.13);
    }
    
    createRenderer() {
      this.renderer = new THREE.WebGLRenderer({ 
        antialias: true,
        alpha: true
      });
      this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight);
      this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      this.renderer.shadowMap.enabled = true;
      this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
      this.renderer.setClearColor(0x000000, 0);
      
      this.renderer.domElement.style.position = 'absolute';
      this.renderer.domElement.style.top = '0';
      this.renderer.domElement.style.left = '0';
      this.renderer.domElement.style.width = '100%';
      this.renderer.domElement.style.height = '100%';
      this.renderer.domElement.style.zIndex = '1';
      this.renderer.domElement.style.pointerEvents = 'none';
      
      this.container.style.position = 'relative';
      this.container.appendChild(this.renderer.domElement);
    }
    
    createLights() {
      const ambientLight = new THREE.AmbientLight(0xffffff, 1);
      this.scene.add(ambientLight);
      
      const directionalLight1 = new THREE.DirectionalLight(0xffffff, 10);
      directionalLight1.position.set(10, 15, 10);
      directionalLight1.castShadow = true;
      this.scene.add(directionalLight1);
      
      const directionalLight2 = new THREE.DirectionalLight(0xffffff, 10);
      directionalLight2.position.set(-10, 10, -5);
      this.scene.add(directionalLight2);
      
      const directionalLight3 = new THREE.DirectionalLight(0xf0f8ff, 5);
      directionalLight3.position.set(5, -10, 15);
      this.scene.add(directionalLight3);
      
      const pointLight1 = new THREE.PointLight(0xffffff, 2, 50);
      pointLight1.position.set(0, 20, 3);
      this.scene.add(pointLight1);
      
      const pointLight2 = new THREE.PointLight(0xffffff, 1.5, 40);
      pointLight2.position.set(15, 5, 15);
      this.scene.add(pointLight2);
    }
    
    createRoundedBoxGeometry(width, length, cornerRadius) {
      const shape = new THREE.Shape();
      const angleStep = Math.PI * 0.5;
      const radius = cornerRadius;
      
      const halfWidth = width / 2;
      const halfLength = length / 2;

      shape.absarc(halfWidth - radius, halfLength - radius, radius, angleStep * 0, angleStep * 1);
      shape.absarc(-halfWidth + radius, halfLength - radius, radius, angleStep * 1, angleStep * 2);
      shape.absarc(-halfWidth + radius, -halfLength + radius, radius, angleStep * 2, angleStep * 3);
      shape.absarc(halfWidth - radius, -halfLength + radius, radius, angleStep * 3, angleStep * 4);

      const extrudeSettings = {
        depth: 0.3,
        bevelEnabled: true,
        bevelThickness: 0.05,
        bevelSize: 0.05,
        bevelSegments: 20,
        curveSegments: 20
      };

      const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
      geometry.center();
      
      return geometry;
    }
    
    createGrid() {
      this.boxes.forEach(box => {
        this.scene.remove(box);
        if (box.geometry) box.geometry.dispose();
        if (box.material) box.material.dispose();
      });
      this.boxes = [];
      
      const geometry = this.createRoundedBoxGeometry(this.boxWidth, this.boxLength, 0.8);
      
      for (let x = 0; x < this.gridSize; x++) {
        for (let z = 0; z < this.gridSize; z++) {
          const posX = (x - (this.gridSize - 1) / 2) * this.spacingX;
          const posZ = (z - (this.gridSize - 1) / 2) * this.spacingZ;
          
          const material = new THREE.MeshPhysicalMaterial({
            color: this.gridColor,
            roughness: this.roughness,
            metalness: this.metalness,
            clearcoat: 1,
            clearcoatRoughness: 0
          });
          
          const mesh = new THREE.Mesh(geometry, material);
          mesh.position.set(posX, -0.85, posZ);
          mesh.rotation.set(Math.PI / 2, 0, 0);
          mesh.userData = {
            gridPosition: [x, z],
            currentScale: 1,
            targetScale: 1
          };
          
          this.scene.add(mesh);
          this.boxes.push(mesh);
        }
      }
    }
    
    setupEventListeners() {
      this.onMouseMove = this.onMouseMove.bind(this);
      this.onResize = this.onResize.bind(this);
      
      this.container.addEventListener('mousemove', this.onMouseMove);
      this.container.addEventListener('mouseleave', () => {
        this.hoveredBox = null;
      });
      window.addEventListener('resize', this.onResize);
    }
    
    onMouseMove(event) {
      const rect = this.container.getBoundingClientRect();
      this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
      this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
      
      this.detectHover();
    }
    
    detectHover() {
      this.raycaster.setFromCamera(this.mouse, this.camera);
      const intersects = this.raycaster.intersectObjects(this.boxes);
      
      if (intersects.length > 0) {
        const mesh = intersects[0].object;
        if (mesh.userData && mesh.userData.gridPosition) {
          this.hoveredBox = mesh.userData.gridPosition;
          return;
        }
      }
      
      this.hoveredBox = null;
    }
    
    updateBoxScales() {
      this.boxes.forEach(box => {
        const gridPosition = box.userData.gridPosition;
        let targetScale = 1;
        
        const isThisBoxHovered = this.hoveredBox && 
          gridPosition[0] === this.hoveredBox[0] && 
          gridPosition[1] === this.hoveredBox[1];
        
        if (isThisBoxHovered) {
          targetScale = 5;
        } else if (this.hoveredBox) {
          const dx = gridPosition[0] - this.hoveredBox[0];
          const dz = gridPosition[1] - this.hoveredBox[1];
          const distance = Math.sqrt(dx * dx + dz * dz);
          
          if (distance <= this.rippleRadius && distance > 0) {
            const falloff = Math.max(0, 1 - (distance / this.rippleRadius));
            const rippleEffect = falloff * this.rippleScale;
            targetScale = 1 + (rippleEffect * 3);
          }
        }
        
        box.userData.targetScale = targetScale;
        
        const lerpFactor = 0.1 * this.animationSpeed;
        const newScale = box.userData.currentScale + (targetScale - box.userData.currentScale) * lerpFactor;
        box.userData.currentScale = newScale;
        
        box.scale.z = newScale;
      });
    }
    
    onResize() {
      if (this.isDestroyed) return;
      
      const width = this.container.offsetWidth;
      const height = this.container.offsetHeight;
      
      this.camera.aspect = width / height;
      this.camera.updateProjectionMatrix();
      this.renderer.setSize(width, height);
    }
    
    animate() {
      if (this.isDestroyed) return;
      
      this.animationId = requestAnimationFrame(() => this.animate());
      
      this.updateBoxScales();
      this.renderer.render(this.scene, this.camera);
    }
    
    destroy() {
      this.isDestroyed = true;
      
      if (this.animationId) {
        cancelAnimationFrame(this.animationId);
      }
      
      this.container.removeEventListener('mousemove', this.onMouseMove);
      window.removeEventListener('resize', this.onResize);
      
      this.boxes.forEach(box => {
        if (box.geometry) box.geometry.dispose();
        if (box.material) box.material.dispose();
      });
      
      if (this.renderer) {
        this.renderer.dispose();
        if (this.renderer.domElement.parentNode) {
          this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
        }
      }
      
      this.scene = null;
      this.camera = null;
      this.renderer = null;
      this.boxes = [];
    }
  }

  window.EnhancedGrid3DAnimation = EnhancedGrid3DAnimation;
  EnhancedGrid3DAnimation.init();
})(window);`;
      }

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

      function copyToClipboard(text) {
        navigator.clipboard.writeText(text)
          .then(() => {
            showNotification('Copied to clipboard!');
          })
          .catch(err => {
            showNotification('Failed to copy to clipboard', 'error');
          });
      }

      window.resetParameter = function(parameterId, defaultValue) {
        const element = document.getElementById(parameterId);
        if (element) {
          element.value = defaultValue;
          const valueElement = document.getElementById(`${parameterId}-value`);
          const valueElement2 = document.getElementById(`${parameterId}-value2`);
          if (valueElement) {
            valueElement.textContent = defaultValue;
          }
          if (valueElement2) {
            valueElement2.textContent = defaultValue;
          }
          
          switch (parameterId) {
            case 'grid-size':
              grid3DConfig.gridSize = defaultValue;
              break;
            case 'box-size':
              grid3DConfig.boxSize = defaultValue;
              break;
            case 'ripple-intensity':
              grid3DConfig.rippleIntensity = defaultValue;
              break;
            case 'ripple-radius':
              grid3DConfig.rippleRadius = defaultValue;
              break;
            case 'animation-speed':
              grid3DConfig.animationSpeed = defaultValue;
              break;
            case 'metalness':
              grid3DConfig.metalness = defaultValue;
              break;
            case 'roughness':
              grid3DConfig.roughness = defaultValue;
              break;
          }
          
          updateGrid3DPreview();
          showNotification(`${parameterId.replace(/-/g, ' ')} reset to default`);
        }
      };



      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 {
          h: Math.round(h * 360),
          s: Math.round(s * 100),
          l: 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 updateColorInputs() {
        const colorInput = document.getElementById('grid-color');
        const hexInput = document.getElementById('grid-color-hex');
        const hslInput = document.getElementById('grid-color-hsl');
        
        if (colorInput && hexInput && hslInput) {
          colorInput.value = grid3DConfig.gridColor;
          hexInput.value = grid3DConfig.gridColor;
          hslInput.value = `hsl(${hexToHsl(grid3DConfig.gridColor).h}, ${hexToHsl(grid3DConfig.gridColor).s}%, ${hexToHsl(grid3DConfig.gridColor).l}%)`;
          
          hexInput.classList.remove('invalid');
          hslInput.classList.remove('invalid');
          
          const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
          if (colorPickerContainer) {
            colorPickerContainer.style.setProperty('--selected-color', grid3DConfig.gridColor);
          }
        }
      }

      function initializeUI() {
        setTimeout(() => {
          initGrid3DFlow();
        }, 100);
        
        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-grid3d');
        });

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

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

        const backgroundPicker = document.getElementById('preview-background-picker');
        const previewContainer = document.getElementById('grid-preview');

        backgroundPicker.addEventListener('input', (e) => {
          const selectedColor = e.target.value;
          previewContainer.style.backgroundColor = selectedColor;
          
          showNotification(`Preview background changed to ${selectedColor}`);
        });

        previewContainer.style.backgroundColor = '#252525';

        document.getElementById('reset-grid').addEventListener('click', () => {
          grid3DConfig.gridSize = defaultConfig.gridSize;
          grid3DConfig.boxSize = defaultConfig.boxSize;
          grid3DConfig.rippleIntensity = defaultConfig.rippleIntensity;
          grid3DConfig.rippleRadius = defaultConfig.rippleRadius;
          
          document.getElementById('grid-size').value = defaultConfig.gridSize;
          document.getElementById('box-size').value = defaultConfig.boxSize;
          document.getElementById('ripple-intensity').value = defaultConfig.rippleIntensity;
          document.getElementById('ripple-radius').value = defaultConfig.rippleRadius;
          
          document.getElementById('grid-size-value').textContent = defaultConfig.gridSize;
          document.getElementById('grid-size-value2').textContent = defaultConfig.gridSize;
          document.getElementById('box-size-value').textContent = defaultConfig.boxSize;
          document.getElementById('ripple-intensity-value').textContent = defaultConfig.rippleIntensity;
          document.getElementById('ripple-radius-value').textContent = defaultConfig.rippleRadius;
          
          updateGrid3DPreview();
          showNotification('Grid settings reset');
        });

        document.getElementById('reset-visual').addEventListener('click', () => {
          grid3DConfig.gridColor = defaultConfig.gridColor;
          grid3DConfig.metalness = defaultConfig.metalness;
          grid3DConfig.roughness = defaultConfig.roughness;
          
          document.getElementById('grid-color').value = defaultConfig.gridColor;
          document.getElementById('metalness').value = defaultConfig.metalness;
          document.getElementById('roughness').value = defaultConfig.roughness;
          
          document.getElementById('metalness-value').textContent = defaultConfig.metalness;
          document.getElementById('roughness-value').textContent = defaultConfig.roughness;
          
          updateColorInputs();
          updateGrid3DPreview();
          showNotification('Visual settings reset');
        });

        document.getElementById('reset-advanced').addEventListener('click', () => {
          grid3DConfig.animationSpeed = defaultConfig.animationSpeed;
          
          document.getElementById('animation-speed').value = defaultConfig.animationSpeed;
          document.getElementById('animation-speed-value').textContent = defaultConfig.animationSpeed;
          
          updateGrid3DPreview();
          showNotification('Advanced settings reset');
        });

        const colorInput = document.getElementById('grid-color');
        const hexInput = document.getElementById('grid-color-hex');
        const hslInput = document.getElementById('grid-color-hsl');
        
        hslInput.value = `hsl(${hexToHsl(colorInput.value).h}, ${hexToHsl(colorInput.value).s}%, ${hexToHsl(colorInput.value).l}%)`;
        
        colorInput.addEventListener('input', () => {
          const color = colorInput.value;
          hexInput.value = color;
          hslInput.value = `hsl(${hexToHsl(color).h}, ${hexToHsl(color).s}%, ${hexToHsl(color).l}%)`;
          hexInput.classList.remove('invalid');
          hslInput.classList.remove('invalid');
          grid3DConfig.gridColor = color;
          
          const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
          colorPickerContainer.style.setProperty('--selected-color', color);
          
          updateGrid3DPreview();
        });
        
        hexInput.addEventListener('input', (e) => {
          let hex = e.target.value;
          
          hex = formatHex(hex);
          e.target.value = hex;
          
          if (isValidHex(hex)) {
            colorInput.value = hex;
            hslInput.value = `hsl(${hexToHsl(hex).h}, ${hexToHsl(hex).s}%, ${hexToHsl(hex).l}%)`;
            grid3DConfig.gridColor = hex;
            e.target.classList.remove('invalid');
            hslInput.classList.remove('invalid');
            
            const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
            colorPickerContainer.style.setProperty('--selected-color', hex);
            
            updateGrid3DPreview();
          } else {
            e.target.classList.add('invalid');
          }
        });
        
        hexInput.addEventListener('blur', (e) => {
          if (!isValidHex(e.target.value)) {
            e.target.value = colorInput.value;
            e.target.classList.remove('invalid');
          }
        });
        
        hslInput.addEventListener('input', (e) => {
          let hsl = e.target.value;
          
          if (isValidHsl(hsl)) {
            const hex = hslToHex(hsl);
            if (hex) {
              colorInput.value = hex;
              hexInput.value = hex;
              grid3DConfig.gridColor = hex;
              e.target.classList.remove('invalid');
              hexInput.classList.remove('invalid');
              
              const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
              colorPickerContainer.style.setProperty('--selected-color', hex);
              
              updateGrid3DPreview();
              return;
            }
          }
          
          e.target.classList.add('invalid');
        });
        
        hslInput.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) {
                colorInput.value = hex;
                hexInput.value = hex;
                grid3DConfig.gridColor = hex;
                e.target.classList.remove('invalid');
                hexInput.classList.remove('invalid');
                updateGrid3DPreview();
                return;
              }
            }
          }
          
          if (!isValidHsl(e.target.value)) {
            e.target.value = `hsl(${hexToHsl(colorInput.value).h}, ${hexToHsl(colorInput.value).s}%, ${hexToHsl(colorInput.value).l}%)`;
            e.target.classList.remove('invalid');
          }
        });

        const rangeInputs = document.querySelectorAll('input[type="range"]');
        rangeInputs.forEach(input => {
          const valueElement = document.getElementById(`${input.id}-value`);
          const valueElement2 = document.getElementById(`${input.id}-value2`);
          
          if (valueElement) {
            valueElement.textContent = input.value;
          }
          if (valueElement2) {
            valueElement2.textContent = input.value;
          }
          
          input.addEventListener('input', () => {
            if (valueElement) {
              valueElement.textContent = input.value;
            }
            if (valueElement2) {
              valueElement2.textContent = input.value;
            }
            
            switch (input.id) {
              case 'grid-size':
                grid3DConfig.gridSize = parseInt(input.value);
                break;
              case 'box-size':
                grid3DConfig.boxSize = parseFloat(input.value);
                break;
              case 'ripple-intensity':
                grid3DConfig.rippleIntensity = parseFloat(input.value);
                break;
              case 'ripple-radius':
                grid3DConfig.rippleRadius = parseFloat(input.value);
                break;
              case 'animation-speed':
                grid3DConfig.animationSpeed = parseFloat(input.value);
                break;
              case 'metalness':
                grid3DConfig.metalness = parseFloat(input.value);
                break;
              case 'roughness':
                grid3DConfig.roughness = parseFloat(input.value);
                break;
            }
            
            updateGrid3DPreview();
          });
        });

        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 'b':
                document.getElementById('preview-background-picker').click();
                break;
            }
          }
        });

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

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

        function loadConfiguration() {
          try {
            const saved = localStorage.getItem('bricksfusion-grid3d-config');
            if (saved) {
              const savedConfig = JSON.parse(saved);
              Object.assign(grid3DConfig, savedConfig);
              
              document.getElementById('grid-size').value = savedConfig.gridSize;
              document.getElementById('box-size').value = savedConfig.boxSize;
              document.getElementById('ripple-intensity').value = savedConfig.rippleIntensity;
              document.getElementById('ripple-radius').value = savedConfig.rippleRadius;
              document.getElementById('animation-speed').value = savedConfig.animationSpeed;
              document.getElementById('grid-color').value = savedConfig.gridColor;
              document.getElementById('metalness').value = savedConfig.metalness;
              document.getElementById('roughness').value = savedConfig.roughness;
              
              document.getElementById('grid-size-value').textContent = savedConfig.gridSize;
              document.getElementById('grid-size-value2').textContent = savedConfig.gridSize;
              document.getElementById('box-size-value').textContent = savedConfig.boxSize;
              document.getElementById('ripple-intensity-value').textContent = savedConfig.rippleIntensity;
              document.getElementById('ripple-radius-value').textContent = savedConfig.rippleRadius;
              document.getElementById('animation-speed-value').textContent = savedConfig.animationSpeed;
              document.getElementById('metalness-value').textContent = savedConfig.metalness;
              document.getElementById('roughness-value').textContent = savedConfig.roughness;
              
              updateColorInputs();
            }
          } catch (e) {
          }
        }

        const originalUpdateGrid3DPreview = updateGrid3DPreview;
        updateGrid3DPreview = function() {
          originalUpdateGrid3DPreview();
          saveConfiguration();
        };

        loadConfiguration();
      }
      
      initializeUI();
    });
  </script>
</body>
</html>
Grid 3D - Bricksfusion
HEAVY

Grid 3D

Creates an interactive 3D grid with ripple elevation effects. Rounded boxes rise and fall based on mouse proximity using smooth interpolation. Uses Three.js with PerspectiveCamera, custom rounded geometry, and MeshPhysicalMaterial for realistic lighting. Perfect for hero sections, interactive backgrounds, or modern portfolio headers.

Grid 3D

Move your mouse over the grid to create ripple effects.

Grid Settings

Grid Size 5-20

Number of boxes per row/column. Higher creates denser grid but impacts performance significantly.

Default: 10

Box Size 0.5-2.0

Scale multiplier for individual box dimensions. Larger boxes create bolder, more prominent grid.

Default: 1.0

Ripple Effect

Intensity 0.5-5.0

Strength of elevation effect. Higher creates more dramatic height changes on hover.

Default: 2.5

Radius 1.0-5.0

Distance ripple effect spreads from cursor. Larger radius affects more surrounding boxes.

Default: 2.0

Animation

Speed 0.5-2.0

Transition speed multiplier. Higher is more responsive, lower is smoother and more elegant.

Default: 1.0

Appearance

Grid Color hex color

Base color of grid boxes. Works with lighting system for realistic shading.

Default: #081316 (dark teal)

Metalness 0.0-1.0

Metallic appearance intensity. Higher creates more reflective, metal-like surface.

Default: 1.0

Roughness 0.0-1.0

Surface roughness. Lower is smoother and shinier, higher is matte and diffuse.

Default: 0.5

Performance

This element uses WebGL with Three.js r128 for GPU-accelerated 3D rendering. Creates custom ExtrudeGeometry for rounded boxes with bevels. Uses MeshPhysicalMaterial with clearcoat for realistic reflections. Features multiple directional and point lights with shadow mapping. Implements raycasting for mouse detection with smooth scale interpolation. Very resource intensive - limit to 1 instance per page. Performance scales exponentially with grid size (10x10 = 100 boxes, 20x20 = 400 boxes). Not recommended for mobile devices.