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>CardTwist Stack 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: 2rem;
}
.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);
}
.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;
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="text"],
input[type="number"],
input[type="url"],
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);
}
input[type="text"]:focus,
input[type="number"]:focus,
input[type="url"]:focus,
select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
.color-row {
display: flex;
align-items: center;
gap: 1.25rem;
padding: 1rem 1.25rem;
background-color: rgba(30, 30, 30, 0.7);
border: 1px solid var(--border);
border-radius: var(--input-radius);
transition: var(--transition);
margin-bottom: 1rem;
}
.color-row:hover {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(239, 96, 19, 0.1);
}
.color-picker-container {
position: relative;
width: 40px;
height: 40px;
border-radius: 8px;
overflow: hidden;
border: 2px solid var(--border);
cursor: pointer;
transition: var(--transition);
flex-shrink: 0;
background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
background-size: 8px 8px;
background-position: 0 0, 0 4px, 4px -4px, -4px 0px;
}
.color-picker-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--current-color, #FFFFFF);
border-radius: 6px;
z-index: 1;
}
.color-picker-container:hover {
border-color: var(--accent);
transform: scale(1.05);
}
input[type="color"] {
position: absolute;
top: -2px;
left: -2px;
width: calc(100% + 4px);
height: calc(100% + 4px);
border: none;
cursor: pointer;
background: transparent;
z-index: 2;
opacity: 0;
}
.color-input-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.color-label {
font-size: 10px;
font-weight: 500;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 0.25rem;
}
.color-input {
padding: 0.5rem 0.75rem;
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 12px;
transition: var(--transition);
min-width: 0;
}
.color-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(239, 96, 19, 0.2);
outline: none;
}
.color-input.invalid {
border-color: var(--danger);
box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.2);
}
.hex-input,
.hsl-input {
width: 100%;
}
.color-input-group:nth-child(2) {
flex: 0.3;
}
.color-input-group:nth-child(3) {
flex: 0.7;
}
.switch-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.2rem;
padding: 0.5rem 0;
}
.switch-label {
font-size: var(--text-xs);
font-weight: 500;
letter-spacing: 0.2px;
display: flex;
align-items: center;
gap: 0.5rem;
}
.switch {
position: relative;
display: inline-block;
width: 52px;
height: 28px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--track);
transition: var(--transition);
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: #f2f2f7;
transition: var(--transition);
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
input:checked + .slider {
background-color: var(--accent);
}
input:checked + .slider:before {
transform: translateX(24px);
}
.image-inputs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.url-input-container {
margin-bottom: 1rem;
}
.url-input-label {
display: block;
margin-bottom: 0.5rem;
font-size: var(--text-xs);
font-weight: 500;
}
.url-input {
width: 100%;
}
.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;
}
.cardtwist-container {
position: relative;
width: 208px;
height: 208px;
margin: 0 auto;
perspective: 600px;
}
.cardtwist-rotate {
position: absolute;
width: 100%;
height: 100%;
cursor: grab;
top: 0;
left: 0;
user-select: none;
touch-action: none;
}
.card-twist {
width: 100%;
height: 100%;
border-radius: 12px;
overflow: hidden;
transition: transform 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
position: relative;
}
.card-img {
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
@media (max-width: 1200px) {
.content {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.preview-section {
position: static;
}
.controls-section {
max-width: 100%;
}
}
@media (max-width: 768px) {
.action-bar {
flex-direction: column;
height: auto;
min-height: var(--action-bar-height);
padding: 0.75rem;
}
.breadcrumb {
order: 1;
width: 100%;
}
.action-buttons {
order: 2;
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
body {
padding-bottom: calc(var(--action-bar-height) + 20px);
}
.notification {
bottom: calc(var(--action-bar-height) + 2rem);
max-width: 280px;
transform: translate(-50%, 250px);
}
.notification.show {
transform: translate(-50%, 0);
opacity: 1;
}
.color-row {
flex-direction: column;
align-items: stretch;
gap: 1rem;
padding: 1rem;
}
.color-picker-container {
align-self: center;
margin-bottom: 0.5rem;
}
.color-input-group {
align-items: stretch;
}
.hex-input,
.hsl-input {
width: 100%;
}
.preview-container {
height: 300px;
}
.data-attribute-display {
font-size: 10px;
padding: 0.4rem 0.6rem;
}
.action-btn {
font-size: 11px;
padding: 0.5rem 0.8rem;
}
.page-title {
font-size: 2rem;
}
.image-inputs {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<div class="action-bar">
<nav class="breadcrumb">
<a href="https://bricksfusion.com" class="breadcrumb-item">Home</a>
<span class="breadcrumb-separator">›</span>
<a href="https://bricksfusion.com/showcase/" class="breadcrumb-item">Showcase</a>
<span class="breadcrumb-separator">›</span>
<span class="breadcrumb-item active">CardTwist Stack</span>
</nav>
<div class="action-buttons">
<div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
data-cardtwist
</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">CardTwist Stack</h1>
<p class="page-subtitle">Interactive card stack animations for Bricks Builder</p>
</div>
<div class="instructions-toggle">
<div class="instructions-card" id="instructions-card">
<div class="instructions-header" id="instructions-toggle">
<div class="instructions-title">
How to Use & Code Information
</div>
<span class="toggle-icon">▼</span>
</div>
<div class="instructions-content" id="instructions-content">
<div class="instructions-grid">
<div class="how-to-use">
<ol>
<li>Customize your card stack using the controls below</li>
<li>Click <strong>Copy JS</strong> to copy the JavaScript code to clipboard</li>
<li>In Bricks Builder, add a <strong>Code</strong> element</li>
<li>Paste the JavaScript code</li>
<li>To add the effect to any section: go to <strong>Section → Style → Attributes</strong>, add <code>data-cardtwist</code> as attribute name (leave value empty)</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<div class="content">
<section class="preview-section">
<div class="preview-container" id="cardtwist-preview">
<div id="cardtwist-container" data-cardtwist data-random-rotation="true" data-sensitivity="200" style="position:relative; width:208px; height:208px; perspective:600px; margin:0 auto;">
</div>
</div>
</section>
<section class="controls-section">
<div class="card">
<div class="card-heading">
Image Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-images" title="Reset Images">↺</button>
</div>
</div>
<div class="card-content">
<div class="image-inputs">
<div class="url-input-container">
<label class="url-input-label">Image 1</label>
<input type="url" class="url-input" id="image-url-1" placeholder="https://example.com/image1.jpg">
</div>
<div class="url-input-container">
<label class="url-input-label">Image 2</label>
<input type="url" class="url-input" id="image-url-2" placeholder="https://example.com/image2.jpg">
</div>
<div class="url-input-container">
<label class="url-input-label">Image 3</label>
<input type="url" class="url-input" id="image-url-3" placeholder="https://example.com/image3.jpg">
</div>
<div class="url-input-container">
<label class="url-input-label">Image 4</label>
<input type="url" class="url-input" id="image-url-4" placeholder="https://example.com/image4.jpg">
</div>
<div class="url-input-container">
<label class="url-input-label">Image 5</label>
<input type="url" class="url-input" id="image-url-5" placeholder="https://example.com/image5.jpg">
</div>
<div class="url-input-container">
<label class="url-input-label">Image 6</label>
<input type="url" class="url-input" id="image-url-6" placeholder="https://example.com/image6.jpg">
</div>
</div>
<div class="color-row">
<div class="color-picker-container">
<input type="color" id="border-color" value="#FFFFFF">
</div>
<div class="color-input-group">
<span class="color-label">HEX</span>
<input type="text" class="color-input hex-input" id="border-color-hex" value="#FFFFFF" placeholder="#FFFFFF">
</div>
<div class="color-input-group">
<span class="color-label">HSL</span>
<input type="text" class="color-input hsl-input" id="border-color-hsl" placeholder="hsl(0, 0%, 100%)">
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Card Properties
<div class="card-actions">
<button class="card-action-btn" id="reset-properties" title="Reset Properties">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Card Width
<span class="help-tooltip" title="Width of each card in pixels">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="card-width-value">208</span>px</span>
<button class="reset-btn" onclick="resetParameter('card-width', 208)">↺</button>
</div>
</div>
<input type="range" id="card-width" min="100" max="400" step="1" value="208">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Card Height
<span class="help-tooltip" title="Height of each card in pixels">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="card-height-value">208</span>px</span>
<button class="reset-btn" onclick="resetParameter('card-height', 208)">↺</button>
</div>
</div>
<input type="range" id="card-height" min="100" max="400" step="1" value="208">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Border Width
<span class="help-tooltip" title="Width of the card borders">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="border-width-value">5</span>px</span>
<button class="reset-btn" onclick="resetParameter('border-width', 5)">↺</button>
</div>
</div>
<input type="range" id="border-width" min="0" max="10" step="1" value="5">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Border Radius
<span class="help-tooltip" title="Roundness of card corners">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="border-radius-value">12</span>px</span>
<button class="reset-btn" onclick="resetParameter('border-radius', 12)">↺</button>
</div>
</div>
<input type="range" id="border-radius" min="0" max="24" step="1" value="12">
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Animation Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-animation" title="Reset Animation Settings">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Drag Sensitivity
<span class="help-tooltip" title="How far a card must be dragged before it moves to front">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="sensitivity-value">200</span></span>
<button class="reset-btn" onclick="resetParameter('sensitivity', 200)">↺</button>
</div>
</div>
<input type="range" id="sensitivity" min="50" max="350" step="10" value="200">
</div>
<div class="switch-container">
<span class="switch-label">
Random Rotation
<span class="help-tooltip" title="Add slight random rotation to cards for natural look">ℹ</span>
</span>
<label class="switch">
<input type="checkbox" id="random-rotation" checked>
<span class="slider"></span>
</label>
</div>
</div>
</div>
</section>
</div>
</div>
<div class="notification" id="notification"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let twistConfig = {
cardWidth: 208,
cardHeight: 208,
sensitivity: 200,
borderWidth: 5,
borderRadius: 12,
borderColor: '#FFFFFF',
randomRotation: true,
images: ['', '', '', '', '', '']
};
const defaultConfig = { ...twistConfig };
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 hexToHsl(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
}
function hslToHex(hsl) {
const match = hsl.match(/hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/);
if (!match) return null;
let h = parseInt(match[1]) / 360;
let s = parseInt(match[2]) / 100;
let l = parseInt(match[3]) / 100;
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
const toHex = (c) => {
const hex = Math.round(c * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function isValidHex(hex) {
return /^#[0-9A-F]{6}$/i.test(hex);
}
function isValidHsl(hsl) {
return /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/i.test(hsl);
}
function formatHex(value) {
let hex = value.replace(/[^0-9A-Fa-f#]/g, '');
if (!hex.startsWith('#')) {
hex = '#' + hex;
}
if (hex.length > 7) {
hex = hex.substring(0, 7);
}
return hex.toUpperCase();
}
function formatHsl(value) {
const cleanValue = value.replace(/[^\d,\s]/g, '');
const numbers = cleanValue.match(/\d+/g);
if (!numbers || numbers.length < 3) {
const partialMatch = value.match(/(\d+)/g);
if (partialMatch && partialMatch.length >= 1) {
const h = Math.min(360, Math.max(0, parseInt(partialMatch[0]) || 0));
const s = Math.min(100, Math.max(0, parseInt(partialMatch[1]) || 50));
const l = Math.min(100, Math.max(0, parseInt(partialMatch[2]) || 50));
return `hsl(${h}, ${s}%, ${l}%)`;
}
return value;
}
let h = Math.min(360, Math.max(0, parseInt(numbers[0])));
let s = Math.min(100, Math.max(0, parseInt(numbers[1])));
let l = Math.min(100, Math.max(0, parseInt(numbers[2])));
return `hsl(${h}, ${s}%, ${l}%)`;
}
function generateUniqueId() {
return Math.random().toString(36).substring(2, 8);
}
function generateFullSectionJSON() {
// Generar IDs únicos
const sectionId = generateUniqueId();
const containerId = generateUniqueId();
const divId = generateUniqueId();
const codeId = generateUniqueId();
const attributeId = generateUniqueId();
const divAttributeId = generateUniqueId();
// Obtener el código JavaScript generado
const jsCode = generateJavaScriptCode();
// Crear la estructura JSON completa
const fullSectionData = {
"content": [
{
"id": sectionId,
"name": "section",
"parent": 0,
"children": [containerId, codeId],
"settings": {
"_height": "200",
"_justifyContent": "center",
"_background": {
"color": {
"hex": "#000000"
}
}
},
"label": "Card Twist Section"
},
{
"id": containerId,
"name": "container",
"parent": sectionId,
"children": [divId],
"settings": {
"_alignItems": "center"
}
},
{
"id": divId,
"name": "div",
"parent": containerId,
"children": [],
"settings": {
"_attributes": [
{
"id": divAttributeId,
"name": "data-cardtwist"
}
]
}
},
{
"id": codeId,
"name": "code",
"parent": sectionId,
"children": [],
"settings": {
"javascriptCode": jsCode,
"executeCode": true,
"_display": "none"
},
"label": "Card Twist JS"
}
],
"source": "bricksCopiedElements",
"sourceUrl": "https://bricksfusion.com",
"version": "2.0.1",
"globalClasses": [],
"globalElements": []
};
return JSON.stringify(fullSectionData, null, 2);
}
function generateJavaScriptCode() {
return `(function () {
const Cardtwist = {
init: function (container) {
container.style.width = "${twistConfig.cardWidth}px";
container.style.height = "${twistConfig.cardHeight}px";
container.style.borderRadius = "${twistConfig.borderRadius}px";
container.style.perspective = "600px";
container.style.position = "relative";
container.style.margin = "0 auto";
const randomRotation = container.getAttribute("data-random-rotation") === "true";
const sensitivity = parseFloat(container.getAttribute("data-sensitivity")) || ${twistConfig.sensitivity};
let cardElements = container.querySelectorAll(".cardtwist-rotate");
if (cardElements.length === 0) {
let images = [];
for (let i = 1; i <= 6; i++) {
let imgAttr = container.getAttribute("data-img" + i);
if (imgAttr) {
images.push(imgAttr);
}
}
if (images.length === 0) {
images = [
'https://images.unsplash.com/photo-1611916656173-875e4277bea6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTg1Mg&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1547347298-4074fc3086f0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTkwMg&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1565310022184-f23a884f29da?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTk3NQ&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1565310022184-f23a884f29da?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTk3NQ&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1547347298-4074fc3086f0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTkwMg&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1611916656173-875e4277bea6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTg1Mg&ixlib=rb-4.0.3&q=80&w=400'
];
}
images.forEach((imgSrc, index) => {
const cardWrapper = document.createElement("div");
cardWrapper.classList.add("cardtwist-rotate");
cardWrapper.style.position = "absolute";
cardWrapper.style.width = "100%";
cardWrapper.style.height = "100%";
cardWrapper.style.cursor = "grab";
cardWrapper.style.top = "0px";
cardWrapper.style.left = "0px";
cardWrapper.style.userSelect = "none";
cardWrapper.style.webkitUserSelect = "none";
cardWrapper.style.msUserSelect = "none";
cardWrapper.style.touchAction = "none";
const cardDiv = document.createElement("div");
cardDiv.classList.add("card");
cardDiv.style.width = "100%";
cardDiv.style.height = "100%";
cardDiv.style.borderRadius = "${twistConfig.borderRadius}px";
cardDiv.style.border = "${twistConfig.borderWidth}px solid ${twistConfig.borderColor}";
cardDiv.style.overflow = "hidden";
cardDiv.style.transition = "transform 0.3s ease";
cardDiv.style.userSelect = "none";
cardDiv.style.webkitUserSelect = "none";
cardDiv.style.msUserSelect = "none";
const img = document.createElement("img");
img.classList.add("card-image");
img.src = imgSrc;
img.alt = "card-" + (index + 1);
img.style.pointerEvents = "none";
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "cover";
cardDiv.appendChild(img);
cardWrapper.appendChild(cardDiv);
container.appendChild(cardWrapper);
});
cardElements = container.querySelectorAll(".cardtwist-rotate");
}
const updateStackOrder = () => {
const cards = Array.from(container.querySelectorAll(".cardtwist-rotate"));
const total = cards.length;
cards.forEach((card, index) => {
const cardInner = card.querySelector(".card");
if (cardInner) {
const randomOffset = randomRotation ? (Math.random() * 10 - 5).toFixed(2) : 0;
const rotateZ = ((total - index - 1) * 4 + Number(randomOffset)).toFixed(2);
const scale = (1 + index * 0.06 - total * 0.06).toFixed(2);
cardInner.style.transformOrigin = "90% 90%";
cardInner.style.transform = "rotateZ(" + rotateZ + "deg) scale(" + scale + ")";
}
});
};
updateStackOrder();
cardElements.forEach((card) => {
if (card._pointerDownHandler) {
card.removeEventListener("pointerdown", card._pointerDownHandler);
card.removeEventListener("pointermove", card._pointerMoveHandler);
card.removeEventListener("pointerup", card._pointerUpHandler);
card.removeEventListener("pointercancel", card._pointerUpHandler);
}
let isDragging = false;
let startX, startY;
let currentX = 0, currentY = 0;
const updateDragTransform = (dx, dy) => {
const computedRotateX = -0.6 * dy;
const computedRotateY = 0.6 * dx;
card.style.transform = "translate(" + dx + "px, " + dy + "px) rotateX(" + computedRotateX + "deg) rotateY(" + computedRotateY + "deg)";
};
const pointerDownHandler = (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
currentX = 0;
currentY = 0;
card.setPointerCapture(e.pointerId);
card.style.transition = "none";
};
const pointerMoveHandler = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
currentX = dx;
currentY = dy;
updateDragTransform(dx, dy);
};
const pointerUpHandler = (e) => {
if (!isDragging) return;
isDragging = false;
card.releasePointerCapture(e.pointerId);
if (Math.abs(currentX) > sensitivity || Math.abs(currentY) > sensitivity) {
const parent = card.parentElement;
if (parent) {
parent.insertBefore(card, parent.firstChild);
updateStackOrder();
}
}
card.style.transition = "transform 0.3s ease";
card.style.transform = "translate(0px, 0px) rotateX(0deg) rotateY(0deg)";
};
card._pointerDownHandler = pointerDownHandler;
card._pointerMoveHandler = pointerMoveHandler;
card._pointerUpHandler = pointerUpHandler;
card.addEventListener("pointerdown", pointerDownHandler);
card.addEventListener("pointermove", pointerMoveHandler);
card.addEventListener("pointerup", pointerUpHandler);
card.addEventListener("pointercancel", pointerUpHandler);
});
}
};
document.addEventListener("DOMContentLoaded", function () {
const twistElements = document.querySelectorAll("[data-cardtwist]");
twistElements.forEach((el) => {
Cardtwist.init(el);
});
});
window.Cardtwist = Cardtwist;
})();`;
}
function updatePreview() {
const container = document.getElementById('cardtwist-container');
if (!container) return;
container.innerHTML = '';
const defaultImages = [
'https://images.unsplash.com/photo-1611916656173-875e4277bea6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTg1Mg&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1547347298-4074fc3086f0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTkwMg&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1565310022184-f23a884f29da?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTk3NQ&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1565310022184-f23a884f29da?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTk3NQ&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1547347298-4074fc3086f0?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTkwMg&ixlib=rb-4.0.3&q=80&w=400',
'https://images.unsplash.com/photo-1611916656173-875e4277bea6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY5MTQ5MTg1Mg&ixlib=rb-4.0.3&q=80&w=400'
];
const imagesToUse = twistConfig.images.map((userImage, index) =>
userImage || defaultImages[index]
);
imagesToUse.forEach((imgUrl, index) => {
createCardElement(container, imgUrl, index);
});
initializeCardEvents();
updateCardsRotation();
}
function createCardElement(container, imgUrl, index) {
const cardWrapper = document.createElement('div');
cardWrapper.classList.add('cardtwist-rotate');
cardWrapper.style.position = 'absolute';
cardWrapper.style.width = `${twistConfig.cardWidth}px`;
cardWrapper.style.height = `${twistConfig.cardHeight}px`;
cardWrapper.style.cursor = 'grab';
cardWrapper.style.top = '0px';
cardWrapper.style.left = '0px';
cardWrapper.style.userSelect = 'none';
cardWrapper.style.touchAction = 'none';
cardWrapper.style.transform = 'translate(0px, 0px) rotateX(0deg) rotateY(0deg)';
cardWrapper.style.transition = 'transform 0.3s ease';
const cardDiv = document.createElement('div');
cardDiv.classList.add('card-twist');
cardDiv.style.width = '100%';
cardDiv.style.height = '100%';
cardDiv.style.borderRadius = `${twistConfig.borderRadius}px`;
cardDiv.style.border = `${twistConfig.borderWidth}px solid ${twistConfig.borderColor}`;
cardDiv.style.overflow = 'hidden';
cardDiv.style.position = 'relative';
cardDiv.style.transformOrigin = '90% 90%';
const img = document.createElement('img');
img.classList.add('card-img');
img.src = imgUrl;
img.alt = `card-${index + 1}`;
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'cover';
img.style.pointerEvents = 'none';
cardDiv.appendChild(img);
cardWrapper.appendChild(cardDiv);
container.appendChild(cardWrapper);
}
function initializeCardEvents() {
const container = document.getElementById('cardtwist-container');
if (!container) return;
const cards = Array.from(container.querySelectorAll('.cardtwist-rotate'));
cards.forEach(card => {
const newCard = card.cloneNode(true);
card.parentNode.replaceChild(newCard, card);
let isDragging = false;
let startX, startY;
let initialCardTransform;
let initialInnerTransform;
newCard.addEventListener('pointerdown', e => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialCardTransform = newCard.style.transform;
initialInnerTransform = newCard.querySelector('.card-twist').style.transform;
newCard.style.transition = 'none';
newCard.style.cursor = 'grabbing';
newCard.style.zIndex = '1000';
newCard.setPointerCapture(e.pointerId);
});
newCard.addEventListener('pointermove', e => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
const rotateX = -dy * 0.5;
const rotateY = dx * 0.5;
newCard.style.transform = `translate(${dx}px, ${dy}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
});
const endDrag = e => {
if (!isDragging) return;
isDragging = false;
newCard.style.transition = 'all 0.3s ease';
newCard.style.cursor = 'grab';
newCard.style.zIndex = '';
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (Math.abs(dx) > twistConfig.sensitivity || Math.abs(dy) > twistConfig.sensitivity) {
container.insertBefore(newCard, container.firstChild);
newCard.style.transform = initialCardTransform;
setTimeout(() => updateCardsRotation(), 0);
} else {
newCard.style.transform = initialCardTransform;
const innerCard = newCard.querySelector('.card-twist');
if (innerCard) {
innerCard.style.transform = initialInnerTransform;
}
}
};
newCard.addEventListener('pointerup', endDrag);
newCard.addEventListener('pointercancel', endDrag);
newCard.addEventListener('pointerleave', endDrag);
});
}
function updateCardsRotation() {
const container = document.getElementById('cardtwist-container');
if (!container) return;
const cards = Array.from(container.querySelectorAll('.cardtwist-rotate'));
const total = cards.length;
cards.forEach((card, index) => {
const cardInner = card.querySelector('.card-twist');
if (cardInner) {
cardInner.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.1, 0.25, 1)';
const randomOffset = twistConfig.randomRotation ? (Math.random() * 10 - 5) : 0;
const rotateZ = ((total - index - 1) * 4 + randomOffset).toFixed(2);
const scale = (1 + index * 0.06 - total * 0.06).toFixed(2);
cardInner.style.transform = `rotateZ(${rotateZ}deg) scale(${scale})`;
}
});
}
function updateVisualProperties() {
const container = document.getElementById('cardtwist-container');
if (!container) return;
container.style.transition = 'width 0.3s ease, height 0.3s ease';
container.style.width = `${twistConfig.cardWidth}px`;
container.style.height = `${twistConfig.cardHeight}px`;
const cards = Array.from(container.querySelectorAll('.cardtwist-rotate'));
cards.forEach(card => {
card.style.transition = 'width 0.3s ease, height 0.3s ease';
card.style.width = `${twistConfig.cardWidth}px`;
card.style.height = `${twistConfig.cardHeight}px`;
const cardInner = card.querySelector('.card-twist');
if (cardInner) {
cardInner.style.transition = 'border 0.3s ease, border-radius 0.3s ease';
cardInner.style.borderRadius = `${twistConfig.borderRadius}px`;
cardInner.style.border = `${twistConfig.borderWidth}px solid ${twistConfig.borderColor}`;
}
});
}
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 'card-width':
twistConfig.cardWidth = defaultValue;
updateVisualProperties();
break;
case 'card-height':
twistConfig.cardHeight = defaultValue;
updateVisualProperties();
break;
case 'sensitivity':
twistConfig.sensitivity = defaultValue;
break;
case 'border-width':
twistConfig.borderWidth = defaultValue;
updateVisualProperties();
break;
case 'border-radius':
twistConfig.borderRadius = defaultValue;
updateVisualProperties();
break;
}
showNotification(`${parameterId.replace(/-/g, ' ')} reset to default`);
}
};
function initializeUI() {
updatePreview();
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-cardtwist');
});
document.getElementById('download-config').addEventListener('click', () => {
copyJsToClipboard();
});
document.getElementById('copy-full-section').addEventListener('click', () => {
copyFullSectionToClipboard();
});
document.getElementById('reset-images').addEventListener('click', () => {
twistConfig.images = [...defaultConfig.images];
for (let i = 1; i <= 6; i++) {
document.getElementById(`image-url-${i}`).value = '';
}
updatePreview();
showNotification('Images reset to default');
});
document.getElementById('reset-properties').addEventListener('click', () => {
twistConfig.cardWidth = defaultConfig.cardWidth;
twistConfig.cardHeight = defaultConfig.cardHeight;
twistConfig.borderWidth = defaultConfig.borderWidth;
twistConfig.borderRadius = defaultConfig.borderRadius;
twistConfig.borderColor = defaultConfig.borderColor;
document.getElementById('card-width').value = defaultConfig.cardWidth;
document.getElementById('card-height').value = defaultConfig.cardHeight;
document.getElementById('border-width').value = defaultConfig.borderWidth;
document.getElementById('border-radius').value = defaultConfig.borderRadius;
document.getElementById('border-color').value = defaultConfig.borderColor;
document.getElementById('border-color-hex').value = defaultConfig.borderColor;
document.getElementById('border-color-hsl').value = hexToHsl(defaultConfig.borderColor);
const container = document.getElementById('border-color').closest('.color-picker-container');
if (container) {
container.style.setProperty('--current-color', defaultConfig.borderColor);
}
document.getElementById('card-width-value').textContent = defaultConfig.cardWidth;
document.getElementById('card-height-value').textContent = defaultConfig.cardHeight;
document.getElementById('border-width-value').textContent = defaultConfig.borderWidth;
document.getElementById('border-radius-value').textContent = defaultConfig.borderRadius;
updateVisualProperties();
showNotification('Card properties reset');
});
document.getElementById('reset-animation').addEventListener('click', () => {
twistConfig.sensitivity = defaultConfig.sensitivity;
twistConfig.randomRotation = defaultConfig.randomRotation;
document.getElementById('sensitivity').value = defaultConfig.sensitivity;
document.getElementById('random-rotation').checked = defaultConfig.randomRotation;
document.getElementById('sensitivity-value').textContent = defaultConfig.sensitivity;
updateCardsRotation();
showNotification('Animation settings reset');
});
const colorInput = document.getElementById('border-color');
const hexInput = document.getElementById('border-color-hex');
const hslInput = document.getElementById('border-color-hsl');
hslInput.value = hexToHsl(colorInput.value);
const container = colorInput.closest('.color-picker-container');
if (container) {
container.style.setProperty('--current-color', colorInput.value);
}
colorInput.addEventListener('input', () => {
const color = colorInput.value;
hexInput.value = color;
hslInput.value = hexToHsl(color);
hexInput.classList.remove('invalid');
hslInput.classList.remove('invalid');
const container = colorInput.closest('.color-picker-container');
if (container) {
container.style.setProperty('--current-color', color);
}
twistConfig.borderColor = color;
updateVisualProperties();
});
hexInput.addEventListener('input', (e) => {
let hex = e.target.value;
hex = formatHex(hex);
e.target.value = hex;
if (isValidHex(hex)) {
colorInput.value = hex;
hslInput.value = hexToHsl(hex);
const container = colorInput.closest('.color-picker-container');
if (container) {
container.style.setProperty('--current-color', hex);
}
twistConfig.borderColor = hex;
e.target.classList.remove('invalid');
hslInput.classList.remove('invalid');
updateVisualProperties();
} else {
e.target.classList.add('invalid');
}
});
hexInput.addEventListener('blur', (e) => {
if (!isValidHex(e.target.value)) {
e.target.value = colorInput.value;
e.target.classList.remove('invalid');
}
});
hslInput.addEventListener('input', (e) => {
let hsl = e.target.value;
if (isValidHsl(hsl)) {
const hex = hslToHex(hsl);
if (hex) {
colorInput.value = hex;
hexInput.value = hex;
const container = colorInput.closest('.color-picker-container');
if (container) {
container.style.setProperty('--current-color', hex);
}
twistConfig.borderColor = hex;
e.target.classList.remove('invalid');
hexInput.classList.remove('invalid');
updateVisualProperties();
return;
}
}
e.target.classList.add('invalid');
});
hslInput.addEventListener('blur', (e) => {
let hsl = e.target.value;
if (!isValidHsl(hsl) && hsl.trim()) {
const formatted = formatHsl(hsl);
if (isValidHsl(formatted)) {
e.target.value = formatted;
const hex = hslToHex(formatted);
if (hex) {
colorInput.value = hex;
hexInput.value = hex;
const container = colorInput.closest('.color-picker-container');
if (container) {
container.style.setProperty('--current-color', hex);
}
twistConfig.borderColor = hex;
e.target.classList.remove('invalid');
hexInput.classList.remove('invalid');
updateVisualProperties();
return;
}
}
}
if (!isValidHsl(e.target.value)) {
e.target.value = hexToHsl(colorInput.value);
e.target.classList.remove('invalid');
}
});
for (let i = 1; i <= 6; i++) {
document.getElementById(`image-url-${i}`).addEventListener('input', (e) => {
const url = e.target.value.trim();
twistConfig.images[i-1] = url;
updatePreview();
});
}
const rangeInputs = document.querySelectorAll('input[type="range"]');
rangeInputs.forEach(input => {
let 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 'card-width':
twistConfig.cardWidth = parseInt(input.value);
updateVisualProperties();
break;
case 'card-height':
twistConfig.cardHeight = parseInt(input.value);
updateVisualProperties();
break;
case 'sensitivity':
twistConfig.sensitivity = parseInt(input.value);
break;
case 'border-width':
twistConfig.borderWidth = parseInt(input.value);
updateVisualProperties();
break;
case 'border-radius':
twistConfig.borderRadius = parseInt(input.value);
updateVisualProperties();
break;
}
});
});
document.getElementById('random-rotation').addEventListener('change', function() {
twistConfig.randomRotation = this.checked;
updateCardsRotation();
});
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;
}
}
});
function saveConfiguration() {
try {
localStorage.setItem('bricksfusion-cardtwist-config', JSON.stringify(twistConfig));
} catch (e) {
}
}
function loadConfiguration() {
try {
const saved = localStorage.getItem('bricksfusion-cardtwist-config');
if (saved) {
const savedConfig = JSON.parse(saved);
Object.assign(twistConfig, savedConfig);
document.getElementById('card-width').value = savedConfig.cardWidth;
document.getElementById('card-height').value = savedConfig.cardHeight;
document.getElementById('sensitivity').value = savedConfig.sensitivity;
document.getElementById('border-width').value = savedConfig.borderWidth;
document.getElementById('border-radius').value = savedConfig.borderRadius;
document.getElementById('border-color').value = savedConfig.borderColor;
document.getElementById('border-color-hex').value = savedConfig.borderColor;
document.getElementById('border-color-hsl').value = hexToHsl(savedConfig.borderColor);
document.getElementById('random-rotation').checked = savedConfig.randomRotation;
const container = document.getElementById('border-color').closest('.color-picker-container');
if (container) {
container.style.setProperty('--current-color', savedConfig.borderColor);
}
savedConfig.images.forEach((image, index) => {
document.getElementById(`image-url-${index + 1}`).value = image;
});
document.getElementById('card-width-value').textContent = savedConfig.cardWidth;
document.getElementById('card-height-value').textContent = savedConfig.cardHeight;
document.getElementById('sensitivity-value').textContent = savedConfig.sensitivity;
document.getElementById('border-width-value').textContent = savedConfig.borderWidth;
document.getElementById('border-radius-value').textContent = savedConfig.borderRadius;
updatePreview();
}
} catch (e) {
}
}
const originalUpdatePreview = updatePreview;
updatePreview = function() {
originalUpdatePreview();
saveConfiguration();
};
const originalUpdateVisualProperties = updateVisualProperties;
updateVisualProperties = function() {
originalUpdateVisualProperties();
saveConfiguration();
};
loadConfiguration();
setTimeout(() => {
showNotification('BricksFusion CardTwist Stack Configurator loaded!');
}, 500);
}
initializeUI();
});
</script>
</body>
</html>
Card Twist
Creates an interactive stack of cards you can drag and twist. Drag a card away to send it to the back. Perfect for galleries, product showcases, or Tinder-style interfaces.
Cards
Upload the images for your card stack. Add as many as you want. They'll appear in a twisted stack ready to interact with.
Default: Placeholder images
Size
Width of each card in the stack. Larger cards are more prominent but take more space.
Default: 208
Height of each card. Common aspect ratios are 1:1 for square or 3:4 for portrait cards.
Default: 208
Appearance
Thickness of the border around each card. Thicker borders create a polaroid-style look.
Default: 5
Roundness of the card corners. 0 is sharp, higher values create more rounded corners.
Default: 12
Color of the border around each card. White creates a classic polaroid effect.
Default: White
Behavior
How far you need to drag a card before it goes to the back. Lower values are more sensitive, higher values require longer drags.
Default: 200
Add random rotation to stacked cards for a more natural, messy look. Turn off for perfectly aligned stacks.
Default: Off
Performance
This element uses pointer events and CSS transforms for smooth interactions. Works great on both desktop and touch devices. Lightweight enough to use multiple times on the same page.