File: /home/bdedition/bddiary.com/daripalla.html
<!DOCTYPE html>
<html lang="bn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<!-- Primary Meta Tags -->
<title>দাঁড়িপাল্লায় ভোট দিন | প্রোফাইল ফ্রেম তৈরি করুন</title>
<meta name="title" content="দাঁড়িপাল্লায় ভোট দিন | প্রোফাইল ফ্রেম তৈরি করুন">
<meta name="description" content="দাঁড়িপাল্লায় ভোট দিন - প্রোফাইল ফ্রেম জেনারেটর। ছবি আপলোড করে ফ্রেম যুক্ত করুন, ডাউনলোড করুন ও ফেসবুকে শেয়ার করুন, সহজ ও নিরাপদভাবে।">
<meta name="keywords" content="দাঁড়িপাল্লা, ভোট, বাংলাদেশ, প্রোফাইল ফ্রেম, ফ্রেম তৈরি, ফ্রেম এডিটর, ভোট ফ্রেম">
<meta name="author" content="daripalla.org">
<meta name="robots" content="index, follow">
<meta name="language" content="Bengali">
<!-- Favicon -->
<link rel="icon" type="image/png" href="https://bddiary.com/assets/logo.png">
<link rel="apple-touch-icon" href="https://bddiary.com/assets/logo.png">
<link rel="shortcut icon" href="https://bddiary.com/assets/logo.png">
<!-- Open Graph / Telegram / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://daripalla.org/">
<meta property="og:title" content="দাঁড়িপাল্লায় ভোট দিন | প্রোফাইল ফ্রেম তৈরি করুন">
<meta property="og:description" content="দাঁড়িপাল্লায় ভোট দিন - প্রোফাইল ফ্রেম জেনারেটর। ছবি আপলোড করে ফ্রেম যুক্ত করুন, ডাউনলোড করুন ও ফেসবুকে শেয়ার করুন, সহজ ও নিরাপদভাবে।">
<meta property="og:image" content="https://bddiary.com/assets/logo.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="দাঁড়িপাল্লায় ভোট দিন">
<meta property="og:locale" content="bn_BD">
<meta property="og:site_name" content="দাঁড়িপাল্লা">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://daripalla.org/">
<meta property="twitter:title" content="দাঁড়িপাল্লায় ভোট দিন | প্রোফাইল ফ্রেম তৈরি করুন">
<meta property="twitter:description" content="দাঁড়িপাল্লায় ভোট দিন - প্রোফাইল ফ্রেম জেনারেটর। ছবি আপলোড করে ফ্রেম যুক্ত করুন, ডাউনলোড করুন ও ফেসবুকে শেয়ার করুন, সহজ ও নিরাপদভাবে।">
<meta property="twitter:image" content="https://bddiary.com/assets/logo.png">
<meta property="twitter:image:alt" content="দাঁড়িপাল্লায় ভোট দিন">
<!-- Additional SEO -->
<meta name="theme-color" content="#16a34a">
<meta name="msapplication-TileColor" content="#16a34a">
<link rel="canonical" href="https://daripalla.org/">
<!-- Google Font - Hind Siliguri -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hind+Siliguri:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Bootstrap 5+ CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css">
<!-- AOS Animation Library -->
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
<style>
:root {
--primary-color: #16a34a;
--primary-dark: #15803d;
--secondary-color: #ea580c;
--light-bg: #f8f9fa;
--shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--shadow-md: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
}
* {
-webkit-tap-highlight-color: transparent;
}
body {
font-family: 'Hind Siliguri', sans-serif;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh;
}
/* Custom Components */
/* Frames Grid Styling */
#framesGrid {
display: flex !important;
gap: 12px !important;
align-items: center !important;
min-height: 120px !important;
padding: 8px 0 !important;
}
#framesGrid .frame-option {
flex-shrink: 0 !important;
opacity: 1 !important;
visibility: visible !important;
}
#framesGrid .frame-option img {
display: block !important;
}
/* Fix for loading states */
.frame-option .spinner-border {
animation: spinner-border 0.75s linear infinite;
}
@keyframes spinner-border {
to { transform: rotate(360deg); }
}
.hero-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 50%, var(--secondary-color) 100%);
position: relative;
overflow: hidden;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 100" fill="white" fill-opacity="0.1"><polygon points="0,100 100,0 200,100 300,0 400,100 500,0 600,100 700,0 800,100 900,0 1000,100 1000,200 0,200"/></svg>') repeat-x;
animation: wave 20s linear infinite;
}
@keyframes wave {
0% { transform: translateX(0); }
100% { transform: translateX(-100px); }
}
.card-custom {
background: white;
border-radius: 20px;
box-shadow: var(--shadow-md);
border: 1px solid rgba(0,0,0,0.08);
transition: all 0.3s ease;
}
.card-custom:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
.btn-primary-custom {
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
border: none;
border-radius: 15px;
font-weight: 600;
padding: 12px 30px;
transition: all 0.3s ease;
box-shadow: var(--shadow-sm);
}
.btn-primary-custom:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
background: linear-gradient(135deg, var(--primary-dark), var(--primary-color));
}
.btn-outline-custom {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 15px;
font-weight: 600;
padding: 10px 28px;
transition: all 0.3s ease;
}
.btn-outline-custom:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
/* Custom range slider */
.form-range {
height: 8px;
background: linear-gradient(to right, #e9ecef, var(--primary-color));
border-radius: 4px;
}
.form-range::-webkit-slider-thumb {
width: 20px;
height: 20px;
background: var(--primary-color);
border: 3px solid white;
border-radius: 50%;
box-shadow: var(--shadow-sm);
}
.form-range::-moz-range-thumb {
width: 20px;
height: 20px;
background: var(--primary-color);
border: 3px solid white;
border-radius: 50%;
box-shadow: var(--shadow-sm);
}
/* Preview canvas */
#previewCanvas {
width: 100% !important;
height: 100% !important;
max-width: 100%;
display: block;
touch-action: none;
border-radius: 15px;
}
/* Fabric.js canvas wrapper */
.canvas-container {
width: 100% !important;
height: 100% !important;
max-width: 100%;
position: relative;
border-radius: 15px !important;
overflow: hidden;
}
.canvas-container canvas {
max-width: 100% !important;
height: auto !important;
border-radius: 15px;
}
/* Frame option */
.frame-option {
position: relative;
width: 100px;
height: 100px;
min-width: 100px;
border-radius: 15px;
overflow: hidden;
cursor: pointer;
border: 3px solid transparent;
transition: all 0.3s ease;
background: var(--light-bg);
touch-action: manipulation;
flex-shrink: 0;
}
.frame-option:hover {
transform: scale(1.05);
border-color: var(--primary-color);
box-shadow: var(--shadow-md);
}
.frame-option:active {
transform: scale(0.95);
}
.frame-option.selected {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.2);
}
.frame-option img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Canvas container for zoom/move */
#previewContainer {
cursor: grab;
user-select: none;
background:
linear-gradient(45deg, #e9ecef 25%, transparent 25%),
linear-gradient(-45deg, #e9ecef 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #e9ecef 75%),
linear-gradient(-45deg, transparent 75%, #e9ecef 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
border-radius: 15px;
}
#previewContainer:active {
cursor: grabbing;
}
#previewContainer.has-image {
cursor: move;
}
/* Toast */
.toast-custom {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: rgba(33, 37, 41, 0.95);
color: white;
padding: 16px 24px;
border-radius: 15px;
box-shadow: var(--shadow-lg);
font-size: 14px;
font-weight: 600;
z-index: 1050;
max-width: 90%;
transition: transform 0.3s ease;
backdrop-filter: blur(10px);
}
.toast-custom.show {
transform: translateX(-50%) translateY(0);
}
.toast-custom.error {
background: rgba(220, 53, 69, 0.95);
}
.toast-custom.success {
background: rgba(25, 135, 84, 0.95);
}
/* Loading spinner */
.spinner-custom {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top: 3px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Upload box */
.upload-area {
border: 3px dashed var(--primary-color);
border-radius: 20px;
background: rgba(22, 163, 74, 0.05);
transition: all 0.3s ease;
}
.upload-area:hover {
background: rgba(22, 163, 74, 0.1);
border-color: var(--primary-dark);
}
.upload-area.has-image {
padding: 0;
border: none;
background: transparent;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary-dark);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.card-custom {
border-radius: 15px;
}
.btn-primary-custom,
.btn-outline-custom {
border-radius: 10px;
padding: 10px 20px;
}
}
/* Animation classes */
.fade-in {
animation: fadeIn 0.6s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Focus states */
.btn:focus,
.form-range:focus {
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.25);
}
input[type="file"] {
display: none;
}
</style>
</head>
<body>
<!-- Header Hero Section -->
<header class="hero-section text-white position-relative" data-aos="fade-down">
<div class="container-fluid py-5">
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<div class="mb-4" data-aos="zoom-in" data-aos-delay="200">
<img src="https://bddiary.com/assets/logo.png" alt="Logo" class="img-fluid" style="height: 80px; filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));">
</div>
<h1 class="display-4 fw-bold mb-3" data-aos="fade-up" data-aos-delay="400" style="text-shadow: 2px 2px 4px rgba(0,0,0,0.3);">
দাঁড়িপাল্লায় ভোট দিন
</h1>
<p class="lead opacity-90 fw-medium" data-aos="fade-up" data-aos-delay="600">
<i class="bi bi-camera-fill me-2"></i>
আপনার ছবিতে ফ্রেম যোগ করুন এবং শেয়ার করুন
</p>
<div class="mt-4" data-aos="fade-up" data-aos-delay="800">
<div class="d-flex justify-content-center gap-3">
<span class="badge bg-light text-dark fs-6 px-3 py-2 rounded-pill">
<i class="bi bi-check-circle-fill text-success me-1"></i>
সহজ ব্যবহার
</span>
<span class="badge bg-light text-dark fs-6 px-3 py-2 rounded-pill">
<i class="bi bi-shield-check-fill text-primary me-1"></i>
নিরাপদ
</span>
<span class="badge bg-light text-dark fs-6 px-3 py-2 rounded-pill">
<i class="bi bi-download text-info me-1"></i>
ফ্রি ডাউনলোড
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Wave divider -->
<div class="position-absolute bottom-0 start-0 w-100">
<svg viewBox="0 0 1200 120" preserveAspectRatio="none" style="height: 60px; width: 100%;">
<path d="M0,0V46.29c47.79,22.2,103.59,32.17,158,28C213.13,69.73,268.14,57.14,320,51.29c52.36-5.92,105.73,2.74,158,2.74s105.64-8.66,158-2.74c51.86,5.85,106.87,18.44,162.15,22.9C952.85,78.46,1008.65,68.49,1056.45,46.29V120H0V0Z" fill="white" fill-opacity="1"></path>
</svg>
</div>
</header>
<!-- Main Content -->
<main class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-8 col-xl-7">
<!-- Frame Selector Section -->
<section class="card-custom p-4 mb-4" data-aos="fade-up" data-aos-delay="100">
<div class="d-flex align-items-center justify-content-between mb-4">
<div class="d-flex align-items-center">
<i class="bi bi-images text-primary fs-4 me-3"></i>
<div>
<h2 class="h4 mb-1 fw-bold text-dark">ফ্রেম নির্বাচন করুন</h2>
</div>
</div>
<div class="text-end d-none d-md-block">
<span class="badge bg-success-subtle text-success">
<i class="bi bi-check-circle me-1"></i>
লোকাল ফাইল
</span>
</div>
</div>
<div class="position-relative">
<div class="d-flex gap-3 overflow-auto pb-2" id="framesGrid" style="scrollbar-width: thin; min-height: 120px;">
<!-- Frames will be loaded here -->
</div>
<!-- Scroll indicators -->
<div class="position-absolute top-50 start-0 translate-middle-y">
<div class="btn btn-sm btn-outline-secondary rounded-circle d-none d-md-block" style="width: 40px; height: 40px;">
<i class="bi bi-chevron-left"></i>
</div>
</div>
<div class="position-absolute top-50 end-0 translate-middle-y">
<div class="btn btn-sm btn-outline-secondary rounded-circle d-none d-md-block" style="width: 40px; height: 40px;">
<i class="bi bi-chevron-right"></i>
</div>
</div>
</div>
</section>
<!-- Canvas & Controls Section -->
<section class="card-custom p-4 mb-4" data-aos="fade-up" data-aos-delay="200">
<!-- Canvas Preview -->
<div class="mb-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="d-flex align-items-center">
<i class="bi bi-eye text-primary fs-4 me-3"></i>
<h3 class="h5 mb-0 fw-bold text-dark">প্রিভিউ</h3>
</div>
<div class="text-muted small">
<i class="bi bi-info-circle me-1"></i>
সাইজ: 1080x1080px
</div>
</div>
<div class="position-relative" style="aspect-ratio: 1;">
<div class="w-100 h-100 position-relative rounded-3 overflow-hidden shadow-sm" id="previewContainer">
<canvas id="previewCanvas" width="1080" height="1080" class="border-0"></canvas>
<!-- Loading overlay -->
<div class="position-absolute top-0 start-0 w-100 h-100 bg-dark bg-opacity-75 d-none flex-column justify-content-center align-items-center text-white rounded-3" id="loadingIndicator">
<div class="spinner-custom mb-3"></div>
<div class="fw-semibold">প্রসেস করা হচ্ছে...</div>
</div>
</div>
</div>
</div>
<!-- Zoom Control -->
<div class="mb-4" data-aos="fade-up" data-aos-delay="300">
<label class="form-label fw-semibold text-dark d-flex align-items-center gap-2 mb-3">
<i class="bi bi-zoom-in text-primary"></i>
<span>জুম কন্ট্রোল</span>
<span class="badge bg-primary ms-auto" id="zoomValue">0%</span>
</label>
<div class="row align-items-center">
<div class="col-2 col-sm-1">
<small class="text-muted">-100%</small>
</div>
<div class="col-8 col-sm-10">
<input type="range" class="form-range" id="zoomSlider" min="-100" max="200" value="0" aria-label="জুম নিয়ন্ত্রণ">
</div>
<div class="col-2 col-sm-1 text-end">
<small class="text-muted">200%</small>
</div>
</div>
<div class="text-center mt-2">
<small class="text-muted">
<i class="bi bi-mouse me-1"></i>
মাউস হুইল বা টাচ দিয়ে জুম করুন
</small>
</div>
</div>
<!-- Action Buttons -->
<div class="d-grid gap-3" data-aos="fade-up" data-aos-delay="400">
<button type="button" class="btn btn-primary-custom btn-lg d-flex align-items-center justify-content-center gap-2" id="selectImageBtn">
<i class="bi bi-upload fs-5"></i>
<span class="fw-bold">ছবি নির্বাচন করুন</span>
</button>
<button type="button" class="btn btn-outline-custom d-none d-flex align-items-center justify-content-center gap-2" id="changeImageBtn">
<i class="bi bi-arrow-repeat fs-5"></i>
<span class="fw-semibold">ছবি পরিবর্তন করুন</span>
</button>
<div class="row g-2">
<div class="col-6">
<button type="button" class="btn btn-primary-custom w-100 d-flex align-items-center justify-content-center gap-2" id="downloadBtn">
<i class="bi bi-download fs-5"></i>
<span class="fw-bold d-none d-sm-inline">ডাউনলোড</span>
</button>
</div>
<div class="col-6">
<button type="button" class="btn btn-outline-custom w-100 d-flex align-items-center justify-content-center gap-2" onclick="shareImage()">
<i class="bi bi-share fs-5"></i>
<span class="fw-semibold d-none d-sm-inline">শেয়ার</span>
</button>
</div>
</div>
</section>
<!-- Instructions Section -->
<section class="card-custom p-4 mb-4" data-aos="fade-up" data-aos-delay="500">
<div class="d-flex align-items-center mb-4">
<i class="bi bi-question-circle text-info fs-4 me-3"></i>
<h3 class="h5 mb-0 fw-bold text-dark">কীভাবে ব্যবহার করবেন?</h3>
</div>
<div class="row g-3">
<div class="col-md-4">
<div class="text-center p-3 bg-light rounded-3 h-100">
<div class="bg-primary rounded-circle d-inline-flex align-items-center justify-content-center text-white fw-bold mb-3" style="width: 50px; height: 50px;">
1
</div>
<h6 class="fw-bold text-dark">ফ্রেম নির্বাচন</h6>
<p class="text-muted small mb-0">পছন্দের ফ্রেম বেছে নিন</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center p-3 bg-light rounded-3 h-100">
<div class="bg-success rounded-circle d-inline-flex align-items-center justify-content-center text-white fw-bold mb-3" style="width: 50px; height: 50px;">
2
</div>
<h6 class="fw-bold text-dark">ছবি আপলোড</h6>
</div>
</div>
<div class="col-md-4">
<div class="text-center p-3 bg-light rounded-3 h-100">
<div class="bg-info rounded-circle d-inline-flex align-items-center justify-content-center text-white fw-bold mb-3" style="width: 50px; height: 50px;">
3
</div>
<h6 class="fw-bold text-dark">ডাউনলোড</h6>
<p class="text-muted small mb-0">ছবি ডাউনলোড ও শেয়ার করুন</p>
</div>
</div>
</div>
</section>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-gradient-dark text-light mt-5" style="background: linear-gradient(135deg, #1a1a1a 0%, #2c3e50 50%, #34495e 100%); position: relative; overflow: hidden;">
<!-- Background Pattern -->
<div class="position-absolute w-100 h-100" style="opacity: 0.05; background-image: url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><defs><pattern id=\"grain\" width=\"100\" height=\"100\" patternUnits=\"userSpaceOnUse\"><circle cx=\"50\" cy=\"50\" r=\"1\" fill=\"white\"/><circle cx=\"25\" cy=\"25\" r=\"0.5\" fill=\"white\"/><circle cx=\"75\" cy=\"75\" r=\"0.5\" fill=\"white\"/></pattern></defs><rect width=\"100\" height=\"100\" fill=\"url(%23grain)\"/></svg>'); background-repeat: repeat;"></div>
<!-- Main Footer Content -->
<div class="container position-relative" style="z-index: 2;">
<!-- Top Section -->
<div class="row py-5">
<div class="col-lg-4 col-md-6 mb-4 mb-lg-0">
<div class="d-flex align-items-center mb-3">
<img src="https://bddiary.com/assets/logo.png" alt="দাঁড়িপাল্লা লোগো" style="height: 50px; filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));">
<div class="ms-3">
<h5 class="mb-1 fw-bold text-white">দাঁড়িপাল্লা</h5>
<p class="mb-0 text-light opacity-75">প্রোফাইল ফ্রেম জেনারেটর</p>
</div>
</div>
<p class="text-light opacity-75 mb-3">
আপনার ছবিতে সুন্দর ফ্রেম যোগ করুন এবং সবার সাথে শেয়ার করুন। সহজ, দ্রুত এবং সম্পূর্ণ বিনামূল্যে।
</p>
<div class="d-flex gap-2">
<span class="badge bg-success px-3 py-2 rounded-pill">
<i class="bi bi-shield-check me-1"></i>
নিরাপদ
</span>
<span class="badge bg-primary px-3 py-2 rounded-pill">
<i class="bi bi-download me-1"></i>
ফ্রি
</span>
</div>
</div>
<div class="col-lg-2 col-md-6 mb-4 mb-lg-0">
<h6 class="fw-bold text-white mb-3">ফিচারস</h6>
<ul class="list-unstyled">
<li class="mb-2">
<span class="text-light opacity-75">
<i class="bi bi-check-circle-fill text-success me-2"></i>
ফ্রেম নির্বাচন
</span>
</li>
<li class="mb-2">
<span class="text-light opacity-75">
<i class="bi bi-check-circle-fill text-success me-2"></i>
ইমেজ এডিটিং
</span>
</li>
<li class="mb-2">
<span class="text-light opacity-75">
<i class="bi bi-check-circle-fill text-success me-2"></i>
তাৎক্ষণিক ডাউনলোড
</span>
</li>
<li class="mb-2">
<span class="text-light opacity-75">
<i class="bi bi-check-circle-fill text-success me-2"></i>
সোশ্যাল শেয়ারিং
</span>
</li>
</ul>
</div>
<div class="col-lg-3 col-md-6 mb-4 mb-lg-0">
<h6 class="fw-bold text-white mb-3">সহায়তা</h6>
<ul class="list-unstyled">
<li class="mb-2">
<a href="#" class="text-light opacity-75 text-decoration-none d-flex align-items-center">
<i class="bi bi-question-circle me-2"></i>
কীভাবে ব্যবহার করবেন
</a>
</li>
<li class="mb-2">
<a href="#" class="text-light opacity-75 text-decoration-none d-flex align-items-center">
<i class="bi bi-shield-exclamation me-2"></i>
গোপনীয়তা নীতি
</a>
</li>
<li class="mb-2">
<a href="#" class="text-light opacity-75 text-decoration-none d-flex align-items-center">
<i class="bi bi-file-text me-2"></i>
শর্তাবলী
</a>
</li>
<li class="mb-2">
<a href="#" class="text-light opacity-75 text-decoration-none d-flex align-items-center">
<i class="bi bi-envelope me-2"></i>
যোগাযোগ
</a>
</li>
</ul>
</div>
<div class="col-lg-3 col-md-6">
<h6 class="fw-bold text-white mb-3">আমাদের সাথে যুক্ত হন</h6>
<div class="d-flex gap-2 mb-3">
<a href="#" class="btn btn-outline-light btn-sm rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="Facebook">
<i class="bi bi-facebook"></i>
</a>
<a href="#" class="btn btn-outline-light btn-sm rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="Twitter">
<i class="bi bi-twitter"></i>
</a>
<a href="#" class="btn btn-outline-light btn-sm rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="YouTube">
<i class="bi bi-youtube"></i>
</a>
<a href="#" class="btn btn-outline-light btn-sm rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;" title="Instagram">
<i class="bi bi-instagram"></i>
</a>
</div>
<div class="bg-dark bg-opacity-25 rounded-3 p-3 border border-light border-opacity-25">
<div class="d-flex align-items-center text-light opacity-75 small">
<i class="bi bi-info-circle me-2 text-info"></i>
<span>আপনার তথ্য সুরক্ষিত থাকে। আমরা কোনো ডেটা সংরক্ষণ করি না।</span>
</div>
</div>
</div>
</div>
<!-- Divider -->
<hr class="border-light border-opacity-25 my-4">
<!-- Bottom Section -->
<div class="row align-items-center py-3">
<div class="col-md-6 mb-2 mb-md-0">
<div class="d-flex align-items-center gap-3">
<small class="text-light opacity-75">
© 2024 <strong>BD Diary</strong> | সকল অধিকার সংরক্ষিত
</small>
</div>
</div>
<div class="col-md-6 text-md-end">
<div class="d-flex align-items-center justify-content-md-end gap-2">
<small class="text-light opacity-75">Developed By:</small>
<a href="http://w3softbd.com" class="text-decoration-none">
<span class="badge bg-gradient text-dark fw-bold px-3 py-2 rounded-pill shadow-sm" style="background: linear-gradient(45deg, #ffd700, #ffed4e); transition: all 0.3s ease; cursor: pointer;" onmouseover="this.style.transform='scale(1.05)'; this.style.boxShadow='0 4px 15px rgba(255,215,0,0.4)'" onmouseout="this.style.transform='scale(1)'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)'">
<i class="bi bi-code-slash me-1"></i>
W3 SOFT
</span>
</a>
</div>
</div>
</div>
</div>
<!-- Decorative Wave -->
<div class="position-absolute top-0 start-0 w-100" style="height: 60px; overflow: hidden; transform: rotate(180deg);">
<svg viewBox="0 0 1200 120" preserveAspectRatio="none" style="height: 100%; width: 100%;">
<path d="M0,0V46.29c47.79,22.2,103.59,32.17,158,28C213.13,69.73,268.14,57.14,320,51.29c52.36-5.92,105.73,2.74,158,2.74s105.64-8.66,158-2.74c51.86,5.85,106.87,18.44,162.15,22.9C952.85,78.46,1008.65,68.49,1056.45,46.29V120H0V0Z" fill="white" fill-opacity="0.1"></path>
</svg>
</div>
</footer>
<!-- Hidden file input -->
<input type="file" id="fileInput" accept="image/*" style="display: none;">
<!-- Toast notification -->
<div id="toast" class="toast-custom">
<span id="toastMessage">বার্তা</span>
</div>
<!-- Bootstrap 5+ JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<!-- AOS Animation Library -->
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<!-- Custom JavaScript -->
<script src="assets/js/app.js"></script>
<!-- Initialize AOS -->
<script>
// Initialize AOS animations
AOS.init({
duration: 800,
easing: 'ease-out-cubic',
once: true,
offset: 100
});
// Enhanced smooth scrolling for internal links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Add loading class to buttons when clicked
document.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', function() {
if (!this.classList.contains('btn-loading')) {
this.classList.add('btn-loading');
setTimeout(() => {
this.classList.remove('btn-loading');
}, 2000);
}
});
});
// Enhanced frame grid scrolling
// framesGrid is already declared in DOM ELEMENTS section
if (framesGrid) {
// Add scroll indicators
const scrollIndicatorLeft = document.querySelector('.position-absolute.start-0');
const scrollIndicatorRight = document.querySelector('.position-absolute.end-0');
if (scrollIndicatorLeft && scrollIndicatorRight) {
// Hide/show scroll indicators based on scroll position
framesGrid.addEventListener('scroll', function() {
scrollIndicatorLeft.style.opacity = this.scrollLeft > 0 ? '1' : '0.3';
scrollIndicatorRight.style.opacity =
this.scrollLeft < (this.scrollWidth - this.clientWidth) ? '1' : '0.3';
});
// Add click handlers for scroll indicators
scrollIndicatorLeft.addEventListener('click', () => {
framesGrid.scrollBy({ left: -200, behavior: 'smooth' });
});
scrollIndicatorRight.addEventListener('click', () => {
framesGrid.scrollBy({ left: 200, behavior: 'smooth' });
});
}
}
// Add ripple effect to buttons
function createRipple(event) {
const button = event.currentTarget;
const circle = document.createElement('span');
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
const rect = button.getBoundingClientRect();
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - rect.left - radius}px`;
circle.style.top = `${event.clientY - rect.top - radius}px`;
circle.classList.add('ripple');
const ripple = button.getElementsByClassName('ripple')[0];
if (ripple) {
ripple.remove();
}
button.appendChild(circle);
}
// Apply ripple effect to all buttons
document.querySelectorAll('.btn').forEach(btn => {
btn.addEventListener('click', createRipple);
});
// Add CSS for ripple effect
const rippleStyle = document.createElement('style');
rippleStyle.textContent = `
.btn {
position: relative;
overflow: hidden;
}
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
transform: scale(0);
animation: ripple-animation 0.6s linear;
pointer-events: none;
}
@keyframes ripple-animation {
to {
transform: scale(4);
opacity: 0;
}
}
.btn-loading {
pointer-events: none;
opacity: 0.8;
}
.btn-loading::after {
content: '';
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-left: 8px;
}
`;
document.head.appendChild(rippleStyle);
</script>
</div>
</div>
<input type="file" id="fileInput" accept="image/*" aria-label="ছবি নির্বাচন করুন" style="display: none;">
</div>
</div>
<!-- Toast Container -->
<div id="toast" class="toast"></div>
<!-- Fabric.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Main Application Script -->
<script>
// ============================================
// DEBUG FUNCTION FOR FRAME TESTING
// ============================================
window.testFrame = function(frameUrl) {
const img = new Image();
img.onload = () => {
showToast(`ফ্রেম সফলভাবে লোড হয়েছে: ${frameUrl}`, 'success');
};
img.onerror = (error) => {
showToast(`ফ্রেম লোড হয়নি: ${frameUrl}`, 'error');
};
img.src = frameUrl;
};
window.testAllFrames = function() {
CONFIG.frames.forEach((frame, index) => {
setTimeout(() => {
testFrame(frame.url);
}, index * 1000);
});
};
// Update debug info
window.updateDebugInfo = function() {
const debugFrameCount = document.getElementById('debugFrameCount');
const debugCanvasReady = document.getElementById('debugCanvasReady');
const debugCurrentFrame = document.getElementById('debugCurrentFrame');
if (debugFrameCount) debugFrameCount.textContent = CONFIG.frames ? CONFIG.frames.length : '0';
if (debugCanvasReady) debugCanvasReady.textContent = fabricCanvas ? 'Yes' : 'No';
if (debugCurrentFrame) debugCurrentFrame.textContent = currentFrame || 'None';
};
// Auto-update debug info
setInterval(updateDebugInfo, 2000);
// Update frame loading status
function updateFrameStatus(loaded, total) {
const statusContainer = document.querySelector('.text-end.d-none.d-md-block');
if (statusContainer) {
const badge = document.createElement('span');
badge.className = `badge ${loaded === total ? 'bg-success' : 'bg-warning'}`;
badge.innerHTML = `<i class="bi bi-check-circle me-1"></i>${loaded}/${total} ফ্রেম`;
statusContainer.innerHTML = '';
statusContainer.appendChild(badge);
}
}
const CONFIG = {
eventName: "দাঁড়িপাল্লায় ভোট দিন",
defaultFormat: "png",
frames: [
{ name: "ফ্রেম ১", url: "assets/img/1.png" },
{ name: "ফ্রেম ২", url: "assets/img/2.png" },
{ name: "ফ্রেম ৩", url: "assets/img/3.png" },
{ name: "ফ্রেম ৪", url: "assets/img/4.png" },
{ name: "ফ্রেম ৫", url: "assets/img/5.png" }
],
exportSize: 1080,
showWatermarkToggle: false,
watermarkText: "#দাঁড়িপাল্লায়ভোটদিন",
maxFileSize: 10 * 1024 * 1024,
maxImageDimension: 4096,
// Auto-detect frames from img folder (for future enhancement)
async detectFrames() {
try {
// This would need server-side support to list directory contents
// For now, using the predefined frames array above
const detectedFrames = [];
const frameFiles = ['1.png', '2.png', '3.png', '4.png', '5.png'];
frameFiles.forEach((file, index) => {
detectedFrames.push({
name: `ফ্রেম ${index + 1}`,
url: `assets/img/${file}`
});
});
return detectedFrames;
} catch (error) {
return this.frames;
}
},
};
// ============================================
// STATE
// ============================================
let fabricCanvas = null;
let userImageObj = null;
let frameImageObj = null;
let currentImageSrc = null;
let currentFrame = null;
let frameImages = {};
let noWatermark = true;
// Touch state
let lastTouchDistance = 0;
let isPinching = false;
// ============================================
// DOM ELEMENTS
// ============================================
const fileInput = document.getElementById('fileInput');
const framesGrid = document.getElementById('framesGrid');
const previewCanvas = document.getElementById('previewCanvas');
const previewContainer = document.getElementById('previewContainer');
const loadingIndicator = document.getElementById('loadingIndicator');
const toast = document.getElementById('toast');
const zoomSlider = document.getElementById('zoomSlider');
const zoomValue = document.getElementById('zoomValue');
const selectImageBtn = document.getElementById('selectImageBtn');
const changeImageBtn = document.getElementById('changeImageBtn');
const downloadBtn = document.getElementById('downloadBtn');
// ============================================
// INITIALIZATION
// ============================================
function init() {
// Initialize Fabric.js canvas with responsive sizing
const container = previewContainer;
const containerWidth = container.offsetWidth;
fabricCanvas = new fabric.Canvas('previewCanvas', {
width: CONFIG.exportSize,
height: CONFIG.exportSize,
backgroundColor: 'transparent',
selection: false,
preserveObjectStacking: true,
renderOnAddRemove: true,
stateful: false
});
// Make canvas responsive - fit within container
function resizeCanvas() {
if (!fabricCanvas || !previewContainer) return;
const containerWidth = previewContainer.offsetWidth;
const containerHeight = previewContainer.offsetHeight;
// Calculate scale to fit container (maintain aspect ratio)
const scale = Math.min(containerWidth, containerHeight) / CONFIG.exportSize;
// Set canvas display size (CSS only - keeps internal size at exportSize)
const canvasElement = fabricCanvas.getElement();
if (canvasElement) {
canvasElement.style.width = (CONFIG.exportSize * scale) + 'px';
canvasElement.style.height = (CONFIG.exportSize * scale) + 'px';
canvasElement.style.maxWidth = '100%';
canvasElement.style.maxHeight = '100%';
}
// Also update upper canvas if exists
const upperCanvas = fabricCanvas.upperCanvasEl;
if (upperCanvas) {
upperCanvas.style.width = (CONFIG.exportSize * scale) + 'px';
upperCanvas.style.height = (CONFIG.exportSize * scale) + 'px';
upperCanvas.style.maxWidth = '100%';
upperCanvas.style.maxHeight = '100%';
}
fabricCanvas.renderAll();
}
// Initial resize - wait for container to be ready
setTimeout(() => {
resizeCanvas();
}, 100);
// Resize on window resize
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(resizeCanvas, 100);
});
// Default object settings - will be customized per object
// Don't disable controls globally - let each object set its own
fabric.Object.prototype.lockRotation = true;
loadFrames().then(() => {
// Load default first frame after all frames are loaded
if (currentFrame && frameImages[currentFrame]) {
updateFrame();
} else {
// Set default frame if no frame is selected
setTimeout(() => {
if (CONFIG.frames && CONFIG.frames.length > 0) {
currentFrame = CONFIG.frames[0].url;
frameImages[currentFrame] = currentFrame;
updateFrame();
}
}, 500);
}
});
selectImageBtn.addEventListener('click', () => fileInput.click());
changeImageBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', handleFileSelect);
zoomSlider.addEventListener('input', handleZoom);
downloadBtn.addEventListener('click', downloadImage);
// Mouse wheel zoom (desktop)
previewCanvas.addEventListener('wheel', handleWheel, { passive: false });
// Touch events for pinch zoom (mobile)
previewCanvas.addEventListener('touchstart', handleTouchStart, { passive: false });
previewCanvas.addEventListener('touchmove', handleTouchMove, { passive: false });
previewCanvas.addEventListener('touchend', handleTouchEnd);
trackVisit();
}
// ============================================
// ANALYTICS TRACKING
// ============================================
function trackVisit() {
const analytics = getAnalytics();
analytics.visits.push({
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
referrer: document.referrer || 'direct'
});
saveAnalytics(analytics);
}
function trackFrameGeneration(frameUrl) {
const analytics = getAnalytics();
analytics.generations.push({
timestamp: new Date().toISOString(),
frame: frameUrl,
action: 'download'
});
saveAnalytics(analytics);
}
function trackFrameShare(frameUrl) {
const analytics = getAnalytics();
analytics.generations.push({
timestamp: new Date().toISOString(),
frame: frameUrl,
action: 'share'
});
saveAnalytics(analytics);
}
function getAnalytics() {
const data = localStorage.getItem('frameAnalytics');
if (data) {
return JSON.parse(data);
}
return {
visits: [],
generations: [],
startDate: new Date().toISOString()
};
}
function saveAnalytics(analytics) {
localStorage.setItem('frameAnalytics', JSON.stringify(analytics));
}
// ============================================
// FRAME MANAGEMENT
// ============================================
// Note: The working loadFrames function is defined at the bottom of the page
function selectFrame(element, frameUrl) {
document.querySelectorAll('.frame-option').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
currentFrame = frameUrl;
updateFrame();
}
function updateFrame() {
if (!frameImages[currentFrame] || !fabricCanvas) {
return;
}
// Remove old frame if exists - ensure complete removal
if (frameImageObj) {
try {
// Remove from canvas
const objects = fabricCanvas.getObjects();
const frameIndex = objects.indexOf(frameImageObj);
if (frameIndex > -1) {
fabricCanvas.remove(frameImageObj);
}
// Dispose the object
frameImageObj.dispose();
} catch (e) {
}
frameImageObj = null;
fabricCanvas.renderAll(); // Render after removal
}
// Add frame on top - exactly 100% width and height
try {
// For local files, use the direct URL instead of data URL
const frameUrl = frameImages[currentFrame] || currentFrame;
fabric.Image.fromURL(frameUrl, (img) => {
if (!img) {
showToast('ফ্রেম লোড করতে সমস্যা হয়েছে। অন্য ফ্রেম নির্বাচন করুন।', 'error');
return;
}
// Scale frame to exactly match canvas size (100% width and height)
const scaleX = CONFIG.exportSize / img.width;
const scaleY = CONFIG.exportSize / img.height;
// Use max to ensure frame covers entire canvas (100% coverage)
const scale = Math.max(scaleX, scaleY);
img.set({
left: CONFIG.exportSize / 2,
top: CONFIG.exportSize / 2,
originX: 'center',
originY: 'center',
scaleX: scale,
scaleY: scale,
selectable: false,
evented: false, // Frame should not block user image interactions
excludeFromExport: false,
lockMovementX: true,
lockMovementY: true,
lockScalingX: true,
lockScalingY: true,
hoverCursor: 'default',
moveCursor: 'default',
// Ensure frame doesn't interfere with controls
perPixelTargetFind: false,
objectCaching: false
});
frameImageObj = img;
fabricCanvas.add(img);
// Ensure frame is on top and user image is below
if (userImageObj) {
fabricCanvas.sendToBack(userImageObj);
} else {
// If no user image, bring frame to front
fabricCanvas.bringToFront(img);
}
fabricCanvas.renderAll();
}, {
crossOrigin: 'anonymous'
});
} catch (error) {
showToast('ফ্রেম লোড করতে সমস্যা হয়েছে।', 'error');
}
}
// ============================================
// FILE HANDLING
// ============================================
function handleFileSelect(e) {
const file = e.target.files[0];
if (!file) return;
// Validate file
if (!file.type.match('image.*')) {
showToast('দয়া করে একটি ছবি ফাইল নির্বাচন করুন।', 'error');
return;
}
if (file.size > CONFIG.maxFileSize) {
showToast('ফাইল খুব বড়। সর্বোচ্চ ১০ MB অনুমোদিত।', 'error');
return;
}
// Read and load image
const reader = new FileReader();
reader.onload = (event) => {
loadImage(event.target.result);
};
reader.readAsDataURL(file);
}
function loadImage(imageSrc) {
currentImageSrc = imageSrc;
fabric.Image.fromURL(imageSrc, (img) => {
// Check if image needs downscaling
const maxDim = Math.max(img.width, img.height);
if (maxDim > CONFIG.maxImageDimension) {
const scale = CONFIG.maxImageDimension / maxDim;
img.scale(scale);
}
// Center the image at canvas center
const canvasSize = CONFIG.exportSize;
const centerX = canvasSize / 2;
const centerY = canvasSize / 2;
// Using default Fabric.js resize controls - no custom handlers needed
img.set({
left: centerX,
top: centerY,
originX: 'center',
originY: 'center',
selectable: true,
evented: true,
// Enable default Fabric.js resize controls (4 corners)
hasControls: true,
hasBorders: true,
borderColor: '#16a34a',
borderOpacityWhenMoving: 1,
borderDashArray: [5, 5],
cornerColor: '#16a34a',
cornerSize: 18,
cornerStyle: 'circle',
transparentCorners: false,
borderScaleFactor: 2,
cornerStrokeColor: '#ffffff',
cornerStrokeWidth: 3,
// Allow movement and scaling
lockMovementX: false,
lockMovementY: false,
lockRotation: true,
lockScalingX: false,
lockScalingY: false,
lockUniScaling: true, // Maintain aspect ratio when resizing from corners (default 4 corner controls)
// Make controls more visible
padding: 8
// Using default Fabric.js corner controls (4 corners) - no custom edge controls
// lockUniScaling ensures aspect ratio is maintained when resizing
});
// Remove old image if exists
if (userImageObj) {
fabricCanvas.remove(userImageObj);
}
userImageObj = img;
fabricCanvas.add(img);
// Ensure user image is below frame
if (frameImageObj) {
fabricCanvas.sendToBack(img);
// Make sure frame doesn't block clicks - bring frame to front but keep it non-interactive
fabricCanvas.bringToFront(frameImageObj);
}
// Set as active object to show controls immediately
fabricCanvas.setActiveObject(img);
// Force show controls - multiple attempts to ensure visibility
setTimeout(() => {
if (fabricCanvas && userImageObj === img) {
fabricCanvas.setActiveObject(img);
fabricCanvas.renderAll();
}
}, 100);
setTimeout(() => {
if (fabricCanvas && userImageObj === img) {
fabricCanvas.setActiveObject(img);
fabricCanvas.renderAll();
}
}, 300);
// Add event listeners for resize and movement
img.on('modified', () => {
fabricCanvas.renderAll();
});
img.on('scaling', () => {
fabricCanvas.renderAll();
});
img.on('moving', () => {
fabricCanvas.renderAll();
});
img.on('selected', () => {
fabricCanvas.renderAll();
});
// Ensure image can be selected even when frame is on top
img.on('mousedown', (e) => {
fabricCanvas.setActiveObject(img);
fabricCanvas.renderAll();
if (e.e) e.e.stopPropagation();
});
// Click on canvas to select image
fabricCanvas.on('mouse:down', (e) => {
if (e.target === img || (!e.target && userImageObj === img)) {
fabricCanvas.setActiveObject(img);
fabricCanvas.renderAll();
}
});
fabricCanvas.renderAll();
// Reset zoom slider to 0% (1x scale)
zoomSlider.value = 0;
zoomValue.textContent = '0%';
// Show change image button
selectImageBtn.classList.add('d-none');
changeImageBtn.classList.remove('d-none');
previewContainer.classList.add('has-image');
// Update frame (ensure frame is on top)
if (frameImageObj) {
fabricCanvas.bringToFront(frameImageObj);
}
fabricCanvas.renderAll();
}, {
crossOrigin: 'anonymous'
});
}
// ============================================
// CANVAS INTERACTION (Mouse Wheel Zoom)
// ============================================
function handleWheel(e) {
if (!userImageObj) return;
e.preventDefault();
const delta = e.deltaY > 0 ? -0.05 : 0.05;
const currentScale = userImageObj.scaleX;
const newScale = Math.max(0, Math.min(3, currentScale + delta));
// Zoom centered on canvas
const canvasCenter = {
x: CONFIG.exportSize / 2,
y: CONFIG.exportSize / 2
};
const scaleRatio = newScale / currentScale;
const imgCenterX = userImageObj.left;
const imgCenterY = userImageObj.top;
userImageObj.scale(newScale);
userImageObj.set({
left: canvasCenter.x - (canvasCenter.x - imgCenterX) * scaleRatio,
top: canvasCenter.y - (canvasCenter.y - imgCenterY) * scaleRatio
});
// Update slider: scale 0-3 maps to -100% to 200%
const zoomPercent = Math.round((newScale * 100) - 100);
zoomSlider.value = Math.max(-100, Math.min(200, zoomPercent));
zoomValue.textContent = zoomSlider.value + '%';
fabricCanvas.renderAll();
}
// ============================================
// CANVAS INTERACTION (Touch - Pinch Zoom)
// ============================================
function handleTouchStart(e) {
if (!userImageObj) return;
if (e.touches.length === 2) {
e.preventDefault();
isPinching = true;
const touch1 = e.touches[0];
const touch2 = e.touches[1];
lastTouchDistance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
}
}
function handleTouchMove(e) {
if (!userImageObj || !isPinching) return;
if (e.touches.length === 2) {
e.preventDefault();
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
if (lastTouchDistance > 0) {
const scaleChange = distance / lastTouchDistance;
const currentScale = userImageObj.scaleX;
const newScale = Math.max(0, Math.min(3, currentScale * scaleChange));
// Zoom centered on canvas (not on touch point for consistency)
const canvasCenter = {
x: CONFIG.exportSize / 2,
y: CONFIG.exportSize / 2
};
const scaleRatio = newScale / currentScale;
const imgCenterX = userImageObj.left;
const imgCenterY = userImageObj.top;
userImageObj.scale(newScale);
userImageObj.set({
left: canvasCenter.x - (canvasCenter.x - imgCenterX) * scaleRatio,
top: canvasCenter.y - (canvasCenter.y - imgCenterY) * scaleRatio
});
// Update slider: scale 0-3 maps to -100% to 200%
const zoomPercent = Math.round((newScale * 100) - 100);
zoomSlider.value = Math.max(-100, Math.min(200, zoomPercent));
zoomValue.textContent = zoomSlider.value + '%';
fabricCanvas.renderAll();
}
lastTouchDistance = distance;
}
}
function handleTouchEnd(e) {
isPinching = false;
lastTouchDistance = 0;
}
// ============================================
// ZOOM SLIDER
// ============================================
function handleZoom(e) {
if (!userImageObj) return;
const sliderValue = parseInt(e.target.value);
zoomValue.textContent = sliderValue + '%';
// Calculate scale: -100% = 0x, 0% = 1x, 100% = 2x, 200% = 3x
const newScale = (sliderValue + 100) / 100; // Range: 0 to 3
const currentScale = userImageObj.scaleX;
// Zoom centered on canvas
const canvasCenter = {
x: CONFIG.exportSize / 2,
y: CONFIG.exportSize / 2
};
// Calculate scale ratio
const scaleRatio = newScale / currentScale;
// Get current image center
const imgCenterX = userImageObj.left;
const imgCenterY = userImageObj.top;
// Apply new scale
userImageObj.scale(newScale);
// Keep image centered on canvas while zooming
userImageObj.set({
left: canvasCenter.x - (canvasCenter.x - imgCenterX) * scaleRatio,
top: canvasCenter.y - (canvasCenter.y - imgCenterY) * scaleRatio
});
fabricCanvas.renderAll();
}
// Fabric.js handles rendering automatically - no need for manual preview functions
function drawWatermark(ctx, size) {
ctx.save();
ctx.font = `${size * 0.025}px 'Hind Siliguri', sans-serif`;
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText(CONFIG.watermarkText, size - 20, size - 20);
ctx.restore();
}
// ============================================
// EXPORT
// ============================================
async function downloadImage() {
if (!userImageObj) {
showToast('দয়া করে আগে একটি ছবি নির্বাচন করুন।', 'error');
return;
}
showLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 100));
// Export canvas to data URL using Fabric.js
const dataURL = fabricCanvas.toDataURL({
format: 'png',
quality: 1.0,
multiplier: 1,
enableRetinaScaling: true
});
const filename = `daripalla_frame_${Date.now()}.png`;
// Create download link
const a = document.createElement('a');
a.href = dataURL;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Track download
trackFrameGeneration(currentFrame);
showToast('ডাউনলোড সম্পন্ন হয়েছে!', 'success');
} catch (error) {
showToast('ডাউনলোড করতে সমস্যা হয়েছে। আবার চেষ্টা করুন।', 'error');
} finally {
showLoading(false);
}
}
async function shareImage() {
if (!userImageObj) {
showToast('দয়া করে আগে একটি ছবি আপলোড করুন।', 'error');
return;
}
showLoading(true);
try {
// Export canvas to data URL using Fabric.js
const dataURL = fabricCanvas.toDataURL({
format: 'png',
quality: 1.0,
multiplier: 1,
enableRetinaScaling: true
});
const filename = `daripalla_frame_${Date.now()}.png`;
// Convert data URL to blob for better handling
const response = await fetch(dataURL);
const blob = await response.blob();
const file = new File([blob], filename, { type: 'image/png' });
// Try multiple sharing methods
let shareSuccess = false;
// Method 1: Native Web Share API (if available)
if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) {
try {
await navigator.share({
files: [file],
title: 'দাঁড়িপাল্লায় ভোট দিন',
text: 'আমার নতুন প্রোফাইল ফ্রেম দেখুন! 🗳️ #দাঁড়িপাল্লায়ভোটদিন',
url: 'http://bddiary.com/'
});
shareSuccess = true;
showToast('শেয়ার সম্পন্ন হয়েছে!', 'success');
} catch (shareError) {
if (shareError.name !== 'AbortError') {
console.log('Native share failed, trying Facebook');
} else {
shareSuccess = true; // User cancelled
}
}
}
// Method 2: Facebook Share Dialog (if native share failed)
if (!shareSuccess) {
// First download the image
const tempLink = document.createElement('a');
tempLink.href = dataURL;
tempLink.download = filename;
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.click();
// Then open Facebook share dialog
const shareText = encodeURIComponent('আমার নতুন প্রোফাইল ফ্রেম দেখুন! দাঁড়িপাল্লায় ভোট দিন 🗳️\n\nআপনিও তৈরি করুন: http://bddiary.com/');
const facebookShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent('http://bddiary.com/')}"e=${shareText}`;
// Open Facebook share in popup
const popup = window.open(
facebookShareUrl,
'facebook-share',
'width=600,height=500,resizable=yes,scrollbars=yes,toolbar=no,location=yes,directories=no,status=yes,menubar=no'
);
if (popup && !popup.closed) {
shareSuccess = true;
showToast('Facebook এ শেয়ার করার জন্য ছবি ডাউনলোড হয়েছে। পোস্টে ছবিটি যোগ করুন।', 'success');
// Focus the popup and monitor when it closes
popup.focus();
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
// Track share when popup closes
trackFrameShare(currentFrame);
}
}, 1000);
} else {
showToast('Facebook শেয়ার ব্লক করা হয়েছে। ছবি ডাউনলোড হয়েছে - ম্যানুয়ালি শেয়ার করুন।', 'success');
}
// Clean up download link
setTimeout(() => {
if (document.body.contains(tempLink)) {
document.body.removeChild(tempLink);
}
}, 2000);
}
// Track the share attempt
if (shareSuccess) {
trackFrameShare(currentFrame);
}
} catch (error) {
console.error('Share error:', error);
showToast('শেয়ার করতে সমস্যা হয়েছে। আবার চেষ্টা করুন।', 'error');
} finally {
showLoading(false);
}
}
// ============================================
// UI HELPERS
// ============================================
function showLoading(show) {
const loadingIndicator = document.getElementById('loadingIndicator');
if (show) {
loadingIndicator.classList.remove('d-none');
loadingIndicator.classList.add('d-flex');
} else {
loadingIndicator.classList.add('d-none');
loadingIndicator.classList.remove('d-flex');
}
}
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
toastMessage.textContent = message;
toast.className = 'toast-custom show';
if (type === 'error') toast.classList.add('error');
if (type === 'success') toast.classList.add('success');
setTimeout(() => {
toast.classList.remove('show');
toast.classList.remove('error', 'success');
}, 3000);
}
// ============================================
// START APPLICATION
// ============================================
function startApp() {
if (typeof fabric === 'undefined') {
// Wait for Fabric.js to load (max 10 seconds)
const startTime = Date.now();
const checkFabric = setInterval(() => {
if (typeof fabric !== 'undefined') {
clearInterval(checkFabric);
init();
} else if (Date.now() - startTime > 10000) {
clearInterval(checkFabric);
showToast('Fabric.js লোড করতে সমস্যা হয়েছে। পেজ রিফ্রেশ করুন।', 'error');
}
}, 100);
return;
}
init();
}
window.addEventListener('DOMContentLoaded', startApp);
</script>
<script>
// Simplified frame loading function to fix frame display issues
async function loadFramesFixed() {
if (!framesGrid) {
return;
}
const framesToLoad = CONFIG.frames;
if (!framesToLoad?.length) {
framesGrid.innerHTML = `
<div class="alert alert-warning w-100 m-0">
<i class="bi bi-exclamation-triangle me-2"></i>
কোনো ফ্রেম কনফিগার করা হয়নি।
</div>
`;
return;
}
// Clear existing content
framesGrid.innerHTML = '';
let loadedCount = 0;
framesToLoad.forEach((frame, index) => {
// Create frame container
const frameDiv = document.createElement('div');
frameDiv.className = 'frame-option';
frameDiv.dataset.frame = frame.url;
// Add loading placeholder
frameDiv.innerHTML = `
<div class="d-flex flex-column align-items-center justify-content-center h-100 text-muted text-center p-2">
<div class="spinner-border spinner-border-sm mb-2" role="status"></div>
<small class="fw-semibold">ফ্রেম ${index + 1}</small>
</div>
`;
// Select first frame by default
if (index === 0) {
frameDiv.classList.add('selected');
currentFrame = frame.url;
}
framesGrid.appendChild(frameDiv);
// Load image
const img = new Image();
img.onload = () => {
// Replace with actual image
frameDiv.innerHTML = '';
const imgElement = document.createElement('img');
imgElement.src = frame.url;
imgElement.alt = frame.name;
imgElement.style.cssText = 'width: 100%; height: 100%; object-fit: cover;';
frameDiv.appendChild(imgElement);
frameDiv.title = frame.name;
// Store for canvas usage
frameImages[frame.url] = frame.url;
loadedCount++;
// Update status
if (typeof updateFrameStatus === 'function') {
updateFrameStatus(loadedCount, framesToLoad.length);
}
// Auto-load first frame in canvas preview
if (index === 0) {
setTimeout(() => {
if (typeof updateFrame === 'function') {
updateFrame();
}
}, 300);
}
};
img.onerror = (error) => {
frameDiv.innerHTML = `
<div class="d-flex flex-column align-items-center justify-content-center h-100 text-danger text-center p-2">
<i class="bi bi-exclamation-triangle fs-4 mb-1"></i>
<small class="fw-semibold">ফ্রেম ${index + 1}</small>
<small style="font-size: 10px;">লোড হয়নি</small>
</div>
`;
// Try to make it still work
frameImages[frame.url] = frame.url;
};
img.src = frame.url;
// Add click handler
frameDiv.addEventListener('click', () => {
document.querySelectorAll('.frame-option').forEach(el => el.classList.remove('selected'));
frameDiv.classList.add('selected');
currentFrame = frame.url;
if (typeof updateFrame === 'function') {
updateFrame();
}
});
});
}
// Override the problematic loadFrames function
window.loadFrames = loadFramesFixed;
// Also make sure it loads when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(loadFramesFixed, 1000);
});
} else {
setTimeout(loadFramesFixed, 1000);
}
</script>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/vcd15cbe7772f49c399c6a5babf22c1241717689176015" integrity="sha512-ZpsOmlRQV6y907TI0dKBHq9Md29nnaEIPlkf84rnaERnq6zvWvPUqr2ft8M1aS28oN72PdrCzSjY4U6VaAw1EQ==" data-cf-beacon='{"version":"2024.11.0","token":"0fcae263d6cc4af69b088db69c0d359c","r":1,"server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>