your personal website on atproto - mirror
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

added music and mobile controls plus debugging

+157 -22
src/lib/cards/TetrisCard/Tetris8Bit.mp3

This is a binary file and will not be displayed.

+151 -21
src/lib/cards/TetrisCard/TetrisCard.svelte
··· 1 1 <script lang="ts"> 2 2 import type { ContentComponentProps } from '../types'; 3 3 import { onMount, onDestroy } from 'svelte'; 4 + import Tetris8BitMusic from './Tetris8Bit.mp3'; 5 + import { isTyping } from '$lib/helper'; 4 6 5 7 let { item }: ContentComponentProps = $props(); 6 8 ··· 9 11 let ctx: CanvasRenderingContext2D | null = null; 10 12 let animationId: number; 11 13 let audioCtx: AudioContext | null = null; 14 + 15 + // Background music 16 + let bgMusic: HTMLAudioElement | null = null; 12 17 13 18 // Theme detection 14 19 let isAccentMode = $state(false); ··· 221 226 222 227 function playMove() { 223 228 // 8-bit tick 224 - playTone(200, 0.03, 'square', 0.025); 229 + playTone(200, 0.03, 'square', 0.08); 225 230 } 226 231 227 232 function playRotate() { 228 233 // 8-bit blip 229 - playTone(400, 0.04, 'square', 0.03); 234 + playTone(400, 0.04, 'square', 0.1); 230 235 } 231 236 232 237 function playDrop() { 233 238 // 8-bit thud 234 - playTone(80, 0.1, 'square', 0.04); 239 + playTone(80, 0.1, 'square', 0.12); 235 240 } 236 241 237 242 function playLineClear(count: number) { 238 243 // Swoosh - original style 239 244 const baseFreq = 400; 240 245 for (let i = 0; i < count; i++) { 241 - setTimeout(() => playTone(baseFreq + i * 100, 0.15, 'sine', 0.08), i * 80); 246 + setTimeout(() => playTone(baseFreq + i * 100, 0.15, 'sine', 0.15), i * 80); 242 247 } 243 248 } 244 249 245 250 function playGameOver() { 246 251 // 8-bit descending 247 - playTone(300, 0.15, 'square', 0.035); 248 - setTimeout(() => playTone(200, 0.15, 'square', 0.03), 150); 249 - setTimeout(() => playTone(120, 0.3, 'square', 0.025), 300); 252 + playTone(300, 0.15, 'square', 0.1); 253 + setTimeout(() => playTone(200, 0.15, 'square', 0.08), 150); 254 + setTimeout(() => playTone(120, 0.3, 'square', 0.06), 300); 255 + stopMusic(); 256 + } 257 + 258 + function startMusic() { 259 + if (!bgMusic) { 260 + bgMusic = new Audio(Tetris8BitMusic); 261 + bgMusic.loop = true; 262 + bgMusic.volume = 0.08; 263 + } 264 + bgMusic.currentTime = 0; 265 + bgMusic.play().catch(() => { 266 + // Autoplay blocked, ignore 267 + }); 268 + } 269 + 270 + function stopMusic() { 271 + if (bgMusic) { 272 + bgMusic.pause(); 273 + bgMusic.currentTime = 0; 274 + } 250 275 } 251 276 252 277 // Initialize grid ··· 389 414 const cleared = clearingLines.length; 390 415 391 416 // Remove lines from bottom to top to maintain indices 417 + // Do all splices first, then add empty rows, to avoid index shifting issues 392 418 for (const row of [...clearingLines].sort((a, b) => b - a)) { 393 419 grid.splice(row, 1); 420 + } 421 + for (let i = 0; i < cleared; i++) { 394 422 grid.unshift(Array(COLS).fill(null)); 395 423 } 396 424 ··· 425 453 426 454 // Handle keyboard input 427 455 function handleKeyDown(e: KeyboardEvent) { 456 + // Don't capture keys when user is typing in an input field 457 + if (isTyping()) return; 458 + 428 459 if (gameState !== 'playing' || isClearingAnimation) { 429 460 if (e.code === 'Space' || e.code === 'Enter') { 430 461 e.preventDefault(); ··· 464 495 // Touch controls 465 496 let touchStartX = 0; 466 497 let touchStartY = 0; 498 + let touchStartTime = 0; 499 + let longPressTimer: ReturnType<typeof setTimeout> | null = null; 500 + let isLongPressing = false; 501 + let longPressInterval: ReturnType<typeof setInterval> | null = null; 502 + let lastMoveX = 0; 503 + let hasMoved = false; 504 + 505 + const LONG_PRESS_DELAY = 300; 506 + const LONG_PRESS_MOVE_INTERVAL = 50; 507 + const SWIPE_THRESHOLD = 30; 508 + const MOVE_CELL_THRESHOLD = 25; 467 509 468 510 function handleTouchStart(e: TouchEvent) { 511 + // Prevent page scrolling when game is active 512 + if (gameState === 'playing') { 513 + e.preventDefault(); 514 + } 515 + 469 516 if (gameState !== 'playing' || isClearingAnimation) { 470 517 if (gameState !== 'playing') startGame(); 471 518 return; 472 519 } 473 - touchStartX = e.touches[0].clientX; 474 - touchStartY = e.touches[0].clientY; 520 + 521 + const touch = e.touches[0]; 522 + touchStartX = touch.clientX; 523 + touchStartY = touch.clientY; 524 + touchStartTime = performance.now(); 525 + lastMoveX = touch.clientX; 526 + hasMoved = false; 527 + isLongPressing = false; 528 + 529 + // Start long press detection 530 + longPressTimer = setTimeout(() => { 531 + isLongPressing = true; 532 + // Start accelerated falling 533 + longPressInterval = setInterval(() => { 534 + if (gameState === 'playing' && !isClearingAnimation) { 535 + if (movePiece(0, 1)) score += 1; 536 + } 537 + }, LONG_PRESS_MOVE_INTERVAL); 538 + }, LONG_PRESS_DELAY); 539 + } 540 + 541 + function handleTouchMove(e: TouchEvent) { 542 + if (gameState !== 'playing' || isClearingAnimation) return; 543 + 544 + // Prevent page scrolling 545 + e.preventDefault(); 546 + 547 + const touch = e.touches[0]; 548 + const dx = touch.clientX - touchStartX; 549 + const dy = touch.clientY - touchStartY; 550 + 551 + // If moved significantly, cancel long press 552 + if (Math.abs(dx) > 10 || Math.abs(dy) > 10) { 553 + if (longPressTimer) { 554 + clearTimeout(longPressTimer); 555 + longPressTimer = null; 556 + } 557 + if (longPressInterval) { 558 + clearInterval(longPressInterval); 559 + longPressInterval = null; 560 + isLongPressing = false; 561 + } 562 + } 563 + 564 + // Only allow horizontal movement if not swiping down 565 + // (vertical movement is greater than horizontal) 566 + if (Math.abs(dy) > Math.abs(dx) && dy > SWIPE_THRESHOLD) { 567 + return; 568 + } 569 + 570 + // Handle horizontal movement in cells 571 + const cellsMoved = Math.floor((touch.clientX - lastMoveX) / MOVE_CELL_THRESHOLD); 572 + if (cellsMoved !== 0) { 573 + hasMoved = true; 574 + for (let i = 0; i < Math.abs(cellsMoved); i++) { 575 + movePiece(cellsMoved > 0 ? 1 : -1, 0); 576 + } 577 + lastMoveX += cellsMoved * MOVE_CELL_THRESHOLD; 578 + } 475 579 } 476 580 477 581 function handleTouchEnd(e: TouchEvent) { 582 + // Clean up long press 583 + if (longPressTimer) { 584 + clearTimeout(longPressTimer); 585 + longPressTimer = null; 586 + } 587 + if (longPressInterval) { 588 + clearInterval(longPressInterval); 589 + longPressInterval = null; 590 + } 591 + 478 592 if (gameState !== 'playing' || isClearingAnimation) return; 479 593 594 + // If was long pressing, don't do anything else 595 + if (isLongPressing) { 596 + isLongPressing = false; 597 + return; 598 + } 599 + 480 600 const touchEndX = e.changedTouches[0].clientX; 481 601 const touchEndY = e.changedTouches[0].clientY; 482 602 const dx = touchEndX - touchStartX; 483 603 const dy = touchEndY - touchStartY; 484 - const threshold = 30; 604 + const elapsed = performance.now() - touchStartTime; 485 605 486 - if (Math.abs(dx) < threshold && Math.abs(dy) < threshold) { 487 - // Tap - rotate 606 + // Quick tap (no movement) - rotate 607 + if (!hasMoved && Math.abs(dx) < SWIPE_THRESHOLD && Math.abs(dy) < SWIPE_THRESHOLD && elapsed < 300) { 488 608 rotatePiece(); 489 - } else if (Math.abs(dx) > Math.abs(dy)) { 490 - // Horizontal swipe 491 - if (dx > threshold) movePiece(1, 0); 492 - else if (dx < -threshold) movePiece(-1, 0); 493 - } else { 494 - // Vertical swipe 495 - if (dy > threshold * 2) hardDrop(); 496 - else if (dy > threshold) movePiece(0, 1); 609 + return; 610 + } 611 + 612 + // Swipe down - hard drop 613 + if (dy > SWIPE_THRESHOLD * 2 && Math.abs(dy) > Math.abs(dx)) { 614 + hardDrop(); 497 615 } 498 616 } 499 617 ··· 511 629 spawnPiece(); 512 630 gameState = 'playing'; 513 631 lastDrop = performance.now(); 632 + startMusic(); 514 633 } 515 634 516 635 function calculateSize() { ··· 811 930 if (audioCtx) { 812 931 audioCtx.close(); 813 932 } 933 + if (bgMusic) { 934 + bgMusic.pause(); 935 + bgMusic = null; 936 + } 937 + if (longPressTimer) { 938 + clearTimeout(longPressTimer); 939 + } 940 + if (longPressInterval) { 941 + clearInterval(longPressInterval); 942 + } 814 943 }); 815 944 </script> 816 945 ··· 819 948 <div bind:this={container} class="relative h-full w-full overflow-hidden"> 820 949 <canvas 821 950 bind:this={canvas} 822 - class="h-full w-full" 951 + class="h-full w-full touch-none" 823 952 ontouchstart={handleTouchStart} 953 + ontouchmove={handleTouchMove} 824 954 ontouchend={handleTouchEnd} 825 955 ></canvas> 826 956
+6 -1
src/lib/cards/TetrisCard/index.ts
··· 1 + //Music by DJARTMUSIC - The Return Of The 8-bit Era 2 + //https://pixabay.com/de/music/videospiele-the-return-of-the-8-bit-era-301292/ 3 + 4 + 1 5 import type { CardDefinition } from '../types'; 2 6 import TetrisCard from './TetrisCard.svelte'; 3 7 import SidebarItemTetrisCard from './SidebarItemTetrisCard.svelte'; ··· 14 18 card.mobileW = 6; 15 19 card.mobileH = 8; 16 20 card.cardData = {}; 17 - } 21 + }, 22 + maxH: 10 18 23 } as CardDefinition & { type: 'tetris' };