<!DOCTYPE html>
<html lang="id" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>suryo_batikkreasi - Studio Edukasi Membatik Digital Siswa</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts Inter & Playfair -->
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['"Plus Jakarta Sans"', 'sans-serif'],
serif: ['"Playfair Display"', 'serif'],
},
colors: {
batikGold: '#C59B27',
batikBrown: '#5C3A21',
batikRed: '#8B263E',
}
}
}
}
</script>
<style>
.pattern-bg {
background-color: #fdfbf7;
background-image: radial-gradient(#c59b27 0.75px, transparent 0.75px), radial-gradient(#c59b27 0.75px, #fdfbf7 0.75px);
background-size: 30px 30px;
background-position: 0 0, 15px 15px;
}
canvas {
touch-action: none;
}
</style>
</head>
<body class="h-full pattern-bg text-slate-800 flex flex-col font-sans selection:bg-amber-200">
<header class="bg-white/90 backdrop-blur-md border-b border-amber-100 sticky top-0 z-50 px-4 lg:px-8 py-3 shadow-sm">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-batikBrown to-batikGold flex items-center justify-center text-white shadow-md">
<i class="fa-solid fa-paintbrush text-lg"></i>
</div>
<div>
<h1 class="font-serif text-xl font-bold text-batikBrown tracking-wide">suryo_batikkreasi</h1>
<p class="text-xs text-slate-500 font-medium">Platform Edukasi & Galeri Batik Digital Siswa</p>
</div>
</div>
<div class="flex items-center space-x-2 sm:space-x-3">
<button onclick="app.switchView('editor')" id="nav-editor" class="px-3.5 py-2 rounded-xl text-sm font-semibold transition bg-amber-100 text-batikBrown shadow-inner">
<i class="fa-solid fa-palette mr-1.5"></i> Studio
</button>
<button onclick="app.switchView('mockup')" id="nav-mockup" class="px-3.5 py-2 rounded-xl text-sm font-semibold transition text-slate-600 hover:bg-amber-50">
<i class="fa-solid fa-shirt mr-1.5"></i> Model Baju
</button>
<button onclick="app.switchView('gallery')" id="nav-gallery" class="px-3.5 py-2 rounded-xl text-sm font-semibold transition text-slate-600 hover:bg-amber-50">
<i class="fa-solid fa-images mr-1.5"></i> Galeri
</button>
</div>
</div>
</header>
<main class="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 overflow-y-auto">
<!-- VIEW 1: STUDIO MEWARNAI -->
<div id="view-editor" class="grid grid-cols-1 lg:grid-cols-12 gap-6 h-full">
<!-- Sidebar Panel Kiri -->
<div class="lg:col-span-4 flex flex-col space-y-4">
<!-- Step 1: Pilih Pola Motif -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-amber-100">
<h2 class="text-xs font-bold tracking-wider text-batikGold uppercase mb-3 flex items-center">
<span class="w-5 h-5 rounded-full bg-amber-100 text-batikBrown inline-flex items-center justify-center mr-2 text-xs">1</span>
Pilih Motif Batik & Edukasi
</h2>
<div class="grid grid-cols-2 gap-2.5" id="pattern-selector"></div>
<div id="pattern-info-card" class="mt-3 p-3 bg-amber-50/60 rounded-xl border border-amber-200/60 text-xs text-slate-600"></div>
</div>
<!-- Step 2: Alat & Warna -->
<div class="bg-white rounded-2xl p-5 shadow-sm border border-amber-100 flex-1 flex flex-col justify-between space-y-4">
<div>
<h2 class="text-xs font-bold tracking-wider text-batikGold uppercase mb-3 flex items-center">
<span class="w-5 h-5 rounded-full bg-amber-100 text-batikBrown inline-flex items-center justify-center mr-2 text-xs">2</span>
Pilih Alat & Warna Kreatif
</h2>
<!-- Tool Switcher -->
<div class="grid grid-cols-2 gap-2 mb-4">
<button onclick="app.setTool('brush')" id="tool-brush" class="py-2.5 px-3 rounded-xl border border-amber-300 bg-amber-50 text-batikBrown text-sm font-semibold flex items-center justify-center space-x-2 transition shadow-sm">
<i class="fa-solid fa-paintbrush"></i>
<span>Kuas Bebas</span>
</button>
<button onclick="app.setTool('fill')" id="tool-fill" class="py-2.5 px-3 rounded-xl border border-slate-200 bg-white text-slate-600 text-sm font-semibold flex items-center justify-center space-x-2 transition hover:bg-slate-50">
<i class="fa-solid fa-fill-drip"></i>
<span>Isi Ruang</span>
</button>
</div>
<!-- Brush Size Slider -->
<div id="brush-size-container" class="mb-4 bg-slate-50 p-3 rounded-xl border border-slate-100">
<div class="flex justify-between text-xs font-medium text-slate-600 mb-1">
<span>Ukuran Kuas</span>
<span id="brush-size-val">15px</span>
</div>
<input type="range" min="5" max="40" value="15" oninput="app.setBrushSize(this.value)" class="w-full accent-batikBrown cursor-pointer">
</div>
<!-- Palette Warna -->
<label class="block text-xs font-semibold text-slate-600 mb-2">Palette Warna Nusantara</label>
<div class="grid grid-cols-6 gap-2 mb-4" id="color-palette"></div>
<!-- Input Nama dan Kelas Siswa -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-2">
<div>
<label class="block text-xs font-semibold text-slate-600 mb-1">Nama Siswa</label>
<input type="text" id="student-name" placeholder="Nama lengkap..." class="w-full px-3 py-2 text-sm rounded-xl border border-slate-200 focus:outline-none focus:border-batikBrown bg-slate-50">
</div>
<div>
<label class="block text-xs font-semibold text-slate-600 mb-1">Kelas</label>
<input type="text" id="student-class" placeholder="Contoh: VII A" class="w-full px-3 py-2 text-sm rounded-xl border border-slate-200 focus:outline-none focus:border-batikBrown bg-slate-50">
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="pt-3 border-t border-slate-100 flex flex-col gap-2">
<div class="flex gap-2">
<button onclick="app.clearCanvas()" class="flex-1 py-2.5 px-2 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-xl font-semibold text-xs transition flex items-center justify-center space-x-1">
<i class="fa-solid fa-rotate-right"></i>
<span>Reset</span>
</button>
<button onclick="app.previewOnShirt()" class="flex-1 py-2.5 px-2 bg-amber-600 hover:bg-amber-700 text-white rounded-xl font-semibold text-xs transition shadow-sm flex items-center justify-center space-x-1">
<i class="fa-solid fa-shirt"></i>
<span>Lihat Baju</span>
</button>
</div>
<button onclick="app.saveArtwork()" class="w-full py-2.5 px-3 bg-gradient-to-r from-batikBrown to-amber-800 hover:opacity-95 text-white rounded-xl font-semibold text-xs sm:text-sm transition shadow-md flex items-center justify-center space-x-1.5">
<i class="fa-solid fa-cloud-arrow-up"></i>
<span>Simpan ke Galeri</span>
</button>
</div>
</div>
</div>
<!-- Canvas Area Kanan -->
<div class="lg:col-span-8 bg-white rounded-2xl p-5 shadow-sm border border-amber-100 flex flex-col items-center justify-center relative min-h-[480px]">
<div class="absolute top-4 left-4 bg-amber-50 border border-amber-200 px-3.5 py-1.5 rounded-lg text-xs font-semibold text-batikBrown flex items-center space-x-2 shadow-xs z-10">
<i class="fa-solid fa-circle-info text-batikGold"></i>
<span id="active-pattern-label">Motif Aktif: Megamendung</span>
</div>
<div class="relative w-full max-w-[500px] aspect-square rounded-xl overflow-hidden shadow-inner border border-slate-200 bg-white flex items-center justify-center mt-8 lg:mt-0">
<canvas id="batikCanvas" width="600" height="600" class="w-full h-full cursor-crosshair"></canvas>
</div>
<div class="flex items-center justify-between w-full max-w-[500px] mt-3 px-1 text-xs text-slate-500">
<span><i class="fa-solid fa-lightbulb text-amber-500 mr-1"></i> Kanvas diperbesar agar mudah diwarnai.</span>
<span id="canvas-dimension">600 x 600 px</span>
</div>
</div>
</div>
<!-- VIEW 2: MOCKUP MODEL BUSANA DENGAN NAMA, KELAS & FITUR SHARE -->
<div id="view-mockup" class="hidden space-y-6">
<div class="bg-gradient-to-r from-batikBrown to-amber-900 rounded-2xl p-6 md:p-8 text-white shadow-md flex flex-col md:flex-row justify-between items-center">
<div>
<h2 class="font-serif text-2xl md:text-3xl font-bold mb-2">Simulasi Model Busana Batik</h2>
<p class="text-amber-100 text-sm max-w-xl">Hasil karya warna motif Anda beserta identitas siswa tersemat langsung pada peraga busana profesional!</p>
</div>
<div class="flex items-center space-x-3 mt-4 md:mt-0">
<button onclick="app.openShareModal()" class="bg-emerald-600 hover:bg-emerald-700 text-white font-semibold px-4 py-3 rounded-xl shadow transition text-sm flex items-center space-x-2">
<i class="fa-solid fa-share-nodes"></i>
<span>Bagikan Karya</span>
</button>
<button onclick="app.switchView('editor')" class="bg-white text-batikBrown font-semibold px-4 py-3 rounded-xl shadow hover:bg-amber-50 transition text-sm flex items-center space-x-2">
<i class="fa-solid fa-paintbrush"></i>
<span>Kembali Mewarnai</span>
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Pilihan Model Baju -->
<div class="lg:col-span-4 bg-white rounded-2xl p-5 shadow-sm border border-amber-100 flex flex-col space-y-4">
<h3 class="text-xs font-bold tracking-wider text-batikGold uppercase">Pilih Jenis Pakaian</h3>
<div class="grid grid-cols-2 gap-3" id="shirt-model-selector">
<button onclick="app.setShirtModel('shirt')" id="model-btn-shirt" class="p-3 rounded-xl border border-batikBrown bg-amber-50 text-batikBrown font-semibold text-xs flex flex-col items-center justify-center space-y-2 transition shadow-xs">
<i class="fa-solid fa-shirt text-2xl"></i>
<span>Kemeja Pria</span>
</button>
<button onclick="app.setShirtModel('blouse')" id="model-btn-blouse" class="p-3 rounded-xl border border-slate-200 bg-white text-slate-600 font-semibold text-xs flex flex-col items-center justify-center space-y-2 transition hover:bg-slate-50">
<i class="fa-solid fa-person-dress text-2xl"></i>
<span>Dress / Blus</span>
</button>
<button onclick="app.setShirtModel('bomber')" id="model-btn-bomber" class="p-3 rounded-xl border border-slate-200 bg-white text-slate-600 font-semibold text-xs flex flex-col items-center justify-center space-y-2 transition hover:bg-slate-50">
<i class="fa-solid fa-vest text-2xl"></i>
<span>Jaket Bomber</span>
</button>
<button onclick="app.setShirtModel('blazer')" id="model-btn-blazer" class="p-3 rounded-xl border border-slate-200 bg-white text-slate-600 font-semibold text-xs flex flex-col items-center justify-center space-y-2 transition hover:bg-slate-50">
<i class="fa-solid fa-user-tie text-2xl"></i>
<span>Outer / Blazer</span>
</button>
</div>
<div class="p-4 bg-amber-50/60 rounded-xl border border-amber-200/60 text-xs text-slate-600 space-y-2">
<p class="font-bold text-batikBrown"><i class="fa-solid fa-circle-info mr-1"></i> Identitas & Atribut</p>
<p>Nama dan kelas yang diisi di studio akan otomatis tertera pada label sertifikat kartu peraga busana di sebelah kanan.</p>
</div>
<button onclick="app.saveArtwork()" class="w-full py-3 bg-batikBrown hover:bg-amber-900 text-white rounded-xl font-semibold text-sm transition shadow-md flex items-center justify-center space-x-2">
<i class="fa-solid fa-cloud-arrow-up"></i>
<span>Simpan Hasil Busana ke Galeri</span>
</button>
</div>
<!-- Preview Area Baju dengan Nama, Kelas & Watermark SMK Kebonjeruk -->
<div class="lg:col-span-8 bg-white rounded-2xl p-6 shadow-sm border border-amber-100 flex flex-col items-center justify-center relative min-h-[500px]">
<div class="absolute top-4 left-4 bg-amber-50 border border-amber-200 px-3.5 py-1.5 rounded-lg text-xs font-semibold text-batikBrown flex items-center space-x-2 shadow-xs">
<i class="fa-solid fa-wand-magic-sparkles text-batikGold"></i>
<span id="active-model-label">Mockup: Kemeja Pria Formal Batik</span>
</div>
<!-- Card Wrapper dengan Nama, Kelas & Watermark SMK Kebonjeruk di bagian akhir -->
<div class="w-full max-w-[420px] bg-slate-50/80 border border-amber-200/70 rounded-2xl p-4 shadow-inner flex flex-col items-center mt-8 lg:mt-0">
<div id="shirt-svg-wrapper" class="w-full h-[320px] flex items-center justify-center transition-all duration-300 mb-3"></div>
<!-- Badge Identitas Siswa pada Mockup -->
<div class="w-full bg-white border border-amber-200 rounded-xl p-3 shadow-xs flex items-center justify-between mb-2.5">
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-lg bg-amber-100 text-batikBrown flex items-center justify-center font-bold text-xs">
<i class="fa-solid fa-id-badge"></i>
</div>
<div>
<p id="preview-student-name" class="text-xs font-bold text-slate-800">Siswa Kreatif</p>
<p class="text-[11px] text-slate-500">Kelas: <span id="preview-student-class" class="font-semibold text-batikBrown">VII</span></p>
</div>
</div>
<span id="preview-pattern-badge" class="text-[10px] font-bold uppercase tracking-wider text-amber-800 bg-amber-50 px-2.5 py-1 rounded-full border border-amber-200">Megamendung</span>
</div>
<!-- Watermark SMK Kebonjeruk di Akhir Preview -->
<div class="w-full pt-2 border-t border-dashed border-amber-200 flex items-center justify-center space-x-2 text-slate-500 text-[11px] font-semibold tracking-wide">
<i class="fa-solid fa-school text-batikBrown"></i>
<span>Karya Resmi Digital • SMK Kebonjeruk</span>
</div>
</div>
</div>
</div>
</div>
<!-- VIEW 3: GALERI KARYA SISWA -->
<div id="view-gallery" class="hidden space-y-6">
<div class="bg-gradient-to-r from-batikBrown to-amber-900 rounded-2xl p-6 md:p-8 text-white shadow-md flex flex-col md:flex-row justify-between items-center">
<div>
<h2 class="font-serif text-2xl md:text-3xl font-bold mb-2">Galeri Pameran Batik Siswa</h2>
<p class="text-amber-100 text-sm max-w-xl">Koleksi mahakarya digital beserta informasi lengkap nama dan kelas siswa.</p>
</div>
<button onclick="app.switchView('editor')" class="mt-4 md:mt-0 bg-white text-batikBrown font-semibold px-5 py-3 rounded-xl shadow hover:bg-amber-50 transition text-sm flex items-center space-x-2">
<i class="fa-solid fa-plus"></i>
<span>Buat Batik Baru</span>
</button>
</div>
<div class="flex flex-wrap items-center gap-2" id="gallery-filters">
<button onclick="app.filterGallery('all')" class="px-4 py-2 rounded-xl text-xs font-semibold bg-batikBrown text-white shadow-sm transition">Semua Karya</button>
<button onclick="app.filterGallery('Megamendung')" class="px-4 py-2 rounded-xl text-xs font-semibold bg-white text-slate-600 hover:bg-amber-50 border border-slate-200 transition">Megamendung</button>
<button onclick="app.filterGallery('Parang')" class="px-4 py-2 rounded-xl text-xs font-semibold bg-white text-slate-600 hover:bg-amber-50 border border-slate-200 transition">Parang Rusak</button>
<button onclick="app.filterGallery('Kawung')" class="px-4 py-2 rounded-xl text-xs font-semibold bg-white text-slate-600 hover:bg-amber-50 border border-slate-200 transition">Kawung</button>
<button onclick="app.filterGallery('Truntum')" class="px-4 py-2 rounded-xl text-xs font-semibold bg-white text-slate-600 hover:bg-amber-50 border border-slate-200 transition">Truntum</button>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6" id="gallery-grid"></div>
</div>
</main>
<!-- MODAL SHARE KARYA -->
<div id="share-modal" class="fixed inset-0 z-50 bg-slate-900/60 backdrop-blur-xs flex items-center justify-center p-4 hidden">
<div class="bg-white rounded-2xl max-w-md w-full p-6 shadow-xl border border-amber-100 space-y-4 transform transition-all">
<div class="flex items-center justify-between border-b border-slate-100 pb-3">
<div class="flex items-center space-x-2">
<div class="w-8 h-8 rounded-lg bg-amber-100 text-batikBrown flex items-center justify-center font-bold">
<i class="fa-solid fa-share-nodes text-sm"></i>
</div>
<h3 class="font-serif text-lg font-bold text-batikBrown">Bagikan Hasil Karya</h3>
</div>
<button onclick="app.closeShareModal()" class="w-8 h-8 rounded-full bg-slate-100 hover:bg-slate-200 text-slate-500 flex items-center justify-center transition">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<p class="text-xs text-slate-600">Pilih cara untuk membagikan karya busana batik Anda kepada teman atau guru:</p>
<div class="grid grid-cols-2 gap-3">
<button onclick="app.copyShareLink()" class="p-3 rounded-xl border border-amber-200 bg-amber-50 hover:bg-amber-100/70 text-batikBrown font-semibold text-xs flex flex-col items-center justify-center space-y-1.5 transition">
<i class="fa-solid fa-link text-lg"></i>
<span>Salin Tautan</span>
</button>
<button onclick="app.downloadShareCard()" class="p-3 rounded-xl border border-amber-200 bg-amber-50 hover:bg-amber-100/70 text-batikBrown font-semibold text-xs flex flex-col items-center justify-center space-y-1.5 transition">
<i class="fa-solid fa-download text-lg"></i>
<span>Unduh Gambar</span>
</button>
</div>
<div class="p-3 bg-slate-50 rounded-xl border border-slate-200 text-[11px] text-slate-500 flex items-center space-x-2">
<i class="fa-solid fa-circle-info text-amber-600"></i>
<span id="share-link-text" class="truncate">https://suryo-batikkreasi.edu/pameran/karya-siswa</span>
</div>
<div class="pt-2 flex justify-end">
<button onclick="app.closeShareModal()" class="px-4 py-2 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded-xl text-xs font-semibold transition">Tutup</button>
</div>
</div>
</div>
<div id="toast" class="fixed bottom-5 right-5 z-50 transform translate-y-20 opacity-0 transition-all duration-300 bg-slate-900 text-white px-5 py-3 rounded-xl shadow-xl flex items-center space-x-3 text-sm font-medium">
<i class="fa-solid fa-circle-check text-emerald-400 text-lg"></i>
<span id="toast-message">Berhasil menyimpan karya!</span>
</div>
<script>
class BatikApp {
constructor() {
this.currentView = 'editor';
this.currentTool = 'brush';
this.brushSize = 15;
this.currentColor = '#8B263E';
this.selectedPatternId = 'megamendung';
this.activeGalleryFilter = 'all';
this.currentShirtModel = 'shirt';
this.canvas = document.getElementById('batikCanvas');
this.ctx = this.canvas.getContext('2d', { willReadFrequently: true });
this.isDrawing = false;
this.lastX = 0;
this.lastY = 0;
this.colors = [
'#8B263E', '#C59B27', '#5C3A21', '#1B3B36', '#1E3A8A', '#7C2D12',
'#B45309', '#047857', '#4338CA', '#BE185D', '#374151', '#F59E0B'
];
this.patterns = {
megamendung: {
name: 'Megamendung (Cirebon)',
desc: 'Motif awan bertumpuk besar asal Cirebon, melambangkan keteduhan dan kebijaksanaan.',
draw: (ctx, w, h) => this.drawMegaMendung(ctx, w, h)
},
parang: {
name: 'Parang Rusak (Yogyakarta)',
desc: 'Motif garis diagonal besar melambangkan semangat juang tak pernah padam.',
draw: (ctx, w, h) => this.drawParang(ctx, w, h)
},
kawung: {
name: 'Kawung (Solo)',
desc: 'Motif bulatan besar berbentuk kolang-kaling melambangkan kemurnian hati.',
draw: (ctx, w, h) => this.drawKawung(ctx, w, h)
},
truntum: {
name: 'Truntum (Surakarta)',
desc: 'Motif taburan bintang besar melambangkan cinta kasih yang tulus.',
draw: (ctx, w, h) => this.drawTruntum(ctx, w, h)
}
};
this.shirtModels = {
shirt: { name: 'Kemeja Pria Formal Batik', icon: 'fa-shirt' },
blouse: { name: 'Dress / Blus Wanita Batik', icon: 'fa-person-dress' },
bomber: { name: 'Jaket Bomber Batik Modern', icon: 'fa-vest' },
blazer: { name: 'Outer / Blazer Batik Elegan', icon: 'fa-user-tie' }
};
this.gallery = [
{ id: 1, title: 'Megamendung Senja', author: 'Siti Aisyah', studentClass: 'VII A', pattern: 'Megamendung', date: '03 Okt 2026', image: 'https://placehold.co/400x400/8B263E/FDFBF7?text=Megamendung+Senja' },
{ id: 2, title: 'Parang Keraton', author: 'Budi Santoso', studentClass: 'VIII B', pattern: 'Parang', date: '02 Okt 2026', image: 'https://placehold.co/400x400/C59B27/5C3A21?text=Parang+Keraton' }
];
this.init();
}
init() {
this.renderPatternSelector();
this.renderPalette();
this.loadPattern(this.selectedPatternId);
this.renderGallery();
this.setupCanvasEvents();
}
switchView(viewName) {
this.currentView = viewName;
const editorView = document.getElementById('view-editor');
const mockupView = document.getElementById('view-mockup');
const galleryView = document.getElementById('view-gallery');
const navEditor = document.getElementById('nav-editor');
const navMockup = document.getElementById('nav-mockup');
const navGallery = document.getElementById('nav-gallery');
editorView.classList.add('hidden');
mockupView.classList.add('hidden');
galleryView.classList.add('hidden');
navEditor.className = "px-3.5 py-2 rounded-xl text-sm font-semibold transition text-slate-600 hover:bg-amber-50";
navMockup.className = "px-3.5 py-2 rounded-xl text-sm font-semibold transition text-slate-600 hover:bg-amber-50";
navGallery.className = "px-3.5 py-2 rounded-xl text-sm font-semibold transition text-slate-600 hover:bg-amber-50";
if (viewName === 'editor') {
editorView.classList.remove('hidden');
navEditor.className = "px-3.5 py-2 rounded-xl text-sm font-semibold transition bg-amber-100 text-batikBrown shadow-inner";
} else if (viewName === 'mockup') {
mockupView.classList.remove('hidden');
navMockup.className = "px-3.5 py-2 rounded-xl text-sm font-semibold transition bg-amber-100 text-batikBrown shadow-inner";
this.updatePreviewStudentInfo();
this.renderShirtPreview();
} else {
galleryView.classList.remove('hidden');
navGallery.className = "px-3.5 py-2 rounded-xl text-sm font-semibold transition bg-amber-100 text-batikBrown shadow-inner";
this.renderGallery();
}
}
updatePreviewStudentInfo() {
const nameInput = document.getElementById('student-name');
const classInput = document.getElementById('student-class');
const studentName = nameInput.value.trim() || 'Siswa Kreatif';
const studentClass = classInput.value.trim() || 'VII';
const shortPattern = this.patterns[this.selectedPatternId].name.split(' ')[0];
document.getElementById('preview-student-name').innerText = studentName;
document.getElementById('preview-student-class').innerText = studentClass;
document.getElementById('preview-pattern-badge').innerText = shortPattern;
}
renderPatternSelector() {
const container = document.getElementById('pattern-selector');
container.innerHTML = Object.keys(this.patterns).map(key => {
const pat = this.patterns[key];
const isSelected = this.selectedPatternId === key;
const shortName = pat.name.split(' ')[0];
return `
<button onclick="app.loadPattern('${key}')" class="p-2.5 rounded-xl border text-center transition flex flex-col items-center justify-center space-y-1 ${isSelected ? 'border-batikBrown bg-amber-50/80 shadow-xs ring-1 ring-batikBrown' : 'border-slate-200 bg-white hover:bg-slate-50'}">
<i class="fa-solid fa-shapes text-batikBrown text-base"></i>
<span class="text-xs font-semibold text-slate-700 truncate w-full">${shortName}</span>
</button>
`;
}).join('');
}
loadPattern(key) {
this.selectedPatternId = key;
const pat = this.patterns[key];
document.getElementById('active-pattern-label').innerText = `Motif Aktif: ${pat.name}`;
document.getElementById('pattern-info-card').innerHTML = `<p class="font-semibold text-batikBrown mb-1"><i class="fa-solid fa-book-open mr-1"></i> ${pat.name}</p><p>${pat.desc}</p>`;
this.renderPatternSelector();
this.redrawCanvas();
}
redrawCanvas() {
const w = this.canvas.width;
const h = this.canvas.height;
this.ctx.fillStyle = '#FFFFFF';
this.ctx.fillRect(0, 0, w, h);
this.patterns[this.selectedPatternId].draw(this.ctx, w, h);
}
renderPalette() {
const paletteContainer = document.getElementById('color-palette');
paletteContainer.innerHTML = this.colors.map(col => `
<button onclick="app.setColor('${col}')" class="w-full aspect-square rounded-xl transition transform hover:scale-105 shadow-xs border-2 ${this.currentColor === col ? 'border-slate-900 scale-105 ring-2 ring-amber-300' : 'border-white'}" style="background-color: ${col};"></button>
`).join('');
}
setColor(col) {
this.currentColor = col;
this.renderPalette();
}
setTool(tool) {
this.currentTool = tool;
const btnBrush = document.getElementById('tool-brush');
const btnFill = document.getElementById('tool-fill');
const sizeContainer = document.getElementById('brush-size-container');
if (tool === 'brush') {
btnBrush.className = "py-2.5 px-3 rounded-xl border border-amber-300 bg-amber-50 text-batikBrown text-sm font-semibold flex items-center justify-center space-x-2 transition shadow-sm";
btnFill.className = "py-2.5 px-3 rounded-xl border border-slate-200 bg-white text-slate-600 text-sm font-semibold flex items-center justify-center space-x-2 transition hover:bg-slate-50";
sizeContainer.style.display = 'block';
} else {
btnFill.className = "py-2.5 px-3 rounded-xl border border-amber-300 bg-amber-50 text-batikBrown text-sm font-semibold flex items-center justify-center space-x-2 transition shadow-sm";
btnBrush.className = "py-2.5 px-3 rounded-xl border border-slate-200 bg-white text-slate-600 text-sm font-semibold flex items-center justify-center space-x-2 transition hover:bg-slate-50";
sizeContainer.style.display = 'none';
}
}
setBrushSize(val) {
this.brushSize = parseInt(val);
document.getElementById('brush-size-val').innerText = `${val}px`;
}
setupCanvasEvents() {
const getPos = (e) => {
const rect = this.canvas.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return {
x: (clientX - rect.left) * (this.canvas.width / rect.width),
y: (clientY - rect.top) * (this.canvas.height / rect.height)
};
};
const startAction = (e) => {
e.preventDefault();
const pos = getPos(e);
if (this.currentTool === 'fill') {
this.floodFill(Math.floor(pos.x), Math.floor(pos.y), this.hexToRgba(this.currentColor));
} else {
this.isDrawing = true;
this.lastX = pos.x;
this.lastY = pos.y;
}
};
const moveAction = (e) => {
if (!this.isDrawing || this.currentTool !== 'brush') return;
e.preventDefault();
const pos = getPos(e);
this.ctx.beginPath();
this.ctx.moveTo(this.lastX, this.lastY);
this.ctx.lineTo(pos.x, pos.y);
this.ctx.strokeStyle = this.currentColor;
this.ctx.lineWidth = this.brushSize;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.ctx.stroke();
this.lastX = pos.x;
this.lastY = pos.y;
};
const endAction = () => {
this.isDrawing = false;
};
this.canvas.addEventListener('mousedown', startAction);
this.canvas.addEventListener('mousemove', moveAction);
window.addEventListener('mouseup', endAction);
this.canvas.addEventListener('touchstart', startAction, { passive: false });
this.canvas.addEventListener('touchmove', moveAction, { passive: false });
window.addEventListener('touchend', endAction);
}
hexToRgba(hex) {
let c = hex.replace('#', '');
if (c.length === 3) c = c.split('').map(x => x + x).join('');
const num = parseInt(c, 16);
return [num >> 16, (num >> 8) & 255, num & 255, 255];
}
floodFill(startX, startY, fillColor) {
const w = this.canvas.width;
const h = this.canvas.height;
const imgData = this.ctx.getImageData(0, 0, w, h);
const data = imgData.data;
const getIdx = (x, y) => (y * w + x) * 4;
const startIdx = getIdx(startX, startY);
const startR = data[startIdx];
const startG = data[startIdx + 1];
const startB = data[startIdx + 2];
const startA = data[startIdx + 3];
if (startR === fillColor[0] && startG === fillColor[1] && startB === fillColor[2] && startA === fillColor[3]) return;
if (startR < 50 && startG < 50 && startB < 50) return;
const matchStartColor = (idx) => {
return data[idx] === startR && data[idx + 1] === startG && data[idx + 2] === startB && data[idx + 3] === startA;
};
const colorPixel = (idx) => {
data[idx] = fillColor[0];
data[idx + 1] = fillColor[1];
data[idx + 2] = fillColor[2];
data[idx + 3] = fillColor[3];
};
const queue = [[startX, startY]];
const visited = new Uint8Array(w * h);
while (queue.length > 0) {
const [x, y] = queue.pop();
if (x < 0 || x >= w || y < 0 || y >= h) continue;
const idx = y * w + x;
if (visited[idx]) continue;
visited[idx] = 1;
const pIdx = getIdx(x, y);
if (data[pIdx] < 50 && data[pIdx + 1] < 50 && data[pIdx + 2] < 50) continue;
if (!matchStartColor(pIdx)) continue;
colorPixel(pIdx);
queue.push([x + 1, y]);
queue.push([x - 1, y]);
queue.push([x, y + 1]);
queue.push([x, y - 1]);
}
this.ctx.putImageData(imgData, 0, 0);
}
clearCanvas() {
this.redrawCanvas();
this.showToast('Kanvas telah diatur ulang.');
}
previewOnShirt() {
this.switchView('mockup');
}
setShirtModel(modelKey) {
this.currentShirtModel = modelKey;
const keys = ['shirt', 'blouse', 'bomber', 'blazer'];
keys.forEach(k => {
const btn = document.getElementById(`model-btn-${k}`);
if (k === modelKey) {
btn.className = "p-3 rounded-xl border border-batikBrown bg-amber-50 text-batikBrown font-semibold text-xs flex flex-col items-center justify-center space-y-2 transition shadow-xs";
} else {
btn.className = "p-3 rounded-xl border border-slate-200 bg-white text-slate-600 font-semibold text-xs flex flex-col items-center justify-center space-y-2 transition hover:bg-slate-50";
}
});
document.getElementById('active-model-label').innerText = `Mockup: ${this.shirtModels[modelKey].name}`;
this.renderShirtPreview();
}
renderShirtPreview() {
const wrapper = document.getElementById('shirt-svg-wrapper');
const dataURL = this.canvas.toDataURL('image/png');
let svgContent = '';
if (this.currentShirtModel === 'shirt') {
svgContent = `
<svg viewBox="0 0 300 350" class="w-full h-full drop-shadow-md">
<defs>
<pattern id="batikTex" width="80" height="80" patternUnits="userSpaceOnUse">
<image href="${dataURL}" width="80" height="80" />
</pattern>
</defs>
<path d="M90 60 L60 80 L30 140 L70 160 L80 320 L220 320 L230 160 L270 140 L240 80 L210 60 C180 80 120 80 90 60 Z" fill="url(#batikTex)" stroke="#5C3A21" stroke-width="3"/>
<polygon points="90,60 150,110 210,60 170,40 150,50 130,40" fill="#fdfbf7" stroke="#5C3A21" stroke-width="2"/>
<line x1="150" y1="110" x2="150" y2="320" stroke="#5C3A21" stroke-width="3" stroke-dasharray="6,6"/>
<circle cx="150" cy="140" r="3" fill="#C59B27"/>
<circle cx="150" cy="180" r="3" fill="#C59B27"/>
<circle cx="150" cy="220" r="3" fill="#C59B27"/>
</svg>
`;
} else if (this.currentShirtModel === 'blouse') {
svgContent = `
<svg viewBox="0 0 300 350" class="w-full h-full drop-shadow-md">
<defs>
<pattern id="batikTex" width="80" height="80" patternUnits="userSpaceOnUse">
<image href="${dataURL}" width="80" height="80" />
</pattern>
</defs>
<path d="M100 50 L50 90 L70 140 L90 330 L210 330 L230 140 L250 90 L200 50 C175 70 125 70 100 50 Z" fill="url(#batikTex)" stroke="#5C3A21" stroke-width="3"/>
<path d="M100 50 Q150 90 200 50 Q150 70 100 50 Z" fill="#fdfbf7" stroke="#5C3A21" stroke-width="2"/>
</svg>
`;
} else if (this.currentShirtModel === 'bomber') {
svgContent = `
<svg viewBox="0 0 300 350" class="w-full h-full drop-shadow-md">
<defs>
<pattern id="batikTex" width="80" height="80" patternUnits="userSpaceOnUse">
<image href="${dataURL}" width="80" height="80" />
</pattern>
</defs>
<path d="M85 70 L40 100 L30 180 L80 320 L220 320 L270 180 L260 100 L215 70 C185 85 115 85 85 70 Z" fill="url(#batikTex)" stroke="#1B3B36" stroke-width="3"/>
<rect x="80" y="300" width="140" height="20" rx="5" fill="#1B3B36"/>
<line x1="150" y1="70" x2="150" y2="320" stroke="#1B3B36" stroke-width="4"/>
</svg>
`;
} else {
svgContent = `
<svg viewBox="0 0 300 350" class="w-full h-full drop-shadow-md">
<defs>
<pattern id="batikTex" width="80" height="80" patternUnits="userSpaceOnUse">
<image href="${dataURL}" width="80" height="80" />
</pattern>
</defs>
<path d="M90 60 L50 90 L40 320 L130 320 L130 130 L170 130 L170 320 L260 320 L250 90 L210 60 C180 80 120 80 90 60 Z" fill="url(#batikTex)" stroke="#5C3A21" stroke-width="3"/>
<polygon points="90,60 130,130 130,60" fill="#fdfbf7" stroke="#5C3A21" stroke-width="2"/>
<polygon points="210,60 170,130 170,60" fill="#fdfbf7" stroke="#5C3A21" stroke-width="2"/>
</svg>
`;
}
wrapper.innerHTML = svgContent;
}
openShareModal() {
const modal = document.getElementById('share-modal');
modal.classList.remove('hidden');
}
closeShareModal() {
const modal = document.getElementById('share-modal');
modal.classList.add('hidden');
}
copyShareLink() {
const dummyText = `https://suryo-batikkreasi.edu/pameran/karya-${Date.now()}`;
const tempInput = document.createElement('input');
tempInput.value = dummyText;
document.body.appendChild(tempInput);
tempInput.select();
document.execCommand('copy');
document.body.removeChild(tempInput);
this.showToast('Tautan karya berhasil disalin ke clipboard!');
this.closeShareModal();
}
downloadShareCard() {
const studentName = document.getElementById('student-name').value.trim() || 'Siswa Kreatif';
this.showToast(`Mengunduh kartu pameran busana untuk ${studentName}...`);
this.closeShareModal();
}
saveArtwork() {
const studentNameInput = document.getElementById('student-name');
const studentClassInput = document.getElementById('student-class');
const studentName = studentNameInput.value.trim() || 'Siswa Kreatif';
const studentClass = studentClassInput.value.trim() || 'VII';
const dataURL = this.canvas.toDataURL('image/png');
const patternNameFull = this.patterns[this.selectedPatternId].name;
const shortPattern = patternNameFull.split(' ')[0];
const newEntry = {
id: Date.now(),
title: `${shortPattern} (${this.shirtModels[this.currentShirtModel].name.split(' ')[0]}) - ${studentName}`,
author: studentName,
studentClass: studentClass,
pattern: shortPattern,
date: 'Hari ini',
image: dataURL
};
this.gallery.unshift(newEntry);
this.showToast('Karya & Mockup Busana berhasil disimpan!');
this.switchView('gallery');
}
filterGallery(patternName) {
this.activeGalleryFilter = patternName;
const buttons = document.querySelectorAll('#gallery-filters button');
buttons.forEach(btn => {
if (btn.innerText.toLowerCase().includes(patternName.toLowerCase()) || (patternName === 'all' && btn.innerText.includes('Semua'))) {
btn.className = "px-4 py-2 rounded-xl text-xs font-semibold bg-batikBrown text-white shadow-sm transition";
} else {
btn.className = "px-4 py-2 rounded-xl text-xs font-semibold bg-white text-slate-600 hover:bg-amber-50 border border-slate-200 transition";
}
});
this.renderGallery();
}
renderGallery() {
const grid = document.getElementById('gallery-grid');
const filtered = this.activeGalleryFilter === 'all'
? this.gallery
: this.gallery.filter(item => item.pattern.toLowerCase().includes(this.activeGalleryFilter.toLowerCase()));
if (filtered.length === 0) {
grid.innerHTML = `
<div class="col-span-full py-12 text-center text-slate-400">
<i class="fa-solid fa-folder-open text-4xl mb-2"></i>
<p class="text-sm">Belum ada karya untuk kategori ini.</p>
</div>
`;
return;
}
grid.innerHTML = filtered.map(item => `
<div class="bg-white rounded-2xl overflow-hidden shadow-sm border border-amber-100 flex flex-col group hover:shadow-md transition">
<div class="aspect-square bg-amber-50/50 overflow-hidden relative flex items-center justify-center p-3">
<img src="${item.image}" alt="${item.title}" class="w-full h-full object-contain rounded-xl group-hover:scale-105 transition duration-300">
</div>
<div class="p-4 flex-1 flex flex-col justify-between">
<div>
<span class="text-[10px] font-bold uppercase tracking-wider text-amber-700 bg-amber-50 px-2.5 py-1 rounded-full border border-amber-200">${item.pattern}</span>
<h3 class="font-bold text-slate-800 mt-2 text-base truncate">${item.title}</h3>
<p class="text-xs text-slate-500 mt-0.5">Karya: <span class="font-medium text-slate-700">${item.author}</span> <span class="text-slate-400">(${item.studentClass})</span></p>
</div>
<div class="mt-4 pt-3 border-t border-slate-100 flex items-center justify-between text-xs text-slate-400">
<span><i class="fa-regular fa-calendar mr-1"></i> ${item.date}</span>
<a href="${item.image}" download="batik-${item.author.toLowerCase().replace(/\s+/g, '-')}.png" class="text-batikBrown font-semibold hover:underline flex items-center space-x-1">
<i class="fa-solid fa-download"></i>
<span>Unduh</span>
</a>
</div>
</div>
</div>
`).join('');
}
showToast(msg) {
const toast = document.getElementById('toast');
const toastMsg = document.getElementById('toast-message');
toastMsg.innerText = msg;
toast.classList.remove('translate-y-20', 'opacity-0');
setTimeout(() => {
toast.classList.add('translate-y-20', 'opacity-0');
}, 3000);
}
drawMegaMendung(ctx, w, h) {
ctx.lineWidth = 6;
ctx.strokeStyle = '#1a1a1a';
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
ctx.arc(w * 0.35, h * 0.35, w * 0.22, Math.PI, 0, false);
ctx.arc(w * 0.65, h * 0.35, w * 0.18, Math.PI, 0, false);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.arc(w * 0.65, h * 0.65, w * 0.22, Math.PI, 0, true);
ctx.arc(w * 0.35, h * 0.65, w * 0.18, Math.PI, 0, true);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.arc(w * 0.5, h * 0.5, w * 0.12, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
drawParang(ctx, w, h) {
ctx.lineWidth = 6;
ctx.strokeStyle = '#1a1a1a';
ctx.fillStyle = '#FFFFFF';
for (let i = -1; i < 3; i++) {
ctx.beginPath();
ctx.moveTo(0, i * 200);
ctx.bezierCurveTo(w * 0.5, i * 200 + 100, w * 0.5, i * 200 + 200, w, i * 200 + 300);
ctx.lineTo(w, i * 200 + 400);
ctx.bezierCurveTo(w * 0.5, i * 200 + 300, w * 0.5, i * 200 + 200, 0, i * 200 + 100);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
}
drawKawung(ctx, w, h) {
ctx.lineWidth = 6;
ctx.strokeStyle = '#1a1a1a';
ctx.fillStyle = '#FFFFFF';
const cx = w / 2;
const cy = h / 2;
const r = w * 0.22;
const offsets = [
{x: 0, y: -r}, {x: r, y: 0}, {x: 0, y: r}, {x: -r, y: 0}
];
offsets.forEach(off => {
ctx.beginPath();
ctx.arc(cx + off.x, cy + off.y, r * 0.9, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
});
ctx.beginPath();
ctx.arc(cx, cy, r * 0.45, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
drawTruntum(ctx, w, h) {
ctx.lineWidth = 6;
ctx.strokeStyle = '#1a1a1a';
ctx.fillStyle = '#FFFFFF';
const centers = [
{x: w * 0.25, y: h * 0.25},
{x: w * 0.75, y: h * 0.25},
{x: w * 0.5, y: h * 0.5},
{x: w * 0.25, y: h * 0.75},
{x: w * 0.75, y: h * 0.75}
];
centers.forEach(c => {
ctx.beginPath();
ctx.arc(c.x, c.y, w * 0.16, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
for (let i = 0; i < 8; i++) {
const angle = (i * Math.PI) / 4;
const bx = c.x + Math.cos(angle) * (w * 0.12);
const by = c.y + Math.sin(angle) * (w * 0.12);
ctx.beginPath();
ctx.arc(bx, by, w * 0.04, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
});
}
}
let app;
window.onload = function() {
app = new BatikApp();
};
</script>
</body>
</html>
0 Comments