Spaces:
Running
Running
Upload 4 files
Browse files- README.md +0 -10
- requirements.txt +1 -0
- script.js +329 -0
- style.css +349 -28
README.md
CHANGED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Sketch2Render
|
| 3 |
-
emoji: 🐠
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: gray
|
| 6 |
-
sdk: static
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
No Packages Require
|
script.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class SketchToRenderApp {
|
| 2 |
+
constructor() {
|
| 3 |
+
this.canvas = document.getElementById('drawingCanvas');
|
| 4 |
+
this.ctx = this.canvas.getContext('2d');
|
| 5 |
+
this.isDrawing = false;
|
| 6 |
+
this.drawingHistory = [];
|
| 7 |
+
this.currentPath = [];
|
| 8 |
+
this.renderedImageUrl = null;
|
| 9 |
+
|
| 10 |
+
// API Key được nhúng trực tiếp trong code
|
| 11 |
+
this.apiKey = 'AIzaSyCinF1_Z7XXPzu_zvMzifKyPIy7i7eUBGc'; // Thay thế bằng API key thực tế
|
| 12 |
+
|
| 13 |
+
this.initializeCanvas();
|
| 14 |
+
this.setupEventListeners();
|
| 15 |
+
this.saveCanvasState();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
initializeCanvas() {
|
| 19 |
+
this.ctx.lineCap = 'round';
|
| 20 |
+
this.ctx.lineJoin = 'round';
|
| 21 |
+
this.ctx.fillStyle = 'white';
|
| 22 |
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
setupEventListeners() {
|
| 26 |
+
// Canvas drawing events
|
| 27 |
+
this.canvas.addEventListener('mousedown', (e) => this.startDrawing(e));
|
| 28 |
+
this.canvas.addEventListener('mousemove', (e) => this.draw(e));
|
| 29 |
+
this.canvas.addEventListener('mouseup', () => this.stopDrawing());
|
| 30 |
+
this.canvas.addEventListener('mouseout', () => this.stopDrawing());
|
| 31 |
+
|
| 32 |
+
// Touch events for mobile
|
| 33 |
+
this.canvas.addEventListener('touchstart', (e) => {
|
| 34 |
+
e.preventDefault();
|
| 35 |
+
const touch = e.touches[0];
|
| 36 |
+
const mouseEvent = new MouseEvent('mousedown', {
|
| 37 |
+
clientX: touch.clientX,
|
| 38 |
+
clientY: touch.clientY
|
| 39 |
+
});
|
| 40 |
+
this.canvas.dispatchEvent(mouseEvent);
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
this.canvas.addEventListener('touchmove', (e) => {
|
| 44 |
+
e.preventDefault();
|
| 45 |
+
const touch = e.touches[0];
|
| 46 |
+
const mouseEvent = new MouseEvent('mousemove', {
|
| 47 |
+
clientX: touch.clientX,
|
| 48 |
+
clientY: touch.clientY
|
| 49 |
+
});
|
| 50 |
+
this.canvas.dispatchEvent(mouseEvent);
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
this.canvas.addEventListener('touchend', (e) => {
|
| 54 |
+
e.preventDefault();
|
| 55 |
+
const mouseEvent = new MouseEvent('mouseup', {});
|
| 56 |
+
this.canvas.dispatchEvent(mouseEvent);
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
// Control events
|
| 60 |
+
document.getElementById('brushSize').addEventListener('input', (e) => {
|
| 61 |
+
document.getElementById('brushSizeValue').textContent = e.target.value + 'px';
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
document.getElementById('clearCanvas').addEventListener('click', () => {
|
| 65 |
+
this.clearCanvas();
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
document.getElementById('undoBtn').addEventListener('click', () => {
|
| 69 |
+
this.undo();
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
document.getElementById('generateBtn').addEventListener('click', () => {
|
| 73 |
+
this.generateRender();
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
document.getElementById('downloadBtn').addEventListener('click', () => {
|
| 77 |
+
this.downloadImage();
|
| 78 |
+
});
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
getMousePos(e) {
|
| 82 |
+
const rect = this.canvas.getBoundingClientRect();
|
| 83 |
+
return {
|
| 84 |
+
x: e.clientX - rect.left,
|
| 85 |
+
y: e.clientY - rect.top
|
| 86 |
+
};
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
startDrawing(e) {
|
| 90 |
+
this.isDrawing = true;
|
| 91 |
+
const pos = this.getMousePos(e);
|
| 92 |
+
this.currentPath = [pos];
|
| 93 |
+
|
| 94 |
+
this.ctx.strokeStyle = document.getElementById('brushColor').value;
|
| 95 |
+
this.ctx.lineWidth = document.getElementById('brushSize').value;
|
| 96 |
+
|
| 97 |
+
this.ctx.beginPath();
|
| 98 |
+
this.ctx.moveTo(pos.x, pos.y);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
draw(e) {
|
| 102 |
+
if (!this.isDrawing) return;
|
| 103 |
+
|
| 104 |
+
const pos = this.getMousePos(e);
|
| 105 |
+
this.currentPath.push(pos);
|
| 106 |
+
|
| 107 |
+
this.ctx.lineTo(pos.x, pos.y);
|
| 108 |
+
this.ctx.stroke();
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
stopDrawing() {
|
| 112 |
+
if (this.isDrawing) {
|
| 113 |
+
this.isDrawing = false;
|
| 114 |
+
this.saveCanvasState();
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
saveCanvasState() {
|
| 119 |
+
this.drawingHistory.push(this.canvas.toDataURL());
|
| 120 |
+
if (this.drawingHistory.length > 20) {
|
| 121 |
+
this.drawingHistory.shift();
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
clearCanvas() {
|
| 126 |
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
| 127 |
+
this.ctx.fillStyle = 'white';
|
| 128 |
+
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
| 129 |
+
this.saveCanvasState();
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
undo() {
|
| 133 |
+
if (this.drawingHistory.length > 1) {
|
| 134 |
+
this.drawingHistory.pop();
|
| 135 |
+
const previousState = this.drawingHistory[this.drawingHistory.length - 1];
|
| 136 |
+
const img = new Image();
|
| 137 |
+
img.onload = () => {
|
| 138 |
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
| 139 |
+
this.ctx.drawImage(img, 0, 0);
|
| 140 |
+
};
|
| 141 |
+
img.src = previousState;
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
async generateRender() {
|
| 146 |
+
if (!this.apiKey || this.apiKey === 'YOUR_GEMINI_API_KEY_HERE') {
|
| 147 |
+
this.showError('Vui lòng cấu hình API Key trong source code');
|
| 148 |
+
return;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Check if canvas is empty
|
| 152 |
+
if (this.isCanvasEmpty()) {
|
| 153 |
+
this.showError('Vui lòng vẽ gì đó trước khi tạo render');
|
| 154 |
+
return;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
this.showLoading(true);
|
| 158 |
+
this.hideError();
|
| 159 |
+
|
| 160 |
+
try {
|
| 161 |
+
// Step 1: Analyze sketch with Gemini
|
| 162 |
+
const analysisResult = await this.analyzeSketchWithGemini();
|
| 163 |
+
console.log('Analysis result:', analysisResult);
|
| 164 |
+
|
| 165 |
+
// Step 2: Generate image using the description from Gemini
|
| 166 |
+
const imageUrl = await this.generateImageFromDescription(analysisResult.description);
|
| 167 |
+
|
| 168 |
+
// Step 3: Display results
|
| 169 |
+
this.displayResults(imageUrl, analysisResult.score, analysisResult.prediction);
|
| 170 |
+
|
| 171 |
+
} catch (error) {
|
| 172 |
+
console.error('Error:', error);
|
| 173 |
+
this.showError(`Lỗi: ${error.message}`);
|
| 174 |
+
} finally {
|
| 175 |
+
this.showLoading(false);
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
isCanvasEmpty() {
|
| 180 |
+
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
| 181 |
+
return imageData.data.every((pixel, index) => {
|
| 182 |
+
if ((index + 1) % 4 === 0) return true; // Alpha channel
|
| 183 |
+
return pixel === 255; // White pixels
|
| 184 |
+
});
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
async analyzeSketchWithGemini() {
|
| 188 |
+
const canvasDataURL = this.canvas.toDataURL('image/png');
|
| 189 |
+
const base64Data = canvasDataURL.split(',')[1];
|
| 190 |
+
|
| 191 |
+
const prompt = `Phân tích bức vẽ sketch này và cung cấp:
|
| 192 |
+
|
| 193 |
+
1. ĐIỂM SỐ: Đánh giá chất lượng và độ rõ ràng từ 1–10, với tiêu chuẩn thoải mái (hình được vẽ bởi người bình thường bằng bút đen trên canvas, không yêu cầu đường nét hoàn hảo).
|
| 194 |
+
2. DỰ ĐOÁN: Tên ngắn gọn của sự vật được vẽ (VD: "Mèo", "Ngôi nhà", "Hoa")
|
| 195 |
+
3. MÔ TẢ: Mô tả ngắn gọn để tạo ảnh render (viết bằng tiếng Anh, phong cách artistic)
|
| 196 |
+
|
| 197 |
+
Định dạng trả lời:
|
| 198 |
+
ĐIỂM: [số]
|
| 199 |
+
DỰ ĐOÁN: [tên sự vật]
|
| 200 |
+
MÔ TẢ: [mô tả ngắn gọn bằng tiếng Anh]`;
|
| 201 |
+
|
| 202 |
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${this.apiKey}`, {
|
| 203 |
+
method: 'POST',
|
| 204 |
+
headers: {
|
| 205 |
+
'Content-Type': 'application/json',
|
| 206 |
+
},
|
| 207 |
+
body: JSON.stringify({
|
| 208 |
+
contents: [{
|
| 209 |
+
parts: [
|
| 210 |
+
{ text: prompt },
|
| 211 |
+
{
|
| 212 |
+
inline_data: {
|
| 213 |
+
mime_type: "image/png",
|
| 214 |
+
data: base64Data
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
]
|
| 218 |
+
}]
|
| 219 |
+
})
|
| 220 |
+
});
|
| 221 |
+
|
| 222 |
+
if (!response.ok) {
|
| 223 |
+
const errorData = await response.json();
|
| 224 |
+
throw new Error(`Gemini API Error: ${errorData.error?.message || 'Unknown error'}`);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
const data = await response.json();
|
| 228 |
+
|
| 229 |
+
if (data.candidates && data.candidates.length > 0) {
|
| 230 |
+
const result = data.candidates[0].content.parts[0].text;
|
| 231 |
+
return this.parseGeminiResponse(result);
|
| 232 |
+
} else {
|
| 233 |
+
throw new Error('Không có kết quả từ Gemini API');
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
parseGeminiResponse(text) {
|
| 238 |
+
console.log('Gemini response:', text);
|
| 239 |
+
|
| 240 |
+
// Extract score
|
| 241 |
+
const scoreMatch = text.match(/ĐIỂM[:\s]*(\d+)/i) || text.match(/score[:\s]*(\d+)/i);
|
| 242 |
+
const score = scoreMatch ? parseInt(scoreMatch[1]) : 7;
|
| 243 |
+
|
| 244 |
+
// Extract prediction
|
| 245 |
+
const predictionMatch = text.match(/DỰ ĐOÁN[:\s]*([^\n]+)/i) || text.match(/prediction[:\s]*([^\n]+)/i);
|
| 246 |
+
const prediction = predictionMatch ? predictionMatch[1].trim() : 'Không xác định';
|
| 247 |
+
|
| 248 |
+
// Extract description
|
| 249 |
+
const descriptionMatch = text.match(/MÔ TẢ[:\s]*([^]*)/i) || text.match(/description[:\s]*([^]*)/i);
|
| 250 |
+
let description = descriptionMatch ? descriptionMatch[1].trim() : text;
|
| 251 |
+
|
| 252 |
+
// If no specific description found, use the whole response
|
| 253 |
+
if (!descriptionMatch) {
|
| 254 |
+
// Try to extract the most descriptive part
|
| 255 |
+
const lines = text.split('\n').filter(line => line.trim().length > 20);
|
| 256 |
+
description = lines.length > 0 ? lines[lines.length - 1] : text;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
return {
|
| 260 |
+
score: Math.min(Math.max(score, 1), 10), // Ensure score is between 1-10
|
| 261 |
+
prediction: prediction.replace(/^["']|["']$/g, ''), // Remove quotes
|
| 262 |
+
description: description.replace(/^["']|["']$/g, '') // Remove quotes
|
| 263 |
+
};
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
async generateImageFromDescription(description) {
|
| 267 |
+
// Using Pollinations AI (free image generation API)
|
| 268 |
+
// This creates an image based on the description from Gemini
|
| 269 |
+
const enhancedPrompt = `${description}, high quality, detailed artwork, professional illustration`;
|
| 270 |
+
const encodedPrompt = encodeURIComponent(enhancedPrompt);
|
| 271 |
+
|
| 272 |
+
// Generate a unique seed to avoid caching
|
| 273 |
+
const seed = Math.floor(Math.random() * 1000000);
|
| 274 |
+
const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?seed=${seed}&width=512&height=512&model=flux`;
|
| 275 |
+
|
| 276 |
+
// Verify the image loads
|
| 277 |
+
return new Promise((resolve, reject) => {
|
| 278 |
+
const img = new Image();
|
| 279 |
+
img.onload = () => resolve(imageUrl);
|
| 280 |
+
img.onerror = () => reject(new Error('Không thể tạo ảnh render'));
|
| 281 |
+
img.src = imageUrl;
|
| 282 |
+
});
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
displayResults(imageUrl, score, prediction) {
|
| 286 |
+
// Display rendered image
|
| 287 |
+
const imageContainer = document.getElementById('imageContainer');
|
| 288 |
+
imageContainer.innerHTML = `<img src="${imageUrl}" alt="Rendered Image" class="rendered-image">`;
|
| 289 |
+
|
| 290 |
+
this.renderedImageUrl = imageUrl;
|
| 291 |
+
document.getElementById('downloadBtn').style.display = 'block';
|
| 292 |
+
|
| 293 |
+
// Display results
|
| 294 |
+
document.getElementById('scoreDisplay').textContent = `${score}/10`;
|
| 295 |
+
document.getElementById('predictionText').textContent = prediction;
|
| 296 |
+
|
| 297 |
+
// Show results
|
| 298 |
+
document.getElementById('resultContent').style.display = 'block';
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
downloadImage() {
|
| 302 |
+
if (this.renderedImageUrl) {
|
| 303 |
+
const link = document.createElement('a');
|
| 304 |
+
link.download = `sketch-render-${Date.now()}.png`;
|
| 305 |
+
link.href = this.renderedImageUrl;
|
| 306 |
+
link.click();
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
showLoading(show) {
|
| 311 |
+
document.getElementById('loading').style.display = show ? 'block' : 'none';
|
| 312 |
+
document.getElementById('resultContent').style.display = show ? 'none' : 'block';
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
showError(message) {
|
| 316 |
+
const errorElement = document.getElementById('errorMessage');
|
| 317 |
+
errorElement.textContent = message;
|
| 318 |
+
errorElement.style.display = 'block';
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
hideError() {
|
| 322 |
+
document.getElementById('errorMessage').style.display = 'none';
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
// Initialize the app when the page loads
|
| 327 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 328 |
+
new SketchToRenderApp();
|
| 329 |
+
});
|
style.css
CHANGED
|
@@ -1,28 +1,349 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
margin: 0;
|
| 3 |
+
padding: 0;
|
| 4 |
+
box-sizing: border-box;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
body {
|
| 8 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 9 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 10 |
+
min-height: 100vh;
|
| 11 |
+
padding: 20px;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.container {
|
| 15 |
+
max-width: 1600px;
|
| 16 |
+
margin: 0 auto;
|
| 17 |
+
background: white;
|
| 18 |
+
border-radius: 20px;
|
| 19 |
+
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
|
| 20 |
+
overflow: hidden;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.header {
|
| 24 |
+
background: linear-gradient(135deg, #4285f4 0%, #34a853 50%, #fbbc05 75%, #ea4335 100%);
|
| 25 |
+
color: white;
|
| 26 |
+
padding: 30px;
|
| 27 |
+
text-align: center;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.header h1 {
|
| 31 |
+
font-size: 2.5rem;
|
| 32 |
+
margin-bottom: 10px;
|
| 33 |
+
font-weight: 700;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.header p {
|
| 37 |
+
font-size: 1.1rem;
|
| 38 |
+
opacity: 0.9;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.main-content {
|
| 42 |
+
display: grid;
|
| 43 |
+
grid-template-columns: 500px 1fr;
|
| 44 |
+
gap: 30px;
|
| 45 |
+
padding: 30px;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.drawing-section {
|
| 49 |
+
display: flex;
|
| 50 |
+
flex-direction: column;
|
| 51 |
+
gap: 20px;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.canvas-container {
|
| 55 |
+
position: relative;
|
| 56 |
+
border: 3px solid #e0e0e0;
|
| 57 |
+
border-radius: 15px;
|
| 58 |
+
overflow: hidden;
|
| 59 |
+
background: #fafafa;
|
| 60 |
+
box-shadow: inset 0 2px 10px rgba(0,0,0,0.05);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#drawingCanvas {
|
| 64 |
+
display: block;
|
| 65 |
+
cursor: crosshair;
|
| 66 |
+
background: white;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.controls {
|
| 70 |
+
display: flex;
|
| 71 |
+
gap: 15px;
|
| 72 |
+
flex-wrap: wrap;
|
| 73 |
+
align-items: center;
|
| 74 |
+
padding: 20px;
|
| 75 |
+
background: #f8f9fa;
|
| 76 |
+
border-radius: 15px;
|
| 77 |
+
border: 1px solid #e9ecef;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.control-group {
|
| 81 |
+
display: flex;
|
| 82 |
+
align-items: center;
|
| 83 |
+
gap: 10px;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.control-group label {
|
| 87 |
+
font-weight: 600;
|
| 88 |
+
color: #495057;
|
| 89 |
+
font-size: 0.9rem;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
input[type="range"] {
|
| 93 |
+
width: 100px;
|
| 94 |
+
accent-color: #4285f4;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
input[type="color"] {
|
| 98 |
+
width: 40px;
|
| 99 |
+
height: 40px;
|
| 100 |
+
border: none;
|
| 101 |
+
border-radius: 8px;
|
| 102 |
+
cursor: pointer;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.btn {
|
| 106 |
+
padding: 12px 24px;
|
| 107 |
+
border: none;
|
| 108 |
+
border-radius: 10px;
|
| 109 |
+
font-weight: 600;
|
| 110 |
+
cursor: pointer;
|
| 111 |
+
transition: all 0.3s ease;
|
| 112 |
+
font-size: 0.9rem;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.btn-primary {
|
| 116 |
+
background: linear-gradient(135deg, #4285f4 0%, #34a853 100%);
|
| 117 |
+
color: white;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.btn-primary:hover {
|
| 121 |
+
transform: translateY(-2px);
|
| 122 |
+
box-shadow: 0 8px 25px rgba(66, 133, 244, 0.4);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.btn-secondary {
|
| 126 |
+
background: #6c757d;
|
| 127 |
+
color: white;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.btn-secondary:hover {
|
| 131 |
+
background: #5a6268;
|
| 132 |
+
transform: translateY(-2px);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.results-section {
|
| 136 |
+
display: flex;
|
| 137 |
+
flex-direction: column;
|
| 138 |
+
gap: 20px;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.generate-section {
|
| 142 |
+
background: #f8f9fa;
|
| 143 |
+
padding: 20px;
|
| 144 |
+
border-radius: 15px;
|
| 145 |
+
border: 1px solid #e9ecef;
|
| 146 |
+
text-align: center;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.generate-section h3 {
|
| 150 |
+
color: #495057;
|
| 151 |
+
margin-bottom: 15px;
|
| 152 |
+
font-size: 1.2rem;
|
| 153 |
+
display: flex;
|
| 154 |
+
align-items: center;
|
| 155 |
+
justify-content: center;
|
| 156 |
+
gap: 10px;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.gemini-logo {
|
| 160 |
+
font-size: 1.5rem;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.render-results {
|
| 164 |
+
background: #f8f9fa;
|
| 165 |
+
padding: 20px;
|
| 166 |
+
border-radius: 15px;
|
| 167 |
+
border: 1px solid #e9ecef;
|
| 168 |
+
min-height: 600px;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.render-results h3 {
|
| 172 |
+
color: #495057;
|
| 173 |
+
margin-bottom: 20px;
|
| 174 |
+
font-size: 1.2rem;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.loading {
|
| 178 |
+
display: none;
|
| 179 |
+
text-align: center;
|
| 180 |
+
padding: 40px;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.loading-spinner {
|
| 184 |
+
width: 50px;
|
| 185 |
+
height: 50px;
|
| 186 |
+
border: 4px solid #e9ecef;
|
| 187 |
+
border-top: 4px solid #4285f4;
|
| 188 |
+
border-radius: 50%;
|
| 189 |
+
animation: spin 1s linear infinite;
|
| 190 |
+
margin: 0 auto 20px;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
@keyframes spin {
|
| 194 |
+
0% { transform: rotate(0deg); }
|
| 195 |
+
100% { transform: rotate(360deg); }
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.result-content {
|
| 199 |
+
display: none;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.result-grid {
|
| 203 |
+
display: grid;
|
| 204 |
+
grid-template-columns: 1fr 300px;
|
| 205 |
+
gap: 20px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.rendered-image-container {
|
| 209 |
+
background: white;
|
| 210 |
+
padding: 20px;
|
| 211 |
+
border-radius: 15px;
|
| 212 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| 213 |
+
text-align: center;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.rendered-image-container h4 {
|
| 217 |
+
color: #495057;
|
| 218 |
+
margin-bottom: 15px;
|
| 219 |
+
font-size: 1.1rem;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.rendered-image {
|
| 223 |
+
max-width: 100%;
|
| 224 |
+
max-height: 400px;
|
| 225 |
+
border-radius: 10px;
|
| 226 |
+
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
| 227 |
+
margin-bottom: 15px;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.image-placeholder {
|
| 231 |
+
width: 100%;
|
| 232 |
+
height: 300px;
|
| 233 |
+
background: #f8f9fa;
|
| 234 |
+
border: 2px dashed #dee2e6;
|
| 235 |
+
border-radius: 10px;
|
| 236 |
+
display: flex;
|
| 237 |
+
align-items: center;
|
| 238 |
+
justify-content: center;
|
| 239 |
+
color: #6c757d;
|
| 240 |
+
font-size: 1.1rem;
|
| 241 |
+
text-align: center;
|
| 242 |
+
padding: 20px;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.result-info {
|
| 246 |
+
display: flex;
|
| 247 |
+
flex-direction: column;
|
| 248 |
+
gap: 15px;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.info-card {
|
| 252 |
+
background: white;
|
| 253 |
+
padding: 20px;
|
| 254 |
+
border-radius: 15px;
|
| 255 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.info-card h4 {
|
| 259 |
+
color: #495057;
|
| 260 |
+
margin-bottom: 10px;
|
| 261 |
+
font-size: 1.1rem;
|
| 262 |
+
display: flex;
|
| 263 |
+
align-items: center;
|
| 264 |
+
gap: 8px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.score-display {
|
| 268 |
+
font-size: 2.5rem;
|
| 269 |
+
font-weight: bold;
|
| 270 |
+
color: #4285f4;
|
| 271 |
+
text-align: center;
|
| 272 |
+
padding: 20px;
|
| 273 |
+
background: linear-gradient(135deg, rgba(66, 133, 244, 0.1) 0%, rgba(52, 168, 83, 0.1) 100%);
|
| 274 |
+
border-radius: 10px;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.prediction-text {
|
| 278 |
+
font-size: 1.2rem;
|
| 279 |
+
color: #495057;
|
| 280 |
+
line-height: 1.6;
|
| 281 |
+
background: #f8f9fa;
|
| 282 |
+
padding: 15px;
|
| 283 |
+
border-radius: 8px;
|
| 284 |
+
border-left: 4px solid #4285f4;
|
| 285 |
+
text-align: center;
|
| 286 |
+
font-weight: 600;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.error-message {
|
| 290 |
+
color: #d93025;
|
| 291 |
+
background: #fce8e6;
|
| 292 |
+
padding: 15px;
|
| 293 |
+
border-radius: 8px;
|
| 294 |
+
border: 1px solid #f5c6cb;
|
| 295 |
+
margin: 10px 0;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.canvas-size-info {
|
| 299 |
+
font-size: 0.8rem;
|
| 300 |
+
color: #6c757d;
|
| 301 |
+
margin-top: 5px;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.download-btn {
|
| 305 |
+
background: #34a853;
|
| 306 |
+
color: white;
|
| 307 |
+
padding: 10px 20px;
|
| 308 |
+
border: none;
|
| 309 |
+
border-radius: 8px;
|
| 310 |
+
font-weight: 600;
|
| 311 |
+
cursor: pointer;
|
| 312 |
+
margin-top: 10px;
|
| 313 |
+
transition: all 0.3s ease;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.download-btn:hover {
|
| 317 |
+
background: #137333;
|
| 318 |
+
transform: translateY(-2px);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
@media (max-width: 1200px) {
|
| 322 |
+
.main-content {
|
| 323 |
+
grid-template-columns: 1fr;
|
| 324 |
+
gap: 20px;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.result-grid {
|
| 328 |
+
grid-template-columns: 1fr;
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
@media (max-width: 768px) {
|
| 333 |
+
.main-content {
|
| 334 |
+
padding: 20px;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.header h1 {
|
| 338 |
+
font-size: 2rem;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.controls {
|
| 342 |
+
flex-direction: column;
|
| 343 |
+
align-items: stretch;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.control-group {
|
| 347 |
+
justify-content: space-between;
|
| 348 |
+
}
|
| 349 |
+
}
|