Skip to content

Commit 74f3e9d

Browse files
Copilotcommjoen
andcommitted
Implement 2.5D visual effects - platforms, shadows, gradients
Co-authored-by: commjoen <1457214+commjoen@users.noreply.github.com>
1 parent ebb4478 commit 74f3e9d

1 file changed

Lines changed: 235 additions & 35 deletions

File tree

src/main.ts

Lines changed: 235 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3280,6 +3280,95 @@ function drawConfetti() {
32803280
}
32813281
}
32823282

3283+
// Helper functions for 2.5D visual effects
3284+
function drawRect3D(x: number, y: number, width: number, height: number, color: string, depth: number = 8) {
3285+
// Draw main face
3286+
ctx.fillStyle = color;
3287+
ctx.fillRect(x, y, width, height);
3288+
3289+
// Draw right side (darker)
3290+
const sideColor = darkenColor(color, 0.3);
3291+
ctx.fillStyle = sideColor;
3292+
ctx.beginPath();
3293+
ctx.moveTo(x + width, y);
3294+
ctx.lineTo(x + width + depth, y - depth);
3295+
ctx.lineTo(x + width + depth, y + height - depth);
3296+
ctx.lineTo(x + width, y + height);
3297+
ctx.closePath();
3298+
ctx.fill();
3299+
3300+
// Draw top side (lighter)
3301+
const topColor = lightenColor(color, 0.2);
3302+
ctx.fillStyle = topColor;
3303+
ctx.beginPath();
3304+
ctx.moveTo(x, y);
3305+
ctx.lineTo(x + depth, y - depth);
3306+
ctx.lineTo(x + width + depth, y - depth);
3307+
ctx.lineTo(x + width, y);
3308+
ctx.closePath();
3309+
ctx.fill();
3310+
}
3311+
3312+
function drawShadow(x: number, y: number, width: number, height: number, offsetX: number = 3, offsetY: number = 3) {
3313+
ctx.save();
3314+
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
3315+
ctx.fillRect(x + offsetX, y + offsetY, width, height);
3316+
ctx.restore();
3317+
}
3318+
3319+
function darkenColor(color: string, factor: number): string {
3320+
if (color.startsWith('#')) {
3321+
// Handle hex colors
3322+
const hex = color.slice(1);
3323+
const r = parseInt(hex.substr(0, 2), 16);
3324+
const g = parseInt(hex.substr(2, 2), 16);
3325+
const b = parseInt(hex.substr(4, 2), 16);
3326+
return `rgb(${Math.floor(r * (1 - factor))}, ${Math.floor(g * (1 - factor))}, ${Math.floor(b * (1 - factor))})`;
3327+
}
3328+
// Return as-is for named colors or rgb colors - we'll handle the main cases
3329+
return color;
3330+
}
3331+
3332+
function lightenColor(color: string, factor: number): string {
3333+
if (color.startsWith('#')) {
3334+
// Handle hex colors
3335+
const hex = color.slice(1);
3336+
const r = parseInt(hex.substr(0, 2), 16);
3337+
const g = parseInt(hex.substr(2, 2), 16);
3338+
const b = parseInt(hex.substr(4, 2), 16);
3339+
return `rgb(${Math.floor(r + (255 - r) * factor)}, ${Math.floor(g + (255 - g) * factor)}, ${Math.floor(b + (255 - b) * factor)})`;
3340+
}
3341+
// Return as-is for named colors or rgb colors
3342+
return color;
3343+
}
3344+
3345+
function drawCoin3D(x: number, y: number, radius: number) {
3346+
// Draw shadow first
3347+
ctx.save();
3348+
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
3349+
ctx.beginPath();
3350+
ctx.ellipse(x + 2, y + 3, radius * 0.8, radius * 0.3, 0, 0, 2 * Math.PI);
3351+
ctx.fill();
3352+
ctx.restore();
3353+
3354+
// Draw coin with gradient
3355+
const gradient = ctx.createRadialGradient(x - radius * 0.3, y - radius * 0.3, 0, x, y, radius);
3356+
gradient.addColorStop(0, '#4df');
3357+
gradient.addColorStop(0.7, '#0cf');
3358+
gradient.addColorStop(1, '#0af');
3359+
3360+
ctx.fillStyle = gradient;
3361+
ctx.beginPath();
3362+
ctx.arc(x, y, radius, 0, 2 * Math.PI);
3363+
ctx.fill();
3364+
3365+
// Add highlight
3366+
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
3367+
ctx.beginPath();
3368+
ctx.arc(x - radius * 0.3, y - radius * 0.3, radius * 0.3, 0, 2 * Math.PI);
3369+
ctx.fill();
3370+
}
3371+
32833372
function draw() {
32843373
// Draw background
32853374
if (imageBg && imageBgLoaded && imageBgObj) {
@@ -3346,8 +3435,7 @@ function draw() {
33463435
ctx.scale(scale, scale);
33473436
}
33483437
ctx.translate(-cameraX, -cameraY);
3349-
// Draw platforms
3350-
ctx.fillStyle = '#654321';
3438+
// Draw platforms with 3D effect
33513439
let lowestPlatformIndex = -1;
33523440
if (levelType === 'vertical' && platforms.length > 0) {
33533441
let maxY = -Infinity;
@@ -3361,6 +3449,15 @@ function draw() {
33613449
for (let i = 0; i < platforms.length; i++) {
33623450
const plat = platforms[i];
33633451
if ('isSlope' in plat && plat.isSlope) {
3452+
// Draw shadow for slope platforms
3453+
drawShadow(plat.x, plat.y, plat.width, plat.height);
3454+
3455+
// Draw slope platform with gradient
3456+
const gradient = ctx.createLinearGradient(plat.x, plat.y, plat.x, plat.y + plat.height);
3457+
gradient.addColorStop(0, '#8b6f47');
3458+
gradient.addColorStop(1, '#654321');
3459+
ctx.fillStyle = gradient;
3460+
33643461
ctx.beginPath();
33653462
ctx.moveTo(plat.x, plat.y);
33663463
ctx.lineTo(plat.x + plat.width, plat.endY);
@@ -3369,7 +3466,11 @@ function draw() {
33693466
ctx.closePath();
33703467
ctx.fill();
33713468
} else {
3372-
ctx.fillRect(plat.x, plat.y, plat.width, plat.height);
3469+
// Draw shadow first
3470+
drawShadow(plat.x, plat.y, plat.width, plat.height);
3471+
3472+
// Draw 3D platform
3473+
drawRect3D(plat.x, plat.y, plat.width, plat.height, '#654321', 6);
33733474
}
33743475
// Draw up-arrow on the visually lowest platform in vertical mode
33753476
if (levelType === 'vertical' && i === lowestPlatformIndex) {
@@ -3379,29 +3480,34 @@ function draw() {
33793480
ctx.textAlign = 'center';
33803481
ctx.textBaseline = 'middle';
33813482
ctx.globalAlpha = 0.85;
3483+
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
3484+
ctx.shadowOffsetX = 2;
3485+
ctx.shadowOffsetY = 2;
3486+
ctx.shadowBlur = 4;
33823487
ctx.fillText('↑', plat.x + plat.width / 2, plat.y + plat.height / 2);
33833488
ctx.globalAlpha = 1;
3489+
ctx.shadowColor = 'transparent';
3490+
ctx.shadowOffsetX = 0;
3491+
ctx.shadowOffsetY = 0;
3492+
ctx.shadowBlur = 0;
33843493
ctx.restore();
33853494
}
33863495
}
3387-
// Draw moving platforms
3388-
ctx.fillStyle = '#888';
3496+
// Draw moving platforms with 3D effect
33893497
for (const plat of movingPlatforms) {
3390-
ctx.fillRect(plat.x, plat.y, plat.width, plat.height);
3498+
drawShadow(plat.x, plat.y, plat.width, plat.height);
3499+
drawRect3D(plat.x, plat.y, plat.width, plat.height, '#888', 4);
33913500
}
3392-
// Draw boxes
3393-
ctx.fillStyle = '#b5651d';
3501+
// Draw boxes with 3D effect
33943502
for (const box of boxes) {
3395-
ctx.fillRect(box.x, box.y, box.width, box.height);
3503+
drawShadow(box.x, box.y, box.width, box.height);
3504+
drawRect3D(box.x, box.y, box.width, box.height, '#b5651d', 5);
33963505
}
3397-
// Draw collectibles
3506+
// Draw collectibles with 3D effects
33983507
for (const c of collectibles) {
33993508
if (!c.collected) {
34003509
if (c.type === 'coin') {
3401-
ctx.fillStyle = '#0cf';
3402-
ctx.beginPath();
3403-
ctx.arc(c.x + c.width / 2, c.y + c.height / 2, 10, 0, 2 * Math.PI);
3404-
ctx.fill();
3510+
drawCoin3D(c.x + c.width / 2, c.y + c.height / 2, 10);
34053511
} else if (c.type === 'heart') {
34063512
// Draw a heart shape
34073513
ctx.save();
@@ -3453,45 +3559,113 @@ function draw() {
34533559
}
34543560
}
34553561
}
3456-
// Draw spikes
3457-
ctx.fillStyle = '#e33';
3562+
// Draw spikes with 3D effect
34583563
for (const spike of spikes) {
3564+
// Draw shadow
3565+
ctx.save();
3566+
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
3567+
ctx.beginPath();
3568+
ctx.moveTo(spike.x + 2, spike.y + spike.height + 2);
3569+
ctx.lineTo(spike.x + spike.width / 2 + 2, spike.y + 2);
3570+
ctx.lineTo(spike.x + spike.width + 2, spike.y + spike.height + 2);
3571+
ctx.closePath();
3572+
ctx.fill();
3573+
ctx.restore();
3574+
3575+
// Draw spike with gradient
3576+
const gradient = ctx.createLinearGradient(spike.x, spike.y, spike.x + spike.width, spike.y + spike.height);
3577+
gradient.addColorStop(0, '#f55');
3578+
gradient.addColorStop(0.5, '#e33');
3579+
gradient.addColorStop(1, '#c22');
3580+
ctx.fillStyle = gradient;
3581+
34593582
ctx.beginPath();
34603583
ctx.moveTo(spike.x, spike.y + spike.height);
34613584
ctx.lineTo(spike.x + spike.width / 2, spike.y);
34623585
ctx.lineTo(spike.x + spike.width, spike.y + spike.height);
34633586
ctx.closePath();
34643587
ctx.fill();
3588+
3589+
// Add highlight
3590+
ctx.strokeStyle = '#f77';
3591+
ctx.lineWidth = 1;
3592+
ctx.beginPath();
3593+
ctx.moveTo(spike.x + spike.width / 2, spike.y);
3594+
ctx.lineTo(spike.x + spike.width / 4, spike.y + spike.height / 2);
3595+
ctx.stroke();
34653596
}
3466-
// Draw spawn tubes
3467-
ctx.fillStyle = '#0a8000'; // Dark green for tubes
3597+
// Draw spawn tubes with 3D effect
34683598
for (const tube of tubes) {
3469-
// Draw tube body
3470-
ctx.fillRect(tube.x, tube.y, tube.width, tube.height);
3599+
// Draw shadow
3600+
drawShadow(tube.x, tube.y, tube.width, tube.height);
3601+
3602+
// Draw tube body with 3D effect
3603+
drawRect3D(tube.x, tube.y, tube.width, tube.height, '#0a8000', 3);
34713604

34723605
// Draw tube opening (darker green) - larger opening for bigger tubes at the TOP where it meets the platform
3473-
ctx.fillStyle = '#064000';
3474-
const openingY = Math.max(tube.y, GROUND_Y - 15); // Position opening at platform level
3606+
const gradient = ctx.createRadialGradient(
3607+
tube.x + tube.width / 2,
3608+
Math.max(tube.y, GROUND_Y - 15) + 7,
3609+
0,
3610+
tube.x + tube.width / 2,
3611+
Math.max(tube.y, GROUND_Y - 15) + 7,
3612+
tube.width / 2
3613+
);
3614+
gradient.addColorStop(0, '#064000');
3615+
gradient.addColorStop(1, '#032000');
3616+
ctx.fillStyle = gradient;
3617+
const openingY = Math.max(tube.y, GROUND_Y - 15);
34753618
ctx.fillRect(tube.x + 4, openingY, tube.width - 8, 15);
34763619

34773620
// Draw pipe details (light green lines) - adjusted for longer tubes
34783621
ctx.fillStyle = '#0c8000';
34793622
ctx.fillRect(tube.x + 8, tube.y + 8, 3, tube.height - 16);
34803623
ctx.fillRect(tube.x + tube.width - 11, tube.y + 8, 3, tube.height - 16);
34813624

3482-
// Add more horizontal bands for longer tubes
3625+
// Add more horizontal bands for longer tubes with gradient
3626+
const bandGradient = ctx.createLinearGradient(tube.x, 0, tube.x + tube.width, 0);
3627+
bandGradient.addColorStop(0, '#0c8000');
3628+
bandGradient.addColorStop(0.5, '#0e9000');
3629+
bandGradient.addColorStop(1, '#0c8000');
3630+
ctx.fillStyle = bandGradient;
34833631
ctx.fillRect(tube.x + 4, tube.y + tube.height / 4, tube.width - 8, 2);
34843632
ctx.fillRect(tube.x + 4, tube.y + tube.height / 2, tube.width - 8, 2);
34853633
ctx.fillRect(tube.x + 4, tube.y + (3 * tube.height) / 4, tube.width - 8, 2);
3486-
3487-
ctx.fillStyle = '#0a8000'; // Reset to main tube color
34883634
}
3489-
// Draw enemies
3635+
// Draw enemies with 3D effects
34903636
for (const enemy of enemies) {
34913637
if (enemy.alive && ropeAnimation.targetEnemy !== enemy) {
3638+
// Draw shadow
3639+
ctx.save();
3640+
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
3641+
ctx.beginPath();
3642+
ctx.ellipse(
3643+
enemy.x + enemy.width / 2 + 2,
3644+
enemy.y + enemy.height + 2,
3645+
enemy.width * 0.4,
3646+
enemy.height * 0.2,
3647+
0,
3648+
0,
3649+
2 * Math.PI
3650+
);
3651+
ctx.fill();
3652+
ctx.restore();
3653+
34923654
if (enemy.type === 'circle') {
3493-
// Draw circle enemies as circles
3494-
ctx.fillStyle = '#f06'; // Pink color for circle enemies
3655+
// Draw circle enemies with 3D gradient
3656+
const gradient = ctx.createRadialGradient(
3657+
enemy.x + enemy.width / 2 - enemy.width * 0.2,
3658+
enemy.y + enemy.height / 2 - enemy.height * 0.2,
3659+
0,
3660+
enemy.x + enemy.width / 2,
3661+
enemy.y + enemy.height / 2,
3662+
enemy.width / 2
3663+
);
3664+
gradient.addColorStop(0, '#f8a');
3665+
gradient.addColorStop(0.7, '#f06');
3666+
gradient.addColorStop(1, '#d04');
3667+
3668+
ctx.fillStyle = gradient;
34953669
ctx.beginPath();
34963670
ctx.arc(
34973671
enemy.x + enemy.width / 2,
@@ -3501,15 +3675,28 @@ function draw() {
35013675
2 * Math.PI
35023676
);
35033677
ctx.fill();
3678+
3679+
// Add highlight
3680+
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
3681+
ctx.beginPath();
3682+
ctx.arc(
3683+
enemy.x + enemy.width / 2 - enemy.width * 0.25,
3684+
enemy.y + enemy.height / 2 - enemy.height * 0.25,
3685+
enemy.width * 0.15,
3686+
0,
3687+
2 * Math.PI
3688+
);
3689+
ctx.fill();
3690+
35043691
// Add simple eyes to make it look more enemy-like
35053692
ctx.fillStyle = '#000';
35063693
const eyeSize = 3;
35073694
ctx.fillRect(enemy.x + 8, enemy.y + 8, eyeSize, eyeSize);
35083695
ctx.fillRect(enemy.x + enemy.width - 11, enemy.y + 8, eyeSize, eyeSize);
35093696
} else {
3510-
// Draw square enemies as rectangles (original behavior)
3511-
ctx.fillStyle = '#f90'; // Orange color for square enemies
3512-
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
3697+
// Draw square enemies with 3D effect
3698+
drawRect3D(enemy.x, enemy.y, enemy.width, enemy.height, '#f90', 3);
3699+
35133700
// Add simple eyes to make it look more enemy-like
35143701
ctx.fillStyle = '#000';
35153702
const eyeSize = 4;
@@ -3532,14 +3719,27 @@ function draw() {
35323719

35333720
// Draw player character (original square or custom emoji)
35343721
ctx.save();
3722+
3723+
// Draw player shadow first
3724+
ctx.save();
3725+
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
3726+
ctx.fillRect(
3727+
player.x - cameraX + 3,
3728+
player.y - cameraY + player.height - 2,
3729+
player.width,
3730+
8
3731+
);
3732+
ctx.restore();
3733+
35353734
if (playerCharacter === 'SQUARE') {
3536-
// Draw original yellow rectangle
3537-
ctx.fillStyle = '#ff0';
3538-
ctx.fillRect(
3735+
// Draw original yellow rectangle with 3D effect
3736+
drawRect3D(
35393737
player.x - cameraX,
35403738
player.y - cameraY,
35413739
player.width,
3542-
player.height
3740+
player.height,
3741+
'#ff0',
3742+
4
35433743
);
35443744
} else {
35453745
// Draw custom emoji character

0 commit comments

Comments
 (0)