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>Grid Distortion Configurator - BricksFusion</title>
<style>
:root {
--background: #000;
--card-bg: #1e1e1e;
--card-bg-hover: #252525;
--text-primary: #f2f2f7;
--text-secondary: #8e8e93;
--accent: #ef6013;
--accent-hover: #c64c0c;
--border: #2c2c2e;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
--track: #2c2c2e;
--thumb: #ef6013;
--card-radius: 16px;
--input-radius: 8px;
--button-radius: 12px;
--transition: all 0.25s ease;
--font: 'Inter', BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Arial, sans-serif;
--action-bar-height: 70px;
--success: #28a745;
--warning: #ffc107;
--danger: #dc3545;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font);
background-color: var(--background);
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding-bottom: var(--action-bar-height);
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--action-bar-height);
background: linear-gradient(145deg, #1a1a1a, #0f0f0f);
border-top: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
padding: 0 1.5rem;
gap: 1rem;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.breadcrumb-item {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 500;
text-decoration: none;
transition: var(--transition);
padding: 0.5rem 0.75rem;
border-radius: 6px;
}
.breadcrumb-item:hover {
color: var(--text-primary);
background-color: rgba(255, 255, 255, 0.05);
}
.breadcrumb-item.active {
color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.breadcrumb-separator {
color: var(--text-secondary);
font-size: var(--text-xs);
opacity: 0.5;
}
.action-buttons {
display: flex;
align-items: center;
gap: 0.75rem;
}
.action-btn {
padding: 0.6rem 1rem;
background-color: var(--card-bg);
color: var(--text-primary);
font-family: var(--font);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid var(--border);
border-radius: var(--button-radius);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
white-space: nowrap;
}
.action-btn:hover {
background-color: var(--card-bg-hover);
border-color: var(--accent);
transform: translateY(-1px);
}
.action-btn.primary {
background: linear-gradient(90deg, var(--accent), #ff8c51);
border-color: var(--accent);
color: white;
}
.action-btn.primary:hover {
background: linear-gradient(90deg, var(--accent-hover), #e67a3f);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 96, 19, 0.3);
}
.data-attribute-display {
background-color: rgba(50, 50, 50, 0.8);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
cursor: pointer;
transition: var(--transition);
user-select: all;
}
.data-attribute-display:hover {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
}
.container {
max-width: 100%;
margin: 0 auto;
padding: 2rem 1.5rem;
}
.page-header {
text-align: center;
margin-bottom: 2rem;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
background: linear-gradient(90deg, var(--accent), #ff8c51);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.page-subtitle {
font-size: var(--text-s);
color: var(--text-secondary);
font-weight: 500;
}
.instructions-toggle {
margin-bottom: 2rem;
}
.instructions-card {
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
transition: var(--transition);
}
.instructions-header {
padding: 1rem 1.5rem;
cursor: pointer;
transition: var(--transition);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid transparent;
}
.instructions-header:hover {
background-color: var(--card-bg-hover);
}
.instructions-card.expanded .instructions-header {
border-bottom-color: var(--border);
}
.instructions-title {
font-size: var(--text-s);
font-weight: 600;
}
.toggle-icon {
font-size: 1.2em;
transition: transform 0.3s ease;
}
.toggle-icon.expanded {
transform: rotate(180deg);
}
.instructions-content {
padding: 0 1.5rem;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.instructions-content.show {
max-height: 500px;
padding: 1.5rem;
}
.instructions-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.how-to-use ol {
padding-left: 1.5rem;
}
.how-to-use li {
margin-bottom: 0.75rem;
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.6;
}
.how-to-use strong {
color: var(--text-primary);
font-weight: 600;
}
.how-to-use code {
background-color: rgba(50, 50, 50, 0.5);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
}
.content {
display: grid;
grid-template-columns: 1fr 500px;
gap: 2rem;
align-items: start;
}
.preview-section {
position: sticky;
top: 2rem;
}
.controls-section {
max-width: 500px;
}
.card {
background-color: var(--card-bg);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}
.preview-container {
height: 400px;
width: 100%;
position: relative;
overflow: hidden;
border-radius: var(--card-radius);
background-color: #000000;
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
.preview-container:hover {
border-color: var(--accent);
box-shadow: 0 0 20px rgba(239, 96, 19, 0.3);
}
.preview-content {
color: white;
text-align: center;
font-weight: bold;
font-size: var(--text-s);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
.preview-controls {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.5rem;
z-index: 10;
}
.preview-btn {
padding: 0.5rem;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
cursor: pointer;
transition: var(--transition);
font-size: var(--text-xs);
backdrop-filter: blur(5px);
}
.preview-btn:hover {
background-color: var(--accent);
border-color: var(--accent);
}
.preview-btn svg {
width: 18px;
height: 18px;
stroke: currentColor;
}
.background-selector-wrapper {
position: relative;
display: inline-block;
}
.background-selector-btn {
position: relative;
}
.background-selector-btn:hover {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
box-shadow: 0 0 8px rgba(239, 96, 19, 0.3);
}
.hidden-color-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 1;
}
.card-heading {
padding: 1rem 1.5rem;
font-size: var(--text-s);
font-weight: 600;
border-bottom: 1px solid var(--border);
letter-spacing: 0.3px;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-actions {
display: flex;
gap: 0.5rem;
}
.card-action-btn {
padding: 0.4rem 0.8rem;
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
font-size: var(--text-xs);
transition: var(--transition);
}
.card-action-btn:hover {
color: var(--text-primary);
border-color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.card-content {
padding: 1.5rem;
}
.control-group {
margin-bottom: 1.5rem;
position: relative;
}
.control-group:last-child {
margin-bottom: 0;
}
.control-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.label-text {
font-size: var(--text-xs);
font-weight: 500;
letter-spacing: 0.2px;
display: flex;
align-items: center;
gap: 0.5rem;
}
.help-tooltip {
cursor: help;
opacity: 0.7;
transition: var(--transition);
}
.help-tooltip:hover {
opacity: 1;
color: var(--accent);
}
.value-display {
display: flex;
align-items: center;
gap: 0.5rem;
}
.value-text {
font-size: var(--text-xs);
color: var(--text-secondary);
background-color: rgba(50, 50, 50, 0.5);
padding: 2px 8px;
border-radius: 4px;
min-width: 45px;
text-align: center;
}
.reset-btn {
padding: 0.2rem 0.4rem;
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
font-size: 10px;
transition: var(--transition);
}
.reset-btn:hover {
color: var(--danger);
border-color: var(--danger);
background-color: rgba(220, 53, 69, 0.1);
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: var(--track);
border-radius: 3px;
outline: none;
margin: 0.8rem 0;
position: relative;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: var(--thumb);
border-radius: 50%;
cursor: pointer;
transition: var(--transition);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 10px rgba(239, 96, 19, 0.5);
}
input[type="url"],
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="url"]:focus,
input[type="text"]:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
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;
}
.url-helper {
background-color: rgba(17, 119, 255, 0.08);
color: #4FC3F7;
padding: 0.75rem 1rem;
border-radius: var(--input-radius);
font-size: var(--text-xs);
margin-bottom: 1rem;
border-left: 3px solid #4FC3F7;
letter-spacing: 0.2px;
line-height: 1.6;
}
@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/Visual-effects/" class="breadcrumb-item">Visual effects</a>
<span class="breadcrumb-separator">›</span>
<span class="breadcrumb-item active">Grid Distortion</span>
</nav>
<div class="action-buttons">
<div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
data-grid-distortion
</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">Grid Distortion</h1>
<p class="page-subtitle">Interactive shader-based distortion 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 grid distortion effect using the controls below</li>
<li>Enter an image URL in the <strong>Image Source</strong> field</li>
<li>Click <strong>Copy JS</strong> to copy the JavaScript code to clipboard</li>
<li>In Bricks Builder, add a <strong>Code</strong> element</li>
<li>Paste or upload the JavaScript code</li>
<li>To add the effect to any section: go to <strong>Section → Style → Attributes</strong>, add <code>data-grid-distortion</code> as attribute name, enter your image URL as the value</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<div class="content">
<section class="preview-section">
<div class="preview-container" id="grid-distortion-preview" data-grid-distortion="true">
<div class="preview-content">Interactive Grid Distortion Preview</div>
<div class="preview-controls">
<button class="preview-btn" id="randomize-grid" title="Randomize (R)">🎲</button>
<div class="background-selector-wrapper">
<button class="preview-btn background-selector-btn" id="background-selector">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12,2 2,7 12,12 22,7"/>
<polyline points="2,17 12,22 22,17"/>
<polyline points="2,12 12,17 22,12"/>
</svg>
</button>
<input type="color" id="preview-background-picker" class="hidden-color-input" value="#000000" title="Change Preview Background (B)">
</div>
</div>
</div>
</section>
<section class="controls-section">
<div class="card">
<div class="card-heading">
Image Source
<div class="card-actions">
<button class="card-action-btn" id="reset-image" title="Reset Image Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="url-helper">
<strong>Note:</strong> The system automatically handles CORS issues and works with most image sources including Unsplash URLs.
</div>
<div class="control-group">
<input type="url" id="image-url" placeholder="Enter image URL..." value="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Image Fit</span>
</div>
<select id="image-fit">
<option value="cover">Cover (Default)</option>
<option value="contain">Contain</option>
<option value="fill">Fill</option>
<option value="auto">Auto</option>
</select>
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Distortion Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-distortion" title="Reset Distortion Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Grid Density
<span class="help-tooltip" title="Controls the resolution of the distortion grid">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="grid-value">20</span></span>
<button class="reset-btn" onclick="resetParameter('grid', 20)">↺</button>
</div>
</div>
<input type="range" id="grid" min="20" max="80" step="2" value="20">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Mouse Sensitivity
<span class="help-tooltip" title="Range of mouse influence on the distortion effect">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="mouse-value">0.15</span></span>
<button class="reset-btn" onclick="resetParameter('mouse', 0.15)">↺</button>
</div>
</div>
<input type="range" id="mouse" min="0.1" max="0.5" step="0.05" value="0.15">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Distortion Strength
<span class="help-tooltip" title="Intensity of the visual distortion effect">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="strength-value">1.0</span></span>
<button class="reset-btn" onclick="resetParameter('strength', 1.0)">↺</button>
</div>
</div>
<input type="range" id="strength" min="0.5" max="3.0" step="0.1" value="1.0">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Recovery Speed
<span class="help-tooltip" title="How quickly the distortion returns to normal state">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="relaxation-value">0.9</span></span>
<button class="reset-btn" onclick="resetParameter('relaxation', 0.9)">↺</button>
</div>
</div>
<input type="range" id="relaxation" min="0.8" max="0.98" step="0.02" value="0.9">
</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">
Performance Mode
<span class="help-tooltip" title="Adjusts rendering quality for better performance">ℹ</span>
</span>
</div>
<select id="performance-mode">
<option value="high">High Quality</option>
<option value="balanced">Balanced (Default)</option>
<option value="performance">Performance</option>
</select>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Animation Smoothness
<span class="help-tooltip" title="Controls the smoothness of the distortion animation">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="smoothness-value">0.05</span></span>
<button class="reset-btn" onclick="resetParameter('smoothness', 0.05)">↺</button>
</div>
</div>
<input type="range" id="smoothness" min="0.02" max="0.1" step="0.01" value="0.05">
</div>
</div>
</div>
</section>
</div>
</div>
<div class="notification" id="notification"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
let gridDistortionConfig = {
imageUrl: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
grid: 20,
mouse: 0.15,
strength: 1.0,
relaxation: 0.9,
imageFit: 'cover',
performanceMode: 'balanced',
smoothness: 0.05
};
const defaultConfig = { ...gridDistortionConfig };
let currentEffect = null;
const fragmentShader = `
precision mediump float;
uniform float time;
uniform sampler2D uDataTexture;
uniform sampler2D uTexture;
uniform vec4 resolution;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vec2 newUV = (vUv - vec2(0.5)) * resolution.zw + vec2(0.5);
vec4 offset = texture2D(uDataTexture, vUv);
vec2 displacedUV = newUV - 0.02 * offset.rg;
vec4 color = texture2D(uTexture, displacedUV);
gl_FragColor = color;
}
`;
const vertexShader = `
precision mediump float;
uniform float time;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vUv = uv;
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
function clamp(number, min, max) {
return Math.max(min, Math.min(number, max));
}
const CORS_PROXIES = [
'https://api.codetabs.com/v1/proxy?quest=',
'https://api.allorigins.win/raw?url=',
'https://corsproxy.org/?',
'https://cors-anywhere.herokuapp.com/',
'https://thingproxy.freeboard.io/fetch/'
];
function convertToDirectImageUrl(url) {
if (url.includes('unsplash.com/') && url.includes('/fotos/')) {
const match = url.match(/\/fotos\/[^\/]+- ([a-zA-Z0-9_-]+)$/);
if (match) {
const photoId = match[1];
return `https://images.unsplash.com/photo-${photoId}?w=1200&q=80&auto=format&fit=crop`;
}
}
if (url.includes('unsplash.com/') && url.includes('/photos/')) {
const match = url.match(/\/photos\/[^\/]+- ([a-zA-Z0-9_-]+)$/);
if (match) {
const photoId = match[1];
return `https://images.unsplash.com/photo-${photoId}?w=1200&q=80&auto=format&fit=crop`;
}
}
return url;
}
class PixelationEffect {
constructor(container, source, options = {}) {
this.container = container;
this.source = convertToDirectImageUrl(source);
this.width = container.offsetWidth;
this.height = container.offsetHeight;
this.settings = {
grid: options.grid || 20,
mouse: options.mouse || 0.15,
strength: options.strength || 1,
relaxation: options.relaxation || 0.9,
imageFit: options.imageFit || 'cover',
performanceMode: options.performanceMode || 'balanced',
smoothness: options.smoothness || 0.05
};
this.size = this.settings.grid;
this.mouse = { x: 0, y: 0, prevX: 0, prevY: 0, vX: 0, vY: 0 };
this.time = 0;
this.isPlaying = true;
this.imageAspect = 1;
this.animationId = null;
this.scene = new THREE.Scene();
const pixelRatio = this.settings.performanceMode === 'performance' ? 1 : Math.min(window.devicePixelRatio, 2);
this.renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: this.settings.performanceMode !== 'performance',
preserveDrawingBuffer: false,
powerPreference: this.settings.performanceMode === 'performance' ? 'default' : 'high-performance'
});
this.renderer.setPixelRatio(pixelRatio);
this.renderer.setSize(this.width, this.height);
this.renderer.setClearColor(0x000000, 0);
container.appendChild(this.renderer.domElement);
Object.assign(this.renderer.domElement.style, {
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
pointerEvents: 'none'
});
const frustumSize = 1;
this.camera = new THREE.OrthographicCamera(
frustumSize / -2, frustumSize / 2,
frustumSize / 2, frustumSize / -2,
-1000, 1000
);
this.camera.position.set(0, 0, 2);
this.geometry = new THREE.PlaneGeometry(1, 1, 1, 1);
this.loadSource();
}
loadSource() {
this.loadImageWithMultipleStrategies();
}
async loadImageWithMultipleStrategies() {
try {
const success = await this.tryLoadWithCanvasProxy(this.source);
if (success) return;
} catch (e) {
// Silently fail and try next method
}
try {
const success = await this.tryLoadWithCors(this.source);
if (success) return;
} catch (e) {
// Silently fail and try next method
}
for (const proxy of CORS_PROXIES) {
try {
const success = await this.tryLoadWithCors(proxy + encodeURIComponent(this.source));
if (success) return;
} catch (e) {
// Silently fail and try next method
}
}
try {
const success = await this.tryLoadWithFetch(this.source);
if (success) return;
} catch (e) {
// Silently fail and try next method
}
this.loadFallbackImage();
}
tryLoadWithCors(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
const timeout = setTimeout(() => {
reject(new Error('Timeout'));
}, 8000);
img.onload = () => {
clearTimeout(timeout);
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
ctx.getImageData(0, 0, 1, 1);
this.imageAspect = img.width / img.height;
this.regenerateGrid();
this.addObjects(img);
this.resize();
this.setupEventListeners();
this.render();
showNotification('Image loaded successfully!', 'success');
resolve(true);
} catch (e) {
reject(new Error('CORS contaminated'));
}
};
img.onerror = () => {
clearTimeout(timeout);
reject(new Error('Failed to load'));
};
img.src = url;
});
}
tryLoadWithCanvasProxy(url) {
return new Promise((resolve, reject) => {
const img = new Image();
const timeout = setTimeout(() => {
reject(new Error('Timeout'));
}, 8000);
img.onload = () => {
clearTimeout(timeout);
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const dataURL = canvas.toDataURL('image/jpeg', 0.9);
const cleanImg = new Image();
cleanImg.onload = () => {
this.imageAspect = cleanImg.width / cleanImg.height;
this.regenerateGrid();
this.addObjects(cleanImg);
this.resize();
this.setupEventListeners();
this.render();
resolve(true);
};
cleanImg.src = dataURL;
} catch (e) {
reject(new Error('Canvas proxy failed'));
}
};
img.onerror = () => {
clearTimeout(timeout);
reject(new Error('Failed to load'));
};
img.src = url;
});
}
tryLoadWithFetch(url) {
return new Promise(async (resolve, reject) => {
try {
const timeout = setTimeout(() => {
reject(new Error('Timeout'));
}, 8000);
const response = await fetch(url, {
mode: 'no-cors',
cache: 'default'
});
clearTimeout(timeout);
if (!response.ok && response.type !== 'opaque') {
throw new Error('Fetch failed');
}
const blob = await response.blob();
const objectURL = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
this.imageAspect = img.width / img.height;
this.regenerateGrid();
this.addObjects(img);
this.resize();
this.setupEventListeners();
this.render();
URL.revokeObjectURL(objectURL);
showNotification('Image loaded successfully!', 'success');
resolve(true);
};
img.onerror = () => {
URL.revokeObjectURL(objectURL);
reject(new Error('Image load failed'));
};
img.src = objectURL;
} catch (e) {
reject(e);
}
});
}
loadFallbackImage() {
const fallbackImage = 'data:image/svg+xml;base64,' + btoa(
'<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">' +
'<defs>' +
'<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">' +
'<stop offset="0%" style="stop-color:#ef6013;stop-opacity:1" />' +
'<stop offset="100%" style="stop-color:#ff8c51;stop-opacity:1" />' +
'</linearGradient>' +
'</defs>' +
'<rect width="512" height="512" fill="url(#grad)" />' +
'<text x="256" y="220" font-family="Arial" font-size="20" fill="white" text-anchor="middle">Grid Distortion</text>' +
'<text x="256" y="250" font-family="Arial" font-size="16" fill="white" text-anchor="middle">Effect Active</text>' +
'<text x="256" y="280" font-family="Arial" font-size="14" fill="white" text-anchor="middle">Custom image loading...</text>' +
'</svg>'
);
const img = new Image();
img.onload = () => {
this.imageAspect = 1;
this.regenerateGrid();
this.addObjects(img);
this.resize();
this.setupEventListeners();
this.render();
showNotification('Using fallback image - original could not be loaded', 'warning');
};
img.src = fallbackImage;
}
setupEventListeners() {
if (this.cleanupListeners) {
this.cleanupListeners();
}
const handleMouseMove = (e) => {
const rect = this.container.getBoundingClientRect();
this.mouse.x = (e.clientX - rect.left) / this.width;
this.mouse.y = (e.clientY - rect.top) / this.height;
this.mouse.vX = this.mouse.x - this.mouse.prevX;
this.mouse.vY = this.mouse.y - this.mouse.prevY;
this.mouse.prevX = this.mouse.x;
this.mouse.prevY = this.mouse.y;
if (Math.abs(this.mouse.vX) > 0.01 || Math.abs(this.mouse.vY) > 0.01) {
this.container.style.borderColor = 'var(--accent)';
setTimeout(() => {
this.container.style.borderColor = 'var(--border)';
}, 100);
}
};
const handleResize = () => {
this.resize();
};
this.container.addEventListener('mousemove', handleMouseMove);
window.addEventListener('resize', handleResize);
this.cleanupListeners = () => {
this.container.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('resize', handleResize);
};
}
resize() {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.renderer.setSize(this.width, this.height);
let a1, a2;
switch (this.settings.imageFit) {
case 'contain':
if (this.height / this.width < this.imageAspect) {
a1 = 1;
a2 = (this.height / this.width) / this.imageAspect;
} else {
a1 = (this.width / this.height) * this.imageAspect;
a2 = 1;
}
break;
case 'fill':
a1 = 1;
a2 = 1;
break;
case 'auto':
a1 = this.imageAspect;
a2 = 1;
break;
default: // cover
if (this.height / this.width > this.imageAspect) {
a1 = (this.width / this.height) * this.imageAspect;
a2 = 1;
} else {
a1 = 1;
a2 = (this.height / this.width) / this.imageAspect;
}
}
if (this.material) {
this.material.uniforms.resolution.value.x = this.width;
this.material.uniforms.resolution.value.y = this.height;
this.material.uniforms.resolution.value.z = a1;
this.material.uniforms.resolution.value.w = a2;
}
this.camera.updateProjectionMatrix();
this.regenerateGrid();
}
regenerateGrid() {
this.size = this.settings.grid;
const width = this.size;
const height = this.size;
const size = width * height;
const data = new Float32Array(4 * size);
for (let i = 0; i < size; i++) {
const r = Math.random() * 255 - 125;
const r1 = Math.random() * 255 - 125;
const stride = i * 4;
data[stride] = r;
data[stride + 1] = r1;
data[stride + 2] = r;
data[stride + 3] = 255;
}
this.texture = new THREE.DataTexture(
data, width, height,
THREE.RGBAFormat, THREE.FloatType
);
this.texture.magFilter = this.texture.minFilter = THREE.NearestFilter;
if (this.material) {
this.material.uniforms.uDataTexture.value = this.texture;
this.material.uniforms.uDataTexture.value.needsUpdate = true;
}
}
addObjects(source) {
this.regenerateGrid();
const texture = new THREE.Texture(source);
texture.needsUpdate = true;
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
texture.minFilter = texture.magFilter = THREE.LinearFilter;
texture.flipY = true;
this.material = new THREE.ShaderMaterial({
side: THREE.DoubleSide,
uniforms: {
time: { value: 0 },
resolution: { value: new THREE.Vector4() },
uTexture: { value: texture },
uDataTexture: { value: this.texture },
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true,
});
this.plane = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.plane);
}
updateDataTexture() {
if (!this.texture) return;
const data = this.texture.image.data;
for (let i = 0; i < data.length; i += 4) {
data[i] *= this.settings.relaxation;
data[i + 1] *= this.settings.relaxation;
}
const gridMouseX = this.size * this.mouse.x;
const gridMouseY = this.size * (1 - this.mouse.y);
const maxDist = this.size * this.settings.mouse;
const aspect = this.height / this.width;
for (let i = 0; i < this.size; i++) {
for (let j = 0; j < this.size; j++) {
const distance = ((gridMouseX - i) ** 2) / aspect + (gridMouseY - j) ** 2;
const maxDistSq = maxDist ** 2;
if (distance < maxDistSq) {
const index = 4 * (i + this.size * j);
let power = maxDist / Math.sqrt(distance);
power = clamp(power, 0, 10);
data[index] += this.settings.strength * 100 * this.mouse.vX * power;
data[index + 1] -= this.settings.strength * 100 * this.mouse.vY * power;
}
}
}
this.mouse.vX *= 0.9;
this.mouse.vY *= 0.9;
this.texture.needsUpdate = true;
}
updateSettings(newSettings) {
this.settings = { ...this.settings, ...newSettings };
if (newSettings.grid && newSettings.grid !== this.size) {
this.regenerateGrid();
}
if (newSettings.imageFit) {
this.resize();
}
}
render = () => {
if (!this.isPlaying) return;
this.time += this.settings.smoothness;
this.updateDataTexture();
if (this.material) {
this.material.uniforms.time.value = this.time;
}
this.animationId = requestAnimationFrame(this.render);
this.renderer.render(this.scene, this.camera);
};
updateSource(newSource) {
this.source = convertToDirectImageUrl(newSource);
if (this.cleanupListeners) {
this.cleanupListeners();
}
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
if (this.plane && this.scene) {
this.scene.remove(this.plane);
}
if (this.material) {
this.material.dispose();
}
if (this.texture) {
this.texture.dispose();
}
this.mouse = { x: 0, y: 0, prevX: 0, prevY: 0, vX: 0, vY: 0 };
this.isPlaying = true;
this.loadSource();
}
destroy() {
this.isPlaying = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
if (this.cleanupListeners) {
this.cleanupListeners();
}
if (this.renderer.domElement.parentNode) {
this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
}
this.renderer.dispose();
this.geometry.dispose();
if (this.material) {
this.material.dispose();
}
if (this.texture) {
this.texture.dispose();
}
}
}
function initGridDistortionEffect() {
const previewContainer = document.getElementById('grid-distortion-preview');
if (currentEffect) {
currentEffect.destroy();
}
currentEffect = new PixelationEffect(previewContainer, gridDistortionConfig.imageUrl, {
grid: gridDistortionConfig.grid,
mouse: gridDistortionConfig.mouse,
strength: gridDistortionConfig.strength,
relaxation: gridDistortionConfig.relaxation,
imageFit: gridDistortionConfig.imageFit,
performanceMode: gridDistortionConfig.performanceMode,
smoothness: gridDistortionConfig.smoothness
});
}
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = `notification ${type}`;
switch(type) {
case 'success':
notification.style.backgroundColor = 'var(--success)';
break;
case 'warning':
notification.style.backgroundColor = 'var(--warning)';
notification.style.color = '#000';
break;
case 'info':
notification.style.backgroundColor = '#17a2b8';
notification.style.color = '#fff';
break;
case 'error':
notification.style.backgroundColor = 'var(--danger)';
break;
}
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);
}, type === 'info' ? 2000 : 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 divId = generateUniqueId();
const codeId = generateUniqueId();
const attributeId1 = generateUniqueId();
const attributeId2 = generateUniqueId();
// Obtener el JavaScript actual con la configuración del usuario
const jsCode = generateJavaScriptCode();
// Crear el objeto JSON completo de Bricks Builder
const bricksJSON = {
"content": [
{
"id": sectionId,
"name": "section",
"parent": 0,
"children": [containerId, codeId],
"settings": {
"_justifyContent": "center",
"_background": {
"color": {
"hex": "#000000"
}
},
"_attributes": [
{
"id": attributeId1,
"name": "data-gemini-effect"
}
]
}
},
{
"id": containerId,
"name": "container",
"parent": sectionId,
"children": [divId],
"settings": {
"_border": {
"radius": {
"top": "15",
"right": "15",
"bottom": "15",
"left": "15"
}
},
"_overflow": "hidden",
"_direction": "row",
"_justifyContent": "center"
}
},
{
"id": divId,
"name": "div",
"parent": containerId,
"children": [],
"settings": {
"_attributes": [
{
"id": attributeId2,
"name": "data-grid-distortion"
}
],
"_height": "350",
"_width": "350",
"_border": {
"radius": {
"top": "15",
"right": "15",
"bottom": "15",
"left": "15"
}
}
},
"label": "Grid Distortion Div"
},
{
"id": codeId,
"name": "code",
"parent": sectionId,
"children": [],
"settings": {
"javascriptCode": jsCode,
"executeCode": true,
"_display": "none"
},
"label": "Grid Distortion 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 EnhancedGridDistortionAnimation = {
instances: [],
config: {
imageUrl: "${gridDistortionConfig.imageUrl}",
grid: ${gridDistortionConfig.grid},
mouse: ${gridDistortionConfig.mouse},
strength: ${gridDistortionConfig.strength},
relaxation: ${gridDistortionConfig.relaxation},
imageFit: "${gridDistortionConfig.imageFit}",
performanceMode: "${gridDistortionConfig.performanceMode}",
smoothness: ${gridDistortionConfig.smoothness}
},
init: function(options = {}) {
const defaultOptions = {
selector: '[data-grid-distortion]',
imageUrlAttr: 'data-grid-distortion'
};
const config = { ...defaultOptions, ...options };
const initInstances = () => {
document.querySelectorAll(config.selector).forEach(element => {
if (!element.hasAttribute('data-grid-initialized')) {
this.createInstance(element, config);
element.setAttribute('data-grid-initialized', 'true');
}
});
};
initInstances();
setTimeout(initInstances, 100);
window.addEventListener('load', initInstances);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
initInstances();
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
},
createInstance: function(element, config) {
const loadThreeJS = () => {
return new Promise((resolve) => {
if (window.THREE) {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
script.onload = resolve;
document.head.appendChild(script);
});
};
loadThreeJS().then(() => {
const source = element.getAttribute(config.imageUrlAttr) || this.config.imageUrl;
const options = {
grid: parseInt(element.getAttribute('data-grid')) || this.config.grid,
mouse: parseFloat(element.getAttribute('data-mouse')) || this.config.mouse,
strength: parseFloat(element.getAttribute('data-strength')) || this.config.strength,
relaxation: parseFloat(element.getAttribute('data-relaxation')) || this.config.relaxation,
imageFit: element.getAttribute('data-image-fit') || this.config.imageFit,
performanceMode: element.getAttribute('data-performance') || this.config.performanceMode,
smoothness: parseFloat(element.getAttribute('data-smoothness')) || this.config.smoothness,
};
element.style.position = element.style.position || 'relative';
element.style.overflow = 'hidden';
new PixelationEffect(element, source, options);
});
}
};
class PixelationEffect {
constructor(container, source, options = {}) {
this.container = container;
this.source = this.convertToDirectImageUrl(source);
this.width = container.offsetWidth;
this.height = container.offsetHeight;
this.settings = {
grid: options.grid || EnhancedGridDistortionAnimation.config.grid,
mouse: options.mouse || EnhancedGridDistortionAnimation.config.mouse,
strength: options.strength || EnhancedGridDistortionAnimation.config.strength,
relaxation: options.relaxation || EnhancedGridDistortionAnimation.config.relaxation,
imageFit: options.imageFit || EnhancedGridDistortionAnimation.config.imageFit,
performanceMode: options.performanceMode || EnhancedGridDistortionAnimation.config.performanceMode,
smoothness: options.smoothness || EnhancedGridDistortionAnimation.config.smoothness
};
this.size = this.settings.grid;
this.mouse = { x: 0, y: 0, prevX: 0, prevY: 0, vX: 0, vY: 0 };
this.time = 0;
this.isPlaying = true;
this.imageAspect = 1;
this.animationId = null;
this.init();
}
convertToDirectImageUrl(url) {
if (url.includes('unsplash.com/') && url.includes('/fotos/')) {
const match = url.match(/\\/fotos\\/[^\\/]+- ([a-zA-Z0-9_-]+)$/);
if (match) {
const photoId = match[1];
return 'https://images.unsplash.com/photo-' + photoId + '?w=1200&q=80&auto=format&fit=crop';
}
}
if (url.includes('unsplash.com/') && url.includes('/photos/')) {
const match = url.match(/\\/photos\\/[^\\/]+- ([a-zA-Z0-9_-]+)$/);
if (match) {
const photoId = match[1];
return 'https://images.unsplash.com/photo-' + photoId + '?w=1200&q=80&auto=format&fit=crop';
}
}
return url;
}
init() {
this.scene = new THREE.Scene();
const pixelRatio = this.settings.performanceMode === 'performance' ? 1 : Math.min(window.devicePixelRatio, 2);
this.renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: this.settings.performanceMode !== 'performance',
preserveDrawingBuffer: false,
powerPreference: this.settings.performanceMode === 'performance' ? 'default' : 'high-performance'
});
this.renderer.setPixelRatio(pixelRatio);
this.renderer.setSize(this.width, this.height);
this.renderer.setClearColor(0x000000, 0);
this.container.appendChild(this.renderer.domElement);
const frustumSize = 1;
this.camera = new THREE.OrthographicCamera(
frustumSize / -2, frustumSize / 2,
frustumSize / 2, frustumSize / -2,
-1000, 1000
);
this.camera.position.set(0, 0, 2);
this.geometry = new THREE.PlaneGeometry(1, 1, 1, 1);
this.loadSource();
}
async loadSource() {
const CORS_PROXIES = [
'https://api.codetabs.com/v1/proxy?quest=',
'https://api.allorigins.win/raw?url=',
'https://corsproxy.org/?',
'https://cors-anywhere.herokuapp.com/',
'https://thingproxy.freeboard.io/fetch/'
];
try {
const success = await this.tryLoadWithCors(this.source);
if (success) return;
} catch (e) {
// Silently continue to next method
}
try {
const success = await this.tryLoadWithoutCors(this.source);
if (success) return;
} catch (e) {
// Silently continue to next method
}
for (const proxy of CORS_PROXIES) {
try {
const success = await this.tryLoadWithCors(proxy + encodeURIComponent(this.source));
if (success) return;
} catch (e) {
// Silently continue to next method
}
}
this.loadFallbackImage();
}
tryLoadWithCors(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
const timeout = setTimeout(() => {
reject(new Error('Timeout'));
}, 5000);
img.onload = () => {
clearTimeout(timeout);
this.imageAspect = img.width / img.height;
this.regenerateGrid();
this.addObjects(img);
this.resize();
this.setupEventListeners();
this.render();
resolve(true);
};
img.onerror = () => {
clearTimeout(timeout);
reject(new Error('Failed to load'));
};
img.src = url;
});
}
tryLoadWithoutCors(url) {
return new Promise((resolve, reject) => {
const img = new Image();
const timeout = setTimeout(() => {
reject(new Error('Timeout'));
}, 5000);
img.onload = () => {
clearTimeout(timeout);
this.imageAspect = img.width / img.height;
this.regenerateGrid();
this.addObjects(img);
this.resize();
this.setupEventListeners();
this.render();
resolve(true);
};
img.onerror = () => {
clearTimeout(timeout);
reject(new Error('Failed to load'));
};
img.src = url;
});
}
loadFallbackImage() {
const fallbackImage = 'data:image/svg+xml;base64,' + btoa('<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#ef6013;stop-opacity:1" /><stop offset="100%" style="stop-color:#ff8c51;stop-opacity:1" /></linearGradient></defs><rect width="512" height="512" fill="url(#grad)" /><text x="256" y="220" font-family="Arial" font-size="20" fill="white" text-anchor="middle">Grid Distortion</text><text x="256" y="250" font-family="Arial" font-size="16" fill="white" text-anchor="middle">Effect Active</text><text x="256" y="280" font-family="Arial" font-size="14" fill="white" text-anchor="middle">Custom image loading...</text></svg>');
const img = new Image();
img.onload = () => {
this.imageAspect = 1;
this.regenerateGrid();
this.addObjects(img);
this.resize();
this.setupEventListeners();
this.render();
};
img.src = fallbackImage;
}
setupEventListeners() {
const handleMouseMove = (e) => {
const rect = this.container.getBoundingClientRect();
this.mouse.x = (e.clientX - rect.left) / this.width;
this.mouse.y = (e.clientY - rect.top) / this.height;
this.mouse.vX = this.mouse.x - this.mouse.prevX;
this.mouse.vY = this.mouse.y - this.mouse.prevY;
this.mouse.prevX = this.mouse.x;
this.mouse.prevY = this.mouse.y;
};
const handleResize = () => {
this.resize();
};
this.container.addEventListener('mousemove', handleMouseMove);
window.addEventListener('resize', handleResize);
this.cleanupListeners = () => {
this.container.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('resize', handleResize);
};
}
resize() {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.renderer.setSize(this.width, this.height);
let a1, a2;
switch (this.settings.imageFit) {
case 'contain':
if (this.height / this.width < this.imageAspect) {
a1 = 1;
a2 = (this.height / this.width) / this.imageAspect;
} else {
a1 = (this.width / this.height) * this.imageAspect;
a2 = 1;
}
break;
case 'fill':
a1 = 1;
a2 = 1;
break;
case 'auto':
a1 = this.imageAspect;
a2 = 1;
break;
default:
if (this.height / this.width > this.imageAspect) {
a1 = (this.width / this.height) * this.imageAspect;
a2 = 1;
} else {
a1 = 1;
a2 = (this.height / this.width) / this.imageAspect;
}
}
if (this.material) {
this.material.uniforms.resolution.value.x = this.width;
this.material.uniforms.resolution.value.y = this.height;
this.material.uniforms.resolution.value.z = a1;
this.material.uniforms.resolution.value.w = a2;
}
this.camera.updateProjectionMatrix();
this.regenerateGrid();
}
regenerateGrid() {
this.size = this.settings.grid;
const width = this.size;
const height = this.size;
const size = width * height;
const data = new Float32Array(4 * size);
for (let i = 0; i < size; i++) {
const r = Math.random() * 255 - 125;
const r1 = Math.random() * 255 - 125;
const stride = i * 4;
data[stride] = r;
data[stride + 1] = r1;
data[stride + 2] = r;
data[stride + 3] = 255;
}
this.texture = new THREE.DataTexture(
data, width, height,
THREE.RGBAFormat, THREE.FloatType
);
this.texture.magFilter = this.texture.minFilter = THREE.NearestFilter;
if (this.material) {
this.material.uniforms.uDataTexture.value = this.texture;
this.material.uniforms.uDataTexture.value.needsUpdate = true;
}
}
addObjects(source) {
this.regenerateGrid();
const texture = new THREE.Texture(source);
texture.needsUpdate = true;
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
texture.minFilter = texture.magFilter = THREE.LinearFilter;
texture.flipY = true;
const fragmentShader = \`
precision mediump float;
uniform float time;
uniform sampler2D uDataTexture;
uniform sampler2D uTexture;
uniform vec4 resolution;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vec2 newUV = (vUv - vec2(0.5)) * resolution.zw + vec2(0.5);
vec4 offset = texture2D(uDataTexture, vUv);
vec2 displacedUV = newUV - 0.02 * offset.rg;
vec4 color = texture2D(uTexture, displacedUV);
gl_FragColor = color;
}
\`;
const vertexShader = \`
precision mediump float;
uniform float time;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vUv = uv;
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
\`;
this.material = new THREE.ShaderMaterial({
side: THREE.DoubleSide,
uniforms: {
time: { value: 0 },
resolution: { value: new THREE.Vector4() },
uTexture: { value: texture },
uDataTexture: { value: this.texture },
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true,
});
this.plane = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.plane);
}
updateDataTexture() {
if (!this.texture) return;
const data = this.texture.image.data;
for (let i = 0; i < data.length; i += 4) {
data[i] *= this.settings.relaxation;
data[i + 1] *= this.settings.relaxation;
}
const gridMouseX = this.size * this.mouse.x;
const gridMouseY = this.size * (1 - this.mouse.y);
const maxDist = this.size * this.settings.mouse;
const aspect = this.height / this.width;
for (let i = 0; i < this.size; i++) {
for (let j = 0; j < this.size; j++) {
const distance = ((gridMouseX - i) ** 2) / aspect + (gridMouseY - j) ** 2;
const maxDistSq = maxDist ** 2;
if (distance < maxDistSq) {
const index = 4 * (i + this.size * j);
let power = maxDist / Math.sqrt(distance);
power = Math.max(0, Math.min(power, 10));
data[index] += this.settings.strength * 100 * this.mouse.vX * power;
data[index + 1] -= this.settings.strength * 100 * this.mouse.vY * power;
}
}
}
this.mouse.vX *= 0.9;
this.mouse.vY *= 0.9;
this.texture.needsUpdate = true;
}
render = () => {
if (!this.isPlaying) return;
this.time += this.settings.smoothness;
this.updateDataTexture();
if (this.material) {
this.material.uniforms.time.value = this.time;
}
this.animationId = requestAnimationFrame(this.render);
this.renderer.render(this.scene, this.camera);
};
updateSettings(newSettings) {
this.settings = { ...this.settings, ...newSettings };
if (newSettings.grid && newSettings.grid !== this.size) {
this.regenerateGrid();
}
if (newSettings.imageFit) {
this.resize();
}
}
destroy() {
this.isPlaying = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
if (this.cleanupListeners) {
this.cleanupListeners();
}
if (this.renderer.domElement.parentNode) {
this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
}
this.renderer.dispose();
this.geometry.dispose();
if (this.material) {
this.material.dispose();
}
if (this.texture) {
this.texture.dispose();
}
}
}
window.EnhancedGridDistortionAnimation = EnhancedGridDistortionAnimation;
window.PixelationEffect = PixelationEffect;
EnhancedGridDistortionAnimation.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');
});
}
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 'grid':
gridDistortionConfig.grid = defaultValue;
break;
case 'mouse':
gridDistortionConfig.mouse = defaultValue;
break;
case 'strength':
gridDistortionConfig.strength = defaultValue;
break;
case 'relaxation':
gridDistortionConfig.relaxation = defaultValue;
break;
case 'smoothness':
gridDistortionConfig.smoothness = defaultValue;
break;
}
if (currentEffect) {
currentEffect.updateSettings(gridDistortionConfig);
}
showNotification(`${parameterId.replace(/-/g, ' ')} reset to default`);
}
};
function generateRandomGrid() {
gridDistortionConfig.grid = Math.floor(Math.random() * 60) + 20;
gridDistortionConfig.mouse = Math.random() * 0.4 + 0.1;
gridDistortionConfig.strength = Math.random() * 2.5 + 0.5;
gridDistortionConfig.relaxation = Math.random() * 0.18 + 0.8;
gridDistortionConfig.smoothness = Math.random() * 0.08 + 0.02;
const imageFitOptions = ['cover', 'contain', 'fill', 'auto'];
const performanceModes = ['high', 'balanced', 'performance'];
gridDistortionConfig.imageFit = imageFitOptions[Math.floor(Math.random() * imageFitOptions.length)];
gridDistortionConfig.performanceMode = performanceModes[Math.floor(Math.random() * performanceModes.length)];
document.getElementById('grid').value = gridDistortionConfig.grid;
document.getElementById('mouse').value = gridDistortionConfig.mouse.toFixed(2);
document.getElementById('strength').value = gridDistortionConfig.strength.toFixed(1);
document.getElementById('relaxation').value = gridDistortionConfig.relaxation.toFixed(2);
document.getElementById('smoothness').value = gridDistortionConfig.smoothness.toFixed(2);
document.getElementById('image-fit').value = gridDistortionConfig.imageFit;
document.getElementById('performance-mode').value = gridDistortionConfig.performanceMode;
document.getElementById('grid-value').textContent = gridDistortionConfig.grid;
document.getElementById('mouse-value').textContent = gridDistortionConfig.mouse.toFixed(2);
document.getElementById('strength-value').textContent = gridDistortionConfig.strength.toFixed(1);
document.getElementById('relaxation-value').textContent = gridDistortionConfig.relaxation.toFixed(2);
document.getElementById('smoothness-value').textContent = gridDistortionConfig.smoothness.toFixed(2);
if (currentEffect) {
currentEffect.updateSettings(gridDistortionConfig);
}
showNotification('Random grid distortion generated!');
}
function initializeUI() {
initGridDistortionEffect();
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-grid-distortion');
});
document.getElementById('download-config').addEventListener('click', () => {
copyJsToClipboard();
});
document.getElementById('copy-full-section').addEventListener('click', () => {
copyFullSectionToClipboard();
});
document.getElementById('randomize-grid').addEventListener('click', () => {
generateRandomGrid();
});
const backgroundPicker = document.getElementById('preview-background-picker');
const previewContainer = document.getElementById('grid-distortion-preview');
backgroundPicker.addEventListener('input', (e) => {
const selectedColor = e.target.value;
previewContainer.style.backgroundColor = selectedColor;
showNotification(`Preview background changed to ${selectedColor}`);
});
previewContainer.style.backgroundColor = '#000000';
document.getElementById('reset-image').addEventListener('click', () => {
gridDistortionConfig.imageUrl = defaultConfig.imageUrl;
gridDistortionConfig.imageFit = defaultConfig.imageFit;
document.getElementById('image-url').value = defaultConfig.imageUrl;
document.getElementById('image-fit').value = defaultConfig.imageFit;
initGridDistortionEffect();
showNotification('Image settings reset to default');
});
document.getElementById('reset-distortion').addEventListener('click', () => {
gridDistortionConfig.grid = 20;
gridDistortionConfig.mouse = 0.15;
gridDistortionConfig.strength = defaultConfig.strength;
gridDistortionConfig.relaxation = defaultConfig.relaxation;
document.getElementById('grid').value = 20;
document.getElementById('mouse').value = 0.15;
document.getElementById('strength').value = defaultConfig.strength;
document.getElementById('relaxation').value = defaultConfig.relaxation;
document.getElementById('grid-value').textContent = 20;
document.getElementById('mouse-value').textContent = 0.15;
document.getElementById('strength-value').textContent = defaultConfig.strength;
document.getElementById('relaxation-value').textContent = defaultConfig.relaxation;
if (currentEffect) {
currentEffect.updateSettings(gridDistortionConfig);
}
showNotification('Distortion settings reset');
});
document.getElementById('reset-advanced').addEventListener('click', () => {
gridDistortionConfig.performanceMode = defaultConfig.performanceMode;
gridDistortionConfig.smoothness = defaultConfig.smoothness;
document.getElementById('performance-mode').value = defaultConfig.performanceMode;
document.getElementById('smoothness').value = defaultConfig.smoothness;
document.getElementById('smoothness-value').textContent = defaultConfig.smoothness;
if (currentEffect) {
currentEffect.updateSettings(gridDistortionConfig);
}
showNotification('Advanced settings reset');
});
document.getElementById('image-url').addEventListener('input', function() {
gridDistortionConfig.imageUrl = this.value;
if (this.value.trim()) {
showNotification('Loading new image...', 'info');
setTimeout(() => {
if (currentEffect) {
currentEffect.updateSource(gridDistortionConfig.imageUrl);
}
}, 500);
}
});
document.getElementById('image-fit').addEventListener('change', function() {
gridDistortionConfig.imageFit = this.value;
if (currentEffect) {
currentEffect.updateSettings({ imageFit: gridDistortionConfig.imageFit });
}
});
document.getElementById('performance-mode').addEventListener('change', function() {
gridDistortionConfig.performanceMode = this.value;
initGridDistortionEffect();
});
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 'grid':
gridDistortionConfig.grid = parseInt(input.value);
break;
case 'mouse':
gridDistortionConfig.mouse = parseFloat(input.value);
break;
case 'strength':
gridDistortionConfig.strength = parseFloat(input.value);
break;
case 'relaxation':
gridDistortionConfig.relaxation = parseFloat(input.value);
break;
case 'smoothness':
gridDistortionConfig.smoothness = parseFloat(input.value);
break;
}
if (currentEffect) {
currentEffect.updateSettings(gridDistortionConfig);
}
});
});
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
if (e.ctrlKey || e.metaKey) {
switch (e.key.toLowerCase()) {
case 'd':
e.preventDefault();
const downloadBtn = document.getElementById('download-config');
if (downloadBtn && downloadBtn.hasAttribute('data-protection-animation')) {
downloadBtn.click();
} else {
copyJsToClipboard();
}
break;
case 's':
e.preventDefault();
const fullSectionBtn = document.getElementById('copy-full-section');
if (fullSectionBtn && fullSectionBtn.hasAttribute('data-protection-animation')) {
fullSectionBtn.click();
} else {
copyFullSectionToClipboard();
}
break;
}
} else {
switch (e.key.toLowerCase()) {
case 'r':
generateRandomGrid();
break;
case 'b':
document.getElementById('preview-background-picker').click();
break;
}
}
});
setTimeout(() => {
showNotification('BricksFusion Grid Distortion Configurator loaded!');
}, 500);
function saveConfiguration() {
try {
localStorage.setItem('bricksfusion-grid-distortion-config', JSON.stringify(gridDistortionConfig));
} catch (e) {
// Silently fail if localStorage is not available
}
}
function loadConfiguration() {
try {
const saved = localStorage.getItem('bricksfusion-grid-distortion-config');
if (saved) {
const savedConfig = JSON.parse(saved);
Object.assign(gridDistortionConfig, savedConfig);
document.getElementById('image-url').value = savedConfig.imageUrl || defaultConfig.imageUrl;
document.getElementById('grid').value = savedConfig.grid || defaultConfig.grid;
document.getElementById('mouse').value = savedConfig.mouse || defaultConfig.mouse;
document.getElementById('strength').value = savedConfig.strength || defaultConfig.strength;
document.getElementById('relaxation').value = savedConfig.relaxation || defaultConfig.relaxation;
document.getElementById('image-fit').value = savedConfig.imageFit || defaultConfig.imageFit;
document.getElementById('performance-mode').value = savedConfig.performanceMode || defaultConfig.performanceMode;
document.getElementById('smoothness').value = savedConfig.smoothness || defaultConfig.smoothness;
document.getElementById('grid-value').textContent = savedConfig.grid || defaultConfig.grid;
document.getElementById('mouse-value').textContent = savedConfig.mouse || defaultConfig.mouse;
document.getElementById('strength-value').textContent = savedConfig.strength || defaultConfig.strength;
document.getElementById('relaxation-value').textContent = savedConfig.relaxation || defaultConfig.relaxation;
document.getElementById('smoothness-value').textContent = savedConfig.smoothness || defaultConfig.smoothness;
initGridDistortionEffect();
}
} catch (e) {
// Silently fail if localStorage is not available
}
}
const originalInitEffect = initGridDistortionEffect;
initGridDistortionEffect = function() {
originalInitEffect();
saveConfiguration();
};
loadConfiguration();
}
initializeUI();
});
</script>
</body>
</html>
Grid Distortion
Creates a fluid distortion effect on images using WebGL shaders. Mouse movement creates ripple-like distortions across a grid. Uses Three.js for 3D rendering with customizable grid density, strength, and relaxation. Perfect for hero images, portfolio showcases, or adding interactive visual interest.
Grid Distortion
Move your mouse over the image to create distortions.
Image
Source image for distortion effect. Supports CORS-enabled images. Falls back to gradient if loading fails.
Required
How image fills container. Options: cover (fills completely), contain (fits inside), fill (stretches), auto (original aspect).
Default: cover
Grid Settings
Density of distortion grid. Higher creates finer detail but impacts performance. Lower is faster.
Default: 20
Radius of mouse influence on grid. Higher creates wider distortion area around cursor.
Default: 0.15
Intensity of distortion displacement. Higher creates more dramatic warping effects.
Default: 1.0
Speed of distortion decay. Lower returns to normal faster, higher creates lingering ripples.
Default: 0.9
Animation
Animation time increment per frame. Lower is slower and smoother, higher is faster.
Default: 0.05
Performance
Rendering quality preset. Options: performance (fastest, lower quality), balanced (recommended), quality (best visuals, slower).
Default: balanced
Performance
This element uses WebGL with Three.js r128 for GPU-accelerated rendering. Creates custom GLSL shaders for distortion effect with DataTexture for grid data. Uses OrthographicCamera with PlaneGeometry and ShaderMaterial. Runs continuous requestAnimationFrame loop for smooth animation. Resource intensive - limit to 1-2 instances per page. Not recommended for mobile devices or low-end hardware on performance mode.