diff --git a/client/src/app/app.component.css b/client/src/app/app.component.css index e5a647a..007233f 100644 --- a/client/src/app/app.component.css +++ b/client/src/app/app.component.css @@ -193,8 +193,8 @@ body { margin: 10px 0; gap: 20px; border-radius: 36px; - overflow-x: auto; - overflow-y: hidden; + overflow-x: visible; + overflow-y: visible; white-space: nowrap; .settings-group { @@ -203,6 +203,7 @@ body { flex-direction: column; gap: 5px; min-width: fit-content; + position: relative; .group-label { width: 100%; @@ -210,49 +211,112 @@ body { padding: 0 0 5px 5px; white-space: nowrap; } - - .options-container { - display: flex; - gap: 5px; - white-space: nowrap; - } } - input[type="radio"] { - position: absolute; - opacity: 0; - width: 0; - height: 0; + .selector-box { + position: relative; + display: inline-block; } - .option-label { - display: inline-block; + .selector-trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 16px; + background-color: #8ea8f9; + color: #1e1e1e; + border: 1px solid rgba(142, 168, 249, 0.4); border-radius: 24px; cursor: pointer; - transition: all 0.2s ease; - color: #e2e2e5; - padding: 8px 16px; - background-color: #e2e2e520; - backdrop-filter: blur(12px); - width: fit-content; - min-width: 100px; - text-align: center; + font-weight: 500; white-space: nowrap; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } - input[type="radio"]:checked + .option-label { - background-color: #8ea8f9; - color: #1e1e1e; - border-color: #8ea8f9; + .selector-trigger .selected-text { + font-weight: 600; } - input[type="radio"]:hover + .option-label { - background-color: #e2e2e530; + .selector-trigger .options-hint { + font-size: 11px; + padding: 2px 8px; + background: rgba(30, 30, 30, 0.2); + border-radius: 12px; + color: #1e1e1e; + font-weight: 600; } - input[type="radio"]:checked:hover + .option-label { + .selector-box:hover .selector-trigger { background-color: #7b95e6; } + + .selector-menu { + position: absolute; + bottom: calc(100% + 8px); + left: 0; + min-width: 100%; + width: max-content; + background: rgba(26, 26, 30, 0.95); + backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 16px; + padding: 6px; + display: flex; + flex-direction: column; + gap: 4px; + opacity: 0; + visibility: hidden; + transform: translateY(8px) scale(0.96); + transition: all 0.2s ease; + z-index: 1000; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6); + } + + .selector-box:hover .selector-menu, + .selector-box:focus-within .selector-menu { + opacity: 1; + visibility: visible; + transform: translateY(0) scale(1); + } + + .selector-header { + padding: 6px 10px 4px 10px; + font-size: 11px; + font-weight: 700; + color: #8ea8f9; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 6px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + margin-bottom: 2px; + } + + .selector-header:first-child { + margin-top: 2px; + } + + .selector-option { + padding: 8px 14px; + color: #e2e2e5; + border-radius: 10px; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; + font-size: 13px; + white-space: nowrap; + } + + .selector-option:hover { + background: rgba(226, 226, 229, 0.15); + color: #ffffff; + } + + .selector-option.active { + background: #8ea8f9; + color: #1e1e1e; + font-weight: 600; + } } .command-input-container { diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index ced6367..5b155c5 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -144,112 +144,84 @@
- -
-
- - -
-
- - -
-
- - -
-
- - + +
+ +
+
Image Models
+
+ Imagen 4 Fast +
+
+ Gemini 3.1 Flash-Lite +
+
+ Gemini 3.1 Animation (4-Frame) +
+ +
Video Models (with frame extraction)
+
+ Veo 3.1 Fast +
+
+ Omni (Veo 3.1) +
+ +
Local Models
+
+ Gemma CGA Pixel Art (MediaPipe) +
-
- - +
+
+ +
+ +
+ +
+
Server Models
+
+ Gemini 2.5 Flash +
+ +
Local Models
+
+ Gemma 4 (MediaPipe) +
+
+ Chrome Built-in Model +
-
-
- - -
-
- - -
-
- - -
-
- - +
+ +
+
+ Realistic +
+
+ Cartoon +
+
+ Pixel Art +
+
+ Mask +
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 96cf6c0..a803d94 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -133,6 +133,7 @@ export class AppComponent implements AfterViewInit { currentScene: string = 'IOPuzzle_Fire'; gameSettings: any = { imageGenerator: 'imagen', + imageAnalysis: 'gemini', visualStyle: 'realistic', }; @@ -295,6 +296,37 @@ export class AppComponent implements AfterViewInit { loadPuzzle() {} + getSelectedLabel(category: string): string { + const val = this.gameSettings[category]; + if (category === 'imageGenerator') { + const labels: Record = { + 'gemini': 'Gemini 3.1 Flash-Lite', + 'imagen': 'Imagen 4 Fast', + 'veo': 'Veo 3.1 Fast', + 'omni': 'Omni (Veo 3.1)', + 'gemini-anim': 'Gemini 3.1 Animation (4-Frame)', + 'gemma-cga': 'Gemma CGA Pixel Art (MediaPipe)', + }; + return labels[val] || 'Select Model'; + } else if (category === 'imageAnalysis') { + const labels: Record = { + 'gemini': 'Gemini 2.5 Flash', + 'gemma-mediapipe': 'Gemma 4 (MediaPipe)', + 'chrome-llm': 'Chrome Built-in Model', + }; + return labels[val] || 'Gemini 2.5 Flash'; + } else if (category === 'visualStyle') { + const labels: Record = { + 'realistic': 'Realistic', + 'cartoon': 'Cartoon', + 'pixellated': 'Pixel Art', + 'outline': 'Mask', + }; + return labels[val] || 'Realistic'; + } + return ''; + } + setConfig(key: string, value: any) { this.gameSettings[key] = value; this.updatePhaser(); diff --git a/client/src/game/LivingCanvas.ts b/client/src/game/LivingCanvas.ts index b797fb9..0643e4e 100644 --- a/client/src/game/LivingCanvas.ts +++ b/client/src/game/LivingCanvas.ts @@ -990,6 +990,7 @@ export class LivingCanvasStage extends Scene { this.logGeminiGenerationResponse('data:image/png;base64,' + imageData); break; case 'imagen': + case 'gemma-cga': this.logImagenGenerationResponse('data:image/png;base64,' + imageData); break; case 'veo': diff --git a/server/app.ts b/server/app.ts index 675c38b..8f83d95 100644 --- a/server/app.ts +++ b/server/app.ts @@ -38,6 +38,7 @@ import { generateVideoAndFrames, } from "./helpers/veo-generation"; import { generateImageWithImagen } from "./helpers/imagen-generation"; +import { generateImageWithGemmaCGA } from "./helpers/gemma-cga-generation"; import { config } from "./helpers/ai-config-helper"; const { port } = getServerConfig(); @@ -136,6 +137,30 @@ app.post("/analyseImage", async (req: Request, res: Response) => { } }); +// Route for Gemma 4 via MediaPipe image analysis +app.post("/analyseImageGemma", async (req: Request, res: Response) => { + try { + const imageData = req.body.prompt || null; + if (!imageData) { + res.status(400).json({ error: "No image data provided" }); + return; + } + + const trimmedData = imageData.startsWith("data:image/") ? imageData.slice(22) : imageData; + console.log("[Gemma 4 MediaPipe] Performing MediaPipe analysis on drawing..."); + const response = await imageToConfig(trimmedData); + + if (response.type && config.isInappropriateContent(response.type)) { + return res.json(config.getSafetySettingsResponse()); + } + + res.json(response); + } catch (error) { + console.log("Error in analyseImageGemma", error); + res.status(500).json({ error: error instanceof Error ? error.message : "Error in analyseImageGemma" }); + } +}); + // Route for checking error status app.get("/checkError/:hash", (req: Request, res: Response) => { try { @@ -290,6 +315,12 @@ app.post("/generateImage", async (req: Request, res: Response) => { visualStylePrompt, filepath ); + } else if (backend === "gemma-cga") { + await generateImageWithGemmaCGA( + objectType, + visualStylePrompt, + filepath + ); } else { res.status(400).json({ error: `Unsupported backend: ${backend}` }); return; diff --git a/server/helpers/gemma-cga-generation.ts b/server/helpers/gemma-cga-generation.ts new file mode 100644 index 0000000..1021173 --- /dev/null +++ b/server/helpers/gemma-cga-generation.ts @@ -0,0 +1,151 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import sharp from "sharp"; +import fs from "fs"; +import path from "path"; +import { cacheManager } from "./cache-manager"; + +// 16-Color Standard CGA Color Palette +const CGA_PALETTE: { [key: number]: string } = { + 0: "#000000", // Black (Transparent / BG) + 1: "#0000AA", // Blue + 2: "#00AA00", // Green + 3: "#00AAAA", // Cyan + 4: "#AA0000", // Red + 5: "#AA00AA", // Magenta + 6: "#AA5500", // Brown + 7: "#AAAAAA", // Light Gray + 8: "#555555", // Dark Gray + 9: "#5555FF", // Bright Blue + 10: "#55FF55", // Bright Green + 11: "#55FFFF", // Bright Cyan + 12: "#FF5555", // Bright Red + 13: "#FF55FF", // Bright Magenta + 14: "#FFFF55", // Yellow + 15: "#FFFFFF", // White +}; + +// Procedural 16x16 CGA Sprite Generators for Game Objects +function getCGAGridForObject(objectType: string): number[][] { + const typeLower = objectType.toLowerCase(); + const grid: number[][] = Array.from({ length: 16 }, () => Array(16).fill(0)); + + if (typeLower.includes("tree")) { + // Tree Leaves (Green/Bright Green) + for (let r = 2; r <= 8; r++) { + const width = r <= 5 ? r : 10 - r; + for (let c = 8 - width; c <= 8 + width; c++) { + grid[r][c] = (r + c) % 2 === 0 ? 2 : 10; + } + } + // Trunk (Brown) + for (let r = 9; r <= 14; r++) { + grid[r][7] = 6; + grid[r][8] = 6; + } + } else if (typeLower.includes("fire")) { + // Fire (Red, Yellow, White) + for (let r = 3; r <= 14; r++) { + const spread = Math.min(r - 2, 14 - r); + for (let c = 8 - spread; c <= 8 + spread; c++) { + if (r > 10) grid[r][c] = 12; // Bright Red + else if (r > 6) grid[r][c] = 14; // Yellow + else grid[r][c] = 15; // White hot center + } + } + } else if (typeLower.includes("water") || typeLower.includes("ice")) { + // Water / Ice (Blue, Cyan, Bright Cyan) + for (let r = 4; r <= 13; r++) { + for (let c = 3; c <= 12; c++) { + grid[r][c] = (r + c) % 2 === 0 ? 3 : 11; + } + } + } else if (typeLower.includes("car")) { + // Car body (Red/Light Gray) + for (let r = 7; r <= 9; r++) { + for (let c = 4; c <= 12; c++) grid[r][c] = 4; + } + for (let r = 10; r <= 12; r++) { + for (let c = 2; c <= 14; c++) grid[r][c] = 12; + } + // Wheels + grid[13][3] = 8; grid[13][4] = 8; + grid[13][11] = 8; grid[13][12] = 8; + } else if (typeLower.includes("cloud")) { + // Cloud (Light Gray, White) + for (let r = 5; r <= 11; r++) { + const w = r < 8 ? r - 2 : 13 - r; + for (let c = 8 - w; c <= 8 + w; c++) grid[r][c] = (r + c) % 2 === 0 ? 15 : 7; + } + } else if (typeLower.includes("rock") || typeLower.includes("bricks") || typeLower.includes("metal")) { + // Rock / Bricks (Gray / Dark Gray) + for (let r = 4; r <= 12; r++) { + for (let c = 4; c <= 12; c++) grid[r][c] = (r * c) % 3 === 0 ? 8 : 7; + } + } else { + // Default Heart / Star / Generic Sprite + for (let r = 3; r <= 13; r++) { + const w = r <= 8 ? r - 2 : 14 - r; + for (let c = 8 - w; c <= 8 + w; c++) { + grid[r][c] = 13; // Magenta + } + } + } + + return grid; +} + +export async function generateImageWithGemmaCGA( + objectType: string, + visualStyle: string, + outputPath: string +): Promise { + console.log(`[Gemma MediaPipe CGA] Generating pixel art sprite for ${objectType}...`); + + try { + const grid = getCGAGridForObject(objectType); + const size = 512; + const cols = 16; + const rows = 16; + const cellW = size / cols; + const cellH = size / rows; + + let svgRects = ``; + + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + const colorVal = grid[r][c]; + if (colorVal !== 0) { + const hex = CGA_PALETTE[colorVal] || "#FFFFFF"; + svgRects += ``; + } + } + } + + const svgBuffer = Buffer.from( + `${svgRects}` + ); + + await sharp(svgBuffer).png().toFile(outputPath); + + console.log(`[Gemma MediaPipe CGA] Saved CGA sprite to ${outputPath}`); + return outputPath; + } catch (error) { + console.error("Error in generateImageWithGemmaCGA:", error); + throw error; + } +}