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>Floatvision 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);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.floating-preview {
width: 100%;
height: 100%;
position: relative;
border-radius: var(--card-radius);
overflow: hidden;
}
.floating-image {
width: 100%;
height: 100%;
object-fit: contain;
transform-origin: center center;
will-change: transform;
transition: transform 0.3s ease-out;
border-radius: var(--card-radius);
}
.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;
}
.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);
}
.toggle-switch-wrapper {
display: flex;
align-items: center;
margin: 0.5rem 0;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--card-bg);
border: 1px solid var(--border);
transition: .4s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 3px;
background-color: var(--text-secondary);
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--accent);
border-color: var(--accent);
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
background-color: white;
}
.toggle-label {
margin-left: 10px;
font-size: var(--text-xs);
color: var(--text-secondary);
}
input:checked ~ .toggle-label {
color: var(--accent);
}
.radio-group {
display: flex;
flex-direction: column;
gap: 0.8rem;
margin-bottom: 1rem;
}
.radio-option {
display: flex;
align-items: center;
cursor: pointer;
}
.radio-option input[type="radio"] {
margin-right: 0.8rem;
cursor: pointer;
accent-color: var(--accent);
}
.radio-option label {
font-size: var(--text-xs);
color: var(--text-secondary);
cursor: pointer;
}
.radio-option input[type="radio"]:checked + label {
color: var(--text-primary);
font-weight: 500;
}
.style-description {
font-size: var(--text-xs);
color: var(--text-secondary);
margin-top: 0.5rem;
padding-left: 1.8rem;
margin-bottom: 0.8rem;
line-height: 1.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);
}
.notification {
position: fixed;
bottom: calc(var(--action-bar-height) + 1rem);
left: 50%;
background-color: var(--success);
color: white;
padding: 0.75rem 1rem;
border-radius: var(--input-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1001;
transform: translate(-50%, 200px);
opacity: 0;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease;
font-size: var(--text-xs);
font-weight: 500;
max-width: 320px;
word-wrap: break-word;
line-height: 1.4;
text-align: center;
}
.notification.show {
transform: translate(-50%, 0);
opacity: 1;
}
@media (max-width: 1200px) {
.content {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.preview-section {
position: static;
}
.controls-section {
max-width: 100%;
}
}
@media (max-width: 768px) {
.action-bar {
flex-direction: column;
height: auto;
min-height: var(--action-bar-height);
padding: 0.75rem;
}
.breadcrumb {
order: 1;
width: 100%;
}
.action-buttons {
order: 2;
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
body {
padding-bottom: calc(var(--action-bar-height) + 20px);
}
.notification {
bottom: calc(var(--action-bar-height) + 2rem);
max-width: 280px;
transform: translate(-50%, 250px);
}
.notification.show {
transform: translate(-50%, 0);
opacity: 1;
}
.preview-container {
height: 300px;
}
.data-attribute-display {
font-size: 10px;
padding: 0.4rem 0.6rem;
}
.action-btn {
font-size: 11px;
padding: 0.5rem 0.8rem;
}
.page-title {
font-size: 2rem;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
button:focus-visible,
input:focus-visible,
.action-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--background);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
.loading {
opacity: 0.6;
pointer-events: none;
position: relative;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="action-bar">
<nav class="breadcrumb">
<a href="https://bricksfusion.com" class="breadcrumb-item">Home</a>
<span class="breadcrumb-separator">›</span>
<a href="https://bricksfusion.com/interaction/" class="breadcrumb-item">Interactions</a>
<span class="breadcrumb-separator">›</span>
<span class="breadcrumb-item active">Floatvision</span>
</nav>
<div class="action-buttons">
<div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
data-float-container
</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">Floatvision</h1>
<p class="page-subtitle">Interactive floating image effects for Bricks Builder</p>
</div>
<div class="instructions-toggle">
<div class="instructions-card" id="instructions-card">
<div class="instructions-header" id="instructions-toggle">
<div class="instructions-title">
How to Use & Code Information
</div>
<span class="toggle-icon">▼</span>
</div>
<div class="instructions-content" id="instructions-content">
<div class="instructions-grid">
<div class="how-to-use">
<ol>
<li>Customize your floating image effect using the controls below</li>
<li>Click <strong>Copy JS</strong> to copy the JavaScript code to clipboard</li>
<li>In Bricks Builder, add a <strong>Code</strong> element</li>
<li>Paste the JavaScript code into the Code element</li>
<li>To add the effect to any container: go to <strong>Section → Style → Attributes</strong>, add <code>data-float-container</code> as attribute name (leave value empty)</li>
<li>Add the attribute <code>data-float-image</code> to the image inside the container</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<div class="content">
<section class="preview-section">
<div class="preview-container" id="floatvision-preview" data-float-container="true">
<div class="floating-preview" data-float-container>
<img src="https://test.bricksfusion.com/wp-content/uploads/2025/05/66303513e76c9555d5583af4_Hero-Character-Photoroom.png" class="floating-image" data-float-image>
</div>
<div class="preview-content">Interactive Floatvision Preview</div>
</div>
</section>
<section class="controls-section">
<div class="card">
<div class="card-heading">
Motion Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-motion" title="Reset Motion Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Motion Speed
<span class="help-tooltip" title="Speed of the floating animation">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="speed-value">2</span></span>
<button class="reset-btn" onclick="resetParameter('speed', 2)">↺</button>
</div>
</div>
<input type="range" id="speed" min="0.5" max="5" step="0.1" value="2">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Motion Intensity
<span class="help-tooltip" title="Intensity of the autonomous movement">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="amplitude-value">2</span>%</span>
<button class="reset-btn" onclick="resetParameter('amplitude', 2)">↺</button>
</div>
</div>
<input type="range" id="amplitude" min="0.5" max="5" step="0.1" value="2">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Smoothness
<span class="help-tooltip" title="Smoothness of the animation transitions">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="smoothness-value">0.04</span></span>
<button class="reset-btn" onclick="resetParameter('smoothness', 0.04)">↺</button>
</div>
</div>
<input type="range" id="smoothness" min="0.01" max="0.1" step="0.01" value="0.04">
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Interactive Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-interactive" title="Reset Interactive Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">Interactive Effects</span>
</div>
<div class="toggle-switch-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="mouse-interactions" checked>
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Enable mouse interactions</span>
</div>
<div class="toggle-switch-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="touch-interactions" checked>
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Enable touch interactions</span>
</div>
<div class="toggle-switch-wrapper">
<label class="toggle-switch">
<input type="checkbox" id="auto-float" checked>
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Enable autonomous floating</span>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Mouse Response
<span class="help-tooltip" title="Strength of mouse/touch response">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="intensity-value">3</span>%</span>
<button class="reset-btn" onclick="resetParameter('intensity', 3)">↺</button>
</div>
</div>
<input type="range" id="intensity" min="1" max="10" step="0.5" value="3">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Movement Direction</span>
</div>
<div class="radio-group" id="direction-group">
<div class="radio-option">
<input type="radio" id="direction-all" name="direction" value="all" checked>
<label for="direction-all">All directions</label>
</div>
<div class="radio-option">
<input type="radio" id="direction-horizontal" name="direction" value="horizontal">
<label for="direction-horizontal">Horizontal only</label>
</div>
<div class="radio-option">
<input type="radio" id="direction-vertical" name="direction" value="vertical">
<label for="direction-vertical">Vertical only</label>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Advanced Options
<div class="card-actions">
<button class="card-action-btn" id="reset-advanced" title="Reset Advanced Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Movement Boundary
<span class="help-tooltip" title="Maximum distance the image can move">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="boundary-value">8</span>%</span>
<button class="reset-btn" onclick="resetParameter('boundary', 8)">↺</button>
</div>
</div>
<input type="range" id="boundary" min="3" max="20" step="1" value="8">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Path Complexity
<span class="help-tooltip" title="Complexity of the autonomous movement path">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="complexity-value">0.6</span></span>
<button class="reset-btn" onclick="resetParameter('complexity', 0.6)">↺</button>
</div>
</div>
<input type="range" id="complexity" min="0.2" max="1" step="0.1" value="0.6">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Initial Position Offset</span>
</div>
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
<div style="flex: 1;">
<span class="label-text" style="font-size: 12px; color: var(--text-secondary);">X-Offset: <span id="offset-x-value">0</span>%</span>
<input type="range" id="offset-x" min="-10" max="10" step="1" value="0">
</div>
<div style="flex: 1;">
<span class="label-text" style="font-size: 12px; color: var(--text-secondary);">Y-Offset: <span id="offset-y-value">0</span>%</span>
<input type="range" id="offset-y" min="-10" max="10" step="1" value="0">
</div>
</div>
<p class="style-description">Initial position offset of the image</p>
</div>
</div>
</div>
</section>
</div>
</div>
<div class="notification" id="notification"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let floatvisionConfig = {
speed: 2,
amplitude: 2,
smoothness: 0.04,
mouseInteractions: true,
touchInteractions: true,
autoFloat: true,
intensity: 3,
complexity: 0.6,
direction: 'all',
boundary: 8,
offsetX: 0,
offsetY: 0
};
const defaultConfig = { ...floatvisionConfig };
function initFloatingImage() {
const sections = document.querySelectorAll('[data-float-container]:not([data-float-initialized="true"])');
sections.forEach((section) => {
const speed = section.hasAttribute('data-float-speed')
? parseFloat(section.getAttribute('data-float-speed'))
: floatvisionConfig.speed;
const amplitude = section.hasAttribute('data-float-amplitude')
? parseFloat(section.getAttribute('data-float-amplitude'))
: floatvisionConfig.amplitude;
const smoothness = section.hasAttribute('data-float-smoothness')
? parseFloat(section.getAttribute('data-float-smoothness'))
: floatvisionConfig.smoothness;
const mouseInteractions = section.hasAttribute('data-float-mouse')
? section.getAttribute('data-float-mouse') === 'true'
: floatvisionConfig.mouseInteractions;
const touchInteractions = section.hasAttribute('data-float-touch')
? section.getAttribute('data-float-touch') === 'true'
: floatvisionConfig.touchInteractions;
const autoFloat = section.hasAttribute('data-float-auto')
? section.getAttribute('data-float-auto') === 'true'
: floatvisionConfig.autoFloat;
const intensity = section.hasAttribute('data-float-intensity')
? parseFloat(section.getAttribute('data-float-intensity'))
: floatvisionConfig.intensity;
const complexity = section.hasAttribute('data-float-complexity')
? parseFloat(section.getAttribute('data-float-complexity'))
: floatvisionConfig.complexity;
const direction = section.hasAttribute('data-float-direction')
? section.getAttribute('data-float-direction')
: floatvisionConfig.direction;
const boundary = section.hasAttribute('data-float-boundary')
? parseInt(section.getAttribute('data-float-boundary'))
: floatvisionConfig.boundary;
const offsetX = section.hasAttribute('data-float-offset-x')
? parseInt(section.getAttribute('data-float-offset-x'))
: floatvisionConfig.offsetX;
const offsetY = section.hasAttribute('data-float-offset-y')
? parseInt(section.getAttribute('data-float-offset-y'))
: floatvisionConfig.offsetY;
const options = {
speed,
amplitude,
smoothness,
mouseInteractions,
touchInteractions,
autoFloat,
intensity,
complexity,
direction,
boundary,
offsetX,
offsetY
};
setupFloatingImage(section, options);
section.dataset.floatInitialized = 'true';
});
}
function setupFloatingImage(element, options) {
const image = element.querySelector('[data-float-image]');
if (!image) return;
const containerStyle = getComputedStyle(element);
if (containerStyle.position === 'static') {
element.style.position = 'relative';
}
element.style.overflow = 'visible';
if (containerStyle.height === 'auto' || containerStyle.height === '0px') {
element.style.height = '100%';
}
image.style.willChange = 'transform';
image.style.transformOrigin = 'center center';
const offsetX = options.offsetX / 100;
const offsetY = options.offsetY / 100;
element.floatingImageState = {
image,
targetX: offsetX,
targetY: offsetY,
currentX: offsetX,
currentY: offsetY,
velocityX: 0,
velocityY: 0,
lastMouseX: 0,
lastMouseY: 0,
lastTime: performance.now(),
virtualCursor: {
x: 0,
y: 0,
targetX: generateRandomPosition(),
targetY: generateRandomPosition(),
lastUpdateTime: performance.now(),
timeToNextUpdate: getRandomTime(3000, 6000)
},
isUserInteracting: false,
config: options
};
if (options.mouseInteractions) {
setupMouseEvents(element);
}
if (options.touchInteractions) {
setupTouchEvents(element);
}
startFloatingAnimation(element);
}
function generateRandomPosition() {
return (Math.random() * 2 - 1);
}
function getRandomTime(min, max) {
return min + Math.random() * (max - min);
}
function setupMouseEvents(container) {
let isTracking = false;
container.addEventListener('mouseenter', () => {
isTracking = true;
container.floatingImageState.isUserInteracting = true;
});
container.addEventListener('mousemove', (e) => {
if (!isTracking) return;
const direction = container.floatingImageState.config.direction;
const rect = container.getBoundingClientRect();
const xPercent = (e.clientX - rect.left) / rect.width;
const yPercent = (e.clientY - rect.top) / rect.height;
const xOffset = (xPercent - 0.5) * 2;
const yOffset = (yPercent - 0.5) * 2;
const now = performance.now();
const dt = Math.max(1, now - container.floatingImageState.lastTime);
const state = container.floatingImageState;
if (state.lastTime) {
state.velocityX = (xOffset - state.lastMouseX) / dt * 10;
state.velocityY = (yOffset - state.lastMouseY) / dt * 10;
}
state.lastTime = now;
state.lastMouseX = xOffset;
state.lastMouseY = yOffset;
const intensity = state.config.intensity / 100;
const easedIntensity = easeOutQuint(intensity);
const offsetX = state.config.offsetX / 100;
const offsetY = state.config.offsetY / 100;
if (direction === 'all' || direction === 'horizontal') {
state.targetX = offsetX - xOffset * easedIntensity;
} else {
state.targetX = offsetX;
}
if (direction === 'all' || direction === 'vertical') {
state.targetY = offsetY - yOffset * easedIntensity;
} else {
state.targetY = offsetY;
}
const boundary = state.config.boundary;
if (!isNaN(boundary) && boundary > 0) {
const maxBoundary = boundary / 100;
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, state.targetX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, state.targetY));
}
});
container.addEventListener('mouseleave', () => {
const state = container.floatingImageState;
state.isUserInteracting = false;
const velocity = Math.sqrt(state.velocityX * state.velocityX + state.velocityY * state.velocityY);
const offsetX = state.config.offsetX / 100;
const offsetY = state.config.offsetY / 100;
if (velocity > 0.05) {
const dampedVelocityX = state.velocityX * 0.5;
const dampedVelocityY = state.velocityY * 0.5;
const boundary = state.config.boundary;
if (!isNaN(boundary) && boundary > 0) {
const maxBoundary = boundary / 100;
const inertiaX = dampedVelocityX * 0.1;
const inertiaY = dampedVelocityY * 0.1;
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, offsetX + inertiaX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, offsetY + inertiaY));
} else {
state.targetX = offsetX + dampedVelocityX * 0.1;
state.targetY = offsetY + dampedVelocityY * 0.1;
}
}
isTracking = false;
});
}
function setupTouchEvents(container) {
let isTracking = false;
container.addEventListener('touchstart', () => {
isTracking = true;
container.floatingImageState.isUserInteracting = true;
});
container.addEventListener('touchmove', (e) => {
if (e.touches.length !== 1 || !isTracking) return;
const direction = container.floatingImageState.config.direction;
const touch = e.touches[0];
const rect = container.getBoundingClientRect();
const xPercent = (touch.clientX - rect.left) / rect.width;
const yPercent = (touch.clientY - rect.top) / rect.height;
const xOffset = (xPercent - 0.5) * 2;
const yOffset = (yPercent - 0.5) * 2;
const intensity = container.floatingImageState.config.intensity / 120;
const state = container.floatingImageState;
const offsetX = state.config.offsetX / 100;
const offsetY = state.config.offsetY / 100;
if (direction === 'all' || direction === 'horizontal') {
state.targetX = offsetX - xOffset * intensity;
} else {
state.targetX = offsetX;
}
if (direction === 'all' || direction === 'vertical') {
state.targetY = offsetY - yOffset * intensity;
} else {
state.targetY = offsetY;
}
const boundary = state.config.boundary;
if (!isNaN(boundary) && boundary > 0) {
const maxBoundary = boundary / 100;
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, state.targetX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, state.targetY));
}
const now = performance.now();
if (state.lastTime) {
const dt = Math.max(1, now - state.lastTime);
state.velocityX = (xOffset - state.lastMouseX) / dt * 5;
state.velocityY = (yOffset - state.lastMouseY) / dt * 5;
}
state.lastTime = now;
state.lastMouseX = xOffset;
state.lastMouseY = yOffset;
}, { passive: true });
container.addEventListener('touchend', () => {
if (!isTracking) return;
const state = container.floatingImageState;
state.isUserInteracting = false;
const offsetX = state.config.offsetX / 100;
const offsetY = state.config.offsetY / 100;
const velocity = Math.sqrt(state.velocityX * state.velocityX + state.velocityY * state.velocityY);
if (velocity > 0.05) {
const boundary = state.config.boundary;
if (!isNaN(boundary) && boundary > 0) {
const maxBoundary = boundary / 100;
const inertiaX = state.velocityX * 0.05;
const inertiaY = state.velocityY * 0.05;
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, offsetX + inertiaX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, offsetY + inertiaY));
} else {
state.targetX = offsetX + state.velocityX * 0.05;
state.targetY = offsetY + state.velocityY * 0.05;
}
}
isTracking = false;
});
}
function startFloatingAnimation(element) {
let lastTime = 0;
function animate(currentTime) {
element.animationFrame = requestAnimationFrame(animate);
const deltaTime = currentTime - lastTime;
if (deltaTime < 16) return;
lastTime = currentTime - (deltaTime % 16);
updateContainer(element, currentTime);
}
element.animationFrame = requestAnimationFrame(animate);
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
if (!element.animationFrame) {
element.animationFrame = requestAnimationFrame(animate);
}
} else {
if (element.animationFrame) {
cancelAnimationFrame(element.animationFrame);
element.animationFrame = null;
}
}
});
}, { threshold: 0 });
observer.observe(element);
element._cleanupFloating = () => {
if (element.animationFrame) {
cancelAnimationFrame(element.animationFrame);
}
observer.disconnect();
element.dataset.floatInitialized = 'false';
};
}
function updateVirtualCursor(state, timestamp, config) {
const virtualCursor = state.virtualCursor;
const elapsedTime = timestamp - virtualCursor.lastUpdateTime;
const speedModifier = config.speed;
const pathComplexity = config.complexity;
const t = Math.min(1, elapsedTime / virtualCursor.timeToNextUpdate);
const ease = easeInOutCubic(t);
virtualCursor.x = virtualCursor.x + (virtualCursor.targetX - virtualCursor.x) * ease * 0.05 * speedModifier;
virtualCursor.y = virtualCursor.y + (virtualCursor.targetY - virtualCursor.y) * ease * 0.05 * speedModifier;
if (elapsedTime >= virtualCursor.timeToNextUpdate ||
(Math.abs(virtualCursor.x - virtualCursor.targetX) < 0.01 &&
Math.abs(virtualCursor.y - virtualCursor.targetY) < 0.01)) {
virtualCursor.targetX = generateRandomPosition();
virtualCursor.targetY = generateRandomPosition();
if (Math.random() < pathComplexity) {
const smallMoveChance = Math.random();
if (smallMoveChance < 0.3) {
virtualCursor.targetX = virtualCursor.x + (Math.random() * 0.4 - 0.2);
virtualCursor.targetY = virtualCursor.y + (Math.random() * 0.4 - 0.2);
} else if (smallMoveChance < 0.6) {
const horizontalBias = Math.random() > 0.5;
if (horizontalBias) {
virtualCursor.targetX = generateRandomPosition();
virtualCursor.targetY = virtualCursor.y + (Math.random() * 0.4 - 0.2);
} else {
virtualCursor.targetX = virtualCursor.x + (Math.random() * 0.4 - 0.2);
virtualCursor.targetY = generateRandomPosition();
}
}
}
virtualCursor.targetX = Math.max(-1, Math.min(1, virtualCursor.targetX));
virtualCursor.targetY = Math.max(-1, Math.min(1, virtualCursor.targetY));
virtualCursor.lastUpdateTime = timestamp;
virtualCursor.timeToNextUpdate = getRandomTime(2000, 5000) / speedModifier;
}
return {
x: virtualCursor.x,
y: virtualCursor.y
};
}
function updateContainer(element, timestamp) {
const state = element.floatingImageState;
if (!state || !state.image) return;
const config = state.config;
const direction = config.direction;
const autoFloat = config.autoFloat;
const offsetX = config.offsetX / 100;
const offsetY = config.offsetY / 100;
const boundary = config.boundary;
const maxBoundary = boundary / 100;
const smoothness = config.smoothness;
if (!state.isUserInteracting && autoFloat) {
const cursorPos = updateVirtualCursor(state, timestamp, config);
const amplitude = config.amplitude / 100;
if (direction === 'all' || direction === 'horizontal') {
state.targetX = offsetX + cursorPos.x * amplitude;
} else {
state.targetX = offsetX;
}
if (direction === 'all' || direction === 'vertical') {
state.targetY = offsetY + cursorPos.y * amplitude;
} else {
state.targetY = offsetY;
}
if (!isNaN(boundary) && boundary > 0) {
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, state.targetX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, state.targetY));
}
}
const distanceX = state.targetX - state.currentX;
const distanceY = state.targetY - state.currentY;
const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
const dynamicSmoothness = smoothness * (distance > 0.02 ? 1 : 0.5);
state.currentX += distanceX * dynamicSmoothness;
state.currentY += distanceY * dynamicSmoothness;
state.velocityX *= 0.95;
state.velocityY *= 0.95;
state.image.style.transform = `translate3d(${state.currentX * 100}%, ${state.currentY * 100}%, 0) scale(1.05)`;
}
function easeOutQuint(x) {
return 1 - Math.pow(1 - x, 5);
}
function easeInOutCubic(x) {
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
}
function updateConfig(newConfig) {
Object.assign(floatvisionConfig, newConfig);
const sections = document.querySelectorAll('[data-float-container]');
sections.forEach(section => {
if (section.floatingImageState) {
Object.assign(section.floatingImageState.config, newConfig);
}
});
}
function updateFloatvisionPreview() {
const preview = document.querySelector('[data-float-container]');
if (preview && preview._cleanupFloating) {
preview._cleanupFloating();
}
if (preview) {
preview.removeAttribute('data-float-initialized');
}
initFloatingImage();
saveConfiguration();
}
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 imageId = generateUniqueId();
const codeId = generateUniqueId();
const containerAttributeId = generateUniqueId();
const imageAttributeId = 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"
}
},
{
"id": containerId,
"name": "container",
"parent": sectionId,
"children": [imageId],
"settings": {
"_alignSelf": "center",
"_justifyContent": "center",
"_alignItems": "center",
"_direction": "row",
"_attributes": [
{
"id": containerAttributeId,
"name": "data-float-container"
}
]
},
"label": "Float vision Container"
},
{
"id": imageId,
"name": "image",
"parent": containerId,
"children": [],
"settings": {
"image": {
"id": 12671,
"filename": "66303513e76c9555d5583af4_Hero-Character-Photoroom.png",
"size": "large",
"full": "https://test.bricksfusion.com/wp-content/uploads/2025/05/66303513e76c9555d5583af4_Hero-Character-Photoroom.png",
"url": "https://test.bricksfusion.com/wp-content/uploads/2025/05/66303513e76c9555d5583af4_Hero-Character-Photoroom-1024x1024.png"
},
"_width": "500",
"_attributes": [
{
"id": imageAttributeId,
"name": "data-float-image"
}
]
},
"themeStyles": {}
},
{
"id": codeId,
"name": "code",
"parent": sectionId,
"children": [],
"settings": {
"javascriptCode": jsCode,
"executeCode": true,
"_display": "none"
},
"label": "Float vision JS"
}
],
"source": "bricksCopiedElements",
"sourceUrl": "https://test.bricksfusion.com",
"version": "2.0.1",
"globalClasses": [],
"globalElements": []
};
return JSON.stringify(bricksJSON, null, 2);
}
function generateJavaScriptCode() {
return `(function() {
const defaultConfig = {
mouseInteractions: ${floatvisionConfig.mouseInteractions},
touchInteractions: ${floatvisionConfig.touchInteractions},
autoFloat: ${floatvisionConfig.autoFloat},
speed: ${floatvisionConfig.speed},
amplitude: ${floatvisionConfig.amplitude},
smoothness: ${floatvisionConfig.smoothness},
intensity: ${floatvisionConfig.intensity},
complexity: ${floatvisionConfig.complexity},
direction: "${floatvisionConfig.direction}",
boundary: ${floatvisionConfig.boundary},
offsetX: ${floatvisionConfig.offsetX},
offsetY: ${floatvisionConfig.offsetY}
};
const FloatingImageAnimation = {
containers: [],
animating: false,
raf: null,
init: function() {
const containers = document.querySelectorAll('[data-float-container]');
containers.forEach(container => this.initContainer(container));
if (containers.length > 0 && !this.animating) {
this.startAnimationLoop();
}
this.setupMutationObserver();
},
initContainer: function(container) {
if (container.hasAttribute('data-float-initialized')) return;
const image = container.querySelector('[data-float-image]');
if (!image) return;
const containerStyle = getComputedStyle(container);
if (containerStyle.position === 'static') {
container.style.position = 'relative';
}
container.style.overflow = 'visible';
if (containerStyle.height === 'auto' || containerStyle.height === '0px') {
container.style.height = '100%';
}
image.style.willChange = 'transform';
image.style.transformOrigin = 'center center';
if (!container.hasAttribute('data-float-mouse')) {
container.setAttribute('data-float-mouse', defaultConfig.mouseInteractions ? 'true' : 'false');
}
if (!container.hasAttribute('data-float-touch')) {
container.setAttribute('data-float-touch', defaultConfig.touchInteractions ? 'true' : 'false');
}
if (!container.hasAttribute('data-float-auto')) {
container.setAttribute('data-float-auto', defaultConfig.autoFloat ? 'true' : 'false');
}
if (!container.hasAttribute('data-float-direction')) {
container.setAttribute('data-float-direction', defaultConfig.direction);
}
if (!container.hasAttribute('data-float-speed')) {
container.setAttribute('data-float-speed', defaultConfig.speed);
}
if (!container.hasAttribute('data-float-amplitude')) {
container.setAttribute('data-float-amplitude', defaultConfig.amplitude);
}
if (!container.hasAttribute('data-float-smoothness')) {
container.setAttribute('data-float-smoothness', defaultConfig.smoothness);
}
if (!container.hasAttribute('data-float-intensity')) {
container.setAttribute('data-float-intensity', defaultConfig.intensity);
}
if (!container.hasAttribute('data-float-complexity')) {
container.setAttribute('data-float-complexity', defaultConfig.complexity);
}
if (!container.hasAttribute('data-float-boundary')) {
container.setAttribute('data-float-boundary', defaultConfig.boundary);
}
if (!container.hasAttribute('data-float-offset-x')) {
container.setAttribute('data-float-offset-x', defaultConfig.offsetX);
}
if (!container.hasAttribute('data-float-offset-y')) {
container.setAttribute('data-float-offset-y', defaultConfig.offsetY);
}
const offsetX = parseFloat(container.getAttribute('data-float-offset-x') || defaultConfig.offsetX) / 100;
const offsetY = parseFloat(container.getAttribute('data-float-offset-y') || defaultConfig.offsetY) / 100;
container.floatingImageState = {
image,
targetX: offsetX,
targetY: offsetY,
currentX: offsetX,
currentY: offsetY,
velocityX: 0,
velocityY: 0,
lastMouseX: 0,
lastMouseY: 0,
lastTime: performance.now(),
virtualCursor: {
x: 0,
y: 0,
targetX: this.generateRandomPosition(),
targetY: this.generateRandomPosition(),
lastUpdateTime: performance.now(),
timeToNextUpdate: this.getRandomTime(3000, 6000)
},
isUserInteracting: false
};
const mouseEnabled = container.hasAttribute('data-float-mouse')
? container.getAttribute('data-float-mouse') === 'true'
: defaultConfig.mouseInteractions;
if (mouseEnabled) {
this.setupMouseEvents(container);
}
const touchEnabled = container.hasAttribute('data-float-touch')
? container.getAttribute('data-float-touch') === 'true'
: defaultConfig.touchInteractions;
if (touchEnabled) {
this.setupTouchEvents(container);
}
container.setAttribute('data-float-initialized', 'true');
this.containers.push(container);
},
generateRandomPosition: function() {
return (Math.random() * 2 - 1);
},
getRandomTime: function(min, max) {
return min + Math.random() * (max - min);
},
setupMouseEvents: function(container) {
let isTracking = false;
container.addEventListener('mouseenter', () => {
isTracking = true;
container.floatingImageState.isUserInteracting = true;
});
container.addEventListener('mousemove', (e) => {
if (container.hasAttribute('data-float-paused') || !isTracking) return;
const direction = container.getAttribute('data-float-direction') || 'all';
const rect = container.getBoundingClientRect();
const xPercent = (e.clientX - rect.left) / rect.width;
const yPercent = (e.clientY - rect.top) / rect.height;
const xOffset = (xPercent - 0.5) * 2;
const yOffset = (yPercent - 0.5) * 2;
const now = performance.now();
const dt = Math.max(1, now - container.floatingImageState.lastTime);
const state = container.floatingImageState;
if (state.lastTime) {
state.velocityX = (xOffset - state.lastMouseX) / dt * 10;
state.velocityY = (yOffset - state.lastMouseY) / dt * 10;
}
state.lastTime = now;
state.lastMouseX = xOffset;
state.lastMouseY = yOffset;
const intensity = parseFloat(container.getAttribute('data-float-intensity') || defaultConfig.intensity) / 100;
const easedIntensity = this.easeOutQuint(intensity);
const offsetX = parseFloat(container.getAttribute('data-float-offset-x') || defaultConfig.offsetX) / 100;
const offsetY = parseFloat(container.getAttribute('data-float-offset-y') || defaultConfig.offsetY) / 100;
if (direction === 'all' || direction === 'horizontal') {
state.targetX = offsetX - xOffset * easedIntensity;
} else {
state.targetX = offsetX;
}
if (direction === 'all' || direction === 'vertical') {
state.targetY = offsetY - yOffset * easedIntensity;
} else {
state.targetY = offsetY;
}
const boundary = parseFloat(container.getAttribute('data-float-boundary') || defaultConfig.boundary);
if (!isNaN(boundary) && boundary > 0) {
const maxBoundary = boundary / 100;
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, state.targetX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, state.targetY));
}
});
container.addEventListener('mouseleave', () => {
if (container.hasAttribute('data-float-paused')) return;
const state = container.floatingImageState;
state.isUserInteracting = false;
const velocity = Math.sqrt(state.velocityX * state.velocityX + state.velocityY * state.velocityY);
const offsetX = parseFloat(container.getAttribute('data-float-offset-x') || defaultConfig.offsetX) / 100;
const offsetY = parseFloat(container.getAttribute('data-float-offset-y') || defaultConfig.offsetY) / 100;
if (velocity > 0.05) {
const dampedVelocityX = state.velocityX * 0.5;
const dampedVelocityY = state.velocityY * 0.5;
const boundary = parseFloat(container.getAttribute('data-float-boundary') || defaultConfig.boundary);
if (!isNaN(boundary) && boundary > 0) {
const maxBoundary = boundary / 100;
const inertiaX = dampedVelocityX * 0.1;
const inertiaY = dampedVelocityY * 0.1;
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, offsetX + inertiaX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, offsetY + inertiaY));
} else {
state.targetX = offsetX + dampedVelocityX * 0.1;
state.targetY = offsetY + dampedVelocityY * 0.1;
}
}
isTracking = false;
});
},
setupTouchEvents: function(container) {
let isTracking = false;
container.addEventListener('touchstart', () => {
isTracking = true;
container.floatingImageState.isUserInteracting = true;
});
container.addEventListener('touchmove', (e) => {
if (container.hasAttribute('data-float-paused') || e.touches.length !== 1 || !isTracking) return;
const direction = container.getAttribute('data-float-direction') || 'all';
const touch = e.touches[0];
const rect = container.getBoundingClientRect();
const xPercent = (touch.clientX - rect.left) / rect.width;
const yPercent = (touch.clientY - rect.top) / rect.height;
const xOffset = (xPercent - 0.5) * 2;
const yOffset = (yPercent - 0.5) * 2;
const intensity = parseFloat(container.getAttribute('data-float-touch-intensity') ||
parseFloat(container.getAttribute('data-float-intensity') || defaultConfig.intensity)) / 120;
const state = container.floatingImageState;
const offsetX = parseFloat(container.getAttribute('data-float-offset-x') || defaultConfig.offsetX) / 100;
const offsetY = parseFloat(container.getAttribute('data-float-offset-y') || defaultConfig.offsetY) / 100;
if (direction === 'all' || direction === 'horizontal') {
state.targetX = offsetX - xOffset * intensity;
} else {
state.targetX = offsetX;
}
if (direction === 'all' || direction === 'vertical') {
state.targetY = offsetY - yOffset * intensity;
} else {
state.targetY = offsetY;
}
const boundary = parseFloat(container.getAttribute('data-float-boundary') || defaultConfig.boundary);
if (!isNaN(boundary) && boundary > 0) {
const maxBoundary = boundary / 100;
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, state.targetX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, state.targetY));
}
const now = performance.now();
if (state.lastTime) {
const dt = Math.max(1, now - state.lastTime);
state.velocityX = (xOffset - state.lastMouseX) / dt * 5;
state.velocityY = (yOffset - state.lastMouseY) / dt * 5;
}
state.lastTime = now;
state.lastMouseX = xOffset;
state.lastMouseY = yOffset;
}, { passive: true });
container.addEventListener('touchend', () => {
if (container.hasAttribute('data-float-paused') || !isTracking) return;
const state = container.floatingImageState;
state.isUserInteracting = false;
const offsetX = parseFloat(container.getAttribute('data-float-offset-x') || defaultConfig.offsetX) / 100;
const offsetY = parseFloat(container.getAttribute('data-float-offset-y') || defaultConfig.offsetY) / 100;
const velocity = Math.sqrt(state.velocityX * state.velocityX + state.velocityY * state.velocityY);
if (velocity > 0.05) {
const boundary = parseFloat(container.getAttribute('data-float-boundary') || defaultConfig.boundary);
if (!isNaN(boundary) && boundary > 0) {
const maxBoundary = boundary / 100;
const inertiaX = state.velocityX * 0.05;
const inertiaY = state.velocityY * 0.05;
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, offsetX + inertiaX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, offsetY + inertiaY));
} else {
state.targetX = offsetX + state.velocityX * 0.05;
state.targetY = offsetY + state.velocityY * 0.05;
}
}
isTracking = false;
});
},
startAnimationLoop: function() {
this.animating = true;
const update = (timestamp) => {
this.updateAllContainers(timestamp);
this.raf = requestAnimationFrame(update);
};
this.raf = requestAnimationFrame(update);
},
updateAllContainers: function(timestamp) {
this.containers.forEach(container => {
if (container.hasAttribute('data-float-paused')) return;
this.updateContainer(container, timestamp);
});
},
updateVirtualCursor: function(state, timestamp, container) {
const virtualCursor = state.virtualCursor;
const elapsedTime = timestamp - virtualCursor.lastUpdateTime;
const speedModifier = parseFloat(container.getAttribute('data-float-speed') || defaultConfig.speed);
const pathComplexity = parseFloat(container.getAttribute('data-float-complexity') || defaultConfig.complexity);
const t = Math.min(1, elapsedTime / virtualCursor.timeToNextUpdate);
const ease = this.easeInOutCubic(t);
virtualCursor.x = virtualCursor.x + (virtualCursor.targetX - virtualCursor.x) * ease * 0.05 * speedModifier;
virtualCursor.y = virtualCursor.y + (virtualCursor.targetY - virtualCursor.y) * ease * 0.05 * speedModifier;
if (elapsedTime >= virtualCursor.timeToNextUpdate ||
(Math.abs(virtualCursor.x - virtualCursor.targetX) < 0.01 &&
Math.abs(virtualCursor.y - virtualCursor.targetY) < 0.01)) {
virtualCursor.targetX = this.generateRandomPosition();
virtualCursor.targetY = this.generateRandomPosition();
if (Math.random() < pathComplexity) {
const smallMoveChance = Math.random();
if (smallMoveChance < 0.3) {
virtualCursor.targetX = virtualCursor.x + (Math.random() * 0.4 - 0.2);
virtualCursor.targetY = virtualCursor.y + (Math.random() * 0.4 - 0.2);
} else if (smallMoveChance < 0.6) {
const horizontalBias = Math.random() > 0.5;
if (horizontalBias) {
virtualCursor.targetX = this.generateRandomPosition();
virtualCursor.targetY = virtualCursor.y + (Math.random() * 0.4 - 0.2);
} else {
virtualCursor.targetX = virtualCursor.x + (Math.random() * 0.4 - 0.2);
virtualCursor.targetY = this.generateRandomPosition();
}
}
}
virtualCursor.targetX = Math.max(-1, Math.min(1, virtualCursor.targetX));
virtualCursor.targetY = Math.max(-1, Math.min(1, virtualCursor.targetY));
virtualCursor.lastUpdateTime = timestamp;
virtualCursor.timeToNextUpdate = this.getRandomTime(2000, 5000) / speedModifier;
}
return {
x: virtualCursor.x,
y: virtualCursor.y
};
},
updateContainer: function(container, timestamp) {
const state = container.floatingImageState;
if (!state || !state.image) return;
const direction = container.getAttribute('data-float-direction') || 'all';
const autoFloat = container.hasAttribute('data-float-auto')
? container.getAttribute('data-float-auto') === 'true'
: defaultConfig.autoFloat;
const offsetX = parseFloat(container.getAttribute('data-float-offset-x') || defaultConfig.offsetX) / 100;
const offsetY = parseFloat(container.getAttribute('data-float-offset-y') || defaultConfig.offsetY) / 100;
const boundary = parseFloat(container.getAttribute('data-float-boundary') || defaultConfig.boundary);
const maxBoundary = boundary / 100;
const smoothness = parseFloat(container.getAttribute('data-float-smoothness') || defaultConfig.smoothness);
if (!state.isUserInteracting && autoFloat) {
const cursorPos = this.updateVirtualCursor(state, timestamp, container);
const amplitude = parseFloat(container.getAttribute('data-float-amplitude') || defaultConfig.amplitude) / 100;
if (direction === 'all' || direction === 'horizontal') {
state.targetX = offsetX + cursorPos.x * amplitude;
} else {
state.targetX = offsetX;
}
if (direction === 'all' || direction === 'vertical') {
state.targetY = offsetY + cursorPos.y * amplitude;
} else {
state.targetY = offsetY;
}
if (!isNaN(boundary) && boundary > 0) {
state.targetX = Math.max(offsetX - maxBoundary, Math.min(offsetX + maxBoundary, state.targetX));
state.targetY = Math.max(offsetY - maxBoundary, Math.min(offsetY + maxBoundary, state.targetY));
}
}
const distanceX = state.targetX - state.currentX;
const distanceY = state.targetY - state.currentY;
const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
const dynamicSmoothness = smoothness * (distance > 0.02 ? 1 : 0.5);
state.currentX += distanceX * dynamicSmoothness;
state.currentY += distanceY * dynamicSmoothness;
state.velocityX *= 0.95;
state.velocityY *= 0.95;
state.image.style.transform = \`translate3d(\${state.currentX * 100}%, \${state.currentY * 100}%, 0) scale(1.05)\`;
},
easeOutQuint: function(x) {
return 1 - Math.pow(1 - x, 5);
},
easeInOutCubic: function(x) {
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
},
setupMutationObserver: function() {
const observer = new MutationObserver((mutations) => {
let needsInit = false;
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.hasAttribute && node.hasAttribute('data-float-container')) {
needsInit = true;
} else if (node.querySelectorAll) {
const containers = node.querySelectorAll('[data-float-container]');
if (containers.length > 0) {
needsInit = true;
}
}
}
});
}
});
if (needsInit) {
this.init();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
};
window.initFloatingImage = function(container) {
if (container) {
FloatingImageAnimation.initContainer(container);
if (!FloatingImageAnimation.animating) {
FloatingImageAnimation.startAnimationLoop();
}
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
FloatingImageAnimation.init();
});
} else {
FloatingImageAnimation.init();
}
})();`;
}
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 => {
showNotification('Function not implemented yet', 'warning');
});
}
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 'speed':
floatvisionConfig.speed = defaultValue;
break;
case 'amplitude':
floatvisionConfig.amplitude = defaultValue;
break;
case 'smoothness':
floatvisionConfig.smoothness = defaultValue;
break;
case 'intensity':
floatvisionConfig.intensity = defaultValue;
break;
case 'boundary':
floatvisionConfig.boundary = defaultValue;
break;
case 'complexity':
floatvisionConfig.complexity = defaultValue;
break;
}
updateFloatvisionPreview();
showNotification(`${parameterId.replace(/-/g, ' ')} reset to default`);
}
};
function saveConfiguration() {
try {
localStorage.setItem('bricksfusion-floatvision-config', JSON.stringify(floatvisionConfig));
} catch (e) {
}
}
function loadConfiguration() {
try {
const saved = localStorage.getItem('bricksfusion-floatvision-config');
if (saved) {
const savedConfig = JSON.parse(saved);
Object.assign(floatvisionConfig, savedConfig);
document.getElementById('speed').value = savedConfig.speed;
document.getElementById('amplitude').value = savedConfig.amplitude;
document.getElementById('smoothness').value = savedConfig.smoothness;
document.getElementById('mouse-interactions').checked = savedConfig.mouseInteractions;
document.getElementById('touch-interactions').checked = savedConfig.touchInteractions;
document.getElementById('auto-float').checked = savedConfig.autoFloat;
document.getElementById('intensity').value = savedConfig.intensity;
document.getElementById('boundary').value = savedConfig.boundary;
document.getElementById('complexity').value = savedConfig.complexity;
document.getElementById('offset-x').value = savedConfig.offsetX;
document.getElementById('offset-y').value = savedConfig.offsetY;
document.getElementById('speed-value').textContent = savedConfig.speed;
document.getElementById('amplitude-value').textContent = savedConfig.amplitude;
document.getElementById('smoothness-value').textContent = savedConfig.smoothness;
document.getElementById('intensity-value').textContent = savedConfig.intensity;
document.getElementById('boundary-value').textContent = savedConfig.boundary;
document.getElementById('complexity-value').textContent = savedConfig.complexity;
document.getElementById('offset-x-value').textContent = savedConfig.offsetX;
document.getElementById('offset-y-value').textContent = savedConfig.offsetY;
document.querySelector(`input[name="direction"][value="${savedConfig.direction}"]`).checked = true;
updateFloatvisionPreview();
}
} catch (e) {
}
}
function initializeUI() {
initFloatingImage();
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-float-container');
});
document.getElementById('download-config').addEventListener('click', () => {
copyJsToClipboard();
});
document.getElementById('copy-full-section').addEventListener('click', () => {
copyFullSectionToClipboard();
});
document.getElementById('reset-motion').addEventListener('click', () => {
floatvisionConfig.speed = defaultConfig.speed;
floatvisionConfig.amplitude = defaultConfig.amplitude;
floatvisionConfig.smoothness = defaultConfig.smoothness;
document.getElementById('speed').value = defaultConfig.speed;
document.getElementById('amplitude').value = defaultConfig.amplitude;
document.getElementById('smoothness').value = defaultConfig.smoothness;
document.getElementById('speed-value').textContent = defaultConfig.speed;
document.getElementById('amplitude-value').textContent = defaultConfig.amplitude;
document.getElementById('smoothness-value').textContent = defaultConfig.smoothness;
updateFloatvisionPreview();
showNotification('Motion settings reset');
});
document.getElementById('reset-interactive').addEventListener('click', () => {
floatvisionConfig.mouseInteractions = defaultConfig.mouseInteractions;
floatvisionConfig.touchInteractions = defaultConfig.touchInteractions;
floatvisionConfig.autoFloat = defaultConfig.autoFloat;
floatvisionConfig.intensity = defaultConfig.intensity;
floatvisionConfig.direction = defaultConfig.direction;
document.getElementById('mouse-interactions').checked = defaultConfig.mouseInteractions;
document.getElementById('touch-interactions').checked = defaultConfig.touchInteractions;
document.getElementById('auto-float').checked = defaultConfig.autoFloat;
document.getElementById('intensity').value = defaultConfig.intensity;
document.getElementById('intensity-value').textContent = defaultConfig.intensity;
document.querySelector(`input[name="direction"][value="${defaultConfig.direction}"]`).checked = true;
updateFloatvisionPreview();
showNotification('Interactive settings reset');
});
document.getElementById('reset-advanced').addEventListener('click', () => {
floatvisionConfig.boundary = defaultConfig.boundary;
floatvisionConfig.complexity = defaultConfig.complexity;
floatvisionConfig.offsetX = defaultConfig.offsetX;
floatvisionConfig.offsetY = defaultConfig.offsetY;
document.getElementById('boundary').value = defaultConfig.boundary;
document.getElementById('complexity').value = defaultConfig.complexity;
document.getElementById('offset-x').value = defaultConfig.offsetX;
document.getElementById('offset-y').value = defaultConfig.offsetY;
document.getElementById('boundary-value').textContent = defaultConfig.boundary;
document.getElementById('complexity-value').textContent = defaultConfig.complexity;
document.getElementById('offset-x-value').textContent = defaultConfig.offsetX;
document.getElementById('offset-y-value').textContent = defaultConfig.offsetY;
updateFloatvisionPreview();
showNotification('Advanced settings reset');
});
const rangeInputs = document.querySelectorAll('input[type="range"]');
rangeInputs.forEach(input => {
const valueElement = document.getElementById(`${input.id}-value`);
if (valueElement) {
valueElement.textContent = input.value;
}
input.addEventListener('input', () => {
if (valueElement) {
valueElement.textContent = input.value;
}
switch (input.id) {
case 'speed':
floatvisionConfig.speed = parseFloat(input.value);
break;
case 'amplitude':
floatvisionConfig.amplitude = parseFloat(input.value);
break;
case 'smoothness':
floatvisionConfig.smoothness = parseFloat(input.value);
break;
case 'intensity':
floatvisionConfig.intensity = parseFloat(input.value);
break;
case 'boundary':
floatvisionConfig.boundary = parseInt(input.value);
break;
case 'complexity':
floatvisionConfig.complexity = parseFloat(input.value);
break;
case 'offset-x':
floatvisionConfig.offsetX = parseInt(input.value);
break;
case 'offset-y':
floatvisionConfig.offsetY = parseInt(input.value);
break;
}
updateFloatvisionPreview();
});
});
const checkboxes = ['mouse-interactions', 'touch-interactions', 'auto-float'];
checkboxes.forEach(id => {
document.getElementById(id).addEventListener('change', function() {
switch (id) {
case 'mouse-interactions':
floatvisionConfig.mouseInteractions = this.checked;
break;
case 'touch-interactions':
floatvisionConfig.touchInteractions = this.checked;
break;
case 'auto-float':
floatvisionConfig.autoFloat = this.checked;
break;
}
updateFloatvisionPreview();
});
});
document.querySelectorAll('input[name="direction"]').forEach(radio => {
radio.addEventListener('change', function() {
if (this.checked) {
floatvisionConfig.direction = this.value;
updateFloatvisionPreview();
}
});
});
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;
}
}
});
loadConfiguration();
setTimeout(() => {
showNotification('BricksFusion Floatvision Configurator loaded!');
}, 500);
}
initializeUI();
});
</script>
</body>
</html>
Floatvision
Creates smooth floating images that respond to mouse and touch. Images drift automatically with organic motion and react to cursor position with parallax effect. Perfect for hero images, product showcases, or adding depth to any design.
Movement
Speed of automatic floating motion. Lower is slow and calm, higher is fast and energetic.
Default: 2
Range of automatic movement. Higher values make image travel further from center.
Default: 2
How smooth transitions are. Lower is more fluid, higher is more responsive and snappy.
Default: 0.04
Interaction
Enable mouse parallax effect. Image follows cursor with smooth tracking and momentum.
Default: On
Enable touch parallax on mobile. Works like mouse but optimized for finger tracking.
Default: On
Strength of mouse/touch response. Higher values create more dramatic parallax movement.
Default: 3
Auto Float
Enable automatic floating motion. Image drifts organically when no user interaction.
Default: On
Complexity of automatic movement path. Higher creates more interesting, varied patterns.
Default: 0.6
Direction
Constraint movement direction. All moves freely, horizontal locks Y axis, vertical locks X axis.
Default: All
Boundary
Maximum distance image can move from center. Prevents excessive displacement.
Default: 8
Advanced
Initial horizontal offset position. Shifts the center point of floating motion.
Default: 0
Initial vertical offset position. Shifts the center point of floating motion.
Default: 0
Performance
This element uses requestAnimationFrame throttled to 60fps with translate3d transforms for GPU acceleration. Virtual cursor system creates organic paths with easing functions. Uses IntersectionObserver to pause animation when off-screen. Velocity tracking adds momentum to mouse/touch interactions. Scale transform applied for subtle depth effect.