I quote leod. That's why you have graphics and tilemap, because you can't draw directly on screen and just having the graphics isn't enough, you have to put them in some way on the screen.
I had this curiosity about playing around with pixels and stuff so I tried to recode the vwf effect myself. It ended up being a pain because it's purely manipulating bits in a way you can get something decent. 2bpp for instance is stored 2 bytes. Each bit represents one pixel, and for each pixel you have 2 bits to have up to 4 colors (00, 01, 10, 11). One byte represents one 8x1 line, but these two bytes aren't stored one after another, the first byte for a line is stored at 0x00 and the next one at 0x10, the next line reads 0x01 and 0x11 and so on.
For instance, here's how I coded the vwf effect (just a part of it, of course):
LDA LetterGFX,x ;first 8x8
LDA LetterGFX+$10,x ;second 8x8 (letters 16x8)
STA $0B ;Second byte
STA $0C ;of information (2bpp)
LDA LetterGFX+$0100,x ;first 8x8
LDA LetterGFX+$0110,x ;second 8x8 (letters 16x8)
STA $3102 ;Second byte
STA $3103 ;of information (2bpp)
LDA $09 : XBA : LSR A : XBA : STA $09
LDA $0B : XBA : LSR A : XBA : STA $0B
LDA $3100 : XBA : LSR A : XBA : STA $3100
LDA $3102 : XBA : LSR A : XBA : STA $3102
LDA $09 : XBA : ASL A : XBA : STA $09
LDA $0B : XBA : ASL A : XBA : STA $0B
LDA $3100 : XBA : ASL A : XBA : STA $3100
LDA $3102 : XBA : ASL A : XBA : STA $3102
CLC : ADC $00
LDA !GFXBuffer,x : ORA $09 : STA !GFXBuffer,x
LDA !GFXBuffer+$01,x : ORA $0B : STA !GFXBuffer+$01,x
LDA !GFXBuffer+$10,x : ORA $0A : STA !GFXBuffer+$10,x
LDA !GFXBuffer+$11,x : ORA $0C : STA !GFXBuffer+$11,x
LDA !GFXBuffer+$100,x : ORA $3100 : STA !GFXBuffer+$100,x
LDA !GFXBuffer+$101,x : ORA $3102 : STA !GFXBuffer+$101,x
LDA !GFXBuffer+$110,x : ORA $3101 : STA !GFXBuffer+$110,x
LDA !GFXBuffer+$111,x : ORA $3103 : STA !GFXBuffer+$111,x
LeterGFX is just a pointer for the .bin with each letter's graphics in 2bpp. You see how I loaded $00, $10, $01 and $11? That's basically the 8x1 line I said, as you might guess, I had to loop through that 8 times to build a whole 8x8 tile. You can also see $0100, $0110, $0101 and $0111, but that's just because I was working with a 8x16 font, so I had to build 2 8x8 tiles.
After storing this data in a buffer, I had to work with the real stuff that would be uploaded to vram, that's !GFXBuffer. That was done by checking this RAM !ShiftValue that I would use to keep track of where I would have to start drawing the next letter inside a 8x8 tile (for example, iirc an "i" occupied 3 pixels, already counting the extra space you need between letters, so after drawing an "i", I would add 3 to that RAM and check whenever it would be #$08 to readjust everything and go to another 8x8 tile without cutting information, that's why it can be a pain in the butt to work only with bits. You don't have any specific opcodes other than ASL LSR AND ORA and so on). As you can see, I loaded this !ShiftValue RAM and did a bunch of ASL/LSR/XBA with the letter GFX so I could set the bits to the right place (in YYCHR, that would be moving everything one pixel to the right or to the left) and then I would take the vram buffer and do an ORA, because we don't want to substitute what we had, but we want to add that extra letter.
I also had that addPattern code, that would let me add a 8x8 filler to the back of the 8x8 (of course, only where there's are letter pixels)
SEC : SBC $0A
SEC : SBC $0A
Basically the same idea, load the part of .bin that has the 8x8 background pattern and merge it with the buffer. This is a bit trickier than just building up the text because I had to make sure I wasn't overwriting anything, so I had to load the $00 byte, merge it with $01 byte (because one bit is enough to say that there's already some pixel there), do the same with the vram buffer, subtract of one of another to remove everything that would be overwritten and finally merge with the vram buffer.
I know it's kinda hard to get the idea just from these random pieces of code, but the point is that you have to figure out which bitshifting operations will get the result you want. AND works as a mask filter, ORA is to merge stuff, LSR and ASL is to shift everything to the left or to the right.
One last example would be the pixel effect I posted in the ASM show-off thread. Unfortunately, 4bpp is a bit different but I don't quite remember how it works, I think it's $00-$01 + $10-$11 for one 8x1 line (0000, 0001, .... 1111). The following code is working in 16-bits:
LDA !GFXBuffer,x ;Load byte (4 bytes per line)
AND InvPixelMask,y ;Keep all pixels which shouldn't be affected now
STA $00 ;Save that value for now
LDA !GFXBuffer2,x ;Load source and get
AND PixelMask,y ;ONLY the necessary pixels
ORA $00 ;Add result on top of initial gfx
STA !GFXBuffer,x ;Save to result buffer
LDA !GFXBuffer+$10,x ;Same deal. This finishes one line
Again, it's just a bunch of math that does what I need. I load the vram buffer (it has the current pixelated image) and filter only the pixels I want to change that frame (that y index is basically a frame index), then I do the same with the buffer which has the whole correct final result and merge with the vram buffer. That way, every frame I change just some pixels of the current on-screen image, while I always hold the final result in a buffer.
Of course, after all that stuff, you have to upload to VRAM and then build the tilemap. I'd say that what's most important is that you know how the x-bpp you want to work with works. If you don't know what each bit represents and how the bytes are organized, you'll end up uploading garbage to VRAM.