From 5ae4090a6106a127ea41d7855d329c27f86940ea Mon Sep 17 00:00:00 2001 From: Christian Johannesen Date: Fri, 16 Oct 2026 03:08:27 +0200 Subject: [PATCH] Add Plasma Grid demo and fix lazy loading --- README.md | 3 +- plasma-grid/plasma-grid.asm | 263 ++++++++++++++++++++++++++++++++++ plasma-grid/plasma-grid.prg | Bin 0 -> 840 bytes programs.json | 6 + public/app.js | 17 ++- scripts/build-machine-code.sh | 3 +- 6 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 plasma-grid/plasma-grid.asm create mode 100644 plasma-grid/plasma-grid.prg diff --git a/README.md b/README.md index bc9b5e3..a5a30c0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # C64 Program Collection -Thirty small, polished programs for the Commodore 64, written in stock Commodore BASIC V2 and 6502 machine code. The collection mixes arcade action, puzzles, simulations, creative tools, music, utilities, and visual effects. Every entry is self-contained and runs on an ordinary PAL or NTSC C64 without a cartridge or BASIC extension. +Thirty-one small, polished programs for the Commodore 64, written in stock Commodore BASIC V2 and 6502 machine code. The collection mixes arcade action, puzzles, simulations, creative tools, music, utilities, and visual effects. Every entry is self-contained and runs on an ordinary PAL or NTSC C64 without a cartridge or BASIC extension. Browse and run the collection at [c64.p20.dev](https://c64.p20.dev). @@ -38,6 +38,7 @@ Browse and run the collection at [c64.p20.dev](https://c64.p20.dev). | Typing Sprint | 2026-10-16 | Word | Machine Code | `A`-`Z`, Space, `Q` | Race the clock while copying a Commodore-themed phrase. | [ASM](typing-sprint/typing-sprint.asm) | [Run](https://c64.p20.dev/program/typing-sprint) | | PETSCII Aquarium | 2026-10-16 | Simulation | Machine Code | `F`, `1`-`3`, `Q` | Watch a tiny animated aquarium and feed its roaming fish. | [ASM](petscii-aquarium/petscii-aquarium.asm) | [Run](https://c64.p20.dev/program/petscii-aquarium) | | Fifteen Puzzle | 2026-10-16 | Puzzle | Machine Code | `W`, `A`, `S`, `D`, `R`, `Q` | Slide numbered tiles until the classic four-by-four board is restored. | [ASM](fifteen-puzzle/fifteen-puzzle.asm) | [Run](https://c64.p20.dev/program/fifteen-puzzle) | +| Plasma Grid | 2026-10-16 | Demo | Machine Code | `1`-`3`, `C`, Space, `Q` | Fill the screen with a flowing field of color generated in real time. | [ASM](plasma-grid/plasma-grid.asm) | [Run](https://c64.p20.dev/program/plasma-grid) | ## Running the programs diff --git a/plasma-grid/plasma-grid.asm b/plasma-grid/plasma-grid.asm new file mode 100644 index 0000000..894e60b --- /dev/null +++ b/plasma-grid/plasma-grid.asm @@ -0,0 +1,263 @@ +!cpu 6510 + +; Plasma Grid - real-time character and color RAM animation. +; Uses the collection's standard black splash screen. +; Self-starting BASIC line: 10 SYS2061 +* = $0801 +!word basic_end +!word 10 +!byte $9e +!text "2061" +!byte 0 +basic_end: !word 0 + +* = $080d +SCREEN = $0400 +COLOR = $d800 +GETIN = $ffe4 +CHROUT = $ffd2 + +start: + lda $d020 + sta old_border + lda $d021 + sta old_background + lda #0 + sta $d020 + sta $d021 + jsr show_splash + lda #0 + sta $d020 + sta $d021 + lda #147 + jsr CHROUT + lda #0 + sta phase_x + sta phase_y + sta palette_offset + sta paused + sta frame_count + lda #3 + sta animation_speed + lda $a2 + sta last_jiffy + jsr render_plasma + +main_loop: + jsr GETIN + beq check_clock + cmp #81 + bne check_palette + jmp exit +check_palette: + cmp #67 + bne check_pause + lda palette_offset + eor #16 + sta palette_offset + jmp check_clock +check_pause: + cmp #32 + bne check_speed_one + lda paused + eor #1 + sta paused + jmp check_clock +check_speed_one: + cmp #49 + bne check_speed_two + lda #6 + sta animation_speed + jmp check_clock +check_speed_two: + cmp #50 + bne check_speed_three + lda #3 + sta animation_speed + jmp check_clock +check_speed_three: + cmp #51 + bne check_clock + lda #1 + sta animation_speed + +check_clock: + lda paused + bne main_loop + lda $a2 + cmp last_jiffy + beq main_loop + sta last_jiffy + inc frame_count + lda frame_count + cmp animation_speed + bcc main_loop + lda #0 + sta frame_count + inc phase_x + lda phase_x + and #63 + sta phase_x + lda phase_x + and #1 + bne draw_frame + inc phase_y + lda phase_y + and #63 + sta phase_y +draw_frame: + jsr render_plasma + jmp main_loop + +render_plasma: + lda #0 + sta row +row_loop: + ldx row + lda screen_row_lo,x + sta $fb + lda screen_row_hi,x + sta $fc + lda color_row_lo,x + sta $fd + lda color_row_hi,x + sta $fe + lda #0 + sta column +column_loop: + lda column + clc + adc phase_x + and #63 + tax + lda wave,x + sta wave_sum + lda row + asl + clc + adc phase_y + and #63 + tax + lda wave,x + clc + adc wave_sum + and #15 + sta pattern + clc + adc palette_offset + tax + lda palettes,x + ldy column + sta ($fd),y + lda pattern + and #4 + beq solid_cell + lda #102 + bne paint_cell +solid_cell: + lda #160 +paint_cell: + sta ($fb),y + inc column + lda column + cmp #40 + bne column_loop + inc row + lda row + cmp #25 + bne row_loop + rts + +show_splash: + ldx #0 +print_splash: + lda splash_text,x + beq wait_for_space + jsr CHROUT + inx + bne print_splash +wait_for_space: + jsr GETIN + cmp #32 + bne wait_for_space + rts + +exit: + lda old_border + sta $d020 + lda old_background + sta $d021 + lda #147 + jsr CHROUT + rts + +splash_text: +!byte 147,13,13,13 +!byte 154 +!text " PLASMA GRID" +!byte 13,13 +!byte 5 +!text " A FLOWING FIELD OF COLOR AND MOTION" +!byte 13,13,13 +!byte 153 +!text " CONTROLS" +!byte 13,13 +!text " " +!byte 158 +!text "1-3" +!byte 5 +!text " SPEED " +!byte 158 +!text "C" +!byte 5 +!text " COLOR " +!byte 158 +!text "SPACE" +!byte 5 +!text " PAUSE " +!byte 158 +!text "Q" +!byte 5 +!text " QUIT" +!byte 13,13,13 +!byte 5 +!text " HTTPS://C64.P20.DEV" +!byte 13,13 +!byte 152 +!text " RELEASED 16 OCTOBER 2026" +!byte 13,13,13 +!byte 158 +!text " PRESS SPACE TO START" +!byte 0 + +; Two deliberately different 16-color sequences. +palettes: +!byte 0,6,11,4,14,3,13,1,13,3,14,4,11,6,0,6 +!byte 0,2,8,7,1,7,8,2,9,10,5,13,3,14,6,0 + +; A 64-step triangle/sine-like wave. Combining horizontal and vertical +; samples produces the moving plasma interference pattern. +wave: +!byte 8,9,10,11,12,13,14,15,15,15,15,14,14,13,12,11 +!byte 10,9,8,7,6,5,4,3,2,1,0,0,0,0,1,1 +!byte 2,3,4,5,6,7,8,9,10,11,12,13,14,15,15,15 +!byte 14,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0 + +screen_row_lo: !for i,0,24 { !byte <(SCREEN+i*40) } +screen_row_hi: !for i,0,24 { !byte >(SCREEN+i*40) } +color_row_lo: !for i,0,24 { !byte <(COLOR+i*40) } +color_row_hi: !for i,0,24 { !byte >(COLOR+i*40) } + +phase_x: !byte 0 +phase_y: !byte 0 +palette_offset: !byte 0 +paused: !byte 0 +animation_speed: !byte 0 +frame_count: !byte 0 +last_jiffy: !byte 0 +row: !byte 0 +column: !byte 0 +wave_sum: !byte 0 +pattern: !byte 0 +old_border: !byte 0 +old_background: !byte 0 diff --git a/plasma-grid/plasma-grid.prg b/plasma-grid/plasma-grid.prg new file mode 100644 index 0000000000000000000000000000000000000000..e0b290bbb2d490b753b7fac45b6cc0e6bd0dcbed GIT binary patch literal 840 zcmZ`%J#5oZ5VjNBaeRG&F>J3}JJ2R6%8;~89ouOwxv^`zz}N{Pq)JSPfq^BXrB;Mu z!m`e-e9vHz%z}LprOuApqZrPCEn*uF%S??GY~{RyoZE0d!{k^*j#~C?fA4Ss1~HoV z8;~^Ma2_@u;R+4t*C46EWCOS1ugIWZhosIkb-sz?s;JZ9sSfEIt#g!q)jB^@6*|Z1 zICOqttqDmJitF6uAbp@k1|&LY#dYq%6UB7{CND%LYG&aey`w#8!sc;e!lX2i;u|az z?`|IThc3Md_Pnq2{@x`Pt?dcvz5GDIYkgtg@Fe)@Kh_cp@S;fz@TkK}u5A%QcIbI{ zjvg%QbkVm>LS(GywB@+h?DisU*_LBcw?z%taeb<{O}gxcw%bONhvz*wH{5pUyG}p| z?Upak$TaXQ%cQhx$fy0Z8+f{5$<)(V0}CrFGF@4@x zw5}4eeOC1?$I=7zEmx^)gl^OFX{A)DqDS{kLp|RL0-TS-Xz0=))csHp6p#v8B9d%D z0!2`SjG7a3Y9_DAm{J5aufZ58TtE~GWDK;tnp0#cn-TFZ6+|(Um1HINSM`r*!n?cs wYEWFbx$)wYTDpG!G5b!JZg0PoMv#Y4M#ymp|Lb@=!n+~fkMMzmZ}JDf0nNJkkN^Mx literal 0 HcmV?d00001 diff --git a/programs.json b/programs.json index 1cf5809..f601dd7 100644 --- a/programs.json +++ b/programs.json @@ -178,5 +178,11 @@ "description": "Slide numbered tiles until the classic four-by-four board is restored.", "controls": [{"keys":["W","A","S","D"],"description":"Slide a tile toward the empty space"},{"keys":["R"],"description":"Shuffle the board again"},{"keys":["Q"],"description":"Return to BASIC"}], "path": "fifteen-puzzle/fifteen-puzzle.prg", "source": "fifteen-puzzle/fifteen-puzzle.asm" + }, + { + "title": "Plasma Grid", "category": "Demo", "language": "machine-code", "releaseDate": "2026-10-16", + "description": "Fill the screen with a flowing field of color generated in real time.", + "controls": [{"keys":["1","2","3"],"description":"Select slow, medium, or fast animation"},{"keys":["C"],"description":"Change color palette"},{"keys":["SPACE"],"description":"Pause or resume the animation"},{"keys":["Q"],"description":"Return to BASIC"}], + "path": "plasma-grid/plasma-grid.prg", "source": "plasma-grid/plasma-grid.asm" } ] diff --git a/public/app.js b/public/app.js index 3336a05..d4855f6 100644 --- a/public/app.js +++ b/public/app.js @@ -23,6 +23,7 @@ let allPrograms = []; let filteredPrograms = []; let visibleProgramCount = 0; const PROGRAM_BATCH_SIZE = 12; +let programObserver; const categoryMeta = { Arcade: '๐ŸŽฎ', Creative: 'โœŽ', Demo: 'โœฆ', Dice: 'โš„', Educational: 'โŒ', @@ -138,6 +139,16 @@ function appendProgramBatch() { status.textContent = `${visibleProgramCount} of ${filteredPrograms.length} matching programs shown ยท ${allPrograms.length} total`; } +function observeProgramSentinel() { + programObserver.disconnect(); + if (!programSentinel.hidden) requestAnimationFrame(() => programObserver.observe(programSentinel)); +} + +function loadBatchNearViewport() { + if (programSentinel.hidden || visibleProgramCount >= filteredPrograms.length) return; + if (programSentinel.getBoundingClientRect().top <= window.innerHeight + 500) appendProgramBatch(); +} + function renderPrograms() { const query = searchInput.value.trim().toLowerCase(); const selectedCategory = categoryFilter.value; @@ -156,6 +167,7 @@ function renderPrograms() { programsElement.replaceChildren(); visibleProgramCount = 0; appendProgramBatch(); + observeProgramSentinel(); } async function showSource(program) { @@ -295,10 +307,11 @@ window.addEventListener('popstate', () => { } }); -const programObserver = new IntersectionObserver(entries => { +programObserver = new IntersectionObserver(entries => { if (entries.some(entry => entry.isIntersecting) && visibleProgramCount < filteredPrograms.length) appendProgramBatch(); }, { rootMargin: '500px 0px' }); -programObserver.observe(programSentinel); +window.addEventListener('scroll', loadBatchNearViewport, { passive: true }); +window.addEventListener('resize', loadBatchNearViewport); function setView(view) { const list = view === 'list'; diff --git a/scripts/build-machine-code.sh b/scripts/build-machine-code.sh index daf5217..8c35391 100644 --- a/scripts/build-machine-code.sh +++ b/scripts/build-machine-code.sh @@ -11,7 +11,8 @@ for source in \ jiffy-clock/jiffy-clock.asm \ typing-sprint/typing-sprint.asm \ petscii-aquarium/petscii-aquarium.asm \ - fifteen-puzzle/fifteen-puzzle.asm + fifteen-puzzle/fifteen-puzzle.asm \ + plasma-grid/plasma-grid.asm do output="${source%.asm}.prg" echo "Assembling $source -> $output"