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.
This commit is contained in:
2026-04-12 11:32:15 +07:00
parent b91b29753f
commit 458751c363
3 changed files with 53 additions and 53 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -8,6 +8,8 @@ import {
CONTAINER_HEIGHT,
WALL_THICKNESS,
DANGER_LINE_Y,
PANEL_WIDTH,
PANEL_GAP,
} from './constants.js';
export function createCanvas() {
@@ -81,31 +83,31 @@ function drawDangerLine(ctx) {
ctx.restore();
}
function drawFruits(ctx, bodies) {
for (const body of bodies) {
if (body.fruitTier === undefined || body.removing) continue;
function drawFruitCircle(ctx, fruit, x, y, radius) {
const r = radius ?? fruit.radius;
const fruit = FRUITS[body.fruitTier];
const { x, y } = body.position;
// Circle
ctx.beginPath();
ctx.arc(x, y, fruit.radius, 0, Math.PI * 2);
ctx.arc(x, y, r, 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.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];
drawFruitCircle(ctx, fruit, body.position.x, body.position.y);
}
}
function drawNextFruitPreview(ctx, state) {
@@ -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) {