Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
<title>Flashcard Animation Studio</title> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css"> | |
<style> | |
:root { | |
--primary-color: #2196F3; | |
--primary-dark: #1976D2; | |
--primary-light: #BBDEFB; | |
--accent-color: #FF4081; | |
--text-color: #212121; | |
--text-secondary: #757575; | |
--white: #FFFFFF; | |
--card-bg: #FAFAFA; | |
--border-color: #E0E0E0; | |
--shadow-color: rgba(0, 0, 0, 0.1); | |
--success-color: #4CAF50; | |
--warning-color: #FFC107; | |
--error-color: #F44336; | |
--header-height: 60px; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
-webkit-tap-highlight-color: transparent; | |
} | |
@font-face { | |
font-family: 'Inter'; | |
src: url('https://cdnjs.cloudflare.com/ajax/libs/inter-ui/3.19.3/Inter.var.woff2') format('woff2-variations'); | |
font-weight: 100 900; | |
font-style: normal; | |
} | |
body { | |
margin: 0; | |
padding: 0; | |
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; | |
background-color: var(--background-color); | |
color: var(--text-color); | |
overflow: hidden; | |
width: 100vw; | |
height: 100vh; | |
position: relative; | |
} | |
/* App Structure */ | |
.app-container { | |
display: flex; | |
flex-direction: column; | |
height: 100vh; | |
width: 100vw; | |
overflow: hidden; | |
position: absolute; | |
top: 0; | |
left: 0; | |
} | |
.app-header { | |
height: var(--header-height); | |
background-color: var(--primary-color); | |
color: var(--white); | |
display: flex; | |
align-items: center; | |
padding: 0 20px; | |
position: relative; | |
z-index: 10; | |
box-shadow: 0 2px 4px var(--shadow-color); | |
} | |
.app-title { | |
font-weight: 600; | |
font-size: 18px; | |
flex: 1; | |
} | |
.app-actions { | |
display: flex; | |
gap: 12px; | |
align-items: center; | |
} | |
.main-content { | |
display: flex; | |
flex: 1; | |
overflow: hidden; | |
height: calc(100vh - var(--header-height)); | |
width: 100%; | |
} | |
.slots-panel { | |
width: 280px; | |
background-color: var(--white); | |
border-right: 1px solid var(--border-color); | |
display: flex; | |
flex-direction: column; | |
height: 100%; | |
transform: translateZ(0); | |
} | |
.canvas-area { | |
flex: 1; | |
background: linear-gradient(135deg, #2196F3, #42a5f5); | |
position: relative; | |
overflow: hidden; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
transform: translateZ(0); | |
width: 100%; | |
height: 100%; | |
min-height: 500px; | |
padding: 30px 20px; | |
} | |
/* Slot Management */ | |
.slots-header { | |
padding: 15px; | |
border-bottom: 1px solid var(--border-color); | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.slots-title { | |
font-weight: 600; | |
font-size: 16px; | |
} | |
.slots-list { | |
flex: 1; | |
overflow-y: auto; | |
padding: 10px; | |
-webkit-overflow-scrolling: touch; | |
} | |
.slot-item { | |
border: 1px solid var(--border-color); | |
border-radius: 8px; | |
margin-bottom: 12px; | |
overflow: hidden; | |
transition: all 0.2s; | |
cursor: pointer; | |
background: var(--card-bg); | |
position: relative; | |
} | |
.slot-item:hover { | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
} | |
.slot-item.active { | |
border-color: var(--primary-color); | |
box-shadow: 0 0 0 2px var(--primary-light); | |
} | |
.slot-header { | |
padding: 12px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
background-color: var(--white); | |
border-bottom: 1px solid var(--border-color); | |
} | |
.slot-title { | |
font-weight: 500; | |
font-size: 14px; | |
} | |
.slot-actions { | |
display: flex; | |
gap: 8px; | |
} | |
.slot-action { | |
width: 24px; | |
height: 24px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
border-radius: 4px; | |
cursor: pointer; | |
color: var(--text-secondary); | |
transition: all 0.2s; | |
} | |
.slot-action:hover { | |
background-color: var(--primary-light); | |
color: var(--primary-color); | |
} | |
.slot-preview { | |
padding: 10px; | |
display: flex; | |
height: 80px; | |
align-items: center; | |
} | |
.slot-thumbnail { | |
width: 60px; | |
height: 60px; | |
border-radius: 4px; | |
object-fit: cover; | |
background-color: var(--primary-light); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
font-size: 24px; | |
color: var(--primary-color); | |
} | |
.slot-info { | |
margin-left: 10px; | |
flex: 1; | |
} | |
.slot-name { | |
font-weight: 500; | |
font-size: 14px; | |
margin-bottom: 4px; | |
} | |
.slot-details { | |
font-size: 12px; | |
color: var(--text-secondary); | |
} | |
.animation-badge { | |
display: inline-block; | |
padding: 2px 6px; | |
background-color: var(--primary-light); | |
color: var(--primary-color); | |
border-radius: 4px; | |
font-size: 10px; | |
font-weight: 500; | |
margin-top: 4px; | |
} | |
/* Canvas Container Styles */ | |
.canvas-container { | |
position: relative; | |
width: 100%; | |
height: 100%; | |
min-height: 450px; | |
background-color: rgba(255, 255, 255, 0.1); | |
border-radius: 0; | |
overflow: hidden; | |
box-shadow: none; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
.canvas-area { | |
flex: 1; | |
background: linear-gradient(135deg, #2196F3, #42a5f5); | |
position: relative; | |
overflow: hidden; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
transform: translateZ(0); | |
width: 100%; | |
height: 100%; | |
min-height: 500px; | |
padding: 30px 20px; | |
} | |
/* Flashcard Styles */ | |
.flashcard { | |
position: absolute; | |
width: 28vw; | |
height: 28vw; | |
max-width: 300px; | |
max-height: 300px; | |
border-radius: 24px; | |
background-color: white; | |
box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.9); | |
overflow: hidden; | |
z-index: 5; | |
transition: transform 0.2s ease, box-shadow 0.2s ease; | |
} | |
.flashcard-left { | |
top: 15%; | |
left: 8%; | |
} | |
.flashcard-right { | |
top: 45%; | |
right: 8%; | |
} | |
.card-content { | |
width: 100%; | |
height: 100%; | |
border-radius: 16px; | |
overflow: hidden; | |
} | |
/* Label Styles */ | |
.label-container { | |
position: absolute; | |
z-index: 10; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
transition: transform 0.2s ease; | |
} | |
.label-left { | |
top: 20%; | |
right: 12%; | |
} | |
.label-right { | |
top: 50%; | |
left: 12%; | |
} | |
.label { | |
padding: 12px 20px; | |
width: 28vw; | |
max-width: 260px; | |
background-color: #002D72; | |
text-align: center; | |
color: white; | |
font-size: clamp(22px, 3.5vw, 32px); | |
font-weight: bold; | |
border-radius: 3px; | |
} | |
.translation { | |
color: white; | |
font-size: clamp(18px, 3vw, 26px); | |
text-align: center; | |
width: 28vw; | |
max-width: 260px; | |
margin-top: 8px; | |
text-shadow: 0 1px 3px rgba(0,0,0,0.3); | |
} | |
/* Style Selector */ | |
.style-selector { | |
position: absolute; | |
bottom: 20px; | |
left: 50%; | |
transform: translateX(-50%); | |
background-color: rgba(255, 255, 255, 0.95); | |
border-radius: 30px; | |
padding: 10px; | |
display: flex; | |
gap: 8px; | |
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); | |
z-index: 5; | |
flex-wrap: wrap; | |
justify-content: center; | |
max-width: 90%; | |
} | |
.style-option { | |
padding: 8px 16px; | |
border-radius: 20px; | |
font-size: 14px; | |
font-weight: 500; | |
cursor: pointer; | |
transition: all 0.2s; | |
white-space: nowrap; | |
} | |
.style-option:hover { | |
background-color: rgba(33, 150, 243, 0.1); | |
} | |
.style-option.active { | |
background-color: var(--primary-color); | |
color: white; | |
} | |
/* Responsive adjustments */ | |
@media (max-width: 1200px) { | |
.flashcard, .label, .translation { | |
width: 180px; | |
} | |
.flashcard { | |
height: 180px; | |
} | |
.flashcard-left { | |
transform: translate(calc(-100% - 20px), -50%); | |
} | |
.flashcard-right { | |
transform: translate(20px, -50%); | |
} | |
.label-left { | |
transform: translate(20px, -50%); | |
} | |
.label-right { | |
transform: translate(calc(-100% - 20px), -50%); | |
} | |
} | |
@media (max-width: 992px) { | |
.canvas-container { | |
min-height: 400px; | |
} | |
.canvas-area { | |
min-height: 450px; | |
} | |
.style-selector { | |
bottom: 10px; | |
padding: 8px; | |
} | |
.style-option { | |
padding: 6px 12px; | |
font-size: 13px; | |
} | |
.main-content { | |
flex-direction: column; | |
} | |
.slots-panel { | |
width: 100%; | |
height: 180px; | |
border-right: none; | |
border-bottom: 1px solid var(--border-color); | |
} | |
.slots-list { | |
display: flex; | |
padding: 10px; | |
overflow-x: auto; | |
overflow-y: hidden; | |
} | |
.slot-item { | |
width: 200px; | |
flex-shrink: 0; | |
margin-right: 10px; | |
margin-bottom: 0; | |
} | |
} | |
@media (max-width: 768px) { | |
.canvas-container { | |
min-height: 350px; | |
} | |
.canvas-area { | |
min-height: 400px; | |
} | |
.flashcard, .label, .translation { | |
width: 150px; | |
} | |
.flashcard { | |
height: 150px; | |
} | |
.label { | |
font-size: 20px; | |
} | |
.translation { | |
font-size: 18px; | |
} | |
.flashcard-left { | |
transform: translate(calc(-100% - 15px), -50%); | |
} | |
.flashcard-right { | |
transform: translate(15px, -50%); | |
} | |
.label-left { | |
transform: translate(15px, -50%); | |
} | |
.label-right { | |
transform: translate(calc(-100% - 15px), -50%); | |
} | |
.app-title { | |
font-size: 16px; | |
} | |
.btn { | |
padding: 6px 10px; | |
font-size: 13px; | |
} | |
} | |
@media (max-width: 576px) { | |
.canvas-container { | |
min-height: 300px; | |
} | |
.canvas-area { | |
min-height: 350px; | |
padding: 20px 10px; | |
} | |
.flashcard, .label, .translation { | |
width: 130px; | |
} | |
.flashcard { | |
height: 130px; | |
} | |
.label { | |
font-size: 18px; | |
padding: 8px 4px; | |
} | |
.translation { | |
font-size: 16px; | |
} | |
.flashcard-left { | |
transform: translate(calc(-100% - 10px), -50%); | |
} | |
.flashcard-right { | |
transform: translate(10px, -50%); | |
} | |
.label-left { | |
transform: translate(10px, -50%); | |
} | |
.label-right { | |
transform: translate(calc(-100% - 10px), -50%); | |
} | |
.style-selector { | |
bottom: 5px; | |
padding: 5px; | |
} | |
.style-option { | |
padding: 5px 10px; | |
font-size: 12px; | |
border-radius: 15px; | |
} | |
.slots-panel { | |
height: 160px; | |
} | |
.slot-item { | |
width: 170px; | |
} | |
} | |
/* Animation Styles */ | |
.anim-slideIn { | |
animation: slideIn 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; | |
} | |
.anim-fadeGrow { | |
animation: fadeGrow 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; | |
} | |
.anim-flip { | |
animation: flip 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; | |
} | |
.anim-bounce { | |
animation: bounce 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; | |
} | |
.anim-rotate { | |
animation: rotate 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; | |
} | |
@keyframes slideIn { | |
from { transform: translateX(-40px); opacity: 0; } | |
to { transform: translateX(0); opacity: 1; } | |
} | |
@keyframes fadeGrow { | |
from { transform: scale(0.8); opacity: 0; } | |
to { transform: scale(1); opacity: 1; } | |
} | |
@keyframes flip { | |
from { transform: rotateY(90deg); opacity: 0; } | |
to { transform: rotateY(0); opacity: 1; } | |
} | |
@keyframes bounce { | |
0% { transform: translateY(30px); opacity: 0; } | |
50% { transform: translateY(-10px); opacity: 1; } | |
100% { transform: translateY(0); opacity: 1; } | |
} | |
@keyframes rotate { | |
from { transform: rotate(-90deg) scale(0.8); opacity: 0; } | |
to { transform: rotate(0) scale(1); opacity: 1; } | |
} | |
/* Animation delay classes */ | |
.delay-0 { animation-delay: 0s; } | |
.delay-1 { animation-delay: 0.2s; } | |
.delay-2 { animation-delay: 0.4s; } | |
.delay-3 { animation-delay: 0.6s; } | |
/* Controls */ | |
.animation-controls { | |
position: absolute; | |
bottom: 20px; | |
left: 50%; | |
transform: translateX(-50%); | |
background-color: rgba(0, 0, 0, 0.6); | |
border-radius: 30px; | |
padding: 8px; | |
display: flex; | |
gap: 8px; | |
z-index: 20; | |
-webkit-backdrop-filter: blur(10px); | |
backdrop-filter: blur(10px); | |
} | |
.property-editor { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
width: 280px; | |
background-color: white; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
overflow: hidden; | |
z-index: 10; | |
transform: translateZ(0); | |
opacity: 0; | |
pointer-events: none; | |
transition: opacity 0.3s, transform 0.3s; | |
} | |
.property-editor.active { | |
opacity: 1; | |
pointer-events: all; | |
transform: translateZ(0) translateY(0); | |
} | |
.property-header { | |
padding: 12px 15px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
border-bottom: 1px solid var(--border-color); | |
background-color: var(--card-bg); | |
} | |
.property-title { | |
font-weight: 600; | |
font-size: 14px; | |
} | |
.property-close { | |
width: 24px; | |
height: 24px; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
border-radius: 12px; | |
cursor: pointer; | |
color: var(--text-secondary); | |
} | |
.property-close:hover { | |
background-color: var(--border-color); | |
} | |
.property-content { | |
padding: 15px; | |
max-height: 400px; | |
overflow-y: auto; | |
-webkit-overflow-scrolling: touch; | |
} | |
.property-group { | |
margin-bottom: 15px; | |
} | |
.property-label { | |
font-size: 12px; | |
font-weight: 500; | |
margin-bottom: 6px; | |
color: var(--text-secondary); | |
} | |
.property-field { | |
width: 100%; | |
padding: 8px 10px; | |
border: 1px solid var(--border-color); | |
border-radius: 4px; | |
font-size: 14px; | |
transition: border-color 0.2s; | |
} | |
.property-field:focus { | |
border-color: var(--primary-color); | |
outline: none; | |
} | |
.image-picker { | |
border: 2px dashed var(--border-color); | |
padding: 15px; | |
text-align: center; | |
cursor: pointer; | |
border-radius: 4px; | |
transition: all 0.2s; | |
} | |
.image-picker:hover { | |
border-color: var(--primary-color); | |
background-color: var(--primary-light); | |
} | |
.image-preview { | |
margin-top: 10px; | |
width: 100%; | |
height: 120px; | |
border-radius: 4px; | |
object-fit: cover; | |
} | |
/* Preview Mode */ | |
.preview-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.9); | |
z-index: 1000; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
opacity: 0; | |
visibility: hidden; | |
transition: opacity 0.3s, visibility 0.3s; | |
} | |
.preview-overlay.active { | |
opacity: 1; | |
visibility: visible; | |
} | |
.preview-container { | |
width: 80%; | |
height: 80%; | |
max-width: 1200px; | |
max-height: 800px; | |
background: linear-gradient(135deg, #2196F3, #42a5f5); | |
border-radius: 12px; | |
overflow: hidden; | |
position: relative; | |
transform: scale(0.9); | |
opacity: 0; | |
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s; | |
} | |
.preview-overlay.active .preview-container { | |
transform: scale(1); | |
opacity: 1; | |
} | |
.preview-controls { | |
position: absolute; | |
bottom: 30px; | |
left: 50%; | |
transform: translateX(-50%); | |
background-color: rgba(0, 0, 0, 0.6); | |
border-radius: 30px; | |
padding: 10px; | |
display: flex; | |
gap: 15px; | |
z-index: 10; | |
-webkit-backdrop-filter: blur(10px); | |
backdrop-filter: blur(10px); | |
} | |
.preview-control { | |
width: 40px; | |
height: 40px; | |
border-radius: 50%; | |
background-color: rgba(255, 255, 255, 0.2); | |
color: white; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
cursor: pointer; | |
transition: background-color 0.2s; | |
font-size: 16px; | |
} | |
.preview-control:hover { | |
background-color: rgba(255, 255, 255, 0.3); | |
} | |
.preview-close { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
width: 40px; | |
height: 40px; | |
border-radius: 50%; | |
background-color: rgba(0, 0, 0, 0.5); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
cursor: pointer; | |
z-index: 10; | |
color: white; | |
-webkit-backdrop-filter: blur(5px); | |
backdrop-filter: blur(5px); | |
} | |
.preview-progress { | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
height: 4px; | |
background-color: var(--primary-dark); | |
} | |
/* Blur Transition */ | |
.blur-transition { | |
animation: blurTransition 1.5s ease forwards; | |
} | |
@keyframes blurTransition { | |
0% { filter: blur(0); opacity: 1; } | |
40% { filter: blur(10px); opacity: 0; } | |
60% { filter: blur(10px); opacity: 0; } | |
100% { filter: blur(0); opacity: 1; } | |
} | |
/* Buttons */ | |
.btn { | |
padding: 8px 12px; | |
border-radius: 6px; | |
border: none; | |
font-weight: 500; | |
font-size: 14px; | |
cursor: pointer; | |
display: inline-flex; | |
align-items: center; | |
justify-content: center; | |
gap: 6px; | |
transition: all 0.2s; | |
} | |
.btn-primary { | |
background-color: var(--primary-color); | |
color: white; | |
} | |
.btn-primary:hover { | |
background-color: var(--primary-dark); | |
} | |
.btn-outline { | |
background-color: transparent; | |
border: 1px solid var(--border-color); | |
color: var(--text-color); | |
} | |
.btn-outline:hover { | |
background-color: var(--card-bg); | |
} | |
.btn-sm { | |
padding: 4px 8px; | |
font-size: 12px; | |
} | |
.btn:active { | |
transform: scale(0.98); | |
} | |
/* Layout Templates */ | |
.layout-templates { | |
display: flex; | |
gap: 10px; | |
margin-top: 10px; | |
flex-wrap: wrap; | |
} | |
.layout-template { | |
width: 60px; | |
height: 40px; | |
border: 1px solid var(--border-color); | |
border-radius: 4px; | |
cursor: pointer; | |
overflow: hidden; | |
position: relative; | |
} | |
.layout-template:hover { | |
border-color: var(--primary-color); | |
} | |
.layout-template.active { | |
border-color: var(--primary-color); | |
box-shadow: 0 0 0 2px var(--primary-light); | |
} | |
.layout-preview { | |
width: 100%; | |
height: 100%; | |
display: flex; | |
flex-wrap: wrap; | |
padding: 2px; | |
} | |
.layout-item { | |
background-color: var(--primary-light); | |
border-radius: 2px; | |
} | |
/* Template 1: 2-2 Split */ | |
.layout-t1 .layout-item:nth-child(1) { | |
width: 45%; | |
height: 100%; | |
} | |
.layout-t1 .layout-item:nth-child(2) { | |
width: 45%; | |
height: 100%; | |
margin-left: 5%; | |
} | |
/* Template 2: L Shape */ | |
.layout-t2 .layout-item:nth-child(1) { | |
width: 60%; | |
height: 100%; | |
} | |
.layout-t2 .layout-item:nth-child(2) { | |
width: 35%; | |
height: 45%; | |
margin-left: 5%; | |
} | |
.layout-t2 .layout-item:nth-child(3) { | |
width: 35%; | |
height: 45%; | |
margin-left: 5%; | |
margin-top: 5%; | |
} | |
/* Template 3: Z Pattern */ | |
.layout-t3 .layout-item:nth-child(1) { | |
width: 45%; | |
height: 45%; | |
} | |
.layout-t3 .layout-item:nth-child(2) { | |
width: 45%; | |
height: 45%; | |
margin-left: 50%; | |
} | |
.layout-t3 .layout-item:nth-child(3) { | |
width: 45%; | |
height: 45%; | |
margin-top: 5%; | |
} | |
.layout-t3 .layout-item:nth-child(4) { | |
width: 45%; | |
height: 45%; | |
margin-left: 50%; | |
margin-top: 5%; | |
} | |
/* Draggable Elements */ | |
.draggable { | |
cursor: move; | |
user-select: none; | |
} | |
.draggable:hover { | |
box-shadow: 0 0 0 2px var(--primary-color); | |
} | |
.dragging { | |
opacity: 0.8; | |
z-index: 100 ; | |
} | |
/* Timing Controls */ | |
.timing-controls { | |
margin-top: 15px; | |
padding-top: 15px; | |
border-top: 1px solid var(--border-color); | |
} | |
.timing-slider { | |
width: 100%; | |
margin: 8px 0; | |
} | |
.timing-value { | |
font-size: 12px; | |
color: var(--text-secondary); | |
text-align: right; | |
} | |
/* Scene Duration Badge */ | |
.duration-badge { | |
display: inline-block; | |
padding: 2px 6px; | |
background-color: var(--primary-light); | |
color: var(--primary-dark); | |
border-radius: 4px; | |
font-size: 10px; | |
font-weight: 500; | |
margin-left: 8px; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="app-container"> | |
<!-- Header --> | |
<header class="app-header"> | |
<div class="app-title">Flashcard Animation Studio</div> | |
<div class="app-actions"> | |
<button class="btn btn-outline" id="saveBtn"> | |
<i class="fas fa-save"></i> Save | |
</button> | |
<button class="btn btn-primary" id="previewBtn"> | |
<i class="fas fa-play"></i> Preview | |
</button> | |
</div> | |
</header> | |
<!-- Main Content --> | |
<div class="main-content"> | |
<!-- Slots Panel --> | |
<div class="slots-panel"> | |
<div class="slots-header"> | |
<div class="slots-title">Animation Slots</div> | |
<div class="slot-action" id="addSlotBtn"> | |
<i class="fas fa-plus"></i> | |
</div> | |
</div> | |
<div class="slots-list" id="slotsList"> | |
<!-- Slots will be dynamically added here --> | |
</div> | |
</div> | |
<!-- Canvas Area --> | |
<div class="canvas-area"> | |
<div class="canvas-container" id="canvas"> | |
<!-- Default content --> | |
<div class="flashcard flashcard-left" id="leftCard"> | |
<div class="card-content"> | |
<!-- SVG or image will be injected dynamically --> | |
</div> | |
</div> | |
<div class="label-container label-left" id="leftLabel"> | |
<div class="label">Kitty</div> | |
<div class="translation">mountain</div> | |
</div> | |
<div class="flashcard flashcard-right" id="rightCard"> | |
<div class="card-content"> | |
<!-- SVG or image will be injected dynamically --> | |
</div> | |
</div> | |
<div class="label-container label-right" id="rightLabel"> | |
<div class="label">playa</div> | |
<div class="translation">beach</div> | |
</div> | |
</div> | |
<!-- Animation Style Selector --> | |
<div class="style-selector" id="styleSelector"> | |
<div class="style-option active" data-style="slideIn">Slide In</div> | |
<div class="style-option" data-style="fadeGrow">Fade & Grow</div> | |
<div class="style-option" data-style="flip">Flip</div> | |
<div class="style-option" data-style="bounce">Bounce</div> | |
<div class="style-option" data-style="rotate">Rotate In</div> | |
</div> | |
</div> | |
</div> | |
<!-- Property Editor --> | |
<div class="property-editor" id="propertyEditor"> | |
<div class="property-header"> | |
<div class="property-title">Edit Element</div> | |
<div class="property-close" id="closePropertyEditor"> | |
<i class="fas fa-times"></i> | |
</div> | |
</div> | |
<div class="property-content"> | |
<!-- Text Element Properties --> | |
<div id="textProperties"> | |
<div class="property-group"> | |
<div class="property-label">Text Content</div> | |
<input type="text" class="property-field" id="textContent"> | |
</div> | |
<div class="property-group"> | |
<div class="property-label">Font Size</div> | |
<input type="range" class="property-field" id="fontSize" min="12" max="48" step="1"> | |
</div> | |
<div class="property-group"> | |
<div class="property-label">Text Color</div> | |
<input type="color" class="property-field" id="textColor"> | |
</div> | |
<div class="property-group"> | |
<div class="property-label">Background Color</div> | |
<input type="color" class="property-field" id="textBgColor"> | |
</div> | |
</div> | |
<!-- Image Element Properties --> | |
<div id="imageProperties"> | |
<div class="property-group"> | |
<div class="property-label">Image</div> | |
<div class="image-picker" id="imagePicker"> | |
<i class="fas fa-upload"></i> | |
<p>Click to upload image</p> | |
<input type="file" id="imageUpload" accept="image/*" style="display: none;"> | |
</div> | |
<img id="currentImage" class="image-preview"> | |
</div> | |
<div class="property-group"> | |
<div class="property-label">Opacity</div> | |
<input type="range" class="property-field" id="imageOpacity" min="0" max="1" step="0.1" value="1"> | |
</div> | |
</div> | |
<!-- Common Properties --> | |
<div class="property-group"> | |
<div class="property-label">Position</div> | |
<div style="display: flex; gap: 10px;"> | |
<div style="flex: 1;"> | |
<small>X</small> | |
<input type="number" class="property-field" id="positionX" style="width: 100%;"> | |
</div> | |
<div style="flex: 1;"> | |
<small>Y</small> | |
<input type="number" class="property-field" id="positionY" style="width: 100%;"> | |
</div> | |
</div> | |
</div> | |
<div class="property-group"> | |
<div class="property-label">Size</div> | |
<div style="display: flex; gap: 10px;"> | |
<div style="flex: 1;"> | |
<small>Width</small> | |
<input type="number" class="property-field" id="elementWidth" style="width: 100%;"> | |
</div> | |
<div style="flex: 1;"> | |
<small>Height</small> | |
<input type="number" class="property-field" id="elementHeight" style="width: 100%;"> | |
</div> | |
</div> | |
</div> | |
<div class="property-group"> | |
<div class="property-label">Animation Delay</div> | |
<select class="property-field" id="animationDelay"> | |
<option value="0">No delay</option> | |
<option value="1">Short (0.2s)</option> | |
<option value="2">Medium (0.4s)</option> | |
<option value="3">Long (0.6s)</option> | |
</select> | |
</div> | |
<!-- Timing Controls --> | |
<div class="timing-controls"> | |
<div class="property-group"> | |
<div class="property-label">Animation Duration (seconds)</div> | |
<input type="range" class="timing-slider property-field" id="animationDuration" min="0.5" max="5" step="0.1" value="0.8"> | |
<div class="timing-value" id="animationDurationValue">0.8s</div> | |
</div> | |
<div class="property-group"> | |
<div class="property-label">Scene Duration (seconds)</div> | |
<input type="range" class="timing-slider property-field" id="sceneDuration" min="1" max="10" step="0.5" value="3"> | |
<div class="timing-value" id="sceneDurationValue">3s</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Preview Mode --> | |
<div class="preview-overlay" id="previewOverlay"> | |
<div class="preview-container" id="previewContainer"> | |
<!-- Content will be dynamically generated here --> | |
<div class="preview-close" id="closePreviewBtn"> | |
<i class="fas fa-times"></i> | |
</div> | |
<div class="preview-controls"> | |
<div class="preview-control" id="prevSlotBtn"> | |
<i class="fas fa-step-backward"></i> | |
</div> | |
<div class="preview-control" id="playPauseBtn"> | |
<i class="fas fa-pause"></i> | |
</div> | |
<div class="preview-control" id="nextSlotBtn"> | |
<i class="fas fa-step-forward"></i> | |
</div> | |
</div> | |
<div class="preview-progress" id="previewProgress"></div> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// App State | |
const appState = { | |
slots: [], | |
currentSlotId: null, | |
activeElement: null, | |
previewMode: false, | |
previewCurrentSlot: 0, | |
previewPlaying: false, | |
previewTimer: null, | |
previewDuration: 3000, // 3 seconds per slot | |
defaultVisible: true // Flag to control default content visibility | |
}; | |
// DOM Elements | |
const elements = { | |
canvas: document.getElementById('canvas'), | |
slotsList: document.getElementById('slotsList'), | |
addSlotBtn: document.getElementById('addSlotBtn'), | |
saveBtn: document.getElementById('saveBtn'), | |
previewBtn: document.getElementById('previewBtn'), | |
styleSelector: document.getElementById('styleSelector'), | |
previewOverlay: document.getElementById('previewOverlay'), | |
leftCard: document.getElementById('leftCard'), | |
rightCard: document.getElementById('rightCard'), | |
leftLabel: document.getElementById('leftLabel'), | |
rightLabel: document.getElementById('rightLabel'), | |
propertyEditor: document.getElementById('propertyEditor'), | |
closePropertyEditor: document.getElementById('closePropertyEditor'), | |
textProperties: document.getElementById('textProperties'), | |
imageProperties: document.getElementById('imageProperties'), | |
textContent: document.getElementById('textContent'), | |
fontSize: document.getElementById('fontSize'), | |
textColor: document.getElementById('textColor'), | |
textBgColor: document.getElementById('textBgColor'), | |
imagePicker: document.getElementById('imagePicker'), | |
imageUpload: document.getElementById('imageUpload'), | |
currentImage: document.getElementById('currentImage'), | |
imageOpacity: document.getElementById('imageOpacity'), | |
positionX: document.getElementById('positionX'), | |
positionY: document.getElementById('positionY'), | |
elementWidth: document.getElementById('elementWidth'), | |
elementHeight: document.getElementById('elementHeight'), | |
animationDelay: document.getElementById('animationDelay'), | |
styleOptions: document.querySelectorAll('.style-option'), | |
previewContainer: document.getElementById('previewContainer'), | |
closePreviewBtn: document.getElementById('closePreviewBtn'), | |
playPauseBtn: document.getElementById('playPauseBtn'), | |
prevSlotBtn: document.getElementById('prevSlotBtn'), | |
nextSlotBtn: document.getElementById('nextSlotBtn'), | |
previewProgress: document.getElementById('previewProgress') | |
}; | |
// SVG Content | |
const svgContent = { | |
mountain: ` | |
<svg viewBox="0 0 220 220" xmlns="http://www.w3.org/2000/svg"> | |
<defs> | |
<linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="100%"> | |
<stop offset="0%" stop-color="#7EC2F3" /> | |
<stop offset="100%" stop-color="#B3E5FC" /> | |
</linearGradient> | |
<linearGradient id="mountainGradient1" x1="0%" y1="0%" x2="100%" y2="100%"> | |
<stop offset="0%" stop-color="#78909C" /> | |
<stop offset="100%" stop-color="#B0BEC5" /> | |
</linearGradient> | |
<linearGradient id="mountainGradient2" x1="0%" y1="0%" x2="100%" y2="100%"> | |
<stop offset="0%" stop-color="#607D8B" /> | |
<stop offset="100%" stop-color="#90A4AE" /> | |
</linearGradient> | |
</defs> | |
<!-- Sky background --> | |
<rect x="0" y="0" width="220" height="220" fill="url(#skyGradient)" /> | |
<!-- Mountains --> | |
<polygon points="0,110 70,40 140,95 220,70 220,125" fill="url(#mountainGradient1)" /> | |
<polygon points="0,125 35,80 105,55 175,90 220,75 220,125" fill="url(#mountainGradient2)" /> | |
<!-- Green hills --> | |
<rect x="0" y="125" width="220" height="95" fill="#8BC34A" /> | |
<!-- Trees with detail --> | |
<g> | |
<polygon points="55,125 65,125 60,90" fill="#33691E" /> | |
<polygon points="80,125 95,125 87.5,80" fill="#33691E" /> | |
<polygon points="110,125 125,125 117.5,95" fill="#33691E" /> | |
<polygon points="40,125 50,125 45,100" fill="#33691E" /> | |
</g> | |
</svg> | |
`, | |
beach: ` | |
<svg viewBox="0 0 220 220" xmlns="http://www.w3.org/2000/svg"> | |
<defs> | |
<linearGradient id="skyGradient2" x1="0%" y1="0%" x2="0%" y2="100%"> | |
<stop offset="0%" stop-color="#7EC2F3" /> | |
<stop offset="100%" stop-color="#B3E5FC" /> | |
</linearGradient> | |
<linearGradient id="oceanGradient" x1="0%" y1="0%" x2="0%" y2="100%"> | |
<stop offset="0%" stop-color="#29B6F6" /> | |
<stop offset="100%" stop-color="#0277BD" /> | |
</linearGradient> | |
</defs> | |
<!-- Sky background --> | |
<rect x="0" y="0" width="220" height="220" fill="url(#skyGradient2)" /> | |
<!-- Sand --> | |
<rect x="0" y="95" width="220" height="125" fill="#FFCC80" /> | |
<!-- Ocean --> | |
<path d="M0,130 C55,117 165,124 220,110 L220,220 L0,220 Z" fill="url(#oceanGradient)" /> | |
<path d="M0,130 C55,117 165,124 220,110" stroke="white" stroke-width="2" fill="none" /> | |
<!-- Beach Items --> | |
<g> | |
<!-- Umbrella --> | |
<line x1="120" y1="145" x2="120" y2="115" stroke="#8D6E63" stroke-width="2" /> | |
<path d="M105,115 A15,5 0 0 0 135,115" fill="#EF5350" /> | |
<!-- Towels --> | |
<rect x="85" y="160" width="20" height="14" fill="#FDD835" /> | |
<rect x="140" y="155" width="20" height="14" fill="#42A5F6" /> | |
<rect x="170" y="165" width="20" height="14" fill="#EF5350" /> | |
</g> | |
</svg> | |
` | |
}; | |
// Initialize the application | |
function init() { | |
// Set SVG content for cards | |
if (elements.leftCard) { | |
elements.leftCard.querySelector('.card-content').innerHTML = svgContent.mountain; | |
} | |
if (elements.rightCard) { | |
elements.rightCard.querySelector('.card-content').innerHTML = svgContent.beach; | |
} | |
// Load data from storage | |
loadFromStorage(); | |
// Event listeners | |
setupEventListeners(); | |
// Render initial slots | |
renderSlotsList(); | |
// If no slots, create a default one | |
if (appState.slots.length === 0) { | |
addNewSlot(); | |
} | |
// Select first slot | |
if (appState.slots.length > 0 && !appState.currentSlotId) { | |
selectSlot(appState.slots[0].id); | |
} | |
// Apply initial animation | |
resetAndAnimateDefaultContent('slideIn'); | |
} | |
// Setup event listeners | |
function setupEventListeners() { | |
// Add new slot | |
if (elements.addSlotBtn) { | |
elements.addSlotBtn.addEventListener('click', addNewSlot); | |
} | |
// Save button | |
if (elements.saveBtn) { | |
elements.saveBtn.addEventListener('click', function() { | |
saveToStorage(); | |
showMessage('Project saved successfully!'); | |
}); | |
} | |
// Preview button | |
if (elements.previewBtn) { | |
elements.previewBtn.addEventListener('click', togglePreview); | |
} | |
// Style selector | |
if (elements.styleSelector) { | |
elements.styleSelector.addEventListener('click', function(e) { | |
if (e.target.classList.contains('style-option')) { | |
const style = e.target.dataset.style; | |
selectAnimationStyle(style); | |
resetAndAnimateDefaultContent(style); | |
} | |
}); | |
} | |
// Close property editor | |
if (elements.closePropertyEditor) { | |
elements.closePropertyEditor.addEventListener('click', closePropertyEditor); | |
} | |
// Preview controls | |
if (elements.closePreviewBtn) { | |
elements.closePreviewBtn.addEventListener('click', closePreview); | |
} | |
if (elements.playPauseBtn) { | |
elements.playPauseBtn.addEventListener('click', togglePlayPause); | |
} | |
if (elements.prevSlotBtn) { | |
elements.prevSlotBtn.addEventListener('click', goToPreviousSlot); | |
} | |
if (elements.nextSlotBtn) { | |
elements.nextSlotBtn.addEventListener('click', goToNextSlot); | |
} | |
// Make canvas elements clickable for editing | |
if (elements.canvas) { | |
elements.canvas.addEventListener('click', function(e) { | |
const clickableElements = [ | |
elements.leftCard, | |
elements.rightCard, | |
elements.leftLabel, | |
elements.rightLabel | |
]; | |
for (const el of clickableElements) { | |
if (el && el.contains(e.target) || el === e.target) { | |
openPropertyEditor(el); | |
break; | |
} | |
} | |
}); | |
} | |
// Slots management - delegate to slotsList | |
if (elements.slotsList) { | |
elements.slotsList.addEventListener('click', function(e) { | |
// Handle delete button click | |
if (e.target.closest('.slot-delete')) { | |
const slotItem = e.target.closest('.slot-item'); | |
if (slotItem && slotItem.dataset.id) { | |
deleteSlot(slotItem.dataset.id); | |
e.stopPropagation(); | |
return; | |
} | |
} | |
// Handle slot selection | |
const slotItem = e.target.closest('.slot-item'); | |
if (slotItem && slotItem.dataset.id) { | |
selectSlot(slotItem.dataset.id); | |
} | |
}); | |
} | |
// Setup draggable elements | |
setupDraggableElements(); | |
// Setup timing control listeners | |
setupTimingControls(); | |
} | |
// Setup draggable elements | |
function setupDraggableElements() { | |
const draggableElements = [ | |
elements.leftCard, | |
elements.rightCard, | |
elements.leftLabel, | |
elements.rightLabel | |
]; | |
draggableElements.forEach(element => { | |
if (!element) return; | |
// Add draggable class | |
element.classList.add('draggable'); | |
// Variables to track dragging | |
let isDragging = false; | |
let startX, startY; | |
let startLeft, startTop; | |
// Mouse down event | |
element.addEventListener('mousedown', function(e) { | |
// Only handle left mouse button | |
if (e.button !== 0) return; | |
isDragging = true; | |
element.classList.add('dragging'); | |
// Get initial positions | |
startX = e.clientX; | |
startY = e.clientY; | |
// Get element's current position | |
const rect = element.getBoundingClientRect(); | |
startLeft = rect.left; | |
startTop = rect.top; | |
// Prevent default behavior | |
e.preventDefault(); | |
}); | |
// Mouse move event (on document to capture all movements) | |
document.addEventListener('mousemove', function(e) { | |
if (!isDragging) return; | |
// Calculate new position | |
const deltaX = e.clientX - startX; | |
const deltaY = e.clientY - startY; | |
// Update element position | |
const canvasRect = elements.canvas.getBoundingClientRect(); | |
const newLeft = (startLeft - canvasRect.left + deltaX) / canvasRect.width * 100; | |
const newTop = (startTop - canvasRect.top + deltaY) / canvasRect.height * 100; | |
// Apply new position | |
element.style.left = `${newLeft}%`; | |
element.style.top = `${newTop}%`; | |
// Remove fixed positioning classes | |
if (element === elements.leftCard) { | |
element.classList.remove('flashcard-left'); | |
} else if (element === elements.rightCard) { | |
element.classList.remove('flashcard-right'); | |
} else if (element === elements.leftLabel) { | |
element.classList.remove('label-left'); | |
} else if (element === elements.rightLabel) { | |
element.classList.remove('label-right'); | |
} | |
// Update position in property editor if open | |
if (appState.activeElement === element) { | |
elements.positionX.value = Math.round(newLeft); | |
elements.positionY.value = Math.round(newTop); | |
} | |
// Save position to current slot | |
saveElementPosition(element, newLeft, newTop); | |
}); | |
// Mouse up event | |
document.addEventListener('mouseup', function() { | |
if (!isDragging) return; | |
isDragging = false; | |
element.classList.remove('dragging'); | |
// Save to storage | |
saveToStorage(); | |
}); | |
}); | |
} | |
// Save element position to current slot | |
function saveElementPosition(element, left, top) { | |
if (!appState.currentSlotId) return; | |
const slot = findSlotById(appState.currentSlotId); | |
if (!slot) return; | |
// Initialize elements array if needed | |
if (!slot.elements) { | |
slot.elements = []; | |
} | |
// Find element in slot or create new entry | |
let elementData = slot.elements.find(el => el.id === element.id); | |
if (!elementData) { | |
elementData = { id: element.id }; | |
slot.elements.push(elementData); | |
} | |
// Update position | |
elementData.left = left; | |
elementData.top = top; | |
} | |
// Setup timing control listeners | |
function setupTimingControls() { | |
// Animation duration slider | |
if (elements.animationDuration) { | |
elements.animationDuration.addEventListener('input', function() { | |
const value = parseFloat(this.value); | |
elements.animationDurationValue.textContent = `${value}s`; | |
// Update current slot | |
if (appState.currentSlotId) { | |
const slot = findSlotById(appState.currentSlotId); | |
if (slot) { | |
slot.animationDuration = value; | |
// Update animation duration for all elements | |
updateAnimationDuration(value); | |
// Save to storage | |
saveToStorage(); | |
} | |
} | |
}); | |
} | |
// Scene duration slider | |
if (elements.sceneDuration) { | |
elements.sceneDuration.addEventListener('input', function() { | |
const value = parseFloat(this.value); | |
elements.sceneDurationValue.textContent = `${value}s`; | |
// Update current slot | |
if (appState.currentSlotId) { | |
const slot = findSlotById(appState.currentSlotId); | |
if (slot) { | |
slot.duration = value; | |
// Update slots list to show new duration | |
renderSlotsList(); | |
// Save to storage | |
saveToStorage(); | |
} | |
} | |
}); | |
} | |
// Setup text content update | |
setupTextContentListeners(); | |
// Setup position and size update | |
setupPositionSizeListeners(); | |
} | |
// Setup text content listeners | |
function setupTextContentListeners() { | |
// Text content field | |
if (elements.textContent) { | |
elements.textContent.addEventListener('input', function() { | |
updateElementText(); | |
}); | |
elements.textContent.addEventListener('change', function() { | |
updateElementText(); | |
saveToStorage(); | |
}); | |
} | |
// Text color field | |
if (elements.textColor) { | |
elements.textColor.addEventListener('input', function() { | |
updateElementStyle('color', this.value); | |
}); | |
elements.textColor.addEventListener('change', function() { | |
updateElementStyle('color', this.value); | |
saveToStorage(); | |
}); | |
} | |
// Background color field | |
if (elements.textBgColor) { | |
elements.textBgColor.addEventListener('input', function() { | |
updateElementStyle('backgroundColor', this.value); | |
}); | |
elements.textBgColor.addEventListener('change', function() { | |
updateElementStyle('backgroundColor', this.value); | |
saveToStorage(); | |
}); | |
} | |
// Font size field | |
if (elements.fontSize) { | |
elements.fontSize.addEventListener('input', function() { | |
updateElementStyle('fontSize', `${this.value}px`); | |
}); | |
elements.fontSize.addEventListener('change', function() { | |
updateElementStyle('fontSize', `${this.value}px`); | |
saveToStorage(); | |
}); | |
} | |
} | |
// Setup position and size listeners | |
function setupPositionSizeListeners() { | |
// Position X field | |
if (elements.positionX) { | |
elements.positionX.addEventListener('input', function() { | |
updateElementPosition(); | |
}); | |
elements.positionX.addEventListener('change', function() { | |
updateElementPosition(); | |
saveToStorage(); | |
}); | |
} | |
// Position Y field | |
if (elements.positionY) { | |
elements.positionY.addEventListener('input', function() { | |
updateElementPosition(); | |
}); | |
elements.positionY.addEventListener('change', function() { | |
updateElementPosition(); | |
saveToStorage(); | |
}); | |
} | |
// Width field | |
if (elements.elementWidth) { | |
elements.elementWidth.addEventListener('input', function() { | |
updateElementSize(); | |
}); | |
elements.elementWidth.addEventListener('change', function() { | |
updateElementSize(); | |
saveToStorage(); | |
}); | |
} | |
// Height field | |
if (elements.elementHeight) { | |
elements.elementHeight.addEventListener('input', function() { | |
updateElementSize(); | |
}); | |
elements.elementHeight.addEventListener('change', function() { | |
updateElementSize(); | |
saveToStorage(); | |
}); | |
} | |
} | |
// Update element text content | |
function updateElementText() { | |
if (!appState.activeElement) return; | |
const textValue = elements.textContent.value; | |
// Check if it's a label container | |
if (appState.activeElement.classList.contains('label-container')) { | |
// Update the label text | |
const labelEl = appState.activeElement.querySelector('.label'); | |
if (labelEl) { | |
labelEl.textContent = textValue; | |
// Save text to slot data | |
saveElementText(appState.activeElement.id, 'label', textValue); | |
} | |
} else if (appState.activeElement.classList.contains('flashcard')) { | |
// For flashcards, we might want to update a title or caption | |
// This is a placeholder for future functionality | |
console.log('Updating flashcard text:', textValue); | |
} | |
} | |
// Update element style | |
function updateElementStyle(property, value) { | |
if (!appState.activeElement) return; | |
// For label containers, update the label element | |
if (appState.activeElement.classList.contains('label-container')) { | |
const labelEl = appState.activeElement.querySelector('.label'); | |
if (labelEl) { | |
labelEl.style[property] = value; | |
// Save style to slot data | |
saveElementStyle(appState.activeElement.id, property, value); | |
} | |
} else if (appState.activeElement.classList.contains('flashcard')) { | |
// For flashcards, update the card itself | |
appState.activeElement.style[property] = value; | |
// Save style to slot data | |
saveElementStyle(appState.activeElement.id, property, value); | |
} | |
} | |
// Update element position | |
function updateElementPosition() { | |
if (!appState.activeElement) return; | |
const x = parseFloat(elements.positionX.value); | |
const y = parseFloat(elements.positionY.value); | |
if (!isNaN(x) && !isNaN(y)) { | |
appState.activeElement.style.left = `${x}%`; | |
appState.activeElement.style.top = `${y}%`; | |
// Save position to current slot | |
saveElementPosition(appState.activeElement, x, y); | |
} | |
} | |
// Update element size | |
function updateElementSize() { | |
if (!appState.activeElement) return; | |
const width = parseFloat(elements.elementWidth.value); | |
const height = parseFloat(elements.elementHeight.value); | |
if (!isNaN(width)) { | |
appState.activeElement.style.width = `${width}px`; | |
// Save width to slot data | |
saveElementSize(appState.activeElement.id, 'width', width); | |
} | |
if (!isNaN(height)) { | |
appState.activeElement.style.height = `${height}px`; | |
// Save height to slot data | |
saveElementSize(appState.activeElement.id, 'height', height); | |
} | |
} | |
// Save element text to current slot | |
function saveElementText(elementId, textType, value) { | |
if (!appState.currentSlotId) return; | |
const slot = findSlotById(appState.currentSlotId); | |
if (!slot) return; | |
// Initialize elements array if needed | |
if (!slot.elements) { | |
slot.elements = []; | |
} | |
// Find element in slot or create new entry | |
let elementData = slot.elements.find(el => el.id === elementId); | |
if (!elementData) { | |
elementData = { id: elementId }; | |
slot.elements.push(elementData); | |
} | |
// Initialize textContent object if needed | |
if (!elementData.textContent) { | |
elementData.textContent = {}; | |
} | |
// Update text content | |
elementData.textContent[textType] = value; | |
} | |
// Save element style to current slot | |
function saveElementStyle(elementId, property, value) { | |
if (!appState.currentSlotId) return; | |
const slot = findSlotById(appState.currentSlotId); | |
if (!slot) return; | |
// Initialize elements array if needed | |
if (!slot.elements) { | |
slot.elements = []; | |
} | |
// Find element in slot or create new entry | |
let elementData = slot.elements.find(el => el.id === elementId); | |
if (!elementData) { | |
elementData = { id: elementId }; | |
slot.elements.push(elementData); | |
} | |
// Initialize styles object if needed | |
if (!elementData.styles) { | |
elementData.styles = {}; | |
} | |
// Update style | |
elementData.styles[property] = value; | |
} | |
// Save element size to current slot | |
function saveElementSize(elementId, dimension, value) { | |
if (!appState.currentSlotId) return; | |
const slot = findSlotById(appState.currentSlotId); | |
if (!slot) return; | |
// Initialize elements array if needed | |
if (!slot.elements) { | |
slot.elements = []; | |
} | |
// Find element in slot or create new entry | |
let elementData = slot.elements.find(el => el.id === elementId); | |
if (!elementData) { | |
elementData = { id: elementId }; | |
slot.elements.push(elementData); | |
} | |
// Update dimension | |
elementData[dimension] = value; | |
} | |
// Update animation duration for all elements | |
function updateAnimationDuration(duration) { | |
// Update CSS animation duration for all animated elements | |
const animatedElements = document.querySelectorAll('.anim-slideIn, .anim-fadeGrow, .anim-flip, .anim-bounce, .anim-rotate'); | |
animatedElements.forEach(el => { | |
el.style.animationDuration = `${duration}s`; | |
}); | |
} | |
// Reset and animate default content with the specified style | |
function resetAndAnimateDefaultContent(style) { | |
if (!appState.defaultVisible) return; | |
const defaultElements = [ | |
elements.leftCard, | |
elements.leftLabel, | |
elements.rightCard, | |
elements.rightLabel | |
]; | |
// Reset animations and hide elements | |
defaultElements.forEach(el => { | |
if (el) { | |
// Remove existing animation classes | |
el.className = el.className.split(' ') | |
.filter(c => !c.startsWith('anim-') && !c.startsWith('delay-')) | |
.join(' '); | |
// Hide element | |
el.style.opacity = '0'; | |
el.style.visibility = 'hidden'; | |
} | |
}); | |
// Apply animations with delay | |
setTimeout(() => { | |
defaultElements.forEach((el, index) => { | |
if (el) { | |
setTimeout(() => { | |
// Show element | |
el.style.opacity = '1'; | |
el.style.visibility = 'visible'; | |
// Apply animation classes | |
el.classList.add(`anim-${style}`); | |
el.classList.add(`delay-${index}`); | |
}, index * 200); | |
} | |
}); | |
}, 100); | |
} | |
// Select animation style button | |
function selectAnimationStyle(style) { | |
const allStyleOptions = elements.styleSelector.querySelectorAll('.style-option'); | |
allStyleOptions.forEach(option => { | |
option.classList.remove('active'); | |
if (option.dataset.style === style) { | |
option.classList.add('active'); | |
} | |
}); | |
// Update current slot with new style | |
if (appState.currentSlotId) { | |
const slot = findSlotById(appState.currentSlotId); | |
if (slot) { | |
slot.animationStyle = style; | |
saveToStorage(); | |
} | |
} | |
} | |
// App initialization | |
init(); | |
// Function to toggle preview mode | |
function togglePreview() { | |
appState.previewMode = !appState.previewMode; | |
if (appState.previewMode) { | |
// Open preview | |
elements.previewOverlay.classList.add('active'); | |
startPreview(); | |
} else { | |
// Close preview | |
closePreview(); | |
} | |
} | |
// Function to close preview | |
function closePreview() { | |
elements.previewOverlay.classList.remove('active'); | |
appState.previewMode = false; | |
// Stop any running timers | |
if (appState.previewTimer) { | |
clearTimeout(appState.previewTimer); | |
appState.previewTimer = null; | |
} | |
} | |
// Function to start preview | |
function startPreview() { | |
// Reset preview state | |
appState.previewCurrentSlot = 0; | |
appState.previewPlaying = true; | |
// Clear any existing timer | |
if (appState.previewTimer) { | |
clearTimeout(appState.previewTimer); | |
} | |
// Render first slot | |
renderPreviewSlot(appState.previewCurrentSlot); | |
// Update play/pause button | |
updatePlayPauseButton(); | |
} | |
// Function to render preview slot | |
function renderPreviewSlot(index) { | |
if (index < 0 || index >= appState.slots.length) return; | |
const slot = appState.slots[index]; | |
// Clone the canvas content for preview | |
const previewContent = document.createElement('div'); | |
previewContent.className = 'canvas-container'; | |
// Clear previous content | |
elements.previewContainer.innerHTML = ''; | |
elements.previewContainer.appendChild(previewContent); | |
// Add controls back | |
const controlsHTML = ` | |
<div class="preview-close" id="closePreviewBtn"> | |
<i class="fas fa-times"></i> | |
</div> | |
<div class="preview-controls"> | |
<div class="preview-control" id="prevSlotBtn"> | |
<i class="fas fa-step-backward"></i> | |
</div> | |
<div class="preview-control" id="playPauseBtn"> | |
<i class="fas fa-${appState.previewPlaying ? 'pause' : 'play'}"></i> | |
</div> | |
<div class="preview-control" id="nextSlotBtn"> | |
<i class="fas fa-step-forward"></i> | |
</div> | |
</div> | |
<div class="preview-progress" id="previewProgress"></div> | |
`; | |
elements.previewContainer.insertAdjacentHTML('beforeend', controlsHTML); | |
// Re-attach event listeners to controls | |
document.getElementById('closePreviewBtn').addEventListener('click', closePreview); | |
document.getElementById('playPauseBtn').addEventListener('click', togglePlayPause); | |
document.getElementById('prevSlotBtn').addEventListener('click', goToPreviousSlot); | |
document.getElementById('nextSlotBtn').addEventListener('click', goToNextSlot); | |
// Get the animation style and duration | |
const style = slot.animationStyle || 'slideIn'; | |
const animationDuration = slot.animationDuration || 0.8; | |
const sceneDuration = slot.duration || 3; | |
// Create elements based on slot data | |
createPreviewElements(previewContent, slot, style, animationDuration); | |
// Start progress bar animation | |
startProgressAnimation(sceneDuration); | |
// Schedule next slot if playing | |
if (appState.previewPlaying) { | |
appState.previewTimer = setTimeout(() => { | |
if (index < appState.slots.length - 1) { | |
appState.previewCurrentSlot++; | |
renderPreviewSlot(appState.previewCurrentSlot); | |
} else { | |
// End of preview, loop back to start | |
appState.previewCurrentSlot = 0; | |
renderPreviewSlot(appState.previewCurrentSlot); | |
} | |
}, sceneDuration * 1000); | |
} | |
} | |
// Create preview elements | |
function createPreviewElements(container, slot, style, animationDuration) { | |
// Default elements if no custom positions | |
if (!slot.elements || slot.elements.length === 0) { | |
// Create default elements | |
const leftCard = document.createElement('div'); | |
leftCard.className = `flashcard flashcard-left anim-${style} delay-0`; | |
leftCard.style.animationDuration = `${animationDuration}s`; | |
leftCard.innerHTML = '<div class="card-content">' + svgContent.mountain + '</div>'; | |
const leftLabel = document.createElement('div'); | |
leftLabel.className = `label-container label-left anim-${style} delay-1`; | |
leftLabel.style.animationDuration = `${animationDuration}s`; | |
leftLabel.innerHTML = '<div class="label">Kitty</div><div class="translation">mountain</div>'; | |
const rightCard = document.createElement('div'); | |
rightCard.className = `flashcard flashcard-right anim-${style} delay-2`; | |
rightCard.style.animationDuration = `${animationDuration}s`; | |
rightCard.innerHTML = '<div class="card-content">' + svgContent.beach + '</div>'; | |
const rightLabel = document.createElement('div'); | |
rightLabel.className = `label-container label-right anim-${style} delay-3`; | |
rightLabel.style.animationDuration = `${animationDuration}s`; | |
rightLabel.innerHTML = '<div class="label">playa</div><div class="translation">beach</div>'; | |
container.appendChild(leftCard); | |
container.appendChild(leftLabel); | |
container.appendChild(rightCard); | |
container.appendChild(rightLabel); | |
} else { | |
// Create elements with custom positions | |
const elementMap = { | |
'leftCard': { | |
className: 'flashcard', | |
content: '<div class="card-content">' + svgContent.mountain + '</div>', | |
delay: 0 | |
}, | |
'rightCard': { | |
className: 'flashcard', | |
content: '<div class="card-content">' + svgContent.beach + '</div>', | |
delay: 2 | |
}, | |
'leftLabel': { | |
className: 'label-container', | |
content: '<div class="label">Kitty</div><div class="translation">mountain</div>', | |
delay: 1 | |
}, | |
'rightLabel': { | |
className: 'label-container', | |
content: '<div class="label">playa</div><div class="translation">beach</div>', | |
delay: 3 | |
} | |
}; | |
slot.elements.forEach(elementData => { | |
const template = elementMap[elementData.id]; | |
if (!template) return; | |
const element = document.createElement('div'); | |
element.className = `${template.className} anim-${style} delay-${template.delay}`; | |
element.style.animationDuration = `${animationDuration}s`; | |
// Apply custom text content if available | |
let content = template.content; | |
if (elementData.textContent && elementData.textContent.label) { | |
// For label containers, update the label text | |
if (template.className === 'label-container') { | |
const labelText = elementData.textContent.label; | |
const translationText = elementData.textContent.translation || | |
(elementData.id === 'leftLabel' ? 'mountain' : 'beach'); | |
content = `<div class="label">${labelText}</div><div class="translation">${translationText}</div>`; | |
} | |
} | |
element.innerHTML = content; | |
// Apply position | |
if (typeof elementData.left === 'number' && typeof elementData.top === 'number') { | |
element.style.position = 'absolute'; | |
element.style.left = `${elementData.left}%`; | |
element.style.top = `${elementData.top}%`; | |
} | |
// Apply custom styles if available | |
if (elementData.styles) { | |
Object.entries(elementData.styles).forEach(([property, value]) => { | |
// For label containers, apply styles to the label element | |
if (template.className === 'label-container' && | |
(property === 'color' || property === 'backgroundColor' || property === 'fontSize')) { | |
// We need to wait for the element to be in the DOM | |
setTimeout(() => { | |
const labelEl = element.querySelector('.label'); | |
if (labelEl) { | |
labelEl.style[property] = value; | |
} | |
}, 0); | |
} else { | |
element.style[property] = value; | |
} | |
}); | |
} | |
// Apply custom size if available | |
if (typeof elementData.width === 'number') { | |
element.style.width = `${elementData.width}px`; | |
} | |
if (typeof elementData.height === 'number') { | |
element.style.height = `${elementData.height}px`; | |
} | |
container.appendChild(element); | |
}); | |
} | |
} | |
// Start progress bar animation | |
function startProgressAnimation(duration) { | |
const progressBar = document.getElementById('previewProgress'); | |
if (!progressBar) return; | |
// Reset progress | |
progressBar.style.width = '0%'; | |
// Animate progress | |
progressBar.style.transition = `width ${duration}s linear`; | |
// Force reflow | |
progressBar.offsetWidth; | |
// Start animation | |
progressBar.style.width = '100%'; | |
} | |
// Function to toggle play/pause | |
function togglePlayPause() { | |
appState.previewPlaying = !appState.previewPlaying; | |
// Update button icon | |
updatePlayPauseButton(); | |
if (appState.previewPlaying) { | |
// Resume playback | |
const slot = appState.slots[appState.previewCurrentSlot]; | |
const remainingTime = getRemainingTime(); | |
if (remainingTime > 0) { | |
// Continue with remaining time | |
appState.previewTimer = setTimeout(() => { | |
if (appState.previewCurrentSlot < appState.slots.length - 1) { | |
appState.previewCurrentSlot++; | |
renderPreviewSlot(appState.previewCurrentSlot); | |
} else { | |
// End of preview, loop back to start | |
appState.previewCurrentSlot = 0; | |
renderPreviewSlot(appState.previewCurrentSlot); | |
} | |
}, remainingTime * 1000); | |
} else { | |
// Move to next slot | |
if (appState.previewCurrentSlot < appState.slots.length - 1) { | |
appState.previewCurrentSlot++; | |
} else { | |
appState.previewCurrentSlot = 0; | |
} | |
renderPreviewSlot(appState.previewCurrentSlot); | |
} | |
} else { | |
// Pause playback | |
if (appState.previewTimer) { | |
clearTimeout(appState.previewTimer); | |
appState.previewTimer = null; | |
} | |
// Pause progress bar animation | |
const progressBar = document.getElementById('previewProgress'); | |
if (progressBar) { | |
const computedStyle = window.getComputedStyle(progressBar); | |
const width = parseFloat(computedStyle.width) / parseFloat(computedStyle.parentElement.offsetWidth) * 100; | |
progressBar.style.transition = 'none'; | |
progressBar.style.width = `${width}%`; | |
} | |
} | |
} | |
// Update play/pause button icon | |
function updatePlayPauseButton() { | |
const playPauseBtn = document.getElementById('playPauseBtn'); | |
if (playPauseBtn) { | |
playPauseBtn.innerHTML = `<i class="fas fa-${appState.previewPlaying ? 'pause' : 'play'}"></i>`; | |
} | |
} | |
// Get remaining time for current slot | |
function getRemainingTime() { | |
const progressBar = document.getElementById('previewProgress'); | |
if (!progressBar) return 0; | |
const computedStyle = window.getComputedStyle(progressBar); | |
const width = parseFloat(computedStyle.width) / parseFloat(computedStyle.parentElement.offsetWidth) * 100; | |
const slot = appState.slots[appState.previewCurrentSlot]; | |
const totalDuration = slot.duration || 3; | |
return totalDuration * (1 - width / 100); | |
} | |
// Function to go to previous slot | |
function goToPreviousSlot() { | |
// Clear current timer | |
if (appState.previewTimer) { | |
clearTimeout(appState.previewTimer); | |
appState.previewTimer = null; | |
} | |
// Go to previous slot | |
appState.previewCurrentSlot = Math.max(0, appState.previewCurrentSlot - 1); | |
renderPreviewSlot(appState.previewCurrentSlot); | |
} | |
// Function to go to next slot | |
function goToNextSlot() { | |
// Clear current timer | |
if (appState.previewTimer) { | |
clearTimeout(appState.previewTimer); | |
appState.previewTimer = null; | |
} | |
// Go to next slot | |
appState.previewCurrentSlot = Math.min(appState.slots.length - 1, appState.previewCurrentSlot + 1); | |
renderPreviewSlot(appState.previewCurrentSlot); | |
} | |
// Function to close property editor | |
function closePropertyEditor() { | |
elements.propertyEditor.classList.remove('active'); | |
} | |
// Function to add a new slot | |
function addNewSlot() { | |
// Create new slot object | |
const newSlot = { | |
id: Date.now().toString(), | |
name: `Scene ${appState.slots.length + 1}`, | |
animationStyle: 'slideIn', | |
duration: 3, // Default scene duration in seconds | |
animationDuration: 0.8, // Default animation duration in seconds | |
elements: [] // Will store element data | |
}; | |
// Add to slots array | |
appState.slots.push(newSlot); | |
// Render slots list | |
renderSlotsList(); | |
// Select the new slot | |
selectSlot(newSlot.id); | |
// Save to storage | |
saveToStorage(); | |
} | |
// Function to delete a slot | |
function deleteSlot(slotId) { | |
// Find index of slot | |
const index = appState.slots.findIndex(slot => slot.id === slotId); | |
if (index === -1) return; | |
// Remove slot | |
appState.slots.splice(index, 1); | |
// If deleted slot was selected, select another slot | |
if (appState.currentSlotId === slotId) { | |
appState.currentSlotId = appState.slots.length > 0 ? appState.slots[0].id : null; | |
} | |
// Render slots list | |
renderSlotsList(); | |
// If there are slots, select one | |
if (appState.slots.length > 0 && appState.currentSlotId) { | |
selectSlot(appState.currentSlotId); | |
} | |
// Save to storage | |
saveToStorage(); | |
} | |
// Function to render slots list | |
function renderSlotsList() { | |
elements.slotsList.innerHTML = ''; | |
appState.slots.forEach(slot => { | |
const slotElement = document.createElement('div'); | |
slotElement.className = `slot-item ${slot.id === appState.currentSlotId ? 'active' : ''}`; | |
slotElement.dataset.id = slot.id; | |
// Get duration or use default | |
const duration = slot.duration || 3; | |
slotElement.innerHTML = ` | |
<div class="slot-header"> | |
<div class="slot-name">${slot.name}</div> | |
<div class="slot-delete"> | |
<i class="fas fa-trash"></i> | |
</div> | |
</div> | |
<div class="slot-preview"> | |
<div class="slot-info"> | |
<div class="slot-details"> | |
<i class="fas fa-film"></i> ${slot.animationStyle || 'slideIn'} | |
<span class="duration-badge">${duration}s</span> | |
</div> | |
</div> | |
</div> | |
`; | |
elements.slotsList.appendChild(slotElement); | |
}); | |
} | |
// Function to select a slot | |
function selectSlot(slotId) { | |
// Update current slot ID | |
appState.currentSlotId = slotId; | |
// Update active class in slots list | |
const allSlots = elements.slotsList.querySelectorAll('.slot-item'); | |
allSlots.forEach(slotElement => { | |
if (slotElement.dataset.id === slotId) { | |
slotElement.classList.add('active'); | |
} else { | |
slotElement.classList.remove('active'); | |
} | |
}); | |
// Get selected slot | |
const slot = findSlotById(slotId); | |
if (!slot) return; | |
// Set animation style | |
const style = slot.animationStyle || 'slideIn'; | |
selectAnimationStyle(style); | |
// Update timing controls | |
updateTimingControlsFromSlot(slot); | |
// Apply element positions from slot data | |
applyElementPositionsFromSlot(slot); | |
} | |
// Update timing controls from slot data | |
function updateTimingControlsFromSlot(slot) { | |
// Update animation duration | |
if (elements.animationDuration && slot.animationDuration) { | |
elements.animationDuration.value = slot.animationDuration; | |
elements.animationDurationValue.textContent = `${slot.animationDuration}s`; | |
updateAnimationDuration(slot.animationDuration); | |
} | |
// Update scene duration | |
if (elements.sceneDuration && slot.duration) { | |
elements.sceneDuration.value = slot.duration; | |
elements.sceneDurationValue.textContent = `${slot.duration}s`; | |
} | |
} | |
// Apply element positions from slot data | |
function applyElementPositionsFromSlot(slot) { | |
if (!slot.elements || !Array.isArray(slot.elements)) return; | |
// Map of element IDs to DOM elements | |
const elementMap = { | |
'leftCard': elements.leftCard, | |
'rightCard': elements.rightCard, | |
'leftLabel': elements.leftLabel, | |
'rightLabel': elements.rightLabel | |
}; | |
// Apply positions to elements | |
slot.elements.forEach(elementData => { | |
const element = elementMap[elementData.id] || document.getElementById(elementData.id); | |
if (!element) return; | |
// Apply position if available | |
if (typeof elementData.left === 'number' && typeof elementData.top === 'number') { | |
element.style.left = `${elementData.left}%`; | |
element.style.top = `${elementData.top}%`; | |
// Remove fixed positioning classes | |
if (element === elements.leftCard) { | |
element.classList.remove('flashcard-left'); | |
} else if (element === elements.rightCard) { | |
element.classList.remove('flashcard-right'); | |
} else if (element === elements.leftLabel) { | |
element.classList.remove('label-left'); | |
} else if (element === elements.rightLabel) { | |
element.classList.remove('label-right'); | |
} | |
} | |
// Apply text content if available | |
if (elementData.textContent) { | |
// For label containers, update the label text | |
if (element.classList.contains('label-container')) { | |
if (elementData.textContent.label) { | |
const labelEl = element.querySelector('.label'); | |
if (labelEl) { | |
labelEl.textContent = elementData.textContent.label; | |
} | |
} | |
if (elementData.textContent.translation) { | |
const translationEl = element.querySelector('.translation'); | |
if (translationEl) { | |
translationEl.textContent = elementData.textContent.translation; | |
} | |
} | |
} | |
} | |
// Apply styles if available | |
if (elementData.styles) { | |
Object.entries(elementData.styles).forEach(([property, value]) => { | |
// For label containers, apply styles to the label element | |
if (element.classList.contains('label-container') && | |
(property === 'color' || property === 'backgroundColor' || property === 'fontSize')) { | |
const labelEl = element.querySelector('.label'); | |
if (labelEl) { | |
labelEl.style[property] = value; | |
} | |
} else { | |
element.style[property] = value; | |
} | |
}); | |
} | |
// Apply size if available | |
if (typeof elementData.width === 'number') { | |
element.style.width = `${elementData.width}px`; | |
} | |
if (typeof elementData.height === 'number') { | |
element.style.height = `${elementData.height}px`; | |
} | |
}); | |
} | |
// Function to find slot by ID | |
function findSlotById(id) { | |
return appState.slots.find(slot => slot.id === id); | |
} | |
// Function to open property editor | |
function openPropertyEditor(element) { | |
if (!element) return; | |
// Show property editor | |
elements.propertyEditor.classList.add('active'); | |
// Store active element | |
appState.activeElement = element; | |
// Get current slot | |
const slot = findSlotById(appState.currentSlotId); | |
if (!slot) return; | |
// Load element properties | |
loadElementProperties(element); | |
// Load timing values from slot | |
updateTimingControlsFromSlot(slot); | |
} | |
// Load element properties into editor | |
function loadElementProperties(element) { | |
// Get element position | |
const rect = element.getBoundingClientRect(); | |
const canvasRect = elements.canvas.getBoundingClientRect(); | |
// Calculate position as percentage of canvas | |
const posX = (rect.left - canvasRect.left) / canvasRect.width * 100; | |
const posY = (rect.top - canvasRect.top) / canvasRect.height * 100; | |
// Update position fields | |
if (elements.positionX) elements.positionX.value = Math.round(posX); | |
if (elements.positionY) elements.positionY.value = Math.round(posY); | |
// Update size fields | |
if (elements.elementWidth) elements.elementWidth.value = Math.round(rect.width); | |
if (elements.elementHeight) elements.elementHeight.value = Math.round(rect.height); | |
// Show/hide appropriate property sections | |
if (element.classList.contains('label-container')) { | |
// Show text properties, hide image properties | |
elements.textProperties.style.display = 'block'; | |
elements.imageProperties.style.display = 'none'; | |
// Load text content | |
const labelEl = element.querySelector('.label'); | |
const translationEl = element.querySelector('.translation'); | |
if (labelEl && elements.textContent) { | |
elements.textContent.value = labelEl.textContent; | |
} | |
// Add translation field if it doesn't exist | |
if (!document.getElementById('translationContent')) { | |
const translationGroup = document.createElement('div'); | |
translationGroup.className = 'property-group'; | |
translationGroup.innerHTML = ` | |
<div class="property-label">Translation</div> | |
<input type="text" class="property-field" id="translationContent"> | |
`; | |
// Insert after text content | |
const textContentGroup = elements.textContent.closest('.property-group'); | |
textContentGroup.parentNode.insertBefore(translationGroup, textContentGroup.nextSibling); | |
// Add event listeners | |
const translationInput = document.getElementById('translationContent'); | |
translationInput.addEventListener('input', updateTranslationText); | |
translationInput.addEventListener('change', function() { | |
updateTranslationText(); | |
saveToStorage(); | |
}); | |
} | |
// Set translation value | |
const translationInput = document.getElementById('translationContent'); | |
if (translationEl && translationInput) { | |
translationInput.value = translationEl.textContent; | |
} | |
// Load text color and background | |
if (labelEl && elements.textColor) { | |
const computedStyle = window.getComputedStyle(labelEl); | |
elements.textColor.value = rgbToHex(computedStyle.color); | |
elements.textBgColor.value = rgbToHex(computedStyle.backgroundColor); | |
} | |
} else if (element.classList.contains('flashcard')) { | |
// Show image properties, hide text properties | |
elements.textProperties.style.display = 'none'; | |
elements.imageProperties.style.display = 'block'; | |
// Load image preview if available | |
const imgEl = element.querySelector('img'); | |
if (imgEl && elements.currentImage) { | |
elements.currentImage.src = imgEl.src; | |
} | |
// Remove translation field if it exists | |
const translationGroup = document.getElementById('translationContent')?.closest('.property-group'); | |
if (translationGroup) { | |
translationGroup.remove(); | |
} | |
} | |
// Load animation delay | |
const delayClass = Array.from(element.classList).find(cls => cls.startsWith('delay-')); | |
if (delayClass && elements.animationDelay) { | |
const delay = delayClass.split('-')[1]; | |
elements.animationDelay.value = delay; | |
} | |
} | |
// Update translation text | |
function updateTranslationText() { | |
if (!appState.activeElement) return; | |
const translationInput = document.getElementById('translationContent'); | |
if (!translationInput) return; | |
const translationValue = translationInput.value; | |
// Check if it's a label container | |
if (appState.activeElement.classList.contains('label-container')) { | |
// Update the translation text | |
const translationEl = appState.activeElement.querySelector('.translation'); | |
if (translationEl) { | |
translationEl.textContent = translationValue; | |
// Save text to slot data | |
saveElementText(appState.activeElement.id, 'translation', translationValue); | |
} | |
} | |
} | |
// Convert RGB to Hex color | |
function rgbToHex(rgb) { | |
// Default to black if not available | |
if (!rgb || rgb === 'transparent') return '#000000'; | |
// Check if already in hex format | |
if (rgb.startsWith('#')) return rgb; | |
// Extract RGB values | |
const rgbMatch = rgb.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i); | |
if (rgbMatch) { | |
const r = parseInt(rgbMatch[1], 10).toString(16).padStart(2, '0'); | |
const g = parseInt(rgbMatch[2], 10).toString(16).padStart(2, '0'); | |
const b = parseInt(rgbMatch[3], 10).toString(16).padStart(2, '0'); | |
return `#${r}${g}${b}`; | |
} | |
return '#000000'; | |
} | |
// Function to save data to storage | |
function saveToStorage() { | |
try { | |
const dataToSave = { | |
slots: appState.slots, | |
currentSlotId: appState.currentSlotId | |
}; | |
localStorage.setItem('flashcardAnimatorData', JSON.stringify(dataToSave)); | |
console.log('Data saved to storage'); | |
} catch (error) { | |
console.error('Error saving to storage:', error); | |
} | |
} | |
// Function to load data from storage | |
function loadFromStorage() { | |
try { | |
const savedData = localStorage.getItem('flashcardAnimatorData'); | |
if (savedData) { | |
const parsedData = JSON.parse(savedData); | |
appState.slots = parsedData.slots || []; | |
appState.currentSlotId = parsedData.currentSlotId || null; | |
console.log('Data loaded from storage'); | |
} | |
} catch (error) { | |
console.error('Error loading from storage:', error); | |
} | |
} | |
// Function to show a message to the user | |
function showMessage(text, duration = 3000) { | |
// Create message element if it doesn't exist | |
let messageElement = document.getElementById('app-message'); | |
if (!messageElement) { | |
messageElement = document.createElement('div'); | |
messageElement.id = 'app-message'; | |
messageElement.style.cssText = ` | |
position: fixed; | |
bottom: 20px; | |
left: 50%; | |
transform: translateX(-50%); | |
background-color: rgba(0, 0, 0, 0.8); | |
color: white; | |
padding: 10px 20px; | |
border-radius: 4px; | |
font-size: 14px; | |
z-index: 1000; | |
opacity: 0; | |
transition: opacity 0.3s; | |
`; | |
document.body.appendChild(messageElement); | |
} | |
// Set message text | |
messageElement.textContent = text; | |
// Show message | |
messageElement.style.opacity = '1'; | |
// Hide after duration | |
setTimeout(() => { | |
messageElement.style.opacity = '0'; | |
}, duration); | |
} | |
}); | |
</script> | |
</body> | |
</html> |