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>Ethereal Trail Configurator - BricksFusion</title>
<style>
:root {
--background: #000;
--card-bg: #1e1e1e;
--card-bg-hover: #252525;
--text-primary: #f2f2f7;
--text-secondary: #8e8e93;
--accent: #ef6013;
--accent-hover: #c64c0c;
--border: #2c2c2e;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
--track: #2c2c2e;
--thumb: #ef6013;
--card-radius: 16px;
--input-radius: 8px;
--button-radius: 12px;
--transition: all 0.25s ease;
--font: 'Inter', BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Arial, sans-serif;
--action-bar-height: 70px;
--success: #28a745;
--warning: #ffc107;
--danger: #dc3545;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font);
background-color: var(--background);
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding-bottom: var(--action-bar-height);
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--action-bar-height);
background: linear-gradient(145deg, #1a1a1a, #0f0f0f);
border-top: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
padding: 0 1.5rem;
gap: 1rem;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.breadcrumb-item {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 500;
text-decoration: none;
transition: var(--transition);
padding: 0.5rem 0.75rem;
border-radius: 6px;
}
.breadcrumb-item:hover {
color: var(--text-primary);
background-color: rgba(255, 255, 255, 0.05);
}
.breadcrumb-item.active {
color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.breadcrumb-separator {
color: var(--text-secondary);
font-size: var(--text-xs);
opacity: 0.5;
}
.action-buttons {
display: flex;
align-items: center;
gap: 0.75rem;
}
.action-btn {
padding: 0.6rem 1rem;
background-color: var(--card-bg);
color: var(--text-primary);
font-family: var(--font);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid var(--border);
border-radius: var(--button-radius);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
white-space: nowrap;
}
.action-btn:hover {
background-color: var(--card-bg-hover);
border-color: var(--accent);
transform: translateY(-1px);
}
.action-btn.primary {
background: linear-gradient(90deg, var(--accent), #ff8c51);
border-color: var(--accent);
color: white;
}
.action-btn.primary:hover {
background: linear-gradient(90deg, var(--accent-hover), #e67a3f);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 96, 19, 0.3);
}
.data-attribute-display {
background-color: rgba(50, 50, 50, 0.8);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
cursor: pointer;
transition: var(--transition);
user-select: all;
}
.data-attribute-display:hover {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
}
.container {
max-width: 100%;
margin: 0 auto;
padding: 2rem 1.5rem;
}
.page-header {
text-align: center;
margin-bottom: 2rem;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
background: linear-gradient(90deg, var(--accent), #ff8c51);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.page-subtitle {
font-size: var(--text-s);
color: var(--text-secondary);
font-weight: 500;
}
.instructions-toggle {
margin-bottom: 2rem;
}
.instructions-card {
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
transition: var(--transition);
}
.instructions-header {
padding: 1rem 1.5rem;
cursor: pointer;
transition: var(--transition);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid transparent;
}
.instructions-header:hover {
background-color: var(--card-bg-hover);
}
.instructions-card.expanded .instructions-header {
border-bottom-color: var(--border);
}
.instructions-title {
font-size: var(--text-s);
font-weight: 600;
}
.toggle-icon {
font-size: 1.2em;
transition: transform 0.3s ease;
}
.toggle-icon.expanded {
transform: rotate(180deg);
}
.instructions-content {
padding: 0 1.5rem;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.instructions-content.show {
max-height: 500px;
padding: 1.5rem;
}
.instructions-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.how-to-use ol {
padding-left: 1.5rem;
}
.how-to-use li {
margin-bottom: 0.75rem;
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.6;
}
.how-to-use strong {
color: var(--text-primary);
font-weight: 600;
}
.how-to-use code {
background-color: rgba(50, 50, 50, 0.5);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
}
.content {
display: grid;
grid-template-columns: 1fr 500px;
gap: 2rem;
align-items: start;
}
.preview-section {
position: sticky;
top: 2rem;
}
.controls-section {
max-width: 500px;
}
.card {
background-color: var(--card-bg);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}
.preview-container {
height: 400px;
width: 100%;
position: relative;
overflow: hidden;
border-radius: var(--card-radius);
background-color: #000000;
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.preview-content {
color: white;
text-align: center;
font-weight: bold;
font-size: var(--text-s);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
.preview-controls {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.5rem;
z-index: 10;
}
.preview-btn {
padding: 0.5rem;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
cursor: pointer;
transition: var(--transition);
font-size: var(--text-xs);
backdrop-filter: blur(5px);
}
.preview-btn:hover {
background-color: var(--accent);
border-color: var(--accent);
}
.preview-btn svg {
width: 18px;
height: 18px;
stroke: currentColor;
}
.background-selector-wrapper {
position: relative;
display: inline-block;
}
.background-selector-btn {
position: relative;
}
.background-selector-btn:hover {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
box-shadow: 0 0 8px rgba(239, 96, 19, 0.3);
}
.hidden-color-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 1;
}
.card-heading {
padding: 1rem 1.5rem;
font-size: var(--text-s);
font-weight: 600;
border-bottom: 1px solid var(--border);
letter-spacing: 0.3px;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-actions {
display: flex;
gap: 0.5rem;
}
.card-action-btn {
padding: 0.4rem 0.8rem;
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
font-size: var(--text-xs);
transition: var(--transition);
}
.card-action-btn:hover {
color: var(--text-primary);
border-color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.card-content {
padding: 1.5rem;
}
.control-group {
margin-bottom: 1.5rem;
position: relative;
}
.control-group:last-child {
margin-bottom: 0;
}
.control-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.label-text {
font-size: var(--text-xs);
font-weight: 500;
letter-spacing: 0.2px;
display: flex;
align-items: center;
gap: 0.5rem;
}
.help-tooltip {
cursor: help;
opacity: 0.7;
transition: var(--transition);
}
.help-tooltip:hover {
opacity: 1;
color: var(--accent);
}
.value-display {
display: flex;
align-items: center;
gap: 0.5rem;
}
.value-text {
font-size: var(--text-xs);
color: var(--text-secondary);
background-color: rgba(50, 50, 50, 0.5);
padding: 2px 8px;
border-radius: 4px;
min-width: 45px;
text-align: center;
}
.reset-btn {
padding: 0.2rem 0.4rem;
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
font-size: 10px;
transition: var(--transition);
}
.reset-btn:hover {
color: var(--danger);
border-color: var(--danger);
background-color: rgba(220, 53, 69, 0.1);
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: var(--track);
border-radius: 3px;
outline: none;
margin: 0.8rem 0;
position: relative;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: var(--thumb);
border-radius: 50%;
cursor: pointer;
transition: var(--transition);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 10px rgba(239, 96, 19, 0.5);
}
.color-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
}
.color-row {
display: flex;
align-items: center;
gap: 1.25rem;
padding: 1rem 1.25rem;
background-color: rgba(30, 30, 30, 0.7);
border: 1px solid var(--border);
border-radius: var(--input-radius);
transition: var(--transition);
}
.color-row:hover {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(239, 96, 19, 0.1);
}
.color-picker-container {
position: relative;
width: 40px;
height: 40px;
border-radius: 8px;
overflow: hidden;
border: 2px solid var(--border);
cursor: pointer;
transition: var(--transition);
flex-shrink: 0;
background: var(--card-bg);
display: flex;
align-items: center;
justify-content: center;
--selected-color: #00ff88;
}
.color-picker-container:hover {
border-color: var(--accent);
transform: scale(1.05);
box-shadow: 0 0 12px rgba(239, 96, 19, 0.3);
}
.color-picker-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--selected-color, #00ff88);
border-radius: 6px;
transition: var(--transition);
}
input[type="color"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
cursor: pointer;
background: transparent;
opacity: 0;
z-index: 2;
}
.color-input-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.color-label {
font-size: 10px;
font-weight: 500;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 0.25rem;
}
.color-input {
padding: 0.5rem 0.75rem;
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 12px;
transition: var(--transition);
min-width: 0;
}
.color-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(239, 96, 19, 0.2);
outline: none;
}
.color-input.invalid {
border-color: var(--danger);
box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.2);
}
.hex-input,
.hsl-input {
width: 100%;
}
.color-input-group:nth-child(2) {
flex: 0.3;
}
.color-input-group:nth-child(3) {
flex: 0.7;
}
select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: var(--input-radius);
font-family: var(--font);
font-size: var(--text-xs);
color: var(--text-primary);
background-color: var(--card-bg);
margin-bottom: 0.75rem;
outline: none;
transition: var(--transition);
}
select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
.notification {
position: fixed;
bottom: calc(var(--action-bar-height) + 1rem);
left: 50%;
background-color: var(--success);
color: white;
padding: 0.75rem 1rem;
border-radius: var(--input-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1001;
transform: translate(-50%, 200px);
opacity: 0;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease;
font-size: var(--text-xs);
font-weight: 500;
max-width: 320px;
word-wrap: break-word;
line-height: 1.4;
text-align: center;
}
.notification.show {
transform: translate(-50%, 0);
opacity: 1;
}
@media (max-width: 1200px) {
.content {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.preview-section {
position: static;
}
.controls-section {
max-width: 100%;
}
}
@media (max-width: 768px) {
.action-bar {
flex-direction: column;
height: auto;
min-height: var(--action-bar-height);
padding: 0.75rem;
}
.breadcrumb {
order: 1;
width: 100%;
}
.action-buttons {
order: 2;
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
body {
padding-bottom: calc(var(--action-bar-height) + 20px);
}
.notification {
bottom: calc(var(--action-bar-height) + 2rem);
max-width: 280px;
transform: translate(-50%, 250px);
}
.notification.show {
transform: translate(-50%, 0);
opacity: 1;
}
.color-row {
flex-direction: column;
align-items: stretch;
gap: 1rem;
padding: 1rem;
}
.color-picker-container {
align-self: center;
margin-bottom: 0.5rem;
}
.color-input-group {
align-items: stretch;
}
.hex-input,
.hsl-input {
width: 100%;
}
.preview-container {
height: 300px;
}
.data-attribute-display {
font-size: 10px;
padding: 0.4rem 0.6rem;
}
.action-btn {
font-size: 11px;
padding: 0.5rem 0.8rem;
}
.page-title {
font-size: 2rem;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
button:focus-visible,
input:focus-visible,
.action-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--background);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
.loading {
opacity: 0.6;
pointer-events: none;
position: relative;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="action-bar">
<nav class="breadcrumb">
<a href="https://bricksfusion.com" class="breadcrumb-item">Home</a>
<span class="breadcrumb-separator">›</span>
<a href="https://bricksfusion.com/visual-effects/" class="breadcrumb-item">Visual effects</a>
<span class="breadcrumb-separator">›</span>
<span class="breadcrumb-item active">Ethereal Trail</span>
</nav>
<div class="action-buttons">
<div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
data-ethereal-trail
</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">Ethereal Trail</h1>
<p class="page-subtitle">Interactive cursor trail effects for Bricks Builder</p>
</div>
<div class="instructions-toggle">
<div class="instructions-card" id="instructions-card">
<div class="instructions-header" id="instructions-toggle">
<div class="instructions-title">
How to Use & Code Information
</div>
<span class="toggle-icon">▼</span>
</div>
<div class="instructions-content" id="instructions-content">
<div class="instructions-grid">
<div class="how-to-use">
<ol>
<li>Customize your ethereal trail 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-ethereal-trail</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="ethereal-preview" data-ethereal-trail="true">
<div class="preview-content">Interactive Ethereal Trail Preview</div>
<div class="preview-controls">
<button class="preview-btn" id="randomize-ethereal" title="Randomize (R)">🎲</button>
<div class="background-selector-wrapper">
<button class="preview-btn background-selector-btn" id="background-selector">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12,2 2,7 12,12 22,7"/>
<polyline points="2,17 12,22 22,17"/>
<polyline points="2,12 12,17 22,12"/>
</svg>
</button>
<input type="color" id="preview-background-picker" class="hidden-color-input" value="#000000" title="Change Preview Background (B)">
</div>
</div>
</div>
</section>
<section class="controls-section">
<div class="card">
<div class="card-heading">
Trail Color
<div class="card-actions">
<button class="card-action-btn" id="reset-colors" title="Reset Colors">↺</button>
</div>
</div>
<div class="card-content">
<div class="color-list">
<div class="color-row">
<div class="color-picker-container">
<input type="color" id="trail-color" value="#00ff88">
</div>
<div class="color-input-group">
<span class="color-label">HEX</span>
<input type="text" class="color-input hex-input" id="trail-color-hex" value="#00ff88" placeholder="#FFFFFF">
</div>
<div class="color-input-group">
<span class="color-label">HSL</span>
<input type="text" class="color-input hsl-input" id="trail-color-hsl" placeholder="hsl(0, 100%, 50%)">
</div>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Color Variation
<span class="help-tooltip" title="Adds subtle color shifts to the trails">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="color-variation-value">20</span>°</span>
<button class="reset-btn" onclick="resetParameter('color-variation', 20)">↺</button>
</div>
</div>
<input type="range" id="color-variation" min="0" max="60" step="5" value="20">
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Animation Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-animation" title="Reset Animation Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Trail Quality
<span class="help-tooltip" title="Higher quality creates smoother trails">ℹ</span>
</span>
</div>
<select id="trail-quality">
<option value="low">Low (40 trails)</option>
<option value="medium" selected>Medium (60 trails)</option>
<option value="high">High (80 trails)</option>
</select>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Trail Speed
<span class="help-tooltip" title="Controls how quickly the trails follow the cursor">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="trail-speed-value">0.5</span>x</span>
<button class="reset-btn" onclick="resetParameter('trail-speed', 0.5)">↺</button>
</div>
</div>
<input type="range" id="trail-speed" min="0.1" max="1" step="0.1" value="0.5">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Trail Length
<span class="help-tooltip" title="Length of the trailing effect">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="trail-length-value">50</span></span>
<button class="reset-btn" onclick="resetParameter('trail-length', 50)">↺</button>
</div>
</div>
<input type="range" id="trail-length" min="20" max="100" step="5" value="50">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Trail Width
<span class="help-tooltip" title="Thickness of the trail lines">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="trail-width-value">1</span>px</span>
<button class="reset-btn" onclick="resetParameter('trail-width', 1)">↺</button>
</div>
</div>
<input type="range" id="trail-width" min="0.5" max="3" step="0.5" value="1">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Opacity
<span class="help-tooltip" title="Transparency level of the trails">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="trail-opacity-value">10</span>%</span>
<button class="reset-btn" onclick="resetParameter('trail-opacity', 10)">↺</button>
</div>
</div>
<input type="range" id="trail-opacity" min="5" max="30" step="5" value="10">
</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">Blend Mode</span>
</div>
<select id="blend-mode">
<option value="source-over">Normal</option>
<option value="lighter" selected>Lighter (Glowing)</option>
<option value="screen">Screen</option>
<option value="multiply">Multiply</option>
<option value="overlay">Overlay</option>
</select>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Spring Tension
<span class="help-tooltip" title="How tightly the trails follow the cursor">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="spring-tension-value">0.45</span></span>
<button class="reset-btn" onclick="resetParameter('spring-tension', 0.45)">↺</button>
</div>
</div>
<input type="range" id="spring-tension" min="0.1" max="0.8" step="0.05" value="0.45">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Friction
<span class="help-tooltip" title="Controls the smoothness of movement">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="friction-value">0.5</span></span>
<button class="reset-btn" onclick="resetParameter('friction', 0.5)">↺</button>
</div>
</div>
<input type="range" id="friction" min="0.3" max="0.9" step="0.05" value="0.5">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Dampening
<span class="help-tooltip" title="How quickly the motion settles">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="dampening-value">0.025</span></span>
<button class="reset-btn" onclick="resetParameter('dampening', 0.025)">↺</button>
</div>
</div>
<input type="range" id="dampening" min="0.01" max="0.05" step="0.005" value="0.025">
</div>
</div>
</div>
</section>
</div>
</div>
<div class="notification" id="notification"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let etherealConfig = {
quality: 'medium',
speed: 0.5,
length: 50,
color: '#00ff88',
colorVariation: 20,
width: 1,
opacity: 10,
blendMode: 'lighter',
springTension: 0.45,
friction: 0.5,
dampening: 0.025
};
const defaultConfig = { ...etherealConfig };
let activeEthereal = null;
function initEtherealTrail() {
const sections = document.querySelectorAll('[data-ethereal-trail]:not([data-ethereal-initialized="true"])');
sections.forEach((section) => {
const options = {
quality: section.getAttribute('data-ethereal-quality') || etherealConfig.quality,
speed: parseFloat(section.getAttribute('data-ethereal-speed')) || etherealConfig.speed,
length: parseInt(section.getAttribute('data-ethereal-length')) || etherealConfig.length,
color: section.getAttribute('data-ethereal-color') || etherealConfig.color,
colorVariation: parseInt(section.getAttribute('data-ethereal-color-variation')) || etherealConfig.colorVariation,
width: parseFloat(section.getAttribute('data-ethereal-width')) || etherealConfig.width,
opacity: parseInt(section.getAttribute('data-ethereal-opacity')) || etherealConfig.opacity,
blendMode: section.getAttribute('data-ethereal-blend-mode') || etherealConfig.blendMode,
springTension: parseFloat(section.getAttribute('data-ethereal-spring-tension')) || etherealConfig.springTension,
friction: parseFloat(section.getAttribute('data-ethereal-friction')) || etherealConfig.friction,
dampening: parseFloat(section.getAttribute('data-ethereal-dampening')) || etherealConfig.dampening
};
setupEtherealTrail(section, options);
section.dataset.etherealInitialized = 'true';
if (section.id === 'ethereal-preview') {
activeEthereal = { element: section, options };
etherealConfig = { ...options };
}
});
}
function setupEtherealTrail(element, options) {
const canvas = document.createElement('canvas');
canvas.style.cssText = 'position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10;';
canvas.setAttribute('data-ethereal-canvas', 'true');
element.appendChild(canvas);
const ctx = canvas.getContext('2d');
const rect = element.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
const pos = { x: rect.width / 2, y: rect.height / 2 };
const lines = [];
const qualitySettings = {
low: 40,
medium: 60,
high: 80
};
const trails = qualitySettings[options.quality] || qualitySettings.medium;
const speedFactor = Math.max(0.1, Math.min(1, options.speed));
for (let i = 0; i < trails; i++) {
const spring = options.springTension + Math.random() * 0.025;
const friction = options.friction * speedFactor + (Math.random() * 0.01 - 0.005);
const nodes = Array(options.length).fill().map(() => ({
x: pos.x, y: pos.y, vx: 0, vy: 0
}));
lines.push({
spring,
friction,
nodes
});
}
element.addEventListener('mousemove', (e) => {
const rect = element.getBoundingClientRect();
if (
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom
) {
pos.x = e.clientX - rect.left;
pos.y = e.clientY - rect.top;
}
});
function hexToHSL(hex) {
let r = parseInt(hex.substring(1,3), 16) / 255;
let g = parseInt(hex.substring(3,5), 16) / 255;
let b = parseInt(hex.substring(5,7), 16) / 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
let 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 *= 60;
}
return {h, s: s*100, l: l*100};
}
function getColor(timestamp) {
const hsl = hexToHSL(options.color);
const opacity = options.opacity / 100;
const variation = options.colorVariation;
return `hsla(${hsl.h + Math.sin(timestamp * 0.001) * variation}, ${hsl.s}%, ${hsl.l}%, ${opacity})`;
}
function updateLine(line) {
let spring = line.spring;
let node = line.nodes[0];
node.vx += (pos.x - node.x) * spring;
node.vy += (pos.y - node.y) * spring;
for (let i = 0; i < line.nodes.length; i++) {
node = line.nodes[i];
if (i > 0) {
let prev = line.nodes[i - 1];
node.vx += (prev.x - node.x) * spring;
node.vy += (prev.y - node.y) * spring;
node.vx += prev.vx * options.dampening;
node.vy += prev.vy * options.dampening;
}
node.vx *= line.friction;
node.vy *= line.friction;
node.x += node.vx;
node.y += node.vy;
spring *= 0.99;
}
}
function drawLine(line) {
let x = line.nodes[0].x, y = line.nodes[0].y;
ctx.beginPath();
ctx.moveTo(x, y);
for (let i = 1; i < line.nodes.length - 2; i++) {
const node = line.nodes[i];
const next = line.nodes[i + 1];
x = (node.x + next.x) * 0.5;
y = (node.y + next.y) * 0.5;
ctx.quadraticCurveTo(node.x, node.y, x, y);
}
const node = line.nodes[line.nodes.length - 2];
const last = line.nodes[line.nodes.length - 1];
ctx.quadraticCurveTo(node.x, node.y, last.x, last.y);
ctx.stroke();
}
function render() {
const timestamp = Date.now();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = options.blendMode;
ctx.strokeStyle = getColor(timestamp);
ctx.lineWidth = options.width;
lines.forEach(line => {
updateLine(line);
drawLine(line);
});
requestAnimationFrame(render);
}
render();
window.addEventListener('resize', () => {
const rect = element.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
});
element.etherealInstance = {
updateConfig: function(newOptions) {
Object.assign(options, newOptions);
}
};
}
function updateEtherealPreview() {
if (activeEthereal && activeEthereal.element.etherealInstance) {
activeEthereal.element.etherealInstance.updateConfig(etherealConfig);
}
}
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 generateUniqueId() {
return Math.random().toString(36).substring(2, 8);
}
function generateFullSectionJSON() {
// Generar IDs únicos para todos los elementos
const sectionId = generateUniqueId();
const containerId = generateUniqueId();
const codeId = generateUniqueId();
const 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": {
"_height": "500",
"_justifyContent": "center",
"_background": {
"color": {
"hex": "#ffffff"
}
},
"_attributes": [{
"id": attributeId,
"name": "data-ethereal-trail"
}]
},
"label": "Ethereal Trail Section"
},
{
"id": containerId,
"name": "container",
"parent": sectionId,
"children": [],
"settings": {
"_alignItems": "center"
},
"label": ""
},
{
"id": codeId,
"name": "code",
"parent": sectionId,
"children": [],
"settings": {
"javascriptCode": jsCode,
"executeCode": true,
"_display": "none"
},
"label": "Ethereal Trail JS"
}
],
"source": "bricksCopiedElements",
"sourceUrl": "https://test.bricksfusion.com",
"version": "2.0.1",
"globalClasses": [],
"globalElements": []
};
return JSON.stringify(bricksJSON, null, 2);
}
function generateJavaScriptCode() {
return `(function(window) {
const EtherealTrailAnimation = {
instances: [],
config: {
quality: "${etherealConfig.quality}",
speed: ${etherealConfig.speed},
length: ${etherealConfig.length},
color: "${etherealConfig.color}",
colorVariation: ${etherealConfig.colorVariation},
width: ${etherealConfig.width},
opacity: ${etherealConfig.opacity},
blendMode: "${etherealConfig.blendMode}",
springTension: ${etherealConfig.springTension},
friction: ${etherealConfig.friction},
dampening: ${etherealConfig.dampening}
},
init: function(options = {}) {
const defaultOptions = {
selector: '[data-ethereal-trail]',
colorAttr: 'data-ethereal-color'
};
const config = { ...defaultOptions, ...options };
const initInstances = () => {
document.querySelectorAll(config.selector).forEach(element => {
if (!element.hasAttribute('data-ethereal-initialized')) {
this.createInstance(element, config);
element.setAttribute('data-ethereal-initialized', 'true');
}
});
};
initInstances();
setTimeout(initInstances, 100);
window.addEventListener('load', initInstances);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
initInstances();
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
},
createInstance: function(element, config) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
element.appendChild(canvas);
if (window.getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
Object.assign(canvas.style, {
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
pointerEvents: 'none',
zIndex: '10'
});
const instance = {
element,
canvas,
ctx,
width: 0,
height: 0,
pos: { x: 0, y: 0 },
lines: [],
options: {
quality: element.getAttribute('data-ethereal-quality') || this.config.quality,
speed: parseFloat(element.getAttribute('data-ethereal-speed')) || this.config.speed,
length: parseInt(element.getAttribute('data-ethereal-length')) || this.config.length,
color: element.getAttribute(config.colorAttr) || this.config.color,
colorVariation: parseInt(element.getAttribute('data-ethereal-color-variation')) || this.config.colorVariation,
width: parseFloat(element.getAttribute('data-ethereal-width')) || this.config.width,
opacity: parseInt(element.getAttribute('data-ethereal-opacity')) || this.config.opacity,
blendMode: element.getAttribute('data-ethereal-blend-mode') || this.config.blendMode,
springTension: parseFloat(element.getAttribute('data-ethereal-spring-tension')) || this.config.springTension,
friction: parseFloat(element.getAttribute('data-ethereal-friction')) || this.config.friction,
dampening: parseFloat(element.getAttribute('data-ethereal-dampening')) || this.config.dampening
},
resizeCanvas: function() {
this.width = this.element.clientWidth;
this.height = this.element.clientHeight;
this.canvas.width = this.width;
this.canvas.height = this.height;
this.pos.x = this.width / 2;
this.pos.y = this.height / 2;
},
createLines: function() {
this.lines = [];
const qualitySettings = { low: 40, medium: 60, high: 80 };
const trails = qualitySettings[this.options.quality] || qualitySettings.medium;
const speedFactor = Math.max(0.1, Math.min(1, this.options.speed));
for (let i = 0; i < trails; i++) {
const spring = this.options.springTension + Math.random() * 0.025;
const friction = this.options.friction * speedFactor + (Math.random() * 0.01 - 0.005);
const nodes = Array(this.options.length).fill().map(() => ({
x: this.pos.x, y: this.pos.y, vx: 0, vy: 0
}));
this.lines.push({ spring, friction, nodes });
}
},
hexToHSL: function(hex) {
let r = parseInt(hex.substring(1,3), 16) / 255;
let g = parseInt(hex.substring(3,5), 16) / 255;
let b = parseInt(hex.substring(5,7), 16) / 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
let 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 *= 60;
}
return {h, s: s*100, l: l*100};
},
getColor: function(timestamp) {
const hsl = this.hexToHSL(this.options.color);
const opacity = this.options.opacity / 100;
const variation = this.options.colorVariation;
return \`hsla(\${hsl.h + Math.sin(timestamp * 0.001) * variation}, \${hsl.s}%, \${hsl.l}%, \${opacity})\`;
},
updateLine: function(line) {
let spring = line.spring;
let node = line.nodes[0];
node.vx += (this.pos.x - node.x) * spring;
node.vy += (this.pos.y - node.y) * spring;
for (let i = 0; i < line.nodes.length; i++) {
node = line.nodes[i];
if (i > 0) {
let prev = line.nodes[i - 1];
node.vx += (prev.x - node.x) * spring;
node.vy += (prev.y - node.y) * spring;
node.vx += prev.vx * this.options.dampening;
node.vy += prev.vy * this.options.dampening;
}
node.vx *= line.friction;
node.vy *= line.friction;
node.x += node.vx;
node.y += node.vy;
spring *= 0.99;
}
},
drawLine: function(line) {
let x = line.nodes[0].x, y = line.nodes[0].y;
this.ctx.beginPath();
this.ctx.moveTo(x, y);
for (let i = 1; i < line.nodes.length - 2; i++) {
const node = line.nodes[i];
const next = line.nodes[i + 1];
x = (node.x + next.x) * 0.5;
y = (node.y + next.y) * 0.5;
this.ctx.quadraticCurveTo(node.x, node.y, x, y);
}
const node = line.nodes[line.nodes.length - 2];
const last = line.nodes[line.nodes.length - 1];
this.ctx.quadraticCurveTo(node.x, node.y, last.x, last.y);
this.ctx.stroke();
},
animate: function() {
requestAnimationFrame(this.animate.bind(this));
const timestamp = Date.now();
this.ctx.clearRect(0, 0, this.width, this.height);
this.ctx.globalCompositeOperation = this.options.blendMode;
this.ctx.strokeStyle = this.getColor(timestamp);
this.ctx.lineWidth = this.options.width;
this.lines.forEach(line => {
this.updateLine(line);
this.drawLine(line);
});
},
resize: function() {
this.resizeCanvas();
this.createLines();
}
};
instance.resizeCanvas();
instance.createLines();
instance.animate();
instance.element.addEventListener('mousemove', (e) => {
const rect = instance.element.getBoundingClientRect();
if (
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom
) {
instance.pos.x = e.clientX - rect.left;
instance.pos.y = e.clientY - rect.top;
}
});
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => instance.resize(), 250);
});
this.instances.push(instance);
}
};
window.EtherealTrailAnimation = EtherealTrailAnimation;
EtherealTrailAnimation.init();
})(window);`;
}
function copyJsToClipboard() {
const jsCode = generateJavaScriptCode();
navigator.clipboard.writeText(jsCode)
.then(() => {
showNotification('JavaScript code copied to clipboard!');
})
.catch(err => {
try {
const textArea = document.createElement('textarea');
textArea.value = jsCode;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('JavaScript code copied to clipboard!');
} catch (fallbackErr) {
showNotification('Failed to copy to clipboard. Please try again.', 'error');
}
});
}
function copyFullSectionToClipboard() {
const sectionJSON = generateFullSectionJSON();
navigator.clipboard.writeText(sectionJSON)
.then(() => {
showNotification('Full section JSON copied to clipboard!');
})
.catch(err => {
try {
const textArea = document.createElement('textarea');
textArea.value = sectionJSON;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('Full section JSON copied to clipboard!');
} catch (fallbackErr) {
showNotification('Failed to copy to clipboard. Please try again.', 'error');
}
});
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text)
.then(() => {
showNotification('Copied to clipboard!');
})
.catch(err => {
showNotification('Failed to copy to clipboard', 'error');
});
}
function hexToHsl(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
function hslToHex(hsl) {
const match = hsl.match(/hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/);
if (!match) return null;
let h = parseInt(match[1]) / 360;
let s = parseInt(match[2]) / 100;
let l = parseInt(match[3]) / 100;
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
const toHex = (c) => {
const hex = Math.round(c * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function isValidHex(hex) {
return /^#[0-9A-F]{6}$/i.test(hex);
}
function isValidHsl(hsl) {
return /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/i.test(hsl);
}
function formatHex(value) {
let hex = value.replace(/[^0-9A-Fa-f#]/g, '');
if (!hex.startsWith('#')) {
hex = '#' + hex;
}
if (hex.length > 7) {
hex = hex.substring(0, 7);
}
return hex.toUpperCase();
}
function formatHsl(value) {
const cleanValue = value.replace(/[^\d,\s]/g, '');
const numbers = cleanValue.match(/\d+/g);
if (!numbers || numbers.length < 3) {
const partialMatch = value.match(/(\d+)/g);
if (partialMatch && partialMatch.length >= 1) {
const h = Math.min(360, Math.max(0, parseInt(partialMatch[0]) || 0));
const s = Math.min(100, Math.max(0, parseInt(partialMatch[1]) || 50));
const l = Math.min(100, Math.max(0, parseInt(partialMatch[2]) || 50));
return `hsl(${h}, ${s}%, ${l}%)`;
}
return value;
}
let h = Math.min(360, Math.max(0, parseInt(numbers[0])));
let s = Math.min(100, Math.max(0, parseInt(numbers[1])));
let l = Math.min(100, Math.max(0, parseInt(numbers[2])));
return `hsl(${h}, ${s}%, ${l}%)`;
}
window.resetParameter = function(parameterId, defaultValue) {
const element = document.getElementById(parameterId);
if (element) {
element.value = defaultValue;
const valueElement = document.getElementById(`${parameterId}-value`);
if (valueElement) {
valueElement.textContent = defaultValue;
}
switch (parameterId) {
case 'trail-speed':
etherealConfig.speed = defaultValue;
break;
case 'trail-length':
etherealConfig.length = defaultValue;
break;
case 'color-variation':
etherealConfig.colorVariation = defaultValue;
break;
case 'trail-width':
etherealConfig.width = defaultValue;
break;
case 'trail-opacity':
etherealConfig.opacity = defaultValue;
break;
case 'spring-tension':
etherealConfig.springTension = defaultValue;
break;
case 'friction':
etherealConfig.friction = defaultValue;
break;
case 'dampening':
etherealConfig.dampening = defaultValue;
break;
}
updateEtherealPreview();
showNotification(`${parameterId.replace(/-/g, ' ')} reset to default`);
}
};
function generateRandomEthereal() {
etherealConfig.speed = Math.random() * 0.9 + 0.1;
etherealConfig.length = Math.floor(Math.random() * 80) + 20;
etherealConfig.color = generateRandomColor();
etherealConfig.colorVariation = Math.floor(Math.random() * 60);
etherealConfig.width = Math.random() * 2.5 + 0.5;
etherealConfig.opacity = Math.floor(Math.random() * 25) + 5;
etherealConfig.blendMode = ['source-over', 'lighter', 'screen', 'multiply', 'overlay'][Math.floor(Math.random() * 5)];
etherealConfig.springTension = Math.random() * 0.7 + 0.1;
etherealConfig.friction = Math.random() * 0.6 + 0.3;
etherealConfig.dampening = Math.random() * 0.04 + 0.01;
document.getElementById('trail-speed').value = etherealConfig.speed;
document.getElementById('trail-length').value = etherealConfig.length;
document.getElementById('trail-color').value = etherealConfig.color;
document.getElementById('color-variation').value = etherealConfig.colorVariation;
document.getElementById('trail-width').value = etherealConfig.width;
document.getElementById('trail-opacity').value = etherealConfig.opacity;
document.getElementById('blend-mode').value = etherealConfig.blendMode;
document.getElementById('spring-tension').value = etherealConfig.springTension;
document.getElementById('friction').value = etherealConfig.friction;
document.getElementById('dampening').value = etherealConfig.dampening;
document.getElementById('trail-speed-value').textContent = etherealConfig.speed.toFixed(1);
document.getElementById('trail-length-value').textContent = etherealConfig.length;
document.getElementById('color-variation-value').textContent = etherealConfig.colorVariation;
document.getElementById('trail-width-value').textContent = etherealConfig.width;
document.getElementById('trail-opacity-value').textContent = etherealConfig.opacity;
document.getElementById('spring-tension-value').textContent = etherealConfig.springTension.toFixed(2);
document.getElementById('friction-value').textContent = etherealConfig.friction.toFixed(2);
document.getElementById('dampening-value').textContent = etherealConfig.dampening.toFixed(3);
updateColorInputs();
updateEtherealPreview();
showNotification('Random ethereal trail generated!');
}
function updateColorInputs() {
const colorInput = document.getElementById('trail-color');
const hexInput = document.getElementById('trail-color-hex');
const hslInput = document.getElementById('trail-color-hsl');
if (colorInput && hexInput && hslInput) {
colorInput.value = etherealConfig.color;
hexInput.value = etherealConfig.color;
hslInput.value = `hsl(${hexToHsl(etherealConfig.color).h}, ${hexToHsl(etherealConfig.color).s}%, ${hexToHsl(etherealConfig.color).l}%)`;
hexInput.classList.remove('invalid');
hslInput.classList.remove('invalid');
const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
if (colorPickerContainer) {
colorPickerContainer.style.setProperty('--selected-color', etherealConfig.color);
}
}
}
function initializeUI() {
initEtherealTrail();
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-ethereal-trail');
});
document.getElementById('download-config').addEventListener('click', () => {
copyJsToClipboard();
});
document.getElementById('copy-full-section').addEventListener('click', () => {
copyFullSectionToClipboard();
});
document.getElementById('randomize-ethereal').addEventListener('click', () => {
generateRandomEthereal();
});
const backgroundPicker = document.getElementById('preview-background-picker');
const previewContainer = document.getElementById('ethereal-preview');
backgroundPicker.addEventListener('input', (e) => {
const selectedColor = e.target.value;
previewContainer.style.backgroundColor = selectedColor;
showNotification(`Preview background changed to ${selectedColor}`);
});
previewContainer.style.backgroundColor = '#000000';
document.getElementById('reset-colors').addEventListener('click', () => {
etherealConfig.color = defaultConfig.color;
etherealConfig.colorVariation = defaultConfig.colorVariation;
document.getElementById('trail-color').value = defaultConfig.color;
document.getElementById('color-variation').value = defaultConfig.colorVariation;
document.getElementById('color-variation-value').textContent = defaultConfig.colorVariation;
updateColorInputs();
updateEtherealPreview();
showNotification('Colors reset to default');
});
document.getElementById('reset-animation').addEventListener('click', () => {
etherealConfig.speed = defaultConfig.speed;
etherealConfig.length = defaultConfig.length;
etherealConfig.width = defaultConfig.width;
etherealConfig.opacity = defaultConfig.opacity;
document.getElementById('trail-speed').value = defaultConfig.speed;
document.getElementById('trail-length').value = defaultConfig.length;
document.getElementById('trail-width').value = defaultConfig.width;
document.getElementById('trail-opacity').value = defaultConfig.opacity;
document.getElementById('trail-speed-value').textContent = defaultConfig.speed;
document.getElementById('trail-length-value').textContent = defaultConfig.length;
document.getElementById('trail-width-value').textContent = defaultConfig.width;
document.getElementById('trail-opacity-value').textContent = defaultConfig.opacity;
updateEtherealPreview();
showNotification('Animation settings reset');
});
document.getElementById('reset-advanced').addEventListener('click', () => {
etherealConfig.blendMode = defaultConfig.blendMode;
etherealConfig.springTension = defaultConfig.springTension;
etherealConfig.friction = defaultConfig.friction;
etherealConfig.dampening = defaultConfig.dampening;
document.getElementById('blend-mode').value = defaultConfig.blendMode;
document.getElementById('spring-tension').value = defaultConfig.springTension;
document.getElementById('friction').value = defaultConfig.friction;
document.getElementById('dampening').value = defaultConfig.dampening;
document.getElementById('spring-tension-value').textContent = defaultConfig.springTension;
document.getElementById('friction-value').textContent = defaultConfig.friction;
document.getElementById('dampening-value').textContent = defaultConfig.dampening;
updateEtherealPreview();
showNotification('Advanced settings reset');
});
const colorInput = document.getElementById('trail-color');
const hexInput = document.getElementById('trail-color-hex');
const hslInput = document.getElementById('trail-color-hsl');
hslInput.value = `hsl(${hexToHsl(colorInput.value).h}, ${hexToHsl(colorInput.value).s}%, ${hexToHsl(colorInput.value).l}%)`;
colorInput.addEventListener('input', () => {
const color = colorInput.value;
hexInput.value = color;
hslInput.value = `hsl(${hexToHsl(color).h}, ${hexToHsl(color).s}%, ${hexToHsl(color).l}%)`;
hexInput.classList.remove('invalid');
hslInput.classList.remove('invalid');
etherealConfig.color = color;
const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
colorPickerContainer.style.setProperty('--selected-color', color);
updateEtherealPreview();
});
hexInput.addEventListener('input', (e) => {
let hex = e.target.value;
hex = formatHex(hex);
e.target.value = hex;
if (isValidHex(hex)) {
colorInput.value = hex;
hslInput.value = `hsl(${hexToHsl(hex).h}, ${hexToHsl(hex).s}%, ${hexToHsl(hex).l}%)`;
etherealConfig.color = hex;
e.target.classList.remove('invalid');
hslInput.classList.remove('invalid');
const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
colorPickerContainer.style.setProperty('--selected-color', hex);
updateEtherealPreview();
} else {
e.target.classList.add('invalid');
}
});
hexInput.addEventListener('blur', (e) => {
if (!isValidHex(e.target.value)) {
e.target.value = colorInput.value;
e.target.classList.remove('invalid');
}
});
hslInput.addEventListener('input', (e) => {
let hsl = e.target.value;
if (isValidHsl(hsl)) {
const hex = hslToHex(hsl);
if (hex) {
colorInput.value = hex;
hexInput.value = hex;
etherealConfig.color = hex;
e.target.classList.remove('invalid');
hexInput.classList.remove('invalid');
const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
colorPickerContainer.style.setProperty('--selected-color', hex);
updateEtherealPreview();
return;
}
}
e.target.classList.add('invalid');
});
hslInput.addEventListener('blur', (e) => {
let hsl = e.target.value;
if (!isValidHsl(hsl) && hsl.trim()) {
const formatted = formatHsl(hsl);
if (isValidHsl(formatted)) {
e.target.value = formatted;
const hex = hslToHex(formatted);
if (hex) {
colorInput.value = hex;
hexInput.value = hex;
etherealConfig.color = hex;
e.target.classList.remove('invalid');
hexInput.classList.remove('invalid');
updateEtherealPreview();
return;
}
}
}
if (!isValidHsl(e.target.value)) {
e.target.value = `hsl(${hexToHsl(colorInput.value).h}, ${hexToHsl(colorInput.value).s}%, ${hexToHsl(colorInput.value).l}%)`;
e.target.classList.remove('invalid');
}
});
const rangeInputs = document.querySelectorAll('input[type="range"]');
rangeInputs.forEach(input => {
const valueElement = document.getElementById(`${input.id}-value`);
if (valueElement) {
valueElement.textContent = input.value;
}
input.addEventListener('input', () => {
if (valueElement) {
if (input.id === 'trail-speed') {
valueElement.textContent = parseFloat(input.value).toFixed(1);
} else if (input.id === 'spring-tension' || input.id === 'friction') {
valueElement.textContent = parseFloat(input.value).toFixed(2);
} else if (input.id === 'dampening') {
valueElement.textContent = parseFloat(input.value).toFixed(3);
} else {
valueElement.textContent = input.value;
}
}
switch (input.id) {
case 'trail-speed':
etherealConfig.speed = parseFloat(input.value);
break;
case 'trail-length':
etherealConfig.length = parseInt(input.value);
break;
case 'color-variation':
etherealConfig.colorVariation = parseInt(input.value);
break;
case 'trail-width':
etherealConfig.width = parseFloat(input.value);
break;
case 'trail-opacity':
etherealConfig.opacity = parseInt(input.value);
break;
case 'spring-tension':
etherealConfig.springTension = parseFloat(input.value);
break;
case 'friction':
etherealConfig.friction = parseFloat(input.value);
break;
case 'dampening':
etherealConfig.dampening = parseFloat(input.value);
break;
}
updateEtherealPreview();
});
});
document.getElementById('trail-quality').addEventListener('change', function() {
etherealConfig.quality = this.value;
updateEtherealPreview();
});
document.getElementById('blend-mode').addEventListener('change', function() {
etherealConfig.blendMode = this.value;
updateEtherealPreview();
});
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':
generateRandomEthereal();
break;
case 'b':
document.getElementById('preview-background-picker').click();
break;
}
}
});
updateColorInputs();
setTimeout(() => {
showNotification('BricksFusion Ethereal Trail Configurator loaded!');
}, 500);
function saveConfiguration() {
try {
localStorage.setItem('bricksfusion-ethereal-config', JSON.stringify(etherealConfig));
} catch (e) {
}
}
function loadConfiguration() {
try {
const saved = localStorage.getItem('bricksfusion-ethereal-config');
if (saved) {
const savedConfig = JSON.parse(saved);
Object.assign(etherealConfig, savedConfig);
document.getElementById('trail-quality').value = savedConfig.quality;
document.getElementById('trail-speed').value = savedConfig.speed;
document.getElementById('trail-length').value = savedConfig.length;
document.getElementById('trail-color').value = savedConfig.color;
document.getElementById('color-variation').value = savedConfig.colorVariation;
document.getElementById('trail-width').value = savedConfig.width;
document.getElementById('trail-opacity').value = savedConfig.opacity;
document.getElementById('blend-mode').value = savedConfig.blendMode;
document.getElementById('spring-tension').value = savedConfig.springTension;
document.getElementById('friction').value = savedConfig.friction;
document.getElementById('dampening').value = savedConfig.dampening;
document.getElementById('trail-speed-value').textContent = savedConfig.speed.toFixed(1);
document.getElementById('trail-length-value').textContent = savedConfig.length;
document.getElementById('color-variation-value').textContent = savedConfig.colorVariation;
document.getElementById('trail-width-value').textContent = savedConfig.width;
document.getElementById('trail-opacity-value').textContent = savedConfig.opacity;
document.getElementById('spring-tension-value').textContent = savedConfig.springTension.toFixed(2);
document.getElementById('friction-value').textContent = savedConfig.friction.toFixed(2);
document.getElementById('dampening-value').textContent = savedConfig.dampening.toFixed(3);
updateColorInputs();
updateEtherealPreview();
}
} catch (e) {
}
}
const originalUpdateEtherealPreview = updateEtherealPreview;
updateEtherealPreview = function() {
originalUpdateEtherealPreview();
saveConfiguration();
};
loadConfiguration();
}
initializeUI();
});
</script>
</body>
</html>
Ethereal Trail
Creates smooth, flowing trails that follow your mouse with spring physics. Multiple strands create organic, ethereal patterns. Perfect for hero sections, interactive backgrounds, or adding magic to any container.
Ethereal Trail
Move your mouse to create flowing trails with spring physics.
Appearance
Base color of the trails. Bright colors work best with lighter blend mode.
Default: #00ff88 (cyan)
How much the color shifts over time in hue. Creates rainbow-like effects with higher values.
Default: 20
Thickness of the trail lines. Thinner creates delicate wisps, thicker creates bold strokes.
Default: 1
Transparency of trails. Lower is more subtle and ethereal, higher is more visible.
Default: 10
How trails blend with background and each other. Lighter creates glowing effects, screen is softer, normal is standard.
Default: Lighter
Animation
How quickly trails follow the mouse. Lower is slower and dreamy, higher is fast and responsive.
Default: 0.5
Number of segments in each trail. More segments create longer, more flowing trails.
Default: 50
Physics
Elasticity of the trail. Higher values make trails snap to cursor faster, lower creates loose flowing motion.
Default: 0.45
Resistance to movement. Higher values slow down trails more, creating smoother deceleration.
Default: 0.5
Energy transfer between segments. Higher values create more connected, wave-like motion.
Default: 0.025
Quality
Number of trail strands. Low has 40 trails, medium has 60, high has 80. More trails look fuller but use more resources.
Default: Medium
Performance
This element uses HTML5 Canvas with spring physics simulation running at 60fps via requestAnimationFrame. Each trail has multiple nodes with velocity and position calculations. Quality setting adjusts the number of simultaneous trails. Automatically handles canvas resizing and cleanup.