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>ScrollMorphGallery Configurator - BricksFusion</title>
<style>
:root {
--background: #000;
--card-bg: #1e1e1e;
--card-bg-hover: #252525;
--text-primary: #f2f2f7;
--text-secondary: #8e8e93;
--accent: #ef6013;
--accent-hover: #c64c0c;
--border: #2c2c2e;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
--track: #2c2c2e;
--thumb: #ef6013;
--card-radius: 16px;
--input-radius: 8px;
--button-radius: 12px;
--transition: all 0.25s ease;
--font: 'Inter', BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Arial, sans-serif;
--action-bar-height: 70px;
--success: #28a745;
--warning: #ffc107;
--danger: #dc3545;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font);
background-color: var(--background);
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding-bottom: var(--action-bar-height);
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--action-bar-height);
background: linear-gradient(145deg, #1a1a1a, #0f0f0f);
border-top: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
padding: 0 1.5rem;
gap: 1rem;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.breadcrumb-item {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 500;
text-decoration: none;
transition: var(--transition);
padding: 0.5rem 0.75rem;
border-radius: 6px;
}
.breadcrumb-item:hover {
color: var(--text-primary);
background-color: rgba(255, 255, 255, 0.05);
}
.breadcrumb-item.active {
color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.breadcrumb-separator {
color: var(--text-secondary);
font-size: var(--text-xs);
opacity: 0.5;
}
.action-buttons {
display: flex;
align-items: center;
gap: 0.75rem;
}
.action-btn {
padding: 0.6rem 1rem;
background-color: var(--card-bg);
color: var(--text-primary);
font-family: var(--font);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid var(--border);
border-radius: var(--button-radius);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
white-space: nowrap;
}
.action-btn:hover {
background-color: var(--card-bg-hover);
border-color: var(--accent);
transform: translateY(-1px);
}
.action-btn.primary {
background: linear-gradient(90deg, var(--accent), #ff8c51);
border-color: var(--accent);
color: white;
}
.action-btn.primary:hover {
background: linear-gradient(90deg, var(--accent-hover), #e67a3f);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 96, 19, 0.3);
}
.data-attribute-display {
background-color: rgba(50, 50, 50, 0.8);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
cursor: pointer;
transition: var(--transition);
user-select: all;
}
.data-attribute-display:hover {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
}
.container {
max-width: 100%;
margin: 0 auto;
padding: 2rem 1.5rem;
}
.page-header {
text-align: center;
margin-bottom: 2rem;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
background: linear-gradient(90deg, var(--accent), #ff8c51);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.page-subtitle {
font-size: var(--text-s);
color: var(--text-secondary);
font-weight: 500;
}
.instructions-toggle {
margin-bottom: 2rem;
}
.instructions-card {
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
transition: var(--transition);
}
.instructions-header {
padding: 1rem 1.5rem;
cursor: pointer;
transition: var(--transition);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid transparent;
}
.instructions-header:hover {
background-color: var(--card-bg-hover);
}
.instructions-card.expanded .instructions-header {
border-bottom-color: var(--border);
}
.instructions-title {
font-size: var(--text-s);
font-weight: 600;
}
.toggle-icon {
font-size: 1.2em;
transition: transform 0.3s ease;
}
.toggle-icon.expanded {
transform: rotate(180deg);
}
.instructions-content {
padding: 0 1.5rem;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.instructions-content.show {
max-height: 500px;
padding: 1.5rem;
}
.instructions-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.how-to-use ol {
padding-left: 1.5rem;
}
.how-to-use li {
margin-bottom: 0.75rem;
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.6;
}
.how-to-use strong {
color: var(--text-primary);
font-weight: 600;
}
.how-to-use code {
background-color: rgba(50, 50, 50, 0.5);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
}
.content {
display: grid;
grid-template-columns: 1fr 500px;
gap: 2rem;
align-items: start;
}
.preview-section {
position: sticky;
top: 2rem;
}
.controls-section {
max-width: 500px;
}
.card {
background-color: var(--card-bg);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}
.preview-container {
height: 500px;
width: 100%;
position: relative;
overflow-y: auto;
overflow-x: hidden;
border-radius: var(--card-radius);
background-color: #252525;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 0;
box-shadow: var(--shadow);
perspective: 1200px;
perspective-origin: center center;
transform-style: preserve-3d;
border: 1px solid var(--border);
}
.preview-container::before {
content: "↓ Scroll to see animation";
position: absolute;
top: 20px;
right: 20px;
background: rgba(239, 96, 19, 0.9);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
z-index: 100;
opacity: 0.8;
animation: fadeInOut 3s ease-in-out infinite;
pointer-events: none;
display: var(--show-scroll-hint, block);
transition: opacity 0.5s ease;
}
@keyframes fadeInOut {
0%, 100% { opacity: 0.8; }
50% { opacity: 0.4; }
}
.preview-container::-webkit-scrollbar {
width: 8px;
}
.preview-container::-webkit-scrollbar-track {
background: var(--track);
}
.preview-container::-webkit-scrollbar-thumb {
background: var(--thumb);
border-radius: 4px;
}
.preview-container::-webkit-scrollbar-thumb:hover {
background: var(--accent-hover);
}
.preview-controls {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.5rem;
z-index: 10;
}
.preview-btn {
padding: 0.5rem;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
cursor: pointer;
transition: var(--transition);
font-size: var(--text-xs);
backdrop-filter: blur(5px);
}
.preview-btn:hover {
background-color: var(--accent);
border-color: var(--accent);
}
.card-heading {
padding: 1rem 1.5rem;
font-size: var(--text-s);
font-weight: 600;
border-bottom: 1px solid var(--border);
letter-spacing: 0.3px;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-actions {
display: flex;
gap: 0.5rem;
}
.card-action-btn {
padding: 0.4rem 0.8rem;
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
font-size: var(--text-xs);
transition: var(--transition);
}
.card-action-btn:hover {
color: var(--text-primary);
border-color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.card-content {
padding: 1.5rem;
}
.control-group {
margin-bottom: 1.5rem;
position: relative;
}
.control-group:last-child {
margin-bottom: 0;
}
.control-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.label-text {
font-size: var(--text-xs);
font-weight: 500;
letter-spacing: 0.2px;
display: flex;
align-items: center;
gap: 0.5rem;
}
.help-tooltip {
cursor: help;
opacity: 0.7;
transition: var(--transition);
}
.help-tooltip:hover {
opacity: 1;
color: var(--accent);
}
.value-display {
display: flex;
align-items: center;
gap: 0.5rem;
}
.value-text {
font-size: var(--text-xs);
color: var(--text-secondary);
background-color: rgba(50, 50, 50, 0.5);
padding: 2px 8px;
border-radius: 4px;
min-width: 45px;
text-align: center;
}
.reset-btn {
padding: 0.2rem 0.4rem;
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
font-size: 10px;
transition: var(--transition);
}
.reset-btn:hover {
color: var(--danger);
border-color: var(--danger);
background-color: rgba(220, 53, 69, 0.1);
}
input[type="range"] {
-webkit-appearance: none;
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);
}
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);
}
.toggle-group {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.toggle-btn {
flex: 1;
padding: 0.5rem;
background-color: var(--card-bg);
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: var(--input-radius);
font-family: var(--font);
font-size: var(--text-xs);
cursor: pointer;
transition: var(--transition);
}
.toggle-btn.active {
background-color: var(--accent);
color: white;
border-color: var(--accent);
}
.toggle-btn:hover:not(.active) {
background-color: rgba(239, 96, 19, 0.1);
border-color: var(--accent);
}
.notification {
position: fixed;
bottom: calc(var(--action-bar-height) + 1rem);
left: 50%;
background-color: var(--success);
color: white;
padding: 0.75rem 1rem;
border-radius: var(--input-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1001;
transform: translate(-50%, 200px);
opacity: 0;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease;
font-size: var(--text-xs);
font-weight: 500;
max-width: 320px;
word-wrap: break-word;
line-height: 1.4;
text-align: center;
}
.notification.show {
transform: translate(-50%, 0);
opacity: 1;
}
.scrollmorph-container {
width: 100%;
height: 100%;
position: relative;
}
.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);
}
.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;
}
.preview-container {
height: 400px;
}
.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">Scroll Gallery</span>
</nav>
<div class="action-buttons">
<div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
data-smg
</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">Scroll Gallery</h1>
<p class="page-subtitle">Interactive 3D scroll gallery 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 ScrollMorphGallery 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 the JavaScript code</li>
<li>To add the effect to any section: go to <strong>Section → Style → Attributes</strong>, add <code>data-smg</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="scrollmorph-preview">
<div class="preview-controls">
<button class="preview-btn" id="generate-random-palette" title="Generate Random Palette (🎲)">🎲</button>
</div>
<div class="scrollmorph-container" data-smg>
</div>
</div>
</section>
<section class="controls-section">
<div class="card">
<div class="card-heading">
Layout Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-layout" title="Reset Layout Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Number of Columns
<span class="help-tooltip" title="Number of columns in the gallery grid">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="columns-value">3</span></span>
<button class="reset-btn" onclick="resetParameter('columns', 3)">↺</button>
</div>
</div>
<input type="range" id="columns" min="2" max="5" step="1" value="3">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Gap Between Items
<span class="help-tooltip" title="Space between gallery items">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="gap-value">8</span>px</span>
<button class="reset-btn" onclick="resetParameter('gap', 8)">↺</button>
</div>
</div>
<input type="range" id="gap" min="4" max="24" step="1" value="8">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Border Radius
<span class="help-tooltip" title="Roundness of image corners">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="border-radius-value">16</span>px</span>
<button class="reset-btn" onclick="resetParameter('border-radius', 16)">↺</button>
</div>
</div>
<input type="range" id="border-radius" min="0" max="32" step="1" value="16">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Start Offset
<span class="help-tooltip" title="Distance from top before animation starts">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="start-offset-value">0</span>vh</span>
<button class="reset-btn" onclick="resetParameter('start-offset', 0)">↺</button>
</div>
</div>
<input type="range" id="start-offset" min="0" max="100" step="10" value="0">
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
3D 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">
Maximum Rotation
<span class="help-tooltip" title="Maximum 3D rotation angle">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="max-rotation-value">75</span>deg</span>
<button class="reset-btn" onclick="resetParameter('max-rotation', 75)">↺</button>
</div>
</div>
<input type="range" id="max-rotation" min="30" max="90" step="5" value="75">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Maximum Scale
<span class="help-tooltip" title="Maximum scale factor during animation">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="max-scale-value">1.2</span>x</span>
<button class="reset-btn" onclick="resetParameter('max-scale', 1.2)">↺</button>
</div>
</div>
<input type="range" id="max-scale" min="1" max="1.5" step="0.05" value="1.2">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Parallax Strength
<span class="help-tooltip" title="Intensity of parallax movement effect">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="parallax-strength-value">30</span>%</span>
<button class="reset-btn" onclick="resetParameter('parallax-strength', 30)">↺</button>
</div>
</div>
<input type="range" id="parallax-strength" min="0" max="30" step="1" value="30">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Container Height
<span class="help-tooltip" title="Controls scroll distance for animation duration">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="container-height-value">180</span>vh</span>
<button class="reset-btn" onclick="resetParameter('container-height', 180)">↺</button>
</div>
</div>
<input type="range" id="container-height" min="150" max="400" step="10" value="180">
</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">Show on Mobile</span>
</div>
<div class="toggle-group">
<button class="toggle-btn active" data-value="true" data-setting="mobile">Enabled</button>
<button class="toggle-btn" data-value="false" data-setting="mobile">Disabled</button>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Image URLs
<div class="card-actions">
<button class="card-action-btn" id="reset-images" title="Reset Image 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">Image 1</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/image1.jpg" class="url-input" data-index="1" value="https://images.unsplash.com/photo-1557672172-298e090bd0f1?w=400&h=400&fit=crop">
</div>
</div>
<div class="image-url-group">
<div class="control-label">
<span class="label-text">Image 2</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/image2.jpg" class="url-input" data-index="2" value="https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=400&fit=crop">
</div>
</div>
<div class="image-url-group">
<div class="control-label">
<span class="label-text">Image 3</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/image3.jpg" class="url-input" data-index="3" value="https://images.unsplash.com/photo-1567359781514-3b964e2b04d6?w=400&h=400&fit=crop">
</div>
</div>
<div class="image-url-group">
<div class="control-label">
<span class="label-text">Image 4</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/image4.jpg" class="url-input" data-index="4" value="https://images.unsplash.com/photo-1574169208507-84376144848b?w=400&h=400&fit=crop">
</div>
</div>
<div class="image-url-group">
<div class="control-label">
<span class="label-text">Image 5</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/image5.jpg" class="url-input" data-index="5" value="https://images.unsplash.com/photo-1553356084-58ef4a67b2a7?w=400&h=400&fit=crop">
</div>
</div>
<div class="image-url-group">
<div class="control-label">
<span class="label-text">Image 6</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/image6.jpg" class="url-input" data-index="6" value="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=400&fit=crop">
</div>
</div>
</div>
<button class="add-image-btn" id="add-image-btn">
<span>+</span> Add Another Image
</button>
</div>
</div>
</section>
</div>
</div>
<div class="notification" id="notification"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let scrollMorphConfig = {
columns: 3,
gap: 8,
borderRadius: 16,
maxRotation: 75,
maxScale: 1.2,
parallaxStrength: 30,
mobile: true,
startOffset: 0,
containerHeight: 180,
images: [
"https://images.unsplash.com/photo-1557672172-298e090bd0f1?w=400&h=400&fit=crop",
"https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=400&fit=crop",
"https://images.unsplash.com/photo-1567359781514-3b964e2b04d6?w=400&h=400&fit=crop",
"https://images.unsplash.com/photo-1574169208507-84376144848b?w=400&h=400&fit=crop",
"https://images.unsplash.com/photo-1553356084-58ef4a67b2a7?w=400&h=400&fit=crop",
"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=400&fit=crop"
]
};
const defaultConfig = { ...scrollMorphConfig };
let scrollAnimationFrame = null;
let currentGallery = null;
const isWindows = navigator.platform.indexOf('Win') > -1;
function initScrollMorphGallery() {
updatePreview();
}
function updatePreview() {
const preview = document.getElementById('scrollmorph-preview');
if (!preview) return;
const container = preview.querySelector('[data-smg]');
if (!container) return;
container.innerHTML = '';
const previewContainerHeight = 1200;
container.style.minHeight = `${previewContainerHeight}px`;
const stickyWrapper = document.createElement('div');
stickyWrapper.className = 'smg-sticky';
stickyWrapper.style.cssText = `
position: sticky;
top: 0;
left: 0;
width: 100%;
height: 100vh;
overflow: hidden;
z-index: 1;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
margin-top: ${Math.min(scrollMorphConfig.startOffset * 5, 100)}px;
`;
const perspectiveWrapper = document.createElement('div');
perspectiveWrapper.className = 'smg-perspective';
perspectiveWrapper.style.cssText = `
position: relative;
width: 90%;
max-width: 1200px;
height: auto;
min-height: 60%;
max-height: 80%;
perspective: 1000px;
perspective-origin: center top;
transform-style: preserve-3d;
padding: 20px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
`;
const galleryContainer = document.createElement('div');
galleryContainer.className = 'smg-container';
galleryContainer.style.cssText = `
position: relative;
width: 100%;
height: auto;
display: grid;
grid-template-columns: repeat(${scrollMorphConfig.columns}, 1fr);
gap: ${scrollMorphConfig.gap}px;
border-radius: ${scrollMorphConfig.borderRadius}px;
transform-style: preserve-3d;
transform-origin: center center;
will-change: transform;
pointer-events: auto;
transition: transform 0.1s linear;
`;
const columns = [];
for (let i = 0; i < scrollMorphConfig.columns; i++) {
const column = document.createElement('div');
column.className = 'smg-column';
column.style.cssText = `
display: flex;
flex-direction: column;
gap: ${scrollMorphConfig.gap}px;
transform-style: preserve-3d;
will-change: transform;
transition: transform 0.1s linear;
`;
column.setAttribute('data-column-index', i);
columns.push(column);
galleryContainer.appendChild(column);
}
function distributeImages(images, columnCount) {
const distribution = Array(columnCount).fill().map(() => []);
if (columnCount === 2) {
images.forEach((img, index) => {
const targetColumn = index % 2;
distribution[targetColumn].push({ img, originalIndex: index });
});
const diff = Math.abs(distribution[0].length - distribution[1].length);
if (diff > 1) {
const longerCol = distribution[0].length > distribution[1].length ? 0 : 1;
const shorterCol = longerCol === 0 ? 1 : 0;
const itemToMove = distribution[longerCol].pop();
distribution[shorterCol].push(itemToMove);
}
} else {
images.forEach((img, index) => {
const targetColumn = index % columnCount;
distribution[targetColumn].push({ img, originalIndex: index });
});
}
return distribution;
}
const imageDistribution = distributeImages(scrollMorphConfig.images, scrollMorphConfig.columns);
imageDistribution.forEach((columnImages, columnIndex) => {
if (columnIndex < columns.length) {
columnImages.forEach(({ img: imgSrc, originalIndex }) => {
const imgWrapper = document.createElement('div');
imgWrapper.className = 'smg-item';
imgWrapper.style.cssText = `
position: relative;
width: 100%;
overflow: hidden;
border-radius: ${scrollMorphConfig.borderRadius}px;
aspect-ratio: 1;
background-color: #f0f0f0;
`;
const img = document.createElement('img');
img.src = imgSrc;
img.alt = `Gallery image ${originalIndex + 1}`;
img.loading = 'lazy';
img.style.cssText = `
width: 100%;
height: 100%;
object-fit: cover;
display: block;
`;
imgWrapper.appendChild(img);
columns[columnIndex].appendChild(imgWrapper);
});
}
});
perspectiveWrapper.appendChild(galleryContainer);
stickyWrapper.appendChild(perspectiveWrapper);
container.appendChild(stickyWrapper);
currentGallery = {
container: container,
gallery: galleryContainer,
wrapper: stickyWrapper
};
if (isWindows) {
galleryContainer.style.willChange = 'transform';
galleryContainer.style.backfaceVisibility = 'hidden';
galleryContainer.style.perspective = '1000px';
}
setTimeout(() => {
setupScrollAnimation();
}, 50);
setTimeout(() => {
updateGalleryTransform();
}, 100);
}
function setupScrollAnimation() {
const preview = document.getElementById('scrollmorph-preview');
if (!preview) return;
let hasScrolled = false;
let lastScrollTop = 0;
let scrollTimeout = null;
const handleScroll = () => {
if (!hasScrolled && preview.scrollTop > 10) {
hasScrolled = true;
preview.style.setProperty('--show-scroll-hint', 'none');
}
const currentScrollTop = preview.scrollTop;
const scrollDiff = Math.abs(currentScrollTop - lastScrollTop);
const scrollThreshold = isWindows ? 2 : 1;
if (scrollDiff < scrollThreshold) return;
lastScrollTop = currentScrollTop;
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
const throttleDelay = isWindows ? 16 : 8;
if (!scrollAnimationFrame) {
scrollAnimationFrame = requestAnimationFrame(() => {
updateGalleryTransform();
scrollAnimationFrame = null;
});
}
const timeoutDelay = isWindows ? 150 : 100;
scrollTimeout = setTimeout(() => {
if (scrollAnimationFrame) {
cancelAnimationFrame(scrollAnimationFrame);
scrollAnimationFrame = null;
}
}, timeoutDelay);
};
preview.removeEventListener('scroll', handleScroll);
preview.addEventListener('scroll', handleScroll, { passive: true });
preview.style.setProperty('--show-scroll-hint', 'block');
updateGalleryTransform();
}
let lastTransformState = null;
function updateGalleryTransform() {
if (!currentGallery) return;
const preview = document.getElementById('scrollmorph-preview');
if (!preview) return;
const container = currentGallery.container;
const gallery = currentGallery.gallery;
const rect = container.getBoundingClientRect();
const previewRect = preview.getBoundingClientRect();
const viewportHeight = preview.offsetHeight;
const totalHeight = container.offsetHeight;
const scrolled = preview.scrollTop;
const scrollProgress = Math.max(0, Math.min(1, scrolled / (totalHeight - viewportHeight)));
const roundedProgress = Math.round(scrollProgress * 1000) / 1000;
const changeThreshold = isWindows ? 0.005 : 0.001;
if (lastTransformState && Math.abs(lastTransformState - roundedProgress) < changeThreshold) {
return;
}
lastTransformState = roundedProgress;
const rotateX = scrollMorphConfig.maxRotation * (1 - Math.min(1, roundedProgress * 2));
const scaleStart = 0.5;
const scaleProgress = Math.max(0, (roundedProgress - scaleStart) / (1 - scaleStart));
const scale = 1 + (scrollMorphConfig.maxScale - 1) * scaleProgress;
const finalRotateX = Math.round(Math.max(0, rotateX) * 100) / 100;
const finalScale = Math.round(scale * 1000) / 1000;
gallery.style.transform = `rotateX(${finalRotateX}deg) scale(${finalScale})`;
if (roundedProgress > 0.5) {
const columns = gallery.querySelectorAll('.smg-column');
const parallaxProgress = (roundedProgress - 0.5) * 2;
columns.forEach((column, index) => {
const parallaxFactor = getParallaxFactor(index, columns.length);
const translateY = Math.round(parallaxFactor * scrollMorphConfig.parallaxStrength * parallaxProgress * 100) / 100;
column.style.transform = `translateY(${translateY}%)`;
});
} else {
const columns = gallery.querySelectorAll('.smg-column');
columns.forEach(column => {
column.style.transform = 'translateY(0)';
});
}
}
function getParallaxFactor(index, total) {
const factors = [0, -0.5, 0.25, -0.25, 0.5];
return factors[index % factors.length];
}
function generateRandomPalette() {
scrollMorphConfig.columns = Math.floor(Math.random() * 4) + 2;
scrollMorphConfig.gap = Math.floor(Math.random() * 20) + 4;
scrollMorphConfig.borderRadius = Math.floor(Math.random() * 32);
scrollMorphConfig.maxRotation = Math.floor(Math.random() * 60) + 30;
scrollMorphConfig.maxScale = Math.round((Math.random() * 0.5 + 1) * 100) / 100;
scrollMorphConfig.parallaxStrength = Math.floor(Math.random() * 30);
scrollMorphConfig.startOffset = Math.floor(Math.random() * 100);
scrollMorphConfig.containerHeight = Math.floor(Math.random() * 250) + 150;
scrollMorphConfig.mobile = Math.random() > 0.5;
document.getElementById('columns').value = scrollMorphConfig.columns;
document.getElementById('gap').value = scrollMorphConfig.gap;
document.getElementById('border-radius').value = scrollMorphConfig.borderRadius;
document.getElementById('max-rotation').value = scrollMorphConfig.maxRotation;
document.getElementById('max-scale').value = scrollMorphConfig.maxScale;
document.getElementById('parallax-strength').value = scrollMorphConfig.parallaxStrength;
document.getElementById('start-offset').value = scrollMorphConfig.startOffset;
document.getElementById('container-height').value = scrollMorphConfig.containerHeight;
document.getElementById('columns-value').textContent = scrollMorphConfig.columns;
document.getElementById('gap-value').textContent = scrollMorphConfig.gap;
document.getElementById('border-radius-value').textContent = scrollMorphConfig.borderRadius;
document.getElementById('max-rotation-value').textContent = scrollMorphConfig.maxRotation;
document.getElementById('max-scale-value').textContent = scrollMorphConfig.maxScale;
document.getElementById('parallax-strength-value').textContent = scrollMorphConfig.parallaxStrength;
document.getElementById('start-offset-value').textContent = scrollMorphConfig.startOffset;
document.getElementById('container-height-value').textContent = scrollMorphConfig.containerHeight;
const toggleBtns = document.querySelectorAll('.toggle-btn[data-setting="mobile"]');
toggleBtns.forEach(btn => {
const value = btn.getAttribute('data-value') === 'true';
if (value === scrollMorphConfig.mobile) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
updatePreview();
showNotification('Random palette generated!');
}
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": {
"_justifyContent": "center",
"_background": {
"color": {
"hex": "#ffffff"
}
}
}
},
{
"id": containerId,
"name": "container",
"parent": sectionId,
"children": [],
"settings": {
"_alignItems": "center",
"_attributes": [
{
"id": attributeId,
"name": "data-smg"
}
],
"_width": "100%",
"_height": "400"
},
"label": "Scroll Gallery Container"
},
{
"id": codeId,
"name": "code",
"parent": sectionId,
"children": [],
"settings": {
"javascriptCode": jsCode,
"executeCode": true,
"_display": "none"
},
"label": "Scroll Gallery JS"
}
],
"source": "bricksCopiedElements",
"sourceUrl": "https://test.bricksfusion.com",
"version": "2.0.1",
"globalClasses": [],
"globalElements": []
};
return JSON.stringify(bricksJSON, null, 2);
}
function generateJavaScriptCode() {
const validUrls = scrollMorphConfig.images.filter(url => url.trim() !== '');
return `(function() {
'use strict';
class ScrollMorphGallery {
constructor() {
this.galleries = [];
this.rafId = null;
this.init();
}
init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setupGalleries());
} else {
this.setupGalleries();
}
if (window.bricksIsFrontend) {
window.addEventListener('bricks:frontend:loaded', () => this.setupGalleries());
}
}
setupGalleries() {
const galleryElements = document.querySelectorAll('[data-smg]');
galleryElements.forEach(element => {
if (!element.hasAttribute('data-smg-initialized')) {
this.createGallery(element);
}
});
}
createGallery(container) {
container.setAttribute('data-smg-initialized', 'true');
const config = {
columns: ${scrollMorphConfig.columns},
gap: '${scrollMorphConfig.gap}px',
borderRadius: '${scrollMorphConfig.borderRadius}px',
maxRotation: ${scrollMorphConfig.maxRotation},
maxScale: ${scrollMorphConfig.maxScale},
parallaxStrength: ${scrollMorphConfig.parallaxStrength},
mobile: ${scrollMorphConfig.mobile},
startOffset: '${scrollMorphConfig.startOffset}vh',
containerHeight: '${scrollMorphConfig.containerHeight}vh',
images: ${JSON.stringify(validUrls)}
};
if (!config.mobile && window.innerWidth <= 768) {
return;
}
const gallery = this.buildGalleryStructure(container, config);
this.galleries.push({
container: container,
gallery: gallery,
config: config,
wrapper: gallery.parentElement,
scrollProgress: 0
});
this.setupScrollAnimation();
this.handleResize();
}
buildGalleryStructure(container, config) {
const stickyWrapper = document.createElement('div');
stickyWrapper.className = 'smg-sticky';
stickyWrapper.style.cssText = \`
position: sticky;
top: 0;
left: 0;
width: 100%;
height: 100vh;
overflow: hidden;
z-index: 1;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
margin-top: \${config.startOffset};
\`;
const perspectiveWrapper = document.createElement('div');
perspectiveWrapper.className = 'smg-perspective';
perspectiveWrapper.style.cssText = \`
position: relative;
width: 90%;
max-width: 1200px;
height: auto;
min-height: 60%;
max-height: 80%;
perspective: 1000px;
perspective-origin: center top;
transform-style: preserve-3d;
padding: 20px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
\`;
const galleryContainer = document.createElement('div');
galleryContainer.className = 'smg-container';
galleryContainer.style.cssText = \`
position: relative;
width: 100%;
height: auto;
display: grid;
grid-template-columns: repeat(\${config.columns}, 1fr);
gap: \${config.gap};
border-radius: \${config.borderRadius};
transform-style: preserve-3d;
transform-origin: center center;
will-change: transform;
pointer-events: auto;
\`;
const images = config.images;
const isMobile = window.innerWidth <= 768;
const actualColumns = isMobile ? 2 : config.columns;
const columns = [];
for (let i = 0; i < config.columns; i++) {
const column = document.createElement('div');
column.className = 'smg-column';
column.style.cssText = \`
display: flex;
flex-direction: column;
gap: \${config.gap};
transform-style: preserve-3d;
will-change: transform;
transition: transform 0.1s linear;
\`;
column.setAttribute('data-column-index', i);
columns.push(column);
galleryContainer.appendChild(column);
}
function distributeImages(images, columnCount) {
const distribution = Array(columnCount).fill().map(() => []);
if (columnCount === 2) {
images.forEach((img, index) => {
const targetColumn = index % 2;
distribution[targetColumn].push({ img, originalIndex: index });
});
const diff = Math.abs(distribution[0].length - distribution[1].length);
if (diff > 1) {
const longerCol = distribution[0].length > distribution[1].length ? 0 : 1;
const shorterCol = longerCol === 0 ? 1 : 0;
const itemToMove = distribution[longerCol].pop();
distribution[shorterCol].push(itemToMove);
}
} else {
images.forEach((img, index) => {
const targetColumn = index % columnCount;
distribution[targetColumn].push({ img, originalIndex: index });
});
}
return distribution;
}
const imageDistribution = distributeImages(images, actualColumns);
imageDistribution.forEach((columnImages, columnIndex) => {
if (columnIndex < columns.length) {
columnImages.forEach(({ img: imgSrc, originalIndex }) => {
const imgWrapper = document.createElement('div');
imgWrapper.className = 'smg-item';
imgWrapper.style.cssText = \`
position: relative;
width: 100%;
overflow: hidden;
border-radius: \${config.borderRadius};
aspect-ratio: 1;
background-color: #f0f0f0;
\`;
const img = document.createElement('img');
img.src = imgSrc;
img.alt = \`Gallery image \${originalIndex + 1}\`;
img.loading = 'lazy';
img.style.cssText = \`
width: 100%;
height: 100%;
object-fit: cover;
display: block;
\`;
imgWrapper.appendChild(img);
columns[columnIndex].appendChild(imgWrapper);
});
}
});
perspectiveWrapper.appendChild(galleryContainer);
stickyWrapper.appendChild(perspectiveWrapper);
container.style.position = 'relative';
if (!container.style.minHeight && !container.style.height) {
container.style.minHeight = config.containerHeight;
}
container.insertBefore(stickyWrapper, container.firstChild);
setTimeout(() => {
this.adjustGallerySpacing(galleryContainer, config);
}, 100);
return galleryContainer;
}
adjustGallerySpacing(galleryContainer, config) {
const rect = galleryContainer.getBoundingClientRect();
const galleryHeight = rect.height;
const maxParallaxMovement = (config.parallaxStrength / 100) * galleryHeight;
const safetyMargin = 100;
const perspectiveWrapper = galleryContainer.parentElement;
perspectiveWrapper.style.paddingBottom = \`\${maxParallaxMovement + safetyMargin}px\`;
}
setupScrollAnimation() {
const handleScroll = () => {
if (!this.rafId) {
this.rafId = requestAnimationFrame(() => {
this.updateGalleryTransforms();
this.rafId = null;
});
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
this.updateGalleryTransforms();
}
updateGalleryTransforms() {
this.galleries.forEach(({ container, gallery, config }) => {
const rect = container.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const totalHeight = rect.height;
const scrolled = -rect.top;
const scrollProgress = Math.max(0, Math.min(1, scrolled / (totalHeight - viewportHeight)));
const rotateX = config.maxRotation * (1 - Math.min(1, scrollProgress * 2));
const scaleStart = 0.5;
const scaleProgress = Math.max(0, (scrollProgress - scaleStart) / (1 - scaleStart));
const scale = 1 + (config.maxScale - 1) * scaleProgress;
gallery.style.transform = \`rotateX(\${Math.max(0, rotateX)}deg) scale(\${scale})\`;
if (scrollProgress > 0.5) {
const columns = gallery.querySelectorAll('.smg-column');
const parallaxProgress = (scrollProgress - 0.5) * 2;
columns.forEach((column, index) => {
const parallaxFactor = this.getParallaxFactor(index, columns.length);
const translateY = parallaxFactor * config.parallaxStrength * parallaxProgress;
column.style.transform = \`translateY(\${translateY}%)\`;
});
} else {
const columns = gallery.querySelectorAll('.smg-column');
columns.forEach(column => {
column.style.transform = 'translateY(0)';
});
}
});
}
getParallaxFactor(index, total) {
const factors = [0, -0.5, 0.25, -0.25, 0.5];
return factors[index % factors.length];
}
handleResize() {
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
this.galleries.forEach(({ container, config, wrapper }) => {
if (!config.mobile && window.innerWidth <= 768) {
if (wrapper) {
wrapper.style.display = 'none';
}
} else {
if (wrapper) {
wrapper.style.display = 'flex';
}
}
});
}, 250);
});
}
destroy() {
this.galleries.forEach(({ container }) => {
container.removeAttribute('data-smg-initialized');
const wrapper = container.querySelector('.smg-sticky');
if (wrapper) {
wrapper.remove();
}
});
this.galleries = [];
}
}
window.ScrollMorphGallery = new ScrollMorphGallery();
const style = document.createElement('style');
style.textContent = \`
.smg-sticky {
overflow: hidden !important;
}
.smg-perspective {
transition: padding 0.3s ease;
overflow: visible;
}
.smg-container {
backface-visibility: hidden;
}
.smg-column {
align-content: start;
}
.smg-item {
transition: transform 0.3s ease;
backface-visibility: hidden;
}
.smg-item:hover {
transform: scale(1.05) translateZ(20px);
}
.smg-item img {
transition: transform 0.5s ease;
}
.smg-item:hover img {
transform: scale(1.1);
}
@media (max-width: 768px) {
.smg-container {
grid-template-columns: repeat(2, 1fr) !important;
}
.smg-perspective {
width: 95% !important;
}
}
\`;
document.head.appendChild(style);
})();`;
}
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 'columns':
scrollMorphConfig.columns = defaultValue;
updatePreview();
break;
case 'gap':
scrollMorphConfig.gap = defaultValue;
updatePreview();
break;
case 'border-radius':
scrollMorphConfig.borderRadius = defaultValue;
updatePreview();
break;
case 'max-rotation':
scrollMorphConfig.maxRotation = defaultValue;
updatePreview();
break;
case 'max-scale':
scrollMorphConfig.maxScale = defaultValue;
updatePreview();
break;
case 'parallax-strength':
scrollMorphConfig.parallaxStrength = defaultValue;
updatePreview();
break;
case 'start-offset':
scrollMorphConfig.startOffset = defaultValue;
updatePreview();
break;
case 'container-height':
scrollMorphConfig.containerHeight = defaultValue;
updatePreview();
break;
}
showNotification(`${parameterId.replace(/-/g, ' ')} reset to default`);
}
};
function initializeUI() {
setTimeout(() => {
initScrollMorphGallery();
}, 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-smg');
});
}
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 generateRandomPaletteBtn = document.getElementById('generate-random-palette');
if (generateRandomPaletteBtn) {
generateRandomPaletteBtn.addEventListener('click', () => {
generateRandomPalette();
});
}
const resetLayout = document.getElementById('reset-layout');
if (resetLayout) {
resetLayout.addEventListener('click', () => {
scrollMorphConfig.columns = defaultConfig.columns;
scrollMorphConfig.gap = defaultConfig.gap;
scrollMorphConfig.borderRadius = defaultConfig.borderRadius;
scrollMorphConfig.startOffset = defaultConfig.startOffset;
const columns = document.getElementById('columns');
const gap = document.getElementById('gap');
const borderRadius = document.getElementById('border-radius');
const startOffset = document.getElementById('start-offset');
const columnsValue = document.getElementById('columns-value');
const gapValue = document.getElementById('gap-value');
const borderRadiusValue = document.getElementById('border-radius-value');
const startOffsetValue = document.getElementById('start-offset-value');
if (columns) columns.value = defaultConfig.columns;
if (gap) gap.value = defaultConfig.gap;
if (borderRadius) borderRadius.value = defaultConfig.borderRadius;
if (startOffset) startOffset.value = defaultConfig.startOffset;
if (columnsValue) columnsValue.textContent = defaultConfig.columns;
if (gapValue) gapValue.textContent = defaultConfig.gap;
if (borderRadiusValue) borderRadiusValue.textContent = defaultConfig.borderRadius;
if (startOffsetValue) startOffsetValue.textContent = defaultConfig.startOffset;
updatePreview();
showNotification('Layout settings reset');
});
}
const resetAnimation = document.getElementById('reset-animation');
if (resetAnimation) {
resetAnimation.addEventListener('click', () => {
scrollMorphConfig.maxRotation = defaultConfig.maxRotation;
scrollMorphConfig.maxScale = defaultConfig.maxScale;
scrollMorphConfig.parallaxStrength = defaultConfig.parallaxStrength;
scrollMorphConfig.containerHeight = defaultConfig.containerHeight;
const maxRotation = document.getElementById('max-rotation');
const maxScale = document.getElementById('max-scale');
const parallaxStrength = document.getElementById('parallax-strength');
const containerHeight = document.getElementById('container-height');
const maxRotationValue = document.getElementById('max-rotation-value');
const maxScaleValue = document.getElementById('max-scale-value');
const parallaxStrengthValue = document.getElementById('parallax-strength-value');
const containerHeightValue = document.getElementById('container-height-value');
if (maxRotation) maxRotation.value = defaultConfig.maxRotation;
if (maxScale) maxScale.value = defaultConfig.maxScale;
if (parallaxStrength) parallaxStrength.value = defaultConfig.parallaxStrength;
if (containerHeight) containerHeight.value = defaultConfig.containerHeight;
if (maxRotationValue) maxRotationValue.textContent = defaultConfig.maxRotation;
if (maxScaleValue) maxScaleValue.textContent = defaultConfig.maxScale;
if (parallaxStrengthValue) parallaxStrengthValue.textContent = defaultConfig.parallaxStrength;
if (containerHeightValue) containerHeightValue.textContent = defaultConfig.containerHeight;
updatePreview();
showNotification('Animation settings reset');
});
}
const resetAdvanced = document.getElementById('reset-advanced');
if (resetAdvanced) {
resetAdvanced.addEventListener('click', () => {
scrollMorphConfig.mobile = defaultConfig.mobile;
const toggleBtns = document.querySelectorAll('.toggle-btn[data-setting="mobile"]');
toggleBtns.forEach(btn => {
const value = btn.getAttribute('data-value') === 'true';
if (value === defaultConfig.mobile) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
updatePreview();
showNotification('Advanced settings reset');
});
}
const resetImages = document.getElementById('reset-images');
if (resetImages) {
resetImages.addEventListener('click', () => {
scrollMorphConfig.images = [...defaultConfig.images];
document.querySelectorAll('.url-input').forEach((input, index) => {
if (index < defaultConfig.images.length) {
input.value = defaultConfig.images[index];
} else {
input.value = '';
}
});
updatePreview();
showNotification('Image URLs reset');
});
}
document.querySelectorAll('.toggle-btn').forEach(btn => {
btn.addEventListener('click', function() {
const setting = this.getAttribute('data-setting');
const value = this.getAttribute('data-value') === 'true';
this.parentElement.querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
scrollMorphConfig[setting] = value;
updatePreview();
});
});
const rangeInputs = document.querySelectorAll('input[type="range"]');
rangeInputs.forEach(input => {
const valueElement = document.getElementById(`${input.id}-value`);
if (valueElement) {
valueElement.textContent = input.value;
}
input.addEventListener('input', () => {
if (valueElement) {
valueElement.textContent = input.value;
}
switch (input.id) {
case 'columns':
scrollMorphConfig.columns = parseInt(input.value);
updatePreview();
break;
case 'gap':
scrollMorphConfig.gap = parseInt(input.value);
updatePreview();
break;
case 'border-radius':
scrollMorphConfig.borderRadius = parseInt(input.value);
updatePreview();
break;
case 'max-rotation':
scrollMorphConfig.maxRotation = parseFloat(input.value);
updatePreview();
break;
case 'max-scale':
scrollMorphConfig.maxScale = parseFloat(input.value);
updatePreview();
break;
case 'parallax-strength':
scrollMorphConfig.parallaxStrength = parseFloat(input.value);
updatePreview();
break;
case 'start-offset':
scrollMorphConfig.startOffset = parseInt(input.value);
updatePreview();
break;
case 'container-height':
scrollMorphConfig.containerHeight = parseInt(input.value);
updatePreview();
break;
}
});
});
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(url);
}
});
scrollMorphConfig.images = newImageUrls;
updatePreview();
});
});
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 > 12) {
showNotification('Maximum 12 images 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">Image ${count}</span>
</div>
<div class="image-url-input">
<input type="text" placeholder="https://example.com/image${count}.jpg" class="url-input" data-index="${count}">
<button class="remove-btn" title="Remove this image">×</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(url);
}
});
scrollMorphConfig.images = newImageUrls;
updatePreview();
});
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(url);
}
});
scrollMorphConfig.images = newImageUrls;
updatePreview();
}, 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 = `Image ${newIndex}`;
input.setAttribute('data-index', newIndex);
input.setAttribute('placeholder', `https://example.com/image${newIndex}.jpg`);
});
}
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;
}
}
});
setTimeout(() => {
showNotification('ScrollMorphGallery configurator loaded!');
}, 500);
}
initializeUI();
});
</script>
</body>
</html>
Scroll Gallery
Creates 3D perspective gallery that rotates and scales as you scroll. Features parallax column movement for depth effect. Gallery starts flat and rotates into view, then scales up while columns move at different speeds. Fully responsive with automatic mobile optimization. Perfect for showcasing photography, portfolio work, or creating immersive visual storytelling.
Scroll Gallery
Scroll down to see the 3D gallery effect in action.
Images
Add the images you want to display in the gallery. Images will be automatically distributed across columns for balanced layout. Upload as many images as needed.
Required
Grid Layout
Number of columns in the gallery grid. Lower creates wider images with fewer columns. Higher produces more columns with narrower images. Automatically reduces to 2 columns on mobile.
Default: 3 columns
Space between images in the grid. Lower creates tight, compact layout. Higher produces airy, spacious arrangement.
Default: 8px
Roundness of image corners. Lower keeps images sharp and rectangular. Higher creates smooth, rounded corners.
Default: 16px
Proportions of each image. 1.0 creates perfect squares. Lower makes vertical rectangles. Higher creates horizontal rectangles.
Default: Square (1.0)
3D Effects
How far the gallery rotates in 3D space. Lower creates gentle tilt. Higher produces dramatic perspective rotation. Gallery starts at this angle and rotates to flat as you scroll.
Default: 75 degrees
How much the gallery grows as you scroll. 1.0 keeps original size. Higher values zoom in progressively, making images larger and more prominent.
Default: 1.2x size
Parallax
How dramatically columns move at different speeds. Lower creates gentle, barely-noticeable depth. Higher produces bold, obvious layering effect. Each column moves at unique speed for 3D depth illusion.
Default: 30%
Container
Vertical position where gallery appears in viewport. Controls how far down the page the sticky gallery begins. Useful for positioning below headers or other content.
Default: Top (0)
Total height of scrollable area. Controls how long the animation lasts. Lower creates quick transformation. Higher produces slow, gradual effect that lets users appreciate each stage.
Default: 180vh
Mobile
Whether to show the 3D effect on mobile devices. When enabled, automatically switches to 2 columns with optimized sizing. When disabled, hides the gallery on mobile completely.
Default: Enabled
Performance
This element uses CSS 3D transforms with scroll-based animation. Features requestAnimationFrame for smooth 60fps scrolling. Implements automatic image distribution across columns for balanced layout. Uses sticky positioning with perspective transforms. Includes responsive behavior with automatic mobile optimization. Moderate performance - suitable for most devices with 1 instance per page recommended.