v2.2
MENU ANIMATIONS
UI SURECART
BUTTONS
<!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
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
Detail level of fluid simulation physics. Lower is faster but less detailed, higher is more realistic but intensive.
Default: 128
Quality of rendered colors and textures. Higher creates sharper, more vibrant fluid trails.
Default: 1024
Dissipation
How quickly colors fade away. Lower makes trails disappear fast, higher keeps colors longer.
Default: 0.8
How quickly fluid motion slows down. Lower creates short-lived movements, higher maintains flow longer.
Default: 0.2
Physics
Base pressure in fluid system. Affects how fluid spreads and flows naturally.
Default: 0.43
Accuracy of pressure calculations. Higher is more realistic but slower. Affects fluid incompressibility.
Default: 26
Strength of vorticity confinement. Creates swirling, turbulent motion. Higher adds more chaos and detail.
Default: 25
Splat
Size of cursor interaction area. Larger radius creates broader fluid strokes.
Default: 0.45
Intensity of cursor impact on fluid. Higher creates more dramatic, explosive interactions.
Default: 13000
Appearance
Enable 3D-like shading effects. Adds depth with lighting calculations based on fluid gradients.
Default: On
How quickly colors cycle through spectrum. Higher changes colors faster for more variety.
Default: 10
Enable alpha transparency. Allows fluid to blend with background content underneath.
Default: On
Set specific color or use 'random' for rainbow spectrum. Fixed colors create cohesive branding.
Default: random
Effects
Enable bloom glow effect. Creates luminous halos around bright fluid areas for a dreamy, ethereal look.
Default: Off
Strength of bloom glow. Higher values create more intense, radiant effects.
Default: 0.8
Brightness level needed to trigger bloom. Lower values bloom more areas, higher limits to brightest spots.
Default: 0.6
Quality of bloom blur passes. Higher creates smoother, more diffused glow but impacts performance.
Default: 8
Enable volumetric light rays effect. Creates radial god rays emanating from bright fluid centers.
Default: Off
Intensity of light ray effect. Higher creates more dramatic, visible rays.
Default: 1.0
Mobile
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.