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>Whisper Motion Configurator - BricksFusion</title>
<style>
:root {
--background: #000;
--card-bg: #1e1e1e;
--card-bg-hover: #252525;
--text-primary: #f2f2f7;
--text-secondary: #8e8e93;
--accent: #ef6013;
--accent-hover: #c64c0c;
--border: #2c2c2e;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
--track: #2c2c2e;
--thumb: #ef6013;
--card-radius: 16px;
--input-radius: 8px;
--button-radius: 12px;
--transition: all 0.25s ease;
--font: 'Inter', BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Arial, sans-serif;
--action-bar-height: 70px;
--success: #28a745;
--warning: #ffc107;
--danger: #dc3545;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font);
background-color: var(--background);
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding-bottom: var(--action-bar-height);
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--action-bar-height);
background: linear-gradient(145deg, #1a1a1a, #0f0f0f);
border-top: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
padding: 0 1.5rem;
gap: 1rem;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.breadcrumb-item {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 500;
text-decoration: none;
transition: var(--transition);
padding: 0.5rem 0.75rem;
border-radius: 6px;
}
.breadcrumb-item:hover {
color: var(--text-primary);
background-color: rgba(255, 255, 255, 0.05);
}
.breadcrumb-item.active {
color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.breadcrumb-separator {
color: var(--text-secondary);
font-size: var(--text-xs);
opacity: 0.5;
}
.action-buttons {
display: flex;
align-items: center;
gap: 0.75rem;
}
.action-btn {
padding: 0.6rem 1rem;
background-color: var(--card-bg);
color: var(--text-primary);
font-family: var(--font);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid var(--border);
border-radius: var(--button-radius);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
white-space: nowrap;
}
.action-btn:hover {
background-color: var(--card-bg-hover);
border-color: var(--accent);
transform: translateY(-1px);
}
.action-btn.primary {
background: linear-gradient(90deg, var(--accent), #ff8c51);
border-color: var(--accent);
color: white;
}
.action-btn.primary:hover {
background: linear-gradient(90deg, var(--accent-hover), #e67a3f);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 96, 19, 0.3);
}
.data-attribute-display {
background-color: rgba(50, 50, 50, 0.8);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
cursor: pointer;
transition: var(--transition);
user-select: all;
}
.data-attribute-display:hover {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
}
.container {
max-width: 100%;
margin: 0 auto;
padding: 2rem 1.5rem;
}
.page-header {
text-align: center;
margin-bottom: 2rem;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
background: linear-gradient(90deg, var(--accent), #ff8c51);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.page-subtitle {
font-size: var(--text-s);
color: var(--text-secondary);
font-weight: 500;
}
.instructions-toggle {
margin-bottom: 2rem;
}
.instructions-card {
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
transition: var(--transition);
}
.instructions-header {
padding: 1rem 1.5rem;
cursor: pointer;
transition: var(--transition);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid transparent;
}
.instructions-header:hover {
background-color: var(--card-bg-hover);
}
.instructions-card.expanded .instructions-header {
border-bottom-color: var(--border);
}
.instructions-title {
font-size: var(--text-s);
font-weight: 600;
}
.toggle-icon {
font-size: 1.2em;
transition: transform 0.3s ease;
}
.toggle-icon.expanded {
transform: rotate(180deg);
}
.instructions-content {
padding: 0 1.5rem;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.instructions-content.show {
max-height: 500px;
padding: 1.5rem;
}
.instructions-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.how-to-use ol {
padding-left: 1.5rem;
}
.how-to-use li {
margin-bottom: 0.75rem;
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.6;
}
.how-to-use strong {
color: var(--text-primary);
font-weight: 600;
}
.how-to-use code {
background-color: rgba(50, 50, 50, 0.5);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
}
.content {
display: grid;
grid-template-columns: 1fr 500px;
gap: 2rem;
align-items: start;
}
.preview-section {
position: sticky;
top: 2rem;
}
.controls-section {
max-width: 500px;
}
.card {
background-color: var(--card-bg);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}
.preview-container {
height: 400px;
width: 100%;
position: relative;
overflow: hidden;
border-radius: var(--card-radius);
background-color: #252525;
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.preview-content {
color: white;
text-align: center;
font-weight: bold;
font-size: var(--text-s);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
.preview-controls {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.5rem;
z-index: 10;
}
.preview-btn {
padding: 0.5rem;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
cursor: pointer;
transition: var(--transition);
font-size: var(--text-xs);
backdrop-filter: blur(5px);
}
.preview-btn:hover {
background-color: var(--accent);
border-color: var(--accent);
}
.preview-btn svg {
width: 18px;
height: 18px;
stroke: currentColor;
}
.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: #ef6013;
}
.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, #ef6013);
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);
}
input[type="text"] {
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);
}
input[type="text"]:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
.image-urls-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.image-input-container {
display: flex;
gap: 0.5rem;
align-items: center;
}
.image-url-input {
flex: 1;
margin-bottom: 0 !important;
}
.btn {
width: 100%;
padding: 0.75rem 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;
justify-content: center;
gap: 0.5rem;
}
.btn:hover {
background-color: var(--card-bg-hover);
border-color: var(--accent);
transform: translateY(-1px);
}
.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;
}
.whisper-motion {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
pointer-events: none;
background-color: transparent !important;
}
[data-whisper-motion] {
position: relative !important;
overflow: hidden !important;
min-height: 200px;
}
[data-whisper-motion] > *:not(.whisper-motion) {
position: relative;
z-index: 50;
pointer-events: auto !important;
}
.whisper-card {
position: absolute;
background-color: rgba(40, 40, 40, 0.7);
backdrop-filter: blur(5px);
border-radius: var(--card-radius, 12px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 25px rgba(0, 0, 0, var(--shadow-intensity, 0.3));
overflow: hidden;
transition: transform 0.5s ease, border-color 0.3s ease, left 0.3s ease, top 0.3s ease, opacity 0.3s ease;
width: var(--card-size, 130px);
height: var(--card-size, 130px);
z-index: 2;
opacity: 0.9;
pointer-events: none;
mix-blend-mode: normal;
will-change: transform, left, top;
}
.whisper-card img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.whisper-card:hover {
border-color: var(--accent-color, var(--accent));
transform: translateY(-5px) rotate(var(--hover-rotation, 0deg));
}
.whisper-card:hover img {
transform: scale(var(--hover-zoom, 1.05));
}
@keyframes floatAnimation {
0% {
transform: translateY(0) translateX(0) rotate(0deg);
}
50% {
transform: translateY(var(--float-y)) translateX(var(--float-x)) rotate(var(--float-rotation));
}
100% {
transform: translateY(0) translateX(0) rotate(0deg);
}
}
@keyframes circularAnimation {
0% {
transform: translateY(0) translateX(0) rotate(0deg);
}
25% {
transform: translateY(var(--float-y, 0px)) translateX(0) rotate(var(--float-rotation, 0deg));
}
50% {
transform: translateY(0) translateX(var(--float-x, 0px)) rotate(calc(var(--float-rotation, 0deg) * 2));
}
75% {
transform: translateY(calc(var(--float-y, 0px) * -1)) translateX(0) rotate(var(--float-rotation, 0deg));
}
100% {
transform: translateY(0) translateX(0) rotate(0deg);
}
}
@keyframes zigzagAnimation {
0% {
transform: translateY(0) translateX(0) rotate(0deg);
}
25% {
transform: translateY(var(--float-y, 0px)) translateX(var(--float-x, 0px)) rotate(var(--float-rotation, 0deg));
}
50% {
transform: translateY(0) translateX(calc(var(--float-x, 0px) * -1.5)) rotate(calc(var(--float-rotation, 0deg) * -1));
}
75% {
transform: translateY(calc(var(--float-y, 0px) * -1)) translateX(var(--float-x, 0px)) rotate(var(--float-rotation, 0deg));
}
100% {
transform: translateY(0) translateX(0) rotate(0deg);
}
}
@keyframes pulseAnimation {
0% {
transform: translateY(0) translateX(0) rotate(0deg) scale(1);
}
50% {
transform: translateY(calc(var(--float-y, 0px) * 0.5)) translateX(calc(var(--float-x, 0px) * 0.5)) rotate(var(--float-rotation, 0deg)) scale(1.05);
}
100% {
transform: translateY(0) translateX(0) rotate(0deg) scale(1);
}
}
@media (max-width: 1200px) {
.content {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.preview-section {
position: static;
}
.controls-section {
max-width: 100%;
}
}
@media (max-width: 768px) {
.action-bar {
flex-direction: column;
height: auto;
min-height: var(--action-bar-height);
padding: 0.75rem;
}
.breadcrumb {
order: 1;
width: 100%;
}
.action-buttons {
order: 2;
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
body {
padding-bottom: calc(var(--action-bar-height) + 20px);
}
.notification {
bottom: calc(var(--action-bar-height) + 2rem);
max-width: 280px;
transform: translate(-50%, 250px);
}
.notification.show {
transform: translate(-50%, 0);
opacity: 1;
}
.color-row {
flex-direction: column;
align-items: stretch;
gap: 1rem;
padding: 1rem;
}
.color-picker-container {
align-self: center;
margin-bottom: 0.5rem;
}
.color-input-group {
align-items: stretch;
}
.hex-input,
.hsl-input {
width: 100%;
}
.preview-container {
height: 300px;
}
.data-attribute-display {
font-size: 10px;
padding: 0.4rem 0.6rem;
}
.action-btn {
font-size: 11px;
padding: 0.5rem 0.8rem;
}
.page-title {
font-size: 2rem;
}
.whisper-card {
width: var(--mobile-size, 80px);
height: var(--mobile-size, 80px);
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
button:focus-visible,
input:focus-visible,
.action-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--background);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
.loading {
opacity: 0.6;
pointer-events: none;
position: relative;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="action-bar">
<nav class="breadcrumb">
<a href="https://bricksfusion.com" class="breadcrumb-item">Home</a>
<span class="breadcrumb-separator">›</span>
<a href="https://bricksfusion.com/visual-effects/" class="breadcrumb-item">Visual effects</a>
<span class="breadcrumb-separator">›</span>
<span class="breadcrumb-item active">Whisper Motion</span>
</nav>
<div class="action-buttons">
<div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
data-whisper-motion
</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">Whisper Motion</h1>
<p class="page-subtitle">Interactive floating card animations for Bricks Builder</p>
</div>
<div class="instructions-toggle">
<div class="instructions-card" id="instructions-card">
<div class="instructions-header" id="instructions-toggle">
<div class="instructions-title">
How to Use & Code Information
</div>
<span class="toggle-icon">▼</span>
</div>
<div class="instructions-content" id="instructions-content">
<div class="instructions-grid">
<div class="how-to-use">
<ol>
<li>Customize your whisper motion animation using the controls below</li>
<li>Click <strong>Copy JS</strong> to copy the JavaScript code to clipboard</li>
<li>In Bricks Builder, add a <strong>Code</strong> element</li>
<li>Paste or upload the JavaScript code</li>
<li>To add the effect to any container: go to <strong>Section → Style → Attributes</strong>, add <code>data-whisper-motion</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="whisper-preview" data-whisper-motion="true">
<div class="preview-content">Interactive Whisper Motion Preview</div>
</div>
</section>
<section class="controls-section">
<div class="card">
<div class="card-heading">
Accent Color
<div class="card-actions">
<button class="card-action-btn" id="reset-colors" title="Reset Colors">↺</button>
</div>
</div>
<div class="card-content">
<div class="color-list">
<div class="color-row">
<div class="color-picker-container">
<input type="color" id="accent-color" value="#ef6013">
</div>
<div class="color-input-group">
<span class="color-label">HEX</span>
<input type="text" class="color-input hex-input" id="accent-color-hex" value="#ef6013" placeholder="#FFFFFF">
</div>
<div class="color-input-group">
<span class="color-label">HSL</span>
<input type="text" class="color-input hsl-input" id="accent-color-hsl" placeholder="hsl(0, 100%, 50%)">
</div>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Shadow Intensity
<span class="help-tooltip" title="Shadow depth for floating cards">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="shadow-intensity-value">0.3</span></span>
<button class="reset-btn" onclick="resetParameter('shadow-intensity', 0.3)">↺</button>
</div>
</div>
<input type="range" id="shadow-intensity" min="0" max="0.8" step="0.05" value="0.3">
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Animation Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-animation" title="Reset Animation Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Animation Speed
<span class="help-tooltip" title="Animation duration in seconds">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="animation-speed-value">5</span>s</span>
<button class="reset-btn" onclick="resetParameter('animation-speed', 5)">↺</button>
</div>
</div>
<input type="range" id="animation-speed" min="2" max="10" step="0.5" value="5">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Float Distance
<span class="help-tooltip" title="Maximum distance cards will float">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="float-distance-value">15</span>px</span>
<button class="reset-btn" onclick="resetParameter('float-distance', 15)">↺</button>
</div>
</div>
<input type="range" id="float-distance" min="5" max="30" step="1" value="15">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Rotation Amount
<span class="help-tooltip" title="Maximum rotation of floating cards">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="rotation-amount-value">5</span>°</span>
<button class="reset-btn" onclick="resetParameter('rotation-amount', 5)">↺</button>
</div>
</div>
<input type="range" id="rotation-amount" min="0" max="15" step="1" value="5">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Hover Zoom
<span class="help-tooltip" title="Scale multiplier on hover">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="hover-zoom-value">1.05</span>x</span>
<button class="reset-btn" onclick="resetParameter('hover-zoom', 1.05)">↺</button>
</div>
</div>
<input type="range" id="hover-zoom" min="1" max="1.2" step="0.01" value="1.05">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Motion Pattern</span>
</div>
<select id="motion-pattern">
<option value="float">Float (Default)</option>
<option value="circular">Circular</option>
<option value="zigzag">Zigzag</option>
<option value="pulse">Pulse</option>
</select>
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Appearance Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-appearance" title="Reset Appearance Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Number of Cards
<span class="help-tooltip" title="Total floating cards in animation">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="card-count-value">5</span></span>
<button class="reset-btn" onclick="resetParameter('card-count', 5)">↺</button>
</div>
</div>
<input type="range" id="card-count" min="3" max="15" step="1" value="5">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Card Size
<span class="help-tooltip" title="Width and height of floating cards">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="card-size-value">130</span>px</span>
<button class="reset-btn" onclick="resetParameter('card-size', 130)">↺</button>
</div>
</div>
<input type="range" id="card-size" min="80" max="200" step="10" value="130">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Card Border Radius
<span class="help-tooltip" title="Roundness of card corners">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="card-radius-value">12</span>px</span>
<button class="reset-btn" onclick="resetParameter('card-radius', 12)">↺</button>
</div>
</div>
<input type="range" id="card-radius" min="0" max="30" step="2" value="12">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Mobile Size
<span class="help-tooltip" title="Card size on mobile devices">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="mobile-size-value">80</span>px</span>
<button class="reset-btn" onclick="resetParameter('mobile-size', 80)">↺</button>
</div>
</div>
<input type="range" id="mobile-size" min="40" max="120" step="5" value="80">
</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">
Magnetic Force
<span class="help-tooltip" title="Magnetic attraction strength to cursor">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="magnetic-force-value">30</span></span>
<button class="reset-btn" onclick="resetParameter('magnetic-force', 30)">↺</button>
</div>
</div>
<input type="range" id="magnetic-force" min="10" max="50" step="5" value="30">
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Images Configuration
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">Image URLs</span>
</div>
<div id="image-urls-container" class="image-urls-container">
</div>
<button id="add-image" class="btn">
<span>➕</span> Add More Images
</button>
</div>
<button id="update-preview" class="btn">
<span>🔄</span> Update Preview
</button>
</div>
</div>
</section>
</div>
</div>
<div class="notification" id="notification"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let whisperConfig = {
animationSpeed: 5,
floatDistance: 15,
rotationAmount: 5,
hoverZoom: 1.05,
cardCount: 5,
cardSize: 130,
cardRadius: 12,
accentColor: '#ef6013',
shadowIntensity: 0.3,
motionPattern: 'float',
mobileSize: 80,
magneticForce: 30,
images: [
'https://images.pexels.com/photos/1366957/pexels-photo-1366957.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/2559941/pexels-photo-2559941.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/2387873/pexels-photo-2387873.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/3075993/pexels-photo-3075993.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1287145/pexels-photo-1287145.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1323550/pexels-photo-1323550.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1402787/pexels-photo-1402787.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/2662116/pexels-photo-2662116.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1544966/pexels-photo-1544966.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1438084/pexels-photo-1438084.jpeg?auto=compress&cs=tinysrgb&w=800'
]
};
const defaultConfig = { ...whisperConfig };
let activeWhisper = null;
function initWhisperMotion() {
const containers = document.querySelectorAll('[data-whisper-motion]:not([data-whisper-initialized="true"])');
containers.forEach((container) => {
const options = {
animationSpeed: container.hasAttribute('data-motion-speed')
? parseFloat(container.getAttribute('data-motion-speed'))
: whisperConfig.animationSpeed,
floatDistance: container.hasAttribute('data-motion-float-distance')
? parseInt(container.getAttribute('data-motion-float-distance'))
: whisperConfig.floatDistance,
rotationAmount: container.hasAttribute('data-motion-rotation')
? parseInt(container.getAttribute('data-motion-rotation'))
: whisperConfig.rotationAmount,
hoverZoom: container.hasAttribute('data-motion-hover-zoom')
? parseFloat(container.getAttribute('data-motion-hover-zoom'))
: whisperConfig.hoverZoom,
cardCount: container.hasAttribute('data-motion-card-count')
? parseInt(container.getAttribute('data-motion-card-count'))
: whisperConfig.cardCount,
cardSize: container.hasAttribute('data-motion-card-size')
? parseInt(container.getAttribute('data-motion-card-size'))
: whisperConfig.cardSize,
cardRadius: container.hasAttribute('data-motion-card-radius')
? parseInt(container.getAttribute('data-motion-card-radius'))
: whisperConfig.cardRadius,
accentColor: container.hasAttribute('data-motion-accent-color')
? container.getAttribute('data-motion-accent-color')
: whisperConfig.accentColor,
shadowIntensity: container.hasAttribute('data-motion-shadow-intensity')
? parseFloat(container.getAttribute('data-motion-shadow-intensity'))
: whisperConfig.shadowIntensity,
motionPattern: container.hasAttribute('data-motion-pattern')
? container.getAttribute('data-motion-pattern')
: whisperConfig.motionPattern,
mobileSize: container.hasAttribute('data-motion-mobile-size')
? parseInt(container.getAttribute('data-motion-mobile-size'))
: whisperConfig.mobileSize,
magneticForce: container.hasAttribute('data-magnetic-force')
? parseInt(container.getAttribute('data-magnetic-force'))
: whisperConfig.magneticForce,
images: JSON.parse(container.getAttribute('data-motion-images') || 'null') || whisperConfig.images
};
setupWhisperMotion(container, options);
container.dataset.whisperInitialized = 'true';
if (container.id === 'whisper-preview') {
activeWhisper = { element: container, options };
}
});
}
function setupWhisperMotion(element, options) {
if (window.getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
element.style.overflow = 'hidden';
element.style.setProperty('--accent-color', options.accentColor);
element.style.setProperty('--card-radius', options.cardRadius + 'px');
element.style.setProperty('--shadow-intensity', options.shadowIntensity);
element.style.setProperty('--hover-zoom', options.hoverZoom);
element.style.setProperty('--card-size', options.cardSize + 'px');
element.style.setProperty('--mobile-size', options.mobileSize + 'px');
Array.from(element.children).forEach(child => {
if (!child.classList.contains('whisper-motion')) {
child.style.position = 'relative';
child.style.zIndex = '50';
child.style.pointerEvents = 'auto';
}
});
const existingMotion = element.querySelector('.whisper-motion');
if (existingMotion) {
existingMotion.remove();
}
const motion = document.createElement('div');
motion.className = 'whisper-motion';
motion.style.position = 'absolute';
motion.style.width = '100%';
motion.style.height = '100%';
motion.style.top = '0';
motion.style.left = '0';
motion.style.zIndex = '1';
motion.style.pointerEvents = 'none';
motion.style.backgroundColor = 'transparent';
let animationName;
switch(options.motionPattern) {
case 'circular':
animationName = 'circularAnimation';
break;
case 'zigzag':
animationName = 'zigzagAnimation';
break;
case 'pulse':
animationName = 'pulseAnimation';
break;
default:
animationName = 'floatAnimation';
}
const cardCount = options.cardCount;
for (let i = 0; i < cardCount; i++) {
const card = document.createElement('div');
card.className = 'whisper-card';
const containerWidth = element.offsetWidth || 500;
const containerHeight = element.offsetHeight || 400;
const maxX = containerWidth - options.cardSize;
const maxY = containerHeight - options.cardSize;
const randomX = Math.floor(Math.random() * (maxX > 0 ? maxX : containerWidth/2));
const randomY = Math.floor(Math.random() * (maxY > 0 ? maxY : containerHeight/2));
const floatX = (Math.random() * 2 - 1) * options.floatDistance;
const floatY = (Math.random() * 2 - 1) * options.floatDistance;
const floatRotation = (Math.random() * 2 - 1) * options.rotationAmount;
const hoverRotation = (Math.random() * 2 - 1) * options.rotationAmount * 2;
const delay = Math.random() * -options.animationSpeed;
card.style.left = `${randomX}px`;
card.style.top = `${randomY}px`;
card.style.animation = `${animationName} ${options.animationSpeed}s ease-in-out infinite`;
card.style.animationDelay = `${delay}s`;
card.style.zIndex = 2;
card.style.backgroundColor = 'rgba(40, 40, 40, 0.7)';
card.style.backdropFilter = 'blur(5px)';
card.style.opacity = '0.9';
card.style.mixBlendMode = 'normal';
card.style.setProperty('--float-x', `${floatX}px`);
card.style.setProperty('--float-y', `${floatY}px`);
card.style.setProperty('--float-rotation', `${floatRotation}deg`);
card.style.setProperty('--hover-rotation', `${hoverRotation}deg`);
card.style.setProperty('--hover-zoom', options.hoverZoom);
card.dataset.originalLeft = `${randomX}`;
card.dataset.originalTop = `${randomY}`;
const img = document.createElement('img');
img.src = options.images[i % options.images.length];
img.alt = '';
img.loading = 'lazy';
img.decoding = 'async';
card.appendChild(img);
motion.appendChild(card);
}
element.appendChild(motion);
setupMagneticEffect(element, motion, options);
element._whisperOptions = options;
element._updateConfig = function(newOptions) {
Object.assign(this._whisperOptions, newOptions);
updateExistingWhisperMotion(this, this._whisperOptions);
};
}
function updateExistingWhisperMotion(element, options) {
element.style.setProperty('--accent-color', options.accentColor);
element.style.setProperty('--card-radius', options.cardRadius + 'px');
element.style.setProperty('--shadow-intensity', options.shadowIntensity);
element.style.setProperty('--hover-zoom', options.hoverZoom);
element.style.setProperty('--card-size', options.cardSize + 'px');
element.style.setProperty('--mobile-size', options.mobileSize + 'px');
const motion = element.querySelector('.whisper-motion');
if (!motion) return;
const cards = motion.querySelectorAll('.whisper-card');
let animationName;
switch(options.motionPattern) {
case 'circular':
animationName = 'circularAnimation';
break;
case 'zigzag':
animationName = 'zigzagAnimation';
break;
case 'pulse':
animationName = 'pulseAnimation';
break;
default:
animationName = 'floatAnimation';
}
cards.forEach((card, index) => {
if (index >= options.cardCount) {
card.remove();
return;
}
const currentDuration = card.style.animationDuration;
const currentDelay = card.style.animationDelay;
card.style.animation = `${animationName} ${options.animationSpeed}s ease-in-out infinite`;
card.style.animationDelay = currentDelay;
const floatX = parseFloat(card.style.getPropertyValue('--float-x'));
const floatY = parseFloat(card.style.getPropertyValue('--float-y'));
const currentRotation = parseFloat(card.style.getPropertyValue('--float-rotation'));
const currentHoverRotation = parseFloat(card.style.getPropertyValue('--hover-rotation'));
const directionX = Math.sign(floatX) || (Math.random() * 2 - 1);
const directionY = Math.sign(floatY) || (Math.random() * 2 - 1);
const directionRotation = Math.sign(currentRotation) || (Math.random() * 2 - 1);
const directionHover = Math.sign(currentHoverRotation) || (Math.random() * 2 - 1);
const newX = directionX * options.floatDistance;
const newY = directionY * options.floatDistance;
const newRotation = directionRotation * options.rotationAmount;
const newHoverRotation = directionHover * options.rotationAmount * 2;
card.style.setProperty('--float-x', `${newX}px`);
card.style.setProperty('--float-y', `${newY}px`);
card.style.setProperty('--float-rotation', `${newRotation}deg`);
card.style.setProperty('--hover-rotation', `${newHoverRotation}deg`);
card.style.setProperty('--hover-zoom', options.hoverZoom);
if (index < options.images.length || options.images.length > 0) {
const img = card.querySelector('img');
const imageIndex = index % options.images.length;
if (img && img.src !== options.images[imageIndex]) {
img.src = options.images[imageIndex];
}
}
});
if (cards.length < options.cardCount) {
for (let i = cards.length; i < options.cardCount; i++) {
const card = document.createElement('div');
card.className = 'whisper-card';
const containerWidth = element.offsetWidth || 500;
const containerHeight = element.offsetHeight || 400;
const maxX = containerWidth - options.cardSize;
const maxY = containerHeight - options.cardSize;
const randomX = Math.floor(Math.random() * (maxX > 0 ? maxX : containerWidth/2));
const randomY = Math.floor(Math.random() * (maxY > 0 ? maxY : containerHeight/2));
const floatX = (Math.random() * 2 - 1) * options.floatDistance;
const floatY = (Math.random() * 2 - 1) * options.floatDistance;
const floatRotation = (Math.random() * 2 - 1) * options.rotationAmount;
const hoverRotation = (Math.random() * 2 - 1) * options.rotationAmount * 2;
const delay = Math.random() * -options.animationSpeed;
card.style.left = `${randomX}px`;
card.style.top = `${randomY}px`;
card.style.animation = `${animationName} ${options.animationSpeed}s ease-in-out infinite`;
card.style.animationDelay = `${delay}s`;
card.style.zIndex = 2;
card.style.backgroundColor = 'rgba(40, 40, 40, 0.7)';
card.style.backdropFilter = 'blur(5px)';
card.style.opacity = '0.9';
card.style.mixBlendMode = 'normal';
card.style.setProperty('--float-x', `${floatX}px`);
card.style.setProperty('--float-y', `${floatY}px`);
card.style.setProperty('--float-rotation', `${floatRotation}deg`);
card.style.setProperty('--hover-rotation', `${hoverRotation}deg`);
card.style.setProperty('--hover-zoom', options.hoverZoom);
card.dataset.originalLeft = `${randomX}`;
card.dataset.originalTop = `${randomY}`;
const img = document.createElement('img');
img.src = options.images[i % options.images.length];
img.alt = '';
img.loading = 'lazy';
img.decoding = 'async';
card.appendChild(img);
motion.appendChild(card);
}
}
}
function setupMagneticEffect(container, cardsContainer, options) {
if (container._handleMouseMove) {
container.removeEventListener('mousemove', container._handleMouseMove);
}
if (container._handleMouseLeave) {
container.removeEventListener('mouseleave', container._handleMouseLeave);
}
container._cardsContainer = cardsContainer;
container._magneticRadius = 200;
container._magneticForce = options.magneticForce;
container._accentColor = options.accentColor;
const cards = cardsContainer.querySelectorAll('.whisper-card');
cards.forEach(card => {
card.style.transform = 'translateZ(0)';
});
let ticking = false;
const handleMouseMove = (e) => {
if (!ticking) {
window.requestAnimationFrame(() => {
handleContainerMouseMove(e, container);
ticking = false;
});
ticking = true;
}
};
const handleMouseLeave = (e) => {
handleContainerMouseLeave(e, container);
};
container._handleMouseMove = handleMouseMove;
container._handleMouseLeave = handleMouseLeave;
container.addEventListener('mousemove', handleMouseMove, { passive: true });
container.addEventListener('mouseleave', handleMouseLeave);
}
function handleContainerMouseMove(e, container) {
const cardsContainer = container._cardsContainer;
const rect = container.getBoundingClientRect();
const magneticRadius = container._magneticRadius;
const magneticForce = container._magneticForce;
const accentColor = container._accentColor;
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const cards = cardsContainer.querySelectorAll('.whisper-card');
cards.forEach(card => {
const cardRect = card.getBoundingClientRect();
const cardX = cardRect.left - rect.left + cardRect.width / 2;
const cardY = cardRect.top - rect.top + cardRect.height / 2;
const deltaX = mouseX - cardX;
const deltaY = mouseY - cardY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < magneticRadius) {
const normalizedDistance = distance / magneticRadius;
const t = 1 - normalizedDistance;
const easeOutCubic = t * t * (3 - 2 * t);
const force = easeOutCubic * magneticForce * 0.5;
const moveX = Math.min(Math.max((deltaX / distance) * force, -20), 20);
const moveY = Math.min(Math.max((deltaY / distance) * force, -20), 20);
const originalLeft = parseInt(card.dataset.originalLeft);
const originalTop = parseInt(card.dataset.originalTop);
card.style.left = `${originalLeft + moveX}px`;
card.style.top = `${originalTop + moveY}px`;
const opacityFactor = easeOutCubic;
const r = parseInt(accentColor.substr(1, 2), 16);
const g = parseInt(accentColor.substr(3, 2), 16);
const b = parseInt(accentColor.substr(5, 2), 16);
const borderColor = `rgba(${r}, ${g}, ${b}, ${opacityFactor * 0.6})`;
card.style.borderColor = borderColor;
card.style.opacity = 0.9 + (opacityFactor * 0.1);
} else {
card.style.left = `${card.dataset.originalLeft}px`;
card.style.top = `${card.dataset.originalTop}px`;
card.style.borderColor = 'rgba(255, 255, 255, 0.1)';
card.style.opacity = 0.9;
}
});
}
function handleContainerMouseLeave(e, container) {
const cardsContainer = container._cardsContainer;
const cards = cardsContainer.querySelectorAll('.whisper-card');
cards.forEach(card => {
card.style.left = `${card.dataset.originalLeft}px`;
card.style.top = `${card.dataset.originalTop}px`;
card.style.borderColor = 'rgba(255, 255, 255, 0.1)';
card.style.opacity = 0.9;
});
}
function updateWhisperPreview() {
const preview = document.getElementById('whisper-preview');
if (preview && preview._updateConfig) {
preview._updateConfig(whisperConfig);
}
}
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = `notification ${type}`;
notification.offsetHeight;
notification.style.visibility = 'visible';
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
if (!notification.classList.contains('show')) {
notification.style.visibility = 'hidden';
}
}, 400);
}, 3000);
}
function generateUniqueId() {
return Math.random().toString(36).substring(2, 8);
}
function generateFullSectionJSON() {
// Generar IDs únicos para todos los elementos
const sectionId = generateUniqueId();
const containerId = generateUniqueId();
const codeId = generateUniqueId();
const attributeId = generateUniqueId();
// Obtener el JavaScript actual con la configuración del usuario
const jsCode = generateJavaScriptCode();
// Crear el objeto JSON completo de Bricks Builder
const bricksJSON = {
"content": [
{
"id": sectionId,
"name": "section",
"parent": 0,
"children": [containerId, codeId],
"settings": {
"_height": "500",
"_justifyContent": "center",
"_background": {
"color": {
"hex": "#000000"
}
},
"_attributes": [
{
"id": attributeId,
"name": "data-whisper-motion"
}
]
},
"label": "Whisper Motion Section"
},
{
"id": containerId,
"name": "container",
"parent": sectionId,
"children": [],
"settings": {
"_alignItems": "center",
"_overflow": "hidden"
}
},
{
"id": codeId,
"name": "code",
"parent": sectionId,
"children": [],
"settings": {
"javascriptCode": jsCode,
"executeCode": true,
"_display": "none"
},
"label": "Whisper Motion JS"
}
],
"source": "bricksCopiedElements",
"sourceUrl": "https://test.bricksfusion.com",
"version": "2.0.1",
"globalClasses": [],
"globalElements": []
};
return JSON.stringify(bricksJSON, null, 2);
}
function generateJavaScriptCode() {
return `(function(window) {
const WhisperMotionAnimation = {
instances: [],
config: {
animationSpeed: ${whisperConfig.animationSpeed},
floatDistance: ${whisperConfig.floatDistance},
rotationAmount: ${whisperConfig.rotationAmount},
hoverZoom: ${whisperConfig.hoverZoom},
cardCount: ${whisperConfig.cardCount},
cardSize: ${whisperConfig.cardSize},
cardRadius: ${whisperConfig.cardRadius},
accentColor: "${whisperConfig.accentColor}",
shadowIntensity: ${whisperConfig.shadowIntensity},
motionPattern: "${whisperConfig.motionPattern}",
mobileSize: ${whisperConfig.mobileSize},
magneticForce: ${whisperConfig.magneticForce},
images: ${JSON.stringify([
'https://images.pexels.com/photos/1366957/pexels-photo-1366957.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/2559941/pexels-photo-2559941.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/2387873/pexels-photo-2387873.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/3075993/pexels-photo-3075993.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1287145/pexels-photo-1287145.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1323550/pexels-photo-1323550.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1402787/pexels-photo-1402787.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/2662116/pexels-photo-2662116.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1544966/pexels-photo-1544966.jpeg?auto=compress&cs=tinysrgb&w=800',
'https://images.pexels.com/photos/1438084/pexels-photo-1438084.jpeg?auto=compress&cs=tinysrgb&w=800'
])}
},
init: function(options = {}) {
const defaultOptions = {
selector: '[data-whisper-motion]'
};
const config = { ...defaultOptions, ...options };
const initInstances = () => {
document.querySelectorAll(config.selector).forEach(element => {
if (!element.hasAttribute('data-whisper-initialized')) {
this.createInstance(element, config);
element.setAttribute('data-whisper-initialized', 'true');
}
});
};
initInstances();
setTimeout(initInstances, 100);
window.addEventListener('load', initInstances);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
initInstances();
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
},
createInstance: function(element, config) {
if (window.getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
element.style.overflow = 'hidden';
const options = {
animationSpeed: parseFloat(element.getAttribute('data-motion-speed')) || this.config.animationSpeed,
floatDistance: parseInt(element.getAttribute('data-motion-float-distance')) || this.config.floatDistance,
rotationAmount: parseInt(element.getAttribute('data-motion-rotation')) || this.config.rotationAmount,
hoverZoom: parseFloat(element.getAttribute('data-motion-hover-zoom')) || this.config.hoverZoom,
cardCount: parseInt(element.getAttribute('data-motion-card-count')) || this.config.cardCount,
cardSize: parseInt(element.getAttribute('data-motion-card-size')) || this.config.cardSize,
cardRadius: parseInt(element.getAttribute('data-motion-card-radius')) || this.config.cardRadius,
accentColor: element.getAttribute('data-motion-accent-color') || this.config.accentColor,
shadowIntensity: parseFloat(element.getAttribute('data-motion-shadow-intensity')) || this.config.shadowIntensity,
motionPattern: element.getAttribute('data-motion-pattern') || this.config.motionPattern,
mobileSize: parseInt(element.getAttribute('data-motion-mobile-size')) || this.config.mobileSize,
magneticForce: parseInt(element.getAttribute('data-magnetic-force')) || this.config.magneticForce,
images: JSON.parse(element.getAttribute('data-motion-images') || 'null') || this.config.images
};
element.style.setProperty('--accent-color', options.accentColor);
element.style.setProperty('--card-radius', options.cardRadius + 'px');
element.style.setProperty('--shadow-intensity', options.shadowIntensity);
element.style.setProperty('--hover-zoom', options.hoverZoom);
element.style.setProperty('--card-size', options.cardSize + 'px');
element.style.setProperty('--mobile-size', options.mobileSize + 'px');
Array.from(element.children).forEach(child => {
if (!child.classList.contains('whisper-motion')) {
child.style.position = 'relative';
child.style.zIndex = '50';
child.style.pointerEvents = 'auto';
}
});
const existingMotion = element.querySelector('.whisper-motion');
if (existingMotion) {
existingMotion.remove();
}
const motion = document.createElement('div');
motion.className = 'whisper-motion';
motion.style.position = 'absolute';
motion.style.width = '100%';
motion.style.height = '100%';
motion.style.top = '0';
motion.style.left = '0';
motion.style.zIndex = '1';
motion.style.pointerEvents = 'none';
motion.style.backgroundColor = 'transparent';
let animationName;
switch(options.motionPattern) {
case 'circular':
animationName = 'circularAnimation';
break;
case 'zigzag':
animationName = 'zigzagAnimation';
break;
case 'pulse':
animationName = 'pulseAnimation';
break;
default:
animationName = 'floatAnimation';
}
const cardCount = options.cardCount;
for (let i = 0; i < cardCount; i++) {
const card = document.createElement('div');
card.className = 'whisper-card';
const containerWidth = element.offsetWidth || 500;
const containerHeight = element.offsetHeight || 400;
const maxX = containerWidth - options.cardSize;
const maxY = containerHeight - options.cardSize;
const randomX = Math.floor(Math.random() * (maxX > 0 ? maxX : containerWidth/2));
const randomY = Math.floor(Math.random() * (maxY > 0 ? maxY : containerHeight/2));
const floatX = (Math.random() * 2 - 1) * options.floatDistance;
const floatY = (Math.random() * 2 - 1) * options.floatDistance;
const floatRotation = (Math.random() * 2 - 1) * options.rotationAmount;
const hoverRotation = (Math.random() * 2 - 1) * options.rotationAmount * 2;
const delay = Math.random() * -options.animationSpeed;
card.style.left = randomX + 'px';
card.style.top = randomY + 'px';
card.style.animation = animationName + ' ' + options.animationSpeed + 's ease-in-out infinite';
card.style.animationDelay = delay + 's';
card.style.zIndex = 2;
card.style.backgroundColor = 'rgba(40, 40, 40, 0.7)';
card.style.backdropFilter = 'blur(5px)';
card.style.opacity = '0.9';
card.style.mixBlendMode = 'normal';
card.style.setProperty('--float-x', floatX + 'px');
card.style.setProperty('--float-y', floatY + 'px');
card.style.setProperty('--float-rotation', floatRotation + 'deg');
card.style.setProperty('--hover-rotation', hoverRotation + 'deg');
card.style.setProperty('--hover-zoom', options.hoverZoom);
card.dataset.originalLeft = randomX;
card.dataset.originalTop = randomY;
const img = document.createElement('img');
img.src = options.images[i % options.images.length];
img.alt = '';
img.loading = 'lazy';
img.decoding = 'async';
card.appendChild(img);
motion.appendChild(card);
}
element.appendChild(motion);
this.setupMagneticEffect(element, motion, options);
},
setupMagneticEffect: function(container, cardsContainer, options) {
if (container._handleMouseMove) {
container.removeEventListener('mousemove', container._handleMouseMove);
}
if (container._handleMouseLeave) {
container.removeEventListener('mouseleave', container._handleMouseLeave);
}
container._cardsContainer = cardsContainer;
container._magneticRadius = 200;
container._magneticForce = options.magneticForce;
container._accentColor = options.accentColor;
const cards = cardsContainer.querySelectorAll('.whisper-card');
cards.forEach(card => {
card.style.transform = 'translateZ(0)';
});
let ticking = false;
const handleMouseMove = (e) => {
if (!ticking) {
window.requestAnimationFrame(() => {
this.handleContainerMouseMove(e, container);
ticking = false;
});
ticking = true;
}
};
const handleMouseLeave = (e) => {
this.handleContainerMouseLeave(e, container);
};
container._handleMouseMove = handleMouseMove.bind(this);
container._handleMouseLeave = handleMouseLeave.bind(this);
container.addEventListener('mousemove', container._handleMouseMove, { passive: true });
container.addEventListener('mouseleave', container._handleMouseLeave);
},
handleContainerMouseMove: function(e, container) {
const cardsContainer = container._cardsContainer;
const rect = container.getBoundingClientRect();
const magneticRadius = container._magneticRadius;
const magneticForce = container._magneticForce;
const accentColor = container._accentColor;
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const cards = cardsContainer.querySelectorAll('.whisper-card');
cards.forEach(card => {
const cardRect = card.getBoundingClientRect();
const cardX = cardRect.left - rect.left + cardRect.width / 2;
const cardY = cardRect.top - rect.top + cardRect.height / 2;
const deltaX = mouseX - cardX;
const deltaY = mouseY - cardY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < magneticRadius) {
const normalizedDistance = distance / magneticRadius;
const t = 1 - normalizedDistance;
const easeOutCubic = t * t * (3 - 2 * t);
const force = easeOutCubic * magneticForce * 0.5;
const moveX = Math.min(Math.max((deltaX / distance) * force, -20), 20);
const moveY = Math.min(Math.max((deltaY / distance) * force, -20), 20);
const originalLeft = parseInt(card.dataset.originalLeft);
const originalTop = parseInt(card.dataset.originalTop);
card.style.left = (originalLeft + moveX) + 'px';
card.style.top = (originalTop + moveY) + 'px';
const opacityFactor = easeOutCubic;
const r = parseInt(accentColor.substr(1, 2), 16);
const g = parseInt(accentColor.substr(3, 2), 16);
const b = parseInt(accentColor.substr(5, 2), 16);
const borderColor = 'rgba(' + r + ', ' + g + ', ' + b + ', ' + (opacityFactor * 0.6) + ')';
card.style.borderColor = borderColor;
card.style.opacity = 0.9 + (opacityFactor * 0.1);
} else {
card.style.left = card.dataset.originalLeft + 'px';
card.style.top = card.dataset.originalTop + 'px';
card.style.borderColor = 'rgba(255, 255, 255, 0.1)';
card.style.opacity = 0.9;
}
});
},
handleContainerMouseLeave: function(e, container) {
const cardsContainer = container._cardsContainer;
const cards = cardsContainer.querySelectorAll('.whisper-card');
cards.forEach(card => {
card.style.left = card.dataset.originalLeft + 'px';
card.style.top = card.dataset.originalTop + 'px';
card.style.borderColor = 'rgba(255, 255, 255, 0.1)';
card.style.opacity = 0.9;
});
}
};
const initStyles = () => {
if (document.getElementById('whisper-motion-styles')) return;
const style = document.createElement('style');
style.id = 'whisper-motion-styles';
style.textContent = \`
[data-whisper-motion] {
position: relative !important;
overflow: hidden !important;
min-height: 200px;
}
[data-whisper-motion] > *:not(.whisper-motion) {
position: relative;
z-index: 50;
pointer-events: auto !important;
}
.whisper-motion {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
pointer-events: none;
background-color: transparent !important;
}
.whisper-card {
position: absolute;
background-color: rgba(40, 40, 40, 0.7);
backdrop-filter: blur(5px);
border-radius: var(--card-radius, 12px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 25px rgba(0, 0, 0, var(--shadow-intensity, 0.3));
overflow: hidden;
transition: transform 0.5s ease, border-color 0.3s ease, left 0.3s ease, top 0.3s ease, opacity 0.3s ease;
width: var(--card-size, 130px);
height: var(--card-size, 130px);
z-index: 2;
opacity: 0.9;
pointer-events: none;
mix-blend-mode: normal;
will-change: transform, left, top;
}
@media (max-width: 768px) {
.whisper-card {
width: var(--mobile-size, 80px);
height: var(--mobile-size, 80px);
}
}
.whisper-card img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.whisper-card:hover {
border-color: var(--accent-color, #ef6013);
transform: translateY(-5px) rotate(var(--hover-rotation, 0deg));
}
.whisper-card:hover img {
transform: scale(var(--hover-zoom, 1.05));
}
@keyframes floatAnimation {
0% {
transform: translateY(0) translateX(0) rotate(0deg);
}
50% {
transform: translateY(var(--float-y)) translateX(var(--float-x)) rotate(var(--float-rotation));
}
100% {
transform: translateY(0) translateX(0) rotate(0deg);
}
}
@keyframes circularAnimation {
0% {
transform: translateY(0) translateX(0) rotate(0deg);
}
25% {
transform: translateY(var(--float-y, 0px)) translateX(0) rotate(var(--float-rotation, 0deg));
}
50% {
transform: translateY(0) translateX(var(--float-x, 0px)) rotate(calc(var(--float-rotation, 0deg) * 2));
}
75% {
transform: translateY(calc(var(--float-y, 0px) * -1)) translateX(0) rotate(var(--float-rotation, 0deg));
}
100% {
transform: translateY(0) translateX(0) rotate(0deg);
}
}
@keyframes zigzagAnimation {
0% {
transform: translateY(0) translateX(0) rotate(0deg);
}
25% {
transform: translateY(var(--float-y, 0px)) translateX(var(--float-x, 0px)) rotate(var(--float-rotation, 0deg));
}
50% {
transform: translateY(0) translateX(calc(var(--float-x, 0px) * -1.5)) rotate(calc(var(--float-rotation, 0deg) * -1));
}
75% {
transform: translateY(calc(var(--float-y, 0px) * -1)) translateX(var(--float-x, 0px)) rotate(var(--float-rotation, 0deg));
}
100% {
transform: translateY(0) translateX(0) rotate(0deg);
}
}
@keyframes pulseAnimation {
0% {
transform: translateY(0) translateX(0) rotate(0deg) scale(1);
}
50% {
transform: translateY(calc(var(--float-y, 0px) * 0.5)) translateX(calc(var(--float-x, 0px) * 0.5)) rotate(var(--float-rotation, 0deg)) scale(1.05);
}
100% {
transform: translateY(0) translateX(0) rotate(0deg) scale(1);
}
}
\`;
document.head.appendChild(style);
};
initStyles();
window.WhisperMotionAnimation = WhisperMotionAnimation;
WhisperMotionAnimation.init();
})(window);`;
}
function copyJsToClipboard() {
const jsCode = generateJavaScriptCode();
navigator.clipboard.writeText(jsCode)
.then(() => {
showNotification('JavaScript code copied to clipboard!');
})
.catch(err => {
try {
const textArea = document.createElement('textarea');
textArea.value = jsCode;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('JavaScript code copied to clipboard!');
} catch (fallbackErr) {
showNotification('Failed to copy to clipboard. Please try again.', 'error');
}
});
}
function copyFullSectionToClipboard() {
const sectionJSON = generateFullSectionJSON();
navigator.clipboard.writeText(sectionJSON)
.then(() => {
showNotification('Full section JSON copied to clipboard!');
})
.catch(err => {
try {
const textArea = document.createElement('textarea');
textArea.value = sectionJSON;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('Full section JSON copied to clipboard!');
} catch (fallbackErr) {
showNotification('Failed to copy to clipboard. Please try again.', 'error');
}
});
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text)
.then(() => {
showNotification('Copied to clipboard!');
})
.catch(err => {
showNotification('Failed to copy to clipboard', 'error');
});
}
function hexToHsl(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
function hslToHex(hsl) {
const match = hsl.match(/hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/);
if (!match) return null;
let h = parseInt(match[1]) / 360;
let s = parseInt(match[2]) / 100;
let l = parseInt(match[3]) / 100;
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
const toHex = (c) => {
const hex = Math.round(c * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function isValidHex(hex) {
return /^#[0-9A-F]{6}$/i.test(hex);
}
function isValidHsl(hsl) {
return /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/i.test(hsl);
}
function formatHex(value) {
let hex = value.replace(/[^0-9A-Fa-f#]/g, '');
if (!hex.startsWith('#')) {
hex = '#' + hex;
}
if (hex.length > 7) {
hex = hex.substring(0, 7);
}
return hex.toUpperCase();
}
function formatHsl(value) {
const cleanValue = value.replace(/[^\d,\s]/g, '');
const numbers = cleanValue.match(/\d+/g);
if (!numbers || numbers.length < 3) {
const partialMatch = value.match(/(\d+)/g);
if (partialMatch && partialMatch.length >= 1) {
const h = Math.min(360, Math.max(0, parseInt(partialMatch[0]) || 0));
const s = Math.min(100, Math.max(0, parseInt(partialMatch[1]) || 50));
const l = Math.min(100, Math.max(0, parseInt(partialMatch[2]) || 50));
return `hsl(${h}, ${s}%, ${l}%)`;
}
return value;
}
let h = Math.min(360, Math.max(0, parseInt(numbers[0])));
let s = Math.min(100, Math.max(0, parseInt(numbers[1])));
let l = Math.min(100, Math.max(0, parseInt(numbers[2])));
return `hsl(${h}, ${s}%, ${l}%)`;
}
function updateColorInputs() {
const colorInput = document.getElementById('accent-color');
const hexInput = document.getElementById('accent-color-hex');
const hslInput = document.getElementById('accent-color-hsl');
if (colorInput && hexInput && hslInput) {
colorInput.value = whisperConfig.accentColor;
hexInput.value = whisperConfig.accentColor;
hslInput.value = `hsl(${hexToHsl(whisperConfig.accentColor).h}, ${hexToHsl(whisperConfig.accentColor).s}%, ${hexToHsl(whisperConfig.accentColor).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', whisperConfig.accentColor);
}
}
}
function updateImageInputs() {
const container = document.getElementById('image-urls-container');
container.innerHTML = '';
whisperConfig.images.forEach((url, index) => {
const inputContainer = document.createElement('div');
inputContainer.className = 'image-input-container';
const input = document.createElement('input');
input.type = 'text';
input.className = 'image-url-input';
input.placeholder = `Image URL ${index + 1}`;
input.value = url;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn';
deleteBtn.style.cssText = 'background-color: #dc3545; width: auto; padding: 0.5rem; margin-bottom: 0;';
deleteBtn.innerHTML = '🗑️';
deleteBtn.onclick = function() {
container.removeChild(inputContainer);
updateImagesArray();
updateWhisperPreview();
};
inputContainer.appendChild(input);
inputContainer.appendChild(deleteBtn);
container.appendChild(inputContainer);
});
}
function updateImagesArray() {
const images = [];
document.querySelectorAll('#image-urls-container .image-url-input').forEach(input => {
if (input.value.trim()) {
images.push(input.value.trim());
}
});
whisperConfig.images = images;
}
function generateRandomWhisper() {
whisperConfig.animationSpeed = Math.random() * 8 + 2;
whisperConfig.floatDistance = Math.floor(Math.random() * 25) + 5;
whisperConfig.rotationAmount = Math.floor(Math.random() * 15);
whisperConfig.hoverZoom = Math.random() * 0.2 + 1;
whisperConfig.cardCount = Math.floor(Math.random() * 7) + 3;
whisperConfig.cardSize = Math.floor(Math.random() * 120) + 80;
whisperConfig.cardRadius = Math.floor(Math.random() * 30);
whisperConfig.accentColor = generateRandomColor();
whisperConfig.shadowIntensity = Math.random() * 0.8;
whisperConfig.motionPattern = ['float', 'circular', 'zigzag', 'pulse'][Math.floor(Math.random() * 4)];
whisperConfig.mobileSize = Math.floor(Math.random() * 80) + 40;
whisperConfig.magneticForce = Math.floor(Math.random() * 40) + 10;
document.getElementById('animation-speed').value = whisperConfig.animationSpeed;
document.getElementById('float-distance').value = whisperConfig.floatDistance;
document.getElementById('rotation-amount').value = whisperConfig.rotationAmount;
document.getElementById('hover-zoom').value = whisperConfig.hoverZoom;
document.getElementById('card-count').value = whisperConfig.cardCount;
document.getElementById('card-size').value = whisperConfig.cardSize;
document.getElementById('card-radius').value = whisperConfig.cardRadius;
document.getElementById('accent-color').value = whisperConfig.accentColor;
document.getElementById('shadow-intensity').value = whisperConfig.shadowIntensity;
document.getElementById('motion-pattern').value = whisperConfig.motionPattern;
document.getElementById('mobile-size').value = whisperConfig.mobileSize;
document.getElementById('magnetic-force').value = whisperConfig.magneticForce;
document.getElementById('animation-speed-value').textContent = whisperConfig.animationSpeed.toFixed(1);
document.getElementById('float-distance-value').textContent = whisperConfig.floatDistance;
document.getElementById('rotation-amount-value').textContent = whisperConfig.rotationAmount;
document.getElementById('hover-zoom-value').textContent = whisperConfig.hoverZoom.toFixed(2);
document.getElementById('card-count-value').textContent = whisperConfig.cardCount;
document.getElementById('card-size-value').textContent = whisperConfig.cardSize;
document.getElementById('card-radius-value').textContent = whisperConfig.cardRadius;
document.getElementById('shadow-intensity-value').textContent = whisperConfig.shadowIntensity.toFixed(2);
document.getElementById('mobile-size-value').textContent = whisperConfig.mobileSize;
document.getElementById('magnetic-force-value').textContent = whisperConfig.magneticForce;
updateColorInputs();
updateWhisperPreview();
showNotification('Random whisper motion generated!');
}
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 'animation-speed':
whisperConfig.animationSpeed = defaultValue;
break;
case 'float-distance':
whisperConfig.floatDistance = defaultValue;
break;
case 'rotation-amount':
whisperConfig.rotationAmount = defaultValue;
break;
case 'hover-zoom':
whisperConfig.hoverZoom = defaultValue;
break;
case 'card-count':
whisperConfig.cardCount = defaultValue;
break;
case 'card-size':
whisperConfig.cardSize = defaultValue;
break;
case 'card-radius':
whisperConfig.cardRadius = defaultValue;
break;
case 'shadow-intensity':
whisperConfig.shadowIntensity = defaultValue;
break;
case 'mobile-size':
whisperConfig.mobileSize = defaultValue;
break;
case 'magnetic-force':
whisperConfig.magneticForce = defaultValue;
break;
}
updateWhisperPreview();
showNotification(`${parameterId.replace(/-/g, ' ')} reset to default`);
}
};
function initializeUI() {
initWhisperMotion();
const instructionsToggle = document.getElementById('instructions-toggle');
const instructionsContent = document.getElementById('instructions-content');
const instructionsCard = document.getElementById('instructions-card');
const toggleIcon = instructionsToggle.querySelector('.toggle-icon');
instructionsToggle.addEventListener('click', () => {
const isVisible = instructionsContent.classList.contains('show');
if (isVisible) {
instructionsContent.classList.remove('show');
instructionsCard.classList.remove('expanded');
toggleIcon.classList.remove('expanded');
} else {
instructionsContent.classList.add('show');
instructionsCard.classList.add('expanded');
toggleIcon.classList.add('expanded');
}
});
document.getElementById('quick-attribute').addEventListener('click', () => {
copyToClipboard('data-whisper-motion');
});
document.getElementById('download-config').addEventListener('click', () => {
copyJsToClipboard();
});
document.getElementById('copy-full-section').addEventListener('click', () => {
copyFullSectionToClipboard();
});
document.getElementById('reset-colors').addEventListener('click', () => {
whisperConfig.accentColor = defaultConfig.accentColor;
whisperConfig.shadowIntensity = defaultConfig.shadowIntensity;
document.getElementById('accent-color').value = defaultConfig.accentColor;
document.getElementById('shadow-intensity').value = defaultConfig.shadowIntensity;
document.getElementById('shadow-intensity-value').textContent = defaultConfig.shadowIntensity;
updateColorInputs();
updateWhisperPreview();
showNotification('Colors reset to default');
});
document.getElementById('reset-animation').addEventListener('click', () => {
whisperConfig.animationSpeed = defaultConfig.animationSpeed;
whisperConfig.floatDistance = defaultConfig.floatDistance;
whisperConfig.rotationAmount = defaultConfig.rotationAmount;
whisperConfig.hoverZoom = defaultConfig.hoverZoom;
whisperConfig.motionPattern = defaultConfig.motionPattern;
document.getElementById('animation-speed').value = defaultConfig.animationSpeed;
document.getElementById('float-distance').value = defaultConfig.floatDistance;
document.getElementById('rotation-amount').value = defaultConfig.rotationAmount;
document.getElementById('hover-zoom').value = defaultConfig.hoverZoom;
document.getElementById('motion-pattern').value = defaultConfig.motionPattern;
document.getElementById('animation-speed-value').textContent = defaultConfig.animationSpeed;
document.getElementById('float-distance-value').textContent = defaultConfig.floatDistance;
document.getElementById('rotation-amount-value').textContent = defaultConfig.rotationAmount;
document.getElementById('hover-zoom-value').textContent = defaultConfig.hoverZoom;
updateWhisperPreview();
showNotification('Animation settings reset');
});
document.getElementById('reset-appearance').addEventListener('click', () => {
whisperConfig.cardCount = defaultConfig.cardCount;
whisperConfig.cardSize = defaultConfig.cardSize;
whisperConfig.cardRadius = defaultConfig.cardRadius;
whisperConfig.mobileSize = defaultConfig.mobileSize;
document.getElementById('card-count').value = defaultConfig.cardCount;
document.getElementById('card-size').value = defaultConfig.cardSize;
document.getElementById('card-radius').value = defaultConfig.cardRadius;
document.getElementById('mobile-size').value = defaultConfig.mobileSize;
document.getElementById('card-count-value').textContent = defaultConfig.cardCount;
document.getElementById('card-size-value').textContent = defaultConfig.cardSize;
document.getElementById('card-radius-value').textContent = defaultConfig.cardRadius;
document.getElementById('mobile-size-value').textContent = defaultConfig.mobileSize;
updateWhisperPreview();
showNotification('Appearance settings reset');
});
document.getElementById('reset-advanced').addEventListener('click', () => {
whisperConfig.magneticForce = defaultConfig.magneticForce;
document.getElementById('magnetic-force').value = defaultConfig.magneticForce;
document.getElementById('magnetic-force-value').textContent = defaultConfig.magneticForce;
updateWhisperPreview();
showNotification('Advanced settings reset');
});
const colorInput = document.getElementById('accent-color');
const hexInput = document.getElementById('accent-color-hex');
const hslInput = document.getElementById('accent-color-hsl');
hslInput.value = `hsl(${hexToHsl(colorInput.value).h}, ${hexToHsl(colorInput.value).s}%, ${hexToHsl(colorInput.value).l}%)`;
colorInput.addEventListener('input', () => {
const color = colorInput.value;
hexInput.value = color;
hslInput.value = `hsl(${hexToHsl(color).h}, ${hexToHsl(color).s}%, ${hexToHsl(color).l}%)`;
hexInput.classList.remove('invalid');
hslInput.classList.remove('invalid');
whisperConfig.accentColor = color;
const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
colorPickerContainer.style.setProperty('--selected-color', color);
updateWhisperPreview();
});
hexInput.addEventListener('input', (e) => {
let hex = e.target.value;
hex = formatHex(hex);
e.target.value = hex;
if (isValidHex(hex)) {
colorInput.value = hex;
hslInput.value = `hsl(${hexToHsl(hex).h}, ${hexToHsl(hex).s}%, ${hexToHsl(hex).l}%)`;
whisperConfig.accentColor = hex;
e.target.classList.remove('invalid');
hslInput.classList.remove('invalid');
const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
colorPickerContainer.style.setProperty('--selected-color', hex);
updateWhisperPreview();
} 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;
whisperConfig.accentColor = hex;
e.target.classList.remove('invalid');
hexInput.classList.remove('invalid');
const colorPickerContainer = colorInput.closest('.color-row').querySelector('.color-picker-container');
colorPickerContainer.style.setProperty('--selected-color', hex);
updateWhisperPreview();
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;
whisperConfig.accentColor = hex;
e.target.classList.remove('invalid');
hexInput.classList.remove('invalid');
updateWhisperPreview();
return;
}
}
}
if (!isValidHsl(e.target.value)) {
e.target.value = `hsl(${hexToHsl(colorInput.value).h}, ${hexToHsl(colorInput.value).s}%, ${hexToHsl(colorInput.value).l}%)`;
e.target.classList.remove('invalid');
}
});
const rangeInputs = document.querySelectorAll('input[type="range"]');
rangeInputs.forEach(input => {
const valueElement = document.getElementById(`${input.id}-value`);
if (valueElement) {
valueElement.textContent = input.value;
}
input.addEventListener('input', () => {
if (valueElement) {
valueElement.textContent = input.value;
}
switch (input.id) {
case 'animation-speed':
whisperConfig.animationSpeed = parseFloat(input.value);
break;
case 'float-distance':
whisperConfig.floatDistance = parseInt(input.value);
break;
case 'rotation-amount':
whisperConfig.rotationAmount = parseInt(input.value);
break;
case 'hover-zoom':
whisperConfig.hoverZoom = parseFloat(input.value);
break;
case 'card-count':
whisperConfig.cardCount = parseInt(input.value);
break;
case 'card-size':
whisperConfig.cardSize = parseInt(input.value);
break;
case 'card-radius':
whisperConfig.cardRadius = parseInt(input.value);
break;
case 'shadow-intensity':
whisperConfig.shadowIntensity = parseFloat(input.value);
break;
case 'mobile-size':
whisperConfig.mobileSize = parseInt(input.value);
break;
case 'magnetic-force':
whisperConfig.magneticForce = parseInt(input.value);
break;
}
updateWhisperPreview();
});
});
document.getElementById('motion-pattern').addEventListener('change', function() {
whisperConfig.motionPattern = this.value;
updateWhisperPreview();
});
document.getElementById('add-image').addEventListener('click', function() {
const container = document.getElementById('image-urls-container');
const inputContainer = document.createElement('div');
inputContainer.className = 'image-input-container';
const newInput = document.createElement('input');
newInput.type = 'text';
newInput.className = 'image-url-input';
newInput.placeholder = `Image URL ${container.children.length + 1}`;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn';
deleteBtn.style.cssText = 'background-color: #dc3545; width: auto; padding: 0.5rem; margin-bottom: 0;';
deleteBtn.innerHTML = '🗑️';
deleteBtn.onclick = function() {
container.removeChild(inputContainer);
updateImagesArray();
updateWhisperPreview();
};
inputContainer.appendChild(newInput);
inputContainer.appendChild(deleteBtn);
container.appendChild(inputContainer);
});
document.getElementById('update-preview').addEventListener('click', function() {
updateImagesArray();
updateWhisperPreview();
});
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;
}
}
});
updateColorInputs();
updateImageInputs();
setTimeout(() => {
showNotification('BricksFusion Whisper Motion Configurator loaded!');
const preview = document.getElementById('whisper-preview');
if (preview && preview._updateConfig) {
preview._updateConfig(whisperConfig);
}
}, 500);
function saveConfiguration() {
try {
localStorage.setItem('bricksfusion-whisper-config', JSON.stringify(whisperConfig));
} catch (e) {
}
}
function loadConfiguration() {
try {
const saved = localStorage.getItem('bricksfusion-whisper-config');
if (saved) {
const savedConfig = JSON.parse(saved);
Object.assign(whisperConfig, savedConfig);
document.getElementById('animation-speed').value = savedConfig.animationSpeed;
document.getElementById('float-distance').value = savedConfig.floatDistance;
document.getElementById('rotation-amount').value = savedConfig.rotationAmount;
document.getElementById('hover-zoom').value = savedConfig.hoverZoom;
document.getElementById('card-count').value = savedConfig.cardCount;
document.getElementById('card-size').value = savedConfig.cardSize;
document.getElementById('card-radius').value = savedConfig.cardRadius;
document.getElementById('accent-color').value = savedConfig.accentColor;
document.getElementById('shadow-intensity').value = savedConfig.shadowIntensity;
document.getElementById('motion-pattern').value = savedConfig.motionPattern;
document.getElementById('mobile-size').value = savedConfig.mobileSize;
document.getElementById('magnetic-force').value = savedConfig.magneticForce;
document.getElementById('animation-speed-value').textContent = savedConfig.animationSpeed;
document.getElementById('float-distance-value').textContent = savedConfig.floatDistance;
document.getElementById('rotation-amount-value').textContent = savedConfig.rotationAmount;
document.getElementById('hover-zoom-value').textContent = savedConfig.hoverZoom;
document.getElementById('card-count-value').textContent = savedConfig.cardCount;
document.getElementById('card-size-value').textContent = savedConfig.cardSize;
document.getElementById('card-radius-value').textContent = savedConfig.cardRadius;
document.getElementById('shadow-intensity-value').textContent = savedConfig.shadowIntensity;
document.getElementById('mobile-size-value').textContent = savedConfig.mobileSize;
document.getElementById('magnetic-force-value').textContent = savedConfig.magneticForce;
updateColorInputs();
updateImageInputs();
updateWhisperPreview();
}
} catch (e) {
}
}
const originalUpdateWhisperPreview = updateWhisperPreview;
updateWhisperPreview = function() {
originalUpdateWhisperPreview();
saveConfiguration();
};
loadConfiguration();
}
initializeUI();
});
</script>
</body>
</html>
Whisper Motion
Creates floating image cards with magnetic mouse interaction and elegant animations. Cards gently drift with customizable motion patterns including float, circular, zigzag, and pulse. Features smooth magnetic effect that attracts cards toward cursor with distance-based force calculations. Includes hover effects with zoom and rotation, backdrop blur for depth, and automatic mobile responsiveness. Uses CSS animations with requestAnimationFrame for cursor tracking. Perfect for creative portfolios, feature showcases, or adding dynamic visual interest to hero sections.
Whisper Motion
Move your mouse to interact with the floating cards.
Cards
Number of floating cards displayed. More cards create fuller, busier composition.
Default: 5
Width and height of each card on desktop. Cards are square and automatically scale on mobile.
Default: 130px
Card size on mobile devices. Automatically applied on screens under 768px wide.
Default: 80px
Corner roundness of cards. 0 creates sharp corners, higher creates more rounded edges.
Default: 12px
Array of image URLs displayed in cards. Cards cycle through images if count exceeds array length.
Default: Sample images
Motion
Animation style for cards. Options: float (smooth drift), circular (orbital motion), zigzag (diagonal path), pulse (scale breathing).
Default: float
Duration of one complete animation cycle. Lower creates faster movement, higher creates slower drift.
Default: 5 seconds
Maximum distance cards travel during animation. Higher creates wider movement range.
Default: 15px
Maximum rotation during animation. Creates gentle tilting effect as cards float.
Default: 5 degrees
Interaction
Strength of mouse attraction effect. Higher makes cards move more dramatically toward cursor.
Default: 30
Image scale on card hover. 1.0 means no zoom, higher creates magnification effect.
Default: 1.05
Appearance
Highlight color for card borders during magnetic interaction and hover states.
Default: #ef6013 (orange)
Depth of card drop shadows. Higher creates more pronounced elevation effect.
Default: 0.3
Performance
This element uses CSS animations for card floating with four motion patterns. Features requestAnimationFrame for smooth magnetic mouse tracking with distance calculations and easing functions. Uses backdrop-filter blur for depth effect which may impact performance on some devices. Each card includes lazy-loaded images with async decoding. Event listeners use passive mode for scroll performance. Magnetic effect calculates position for each card on mousemove with throttling. Medium weight - suitable for hero sections and portfolio showcases. Best used with 5-8 cards on desktop. Automatically adjusts card size and reduces effects on mobile. Works well on modern devices but consider reducing card count on lower-end hardware.