wordlist / index.html
luigi12345's picture
Update index.html
ea48e56 verified
<!DOCTYPE html>
<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 !important;
}
/* 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>