Spaces:
Running
Running
| class SketchToRenderApp { | |
| constructor() { | |
| this.canvas = document.getElementById('drawingCanvas'); | |
| this.ctx = this.canvas.getContext('2d'); | |
| this.isDrawing = false; | |
| this.drawingHistory = []; | |
| this.currentPath = []; | |
| this.renderedImageUrl = null; | |
| this.initializeCanvas(); | |
| this.setupEventListeners(); | |
| this.saveCanvasState(); | |
| } | |
| initializeCanvas() { | |
| this.ctx.lineCap = 'round'; | |
| this.ctx.lineJoin = 'round'; | |
| this.ctx.fillStyle = 'white'; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| } | |
| setupEventListeners() { | |
| this.canvas.addEventListener('mousedown', (e) => this.startDrawing(e)); | |
| this.canvas.addEventListener('mousemove', (e) => this.draw(e)); | |
| this.canvas.addEventListener('mouseup', () => this.stopDrawing()); | |
| this.canvas.addEventListener('mouseout', () => this.stopDrawing()); | |
| this.canvas.addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| const mouseEvent = new MouseEvent('mousedown', { | |
| clientX: touch.clientX, | |
| clientY: touch.clientY | |
| }); | |
| this.canvas.dispatchEvent(mouseEvent); | |
| }); | |
| this.canvas.addEventListener('touchmove', (e) => { | |
| e.preventDefault(); | |
| const touch = e.touches[0]; | |
| const mouseEvent = new MouseEvent('mousemove', { | |
| clientX: touch.clientX, | |
| clientY: touch.clientY | |
| }); | |
| this.canvas.dispatchEvent(mouseEvent); | |
| }); | |
| this.canvas.addEventListener('touchend', (e) => { | |
| e.preventDefault(); | |
| const mouseEvent = new MouseEvent('mouseup', {}); | |
| this.canvas.dispatchEvent(mouseEvent); | |
| }); | |
| document.getElementById('brushSize').addEventListener('input', (e) => { | |
| document.getElementById('brushSizeValue').textContent = e.target.value + 'px'; | |
| }); | |
| document.getElementById('clearCanvas').addEventListener('click', () => this.clearCanvas()); | |
| document.getElementById('undoBtn').addEventListener('click', () => this.undo()); | |
| document.getElementById('generateBtn').addEventListener('click', () => this.generateRender()); | |
| document.getElementById('downloadBtn').addEventListener('click', () => this.downloadImage()); | |
| } | |
| getMousePos(e) { | |
| const rect = this.canvas.getBoundingClientRect(); | |
| return { | |
| x: e.clientX - rect.left, | |
| y: e.clientY - rect.top | |
| }; | |
| } | |
| startDrawing(e) { | |
| this.isDrawing = true; | |
| const pos = this.getMousePos(e); | |
| this.currentPath = [pos]; | |
| this.ctx.strokeStyle = document.getElementById('brushColor').value; | |
| this.ctx.lineWidth = document.getElementById('brushSize').value; | |
| this.ctx.beginPath(); | |
| this.ctx.moveTo(pos.x, pos.y); | |
| } | |
| draw(e) { | |
| if (!this.isDrawing) return; | |
| const pos = this.getMousePos(e); | |
| this.currentPath.push(pos); | |
| this.ctx.lineTo(pos.x, pos.y); | |
| this.ctx.stroke(); | |
| } | |
| stopDrawing() { | |
| if (this.isDrawing) { | |
| this.isDrawing = false; | |
| this.saveCanvasState(); | |
| } | |
| } | |
| saveCanvasState() { | |
| this.drawingHistory.push(this.canvas.toDataURL()); | |
| if (this.drawingHistory.length > 20) { | |
| this.drawingHistory.shift(); | |
| } | |
| } | |
| clearCanvas() { | |
| this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
| this.ctx.fillStyle = 'white'; | |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); | |
| this.saveCanvasState(); | |
| } | |
| undo() { | |
| if (this.drawingHistory.length > 1) { | |
| this.drawingHistory.pop(); | |
| const previousState = this.drawingHistory[this.drawingHistory.length - 1]; | |
| const img = new Image(); | |
| img.onload = () => { | |
| this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
| this.ctx.drawImage(img, 0, 0); | |
| }; | |
| img.src = previousState; | |
| } | |
| } | |
| async generateRender() { | |
| if (!this.isCanvasEmpty()) { | |
| this.showLoading(true); | |
| this.hideError(); | |
| try { | |
| const analysisResult = await this.analyzeSketchWithGemini(); | |
| const imageUrl = await this.generateImageFromDescription(analysisResult.description); | |
| this.displayResults(imageUrl, analysisResult.score, analysisResult.prediction); | |
| } catch (error) { | |
| console.error(error); | |
| this.showError(`Lỗi: ${error.message}`); | |
| } finally { | |
| this.showLoading(false); | |
| } | |
| } else { | |
| this.showError('Vui lòng vẽ gì đó trước khi tạo render'); | |
| } | |
| } | |
| isCanvasEmpty() { | |
| const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); | |
| return imageData.data.every((pixel, index) => { | |
| if ((index + 1) % 4 === 0) return true; | |
| return pixel === 255; | |
| }); | |
| } | |
| async analyzeSketchWithGemini() { | |
| const canvasDataURL = this.canvas.toDataURL('image/png'); | |
| const base64Data = canvasDataURL.split(',')[1]; | |
| const prompt = `Hãy phân tích bức vẽ sketch sau và cung cấp: | |
| 1. ĐIỂM SỐ: Đánh giá độ thẩm mỹ của sketch theo thang 6–10 | |
| 2. DỰ ĐOÁN: Tên ngắn gọn của vật thể được vẽ (ví dụ: "Mèo", "Ngôi nhà", "Hoa") | |
| 3. MÔ TẢ: Viết mô tả ngắn gọn bằng tiếng Anh, mô tả nội dung sketch dùng cho AI vẽ lại. Bao gồm: | |
| - Miêu tả đối tượng | |
| - Phong cách: cute, line-art style, soft pastel colors, minimalist illustration | |
| - Tránh chi tiết phức tạp, ưu tiên phong cách đơn giản, đáng yêu | |
| Định dạng phản hồi: | |
| ĐIỂM: [6–10] | |
| DỰ ĐOÁN: [Tên đối tượng] | |
| MÔ TẢ: ["A cute [object] in line-art style, with soft pastel colors, minimalist illustration..."]`; | |
| const geminiApiKey = 'AIzaSyCinF1_Z7XXPzu_zvMzifKyPIy7i7eUBGc'; // dùng key bạn dán sẵn | |
| const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${geminiApiKey}`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| contents: [{ | |
| parts: [ | |
| { text: prompt }, | |
| { | |
| inline_data: { | |
| mime_type: "image/png", | |
| data: base64Data | |
| } | |
| } | |
| ] | |
| }] | |
| }) | |
| }); | |
| const data = await response.json(); | |
| const text = data.candidates[0].content.parts[0].text; | |
| return this.parseGeminiResponse(text); | |
| } | |
| parseGeminiResponse(text) { | |
| const score = parseInt(text.match(/ĐIỂM[:\s]*(\d+)/i)?.[1] || 7); | |
| const prediction = (text.match(/DỰ ĐOÁN[:\s]*(.*)/i)?.[1] || 'Không xác định').trim(); | |
| const description = (text.match(/MÔ TẢ[:\s]*([\s\S]*)/i)?.[1] || 'simple line drawing').trim(); | |
| return { score, prediction, description }; | |
| } | |
| // async generateImageFromDescription(description) { | |
| // const enhancedPrompt = `${description}`; | |
| // const seed = Math.floor(Math.random() * 1e6); | |
| // const engineId = 'stable-diffusion-xl-1024-v1-0'; | |
| // const url = `https://api.stability.ai/v1/generation/${engineId}/text-to-image`; | |
| // const STABILITY_API_KEY = 'sk-JzOAcde6bqHRjfInzdrNmNjjrCs0LFAvB838xi14OhhEQ06T '; // Dán key của bạn | |
| // const response = await fetch(url, { | |
| // method: 'POST', | |
| // headers: { | |
| // 'Content-Type': 'application/json', | |
| // 'Accept': 'application/json', | |
| // 'Authorization': `Bearer ${STABILITY_API_KEY}` | |
| // }, | |
| // body: JSON.stringify({ | |
| // text_prompts: [{ text: enhancedPrompt }], | |
| // cfg_scale: 7, | |
| // width: 1024, | |
| // height: 1024, | |
| // samples: 1, | |
| // steps: 30, | |
| // seed: seed | |
| // }) | |
| // }); | |
| // const data = await response.json(); | |
| // const base64 = data.artifacts[0].base64; | |
| // return `data:image/png;base64,${base64}`; | |
| // } | |
| async generateImageFromDescription(description) { | |
| // Using Pollinations AI (free image generation API) | |
| // This creates an image based on the description from Gemini | |
| const enhancedPrompt = `${description}, high quality, detailed artwork, colored pencil, cute illustration`; | |
| const encodedPrompt = encodeURIComponent(enhancedPrompt); | |
| // Generate a unique seed to avoid caching | |
| const seed = Math.floor(Math.random() * 1000000); | |
| const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?seed=${seed}&width=512&height=512&model=flux`; | |
| // Verify the image loads | |
| return new Promise((resolve, reject) => { | |
| const img = new Image(); | |
| img.onload = () => resolve(imageUrl); | |
| img.onerror = () => reject(new Error('Không thể tạo ảnh render')); | |
| img.src = imageUrl; | |
| }); | |
| } | |
| displayResults(imageUrl, score, prediction) { | |
| const imageContainer = document.getElementById('imageContainer'); | |
| imageContainer.innerHTML = `<img src="${imageUrl}" alt="Rendered Image" class="rendered-image">`; | |
| this.renderedImageUrl = imageUrl; | |
| document.getElementById('downloadBtn').style.display = 'block'; | |
| document.getElementById('scoreDisplay').textContent = `${score}/10`; | |
| document.getElementById('predictionText').textContent = prediction; | |
| document.getElementById('resultContent').style.display = 'block'; | |
| } | |
| downloadImage() { | |
| if (this.renderedImageUrl) { | |
| const link = document.createElement('a'); | |
| link.download = `sketch-render-${Date.now()}.png`; | |
| link.href = this.renderedImageUrl; | |
| link.click(); | |
| } | |
| } | |
| showLoading(show) { | |
| document.getElementById('loading').style.display = show ? 'block' : 'none'; | |
| document.getElementById('resultContent').style.display = show ? 'none' : 'block'; | |
| } | |
| showError(message) { | |
| const errorElement = document.getElementById('errorMessage'); | |
| errorElement.textContent = message; | |
| errorElement.style.display = 'block'; | |
| } | |
| hideError() { | |
| document.getElementById('errorMessage').style.display = 'none'; | |
| } | |
| } | |
| // Khởi tạo khi DOM sẵn sàng | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new SketchToRenderApp(); | |
| }); |