/* Nút cố định "Bắt đầu tạo hình" */
.fixed-btn {
position: fixed;
bottom: 30px;
right: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px 30px;
font-size: 16px;
font-weight: bold;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
z-index: 1000;
transition: all 0.3s ease;
}
.fixed-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
.fixed-btn:active {
transform: translateY(0);
}
.fixed-btn.loading {
opacity: 0.85;
cursor: wait;
}
/* Nút minimized gallery */
.minimized-gallery-btn {
bottom: 30px;
right: 30px;
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
z-index: 1500; /* Cao hơn fixed-btn để hiển thị trên nút "Bắt đầu tạo hình" */
}
.minimized-gallery-btn.hidden {
display: none;
}
/* Modal Hướng dẫn */
.guide-modal-content {
max-width: 800px;
max-height: 90vh;
}
.guide-modal-body {
padding: 20px;
overflow-y: auto;
max-height: calc(90vh - 100px);
}
.guide-content {
white-space: pre-wrap;
font-family: 'Arial', sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
background: white;
padding: 15px;
border-radius: 4px;
border: 1px solid #ddd;
}
/* Modal Thư viện */
.library-modal-content {
max-width: 900px;
max-height: 90vh;
}
.library-modal-body {
padding: 20px;
overflow-y: auto;
max-height: calc(90vh - 100px);
}
.library-content {
position: relative;
}
.library-loading {
text-align: center;
padding: 40px;
font-size: 16px;
color: #666;
}
.library-empty {
text-align: center;
padding: 40px;
font-size: 16px;
color: #999;
}
.library-images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
padding: 10px 0;
}
.library-image-item {
position: relative;
border: 2px solid #ddd;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
background: #f5f5f5;
aspect-ratio: 1;
}
.library-image-item:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.library-image-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.library-image-item .image-name {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px;
font-size: 12px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.library-empty.hidden,
.library-loading.hidden {
display: none;
}
/* Dialog xác nhận "Bắt đầu" */
.confirm-modal-content {
max-width: 450px;
padding: 0;
}
.confirm-dialog {
display: flex;
flex-direction: column;
align-items: center;
padding: 30px;
text-align: center;
}
.confirm-icon {
font-size: 64px;
margin-bottom: 20px;
color: #4A90E2;
}
.confirm-content {
margin-bottom: 30px;
}
.confirm-title {
font-size: 20px;
font-weight: bold;
margin: 0 0 15px 0;
color: #333;
}
.confirm-message {
font-size: 14px;
line-height: 1.6;
color: #666;
margin: 0;
white-space: pre-line;
}
.confirm-buttons {
display: flex;
gap: 12px;
justify-content: center;
width: 100%;
}
.confirm-buttons .btn {
min-width: 100px;
padding: 10px 20px;
}
/* Dialog tải DXF (giống desktop app) */
.dxf-modal-content {
max-width: 400px;
padding: 0;
}
.dxf-dialog {
display: flex;
flex-direction: column;
}
.dxf-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
}
.dxf-dialog-header h3 {
margin: 0;
font-size: 16px;
font-weight: bold;
color: #333;
}
.dxf-dialog-body {
padding: 20px;
display: flex;
flex-direction: column;
}
.dxf-info-label {
font-size: 9pt;
color: #666;
margin-bottom: 15px;
line-height: 1.4;
}
.dxf-input-frame {
display: flex;
align-items: center;
margin-bottom: 10px;
width: 100%;
}
.dxf-input-frame:last-of-type {
margin-bottom: 15px;
}
.dxf-label {
width: 100px;
min-width: 100px;
text-align: left;
padding-right: 5px;
font-size: 14px;
color: #333;
}
.dxf-entry {
flex: 1;
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
width: 100%;
max-width: 240px;
}
.dxf-entry:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.dxf-dialog-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 10px;
}
.dxf-dialog-buttons .btn {
min-width: 100px;
padding: 8px 20px;
font-size: 14px;
}
/* Modal overlay */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
animation: fadeIn 0.3s ease;
}
.modal.hidden {
display: none;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Modal content */
.modal-content {
background: white;
border-radius: 15px;
width: 90%;
max-width: 900px; /* Tăng từ 800px để gallery có nhiều không gian hơn */
max-height: 95vh; /* Tăng từ 90vh để gallery có nhiều không gian hơn */
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
animation: slideUp 0.3s ease;
/* Chỉ có 1 scrollbar duy nhất ở đây */
}
@keyframes slideUp {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Modal header */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px;
border-bottom: 2px solid #f0f0f0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 15px 15px 0 0;
}
.modal-header h2 {
margin: 0;
font-size: 24px;
}
.modal-header-buttons {
display: flex;
gap: 10px;
align-items: center;
}
.minimize-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
font-size: 24px;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.minimize-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.close-btn {
background: none;
border: none;
color: white;
font-size: 32px;
cursor: pointer;
padding: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.3s ease;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Form view */
.form-view {
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.form-group input[type="number"],
.form-group input[type="text"],
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.form-group input[type="number"]:focus,
.form-group input[type="text"]:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-group input[type="checkbox"] {
width: auto;
margin-right: 8px;
}
.dialog-heading {
text-align: center;
margin-bottom: 18px;
}
.dialog-heading h3 {
margin: 0;
font-size: 20px;
font-weight: 700;
letter-spacing: 0.5px;
}
.dialog-subtitle {
margin: 8px 0 0;
font-size: 14px;
color: #3a63d4;
line-height: 1.5;
}
.form-section {
border: 1px solid #e0e0e0;
border-radius: 10px;
padding: 18px;
margin-bottom: 18px;
background: #ffffff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
.section-title {
font-size: 14px;
font-weight: 700;
color: #333;
margin-bottom: 14px;
}
.section-row {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.section-row label {
min-width: 120px;
margin: 0;
font-weight: 600;
color: #333;
}
.section-row input[type="text"],
.section-row input[type="number"],
.section-row select {
flex: 1;
}
.section-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 18px;
}
.field label {
display: block;
margin-bottom: 6px;
font-weight: 600;
color: #333;
}
.inline-field {
display: flex;
align-items: center;
gap: 10px;
}
.inline-field select,
.inline-field input {
flex: 1;
}
.unit {
font-size: 13px;
color: #666;
}
.help-text {
margin: 12px 0 0 0;
font-size: 12px;
color: #2b61d8;
line-height: 1.4;
}
.help-inline {
font-size: 12px;
color: #666;
}
.size-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 18px;
}
.size-item label {
font-weight: 600;
color: #333;
display: block;
margin-bottom: 6px;
}
.size-input {
display: flex;
align-items: center;
gap: 8px;
}
.size-item select {
width: 100%;
padding: 10px;
border: 2px solid #e0e0e0;
border-radius: 7px;
font-size: 14px;
transition: border-color 0.3s ease;
}
.size-item select:focus {
outline: none;
border-color: #667eea;
}
.size-unit {
font-size: 13px;
color: #666;
font-weight: 500;
white-space: nowrap;
}
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 8px 10px;
background: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.radio-group label {
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
color: #444;
}
.radio-group input[type="radio"] {
accent-color: #667eea;
transform: scale(1.1);
}
.type-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
}
.type-grid {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
}
.checkbox-inline {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #444;
}
.checkbox-inline input[type="checkbox"] {
transform: scale(1.1);
accent-color: #667eea;
}
.count-grid {
display: grid;
grid-template-columns: auto minmax(150px, 190px) 1fr;
column-gap: 16px;
row-gap: 12px;
align-items: center;
}
.count-label {
font-weight: 600;
color: #333;
}
.count-field {
display: flex;
align-items: center;
gap: 10px;
}
.count-field input {
width: 80px;
}
.progress-wrapper {
justify-self: stretch;
}
.progress-wrapper .progress-container {
margin-top: 0;
padding: 0;
background: transparent;
border-radius: 0;
}
.progress-wrapper .progress-bar {
height: 22px;
}
.progress-wrapper .progress-text {
font-size: 12px;
text-align: right;
margin-top: 6px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-actions {
margin-top: 25px;
padding-top: 20px;
border-top: 2px solid #f0f0f0;
}
/* Buttons */
.btn {
padding: 12px 30px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #e0e0e0;
color: #333;
}
.btn-secondary:hover {
background: #d0d0d0;
}
.btn-secondary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-tertiary {
background: #f2f2f2;
color: #444;
cursor: pointer;
pointer-events: auto;
}
.btn-tertiary:hover:not(:disabled) {
background: #e4e4e4;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.btn-tertiary:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
.action-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, auto));
gap: 12px;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.btn-full {
width: 100%;
}
/* Progress bar */
.progress-container {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.progress-bar {
width: 100%;
height: 30px;
background: #e0e0e0;
border-radius: 15px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
width: 0%;
transition: width 0.3s ease;
border-radius: 15px;
}
.progress-text {
text-align: center;
color: #666;
font-size: 14px;
margin: 0;
}
.hidden {
display: none !important;
}
.global-progress {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: rgba(34, 40, 49, 0.92);
color: #fff;
padding: 10px 24px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.25);
z-index: 2500;
}
.global-progress-inner {
max-width: 960px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 4px;
}
#global-progress-text {
font-size: 14px;
font-weight: 600;
letter-spacing: 0.3px;
}
.global-progress-bar {
width: 100%;
height: 9px;
background: rgba(255, 255, 255, 0.18);
border-radius: 999px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.25);
}
.global-progress-bar > div {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #6a9dfc 0%, #8d6bfc 100%);
border-radius: 999px;
transition: width 0.3s ease;
}
/* Gallery view */
.gallery-view {
padding: 20px;
/* Bỏ max-height và overflow-y vì modal-content đã có scrollbar */
}
.gallery-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 2px solid #f0f0f0;
}
.gallery-header h3 {
margin: 0;
font-size: 22px;
color: #333;
}
.gallery-body {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
/* Canvas container với nút điều hướng hai bên */
.gallery-canvas-container {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
position: relative;
min-height: 400px;
}
.gallery-nav-btn-side {
background: #f2f2f2;
border: none;
border-radius: 6px;
padding: 10px 14px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
color: #444;
white-space: nowrap;
align-self: center;
}
.gallery-nav-btn-side:hover:not(:disabled) {
background: #e0e0e0;
}
.gallery-nav-btn-side:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.gallery-canvas-wrapper {
flex: 1;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
min-height: 350px;
height: 400px; /* Giảm từ 500px xuống 400px */
position: relative;
width: 100%;
}
#gallery-main-canvas {
max-width: 100%;
max-height: 100%;
cursor: grab;
}
#gallery-main-canvas:active {
cursor: grabbing;
}
/* Zoom controls */
.gallery-zoom-controls {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 0;
font-size: 12px;
}
.zoom-label {
font-size: 12px;
margin-right: 3px;
}
.zoom-btn-small {
width: 24px;
height: 24px;
border: 1px solid #ccc;
border-radius: 3px;
background: white;
cursor: pointer;
font-size: 14px;
font-weight: bold;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.zoom-btn-small:hover {
background: #f0f0f0;
}
.zoom-level-text {
font-size: 12px;
width: 40px;
text-align: center;
margin: 0 3px;
}
.zoom-spacer {
width: 20px;
}
/* Label số hình hiện tại */
.gallery-image-label {
text-align: center;
padding: 2px 0;
font-size: 14px;
color: #333;
}
/* Thông tin tranh và nút hành động */
.gallery-info-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
gap: 10px;
}
.gallery-info-text {
flex: 1;
font-size: 12px;
color: #333;
}
.gallery-action-buttons {
display: flex;
gap: 5px;
}
.btn-action {
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.btn-action:hover {
background: #f0f0f0;
}
.gallery-select-button {
display: flex;
justify-content: center;
margin: 2px 0 0 0;
padding: 0;
}
.btn-select {
background: #4CAF50;
color: white;
border: 2px solid #2d7a32;
border-radius: 6px;
padding: 6px 12px;
font-size: 8px;
font-weight: bold;
cursor: pointer;
line-height: 1.2;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease;
}
.btn-select:hover {
background: #45a049;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
}
.btn-select:active {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transform: translateY(1px);
}
.gallery-basic-info {
display: flex;
justify-content: center;
padding: 10px;
font-size: 10px;
color: #333;
}
.gallery-thumbnails {
border-top: 2px solid #f0f0f0;
padding-top: 12px;
}
.thumbnail-list {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 6px;
scrollbar-width: thin;
}
.thumbnail-item {
border: 2px solid transparent;
border-radius: 10px;
padding: 4px;
background: white;
cursor: pointer;
transition: all 0.2s ease;
}
.thumbnail-item img {
width: 72px;
height: 72px;
object-fit: contain;
border-radius: 6px;
background: #f4f4f4;
}
.thumbnail-item.active {
border-color: #6a9dfc;
box-shadow: 0 0 0 3px rgba(106, 157, 252, 0.25);
}
.thumbnail-item:hover {
border-color: #90b5ff;
}
/* Responsive */
@media (max-width: 768px) {
.modal-content {
width: 95%;
max-height: 95vh;
}
.size-grid {
grid-template-columns: 1fr;
}
.size-unit {
margin-left: 0;
}
.section-row {
flex-direction: column;
align-items: flex-start;
}
.section-row label {
min-width: auto;
}
.type-row,
.type-grid {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.count-grid {
grid-template-columns: 1fr;
}
.action-row {
grid-template-columns: 1fr;
justify-items: stretch;
}
.gallery-body {
flex-direction: column;
}
.gallery-main {
min-width: auto;
flex: 1;
}
.gallery-info-panel {
flex: 1;
width: 100%;
}
}
/* Cửa sổ "Hình Đã Chọn" */
.selected-window {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 3000;
animation: fadeIn 0.3s ease;
}
.selected-window-content {
background: white;
border-radius: 12px;
width: 900px;
max-width: 95vw;
max-height: 95vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.selected-window-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 2px solid #e0e0e0;
}
.selected-window-header h3 {
margin: 0;
font-size: 16px;
font-weight: bold;
color: #333;
}
.selected-window-body {
padding: 10px;
overflow-y: auto;
flex: 1;
}
.selected-canvas-container {
width: 100%;
height: 500px;
border: 1px solid #e0e0e0;
background: white;
margin-bottom: 5px;
overflow: auto;
}
#selected-image-canvas {
display: block;
margin: 0 auto;
}
.selected-zoom-controls {
display: flex;
align-items: center;
gap: 5px;
padding: 5px 0;
font-size: 8px;
}
.zoom-btn {
width: 24px;
height: 24px;
border: 1px solid #ccc;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
}
.zoom-btn:hover {
background: #e0e0e0;
}
.zoom-spacer {
width: 10px;
}
.btn-view-gif, .btn-view-image {
padding: 6px 12px;
border: 2px solid #4A90E2;
border-radius: 4px;
font-size: 9px;
font-weight: bold;
cursor: pointer;
background: #6BA3D6;
color: black;
transition: all 0.2s ease;
}
.btn-view-gif:hover:not(:disabled), .btn-view-image:hover:not(:disabled) {
background: #5C9BD1;
}
.btn-view-gif:disabled, .btn-view-image:disabled {
background: #9BC4E2;
color: darkgray;
cursor: not-allowed;
}
.selected-info-frame {
padding: 10px;
font-size: 10px;
color: #333;
}
.selected-actions-frame {
display: flex;
justify-content: flex-end;
gap: 5px;
padding: 10px;
border-top: 1px solid #e0e0e0;
}
.btn-action {
padding: 8px 15px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f5f5f5;
color: #333;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-action:hover {
background: #e0e0e0;
}
🎨 Bắt đầu tạo tranh
🖼️ Hình tranh bạn đã tạo
NGHỆ THUẬT DÂY ĐỘC ĐÁO
Chọn từ: Mẫu có sẵn, Tác phẩm nghệ thuật hoàn thiện & Thiết kế theo yêu cầu
VỀ CHÚNG TÔI
"Yêu cái đẹp là thưởng thức. Tạo ra cái đẹp là nghệ thuật." Ralph Waldo Emerson. String Art - #nghệ_thuật_chuỗi: Các đường cong được tạo nên từ chuỗi các đường thẳng đi theo một tỷ lệ định sẵn trước kết hợp với các màu sắc tạo nên một loại hình nghệ thuật độc đáo. Từ những sợi dây những cây đinh, nguyên vật liệu đơn giản dưới sự kết hợp tài tình của phép tỷ lệ đã tạo nên một bức tranh với nhiều màu sắc độc đáo, mang đậm tính chất số học. Sản phẩm làm thủ công với độ chính xác cao, tỉ mỉ. Vật liệu đinh được chọn lựa đều đẹp. Sợi dây bóng đẹp đặc biệt không dính bụi.
3 BƯỚC ĐỂ TẠO RA TÁC PHẨM NGHỆ THUẬT THEO YÊU CẦU CỦA BẠN 🎨
Bước 1: Chọn thiết kế của bạn
Duyệt qua thư viện ảnh của chúng tôi hoặc tải ảnh của riêng bạn lên để tạo ra một tác phẩm độc đáo.
Bước 2: Xem trước & Hoàn thiện
Nhận bản xem trước trong vòng 24 giờ, với số lần sửa đổi không giới hạn và đảm bảo hài lòng 100%.
Mở hộp bộ sản phẩm, bắt đầu chế tạo và tự hào trưng bày kiệt tác hoàn thiện của bạn!
Bạn lo lắng về diện mạo cuối cùng? Hãy thử xem trước tác phẩm nghệ thuật miễn phí! 🤩
SAV01 SAV01 - Nghệ thuật chuỗi - Sunrise
Kích thước: 60x60cm
Chất liệu: Gỗ, đinh, giây màu
Thiết kế hoàn toàn thủ công
SAV02 SAV02 - Universe
Kích thước: 60x60cm
Chất liệu: Gỗ, đinh, giây màu
Sản phẩm thủ công
SAV03 SAV03 - Nghệ thuật chuỗi
Kích thước: 60x60cm
Chất liệu: Gỗ, đinh, giây màu
Thiết kế hoàn toàn thủ công
DUYỆT QUA THƯ VIỆN ĐỂ CÓ THÊM CẢM HỨNG
Chọn từ: Mẫu có sẵn, Tác phẩm nghệ thuật hoàn thiện & Thiết kế theo yêu cầu
TẠI SAO CHỌN CHÚNG TÔI? Sửa đổi không giới hạn Nhận bản xem trước qua email và yêu cầu thay đổi nhiều lần cho đến khi bạn hài lòng 100%.
Miễn phí vận chuyển Tận hưởng dịch vụ giao hàng tiêu chuẩn miễn phí với quy trình xử lý nhanh chóng - đơn hàng sẽ được giao trong vòng 1-2 ngày!
Vật liệu cao cấp Được chế tác bằng vật liệu chất lượng cao để tạo nên kiệt tác bền vững.
Hỗ trợ khách hàng nhanh chóng Bạn cần trợ giúp? Chúng tôi luôn sẵn sàng hỗ trợ bạn.
Vật liệu được tuyển chọn nghiêm ngặt, đảm bảo chất lượng, chắc chắn và tính thẩm mỹ
Thực hiện hoàn toàn thủ công bởi những người thợ giàu kinh nghiệm trong mỹ thuật, không gian và vật lý
Thiết kế thanh lịch, tinh tế, tác động thị giác ấn tượng. Chúng tôi tối ưu hóa mọi ảnh để đảm bảo tác phẩm nghệ thuật bằng dây của bạn trông tuyệt đẹp, với tác phẩm cuối cùng hoàn toàn khớp với bản xem trước.
Trải nghiệm sản phẩm của khách hàng, từ 319 đánh giá
Đánh giá của khách hàng
Tôi vừa mới mua tác phẩm nghệ thuật này. Nó thực sự rất đẹp. Chất lượng cũng rất tốt. Nhìn chung rất đáng giá
Diệu Đoan
Chất lượng tuyệt vời! Mọi thứ đều gọn gàng, ngăn nắp, được trình bày một cách khoa học và hấp dẫn. Hài lòng về sản phẩm.
An Lành
Đóng gói rất cẩn thận. Sản phẩm tinh tế. Giao hàng đúng hẹn. Rất hài lòng.
Hà An
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
Tôi vừa mới mua tác phẩm nghệ thuật này. Nó thực sự rất đẹp. Chất lượng cũng rất tốt. Nhìn chung rất đáng giá
Diệu Đoan
Chất lượng tuyệt vời! Mọi thứ đều gọn gàng, ngăn nắp, được trình bày một cách khoa học và hấp dẫn. Hài lòng về sản phẩm.
An Lành
Đóng gói rất cẩn thận. Sản phẩm tinh tế. Giao hàng đúng hẹn. Rất hài lòng.
Hà An
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
Còn tốt hơn mong đợi! Tôi mua sản phẩm này làm quà tặng. Nó thực sự rất đẹp. Chất lượng cũng rất tốt.
Quang Lâm
DỊCH VỤ KHÁCH HÀNG Hướng dẫn & Câu hỏi thường gặp
http://thenounproject.com The Noun Project Icon Template Reminders Strokes Try to keep strokes at 4px Minimum stroke weight is 2px For thicker strokes use even numbers: 6px, 8px etc. Remember to expand strokes before saving as an SVG Size Cannot be wider or taller than 100px (artboard size) Scale your icon to fill as much of the artboard as possible Ungroup If your design has more than one shape, make sure to ungroup Save as Save as .SVG and make sure “Use Artboards” is checked 100px .SVG
STRING ART VIETNAM - NGHỆ THUẬT CHUỖI Address: Thu Duc, Ho Chi Minh
Email: vietnamstringart@gmail.com
Website: www.stringartvn.com
©2025 Allrights reserved stringartvn.com
Chuyển khoản ngân hàng
🇻🇳 Việt Nam (VNĐ)
Áp dụng
Ưu đãi phí vận chuyển (Bạn có thể chọn 1 mã giảm giá)
Mã giảm giá (Bạn có thể chọn 1 mã giảm giá)
/**
* JavaScript xử lý modal và gọi API Backend
*/
// Cấu hình API
// THAY ĐỔI URL NÀY THÀNH URL BACKEND THỰC TẾ CỦA BẠN
// Ví dụ: const API_BASE_URL = '/api'; (nếu Backend chạy trên cùng domain)
// Hoặc: const API_BASE_URL = 'https://api.stringartvn.com/api'; (nếu Backend chạy trên domain khác)
const API_BASE_URL = 'https://api.stringartvn.com/api';
// Giá trị mặc định (sẽ được load từ settings)
let defaultSettings = {
paper_size: '400',
bg_shape: 'vuông',
bg_color: 'đen',
pen_color: 'đỏ',
count: 1,
shape_type: 'symmetric'
};
// Settings từ backend (bao gồm danh sách options)
let settingsData = null;
// DOM Elements
const startArtBtn = document.getElementById('start-art-btn');
const artModal = document.getElementById('art-modal');
const closeModalBtn = document.getElementById('close-modal-btn');
const cancelBtn = document.getElementById('cancel-btn');
const artForm = document.getElementById('art-form');
const inputForm = document.getElementById('input-form');
const galleryView = document.getElementById('gallery-view');
const progressContainer = document.getElementById('progress-container');
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
const globalProgress = document.getElementById('global-progress');
const globalProgressFill = document.getElementById('global-progress-fill');
const globalProgressText = document.getElementById('global-progress-text');
const galleryTitle = document.getElementById('gallery-title');
const galleryMainCanvas = document.getElementById('gallery-main-canvas');
const galleryPrevBtn = document.getElementById('gallery-prev-btn');
const galleryNextBtn = document.getElementById('gallery-next-btn');
const galleryInfoText = document.getElementById('gallery-info-text');
const galleryImageLabelText = document.getElementById('gallery-image-label-text');
const galleryThumbnails = document.getElementById('gallery-thumbnails');
const newArtBtn = document.getElementById('new-art-btn');
const galleryZoomInBtn = document.getElementById('gallery-zoom-in-btn');
const galleryZoomOutBtn = document.getElementById('gallery-zoom-out-btn');
const galleryZoomResetBtn = document.getElementById('gallery-zoom-reset-btn');
const galleryZoomLevelText = document.getElementById('gallery-zoom-level-text');
const galleryViewGifBtn = document.getElementById('gallery-view-gif-btn');
const galleryViewImageBtn = document.getElementById('gallery-view-image-btn');
const galleryDxfBtn = document.getElementById('gallery-dxf-btn');
const galleryCartBtn = document.getElementById('gallery-cart-btn');
const minimizeModalBtn = document.getElementById('minimize-modal-btn');
const minimizedGalleryBtn = document.getElementById('minimized-gallery-btn');
const DEFAULT_START_BUTTON_TEXT = startArtBtn ? startArtBtn.textContent : '🎨 Bắt đầu tạo hình';
// State
let currentSessionId = null;
let galleryImages = [];
let currentGalleryIndex = 0;
let currentSelectedImageIndex = null;
let currentZoomLevel = 0.25; // Giảm từ 0.3 xuống 0.25 (25%) để khớp với desktop app
let currentImageObj = null;
let canvasCtx = null;
let galleryCanvasCtx = null; // Context cho gallery main canvas
let isGifMode = false;
let animationFrameId = null;
let currentPathData = null;
// Biến cho drag/pan
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
let imageOffsetX = 0;
let imageOffsetY = 0;
// Load settings từ backend khi khởi tạo
async function loadSettings() {
try {
const response = await fetch(`${API_BASE_URL}/settings`);
if (response.ok) {
const result = await response.json();
if (result.success) {
settingsData = result.settings;
defaultSettings = result.defaults;
console.log('Settings loaded:', settingsData);
console.log('Defaults loaded:', defaultSettings);
// Populate options từ settings
populateOptionsFromSettings();
// Áp dụng giá trị mặc định vào form
applyDefaultValues();
}
}
} catch (err) {
console.error('Error loading settings:', err);
// Vẫn dùng giá trị mặc định hardcode nếu không load được
applyDefaultValues();
}
}
// Populate options trong select từ settings
function populateOptionsFromSettings() {
if (!settingsData) return;
// Populate paper_size options
const paperSizeSelect = document.getElementById('paper-size');
if (paperSizeSelect && settingsData.paper_sizes) {
// Xóa tất cả options hiện tại
paperSizeSelect.innerHTML = '';
// Thêm options từ settings
settingsData.paper_sizes.forEach(size => {
const option = document.createElement('option');
option.value = size;
option.textContent = size;
paperSizeSelect.appendChild(option);
});
}
// Populate bg_shape options
const bgShapeSelect = document.getElementById('bg-shape');
if (bgShapeSelect && settingsData.shapes) {
bgShapeSelect.innerHTML = '';
settingsData.shapes.forEach(shape => {
const option = document.createElement('option');
option.value = shape;
option.textContent = shape.charAt(0).toUpperCase() + shape.slice(1); // Capitalize first letter
bgShapeSelect.appendChild(option);
});
}
// Populate bg_color options
const bgColorSelect = document.getElementById('bg-color');
if (bgColorSelect && settingsData.bg_colors) {
bgColorSelect.innerHTML = '';
settingsData.bg_colors.forEach(color => {
const option = document.createElement('option');
option.value = color;
option.textContent = color.charAt(0).toUpperCase() + color.slice(1);
bgColorSelect.appendChild(option);
});
}
// Populate pen_color options
const penColorSelect = document.getElementById('pen-color');
if (penColorSelect && settingsData.pen_colors) {
penColorSelect.innerHTML = '';
settingsData.pen_colors.forEach(color => {
const option = document.createElement('option');
option.value = color;
option.textContent = color.charAt(0).toUpperCase() + color.slice(1);
penColorSelect.appendChild(option);
});
}
// Update max count cho count input
const countInput = document.getElementById('count');
if (countInput && settingsData.current_max_count) {
countInput.max = settingsData.current_max_count;
const helpInline = countInput.parentElement.querySelector('.help-inline');
if (helpInline) {
helpInline.textContent = `(1-${settingsData.current_max_count})`;
}
}
}
// Áp dụng giá trị mặc định vào form
function applyDefaultValues() {
const paperSizeSelect = document.getElementById('paper-size');
const bgShapeSelect = document.getElementById('bg-shape');
const bgColorSelect = document.getElementById('bg-color');
const penColorSelect = document.getElementById('pen-color');
const countInput = document.getElementById('count');
const shapeTypeRadios = document.querySelectorAll('input[name="shape_type"]');
if (paperSizeSelect && defaultSettings.paper_size) {
paperSizeSelect.value = defaultSettings.paper_size;
}
if (bgShapeSelect && defaultSettings.bg_shape) {
bgShapeSelect.value = defaultSettings.bg_shape;
}
if (bgColorSelect && defaultSettings.bg_color) {
bgColorSelect.value = defaultSettings.bg_color;
}
if (penColorSelect && defaultSettings.pen_color) {
penColorSelect.value = defaultSettings.pen_color;
}
if (countInput && defaultSettings.count) {
countInput.value = defaultSettings.count;
}
if (shapeTypeRadios && defaultSettings.shape_type) {
shapeTypeRadios.forEach(radio => {
if (radio.value === defaultSettings.shape_type) {
radio.checked = true;
}
});
}
}
// ============================================
// Logic loại trừ lẫn nhau: Fibonacci và Loại hình
// ============================================
// Xử lý khi thay đổi checkbox Fibonacci
function setupFibonacciLogic() {
const fibonacciCheckbox = document.getElementById('use-fibonacci');
const shapeTypeRadios = document.querySelectorAll('input[name="shape_type"]');
if (!fibonacciCheckbox || shapeTypeRadios.length === 0) {
return; // Chưa có elements, bỏ qua
}
// Xóa listeners cũ nếu có (bằng cách clone và replace)
if (fibonacciCheckbox.hasAttribute('data-fibonacci-listener')) {
return; // Đã setup rồi, không setup lại
}
// Handler cho Fibonacci checkbox
function onFibonacciChange() {
if (this.checked) {
// Khi tick Fibonacci, bỏ chọn tất cả radio button
shapeTypeRadios.forEach(radio => {
radio.checked = false;
});
console.log('✓ Fibonacci được chọn - bỏ chọn đối xứng/không đối xứng/cả hai');
} else {
// Khi bỏ tick Fibonacci, chọn mặc định "symmetric"
const symmetricRadio = document.querySelector('input[name="shape_type"][value="symmetric"]');
if (symmetricRadio) {
symmetricRadio.checked = true;
}
console.log('✓ Fibonacci bỏ chọn - chọn mặc định đối xứng');
}
}
// Handler cho radio buttons
function onShapeTypeChange() {
// Khi chọn 1 trong 3 radio button, bỏ chọn Fibonacci
if (fibonacciCheckbox && fibonacciCheckbox.checked) {
fibonacciCheckbox.checked = false;
console.log(`✓ Chọn ${this.value} - bỏ chọn Fibonacci`);
}
}
// Thêm event listeners
fibonacciCheckbox.addEventListener('change', onFibonacciChange);
fibonacciCheckbox.setAttribute('data-fibonacci-listener', 'true');
shapeTypeRadios.forEach(radio => {
if (!radio.hasAttribute('data-shape-listener')) {
radio.addEventListener('change', onShapeTypeChange);
radio.setAttribute('data-shape-listener', 'true');
}
});
}
// Event Listeners
startArtBtn.addEventListener('click', () => {
showModal();
showInputForm();
});
// Load settings khi trang được tải
loadSettings();
// Setup logic loại trừ lẫn nhau cho Fibonacci và Loại hình
setupFibonacciLogic();
// Khởi tạo confirm dialog khi DOM sẵn sàng
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initConfirmDialog);
} else {
initConfirmDialog();
}
closeModalBtn.addEventListener('click', () => {
hideModal();
// Ẩn nút minimized khi đóng hoàn toàn
if (minimizedGalleryBtn) {
minimizedGalleryBtn.classList.add('hidden');
}
});
cancelBtn.addEventListener('click', () => {
hideModal();
// Ẩn nút minimized khi đóng hoàn toàn
if (minimizedGalleryBtn) {
minimizedGalleryBtn.classList.add('hidden');
}
});
// Nút minimize
if (minimizeModalBtn) {
minimizeModalBtn.addEventListener('click', () => {
minimizeModal();
});
}
// Nút minimized gallery
if (minimizedGalleryBtn) {
minimizedGalleryBtn.addEventListener('click', () => {
restoreModal();
});
}
// KHÔNG đóng modal khi click ra ngoài (theo yêu cầu)
// artModal.addEventListener('click', (e) => {
// if (e.target === artModal) {
// hideModal();
// }
// });
if (newArtBtn) {
newArtBtn.addEventListener('click', () => {
// Khi nhấn "Tạo hình mới": đóng gallery và mở lại form nhập liệu
// Modal vẫn mở, chỉ chuyển từ gallery view sang input form
showInputForm();
// Ẩn nút minimized
if (minimizedGalleryBtn) {
minimizedGalleryBtn.classList.add('hidden');
}
// Đảm bảo modal được hiển thị
showModal();
});
}
if (galleryPrevBtn) {
galleryPrevBtn.addEventListener('click', () => navigateGallery(-1));
}
if (galleryNextBtn) {
galleryNextBtn.addEventListener('click', () => navigateGallery(1));
}
// Event listeners cho gallery zoom controls
if (galleryZoomInBtn) {
galleryZoomInBtn.addEventListener('click', () => zoomGalleryCanvas(1.1));
}
if (galleryZoomOutBtn) {
galleryZoomOutBtn.addEventListener('click', () => zoomGalleryCanvas(1/1.1));
}
if (galleryZoomResetBtn) {
galleryZoomResetBtn.addEventListener('click', () => resetGalleryZoom());
}
if (galleryViewGifBtn) {
galleryViewGifBtn.addEventListener('click', () => switchToGifMode());
}
if (galleryViewImageBtn) {
galleryViewImageBtn.addEventListener('click', () => switchToImageMode());
}
if (galleryDxfBtn) {
galleryDxfBtn.addEventListener('click', () => {
if (galleryImages[currentGalleryIndex]) {
downloadDXF(galleryImages[currentGalleryIndex].index);
}
});
}
if (galleryCartBtn) {
galleryCartBtn.addEventListener('click', () => {
if (galleryImages[currentGalleryIndex]) {
addToCart(galleryImages[currentGalleryIndex].index);
}
});
}
// Hàm thêm vào giỏ hàng
function addToCart(imageIndex) {
alert('Tính năng giỏ hàng chưa khả dụng trên phiên bản web.\nHình #' + imageIndex + ' sẽ được thêm vào giỏ hàng khi tính năng được triển khai.');
}
function setStartButtonLoading(isLoading) {
if (!startArtBtn) return;
if (isLoading) {
startArtBtn.textContent = '⏳ Đang tạo hình...';
startArtBtn.classList.add('loading');
startArtBtn.disabled = true;
} else {
startArtBtn.textContent = DEFAULT_START_BUTTON_TEXT;
startArtBtn.classList.remove('loading');
startArtBtn.disabled = false;
}
}
// ============================================
// Dialog xác nhận "Bắt đầu"
// ============================================
// Khởi tạo các element khi DOM sẵn sàng
let confirmStartModal = null;
let confirmMessage = null;
let confirmOkBtn = null;
let confirmCancelBtn = null;
function initConfirmDialog() {
confirmStartModal = document.getElementById('confirm-start-modal');
confirmMessage = document.getElementById('confirm-message');
confirmOkBtn = document.getElementById('confirm-ok-btn');
confirmCancelBtn = document.getElementById('confirm-cancel-btn');
if (confirmStartModal && confirmMessage && confirmOkBtn && confirmCancelBtn) {
console.log('Confirm dialog elements initialized');
} else {
console.warn('Some confirm dialog elements not found:', {
modal: !!confirmStartModal,
message: !!confirmMessage,
okBtn: !!confirmOkBtn,
cancelBtn: !!confirmCancelBtn
});
}
}
// Hiển thị dialog xác nhận
function showConfirmDialog(luckyNumber, count) {
return new Promise((resolve) => {
// Khởi tạo lại nếu chưa có
if (!confirmStartModal || !confirmMessage || !confirmOkBtn || !confirmCancelBtn) {
initConfirmDialog();
}
if (!confirmStartModal || !confirmMessage) {
console.warn('Confirm dialog not found, proceeding without confirmation');
// Nếu không có dialog, tiếp tục luôn
resolve(true);
return;
}
// Cập nhật nội dung message
confirmMessage.textContent = `Đang tạo ${count} tranh từ số ${luckyNumber}...\n\n(Thời gian tạo mất khoảng 1 phút, xin vui lòng chờ)\n\nNhấn OK để bắt đầu hoặc Hủy để hủy bỏ.`;
// Hiển thị modal
confirmStartModal.classList.remove('hidden');
console.log('Confirm dialog shown');
// Xử lý nút OK
const handleOk = () => {
console.log('User clicked OK');
confirmStartModal.classList.add('hidden');
if (confirmOkBtn) confirmOkBtn.removeEventListener('click', handleOk);
if (confirmCancelBtn) confirmCancelBtn.removeEventListener('click', handleCancel);
if (confirmStartModal) confirmStartModal.removeEventListener('click', handleOutsideClick);
resolve(true);
};
// Xử lý nút Cancel
const handleCancel = () => {
console.log('User clicked Cancel');
confirmStartModal.classList.add('hidden');
if (confirmOkBtn) confirmOkBtn.removeEventListener('click', handleOk);
if (confirmCancelBtn) confirmCancelBtn.removeEventListener('click', handleCancel);
if (confirmStartModal) confirmStartModal.removeEventListener('click', handleOutsideClick);
resolve(false);
};
// Đóng khi click bên ngoài
const handleOutsideClick = (e) => {
if (e.target === confirmStartModal) {
handleCancel();
}
};
if (confirmOkBtn) {
confirmOkBtn.addEventListener('click', handleOk);
}
if (confirmCancelBtn) {
confirmCancelBtn.addEventListener('click', handleCancel);
}
if (confirmStartModal) {
confirmStartModal.addEventListener('click', handleOutsideClick);
}
});
}
// Form submit
artForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Validate form trước khi hiển thị dialog
const luckyNumberInput = document.getElementById('lucky-number');
const luckyNumber = luckyNumberInput.value.trim();
if (!luckyNumber) {
alert('Vui lòng nhập text hoặc số may mắn của bạn.');
luckyNumberInput.focus();
return;
}
const countValue = parseInt(document.getElementById('count').value, 10);
if (Number.isNaN(countValue) || countValue < 1 || countValue > 10) {
alert('Số lượng hình phải nằm trong khoảng 1-10.');
document.getElementById('count').focus();
return;
}
// Hiển thị dialog xác nhận (giống desktop app)
const confirmed = await showConfirmDialog(luckyNumber, countValue);
// Nếu người dùng nhấn Hủy hoặc đóng bằng X (result = False), không làm gì
if (!confirmed) {
return;
}
// Nếu người dùng nhấn OK, tiếp tục tạo tranh
await createArt();
});
// Functions
function showModal() {
artModal.classList.remove('hidden');
document.body.style.overflow = 'hidden'; // Ngăn scroll background
}
function hideModal() {
artModal.classList.add('hidden');
document.body.style.overflow = ''; // Cho phép scroll lại
// Reset form
artForm.reset();
currentSessionId = null;
// Ẩn nút minimized khi đóng hoàn toàn
if (minimizedGalleryBtn) {
minimizedGalleryBtn.classList.add('hidden');
}
}
// Thu nhỏ modal
function minimizeModal() {
artModal.classList.add('hidden');
// Chỉ hiển thị nút minimized nếu đang ở gallery view (có hình)
if (minimizedGalleryBtn && galleryImages.length > 0) {
minimizedGalleryBtn.classList.remove('hidden');
}
}
// Khôi phục modal từ minimized
function restoreModal() {
artModal.classList.remove('hidden');
// Ẩn nút minimized
if (minimizedGalleryBtn) {
minimizedGalleryBtn.classList.add('hidden');
}
// Đảm bảo gallery được hiển thị nếu đang có hình
if (galleryImages.length > 0) {
showGallery();
} else {
showInputForm();
}
}
function showInputForm() {
inputForm.classList.remove('hidden');
galleryView.classList.add('hidden');
progressContainer.classList.add('hidden');
hideGlobalProgress();
setStartButtonLoading(false);
// Reset trạng thái nút và tiến độ giống app sau mỗi lần tạo
const submitBtn = document.getElementById('submit-btn');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '🚀 Bắt đầu tạo';
}
updateProgress(0, 'Đang tạo tranh...');
if (progressFill) {
progressFill.style.width = '0%';
}
// Populate lại options từ settings nếu chưa có
if (settingsData) {
populateOptionsFromSettings();
}
// Áp dụng lại giá trị mặc định từ settings khi hiển thị form
applyDefaultValues();
// Setup lại logic loại trừ lẫn nhau (vì có thể form được tạo lại)
setupFibonacciLogic();
}
function showGallery() {
inputForm.classList.add('hidden');
galleryView.classList.remove('hidden');
progressContainer.classList.add('hidden');
setStartButtonLoading(false);
// Đảm bảo nút minimized được ẩn khi hiển thị gallery (vì modal đang mở)
if (minimizedGalleryBtn) {
minimizedGalleryBtn.classList.add('hidden');
}
}
function showProgress() {
progressContainer.classList.remove('hidden');
}
function showGlobalProgress() {
if (globalProgress) {
globalProgress.classList.remove('hidden');
}
}
function hideGlobalProgress() {
if (globalProgress) {
globalProgress.classList.add('hidden');
}
}
function updateProgress(percent, text) {
progressFill.style.width = `${percent}%`;
progressText.textContent = text || `Đang tạo tranh... ${percent}%`;
if (globalProgressFill) {
globalProgressFill.style.width = `${percent}%`;
}
if (globalProgressText) {
globalProgressText.textContent = text || `Đang tạo tranh... ${percent}%`;
}
}
async function createArt() {
try {
// Lấy dữ liệu từ form (đã được validate trong form submit handler)
const luckyNumberInput = document.getElementById('lucky-number');
const luckyNumber = luckyNumberInput.value.trim();
const countValue = parseInt(document.getElementById('count').value, 10);
const paperSizeValue = parseInt(document.getElementById('paper-size').value, 10);
const fibonacciChecked = document.getElementById('use-fibonacci').checked;
const selectedShapeType = document.querySelector('input[name="shape_type"]:checked');
// Logic giống desktop app: nếu Fibonacci được chọn thì shape_type = ""
// Nếu không có Fibonacci, dùng shape_type đã chọn hoặc mặc định "symmetric"
let shapeType = 'symmetric';
if (fibonacciChecked) {
shapeType = ''; // Fibonacci được chọn, không dùng shape_type
} else if (selectedShapeType) {
shapeType = selectedShapeType.value;
}
const formData = {
lucky_number: luckyNumber,
count: countValue,
paper_size: paperSizeValue,
shape_type: shapeType,
use_fibonacci: fibonacciChecked,
bg_color: document.getElementById('bg-color').value,
pen_color: document.getElementById('pen-color').value,
bg_shape: document.getElementById('bg-shape').value,
rotation_offset: 0.0
};
// Hiển thị progress
showProgress();
updateProgress(10, 'Đang gửi request...');
// Disable submit button
const submitBtn = document.getElementById('submit-btn');
submitBtn.disabled = true;
submitBtn.textContent = 'Đang tạo...';
// Thu nhỏ cửa sổ chính, hiển thị thanh tiến trình toàn màn hình
hideModal();
showGlobalProgress();
document.body.style.overflow = 'auto';
setStartButtonLoading(true);
// Gọi API
updateProgress(30, 'Đang tạo tranh...');
const response = await fetch(`${API_BASE_URL}/create-art`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Lỗi tạo tranh');
}
updateProgress(70, 'Đang xử lý kết quả...');
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Lỗi tạo tranh');
}
currentSessionId = result.session_id;
updateProgress(100, 'Hoàn thành!');
// Đợi một chút rồi hiển thị gallery
setTimeout(() => {
loadGallery(result.session_id);
}, 500);
} catch (error) {
console.error('Lỗi:', error);
alert(`Lỗi: ${error.message}`);
updateProgress(0, '');
progressContainer.classList.add('hidden');
hideGlobalProgress();
showModal();
showInputForm();
setStartButtonLoading(false);
// Enable submit button lại
const submitBtn = document.getElementById('submit-btn');
submitBtn.disabled = false;
submitBtn.textContent = 'Bắt đầu tạo';
}
}
async function loadGallery(sessionId) {
try {
// Gọi API lấy danh sách hình
const response = await fetch(`${API_BASE_URL}/gallery/${sessionId}`);
if (!response.ok) {
throw new Error('Không thể lấy danh sách hình');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Lỗi lấy danh sách hình');
}
// Hiển thị gallery
hideGlobalProgress();
showModal();
showGallery();
// Render hình ảnh
renderGallery(result.images || []);
} catch (error) {
console.error('Lỗi:', error);
alert(`Lỗi: ${error.message}`);
hideGlobalProgress();
showModal();
showInputForm();
}
}
function renderGallery(images) {
galleryImages = images || [];
currentGalleryIndex = 0;
setStartButtonLoading(false);
// Debug: log images để kiểm tra
console.log('renderGallery called with', images.length, 'images');
if (images.length > 0) {
console.log('First image object:', images[0]);
}
if (galleryTitle) {
galleryTitle.textContent = `🎨 ${galleryImages.length} Bức Tranh Đã Tạo`;
}
// Không còn nút select nữa
renderGalleryThumbnails();
// Khởi tạo canvas sau khi gallery được hiển thị (đợi DOM update)
// Sử dụng requestAnimationFrame để đảm bảo DOM đã render xong
requestAnimationFrame(() => {
setTimeout(() => {
console.log('Initializing gallery canvas...');
console.log('galleryMainCanvas element:', galleryMainCanvas);
console.log('galleryView element:', galleryView);
console.log('galleryView.classList:', galleryView ? galleryView.classList.toString() : 'null');
if (!galleryMainCanvas) {
console.error('galleryMainCanvas is null!');
return;
}
initGalleryCanvas();
if (galleryImages.length > 0) {
console.log('Loading first image to canvas, total images:', galleryImages.length);
setGalleryImage(0);
} else {
console.log('No images to display');
// Vẽ nền trắng nếu không có hình
if (galleryMainCanvas && galleryCanvasCtx) {
galleryCanvasCtx.fillStyle = '#ffffff';
galleryCanvasCtx.fillRect(0, 0, galleryMainCanvas.width, galleryMainCanvas.height);
}
}
}, 100); // Tăng timeout lên 100ms để đảm bảo DOM đã render
});
if (galleryImages.length === 0) {
if (galleryMainCanvas && galleryCanvasCtx) {
galleryCanvasCtx.clearRect(0, 0, galleryMainCanvas.width, galleryMainCanvas.height);
}
updateGalleryInfo(null);
updateGalleryImageLabel();
if (galleryThumbnails) {
galleryThumbnails.innerHTML = 'Chưa có hình nào
';
}
if (galleryPrevBtn) galleryPrevBtn.disabled = true;
if (galleryNextBtn) galleryNextBtn.disabled = true;
}
}
function renderGalleryThumbnails() {
if (!galleryThumbnails) return;
galleryThumbnails.innerHTML = '';
galleryImages.forEach((image, index) => {
const thumb = document.createElement('button');
thumb.type = 'button';
thumb.className = 'thumbnail-item';
thumb.dataset.index = index;
// Luôn dùng clean image cho thumbnails (không có thông số)
let imageSrc = '';
if (image.clean_image_url) {
imageSrc = image.clean_image_url;
} else if (image.image_url) {
// Nếu không có clean_image_url, thử thay đổi URL để lấy clean image
imageSrc = image.image_url.replace('?type=full', '?type=clean').replace(/\/api\/image\/([^\/]+)\/(\d+)$/, '/api/image/$1/$2?type=clean');
} else if (currentSessionId) {
// Fallback: tạo URL clean image
imageSrc = `${API_BASE_URL}/image/${currentSessionId}/${image.index}?type=clean`;
}
// Resolve URL nếu cần
if (imageSrc && !imageSrc.startsWith('http')) {
if (imageSrc.startsWith('/api')) {
const baseUrl = API_BASE_URL.replace(/\/api\/?$/, '');
imageSrc = baseUrl + imageSrc;
} else {
imageSrc = API_BASE_URL + (imageSrc.startsWith('/') ? '' : '/') + imageSrc;
}
}
thumb.innerHTML = ` `;
thumb.addEventListener('click', () => setGalleryImage(index));
galleryThumbnails.appendChild(thumb);
});
}
function updateThumbnailActive() {
if (!galleryThumbnails) return;
Array.from(galleryThumbnails.children).forEach((child, idx) => {
if (idx === currentGalleryIndex) {
child.classList.add('active');
} else {
child.classList.remove('active');
}
});
}
function resolveImageSrc(image, preferClean = true) {
if (!image) {
console.error('resolveImageSrc: image is null');
return '';
}
let imageSrc = '';
// Luôn ưu tiên clean_image_url nếu preferClean = true
if (preferClean) {
if (image.clean_image_url) {
imageSrc = image.clean_image_url;
} else if (image.image_url) {
// Nếu không có clean_image_url, thử thay đổi URL để lấy clean image
imageSrc = image.image_url.replace('?type=full', '?type=clean').replace(/\/api\/image\/([^\/]+)\/(\d+)$/, '/api/image/$1/$2?type=clean');
} else if (currentSessionId && image.index) {
// Fallback: tạo URL clean image
imageSrc = `/api/image/${currentSessionId}/${image.index}?type=clean`;
}
} else {
// Nếu không preferClean, dùng image_url
if (image.image_url) {
imageSrc = image.image_url;
} else if (currentSessionId && image.index) {
imageSrc = `/api/image/${currentSessionId}/${image.index}?type=full`;
}
}
if (!imageSrc) {
console.warn('No image URL found in image object:', image);
return '';
}
if (!imageSrc.startsWith('http')) {
if (imageSrc.startsWith('/api')) {
const baseUrl = API_BASE_URL.replace(/\/api\/?$/, '');
imageSrc = baseUrl + imageSrc;
} else {
imageSrc = API_BASE_URL + (imageSrc.startsWith('/') ? '' : '/') + imageSrc;
}
}
console.log('Resolved image src:', imageSrc, '(preferClean:', preferClean, ')');
return imageSrc;
}
function navigateGallery(delta) {
if (!galleryImages.length) return;
const newIndex = currentGalleryIndex + delta;
if (newIndex < 0 || newIndex >= galleryImages.length) return;
setGalleryImage(newIndex);
}
function updateGalleryNavButtons() {
if (galleryPrevBtn) {
galleryPrevBtn.disabled = currentGalleryIndex <= 0;
}
if (galleryNextBtn) {
galleryNextBtn.disabled = currentGalleryIndex >= galleryImages.length - 1;
}
}
function updateGalleryInfo(metadata) {
if (!galleryInfoText) {
console.warn('galleryInfoText element not found');
return;
}
if (!metadata) {
galleryInfoText.textContent = 'Đang tải thông tin...';
return;
}
const paperSize = metadata.paper_size || 'N/A';
const bgShape = metadata.bg_shape || 'vuông';
const bgColor = metadata.bg_color || 'N/A';
const penColor = metadata.pen_color || 'N/A';
let sizeText = '';
if (bgShape.toLowerCase() === 'tròn') {
sizeText = `Đường kính D= ${paperSize} mm`;
} else {
sizeText = `${paperSize}x${paperSize} mm`;
}
galleryInfoText.textContent = `Kích thước tranh: ${sizeText} | Loại hình: ${bgShape} | Màu nền: ${bgColor} | Màu nét vẽ: ${penColor}`;
console.log('Gallery info updated:', galleryInfoText.textContent);
}
function updateGalleryImageLabel() {
if (galleryImageLabelText) {
galleryImageLabelText.textContent = `Hình ${currentGalleryIndex + 1}/${galleryImages.length}`;
}
}
async function setGalleryImage(index) {
console.log('setGalleryImage called with index:', index);
if (!galleryImages.length || index < 0 || index >= galleryImages.length) {
console.error('Invalid index or no images:', index, 'total:', galleryImages.length);
return;
}
currentGalleryIndex = index;
const currentImage = galleryImages[index];
console.log('Current image object:', currentImage);
// Dừng GIF nếu đang chạy
if (isGifMode) {
switchToImageMode();
}
// Reset drag offset khi chuyển hình
imageOffsetX = 0;
imageOffsetY = 0;
isDragging = false;
// Load image vào canvas
console.log('Calling loadImageToGalleryCanvas...');
await loadImageToGalleryCanvas(currentImage);
console.log('loadImageToGalleryCanvas completed');
updateGalleryNavButtons();
updateThumbnailActive();
updateGalleryInfo(currentImage.metadata);
updateGalleryImageLabel();
// Load metadata và path_data ngay lập tức
if (currentImage.path_data) {
// Nếu đã có path_data, extract metadata
currentPathData = currentImage.path_data;
if (!currentImage.metadata) {
currentImage.metadata = {
paper_size: currentImage.path_data.paper_size || '-',
bg_shape: currentImage.path_data.bg_shape || '-',
bg_color: currentImage.path_data.bg_color || '-',
pen_color: currentImage.path_data.pen_color || '-'
};
}
updateGalleryInfo(currentImage.metadata);
} else if (currentImage.path_data_url) {
// Load path_data từ URL
try {
console.log('Loading path data from:', currentImage.path_data_url);
// Resolve URL đúng cách
let pathDataUrl = currentImage.path_data_url;
if (!pathDataUrl.startsWith('http')) {
if (pathDataUrl.startsWith('/api')) {
const baseUrl = API_BASE_URL.replace(/\/api\/?$/, '');
pathDataUrl = baseUrl + pathDataUrl;
} else {
pathDataUrl = API_BASE_URL + (pathDataUrl.startsWith('/') ? '' : '/') + pathDataUrl;
}
}
console.log('Resolved path data URL:', pathDataUrl);
const response = await fetch(pathDataUrl);
if (response.ok) {
const data = await response.json();
console.log('Path data loaded:', data);
currentImage.path_data = data;
currentPathData = data;
// Extract metadata từ path_data
currentImage.metadata = {
paper_size: data.paper_size || '-',
bg_shape: data.bg_shape || '-',
bg_color: data.bg_color || '-',
pen_color: data.pen_color || '-'
};
updateGalleryInfo(currentImage.metadata);
} else {
console.error('Failed to load path data, status:', response.status);
updateGalleryInfo(null);
}
} catch (err) {
console.error('Error loading path data:', err);
updateGalleryInfo(null);
}
} else if (currentImage.metadata) {
// Nếu đã có metadata, hiển thị ngay
updateGalleryInfo(currentImage.metadata);
} else {
// Nếu không có gì, hiển thị thông báo
updateGalleryInfo(null);
}
}
function viewImage(imageIndex) {
// Mở hình trong tab mới hoặc modal lớn hơn
const imageEntry = galleryImages.find((img) => img.index === imageIndex);
const imageUrl = resolveImageSrc(imageEntry || { image_url: `/api/image/${currentSessionId}/${imageIndex}?type=clean` }, true);
window.open(imageUrl, '_blank');
}
// Hiển thị dialog tải DXF (giống desktop app)
function showDXFDownloadDialog(imageIndex) {
return new Promise((resolve) => {
// Lấy các element
const dxfModal = document.getElementById('dxf-download-modal');
const emailInput = document.getElementById('dxf-email-input');
const codeInput = document.getElementById('dxf-code-input');
const confirmBtn = document.getElementById('dxf-confirm-btn');
const cancelBtn = document.getElementById('dxf-cancel-btn');
const closeBtn = document.getElementById('close-dxf-modal-btn');
if (!dxfModal || !emailInput || !codeInput || !confirmBtn || !cancelBtn) {
console.warn('DXF dialog elements not found');
resolve(null);
return;
}
// Reset form
emailInput.value = '';
codeInput.value = '';
// Hiển thị modal
dxfModal.classList.remove('hidden');
// Focus vào email input
setTimeout(() => emailInput.focus(), 100);
// Validation email
function validateEmail(email) {
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return pattern.test(email);
}
// Check email domain (async)
async function checkEmailDomain(email) {
try {
const domain = email.split('@')[1];
// Sử dụng DNS lookup qua API hoặc đơn giản chỉ validate format
// Trên web, việc check DNS domain phức tạp, nên chỉ validate format
return true;
} catch (e) {
return false;
}
}
// Xử lý xác nhận
const handleConfirm = async () => {
const email = emailInput.value.trim();
const code = codeInput.value.trim();
if (!email) {
alert('Vui lòng nhập email!');
return;
}
if (!validateEmail(email)) {
alert('Email không hợp lệ!');
return;
}
// Check domain (có thể bỏ qua trên web vì phức tạp)
// const domainValid = await checkEmailDomain(email);
// if (!domainValid) {
// if (!confirm('Không thể xác minh domain email!\n\nBạn có chắc chắn muốn tiếp tục không?')) {
// return;
// }
// }
if (!code) {
alert('Vui lòng nhập mã tải file!');
return;
}
// Đóng modal
dxfModal.classList.add('hidden');
// Remove listeners
confirmBtn.removeEventListener('click', handleConfirm);
cancelBtn.removeEventListener('click', handleCancel);
if (closeBtn) closeBtn.removeEventListener('click', handleCancel);
if (dxfModal) dxfModal.removeEventListener('click', handleOutsideClick);
// Resolve với email và code
resolve({ email, code });
};
// Xử lý hủy
const handleCancel = () => {
dxfModal.classList.add('hidden');
// Remove listeners
confirmBtn.removeEventListener('click', handleConfirm);
cancelBtn.removeEventListener('click', handleCancel);
if (closeBtn) closeBtn.removeEventListener('click', handleCancel);
if (dxfModal) dxfModal.removeEventListener('click', handleOutsideClick);
resolve(null);
};
// Đóng khi click bên ngoài
const handleOutsideClick = (e) => {
if (e.target === dxfModal) {
handleCancel();
}
};
// Enter key để xác nhận
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleConfirm();
} else if (e.key === 'Escape') {
handleCancel();
}
};
// Thêm event listeners
confirmBtn.addEventListener('click', handleConfirm);
cancelBtn.addEventListener('click', handleCancel);
if (closeBtn) closeBtn.addEventListener('click', handleCancel);
if (dxfModal) dxfModal.addEventListener('click', handleOutsideClick);
emailInput.addEventListener('keydown', handleKeyPress);
codeInput.addEventListener('keydown', handleKeyPress);
});
}
async function downloadDXF(imageIndex) {
try {
// Hiển thị dialog nhập email và mã tải (giống desktop app)
const result = await showDXFDownloadDialog(imageIndex);
if (!result) {
return; // Người dùng hủy
}
const { email, code } = result;
// Gọi API download DXF
const response = await fetch(`${API_BASE_URL}/download-dxf`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: currentSessionId,
image_index: imageIndex,
email: email,
download_code: code
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Lỗi tải DXF');
}
const apiResult = await response.json();
if (apiResult.success) {
// Tải file DXF xuống thư mục Downloads của khách hàng
const dxfUrl = `${API_BASE_URL}${apiResult.dxf_url}`;
const dxfFilename = apiResult.dxf_filename || 'drawing.dxf';
try {
// Dùng fetch để tải file dưới dạng blob
const fileResponse = await fetch(dxfUrl);
if (!fileResponse.ok) {
throw new Error('Không thể tải file DXF');
}
// Chuyển response thành blob
const blob = await fileResponse.blob();
// Tạo URL từ blob
const blobUrl = window.URL.createObjectURL(blob);
// Tạo link tải tự động
const link = document.createElement('a');
link.href = blobUrl;
link.download = dxfFilename; // Tên file sẽ được lưu
link.style.display = 'none'; // Ẩn link
// Thêm vào DOM trước khi click
document.body.appendChild(link);
// Trigger click để tải file
link.click();
// Dọn dẹp: xóa link và revoke blob URL
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
}, 100);
// Thông báo thành công (không làm thay đổi trang)
alert(`Đã tải file DXF thành công!\n\nFile: ${dxfFilename}\nĐã được lưu vào thư mục Downloads.`);
} catch (downloadError) {
console.error('Lỗi tải file:', downloadError);
// Fallback: thử cách cũ với link trực tiếp
const link = document.createElement('a');
link.href = dxfUrl;
link.download = dxfFilename;
link.target = '_blank'; // Mở trong tab mới nếu cần
link.style.display = 'none';
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
}, 100);
alert(`Đã tải file DXF thành công!\n\nFile: ${dxfFilename}\nĐã được lưu vào thư mục Downloads.`);
}
} else {
throw new Error(apiResult.error || 'Lỗi tải DXF');
}
} catch (error) {
console.error('Lỗi tải DXF:', error);
alert(`Lỗi tải DXF: ${error.message}\n\nVui lòng thử lại.`);
// KHÔNG đóng gallery, chỉ hiển thị thông báo lỗi
// Gallery vẫn mở để người dùng có thể thử lại hoặc làm việc khác
}
}
// Cửa sổ "Hình Đã Chọn"
function openSelectedImageWindow(index) {
if (index < 0 || index >= galleryImages.length) return;
currentSelectedImageIndex = index;
const image = galleryImages[index];
if (selectedImageTitle) {
selectedImageTitle.textContent = `🎨 Hình #${image.index} - Đã Chọn`;
}
// Hiển thị thông tin cơ bản
if (selectedBasicInfoText && image.metadata) {
const metadata = image.metadata;
const paperSize = metadata.paper_size || 'N/A';
const bgShape = metadata.bg_shape || 'vuông';
const bgColor = metadata.bg_color || 'N/A';
const penColor = metadata.pen_color || 'N/A';
let sizeText = '';
if (bgShape.toLowerCase() === 'tròn') {
sizeText = `Đường kính D= ${paperSize} mm`;
} else {
sizeText = `${paperSize}x${paperSize} mm`;
}
selectedBasicInfoText.textContent = `Kích thước tranh: ${sizeText} | Loại hình: ${bgShape} | Màu nền: ${bgColor} | Màu nét vẽ: ${penColor}`;
}
// Load và vẽ hình lên canvas
loadImageToCanvas(image);
// Reset zoom
currentZoomLevel = 0.3;
updateZoomDisplay();
// Hiển thị cửa sổ
if (selectedImageWindow) {
selectedImageWindow.classList.remove('hidden');
}
// Reset nút GIF/Hình
if (selectedViewGifBtn) {
selectedViewGifBtn.disabled = false;
selectedViewGifBtn.style.background = '#6BA3D6';
selectedViewGifBtn.style.color = 'black';
}
if (selectedViewImageBtn) {
selectedViewImageBtn.disabled = true;
selectedViewImageBtn.style.background = '#9BC4E2';
selectedViewImageBtn.style.color = 'darkgray';
}
}
function hideSelectedImageWindow() {
if (selectedImageWindow) {
selectedImageWindow.classList.add('hidden');
}
currentSelectedImageIndex = null;
currentImageObj = null;
}
async function loadImageToCanvas(image) {
if (!selectedImageCanvas) return;
const ctx = selectedImageCanvas.getContext('2d');
if (!ctx) return;
canvasCtx = ctx;
const imageUrl = resolveImageSrc(image, true);
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
currentImageObj = img;
drawCanvas();
};
img.onerror = () => {
console.error('Lỗi tải hình');
};
img.src = imageUrl;
}
function drawCanvas() {
if (!selectedImageCanvas || !canvasCtx || !currentImageObj) return;
const canvas = selectedImageCanvas;
const ctx = canvasCtx;
const img = currentImageObj;
// Tính kích thước canvas dựa trên zoom
const actualZoom = currentZoomLevel * 2.0;
const canvasWidth = canvas.parentElement.clientWidth - 20;
const canvasHeight = 500;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// Tính kích thước hình với zoom
const imgWidth = img.width * actualZoom;
const imgHeight = img.height * actualZoom;
// Căn giữa hình
const x = (canvasWidth - imgWidth) / 2;
const y = (canvasHeight - imgHeight) / 2;
// Xóa canvas
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// Vẽ hình
ctx.drawImage(img, x, y, imgWidth, imgHeight);
}
function zoomCanvas(factor) {
currentZoomLevel *= factor;
if (currentZoomLevel < 0.2) currentZoomLevel = 0.2;
if (currentZoomLevel > 1.0) currentZoomLevel = 1.0;
updateZoomDisplay();
drawCanvas();
}
function resetZoom() {
currentZoomLevel = 0.3;
updateZoomDisplay();
drawCanvas();
}
function updateZoomDisplay() {
if (zoomLevelText) {
zoomLevelText.textContent = `${Math.round(currentZoomLevel * 100)}%`;
}
}
// ============================================
// Gallery Canvas Functions (giống desktop app)
// ============================================
// Khởi tạo canvas khi gallery được hiển thị
function initGalleryCanvas() {
console.log('initGalleryCanvas called');
if (!galleryMainCanvas) {
console.error('galleryMainCanvas not found');
return;
}
console.log('galleryMainCanvas found, getting context...');
if (!galleryCanvasCtx) {
galleryCanvasCtx = galleryMainCanvas.getContext('2d');
console.log('Canvas context obtained:', galleryCanvasCtx ? 'success' : 'failed');
} else {
console.log('Canvas context already exists');
}
// Set canvas size
console.log('Updating canvas size...');
updateCanvasSize();
console.log('Canvas size updated, width:', galleryMainCanvas.width, 'height:', galleryMainCanvas.height);
// Resize canvas khi window resize (chỉ thêm listener một lần)
if (!window._galleryResizeListenerAdded) {
window.addEventListener('resize', () => {
updateCanvasSize();
if (galleryImages[currentGalleryIndex]) {
loadImageToGalleryCanvas(galleryImages[currentGalleryIndex]);
}
});
window._galleryResizeListenerAdded = true;
}
// Thêm event listener cho mouse wheel zoom (chỉ thêm một lần)
if (!galleryMainCanvas._wheelListenerAdded) {
galleryMainCanvas.addEventListener('wheel', (event) => {
event.preventDefault(); // Ngăn scroll trang
onCanvasMouseWheel(event);
}, { passive: false });
galleryMainCanvas._wheelListenerAdded = true;
console.log('Mouse wheel zoom listener added to canvas');
}
// Thêm event listener cho drag/pan (chỉ thêm một lần)
if (!galleryMainCanvas._dragListenerAdded) {
galleryMainCanvas.addEventListener('mousedown', onCanvasMouseDown);
galleryMainCanvas.addEventListener('mousemove', onCanvasMouseMove);
galleryMainCanvas.addEventListener('mouseup', onCanvasMouseUp);
galleryMainCanvas.addEventListener('mouseleave', onCanvasMouseUp); // Dừng drag khi chuột ra khỏi canvas
galleryMainCanvas._dragListenerAdded = true;
console.log('Drag/pan listeners added to canvas');
}
}
// Update canvas size
function updateCanvasSize() {
if (!galleryMainCanvas) return;
const wrapper = galleryMainCanvas.parentElement;
if (wrapper) {
const rect = wrapper.getBoundingClientRect();
// Đảm bảo có kích thước hợp lệ (dùng clientWidth/clientHeight thay vì getBoundingClientRect)
const width = Math.max(wrapper.clientWidth || rect.width || 600, 400);
const height = Math.max(wrapper.clientHeight || rect.height || 500, 400);
galleryMainCanvas.width = width;
galleryMainCanvas.height = height;
// Set CSS size để canvas hiển thị đúng
galleryMainCanvas.style.width = width + 'px';
galleryMainCanvas.style.height = height + 'px';
console.log(`Canvas size updated: ${width}x${height}`);
} else {
console.error('Canvas wrapper not found');
}
}
// Load image vào canvas
async function loadImageToGalleryCanvas(image) {
if (!galleryMainCanvas) {
console.error('galleryMainCanvas not found in loadImageToGalleryCanvas');
return;
}
if (!galleryCanvasCtx) {
initGalleryCanvas();
if (!galleryCanvasCtx) {
console.error('Cannot get canvas context');
return;
}
}
// Update canvas size trước khi vẽ
updateCanvasSize();
// Luôn dùng clean image (không có thông số) cho canvas
const imageSrc = resolveImageSrc(image, true);
if (!imageSrc) {
console.error('No image source found for image:', image);
// Vẽ thông báo lỗi
if (galleryCanvasCtx) {
galleryCanvasCtx.fillStyle = '#ffffff';
galleryCanvasCtx.fillRect(0, 0, galleryMainCanvas.width, galleryMainCanvas.height);
galleryCanvasCtx.fillStyle = '#ff0000';
galleryCanvasCtx.font = '16px Arial';
galleryCanvasCtx.fillText('Không tìm thấy hình ảnh', 10, 30);
}
return;
}
console.log('Loading clean image to canvas:', imageSrc);
try {
const img = new Image();
// Không set crossOrigin nếu là cùng origin
if (imageSrc.startsWith('http') && !imageSrc.includes(window.location.hostname)) {
img.crossOrigin = 'anonymous';
}
await new Promise((resolve, reject) => {
img.onload = () => {
console.log('Image loaded successfully:', img.width, 'x', img.height);
resolve();
};
img.onerror = (err) => {
console.error('Image load error:', err, 'URL:', imageSrc);
reject(err);
};
img.src = imageSrc;
});
// Vẽ image với zoom (giống desktop app: zoom_level * 2.0)
const actualZoom = currentZoomLevel * 2.0;
const canvasWidth = galleryMainCanvas.width;
const canvasHeight = galleryMainCanvas.height;
const imgWidth = img.width * actualZoom;
const imgHeight = img.height * actualZoom;
// Tính vị trí vẽ hình (căn giữa + offset từ drag)
let x = (canvasWidth - imgWidth) / 2 + imageOffsetX;
let y = (canvasHeight - imgHeight) / 2 + imageOffsetY;
console.log(`Drawing image at zoom ${currentZoomLevel} (actual: ${actualZoom}), size: ${imgWidth}x${imgHeight}, pos: ${x}, ${y}, offset: ${imageOffsetX}, ${imageOffsetY}`);
// Vẽ nền trắng trước (đảm bảo canvas luôn có background)
galleryCanvasCtx.fillStyle = '#ffffff';
galleryCanvasCtx.fillRect(0, 0, canvasWidth, canvasHeight);
// Vẽ hình (chỉ vẽ nếu có kích thước hợp lệ)
if (imgWidth > 0 && imgHeight > 0 && canvasWidth > 0 && canvasHeight > 0) {
console.log('Drawing image to canvas at position:', x, y);
galleryCanvasCtx.drawImage(img, x, y, imgWidth, imgHeight);
console.log('Image drawn successfully to canvas');
} else {
console.warn('Image size invalid, skipping draw. imgWidth:', imgWidth, 'imgHeight:', imgHeight, 'canvasWidth:', canvasWidth, 'canvasHeight:', canvasHeight);
}
currentImageObj = img;
// Force canvas to update (thử vẽ lại để đảm bảo)
if (galleryMainCanvas) {
console.log('Canvas final state - width:', galleryMainCanvas.width, 'height:', galleryMainCanvas.height, 'style.width:', galleryMainCanvas.style.width, 'style.height:', galleryMainCanvas.style.height);
}
} catch (err) {
console.error('Không thể tải hình vào canvas:', err);
// Vẽ thông báo lỗi
if (galleryCanvasCtx) {
galleryCanvasCtx.fillStyle = 'white';
galleryCanvasCtx.fillRect(0, 0, galleryMainCanvas.width, galleryMainCanvas.height);
galleryCanvasCtx.fillStyle = 'red';
galleryCanvasCtx.font = '16px Arial';
galleryCanvasCtx.fillText('Lỗi tải hình', 10, 30);
}
}
}
// Zoom canvas
function zoomGalleryCanvas(factor) {
currentZoomLevel = Math.max(0.2, Math.min(1.0, currentZoomLevel * factor));
updateGalleryZoomLabel();
if (galleryImages[currentGalleryIndex]) {
loadImageToGalleryCanvas(galleryImages[currentGalleryIndex]);
}
}
// Reset zoom
function resetGalleryZoom() {
currentZoomLevel = 0.25; // 25%
imageOffsetX = 0; // Reset offset khi reset zoom
imageOffsetY = 0;
updateGalleryZoomLabel();
if (galleryImages[currentGalleryIndex]) {
loadImageToGalleryCanvas(galleryImages[currentGalleryIndex]);
}
}
// Debounce cho zoom để tránh nháy
let zoomTimeout = null;
let pendingZoomLevel = null;
let isZooming = false; // Flag để tránh zoom quá nhiều lần
// Xử lý mouse down để bắt đầu drag
function onCanvasMouseDown(event) {
// Chỉ drag khi đang ở chế độ xem hình (không phải GIF)
if (isGifMode) {
return;
}
isDragging = true;
dragStartX = event.clientX - imageOffsetX;
dragStartY = event.clientY - imageOffsetY;
galleryMainCanvas.style.cursor = 'grabbing';
}
// Xử lý mouse move để drag hình
function onCanvasMouseMove(event) {
if (!isDragging || isGifMode) {
return;
}
// Tính offset mới
imageOffsetX = event.clientX - dragStartX;
imageOffsetY = event.clientY - dragStartY;
// Reload hình với offset mới
if (galleryImages[currentGalleryIndex]) {
loadImageToGalleryCanvas(galleryImages[currentGalleryIndex]);
}
}
// Xử lý mouse up để kết thúc drag
function onCanvasMouseUp(event) {
if (isDragging) {
isDragging = false;
galleryMainCanvas.style.cursor = 'grab';
}
}
// Xử lý mouse wheel zoom (giống desktop app)
function onCanvasMouseWheel(event) {
// Chỉ zoom khi đang ở chế độ xem hình (không phải GIF)
if (isGifMode) {
return;
}
// Xác định hướng zoom: deltaY < 0 = zoom in, deltaY > 0 = zoom out
const zoomFactor = event.deltaY < 0 ? 1.1 : 1.0 / 1.1;
// Zoom tại trung tâm canvas (giống desktop app)
const oldZoom = currentZoomLevel;
let newZoom = oldZoom * zoomFactor;
// Giới hạn zoom (giống desktop app: 0.2 - 1.0)
const minZoom = 0.2;
const maxZoom = 1.0;
// Tính max zoom an toàn dựa trên kích thước canvas và hình
if (newZoom > 1.0 && currentImageObj) {
const canvasWidth = galleryMainCanvas.width;
const canvasHeight = galleryMainCanvas.height;
if (canvasWidth > 1 && canvasHeight > 1) {
const imgWidth = currentImageObj.width;
const imgHeight = currentImageObj.height;
if (imgWidth > 0 && imgHeight > 0) {
const maxZoomX = (canvasWidth * 0.95) / imgWidth;
const maxZoomY = (canvasHeight * 0.95) / imgHeight;
const maxZoomSafeActual = Math.min(maxZoomX, maxZoomY, maxZoom * 2.0);
const maxZoomSafeActualClamped = Math.max(maxZoomSafeActual, 2.0);
const maxZoomSafe = maxZoomSafeActualClamped / 2.0;
newZoom = Math.min(newZoom, maxZoomSafe);
}
}
}
if (newZoom < minZoom) {
newZoom = minZoom;
} else if (newZoom > maxZoom) {
newZoom = maxZoom;
}
// Chỉ update nếu thay đổi đáng kể (tránh zoom quá nhiều lần)
if (Math.abs(newZoom - oldZoom) < 0.01) {
return;
}
// Cập nhật zoom level ngay lập tức (để label cập nhật)
currentZoomLevel = newZoom;
updateGalleryZoomLabel();
// Debounce việc reload hình để tránh nháy - tăng delay và dùng requestAnimationFrame
pendingZoomLevel = newZoom;
if (zoomTimeout) {
cancelAnimationFrame(zoomTimeout);
}
// Dùng requestAnimationFrame để mượt hơn
zoomTimeout = requestAnimationFrame(() => {
setTimeout(() => {
if (pendingZoomLevel !== null && galleryImages[currentGalleryIndex]) {
currentZoomLevel = pendingZoomLevel;
loadImageToGalleryCanvas(galleryImages[currentGalleryIndex]);
pendingZoomLevel = null;
}
}, 150); // Tăng delay lên 150ms để mượt hơn
});
}
// Update zoom label
function updateGalleryZoomLabel() {
if (galleryZoomLevelText) {
galleryZoomLevelText.textContent = `${Math.round(currentZoomLevel * 100)}%`;
}
}
// Switch to GIF mode
function switchToGifMode() {
if (isGifMode) return;
// Kiểm tra có path_data không
const currentImage = galleryImages[currentGalleryIndex];
if (!currentImage) {
console.error('No current image');
alert('Không có hình ảnh để hiển thị GIF');
return;
}
// Nếu chưa có path_data, thử load
if (!currentPathData) {
if (currentImage.path_data) {
// Nếu đã có path_data trong image object
currentPathData = currentImage.path_data;
console.log('Using existing path_data from image object');
} else if (currentImage.path_data_url) {
// Load từ URL
console.log('Loading path data for GIF from URL...');
loadPathDataForGif(currentImage.path_data_url).then((success) => {
if (success && currentPathData) {
console.log('Path data loaded successfully, starting GIF...');
isGifMode = true;
updateGifButtons();
if (currentZoomLevel < 0.25) {
currentZoomLevel = 0.25;
updateGalleryZoomLabel();
}
startGifAnimation();
} else {
console.error('Failed to load path data');
alert('Không thể tải dữ liệu để hiển thị GIF. Vui lòng thử lại.');
}
}).catch((err) => {
console.error('Error in loadPathDataForGif promise:', err);
alert('Lỗi khi tải dữ liệu GIF: ' + err.message);
});
return;
} else {
console.error('No path_data or path_data_url available');
console.log('Current image object:', currentImage);
alert('Không có dữ liệu để hiển thị GIF. Vui lòng thử tạo hình lại.');
return;
}
}
// Nếu đã có path_data, bắt đầu GIF ngay
isGifMode = true;
updateGifButtons();
// Đảm bảo zoom level >= 25%
if (currentZoomLevel < 0.25) {
currentZoomLevel = 0.25;
updateGalleryZoomLabel();
}
startGifAnimation();
}
function updateGifButtons() {
if (galleryViewGifBtn) {
galleryViewGifBtn.disabled = isGifMode;
galleryViewGifBtn.style.background = isGifMode ? '#9BC4E2' : '#6BA3D6';
galleryViewGifBtn.style.color = isGifMode ? 'darkgray' : 'black';
}
if (galleryViewImageBtn) {
galleryViewImageBtn.disabled = !isGifMode;
galleryViewImageBtn.style.background = !isGifMode ? '#9BC4E2' : '#6BA3D6';
galleryViewImageBtn.style.color = !isGifMode ? 'darkgray' : 'black';
}
}
// Switch to Image mode
function switchToImageMode() {
if (!isGifMode) return;
isGifMode = false;
if (animationFrameId) {
clearTimeout(animationFrameId);
animationFrameId = null;
}
// Reset drag state
isDragging = false;
updateGifButtons();
// Reload image
if (galleryImages[currentGalleryIndex]) {
loadImageToGalleryCanvas(galleryImages[currentGalleryIndex]);
}
}
// Load path data cho GIF animation
async function loadPathDataForGif(pathDataUrl) {
try {
console.log('Loading path data for GIF from:', pathDataUrl);
// Resolve URL đúng cách
let url = pathDataUrl;
if (!url.startsWith('http')) {
if (url.startsWith('/api')) {
const baseUrl = API_BASE_URL.replace(/\/api\/?$/, '');
url = baseUrl + url;
} else {
url = API_BASE_URL + (url.startsWith('/') ? '' : '/') + url;
}
}
console.log('Resolved URL for GIF:', url);
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
console.log('Path data loaded for GIF:', data);
currentPathData = data;
if (galleryImages[currentGalleryIndex]) {
galleryImages[currentGalleryIndex].path_data = data;
// Cũng extract metadata
if (!galleryImages[currentGalleryIndex].metadata) {
galleryImages[currentGalleryIndex].metadata = {
paper_size: data.paper_size || '-',
bg_shape: data.bg_shape || '-',
bg_color: data.bg_color || '-',
pen_color: data.pen_color || '-'
};
}
}
return true;
} else {
console.error('Failed to load path data, status:', response.status);
}
} catch (err) {
console.error('Error loading path data for GIF:', err);
}
return false;
}
// Start GIF animation (giống desktop app)
function startGifAnimation() {
if (!currentPathData || !galleryMainCanvas || !galleryCanvasCtx) {
console.error('Cannot start GIF animation: missing data or canvas');
return;
}
if (animationFrameId) {
clearTimeout(animationFrameId);
animationFrameId = null;
}
try {
const path_x = currentPathData.path_x || [];
const path_y = currentPathData.path_y || [];
const paper_size = currentPathData.paper_size || 400;
const bg_color = currentPathData.bg_color || 'trắng';
const pen_color = currentPathData.pen_color || 'đen';
const bg_shape = currentPathData.bg_shape || 'vuông';
const rotation_offset = parseFloat(currentPathData.rotation_offset || 0);
if (path_x.length === 0 || path_y.length === 0) {
console.error('Invalid path data');
return;
}
const colorMapBg = { "trắng": "#ffffff", "đen": "#000000", "xám": "#808080" };
const colorMapPen = { "đen": "#000000", "đỏ": "#ff0000", "vàng": "#ffff00" };
const bgColorHex = colorMapBg[bg_color] || "#ffffff";
const penColorHex = colorMapPen[pen_color] || "#000000";
const canvasWidth = galleryMainCanvas.width;
const canvasHeight = galleryMainCanvas.height;
const actualZoom = currentZoomLevel * 2.0;
const half = paper_size / 2;
const margin = 5;
const totalSizeMm = paper_size + 2 * margin;
let scale;
if (currentImageObj) {
const imgWidth = currentImageObj.width;
const zoomedWidth = imgWidth * actualZoom;
scale = zoomedWidth / totalSizeMm;
} else {
scale = actualZoom;
}
const center_x = canvasWidth / 2;
const center_y = canvasHeight / 2;
function drawBackground() {
galleryCanvasCtx.fillStyle = '#ffffff';
galleryCanvasCtx.fillRect(0, 0, canvasWidth, canvasHeight);
const shadowLayers = 4;
const shadowMaxSize = 6.0;
const shadowBaseAlpha = 0.15;
for (let layer = shadowLayers; layer > 0; layer--) {
const layerSize = shadowMaxSize * (layer / shadowLayers);
const grayValue = Math.floor(255 * (1 - shadowBaseAlpha * (layer / shadowLayers)));
const shadowColor = `rgb(${grayValue}, ${grayValue}, ${grayValue})`;
const shadowRadius = (half + layerSize) * scale;
galleryCanvasCtx.fillStyle = shadowColor;
if (bg_shape.toLowerCase() === "tròn") {
galleryCanvasCtx.beginPath();
galleryCanvasCtx.arc(center_x, center_y, shadowRadius, 0, Math.PI * 2);
galleryCanvasCtx.fill();
} else {
galleryCanvasCtx.fillRect(center_x - shadowRadius, center_y - shadowRadius, shadowRadius * 2, shadowRadius * 2);
}
}
galleryCanvasCtx.fillStyle = bgColorHex;
if (bg_shape.toLowerCase() === "tròn") {
galleryCanvasCtx.beginPath();
galleryCanvasCtx.arc(center_x, center_y, half * scale, 0, Math.PI * 2);
galleryCanvasCtx.fill();
} else {
galleryCanvasCtx.fillRect(center_x - half * scale, center_y - half * scale, half * scale * 2, half * scale * 2);
}
}
drawBackground();
let currentPointIndex = 0;
const totalPoints = path_x.length;
const pointsPerSecond = 400;
const delayMs = 1000 / pointsPerSecond;
function drawFrame() {
if (!isGifMode || currentPointIndex >= totalPoints) {
animationFrameId = null;
return;
}
galleryCanvasCtx.fillStyle = '#ffffff';
galleryCanvasCtx.fillRect(0, 0, canvasWidth, canvasHeight);
drawBackground();
if (currentPointIndex > 1) {
galleryCanvasCtx.strokeStyle = penColorHex;
galleryCanvasCtx.lineWidth = 1;
galleryCanvasCtx.beginPath();
for (let i = 0; i <= currentPointIndex; i++) {
let x = path_x[i];
let y = path_y[i];
if (rotation_offset !== 0) {
const rotationRad = -rotation_offset * Math.PI / 180;
const cosR = Math.cos(rotationRad);
const sinR = Math.sin(rotationRad);
x = x * cosR + y * sinR;
y = -path_x[i] * sinR + y * cosR;
}
const canvasX = center_x + x * scale;
const canvasY = center_y - y * scale;
if (i === 0) {
galleryCanvasCtx.moveTo(canvasX, canvasY);
} else {
galleryCanvasCtx.lineTo(canvasX, canvasY);
}
}
galleryCanvasCtx.stroke();
}
if (currentPointIndex < totalPoints) {
let penX = path_x[currentPointIndex];
let penY = path_y[currentPointIndex];
if (rotation_offset !== 0) {
const rotationRad = -rotation_offset * Math.PI / 180;
const cosR = Math.cos(rotationRad);
const sinR = Math.sin(rotationRad);
penX = penX * cosR + penY * sinR;
penY = -path_x[currentPointIndex] * sinR + penY * cosR;
}
const penCanvasX = center_x + penX * scale;
const penCanvasY = center_y - penY * scale;
galleryCanvasCtx.fillStyle = '#ff0000';
galleryCanvasCtx.beginPath();
galleryCanvasCtx.arc(penCanvasX, penCanvasY, 3, 0, Math.PI * 2);
galleryCanvasCtx.fill();
}
currentPointIndex++;
if (currentPointIndex < totalPoints && isGifMode) {
// Dùng setTimeout trực tiếp thay vì requestAnimationFrame + setTimeout
animationFrameId = setTimeout(drawFrame, delayMs);
} else {
animationFrameId = null;
}
}
drawFrame();
} catch (err) {
console.error('Error starting GIF animation:', err);
switchToImageMode();
}
}
// Export functions để dùng trong HTML
window.viewImage = viewImage;
window.downloadDXF = downloadDXF;
// ============================================
// Hướng dẫn và Thư viện
// ============================================
// Khởi tạo Guide và Library khi DOM sẵn sàng
function initGuideAndLibrary() {
// DOM Elements cho Guide và Library
const guideBtn = document.getElementById('btn-guide');
const libraryBtn = document.getElementById('btn-library');
const guideModal = document.getElementById('guide-modal');
const libraryModal = document.getElementById('library-modal');
const closeGuideBtn = document.getElementById('close-guide-btn');
const closeLibraryBtn = document.getElementById('close-library-btn');
const guideContent = document.getElementById('guide-content');
const libraryImages = document.getElementById('library-images');
const libraryLoading = document.getElementById('library-loading');
const libraryEmpty = document.getElementById('library-empty');
// Mở modal hướng dẫn
async function openGuideModal() {
try {
if (!guideModal || !guideContent) {
console.error('Guide modal elements not found');
alert('Không tìm thấy modal hướng dẫn. Vui lòng refresh trang.');
return;
}
guideModal.classList.remove('hidden');
guideContent.textContent = 'Đang tải hướng dẫn...';
console.log('Fetching guide from:', `${API_BASE_URL}/guide`);
const response = await fetch(`${API_BASE_URL}/guide`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('Guide API response:', result);
if (result.success) {
guideContent.textContent = result.content || 'Nội dung hướng dẫn trống.';
} else {
guideContent.textContent = `Lỗi: ${result.error || 'Không thể tải hướng dẫn'}`;
}
} catch (err) {
console.error('Error loading guide:', err);
if (guideContent) {
guideContent.textContent = `Lỗi khi tải hướng dẫn: ${err.message || err}. Vui lòng kiểm tra:\n1. Backend server đang chạy tại http://localhost:5000\n2. File huongdansudung.txt tồn tại trong folder lucky_painter_app`;
}
alert(`Lỗi tải hướng dẫn: ${err.message || err}\n\nVui lòng kiểm tra:\n- Backend server đang chạy\n- Console (F12) để xem chi tiết lỗi`);
}
}
// Đóng modal hướng dẫn
function closeGuideModal() {
if (guideModal) {
guideModal.classList.add('hidden');
}
}
// Mở modal thư viện
async function openLibraryModal() {
try {
if (!libraryModal || !libraryImages || !libraryLoading || !libraryEmpty) {
console.error('Library modal elements not found');
alert('Không tìm thấy modal thư viện. Vui lòng refresh trang.');
return;
}
libraryModal.classList.remove('hidden');
libraryImages.innerHTML = '';
libraryLoading.classList.remove('hidden');
libraryEmpty.classList.add('hidden');
console.log('Fetching library from:', `${API_BASE_URL}/library`);
const response = await fetch(`${API_BASE_URL}/library`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('Library API response:', result);
libraryLoading.classList.add('hidden');
if (result.success && result.images && result.images.length > 0) {
console.log(`Loading ${result.images.length} images`);
result.images.forEach((filename, index) => {
const imageItem = document.createElement('div');
imageItem.className = 'library-image-item';
const img = document.createElement('img');
const encodedFilename = encodeURIComponent(filename);
const imageUrl = `${API_BASE_URL}/library/image/${encodedFilename}`;
img.src = imageUrl;
img.alt = filename;
img.loading = 'lazy';
img.onerror = function() {
console.error('Error loading image:', imageUrl);
this.style.display = 'none';
imageItem.innerHTML = `Lỗi tải hình: ${filename}
`;
};
img.onload = function() {
console.log('Image loaded successfully:', filename);
};
const imageName = document.createElement('div');
imageName.className = 'image-name';
imageName.textContent = filename.length > 25 ? filename.substring(0, 22) + '...' : filename;
imageItem.appendChild(img);
imageItem.appendChild(imageName);
// Click để xem hình lớn
imageItem.addEventListener('click', () => {
window.open(img.src, '_blank');
});
libraryImages.appendChild(imageItem);
});
} else {
libraryEmpty.textContent = result.success ? 'Thư viện chưa có hình ảnh nào.' : `Lỗi: ${result.error || 'Không thể tải danh sách hình ảnh'}`;
libraryEmpty.classList.remove('hidden');
}
} catch (err) {
console.error('Error loading library:', err);
if (libraryLoading) libraryLoading.classList.add('hidden');
if (libraryEmpty) {
libraryEmpty.textContent = `Lỗi khi tải thư viện: ${err.message || err}\n\nVui lòng kiểm tra:\n1. Backend server đang chạy tại http://localhost:5000\n2. Folder thuvien_anh tồn tại trong lucky_painter_app`;
libraryEmpty.classList.remove('hidden');
}
alert(`Lỗi tải thư viện: ${err.message || err}\n\nVui lòng kiểm tra:\n- Backend server đang chạy\n- Console (F12) để xem chi tiết lỗi`);
}
}
// Đóng modal thư viện
function closeLibraryModal() {
if (libraryModal) {
libraryModal.classList.add('hidden');
}
}
// Event listeners
if (guideBtn) {
// Đảm bảo nút không bị disabled
guideBtn.removeAttribute('disabled');
guideBtn.disabled = false;
guideBtn.style.pointerEvents = 'auto';
guideBtn.style.cursor = 'pointer';
// Xóa listener cũ nếu có để tránh duplicate
const newGuideBtn = guideBtn.cloneNode(true);
guideBtn.parentNode.replaceChild(newGuideBtn, guideBtn);
newGuideBtn.addEventListener('click', openGuideModal);
newGuideBtn.setAttribute('data-listener-added', 'true');
console.log('Guide button event listener added', newGuideBtn);
} else {
console.warn('Guide button not found. Available buttons:', document.querySelectorAll('button[id*="guide"], button[id*="library"]'));
}
if (libraryBtn) {
// Đảm bảo nút không bị disabled
libraryBtn.removeAttribute('disabled');
libraryBtn.disabled = false;
libraryBtn.style.pointerEvents = 'auto';
libraryBtn.style.cursor = 'pointer';
// Xóa listener cũ nếu có để tránh duplicate
const newLibraryBtn = libraryBtn.cloneNode(true);
libraryBtn.parentNode.replaceChild(newLibraryBtn, libraryBtn);
newLibraryBtn.addEventListener('click', openLibraryModal);
newLibraryBtn.setAttribute('data-listener-added', 'true');
console.log('Library button event listener added', newLibraryBtn);
} else {
console.warn('Library button not found. Available buttons:', document.querySelectorAll('button[id*="guide"], button[id*="library"]'));
}
if (closeGuideBtn) {
closeGuideBtn.addEventListener('click', closeGuideModal);
}
if (closeLibraryBtn) {
closeLibraryBtn.addEventListener('click', closeLibraryModal);
}
// Đóng modal khi click bên ngoài
if (guideModal) {
guideModal.addEventListener('click', (e) => {
if (e.target === guideModal) {
closeGuideModal();
}
});
}
if (libraryModal) {
libraryModal.addEventListener('click', (e) => {
if (e.target === libraryModal) {
closeLibraryModal();
}
});
}
}
// Khởi tạo khi DOM sẵn sàng
function initGuideAndLibraryWhenReady() {
// Đợi một chút để đảm bảo tất cả HTML đã được parse
setTimeout(() => {
initGuideAndLibrary();
}, 100);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initGuideAndLibraryWhenReady);
} else {
// DOM đã sẵn sàng, chạy ngay nhưng vẫn delay một chút
initGuideAndLibraryWhenReady();
}
// Cũng thử khởi tạo khi window load (backup)
window.addEventListener('load', () => {
setTimeout(() => {
// Kiểm tra lại nếu chưa khởi tạo thành công
const guideBtn = document.getElementById('btn-guide');
const libraryBtn = document.getElementById('btn-library');
if (guideBtn && !guideBtn.hasAttribute('data-listener-added')) {
console.log('Re-initializing guide and library on window load');
initGuideAndLibrary();
}
}, 200);
});