टर्मिनल के लिए पाठ आधारित प्रतिपादन / एनीमेशन इंजन
यह परियोजना लोकप्रिय ड्रॉइल परियोजना से प्रेरित थी, जो ब्रेल यूनिकोड वर्णों का उपयोग करके टर्मिनल को आकर्षित करने देती है।
सामान्य ASCII वर्णों की तुलना में ब्रेल वर्णों के साथ ड्राइंग का लाभ सरल है: क्योंकि प्रत्येक "ब्रेल-वर्ण" 2 x 4 = 8
संभावित स्पॉट से बना होता है , हमारे पास 256
संभावित वेरिएंट हैं जो हम प्रति चरित्र आकर्षित कर सकते हैं। ये ब्रेल-पैटर्न बहुत "महीन / चिकनी" ड्राइंग के लिए अनुमति देते हैं।
मेरा कार्यान्वयन एक रेंडरिंग इंजन के साथ भी आता है जो ncurses लाइब्रेरी का उपयोग करके स्क्रीन पर जो कुछ भी खींचा गया है उसे एनिमेट करने की अनुमति देता है। मेरे क्रियान्वयन का उद्देश्य बहुत अधिक प्रदर्शन करने वाले से है:
- स्मृति की न्यूनतम मात्रा का उपयोग करना।
- बहुत अच्छा रनटाइम रहा।
जबकि अभी भी उपयोग करना आसान है।
यहाँ कुछ उदाहरण हैं जो प्रदर्शित करते हैं कि इस पुस्तकालय के साथ क्या किया जा सकता है। इन उदाहरणों में भी पाया जा सकता है examples.c
:
मैं अपनी ग्रिड संरचना के कार्यान्वयन से पहले से ही काफी खुश हूं, जो बहुत कॉम्पैक्ट तरीके से डेटा को स्टोर और एक्सेस करता है। मैं उत्सुक हूं कि क्या रेंडरिंग संरचना के प्रदर्शन में और सुधार किया जा सकता है? मैं पहले से ही केवल वही प्रस्तुत करने की कोशिश कर रहा हूं जो पिछले फ्रेम से बदल गया है, लेकिन शायद मैं इसे और भी अधिक अनुकूलित कर सकता हूं?
इसके अलावा, मैं अनिश्चित हूं अगर मेरा कार्यान्वयन सी-स्टाइल कोडिंग दिशानिर्देशों का अच्छा उपयोग करता है। इसके अतिरिक्त, मैं यह सुनिश्चित करना चाहता हूं कि पुस्तकालय उपयोगकर्ता के अनुकूल हो। इसलिए, मुझे बताएं कि इस लाइब्रेरी के एपीआई से आपको (उपयोगकर्ता के रूप में) क्या कार्यक्षमता की उम्मीद है, और अगर वर्तमान स्थिति में इसका उपयोग करते समय आपको कुछ भी याद है।
ग्रिड
#define _POSIX_C_SOURCE 199309L
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <time.h>
#include "grid.h"
#include "unicode.h"
#include "constants.h"
grid *grid_new(int grid_width, int grid_height)
{
if ((grid_width % 2 != 0) || (grid_height % 4 != 0))
return NULL;
grid *p_grid = calloc(1, sizeof(*p_grid));
p_grid->width = grid_width;
p_grid->height = grid_height;
p_grid->buffer_size = grid_width / group_width * grid_height / group_height;
p_grid->buffer = calloc(p_grid->buffer_size, sizeof(int));
return p_grid;
}
void grid_free(grid *p_grid)
{
free(p_grid->buffer);
free(p_grid);
}
void grid_clear(grid *g)
{
for (int i = 0; i < g->buffer_size; ++i)
{
g->buffer[i] = 0x00;
}
}
void grid_fill(grid *g)
{
for (int i = 0; i < g->buffer_size; ++i)
{
g->buffer[i] = 0xFF;
}
}
void grid_print_buffer(grid *g, char* tag) {
printf(tag);
for (int i = 0; i < g->buffer_size; i++)
{
printf("0x%02x%s", g->buffer[i], i == g->buffer_size - 1 ? "\n" : ",");
}
}
void grid_modify_pixel(grid *g, int x, int y, int value)
{
// ToDo validate coords
int bytes_per_line = g->width / group_width;
int byte_idx = (x / group_width) + (y / group_height) * bytes_per_line;
int bit_idx = (x % group_width) * group_height + (y % group_height);
g->buffer[byte_idx] = (g->buffer[byte_idx] & ~(1 << bit_idx)) | (value << bit_idx);
}
void grid_set_pixel(grid *g, int x, int y)
{
grid_modify_pixel(g, x, y, 1);
}
void grid_unset_pixel(grid *g, int x, int y)
{
grid_modify_pixel(g, x, y, 0);
}
void grid_draw_line(grid *g, int x1, int y1, int x2, int y2)
{
// Bresenham's line algorithm
int x_diff = x1 > x2 ? x1 - x2 : x2 - x1;
int y_diff = y1 > y2 ? y1 - y2 : y2 - y1;
int x_direction = x1 <= x2 ? 1 : -1;
int y_direction = y1 <= y2 ? 1 : -1;
int err = (x_diff > y_diff ? x_diff : -y_diff) / 2;
while (1)
{
grid_set_pixel(g, x1, y1);
if (x1 == x2 && y1 == y2)
{
break;
}
int err2 = err;
if (err2 > -x_diff)
{
err -= y_diff;
x1 += x_direction;
}
if (err2 < y_diff)
{
err += x_diff;
y1 += y_direction;
}
}
}
void grid_draw_triangle(grid *g, int x1, int y1, int x2, int y2, int x3, int y3)
{
// ToDo: Add filling algorithm
grid_draw_line(g, x1, y1, x2, y2);
grid_draw_line(g, x2, y2, x3, y3);
grid_draw_line(g, x3, y3, x1, y1);
}
ग्रिड
#ifndef GRID_H
#define GRID_H
typedef struct
{
int width;
int height;
int buffer_size;
int *buffer;
} grid;
grid *grid_new(int grid_width, int grid_height);
void grid_free(grid *p_grid);
void grid_clear(grid *g);
void grid_fill(grid *g);
void grid_print_buffer(grid *g, char* tag);
void grid_modify_pixel(grid *g, int x, int y, int value);
void grid_set_pixel(grid *g, int x, int y);
void grid_unset_pixel(grid *g, int x, int y);
void grid_draw_line(grid *g, int x1, int y1, int x2, int y2);
void grid_draw_triangle(grid *g, int x1, int y1, int x2, int y2, int x3, int y3);
#endif
रेंडरर ।.c
#include "grid.h"
#include "unicode.h"
#include "renderer.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "constants.h"
#include <ncurses.h>
#include <unistd.h>
#include <locale.h>
render_context* p_render_context;
const int braille_offset = 0x2800;
const int TRANSFORMATION_MATRIX[8] ={ 0x01, 0x02, 0x04, 0x40, 0x08, 0x10, 0x20, 0x80 };
wchar_t lookup_table[256] ={};
void renderer_new(grid *p_grid) {
// Set locale for ncurses to process unicode correctly
setlocale(LC_ALL, "");
// Generate braille lookup table
grid_generate_lookup_table();
// Create copy of initial grid for caching, but zero out buffer
grid *p_cached_grid = calloc(1, sizeof(*p_grid));
p_cached_grid->width = p_grid->width;
p_cached_grid->height = p_grid->height;
p_cached_grid->buffer_size = p_grid->buffer_size;
p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof(int));
// Store cached grid in render_context
p_render_context = calloc(1, sizeof(*p_render_context));
p_render_context->p_cached_grid = p_cached_grid;
p_render_context->frames_rendered = 0;
// Initialize ncurses
initscr();
noecho();
curs_set(0);
}
void renderer_update(grid* p_grid)
{
// Notes:
// Should only render the characters that changed from current grid buffer to the cached one
// Iterate over grid and look for differences to cached_grid
for (int i = 0; i < p_grid->buffer_size; i++)
{
// Difference was found, note that this character must be re-rendered
if (p_grid->buffer[i] != p_render_context->p_cached_grid->buffer[i]) {
// Compute row and column index of the character we need to re-render
int pos_x = i % (p_render_context->p_cached_grid->width / group_width);
int pos_y = i / (p_render_context->p_cached_grid->width / group_width);
// Obtain correct braille character
char uc[5];
int braille = lookup_table[p_grid->buffer[i]];
int_to_unicode_char(braille, uc);
// Linebreak if we reached the right end of the grid
if (i % (p_grid->width / group_width) == 0 && i != 0)
{
printw("\n");
}
// Render the braille character at the position that changed
mvprintw(pos_y, pos_x, uc);
//printw("Change index %i [%i->%i] Rerendering coordinate (%i, %i).\n", i, p_render_context->p_cached_grid->buffer[i], p_grid->buffer[i], pos_x, pos_y);
}
}
// ToDo: Update p_cached_grid
p_render_context->frames_rendered++;
//grid_print_buffer(p_render_context->p_cached_grid, "cached: ");
//grid_print_buffer(p_grid, "current: ");
// Update cached buffer with current one
memcpy(p_render_context->p_cached_grid->buffer, p_grid->buffer, sizeof(int) * p_grid->buffer_size);
// Sleep some milliseconds so that changes are visible to the human eye
napms(render_delay_ms);
// Refresh terminal to render changes
refresh();
}
void renderer_free()
{
// Wait before all allocations are free'd
napms(2000);
// Free all allocations and end ncurses window
free(p_render_context->p_cached_grid->buffer);
free(p_render_context->p_cached_grid);
free(p_render_context);
endwin();
}
void grid_generate_lookup_table()
{
for (int i = 0; i < 256; ++i)
{
int unicode = braille_offset;
for (int j = 0; j < 8; ++j)
{
if (((i & (1 << j)) != 0))
{
unicode += TRANSFORMATION_MATRIX[j];
}
}
lookup_table[i] = unicode;
}
}
रेंडरर.ह
#ifndef RENDERER_H
#define RENDERER_H
#include "grid.h"
typedef struct {
grid* p_cached_grid;
int frames_rendered;
} render_context;
void renderer_new(grid* p_grid);
void renderer_update(grid* p_grid);
void renderer_free();
void grid_generate_lookup_table();
#endif
यूनिकोड
void int_to_unicode_char(unsigned int code, char *chars)
{
if (code <= 0x7F)
{
chars[0] = (code & 0x7F);
chars[1] = '\0';
}
else if (code <= 0x7FF)
{
// one continuation byte
chars[1] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[0] = 0xC0 | (code & 0x1F);
chars[2] = '\0';
}
else if (code <= 0xFFFF)
{
// two continuation bytes
chars[2] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[1] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[0] = 0xE0 | (code & 0xF);
chars[3] = '\0';
}
else if (code <= 0x10FFFF)
{
// three continuation bytes
chars[3] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[2] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[1] = 0x80 | (code & 0x3F);
code = (code >> 6);
chars[0] = 0xF0 | (code & 0x7);
chars[4] = '\0';
}
else
{
// unicode replacement character
chars[2] = 0xEF;
chars[1] = 0xBF;
chars[0] = 0xBD;
chars[3] = '\0';
}
}
यूनिकोड.एच
#ifndef UNICODE_H
#define UNICODE_H
void int_to_unicode_char(unsigned int code, char *chars);
#endif
constants.c
const int group_height = 4;
const int group_width = 2;
const int render_delay_ms = 10;
constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H
extern const int group_height;
extern const int group_width;
extern const int render_delay_ms;
#endif
उदाहरण
#include <math.h>
#include "grid.h"
#include "renderer.h"
#include <stdio.h>
void example_filling_bar()
{
int width = 100;
int height = 24;
grid *g = grid_new(width, height);
renderer_new(g);
// Fill grid from left to right (simple animation)
renderer_update(g);
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
grid_set_pixel(g, i, j);
}
renderer_update(g);
}
// Free allocations
renderer_free();
grid_free(g);
}
void example_build_block()
{
int width = 100;
int height = 40;
grid *g = grid_new(width, height);
renderer_new(g);
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
grid_set_pixel(g, x, y);
renderer_update(g);
}
}
// Free allocations
renderer_free();
grid_free(g);
}
void example_sine_tracking()
{
int width = 124;
int height = 40;
grid *g = grid_new(width, height);
renderer_new(g);
double shift = 0;
while (1)
{
grid_clear(g);
// Draw line
grid_draw_line(g, 0, height / 2, width - 1, (height + sin(shift) * height) / 2);
// Draw curve
for (int j = 0; j < width; j++)
{
grid_set_pixel(g, j, (height / 2 * sin(0.05 * j + shift) + height / 2));
}
// Move curve
shift += 0.05;
renderer_update(g);
}
// Free allocations
renderer_free();
grid_free(g);
}
void example_spiral_effect()
{
int width = 60;
int height = 32;
grid *g = grid_new(width, height);
renderer_new(g);
// Start with an empty grid
grid_clear(g);
int m = width, n = height;
int sr = 0, sc = 0, er = m - 1, ec = n - 1;
while (sr <= er && sc <= ec)
{
for (int i = sc; i <= ec; ++i)
{
grid_set_pixel(g, sr, i);
renderer_update(g);
}
for (int i = sr + 1; i <= er; ++i)
{
grid_set_pixel(g, i, ec);
renderer_update(g);
}
for (int i = ec - 1; sr != er && i >= sc; --i)
{
grid_set_pixel(g, er, i);
renderer_update(g);
}
for (int i = er - 1; sc != ec && i > sr; --i)
{
grid_set_pixel(g, i, sc);
renderer_update(g);
}
sr++, sc++;
er--, ec--;
}
// Free allocations
renderer_free();
grid_free(g);
}
उदाहरण
#ifndef EXAMPLES_H
#define EXAMPLES_H
#include "grid.h"
void example_filling_bar();
void example_build_block();
void example_sine_tracking();
void example_spiral_effect();
#endif
मुख्य
#include <stdio.h>
#include <unistd.h>
#include <math.h>
#include "examples.h"
int main()
{
//example_sine_tracking();
//example_build_block();
example_spiral_effect();
return 0;
}
और अंत में, सब कुछ संकलित करने के लिए मेकफाइल:
prog:
gcc -g -o dots examples.c constants.c grid.c unicode.c renderer.c main.c -Wall -Werror -lncursesw -lm
clean:
rm dots
मैं हर प्रतिक्रिया की सराहना करता हूं! परियोजना GitHub पर भी उपलब्ध है:https://github.com/766F6964/DotDotDot
नोट : इसका परीक्षण करते समय, सुनिश्चित करें कि आपके पास एक टर्मिनल फ़ॉन्ट स्थापित है जो ब्रेल वर्णों को ठीक से प्रदर्शित कर सकता है, अन्यथा यह गड़बड़ दिखाई देगा।
जवाब
बहुत अच्छा कोड।
कुछ पक्ष मुद्दों की थोड़ी समीक्षा।
sizeof *ptr
बनाम sizeof type
sizeof *ptr
2 में से 2 मामलों में कोड का अच्छी तरह से उपयोग किया जाता है।
grid *p_cached_grid = calloc(1, sizeof(*p_grid));
p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof(int)); // why sizeof(int)
p_render_context = calloc(1, sizeof(*p_render_context));
इसे जारी रखने की सलाह देते हैं
// p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof(int));
p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof *(p_cached_grid->buffer));
// or
p_cached_grid->buffer = calloc(p_grid->buffer_size, sizeof p_cached_grid->buffer[0]);
// or other variations.
सरोगेट्स की अनुचित हैंडलिंग
हालांकि इस कोड के लिए महत्वपूर्ण नहीं है , सरोगेट्स का पता लगाने के लिए बेहतर है और शायद एक त्रुटि (यूनिकोड प्रतिस्थापन चरित्र) के रूप में संभालता है।
ब्रेसेनहम की लाइन एल्गोरिदम
सामान्य कार्यान्वयन से बेहतर है।
के लिए इस कोड, कोई समस्या नहीं देखा।
में सामान्य कोड में विफल रहता है जब x1 - x2
या y1 - y2
अतिप्रवाह। unsigned
व्यापक गणित का सहारा लिए बिना अंतर को संभालने के लिए इसका उपयोग करने के तरीके हैं ।
मैं कुछ नमूना कोड पोस्ट करूंगा, लेकिन मेरा रेफरी कोड आज तक नहीं है।
निरंतर नाम का उपयोग करें
आपने परिभाषित किया grid_width
और grid_height
, बहुत अच्छा, लेकिन दुर्भाग्य से आप इसका लगातार उपयोग नहीं कर रहे हैं। में grid_new()
उदाहरण के लिए, पहली पंक्ति के साथ बदला जा सकता है:
if ((grid_width % group_width != 0) || (grid_height % group_height != 0))
इसके अलावा, यह ग्लोबल कॉन्स्टेंट होने का रिवाज है जैसे कि ये सभी CAPS में लिखे गए हैं, इसलिए चर से अलग करना आसान है।
उपयोग करना memset()
आप में छोरों लिखा है grid_clear()
और grid_fill()
, लेकिन आप आसानी से इस कार्य को कर सकते हैं memset(), जो और अधिक अनुकूलित होने की संभावना है। निश्चित रूप से, grid_clear()
करने के लिए फिर से लिखा जा सकता है memset(g->buffer, 0, g->buffer_size * sizeof(*g->buffer))
। यदि g->buffer
ए था uint8_t *
, तो आप memset()
अंदर भी उपयोग कर सकते हैं grid_fill()
।
uint8_t
ग्रिड के लिए उपयोग करें
आप ग्रिड में प्रत्येक वर्ण के लिए केवल 8 बिट्स का उपयोग कर रहे हैं, इसलिए आप इसे एक के uint8_t
बजाय स्टोर कर सकते हैं int
। यह एक कारक 4 द्वारा ग्रिड के मेमोरी उपयोग को कम करता है, और इसमें memset()
उपयोग करने की अनुमति भी देता है grid_fill()
।
लुकअप तालिका को हार्डकोड करने पर विचार करें
आप सोच सकते हैं, यह क्या है? हर कोई जानता है कि आपको हार्डकोडिंग चीजों से बचना चाहिए! लेकिन इस मामले में, यूनिकोड ब्रेल वर्णों को पत्थर में सेट किया गया है, और आप वर्णों को उत्पन्न करने के लिए बहुत सारे कोड बर्बाद कर रहे हैं, और कुछ सीपीयू चक्र हर बार जब आप अपना कार्यक्रम शुरू करते हैं, जब आप बस लिख सकते हैं:
wchar_t lookup_table[256] = L"⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇"
L"⠈⠉⠊⠋⠌⠍⠎⠏... "
...
L" ...⣿";
Ncursesw का उपयोग करने पर विचार करें
wchar_t
अपने आप को UTF-8 स्ट्रिंग से कनवर्ट करने के बजाय , आप उन ncurses के विस्तृत संस्करण का उपयोग कर सकते हैं जो आपको wchar_t
स्ट्रिंग्स को सीधे प्रिंट करने की अनुमति देता है । संस्करण 6 से ncurses के बाद से, यह डिफ़ॉल्ट रूप से शामिल है, और आप mvaddwstr()इसके बजाय उपयोग कर सकते हैं विस्तृत स्ट्रिंग्स मुद्रित करने के लिए mvprintw()
।
ग्रिड को कैशिंग न करने पर विचार करें
Ncurses की एक बड़ी विशेषता यह है कि यह स्क्रीन पर जो कुछ भी है उसे कैश करता है, और जो वास्तव में बदल गया है उसे अपडेट करने के लिए केवल आवश्यक वर्ण और नियंत्रण कोड टर्मिनल को भेजेगा। आप स्वयं भी ऐसा कर रहे हैं, इस प्रकार नक़ल करने वाले क्या कर रहे हैं।
मैं इस अक्षमता से छुटकारा पाने के दो तरीके देखता हूं। सबसे पहले, आप अपने स्वयं के बफ़र्स के साथ पूरी तरह से दूर कर सकते हैं, और बस सीधे शाप कार्यों के साथ स्क्रीन पर लिख सकते हैं। बेशक, यदि आपको ब्रेल वर्ण में एकल डॉट को अपडेट करने की आवश्यकता है, तो आपको यह जानना होगा कि ब्रेल पैटर्न स्क्रीन पर पहले से ही क्या है। आप स्क्रीन की सामग्री को वापस जैसे कमांड के साथ पढ़ सकते हैं mvin_wch()। दोष यह है कि अलग-अलग वर्णों को वापस पढ़ने के परिणामस्वरूप बहुत सारे फ़ंक्शन कॉल हो सकते हैं, और आपको ब्रेल वर्ण को फिर से बिटकोस्क में डिकोड करना होगा।
एक अन्य विकल्प एक एकल बफर रखना है, और बस पूरे बफर को प्रत्येक ताज़ा करने के लिए ncurses को देना है। यदि आपको लगता है कि यह अक्षम है, तो विचार करें कि आप स्वयं हर ताज़ा कैश किए गए बफर को पूरे बफर की नकल कर रहे थे। यदि आप इस तरह से जाते हैं, तो आप संभवतः व्यक्तिगत डॉट्स के आसान हेरफेर के लिए मूल बफ़र चाहते हैं, और दूसरे प्रकार का एक बफ़र wchar_t *
जिसे आप समानांतर में अपडेट करते हैं, और आप एक बार में प्रिंट करने के लिए ncurses को भेज सकते हैं। ध्यान दें, यहाँ भी एक wmemset()सहायक हो सकता है।
मैं दूसरे विकल्प के लिए जाने का सुझाव दूंगा। आपको अपने कोड को बेंचमार्क करना शुरू करना चाहिए ताकि आप इसके प्रदर्शन को निष्पक्ष रूप से माप सकें।