Smileymation

R_000022 2024-10-23 C64 Animation (PETSCII) Tools: PETSCII Editor, C64 Studio, Excel Get it from CSDb.dk Grab the source code

Credits

Graphics by 4gentE
Music by Argon
Code by jmin

How it started

Pixeling is fun, but enhancing static pictures with animations and/or other tricks like raster bars has been on my todo list from day one. However, pixeling was so enjoyable that I kept postponing my dive into C64 ASM coding over and over again in the past months.

Similar to me pointing at Raistlin for become a scener, I am able to point to JackAsser for encouraging me to become a coder. His "Learn and do it. It would be AWESOME" reply to my Ceci n'est pas PETSCII pic never left my mind, so thank you both for being major signposts and the well-needed ass-kicks.

Before we jump into the story of my first released code, a short disclaimer:
While Smileymation is my first code released, it is not my first C64 ASM code per se. I started another project before this one, but it has become part of a larger F4CG group production, so it will see light of day sometime later. To document my journey in order of release, I have to acknowledge that parts of this release were not written from scratch; they either were lifted from the yet unreleased demo part or were advanced iteration of that code. Of course, there's new stuff too, so it's complicated. Waiting for the first code to be released would have been an option, but — as you surely have noticed — I'm somewhat of an impatient guy, so I just had to push it out there. Anyway, please read the following with that in mind.

About this release

When 4gentE's Among the Living pic popped up on CSDb I knew I needed to sit down and create a short color cycle animation for it. I mean, the pic is perfect, right? Similar to my Color GP PETSCII, the parts to splash some colors onto are all there already; I just need to update them. Boldly, I went ahead and asked via comment for an OK, which I received, so my second coding project was officially green-lit. Hooray!

Drawing a PETSCII

My first goal as a coder was to write a simple PETSCII viewer. The main reason is that PETSCII Editor's export isn't really the best, as it includes all the charset data even if the default charset is used, which leads to unnecessarily big .prg files.

Sure, there are tools out there that can convert .pngs into .prgs (complete with all the bells and whistles like crunching), but I set out to learn C64 ASM, so I needed something easy to begin with anyways. Thus, turning the raw bytes of chars and colors into a pic it is!

With zero ASM knowledge, I started by exploring the tools' .asm exports. BASIC starter? Check. A loop for drawing the chars and colors? Check. Let's see if I can write my own loop for reading 40x25 bytes of char data and 40x25 bytes of color data, and then shoving them into the correct screen memory.

What I finally ended up with are the following two functions doing exactly that: The first function reads char bytes stored in petscii_char and pushes them onto the screen:

drawPETSCII:
    lda petscii_char,x
    sta $0400,x
    lda petscii_char+$0fa,x
    sta $04fa,x
    lda petscii_char+$1f4,x
    sta $05f4,x
    lda petscii_char+$2ee,x
    sta $06ee,x
    inx
    cpx #$fa
    bne drawPETSCII
    rts

The second function is quite similar; it reads the color bytes from petscii_color and pushes them into memory to transform the already rendered PETSCII on screen from monochrome to rainbow.

drawColor:
    lda petscii_color,x
    sta $d800,x
    lda petscii_color+$0fa,x
    sta $d8fa,x
    lda petscii_color+$1f4,x
    sta $d9f4,x
    lda petscii_color+$2ee,x
    sta $daee,x
    inx
    cpx #$fa
    bne drawColor
    rts

"But jmin, why two separate functions when it could be one single loop" I hear you ask? Well, drawing a PETSCII is where I started, but my real goal is to bring it to life by either updating the chars or the colors.

At first, I reopened PETSCII Editor and started redoing the colors for each frame needed. It's of course stupid, but then again, I had to start somewhere and by using the editor, I could experiment with color combinations and see how the colors would flow through the pic.

With the frame data in hand, I build a big fat loop that would update the entire screen's color data, thus creating my first animation. It worked, of course, but at the same time, it consumed a lot of memory, causing the created .prg to multiply in size.

So, what if I defined a sequence of colors to cycle through and used this array when updating the PETSCII? Which part of the pic to updated can be extracted from the single color frames (actually, singular, because the frames I made already followed a sliced-up image). Jep, that's more like it.

colorGradient:
    !byte 00, 06, 11, 04, 12, 05, 15, 13, 15, 05, 12, 04, 11, 06, 00

Next up, slicing the PETSCII and writing down the addresses of each slice.

Excel? Excel!

I'm sure there's a tool out there for generating lists of STA commands based on sections of a screen, but I'm also sure that's a job that can be done in Excel within minutes. So, let's fire it up!

All I have to do now is slice up the pic by filling in the slice numbers. The rest is done automatically and just needs to be copied over into my .asm file.

ccycle:
    ldx currentFrame
    
    ; draw slice 01
    lda colorGradient,x    
    sta $d803
    sta $d804
    sta $d822
    sta $d823
    :
    
    ; draw slice 02
    inx
    lda colorGradient,x
    sta $d805
    sta $d806
    sta $d807
    sta $d81f
    :    

    ; draw slice XX
    :
    

There's a bit more going on here, like looping through all of the colors of the defined gradient, and so on, but that's more or less how it works.

Overall, Smileymation is composed of five color bits: two static (the initial one following 4gentE's pic and a grey variant), and the following three color cycle loops:

Wait, you're seeing four different loops? Nah, one of the four shown isn't really there; it's just combining the first two.

Char animation

Color cycling is neat, but I also wanted to animate the pic itself. I kept it simple, with only two chars being updated throughout the sequence: the center smiley's eyes!

For this, no elaborate code or Excel is needed. It all comes down to a couple of small functions that update the two chars in question with specific PETSCII codes.

eyesOpen:
    lda #$d1
    sta $05CA
    sta $05CC
    rts
    
eyesClosed:
    lda #$ad
    sta $05CA
    sta $05CC
    rts
    
eyesInsane:
    lda #$d7
    sta $05CA
    sta $05CC 
    rts    

eyesLU:
    lda #$7a
    sta $05CA
    sta $05CC 
    rts    
    
eyesRU:
    lda #$4c
    sta $05CA
    sta $05CC 
    rts    
    
eyesLD:
    lda #$50
    sta $05CA
    sta $05CC 
    rts    
    
eyesRD:
    lda #$4f
    sta $05CA
    sta $05CC 
    rts    
    

With these in place, all I need to do is calling the variant that's needed, and we're good.

Next up: getting the timing right to create all the animations and keep it all running smoothly.

Halt! Halt! Halt!

To more experienced coders: please note that I (still) don't really know what I'm doing here, and clean IRQs are still way over my head. However, the following is the first (and easiest) method I found for adding a timing function: waiting for a specific raster being drawn and thus calling my code at the same time each frame (or actually returning to my code at the same time each frame):

wait:
    lda timingFlag
    cmp $d012
    bne wait
    lda $d012

wait_nextLine:
    cmp $d012
    beq wait_nextLine
    rts
    

Basically, my idea was to trigger the wait whenever I'm done with an action for a frame. Calling it again would add a delay of exactly one frame, which works perfectly for syncing everything up and adding a pause when needed.

With that in place, I could write a short intro sequence as well as the main loop.

intro:  
    ldx #$00
    jsr drawPETSCII    
    ldx #$00
    jsr drawColor  

    jsr wait       
    jsr eyesClosed
    
    jsr wait    
    jsr eyesOpen
    
    jsr wait    
    jsr eyesClosed
    
    jsr wait    
    jsr eyesOpen
    
    jmp mainLoop
    

To keep the example code above simple, I reduced the number of waits to a single one. However, waiting just a single frame would, of course, be way too fast. Initially, I added as many jsr wait as needed, but later, I extended my wait function to accept a parameter that defines how many frames should be waited for in a single call.

In the mailLoop, I'm calling the color cycle functions in sequence, with the various eye movements in-between. Nothing magical happening here.

Adding Music

So, animating the pic is great and all, but it feels incomplete without some .sid music playing, right? I'm not a metal-head, but the mates at F4CG pointed out that the PETSCII is a reference to Anthrax's album Among the Living, so adding an Anthrax tune seemed fitting.

Goerp dug up Got the time (which isn't on that album, and, fun fact, it's not even an original Anthrax tune, but let's not get be too nerdy here, OK?), and that's the track I downloaded for my mission to get some music playing.

AFAIK playing a .sid isn't too difficult — all it takes is calling its play function every frame. Ha! With my wait function already in place, was just a matter of adding a single line of code and voilĂ , that was enough to get this banger of a song working. Well, it does need to be initialized first, and I had to shift things around in memory to fit the .sid data, but let's just say it was as simple as adding a jsr $1003, shall we?

Cleaning up my mess

With everything working, my final task was to clean up all the messy bits that I've added along the way. As mentioned before, the wait function needed to support a duration parameter, so instead of having dozens of jsr wait calls, I simply set the duration and call the wait once.

Additionally, I tweaked the gradients for a smoother flow, especially the last, larger one. While cleaning it up, I got the idea to speed up the last gradient, and again, it was just a matter of reducing the number of wait calls going from 3 to 2 to 1, with the last one becoming lightning fast. Love it!

50 vs 60hz

Up until now, I hadn't paid any attention to PAL/NTSC differences, so I had all the emulators I used over the years set to PAL. Being around CSDb for some time now, I did of course see all the "NTSC fixed" releases, I therefore decided to take a peek what's needed here exactly. As we all konw, there's an extra 10hz which of course does speed up the animation and the music because they're both tied to frames. Now, one could argue that watching Smileymation on a NTSC machine is just booting up a "speed metal" version, but I'm not sure that joke would land. So, let's figure out how to fix the music.

On Codebase64, I found the following small function that checks whether a PAL or NTSC system is used:

l1: 
    lda $d012
l2: 
    cmp $d012
    beq l2
    bmi l1
    cmp #$20
    bcc ntsc
    

OK, that looks simple! A loop that branches to ntsc if it's a NTSC machine. I can work with that!

To slow down the music, the easiest way is to skip a beat every 6th frame and that's exactly what I implemented. I'm using the PAL/NTSC function above for setting a variable palNtsc: if it's 0 (PAL), the music is called every frame; if it's not 0 (NTSC), I decrement it, and when it hits 0, it's time to skip the music for that frame and reset the variable for another countdown. There's probably a more elegant solution out there, but for my first dabbling into coding, I'm just happy it works.

music:
    ldx palNtsc
    cpx #$00
    beq playmusic
    dex
    stx palNtsc
    cpx #$00
    bne playmusic
    ldx #$07
    stx palNtsc
    rts
playmusic:
    jsr $1003 ; play the music
    rts
    

Skipping a frame might be noticeable to a trained ear, but for my first implementation, I'd say it's good enough.

For the animation, I don't change a thing. Having it running at 60hz is as beautiful as running it at 50hz I'd say.

Conclusion

What a fun project! While my code isn't necessarily how things are usually done, it does work, and I did learn a lot while writing it. There's plenty of room for improvement, and I can't wait to dive into handling timing (and IRQs) differently, but for a first release, I'm really happy with it.


What's CSDb thinking about it

Smasher (25.10.2024)

please update your function in F4cg: graphician... and coder! :)
cool one!!