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>Brand Carousel 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);
display: flex;
align-items: center;
justify-content: center;
}
.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: #3b82f6;
}
.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, #3b82f6);
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;
}
#brand-carousel-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
border-radius: var(--card-radius);
}
.brand-carousel-preview {
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
height: 100%;
width: 100%;
border-radius: 8px;
position: relative;
background-color: #000000;
}
.carousel-blur-left,
.carousel-blur-right {
position: absolute;
top: 0;
bottom: 0;
width: 100px;
pointer-events: none;
z-index: 10;
transition: var(--transition);
}
.carousel-blur-left {
left: 0;
}
.carousel-blur-right {
right: 0;
}
.brand-carousel-track {
display: flex;
align-items: center;
will-change: transform;
padding: 0;
position: relative;
gap: 30px;
backface-visibility: hidden;
perspective: 1000px;
transform: translateZ(0);
overflow: visible;
white-space: nowrap;
}
.brand-logo {
height: 50px;
width: auto;
opacity: 0.9;
transition: all 0.3s ease;
display: block;
max-width: none;
filter: brightness(0.95) contrast(1.1);
padding: 5px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
flex-shrink: 0;
}
.brand-logo:hover {
opacity: 1;
transform: translateY(-2px);
filter: brightness(1) contrast(1.2);
background: rgba(255, 255, 255, 0.15);
}
.image-url-group {
margin-bottom: 1rem;
}
.image-url-input {
display: flex;
gap: 0.5rem;
}
.url-input {
flex-grow: 1;
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);
outline: none;
transition: var(--transition);
}
.url-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
.remove-btn {
background-color: rgba(255, 59, 48, 0.2);
color: #ff3b30;
border: none;
border-radius: var(--input-radius);
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 16px;
transition: var(--transition);
}
.remove-btn:hover {
background-color: rgba(255, 59, 48, 0.3);
}
.add-image-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.6rem 1rem;
background-color: rgba(50, 50, 50, 0.5);
color: var(--text-primary);
border: 1px dashed var(--border);
border-radius: var(--input-radius);
font-size: var(--text-xs);
cursor: pointer;
transition: var(--transition);
width: 100%;
margin-top: 0.5rem;
}
.add-image-btn:hover {
background-color: rgba(80, 80, 80, 0.5);
border-color: var(--text-secondary);
}
/* Optimized update indicator */
.updating {
position: relative;
}
.updating::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
animation: updatePulse 0.6s ease-out;
}
@keyframes updatePulse {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
@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/showcase/" class="breadcrumb-item">Showcase</a>
<span class="breadcrumb-separator">›</span>
<span class="breadcrumb-item active">Brand Carousel</span>
</nav>
<div class="action-buttons">
<div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
data-brand-carousel
</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">Brand Carousel</h1>
<p class="page-subtitle">Interactive brand showcase 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 brand carousel 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-brand-carousel</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="carousel-preview">
<div class="preview-content">Interactive Brand Carousel Preview</div>
<div class="preview-controls">
<button class="preview-btn" id="randomize-carousel" 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 id="brand-carousel-container" data-brand-carousel="true"></div>
</div>
</section>
<section class="controls-section">
<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">
Logo Height
<span class="help-tooltip" title="Height of brand logos in pixels">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="image-height-value">50</span>px</span>
<button class="reset-btn" onclick="resetParameter('image-height', 50)">↺</button>
</div>
</div>
<input type="range" id="image-height" min="30" max="100" step="5" value="50">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Animation Speed
<span class="help-tooltip" title="Speed multiplier for the carousel animation">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="animation-speed-value">1</span>x</span>
<button class="reset-btn" onclick="resetParameter('animation-speed', 1)">↺</button>
</div>
</div>
<input type="range" id="animation-speed" min="0.2" max="3" step="0.1" value="1">
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Blur Effects
<div class="card-actions">
<button class="card-action-btn" id="reset-blur" title="Reset Blur Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="color-list">
<div class="color-row">
<div class="color-picker-container">
<input type="color" id="blur-color" value="#000000">
</div>
<div class="color-input-group">
<span class="color-label">HEX</span>
<input type="text" class="color-input hex-input" id="blur-color-hex" value="#000000" placeholder="#FFFFFF">
</div>
<div class="color-input-group">
<span class="color-label">HSL</span>
<input type="text" class="color-input hsl-input" id="blur-color-hsl" placeholder="hsl(0, 100%, 50%)">
</div>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Blur Intensity
<span class="help-tooltip" title="Opacity of edge blur effects">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="blur-opacity-value">90</span>%</span>
<button class="reset-btn" onclick="resetParameter('blur-opacity', 90)">↺</button>
</div>
</div>
<input type="range" id="blur-opacity" min="0" max="100" step="10" value="90">
</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">Logo Background</span>
</div>
<select id="logo-background">
<option value="true" selected>Show</option>
<option value="false">Hide</option>
</select>
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Brand Logos
<div class="card-actions">
<button class="card-action-btn" id="reset-logos" title="Reset Logo URLs">↺</button>
</div>
</div>
<div class="card-content">
<div id="image-urls-container">
<div class="image-url-group">
<div class="control-label">
<span class="label-text">Logo 1</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/logo1.png" class="url-input" data-index="1">
</div>
</div>
<div class="image-url-group">
<div class="control-label">
<span class="label-text">Logo 2</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/logo2.png" class="url-input" data-index="2">
</div>
</div>
</div>
<button class="add-image-btn" id="add-image-btn">
<span>+</span> Add Another Logo
</button>
</div>
</div>
</section>
</div>
</div>
<div class="notification" id="notification"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let carouselConfig = {
imageHeight: 50,
animationSpeed: 1,
blurColor: '#000000',
blurOpacity: 0.9,
imageUrls: [],
defaultLogos: [
{ src: "https://www.svgrepo.com/show/303205/html-5-logo.svg", alt: "HTML5" },
{ src: "https://www.svgrepo.com/show/303481/css-3-logo.svg", alt: "CSS3" },
{ src: "https://www.svgrepo.com/show/303206/javascript-logo.svg", alt: "JavaScript" },
{ src: "https://www.svgrepo.com/show/303266/nodejs-icon-logo.svg", alt: "Node.js" },
{ src: "https://www.svgrepo.com/show/303557/redux-logo.svg", alt: "Redux" },
{ src: "https://www.svgrepo.com/show/303157/react-logo.svg", alt: "React" },
{ src: "https://www.svgrepo.com/show/303251/mysql-logo.svg", alt: "MySQL" },
{ src: "https://www.svgrepo.com/show/303208/php-1-logo.svg", alt: "PHP" }
],
showBackground: true
};
const defaultConfig = { ...carouselConfig };
let activeCarousel = null;
// Optimized InfiniteCarousel class with updateConfig method
class InfiniteCarousel {
constructor(container, options = {}) {
this.container = container;
this.track = container.querySelector('.brand-carousel-track');
this.preview = container.querySelector('.brand-carousel-preview');
this.leftBlur = container.querySelector('.carousel-blur-left');
this.rightBlur = container.querySelector('.carousel-blur-right');
this.options = {
imageHeight: options.imageHeight || 50,
speed: options.speed || 1,
gap: options.gap || 30,
blurColor: options.blurColor || '#000000',
blurOpacity: options.blurOpacity || 0.9,
showBackground: options.showBackground !== false,
...options
};
this.animationId = null;
this.currentX = 0;
this.logos = [];
this.clones = [];
this.containerWidth = 0;
this.contentWidth = 0;
this.isRunning = false;
this.isScrolling = false;
this.lastMeasureTime = 0;
this.isVisible = true;
this.isUpdating = false;
this.init();
}
// New optimized updateConfig method
updateConfig(newOptions, updateType = 'full') {
if (this.isUpdating) return;
this.isUpdating = true;
// Show visual feedback
this.container.classList.add('updating');
setTimeout(() => this.container.classList.remove('updating'), 600);
const prevOptions = { ...this.options };
Object.assign(this.options, newOptions);
// Only update what's necessary based on what changed
if (updateType === 'visual' || this.optionsChanged(prevOptions, ['blurColor', 'blurOpacity'])) {
this.updateBlurEffects();
this.updatePreviewBackground();
}
if (updateType === 'style' || this.optionsChanged(prevOptions, ['imageHeight', 'showBackground'])) {
this.updateLogoStyles();
}
if (updateType === 'speed' || this.optionsChanged(prevOptions, ['speed'])) {
// Speed changes are applied in real-time during animation
}
if (updateType === 'content' || this.optionsChanged(prevOptions, ['imageUrls'])) {
this.updateContent();
}
this.isUpdating = false;
}
optionsChanged(prevOptions, keys) {
return keys.some(key => prevOptions[key] !== this.options[key]);
}
updateBlurEffects() {
if (!this.leftBlur || !this.rightBlur) return;
const rgb = this.hexToRgb(this.options.blurColor);
const blurColor = rgb ? `${rgb.r},${rgb.g},${rgb.b}` : '0,0,0';
const opacity = this.options.blurOpacity;
this.leftBlur.style.background = `linear-gradient(to right, rgba(${blurColor},${opacity}), rgba(${blurColor},0))`;
this.rightBlur.style.background = `linear-gradient(to left, rgba(${blurColor},${opacity}), rgba(${blurColor},0))`;
}
updatePreviewBackground() {
if (this.preview) {
this.preview.style.backgroundColor = this.options.blurColor;
}
}
updateLogoStyles() {
const logos = this.track.querySelectorAll('.brand-logo');
logos.forEach(logo => {
logo.style.height = `${this.options.imageHeight}px`;
if (this.options.showBackground) {
logo.style.background = 'rgba(255, 255, 255, 0.1)';
logo.style.padding = '5px';
} else {
logo.style.background = 'none';
logo.style.padding = '0';
}
});
// Recalculate dimensions after style changes
setTimeout(() => {
this.measureDimensions();
this.createClones();
}, 50);
}
updateContent() {
// Only recreate content if logos actually changed
this.stop();
this.track.innerHTML = '';
this.clones = [];
const logoUrls = this.options.imageUrls?.length > 0
? this.options.imageUrls
: this.getDefaultLogos();
logoUrls.forEach(logo => {
const img = document.createElement('img');
img.src = logo.src;
img.alt = logo.alt;
img.className = 'brand-logo';
img.style.height = `${this.options.imageHeight}px`;
if (this.options.showBackground) {
img.style.background = 'rgba(255, 255, 255, 0.1)';
img.style.padding = '5px';
} else {
img.style.background = 'none';
img.style.padding = '0';
}
this.track.appendChild(img);
});
this.logos = Array.from(this.track.children);
this.preloadImages().then(() => {
this.measureDimensions();
this.createClones();
this.start();
});
}
init() {
this.logos = Array.from(this.track.children);
if (this.logos.length === 0) {
this.createDefaultContent();
}
this.preloadImages().then(() => {
this.measureDimensions();
this.createClones();
this.setupIntersectionObserver();
this.setupScrollDetection();
this.setupResizeHandler();
this.updateBlurEffects();
this.updatePreviewBackground();
this.start();
});
}
createDefaultContent() {
const logoUrls = this.options.imageUrls?.length > 0
? this.options.imageUrls
: this.getDefaultLogos();
logoUrls.forEach(logo => {
const img = document.createElement('img');
img.src = logo.src;
img.alt = logo.alt;
img.className = 'brand-logo';
img.style.height = `${this.options.imageHeight}px`;
if (this.options.showBackground) {
img.style.background = 'rgba(255, 255, 255, 0.1)';
img.style.padding = '5px';
} else {
img.style.background = 'none';
img.style.padding = '0';
}
this.track.appendChild(img);
});
this.logos = Array.from(this.track.children);
}
getDefaultLogos() {
return [
{ src: "https://www.svgrepo.com/show/303205/html-5-logo.svg", alt: "HTML5" },
{ src: "https://www.svgrepo.com/show/303481/css-3-logo.svg", alt: "CSS3" },
{ src: "https://www.svgrepo.com/show/303206/javascript-logo.svg", alt: "JavaScript" },
{ src: "https://www.svgrepo.com/show/303266/nodejs-icon-logo.svg", alt: "Node.js" },
{ src: "https://www.svgrepo.com/show/303557/redux-logo.svg", alt: "Redux" },
{ src: "https://www.svgrepo.com/show/303157/react-logo.svg", alt: "React" },
{ src: "https://www.svgrepo.com/show/303251/mysql-logo.svg", alt: "MySQL" },
{ src: "https://www.svgrepo.com/show/303208/php-1-logo.svg", alt: "PHP" }
];
}
preloadImages() {
const images = this.logos.filter(logo => logo.tagName === 'IMG');
const promises = images.map(img => {
return new Promise((resolve) => {
if (img.complete && img.naturalWidth > 0) {
resolve();
} else {
const handleLoad = () => {
img.removeEventListener('load', handleLoad);
img.removeEventListener('error', handleError);
resolve();
};
const handleError = () => {
img.removeEventListener('load', handleLoad);
img.removeEventListener('error', handleError);
resolve();
};
img.addEventListener('load', handleLoad);
img.addEventListener('error', handleError);
setTimeout(() => {
if (!img.complete) {
handleError();
}
}, 3000);
}
});
});
return Promise.all(promises);
}
setupIntersectionObserver() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
this.isVisible = entry.isIntersecting;
if (!this.isVisible) {
this.pause();
} else {
this.resume();
}
});
}, {
threshold: 0.1,
rootMargin: '50px'
});
observer.observe(this.container);
this.intersectionObserver = observer;
}
setupScrollDetection() {
let scrollTimer = null;
const handleScroll = () => {
this.isScrolling = true;
clearTimeout(scrollTimer);
scrollTimer = setTimeout(() => {
this.isScrolling = false;
}, 150);
};
window.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('touchmove', handleScroll, { passive: true });
this.scrollHandler = handleScroll;
}
setupResizeHandler() {
let resizeTimer = null;
let lastWidth = this.container.offsetWidth;
const handleResize = () => {
if (this.isScrolling) return;
const currentWidth = this.container.offsetWidth;
if (Math.abs(currentWidth - lastWidth) < 10) return;
lastWidth = currentWidth;
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
if (!this.isScrolling && this.isVisible) {
this.measureDimensions();
this.createClones();
// Ensure animation continues after resize
if (!this.isRunning && this.isVisible) {
this.start();
}
}
}, 250);
};
window.addEventListener('resize', handleResize, { passive: true });
this.resizeHandler = handleResize;
}
measureDimensions() {
const now = Date.now();
if (now - this.lastMeasureTime < 100) return;
this.lastMeasureTime = now;
this.containerWidth = this.container.offsetWidth;
this.contentWidth = 0;
this.logos.forEach(logo => {
if (logo.offsetWidth > 0) {
this.contentWidth += logo.offsetWidth + this.options.gap;
}
});
this.contentWidth = Math.max(this.contentWidth - this.options.gap, 100);
}
createClones() {
if (this.isScrolling) return;
this.clones.forEach(clone => clone.remove());
this.clones = [];
if (this.contentWidth === 0) return;
const totalNeeded = Math.ceil((this.containerWidth * 2.5) / this.contentWidth) + 1;
for (let i = 0; i < totalNeeded; i++) {
this.logos.forEach(logo => {
const clone = logo.cloneNode(true);
clone.classList.add('carousel-clone');
this.track.appendChild(clone);
this.clones.push(clone);
});
}
}
start() {
if (this.isRunning || !this.isVisible) return;
this.isRunning = true;
this.animate();
}
pause() {
this.isRunning = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
resume() {
if (!this.isRunning && this.isVisible) {
this.start();
}
}
stop() {
this.pause();
}
animate() {
if (!this.isRunning || !this.isVisible) return;
// Use current speed from options (allows real-time speed changes)
this.currentX -= this.options.speed * 0.5;
if (Math.abs(this.currentX) >= this.contentWidth + this.options.gap) {
this.currentX = 0;
}
this.track.style.transform = `translateX(${this.currentX}px)`;
this.animationId = requestAnimationFrame(() => this.animate());
}
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)
} : null;
}
destroy() {
this.stop();
this.clones.forEach(clone => clone.remove());
this.clones = [];
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
}
if (this.scrollHandler) {
window.removeEventListener('scroll', this.scrollHandler);
window.removeEventListener('touchmove', this.scrollHandler);
}
}
}
function initBrandCarousel() {
const container = document.getElementById('brand-carousel-container');
if (!container) return;
const previewContent = document.querySelector('.preview-content');
if (previewContent) {
previewContent.style.display = 'none';
}
// Only create new instance if one doesn't exist
if (!activeCarousel) {
container.innerHTML = '';
const preview = document.createElement('div');
preview.className = 'brand-carousel-preview';
const track = document.createElement('div');
track.className = 'brand-carousel-track';
const leftBlur = document.createElement('div');
leftBlur.className = 'carousel-blur-left';
const rightBlur = document.createElement('div');
rightBlur.className = 'carousel-blur-right';
preview.appendChild(leftBlur);
preview.appendChild(track);
preview.appendChild(rightBlur);
container.appendChild(preview);
setTimeout(() => {
activeCarousel = new InfiniteCarousel(preview, {
imageHeight: carouselConfig.imageHeight,
speed: carouselConfig.animationSpeed,
blurColor: carouselConfig.blurColor,
blurOpacity: carouselConfig.blurOpacity,
imageUrls: carouselConfig.imageUrls,
showBackground: carouselConfig.showBackground
});
}, 100);
}
}
// Optimized update function that uses updateConfig instead of recreating
function updateCarouselPreview(updateType = 'full') {
if (activeCarousel && activeCarousel.updateConfig) {
const newOptions = {
imageHeight: carouselConfig.imageHeight,
speed: carouselConfig.animationSpeed,
blurColor: carouselConfig.blurColor,
blurOpacity: carouselConfig.blurOpacity,
imageUrls: carouselConfig.imageUrls,
showBackground: carouselConfig.showBackground
};
activeCarousel.updateConfig(newOptions, updateType);
} else {
// Fallback to full recreation if carousel doesn't exist
if (activeCarousel) {
activeCarousel.destroy();
activeCarousel = null;
}
initBrandCarousel();
}
}
function 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)
} : null;
}
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 generateUniqueId() {
return Math.random().toString(36).substring(2, 8);
}
function generateFullSectionJSON() {
const sectionId = generateUniqueId();
const containerId = generateUniqueId();
const codeId = generateUniqueId();
const attributeId = generateUniqueId();
const jsCode = generateJavaScriptCode();
const fullSectionData = {
"content": [
{
"id": sectionId,
"name": "section",
"parent": 0,
"children": [containerId, codeId],
"settings": {
"_height": "200",
"_justifyContent": "center",
"_background": {
"color": {
"hex": "#000000"
}
}
},
"label": "Brand Carousel Section"
},
{
"id": containerId,
"name": "container",
"parent": sectionId,
"children": [],
"settings": {
"_attributes": [
{
"id": attributeId,
"name": "data-brand-carousel"
}
]
}
},
{
"id": codeId,
"name": "code",
"parent": sectionId,
"children": [],
"settings": {
"javascriptCode": jsCode,
"executeCode": true,
"_display": "none"
},
"label": "Brand Carousel JS"
}
],
"source": "bricksCopiedElements",
"sourceUrl": "https://bricksfusion.com",
"version": "2.0.1",
"globalClasses": [],
"globalElements": []
};
return JSON.stringify(fullSectionData, null, 2);
}
function generateJavaScriptCode() {
const validUrls = carouselConfig.imageUrls.filter(logo => logo.src.trim() !== '');
const logoUrlsCode = validUrls.map((logo, index) =>
` c.setAttribute('data-brand-${index + 1}', '${logo.src}');`
).join('\n');
return `(function(){
const style = document.createElement('style');
style.textContent = \`
.brand-carousel-preview {
display: flex;
align-items: center;
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
}
.brand-carousel-track {
display: flex;
align-items: center;
will-change: transform;
padding: 0;
position: relative;
gap: 30px;
backface-visibility: hidden;
perspective: 1000px;
transform: translateZ(0);
}
.brand-logo {
height: ${carouselConfig.imageHeight}px;
width: auto;
opacity: 0.9;
transition: all 0.3s ease;
display: block;
max-width: none;
filter: brightness(0.95) contrast(1.1);
${carouselConfig.showBackground ? `
padding: 5px;
background: rgba(255, 255, 255, 0.1);
` : ''}
border-radius: 8px;
flex-shrink: 0;
}
.brand-logo:hover {
opacity: 1;
transform: translateY(-2px);
filter: brightness(1) contrast(1.2);
${carouselConfig.showBackground ? `
background: rgba(255, 255, 255, 0.15);
` : ''}
}
.carousel-blur-left,
.carousel-blur-right {
position: absolute;
top: 0;
bottom: 0;
width: min(80px, 15%);
pointer-events: none;
z-index: 1;
}
.carousel-blur-left {
left: 0;
background: linear-gradient(to right, rgba(${(() => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(carouselConfig.blurColor);
return result ? `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` : '0,0,0';
})()},${carouselConfig.blurOpacity}), rgba(${(() => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(carouselConfig.blurColor);
return result ? `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` : '0,0,0';
})()},0));
}
.carousel-blur-right {
right: 0;
background: linear-gradient(to left, rgba(${(() => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(carouselConfig.blurColor);
return result ? `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` : '0,0,0';
})()},${carouselConfig.blurOpacity}), rgba(${(() => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(carouselConfig.blurColor);
return result ? `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` : '0,0,0';
})()},0));
}
\`;
document.head.appendChild(style);
class InfiniteCarousel {
constructor(container, options = {}) {
this.container = container;
this.track = container.querySelector('.brand-carousel-track');
this.options = {
speed: options.speed || ${carouselConfig.animationSpeed},
gap: options.gap || 30,
...options
};
this.animationId = null;
this.currentX = 0;
this.logos = [];
this.clones = [];
this.containerWidth = 0;
this.contentWidth = 0;
this.isRunning = false;
this.isVisible = true;
this.lastMeasureTime = 0;
this.init();
}
init() {
this.logos = Array.from(this.track.children);
if (this.logos.length === 0) return;
this.preloadImages().then(() => {
this.measureDimensions();
this.createClones();
this.setupIntersectionObserver();
this.setupResizeHandler();
this.start();
});
}
updateConfig(newOptions) {
Object.assign(this.options, newOptions);
// Update dimensions and clones after config changes
setTimeout(() => {
this.measureDimensions();
this.createClones();
}, 50);
}
preloadImages() {
const images = this.logos.filter(logo => logo.tagName === 'IMG');
const promises = images.map(img => {
return new Promise((resolve) => {
if (img.complete && img.naturalWidth > 0) {
resolve();
} else {
const handleLoad = () => {
img.removeEventListener('load', handleLoad);
img.removeEventListener('error', handleError);
resolve();
};
const handleError = () => {
img.removeEventListener('load', handleLoad);
img.removeEventListener('error', handleError);
if (img.src.includes('.svg')) {
const fallbackSrc = img.src.replace('.svg', '.png');
if (fallbackSrc !== img.src) {
img.src = fallbackSrc;
img.addEventListener('load', handleLoad);
img.addEventListener('error', () => resolve());
} else {
resolve();
}
} else {
resolve();
}
};
img.addEventListener('load', handleLoad);
img.addEventListener('error', handleError);
setTimeout(() => {
if (!img.complete) {
handleError();
}
}, 3000);
}
});
});
return Promise.all(promises);
}
setupIntersectionObserver() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
this.isVisible = entry.isIntersecting;
if (!this.isVisible) {
this.pause();
} else {
this.resume();
}
});
}, {
threshold: 0.1,
rootMargin: '50px'
});
observer.observe(this.container);
this.intersectionObserver = observer;
}
setupResizeHandler() {
let resizeTimer = null;
let lastWidth = this.container.offsetWidth;
const handleResize = () => {
const currentWidth = this.container.offsetWidth;
if (Math.abs(currentWidth - lastWidth) < 10) return;
lastWidth = currentWidth;
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
if (this.isVisible) {
this.measureDimensions();
this.createClones();
// Ensure animation continues after resize
if (!this.isRunning && this.isVisible) {
this.start();
}
}
}, 250);
};
window.addEventListener('resize', handleResize, { passive: true });
this.resizeHandler = handleResize;
}
measureDimensions() {
const now = Date.now();
if (now - this.lastMeasureTime < 100) return;
this.lastMeasureTime = now;
this.containerWidth = this.container.offsetWidth;
this.contentWidth = 0;
this.logos.forEach(logo => {
if (logo.offsetWidth > 0) {
this.contentWidth += logo.offsetWidth + this.options.gap;
}
});
this.contentWidth = Math.max(this.contentWidth - this.options.gap, 100);
}
createClones() {
this.clones.forEach(clone => clone.remove());
this.clones = [];
if (this.contentWidth === 0) return;
const totalNeeded = Math.ceil((this.containerWidth * 2.5) / this.contentWidth) + 1;
for (let i = 0; i < totalNeeded; i++) {
this.logos.forEach(logo => {
const clone = logo.cloneNode(true);
clone.classList.add('carousel-clone');
this.track.appendChild(clone);
this.clones.push(clone);
});
}
}
start() {
if (this.isRunning || !this.isVisible) return;
this.isRunning = true;
this.animate();
}
pause() {
this.isRunning = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
resume() {
if (!this.isRunning && this.isVisible) {
this.start();
}
}
stop() {
this.pause();
}
animate() {
if (!this.isRunning || !this.isVisible) return;
this.currentX -= this.options.speed * 0.5;
if (Math.abs(this.currentX) >= this.contentWidth + this.options.gap) {
this.currentX = 0;
}
this.track.style.transform = \`translateX(\${this.currentX}px)\`;
this.animationId = requestAnimationFrame(() => this.animate());
}
destroy() {
this.stop();
this.clones.forEach(clone => clone.remove());
this.clones = [];
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
}
}
}
function init() {
const c = document.querySelector('[data-brand-carousel]');
if(!c) return;
if(c._carouselInstance) {
c._carouselInstance.destroy();
}
c.innerHTML = '';
const preview = document.createElement('div');
preview.className = 'brand-carousel-preview';
const track = document.createElement('div');
track.className = 'brand-carousel-track';
const leftBlur = document.createElement('div');
leftBlur.className = 'carousel-blur-left';
const rightBlur = document.createElement('div');
rightBlur.className = 'carousel-blur-right';
${logoUrlsCode}
let logos = [];
for(let i = 1; i <= 8; i++) {
const src = c.getAttribute(\`data-brand-\${i}\`);
if(src) {
logos.push({
src: src,
alt: \`Brand \${i}\`
});
}
}
if (logos.length === 0) {
logos = [
{ src: "https://www.svgrepo.com/show/303205/html-5-logo.svg", alt: "HTML5" },
{ src: "https://www.svgrepo.com/show/303481/css-3-logo.svg", alt: "CSS3" },
{ src: "https://www.svgrepo.com/show/303206/javascript-logo.svg", alt: "JavaScript" },
{ src: "https://www.svgrepo.com/show/303266/nodejs-icon-logo.svg", alt: "Node.js" }
];
}
logos.forEach(logo => {
const img = document.createElement('img');
img.src = logo.src;
img.alt = logo.alt;
img.className = 'brand-logo';
track.appendChild(img);
});
preview.appendChild(leftBlur);
preview.appendChild(track);
preview.appendChild(rightBlur);
c.appendChild(preview);
setTimeout(() => {
c._carouselInstance = new InfiniteCarousel(preview, {
speed: ${carouselConfig.animationSpeed},
gap: 30
});
}, 100);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
document.addEventListener('bricks/content_loaded', init);
setTimeout(init, 100);
})();`;
}
function copyJsToClipboard() {
const jsCode = generateJavaScriptCode();
navigator.clipboard.writeText(jsCode)
.then(() => {
showNotification('JavaScript code copied to clipboard!');
})
.catch(err => {
try {
const textArea = document.createElement('textarea');
textArea.value = jsCode;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('JavaScript code copied to clipboard!');
} catch (fallbackErr) {
showNotification('Failed to copy to clipboard. Please try again.', 'error');
}
});
}
function copyFullSectionToClipboard() {
const sectionJSON = generateFullSectionJSON();
navigator.clipboard.writeText(sectionJSON)
.then(() => {
showNotification('Full section JSON copied to clipboard!');
})
.catch(err => {
try {
const textArea = document.createElement('textarea');
textArea.value = sectionJSON;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('Full section JSON copied to clipboard!');
} catch (fallbackErr) {
showNotification('Failed to copy to clipboard. Please try again.', 'error');
}
});
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text)
.then(() => {
showNotification('Copied to clipboard!');
})
.catch(err => {
showNotification('Failed to copy to clipboard', 'error');
});
}
window.resetParameter = function(parameterId, defaultValue) {
const element = document.getElementById(parameterId);
if (element) {
element.value = defaultValue;
const valueElement = document.getElementById(`${parameterId}-value`);
if (valueElement) {
valueElement.textContent = defaultValue;
}
switch (parameterId) {
case 'image-height':
carouselConfig.imageHeight = defaultValue;
updateCarouselPreview('style');
break;
case 'animation-speed':
carouselConfig.animationSpeed = defaultValue;
updateCarouselPreview('speed');
break;
case 'blur-opacity':
carouselConfig.blurOpacity = defaultValue / 100;
updateCarouselPreview('visual');
break;
}
showNotification(`${parameterId.replace(/-/g, ' ')} reset to default`);
}
};
function generateRandomCarousel() {
carouselConfig.imageHeight = Math.floor(Math.random() * 70) + 30;
carouselConfig.animationSpeed = Math.random() * 2.8 + 0.2;
carouselConfig.blurColor = generateRandomColor();
carouselConfig.blurOpacity = (Math.floor(Math.random() * 10) + 1) / 10;
carouselConfig.showBackground = Math.random() > 0.5;
document.getElementById('image-height').value = carouselConfig.imageHeight;
document.getElementById('animation-speed').value = carouselConfig.animationSpeed;
document.getElementById('blur-color').value = carouselConfig.blurColor;
document.getElementById('blur-opacity').value = carouselConfig.blurOpacity * 100;
document.getElementById('logo-background').value = carouselConfig.showBackground;
document.getElementById('image-height-value').textContent = carouselConfig.imageHeight;
document.getElementById('animation-speed-value').textContent = carouselConfig.animationSpeed;
document.getElementById('blur-opacity-value').textContent = Math.round(carouselConfig.blurOpacity * 100);
updateColorInputs();
updateCarouselPreview('full');
showNotification('Random brand carousel generated!');
}
function updateColorInputs() {
const colorInput = document.getElementById('blur-color');
const hexInput = document.getElementById('blur-color-hex');
const hslInput = document.getElementById('blur-color-hsl');
if (colorInput && hexInput && hslInput) {
colorInput.value = carouselConfig.blurColor;
hexInput.value = carouselConfig.blurColor;
const hsl = hexToRgb(carouselConfig.blurColor);
if (hsl) {
const hslColor = rgbToHsl(hsl.r, hsl.g, hsl.b);
hslInput.value = `hsl(${hslColor.h}, ${hslColor.s}%, ${hslColor.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', carouselConfig.blurColor);
}
}
}
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 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 initializeUI() {
// Initialize carousel
setTimeout(() => {
initBrandCarousel();
}, 100);
const instructionsToggle = document.getElementById('instructions-toggle');
const instructionsContent = document.getElementById('instructions-content');
const instructionsCard = document.getElementById('instructions-card');
const toggleIcon = instructionsToggle?.querySelector('.toggle-icon');
if (instructionsToggle && instructionsContent && instructionsCard && toggleIcon) {
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');
}
});
}
const quickAttribute = document.getElementById('quick-attribute');
if (quickAttribute) {
quickAttribute.addEventListener('click', () => {
copyToClipboard('data-brand-carousel');
});
}
const downloadConfig = document.getElementById('download-config');
if (downloadConfig) {
downloadConfig.addEventListener('click', () => {
copyJsToClipboard();
});
}
const copyFullSection = document.getElementById('copy-full-section');
if (copyFullSection) {
copyFullSection.addEventListener('click', () => {
copyFullSectionToClipboard();
});
}
const randomizeCarousel = document.getElementById('randomize-carousel');
if (randomizeCarousel) {
randomizeCarousel.addEventListener('click', () => {
generateRandomCarousel();
});
}
const backgroundPicker = document.getElementById('preview-background-picker');
const previewContainer = document.getElementById('carousel-preview');
if (backgroundPicker && previewContainer) {
backgroundPicker.addEventListener('input', (e) => {
const selectedColor = e.target.value;
previewContainer.style.backgroundColor = selectedColor;
const carouselPreview = previewContainer.querySelector('.brand-carousel-preview');
if (carouselPreview) {
carouselPreview.style.backgroundColor = selectedColor;
}
showNotification(`Preview background changed to ${selectedColor}`);
});
previewContainer.style.backgroundColor = '#000000';
}
const resetAnimation = document.getElementById('reset-animation');
if (resetAnimation) {
resetAnimation.addEventListener('click', () => {
carouselConfig.imageHeight = defaultConfig.imageHeight;
carouselConfig.animationSpeed = defaultConfig.animationSpeed;
const imageHeight = document.getElementById('image-height');
const animationSpeed = document.getElementById('animation-speed');
const imageHeightValue = document.getElementById('image-height-value');
const animationSpeedValue = document.getElementById('animation-speed-value');
if (imageHeight) imageHeight.value = defaultConfig.imageHeight;
if (animationSpeed) animationSpeed.value = defaultConfig.animationSpeed;
if (imageHeightValue) imageHeightValue.textContent = defaultConfig.imageHeight;
if (animationSpeedValue) animationSpeedValue.textContent = defaultConfig.animationSpeed;
updateCarouselPreview('style');
showNotification('Animation settings reset');
});
}
const resetBlur = document.getElementById('reset-blur');
if (resetBlur) {
resetBlur.addEventListener('click', () => {
carouselConfig.blurColor = defaultConfig.blurColor;
carouselConfig.blurOpacity = defaultConfig.blurOpacity;
const blurColor = document.getElementById('blur-color');
const blurOpacity = document.getElementById('blur-opacity');
const blurOpacityValue = document.getElementById('blur-opacity-value');
if (blurColor) blurColor.value = defaultConfig.blurColor;
if (blurOpacity) blurOpacity.value = defaultConfig.blurOpacity * 100;
if (blurOpacityValue) blurOpacityValue.textContent = Math.round(defaultConfig.blurOpacity * 100);
updateColorInputs();
updateCarouselPreview('visual');
showNotification('Blur settings reset');
});
}
const resetAdvanced = document.getElementById('reset-advanced');
if (resetAdvanced) {
resetAdvanced.addEventListener('click', () => {
carouselConfig.showBackground = defaultConfig.showBackground;
const logoBackground = document.getElementById('logo-background');
if (logoBackground) logoBackground.value = defaultConfig.showBackground;
updateCarouselPreview('style');
showNotification('Advanced settings reset');
});
}
const resetLogos = document.getElementById('reset-logos');
if (resetLogos) {
resetLogos.addEventListener('click', () => {
carouselConfig.imageUrls = [];
document.querySelectorAll('.url-input').forEach(input => {
input.value = '';
});
updateCarouselPreview('content');
showNotification('Logo URLs cleared');
});
}
const colorInput = document.getElementById('blur-color');
const hexInput = document.getElementById('blur-color-hex');
const hslInput = document.getElementById('blur-color-hsl');
if (colorInput && hexInput && hslInput) {
const hsl = hexToRgb(colorInput.value);
if (hsl) {
const hslColor = rgbToHsl(hsl.r, hsl.g, hsl.b);
hslInput.value = `hsl(${hslColor.h}, ${hslColor.s}%, ${hslColor.l}%)`;
}
colorInput.addEventListener('input', () => {
const color = colorInput.value;
hexInput.value = color;
const rgb = hexToRgb(color);
if (rgb) {
const hslColor = rgbToHsl(rgb.r, rgb.g, rgb.b);
hslInput.value = `hsl(${hslColor.h}, ${hslColor.s}%, ${hslColor.l}%)`;
}
hexInput.classList.remove('invalid');
hslInput.classList.remove('invalid');
carouselConfig.blurColor = color;
const colorPickerContainer = colorInput.closest('.color-row')?.querySelector('.color-picker-container');
if (colorPickerContainer) {
colorPickerContainer.style.setProperty('--selected-color', color);
}
updateCarouselPreview('visual');
});
hexInput.addEventListener('input', (e) => {
let hex = e.target.value;
hex = formatHex(hex);
e.target.value = hex;
if (isValidHex(hex)) {
colorInput.value = hex;
const rgb = hexToRgb(hex);
if (rgb) {
const hslColor = rgbToHsl(rgb.r, rgb.g, rgb.b);
hslInput.value = `hsl(${hslColor.h}, ${hslColor.s}%, ${hslColor.l}%)`;
}
carouselConfig.blurColor = hex;
e.target.classList.remove('invalid');
hslInput.classList.remove('invalid');
const colorPickerContainer = colorInput.closest('.color-row')?.querySelector('.color-picker-container');
if (colorPickerContainer) {
colorPickerContainer.style.setProperty('--selected-color', hex);
}
updateCarouselPreview('visual');
} 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;
carouselConfig.blurColor = hex;
e.target.classList.remove('invalid');
hexInput.classList.remove('invalid');
const colorPickerContainer = colorInput.closest('.color-row')?.querySelector('.color-picker-container');
if (colorPickerContainer) {
colorPickerContainer.style.setProperty('--selected-color', hex);
}
updateCarouselPreview('visual');
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;
carouselConfig.blurColor = hex;
e.target.classList.remove('invalid');
hexInput.classList.remove('invalid');
updateCarouselPreview('visual');
return;
}
}
}
if (!isValidHsl(e.target.value)) {
const rgb = hexToRgb(colorInput.value);
if (rgb) {
const hslColor = rgbToHsl(rgb.r, rgb.g, rgb.b);
e.target.value = `hsl(${hslColor.h}, ${hslColor.s}%, ${hslColor.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 === 'blur-opacity') {
valueElement.textContent = input.value;
carouselConfig.blurOpacity = parseInt(input.value) / 100;
updateCarouselPreview('visual');
} else {
valueElement.textContent = input.value;
}
}
switch (input.id) {
case 'image-height':
carouselConfig.imageHeight = parseInt(input.value);
updateCarouselPreview('style');
break;
case 'animation-speed':
carouselConfig.animationSpeed = parseFloat(input.value);
updateCarouselPreview('speed');
break;
}
});
});
const logoBackground = document.getElementById('logo-background');
if (logoBackground) {
logoBackground.addEventListener('change', function() {
carouselConfig.showBackground = this.value === 'true';
updateCarouselPreview('style');
});
}
document.querySelectorAll('.url-input').forEach(input => {
input.addEventListener('input', () => {
const newImageUrls = [];
document.querySelectorAll('.url-input').forEach(urlInput => {
const url = urlInput.value.trim();
if (url) {
newImageUrls.push({
src: url,
alt: `Brand ${urlInput.getAttribute('data-index')}`
});
}
});
carouselConfig.imageUrls = newImageUrls;
updateCarouselPreview('content');
});
});
const addImageBtn = document.getElementById('add-image-btn');
if (addImageBtn) {
addImageBtn.addEventListener('click', addImageInput);
}
function addImageInput() {
const container = document.getElementById('image-urls-container');
const count = container.querySelectorAll('.image-url-group').length + 1;
if (count > 8) {
showNotification('Maximum 8 logos allowed', 'warning');
return;
}
const groupDiv = document.createElement('div');
groupDiv.className = 'image-url-group';
groupDiv.style.opacity = '0';
groupDiv.style.transform = 'translateY(10px)';
groupDiv.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
groupDiv.innerHTML = `
<div class="control-label">
<span class="label-text">Logo ${count}</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/logo${count}.png" class="url-input" data-index="${count}">
<button class="remove-btn" title="Remove this logo">×</button>
</div>
`;
container.appendChild(groupDiv);
setTimeout(() => {
groupDiv.style.opacity = '1';
groupDiv.style.transform = 'translateY(0)';
}, 10);
const input = groupDiv.querySelector('.url-input');
input.addEventListener('input', () => {
const newImageUrls = [];
document.querySelectorAll('.url-input').forEach(urlInput => {
const url = urlInput.value.trim();
if (url) {
newImageUrls.push({
src: url,
alt: `Brand ${urlInput.getAttribute('data-index')}`
});
}
});
carouselConfig.imageUrls = newImageUrls;
updateCarouselPreview('content');
});
const removeBtn = groupDiv.querySelector('.remove-btn');
removeBtn.addEventListener('click', function() {
groupDiv.style.opacity = '0';
groupDiv.style.transform = 'translateY(10px)';
setTimeout(() => {
container.removeChild(groupDiv);
updateImageIndexes();
const newImageUrls = [];
document.querySelectorAll('.url-input').forEach(urlInput => {
const url = urlInput.value.trim();
if (url) {
newImageUrls.push({
src: url,
alt: `Brand ${urlInput.getAttribute('data-index')}`
});
}
});
carouselConfig.imageUrls = newImageUrls;
updateCarouselPreview('content');
}, 300);
});
}
function updateImageIndexes() {
const groups = document.querySelectorAll('.image-url-group');
groups.forEach((group, index) => {
const labelText = group.querySelector('.label-text');
const input = group.querySelector('.url-input');
const newIndex = index + 1;
labelText.textContent = `Logo ${newIndex}`;
input.setAttribute('data-index', newIndex);
input.setAttribute('placeholder', `https://example.com/logo${newIndex}.png`);
});
}
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':
generateRandomCarousel();
break;
case 'b':
document.getElementById('preview-background-picker').click();
break;
}
}
});
updateColorInputs();
setTimeout(() => {
showNotification('Brand Carousel configurator loaded!');
}, 500);
}
initializeUI();
});
</script>
</body>
</html>
Brand Carousel
An infinite scrolling carousel that displays brand logos continuously. Perfect for showing client logos, partner brands, or tech stack on your website.
Logos
Upload the logos you want to display in the carousel. Add as many as you need. The carousel will loop them infinitely for a seamless effect.
Default: Tech stack placeholder logos
Appearance
Height of each logo. Smaller values for subtle branding, larger for prominent display. Width adjusts automatically to maintain proportions.
Default: 50
Adds a subtle rounded background behind each logo. Turn off for transparent logos that blend directly with your design.
Default: On
Animation
How fast the logos scroll across the screen. Lower values are slow and calm, higher values are fast and energetic. 1.0 is a good default pace.
Default: 1.0
Blur Effects
Color of the fade effect on the left and right edges. Should match your background color for a seamless blend.
Default: Black
Strength of the fade effect. 1.0 creates a strong fade, lower values make it more subtle and see-through.
Default: 0.9
Performance
This carousel is highly optimized using requestAnimationFrame and automatically pauses when scrolled out of view. Safe to use multiple times on the same page with no performance impact.