From 458751c3634c91584fdc4812b95a791e7c640f79 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 12 Apr 2026 11:32:15 +0700 Subject: [PATCH] fix: NEXT panel clipping, add labels to previews, improve fruit colors Derive canvas width from container + panel dimensions so the NEXT panel fits with consistent padding. Extract drawFruitCircle helper so all fruit rendering (in-game, cursor preview, NEXT panel) shows the name label consistently. Update color palette for maximum visual distinction between similar fruits (cherry/strawberry/apple, dekopon/persimmon). Center score text over the container instead of the canvas. --- src/constants.js | 18 ++++++++++---- src/fruits.js | 24 +++++++++--------- src/renderer.js | 64 ++++++++++++++++++++---------------------------- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/constants.js b/src/constants.js index 073dc0c..4f960af 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,13 +1,21 @@ // Game dimensions -export const CANVAS_WIDTH = 500; -export const CANVAS_HEIGHT = 700; +export const WALL_THICKNESS = 10; export const CONTAINER_WIDTH = 400; export const CONTAINER_HEIGHT = 600; -export const CONTAINER_X = (CANVAS_WIDTH - CONTAINER_WIDTH) / 2; -export const CONTAINER_Y = CANVAS_HEIGHT - CONTAINER_HEIGHT - 20; -export const WALL_THICKNESS = 10; +// NEXT fruit panel sits to the right of the container +export const PANEL_WIDTH = 60; +export const PANEL_GAP = 12; + +// Canvas sized to fit: padding + container + walls + gap + panel + padding +const PADDING = 20; +export const CANVAS_WIDTH = PADDING + WALL_THICKNESS + CONTAINER_WIDTH + WALL_THICKNESS + PANEL_GAP + PANEL_WIDTH + PADDING; +export const CANVAS_HEIGHT = 700; + +// Container is positioned so the panel fits to its right +export const CONTAINER_X = PADDING + WALL_THICKNESS; +export const CONTAINER_Y = CANVAS_HEIGHT - CONTAINER_HEIGHT - 20; // Danger line: ~50px below the top of the container walls export const DANGER_LINE_Y = CONTAINER_Y + 50; diff --git a/src/fruits.js b/src/fruits.js index b67e9b8..37cd4c6 100644 --- a/src/fruits.js +++ b/src/fruits.js @@ -1,18 +1,20 @@ // Community-standard fruit stats (TomboFry/moonfloof clones, Suika Game Wiki). // Radii scaled from the standard (24..192) by 0.52 to fit our 400px container. // Points match the Nintendo Switch scoring: 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66. +// Colors chosen for maximum visual distinction between similar fruits: +// Cherry/Strawberry/Apple use crimson/pink/vivid-red; Dekopon/Persimmon use orange/red-orange. export const FRUITS = [ - { tier: 0, name: 'Cherry', radius: 12, color: '#E8373B', points: 1 }, - { tier: 1, name: 'Strawberry', radius: 17, color: '#FF6B6B', points: 3 }, - { tier: 2, name: 'Grape', radius: 21, color: '#7B52AB', points: 6 }, - { tier: 3, name: 'Dekopon', radius: 29, color: '#FF8C00', points: 10 }, - { tier: 4, name: 'Persimmon', radius: 33, color: '#E2611C', points: 15 }, - { tier: 5, name: 'Apple', radius: 37, color: '#C0392B', points: 21 }, - { tier: 6, name: 'Pear', radius: 44, color: '#C8D44E', points: 28 }, - { tier: 7, name: 'Peach', radius: 50, color: '#FFBF8C', points: 36 }, - { tier: 8, name: 'Pineapple', radius: 67, color: '#F5C518', points: 45 }, - { tier: 9, name: 'Melon', radius: 83, color: '#90C040', points: 55 }, - { tier: 10, name: 'Watermelon', radius: 100, color: '#3A7D44', points: 66 }, + { tier: 0, name: 'Cherry', radius: 12, color: '#CC0022', points: 1 }, + { tier: 1, name: 'Strawberry', radius: 17, color: '#FF3B5C', points: 3 }, + { tier: 2, name: 'Grape', radius: 21, color: '#7B2D8B', points: 6 }, + { tier: 3, name: 'Dekopon', radius: 29, color: '#FF7A00', points: 10 }, + { tier: 4, name: 'Persimmon', radius: 33, color: '#E84A00', points: 15 }, + { tier: 5, name: 'Apple', radius: 37, color: '#E8003D', points: 21 }, + { tier: 6, name: 'Pear', radius: 44, color: '#C8D400', points: 28 }, + { tier: 7, name: 'Peach', radius: 50, color: '#FFBF80', points: 36 }, + { tier: 8, name: 'Pineapple', radius: 67, color: '#FFD700', points: 45 }, + { tier: 9, name: 'Melon', radius: 83, color: '#5CB85C', points: 55 }, + { tier: 10, name: 'Watermelon', radius: 100, color: '#1A7A2E', points: 66 }, ]; // Only the 5 smallest fruits can be randomly selected for dropping diff --git a/src/renderer.js b/src/renderer.js index 72a4749..4f63541 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -8,6 +8,8 @@ import { CONTAINER_HEIGHT, WALL_THICKNESS, DANGER_LINE_Y, + PANEL_WIDTH, + PANEL_GAP, } from './constants.js'; export function createCanvas() { @@ -81,30 +83,30 @@ function drawDangerLine(ctx) { ctx.restore(); } +function drawFruitCircle(ctx, fruit, x, y, radius) { + const r = radius ?? fruit.radius; + + ctx.beginPath(); + ctx.arc(x, y, r, 0, Math.PI * 2); + ctx.fillStyle = fruit.color; + ctx.fill(); + + ctx.strokeStyle = darkenColor(fruit.color, 0.2); + ctx.lineWidth = 2; + ctx.stroke(); + + ctx.fillStyle = '#FFFFFF'; + ctx.font = `bold ${Math.max(10, r * 0.6)}px sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(fruit.name.slice(0, 2), x, y); +} + function drawFruits(ctx, bodies) { for (const body of bodies) { if (body.fruitTier === undefined || body.removing) continue; - const fruit = FRUITS[body.fruitTier]; - const { x, y } = body.position; - - // Circle - ctx.beginPath(); - ctx.arc(x, y, fruit.radius, 0, Math.PI * 2); - ctx.fillStyle = fruit.color; - ctx.fill(); - - // Darker border - ctx.strokeStyle = darkenColor(fruit.color, 0.2); - ctx.lineWidth = 2; - ctx.stroke(); - - // Label - ctx.fillStyle = '#FFFFFF'; - ctx.font = `bold ${Math.max(10, fruit.radius * 0.6)}px sans-serif`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(fruit.name.slice(0, 2), x, y); + drawFruitCircle(ctx, fruit, body.position.x, body.position.y); } } @@ -118,13 +120,7 @@ function drawNextFruitPreview(ctx, state) { ctx.save(); ctx.globalAlpha = state.isDropCooldown ? 0.3 : 0.7; - ctx.beginPath(); - ctx.arc(x, y, fruit.radius, 0, Math.PI * 2); - ctx.fillStyle = fruit.color; - ctx.fill(); - ctx.strokeStyle = darkenColor(fruit.color, 0.2); - ctx.lineWidth = 2; - ctx.stroke(); + drawFruitCircle(ctx, fruit, x, y); // Drop guide line if (!state.isDropCooldown) { @@ -143,9 +139,9 @@ function drawNextFruitPreview(ctx, state) { function drawNextFruitPanel(ctx, state) { if (state.isGameOver) return; - const panelX = CONTAINER_X + CONTAINER_WIDTH + WALL_THICKNESS + 10; + const panelX = CONTAINER_X + CONTAINER_WIDTH + WALL_THICKNESS + PANEL_GAP; const panelY = CONTAINER_Y; - const panelSize = 60; + const panelSize = PANEL_WIDTH; ctx.fillStyle = '#FFFDF5'; ctx.strokeStyle = '#8B7355'; @@ -166,13 +162,7 @@ function drawNextFruitPanel(ctx, state) { const cx = panelX + panelSize / 2; const cy = panelY + 46; - ctx.beginPath(); - ctx.arc(cx, cy, previewRadius, 0, Math.PI * 2); - ctx.fillStyle = fruit.color; - ctx.fill(); - ctx.strokeStyle = darkenColor(fruit.color, 0.2); - ctx.lineWidth = 2; - ctx.stroke(); + drawFruitCircle(ctx, fruit, cx, cy, previewRadius); } function drawScore(ctx, score) { @@ -180,7 +170,7 @@ function drawScore(ctx, score) { ctx.font = 'bold 28px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; - ctx.fillText(`Score: ${score}`, CANVAS_WIDTH / 2, 10); + ctx.fillText(`Score: ${score}`, CONTAINER_X + CONTAINER_WIDTH / 2, 10); } function drawGameOverOverlay(ctx, score) {