I have been learning Java for a year now, which made me come to conclusion that I've developed filtered word, so now I try to fight it with my beloved C.
I really like terminals, and I really like anime, so why don't I make a terminal? I have made a very simple terminal that isn't a terminal emulator nor multiplexer, but it still works. By "works" I mean it runs nvim (although with default configuration, but I was too lazy to fix it), so I call it a win. But now I really want to make a real terminal emulator, in C, which is a bit hardcore, so I allowed myself to use SDL3 for graphics and input (and I can even add some sfx and music later :D).
As everyone knows, to learn something you need to do something, so I decided to make some mini projects before I decide to conquer this mountain.
I don't have real goals to be honest, only . What I want to do though is to render an image (because how else can I draw lovely anime girls on the screen) and do some manipulations with it, f.e. making it transparent (which is the simplest :>), render fading borders, and make fading as custom shapes (kinda hard to explain, but you will see it as examples).
Also I stopped caring about the c-style namings, if you want to be nerdy about it, fuck off.
Okay the main struct I will have, that will be used across the program, is State
typedef struct {
SDL_Window* window;
SDL_Renderer* renderer;
} State;
State state; // global variable, if you didn't notice
It is just a placeholder for window and corresponding renderer (if you don't know, usually SDL_Window can have only one SDL_Renderer).
I will not use the SDL_main, because I don't like it, I don't need to render the blank screen 100s times a second.
So the structure of my main function ;)
int main() {
init(); // creates SDL_Window and SDL_Renderer and put it in the state
bool window_should_close = false;
while (!window_should_close) {
SDL_Event event;
SDL_WaitEvent(&event); // suspends the thread until the event is found
if (event.type == SDL_EVENT_QUIT)
window_should_close = true;
}
destroy(); // SDL_DestroyWindow and SDL_DestroyRenderer
return 0;
}
I just like it as it is, so expect no changes in future.
Now next important thing. I will be working with SDL_Texture instead of SDL_Surface, because texture is rendered using GPU, while surface uses CPU, however SDL_UpdateTexture is using CPU to update each pixel (which you may guess is kinda shit slow), and it is our mc...
Anyway, the next important (or not) thing is the Image struct
typedef struct {
SDL_Texture* texture;
int pitch;
} Image;
The first version was only SDL_Texture (there was not such struct as Image), but SDL_UpdateTexture needs the pitch if you want to update the whole texture and SDL_Texture doesn't store it in itself. The width and height of the image is stored inside the texture as w and h (they are ints).
Having Image as an independent struct solves 1 real and 1 imaginary problem:
- the real probles is where to store the pitch
- the imaginary is how to ensure SDL_Texture uses RGBA pixels
So what is the pitch?
Imagine having a picture 5x5 pixels, each pixel is RGBA, each uses 4 bytes (so 1 byte is Red, 1 is Green, 1 is Blue, 1 is Alpha), so for this picture (or better to say SDL_Texture) we use 5x5x4 (100) bytes, the catch is that each row of pixels can have more then the 5x4 bytes, these are called paddings, so the pitch has both paddings and pixels in a row. If the image 5x5 pixels have 12 bytes paddings, then the pitch is 32 bytes. Pretty simple, yeah?
Okay the Image with corresponding image_ functions will be in the image.h file, the first 2 functions are basically allocating data and freeing data
Image* image_open_rgba(char* filepath, SDL_Renderer* renderer) {
Image* image = malloc(sizeof(Image));
SDL_Surface* image_surface = IMG_Load(filepath);
if (!image_surface) { // no image at filepath
free(image);
return NULL;
}
SDL_Surface* surface = SDL_ConvertSurface(image_surface, SDL_PIXELFORMAT_RGBA32); // ensure rgba ._.
image->texture = SDL_CreateTextureFromSurface(renderer, surface); // texture ?
image->pitch = surface->pitch; // the pain in the ass ahh variable
SDL_DestroySurface(image_surface);
SDL_DestroySurface(surface);
return image;
}
void image_destroy(Image* image) {
SDL_DestroyTexture(image->texture);
free(image);
}
The thing you might have found here is I don't care if something crashes (e.g. SDL_ConvertSurface) the program will crash, but ehh Java doesn't care as well when you do Image image = new Image(); so me neither. If it crushes, let it segfault your ass.
The main goal in the image_open_rgba is to ensure Image->texture pixels are in rgba format, because every other function will rely on the suggestion that it is true.
And now lets change the main a bit so we can draw the image
int main() {
init();
Image* image = image_open_rgba("./images/wallpaper.jpg", state.renderer); // create the image
bool window_should_close = false;
while (!window_should_close) {
SDL_RenderTexture(state.renderer, image->texture, NULL, NULL); // our lovely render texture
SDL_RenderPresent(state.renderer); // don't forget the present changes
SDL_Event event;
SDL_WaitEvent(&event);
if (event.type == SDL_EVENT_QUIT)
window_should_close = true;
}
image_destroy(image); // don't forget to free the image, or else MEMORY LEAK
destroy();
return 0;
}
Now if we test it should draw the window
and yeah, it works.
So now lets start with the most simple thing we can do, that is changing the transparency of each pixel.
The function I came up with is pretty simple
void image_change_transparency(Image* image, uint8_t a) {
size_t size = 4 * image->texture->w * image->texture->h;
uint8_t *pixel_stuff = calloc(size, sizeof(uint8_t)); // too many bytes to store on the stack
for (size_t i = 3; i < size; i+=4)
pixel_stuff[i] = a; // nice transparency
SDL_UpdateTexture(image->texture, NULL, pixel_stuff, image->pitch);
free(pixel_stuff);
}
Now add the image_change_transparency(image, 100) before the main loop and we can test it.
Ehh, kinda bad, I guess I know what is happening, instead of changing only the transparency I actually made the picture black and set transparency to 100, which is exactly what I didn't need to do.
Okay I am sure it is the error, so I won't change the background color to make sure v*v.
Ehhh current Image struct won't work out, because to get the previous pixels I need SDL_Surface, so lets make a quick change to the function that creates the image and change the struct
// new struct
typedef struct {
SDL_Texture* texture;
SDL_Surface* surface; // will be changed each time I change the pixels
int pitch;
} Image;
// new functions
Image* image_open_rgba(char* filepath, SDL_Renderer* renderer) {
Image* image = malloc(sizeof(Image));
SDL_Surface* image_surface = IMG_Load(filepath);
if (!image_surface) { // no image at filepath
free(image);
return NULL;
}
SDL_Surface* surface = SDL_ConvertSurface(image_surface, SDL_PIXELFORMAT_RGBA32);
image->texture = SDL_CreateTextureFromSurface(renderer, surface);
image->surface = surface; // save the converted surface
image->pitch = surface->pitch; // possibly don't need it as it is saved in the surface
SDL_DestroySurface(image_surface); // now only free the image_surface
return image;
}
void image_destroy(Image* image) {
SDL_DestroyTexture(image->texture);
SDL_DestroySurface(image->surface); // add this line
free(image);
}
And now the transparency function becomes a bit more fancy.
void image_change_transparency(Image* image, uint8_t a) {
size_t size = 4 * image->texture->w * image->texture->h;
uint8_t *pixel_stuff = calloc(size, sizeof(uint8_t));
memcpy(pixel_stuff, image->surface->pixels, size); // first get current pixels from the surface
for (size_t i = 3; i < size; i+=4)
pixel_stuff[i] = a;
memcpy(image->surface->pixels, pixel_stuff, size); // now copy the updated pixels to the surface to save them
SDL_UpdateTexture(image->texture, NULL, pixel_stuff, image->pitch);
free(pixel_stuff);
}
And vuala it works!
Also add these lines after the while() but before the SDL_Render stuff
SDL_SetRenderDrawColor(state.renderer, 100, 0, 0, 0); // just a red background so we can see the changes
SDL_RenderClear(state.renderer); // clear the shit!
Kinda looks as a crime scene, but who cares?
Okay now lets try to make some fading in the image.
void image_simple_fade(Image* image) {
size_t size = 4 * image->texture->w * image->texture->h;
uint8_t *pixel_stuff = calloc(size, sizeof(uint8_t));
memcpy(pixel_stuff, image->surface->pixels, size);
float fade_speed = (float) 255 / image->texture->w; // pretty important btw
for (uint32_t row = 0; row < image->texture->w; row++) {
float fade = 0;
for (uint32_t col = 0; col < image->texture->h; col++) {
uint32_t pixel = row * image->texture->w + col;
pixel_stuff[pixel * 4 + 3] /* getting the byte */ = fade;
fade += fade_speed;
}
}
memcpy(image->surface->pixels, pixel_stuff, size);
SDL_UpdateTexture(image->texture, NULL, pixel_stuff, image->pitch);
free(pixel_stuff);
}
Ehh something went wrong here...
Yea a bit embarrassing but the error is kinda smol
void image_simple_fade(Image* image) {
size_t size = 4 * image->texture->w * image->texture->h;
uint8_t *pixel_stuff = calloc(size, sizeof(uint8_t));
memcpy(pixel_stuff, image->surface->pixels, size);
float fade_speed = (float) 255 / image->texture->w;
// for (uint32_t row = 0; row < image->texture->w <--- change; row++) {
for (uint32_t row = 0; row < image->texture->h; row++) {
float fade = 0;
uint32_t starting_pixel = row * image->texture->w;
// for (uint32_t col = 0; col < image->texture->h <---- change; col++) {
for (uint32_t col = 0; col < image->texture->w; col++) {
uint32_t pixel = starting_pixel + col;
pixel_stuff[pixel * 4 + 3] = fade;
fade += fade_speed;
}
}
memcpy(image->surface->pixels, pixel_stuff, size);
SDL_UpdateTexture(image->texture, NULL, pixel_stuff, image->pitch);
free(pixel_stuff);
}
Yep that works, haha.
Okay one important thing I completely forgot about is to use pitch when creating a buffer for pixels, so change everywhere
size_t size = 4 * image->texture->w * image->texture->h;
to
size_t size = image->surface->pitch * image->texture->h;
Now lets create an improved version that creates fading from point to point, this is more interesting as we need to come up with algorithm to gradually change alpha from starting point to target point.
To do so we can calculate how far is the current pixel from the starting pixel and change the alpha respectfully, but it won't really work as we need one more thing to be able to calculate the a, this can be the distance between the start and target points.
So lets say (also all the distances will be squared as I don't want to make my cpu feel more filtered word by making it do square rooting)
float dx = (float)target_x - start_x;
float dy = (float)target_y - target_y;
float distance_squared = dx*dx + dy*dy; // if you don't know why is this a distance, go learn some school math
now having the distance we can calculate the pixel fade by
// assume we have pixel_x and pixel_y which are the pixel position
float fade = 255 * ((pixel_x*pixel_x + pixel_y*pixel_y) / distance_squared);
Currently the code looks like this, it doesn't work as intended but I want to show you what it currently does as it creates pretty cool looking textures.
void image_fade(Image *image, uint32_t start_x, uint32_t start_y, uint32_t target_x, uint32_t target_y) {
uint32_t width = image->texture->w;
uint32_t height = image->texture->h;
float dx = (float)target_x - start_x;
float dy = (float)target_y - start_y;
float distance_squared = dx * dx + dy * dy;
if (distance_squared == 0.0f)
return;
size_t size = image->surface->pitch * height;
uint8_t *pixel_stuff = malloc(size);
memcpy(pixel_stuff, image->surface->pixels, size);
for (uint32_t pixel_y = 0; pixel_y < height; pixel_y++) {
uint32_t start_pixel = pixel_y * width;
for (uint32_t pixel_x = 0; pixel_x < width; pixel_x++) {
float fade = 255 *
((pixel_x*pixel_x + pixel_y*pixel_y) / distance_squared);
uint32_t pixel = start_pixel + pixel_x;
pixel_stuff[pixel * 4 + 3] = fade;
}
}
memcpy(image->surface->pixels, pixel_stuff, size);
SDL_UpdateTexture(image->texture, NULL, pixel_stuff, image->pitch);
free(pixel_stuff);
}
So lets run it with this call
image_fade(image, 0, 0, image->texture->w, image->texture->h);
Now lets try to shift the start position
image_fade(image, 1000, 100, image->texture->w, image->texture->h);
Okay now lets keep on making it work as intended, lets try adding a check for the distance if it is in the allowed values
void image_fade(Image *image, uint32_t start_x, uint32_t start_y, uint32_t target_x, uint32_t target_y) {
uint32_t width = image->texture->w;
uint32_t height = image->texture->h;
float dx = (float)target_x - start_x;
float dy = (float)target_y - start_y;
float distance_squared = dx * dx + dy * dy;
if (distance_squared == 0.0f)
return;
size_t size = image->surface->pitch * height;
uint8_t *pixel_stuff = malloc(size);
memcpy(pixel_stuff, image->surface->pixels, size);
for (uint32_t pixel_y = 0; pixel_y < height; pixel_y++) {
uint32_t start_pixel = pixel_y * width;
for (uint32_t pixel_x = 0; pixel_x < width; pixel_x++) {
uint32_t curr_distance = (pixel_x*pixel_x + pixel_y*pixel_y); // also save the distance
if (curr_distance > distance_squared) continue; // this check
uint8_t fade = 100 *
(curr_distance / distance_squared);
uint32_t pixel = start_pixel + pixel_x;
pixel_stuff[pixel * 4 + 3] = fade;
}
}
memcpy(image->surface->pixels, pixel_stuff, size);
SDL_UpdateTexture(image->texture, NULL, pixel_stuff, image->pitch);
free(pixel_stuff);
}
If you read the code, you may have noticed a few more changes, the fade is now uint8_t and I also changed the 255 to 100, so you can see what is wrong with this version, so lets run it.
I will be running the function with this call
image_fade(image, 1000, 100, image->texture->w, image->texture->h);
The first thing you may have noticed is that the fading is a bit round, which is not intended (also it is more visible because of the 100 instead of 255)

If you wander what I want to have here, imagine a rectangle rotated and put on the image, the start part of the rectangle is where the a is 0 and the end part is where the a is 255, and image is gradually fading from the start to the end of the rectangle.
So what exactly is distance_squared here, it shows the distance between the 2 points (I mean between the rectange start and end), to make fading in shape of a rectangle I need to store some value that represents how far is the pixel on the rectangle, the current_distance although shows how far the point is from the (0, 0) instead of the rectangle start position. Also I forgot to mention, the start position and end position are situated on the centers of the sides of our lovely rectangle.
To achieve better result we need to get to know a pretty thing called dot product. It is pretty essential to game devs as it helps to determine collisions between 2 points, here we will use it to determine length to the rectangle side and if the pixel is inside the rectangle or not.
So what is a dot point?
Imagine we have 2 randomly situated points, A and B, each has its own global position (as x and y). We firstly say that these A and B must be vectors (not the shit from cpp) to calculate our magic dot product. So we need for them both to have a common point (which is a start_x and start_y in the function we use), for this example we say that they come from (0, 0), so then we need to construct the vectors, it is pretty simple:
vecA = (xA - x0, yA - y0) and vecB = (xB - x0, yB - y0)
then we calculate the dot shit dot = vecA.x * vecB.x + vecA.y * vecB.y.
After we get the dot idk we can go to the next stage, which is just getting the length from the pixel to the side of the rectangle, so len = dot / distance_squared, the distance_squared is just the length of one of the vectors (because we can construct the same, but for the other vector, so we choose which vector is our main vector, e.g. the one that gives us the length for the distance, in case of rectangles it is the distance between the start and end).
And now when we know the theory we can construct it in the lovely C code:
void image_fade(Image *image, uint32_t start_x, uint32_t start_y, uint32_t target_x, uint32_t target_y) {
uint32_t width = image->texture->w; // yea i am tired of writing image->texture->w/h everywhere
uint32_t height = image->texture->h;
float dx = (float)target_x - start_x; // don't forget that dx is end - start
float dy = (float)target_y - start_y;
float distance_squared = dx * dx + dy * dy; // cool distance between the start and end
if (distance_squared == 0.0f)
return;
size_t size = image->surface->pitch * height;
uint8_t *pixel_stuff = malloc(size);
memcpy(pixel_stuff, image->surface->pixels, size);
for (uint32_t y = 0; y < height; y++) {
uint32_t start_pixel = y * width;
for (uint32_t x = 0; x < width; x++) {
int32_t pixel_local_x = x - start_x;
int32_t pixel_local_y = y - start_y;
float dot = dx * pixel_local_x + dy * pixel_local_y; // cool dot product constructed by me
float t = (dot / distance_squared); // way cooler way to get the length, also I forgot to take away the parentheses
if (t < 0) t = 0; // essential for when the dot is outside of the rectangle, but is is "behind outside"
else if (t > 1) t = 1; // still outside, but now it is "in front outside"
uint8_t fade = 255 * t; // fade i guess
uint32_t pixel = start_pixel + x; // pixel you guess
pixel_stuff[pixel * 4 + 3] = fade; // alpha computer guesses
}
}
memcpy(image->surface->pixels, pixel_stuff, size);
SDL_UpdateTexture(image->texture, NULL, pixel_stuff, image->pitch);
free(pixel_stuff);
}
And now, after a few hours of banging my brain with all the game dev shit to construct the shading, I can test it out!
The function to call this is image_fade(image, 1000, 100, image->texture->w, image->texture->h); if you wonder
Okay and the last thing I want to add is to make an ellipse alpha. To achieve this a lot more achievable goal then the previous one. Okay the function will be void image_fade_ellipse(Image* image, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t fade_distance);
So the code I did in literally one run
void image_fade_ellipse(Image* image, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t fade_distance) {
size_t size = image->surface->pitch * image->texture->h;
uint8_t *pixel_stuff = calloc(size, sizeof(uint8_t));
memcpy(pixel_stuff, image->surface->pixels, size);
uint32_t width = image->surface->w;
uint32_t height = image->surface->h;
uint32_t fade_distance_squared = fade_distance * fade_distance;
uint32_t ellipse_width_squared = w * w;
uint32_t ellipse_height_squared = h * h;
uint32_t ellipse_fade_width_squared = (w+fade_distance) * (w+fade_distance);
uint32_t ellipse_fade_height_squared = (h+fade_distance) * (h+fade_distance);
for (uint32_t pixel_y = 0; pixel_y < height; pixel_y++) {
uint32_t start_pixel = pixel_y * image->texture->w;
for (uint32_t pixel_x = 0; pixel_x < width; pixel_x++) {
uint32_t dx = x - pixel_x;
uint32_t dy = y - pixel_y;
uint32_t pixel = start_pixel + pixel_x;
float F_fade = ((dx*dx) / ellipse_fade_width_squared) + ((dy*dy) / ellipse_fade_height_squared);
if (F_fade > 1) {
pixel_stuff[4 * pixel + 3] = 255;
continue;
}
float F_ellipse = ((dx*dx) / ellipse_width_squared) + ((dy*dy) / ellipse_height_squared);
if (F_ellipse < 0)
continue;
uint32_t distance_squared = dx*dx + dy*dy;
uint8_t fade = 255 * distance_squared / fade_distance_squared;
pixel_stuff[4 * pixel + 3] = fade;
}
}
memcpy(image->surface->pixels, pixel_stuff, size);
SDL_UpdateTexture(image->texture, NULL, pixel_stuff, image->pitch);
free(pixel_stuff);
}
To clear all the questions I will say it doesn't work, but I am too tired to debug it, so just look at the result, somehow it looks amazing.

I have no idea why it works like this, and I am too tired to deal with that shit, so see you soon!






















