Logo (Spin): adds a logo that turns in step with --watch

The builtin logo, or a text file in logo syntax, drawn as an extruded slab:
the front is the art as it is, the back is the art mirrored, and the side
walls are shaded with block glyphs. Rendering is pure: no terminal, no
input, no timer.

`--logo-type spin` hands every frame to the normal logo pipeline, so the
modules are laid out beside it exactly as they are for any other type, and
the first frame without --watch is byte-identical to the same art printed
as a builtin logo.

The rotation is driven by the `--watch` loop: `ffSpinTick()` runs once per
refresh and `ffLogoPrint()` is called only when it reports a step, so the
logo and the modules can never drift apart, and a build that never uses the
type costs one comparison per refresh.

Speed is set either as degrees per frame (--logo-spin-speed-per-frame,
default 10) or as seconds per revolution (--logo-spin-speed-per-round,
read from the clock and winning over the former when both are given). Both
take negative values to turn the other way, and --logo-spin-angle picks the
frame to start from.
This commit is contained in:
Carter Li
2026-11-04 11:14:11 +08:00
parent 71f68ce0c5
commit 56f4dc00a2
14 changed files with 1199 additions and 7 deletions
+10
View File
@@ -564,6 +564,8 @@ set(LIBFASTFETCH_SRC
src/detection/zpool/zpool.c
src/logo/builtin.c
src/logo/logo.c
src/logo/spin/render.c
src/logo/spin/spin.c
src/modules/modules.c
src/options/display.c
src/options/logo.c
@@ -2368,6 +2370,13 @@ if (BUILD_TESTS)
PRIVATE libfastfetch
)
add_executable(fastfetch-test-spin
tests/spin.c
)
target_link_libraries(fastfetch-test-spin
PRIVATE libfastfetch
)
add_executable(fastfetch-test-strbuf
tests/strbuf.c
)
@@ -2490,6 +2499,7 @@ if (BUILD_TESTS)
enable_testing()
add_test(NAME test-cache COMMAND fastfetch-test-cache)
add_test(NAME test-logo COMMAND fastfetch-test-logo)
add_test(NAME test-spin COMMAND fastfetch-test-spin)
add_test(NAME test-strbuf COMMAND fastfetch-test-strbuf)
add_test(NAME test-list COMMAND fastfetch-test-list)
add_test(NAME test-format COMMAND fastfetch-test-format)
+37
View File
@@ -203,10 +203,47 @@
"iterm": "Image file, rendered using the iTerm graphics protocol",
"chafa": "Image file, printed as ASCII art using libchafa",
"raw": "Image file, printed as raw binary string",
"spin": "Built-in logo (or a text file) drawn as a 3D slab; it turns one step per refresh with `--watch`",
"none": "Disable logo printing"
}
}
},
{
"long": "logo-spin-speed-per-frame",
"desc": "Set how far the spin logo turns per drawn frame, in degrees",
"remark": "Only used by `--logo-type spin`. One revolution takes 360 divided by this value frames, so `--watch` sets the pace; negative turns the other way. 0 means not set, and the default is 10",
"arg": {
"type": "num",
"default": 10
}
},
{
"long": "logo-spin-speed-per-round",
"desc": "Set how many seconds one revolution of the spin logo takes",
"remark": "Only used by `--logo-type spin`. Read from the clock, so a slow refresh turns further instead of slowing the logo down; negative turns the other way. 0 means not set, and this wins over `--logo-spin-speed-per-frame` when both are given",
"arg": {
"type": "num",
"default": 0
}
},
{
"long": "logo-spin-tilt",
"desc": "Set the camera tilt of the spin logo, in degrees",
"remark": "Only used by `--logo-type spin`",
"arg": {
"type": "num",
"default": 0
}
},
{
"long": "logo-spin-angle",
"desc": "Set the rotation of the first spin logo frame, in degrees",
"remark": "Only used by `--logo-type spin`. Without `--watch` the logo is drawn at this angle and stays there. 360 is the same as 0",
"arg": {
"type": "num",
"default": 0
}
},
{
"long": "logo-width",
"desc": "Set the width of the logo (in characters) if it is an image",
+47 -1
View File
@@ -652,6 +652,10 @@
"const": "raw",
"description": "Image file rendered as a raw binary string"
},
{
"const": "spin",
"description": "Built-in logo (or a text file) drawn as a 3D slab, turning one step per `--watch` refresh"
},
{
"const": "none",
"description": "Disable logo printing"
@@ -661,7 +665,49 @@
},
"source": {
"type": "string",
"description": "Path to the logo source file"
"description": "Path to the logo source file. For the spin logo: `auto` (the detected OS), any built-in logo name, or a text file in fastfetch logo syntax"
},
"spin": {
"type": "object",
"additionalProperties": false,
"description": "Options of the spin logo (`logo.type: spin`)",
"properties": {
"speed": {
"type": "object",
"additionalProperties": false,
"description": "How fast the logo turns. 0 means not set for either key; `perRound` wins over `perFrame` when both are given",
"properties": {
"perFrame": {
"type": "number",
"description": "Degrees per drawn frame; a revolution takes 360 / perFrame frames, negative turns the other way. 0 means not set, and 10 is used when neither speed is set",
"minimum": -360,
"maximum": 360,
"default": 0
},
"perRound": {
"type": "number",
"description": "Seconds one revolution takes, measured on the clock, negative turns the other way",
"minimum": -3600,
"maximum": 3600,
"default": 0
}
}
},
"tilt": {
"type": "number",
"description": "Camera tilt in degrees",
"minimum": -80,
"maximum": 80,
"default": 0
},
"angle": {
"type": "number",
"description": "Rotation of the first frame in degrees; 360 is the same as 0. Without `--watch` this is the only frame drawn",
"minimum": 0,
"maximum": 360,
"default": 0
}
}
},
"color": {
"type": "object",
+14
View File
@@ -2,6 +2,7 @@
#include "common/ffdata.h"
#include "detection/version/version.h"
#include "logo/logo.h"
#include "logo/spin/spin.h"
#include "common/FFcache.h"
#include "common/commandoption.h"
#include "common/genconfig.h"
@@ -754,6 +755,12 @@ static void parseConfigFiles(FFdata* data) {
}
}
// `-30`, `-.5`: a negative number is the value of the previous option, not the
// name of the next one (`--logo-spin-tilt -30`).
static bool isNegativeNumber(const char* str) {
return str[0] == '-' && (isdigit((unsigned char) str[1]) || (str[1] == '.' && isdigit((unsigned char) str[2])));
}
static void parseArguments(FFdata* data, int argc, char** argv, void (*parser)(FFdata* data, char* key, char* value)) {
for (int i = 1; i < argc; i++) {
const char* key = argv[i];
@@ -768,6 +775,7 @@ static void parseArguments(FFdata* data, int argc, char** argv, void (*parser)(F
if (i == argc - 1 || (argv[i + 1][0] == '-' && argv[i + 1][1] != '\0' && // `-` is used as an alias for `/dev/stdin`
!ffStrEqualsIgnCase(argv[i], "--separator-string") // Separator string can start with a -
&& !isNegativeNumber(argv[i + 1]) // A negative number is a value
)) {
parser(data, argv[i], nullptr);
} else {
@@ -817,6 +825,12 @@ static void run(FFdata* data) {
fputs("\e[H", stdout); // Move cursor to the top left corner to overwrite the previous output
instance.state.keysHeight = 0; // Reset keysHeight so `ffLogoPrintRemaining` will recalculate it
ffCacheInvalidateAll(); // Drop the detection results cached for the previous round, so that this round re-detects them
// `--logo-type spin`: one rotation step per refresh, so the logo turns in sync with
// the modules instead of owning a timer of its own. A still logo, or any other logo
// type, costs a single `false` here.
if (ffSpinTick()) {
ffLogoPrint();
}
} else {
break;
}
+12
View File
@@ -27,6 +27,17 @@ typedef struct FFLogoLineCacheState {
uint32_t rightOffset;
} FFLogoLineCacheState;
// `--logo-type spin`: kept here instead of in a file-static block so that the
// state lives with the rest of the run and starts clean on every invocation.
typedef struct FFLogoSpinState {
float degrees; // rotation of the frame drawn last
double lastDrawMs; // when that frame was drawn
bool started; // `logo.spin.angle` has been applied
bool printed; // a frame was drawn, so `ffSpinTick` has work to do
uint32_t printedWidth;
uint32_t printedHeight;
} FFLogoSpinState;
typedef struct FFstate {
uint32_t logoWidth;
uint32_t logoHeight;
@@ -35,6 +46,7 @@ typedef struct FFstate {
uint32_t dynamicInterval;
FFPlatform platform;
FFLogoLineCacheState logoLineCache;
FFLogoSpinState logoSpin;
} FFstate;
typedef struct FFinstance {
+12 -6
View File
@@ -8,6 +8,7 @@
#include "detection/os/os.h"
#include "detection/terminalshell/terminalshell.h"
#include "detection/terminalsize/terminalsize.h"
#include "logo/spin/spin.h"
#include <ctype.h>
#include <stdlib.h>
@@ -324,7 +325,7 @@ void ffLogoPrintChars(const char* data, bool doColorReplacement) {
logoLineCacheClear(cache);
}
static void logoApplyColors(const FFlogo* logo, bool replacement) {
void ffLogoApplyColors(const FFlogo* logo, bool replacement) {
if (instance.config.display.colorTitle.length == 0) {
ffStrbufSetStatic(&instance.config.display.colorTitle, logo->colorTitle ?: logo->colors[0]);
}
@@ -439,14 +440,14 @@ static const FFlogo* logoGetBuiltinDetected(FFLogoSize size) {
}
static void logoPrintStruct(const FFlogo* logo) {
logoApplyColors(logo, true);
ffLogoApplyColors(logo, true);
ffLogoPrintChars(logo->lines, true);
}
static void logoPrintNone(void) {
if (!instance.config.display.pipe) {
logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false);
ffLogoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false);
}
instance.state.logoHeight = 0;
instance.state.logoWidth = 0;
@@ -484,7 +485,7 @@ static bool logoPrintData(bool doColorReplacement, FFstrbuf* source) {
return false;
}
logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement);
ffLogoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement);
ffLogoPrintChars(source->chars, doColorReplacement);
return true;
}
@@ -548,7 +549,7 @@ static bool logoPrintFileIfExists(bool doColorReplacement, bool raw) {
return false;
}
logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement);
ffLogoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement);
if (raw) {
return ffLogoPrintCharsRaw(content.chars, content.length, instance.config.display.showErrors);
}
@@ -563,7 +564,7 @@ static bool logoPrintImageIfExists(FFLogoType logo, bool printError) {
return false;
}
logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false);
ffLogoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false);
return true;
}
#endif
@@ -654,6 +655,11 @@ void ffLogoPrint(void) {
return;
}
if (options->type == FF_LOGO_TYPE_SPIN) {
ffSpinLogoPrint();
return;
}
// If the source is not set, we can directly print the detected logo.
if (options->source.length == 0) {
ffLogoPrintDetected(options->type == FF_LOGO_TYPE_SMALL ? FF_LOGO_SIZE_SMALL : FF_LOGO_SIZE_NORMAL);
+2
View File
@@ -38,6 +38,8 @@ void ffLogoBuiltinListAutocompletion(void);
void ffLogoPrintDetected(FFLogoSize size);
const FFlogo* ffLogoGetBuiltinForName(const FFstrbuf* name, FFLogoSize size);
const FFlogo* ffLogoGetBuiltinDetected(FFLogoSize size);
// Fills the `logo.color.N` slots that the user did not set from `logo`.
void ffLogoApplyColors(const FFlogo* logo, bool replacement);
// builtin.c
extern const FFlogo* ffLogoBuiltins[];
+506
View File
@@ -0,0 +1,506 @@
#include "logo/spin/render.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
// Terminal cells are about twice as tall as wide. World units are cell
// heights, so one column is half a unit wide.
#define FF_SPIN_CELL_ASPECT 0.5f
// Camera distance in bounding-sphere radii. Smaller means stronger perspective.
#define FF_SPIN_CAMERA_RADII 4.0f
// Shading for the slab's edges, dark to bright. The faces keep the logo's glyphs.
static const uint32_t FFSpinWallRamp[] = { 0x2591, 0x2592, 0x2593, 0x2588 }; // ░▒▓█
static const float FFSpinLight[3] = { -0.48f, 0.6f, 0.64f }; // unit length, camera space
typedef struct SpinGeometry {
float hx, hy, hz; // half extents of the slab
float camera; // camera distance from the slab center
float focal; // maps the front face 1:1 onto cells at yaw 0
} SpinGeometry;
static SpinGeometry spinGeometry(const FFSpinArt* art) {
SpinGeometry g;
g.hx = (float) art->width * FF_SPIN_CELL_ASPECT * 0.5f;
g.hy = (float) art->height * 0.5f;
g.hz = fmaxf(0.3f, fmaxf(g.hx, g.hy) * 0.055f);
float radius = sqrtf(g.hx * g.hx + g.hy * g.hy + g.hz * g.hz);
g.camera = radius * FF_SPIN_CAMERA_RADII;
g.focal = g.camera - g.hz;
return g;
}
static uint32_t utf8Decode(const char** cursor) {
const unsigned char* s = (const unsigned char*) *cursor;
uint32_t length, glyph;
if (s[0] < 0x80) {
*cursor += 1;
return s[0];
}
if ((s[0] & 0xE0) == 0xC0) {
length = 2;
glyph = s[0] & 0x1F;
} else if ((s[0] & 0xF0) == 0xE0) {
length = 3;
glyph = s[0] & 0x0F;
} else if ((s[0] & 0xF8) == 0xF0) {
length = 4;
glyph = s[0] & 0x07;
} else {
*cursor += 1;
return '?';
}
for (uint32_t i = 1; i < length; ++i) {
if ((s[i] & 0xC0) != 0x80) {
*cursor += i;
return '?';
}
glyph = (glyph << 6) | (s[i] & 0x3F);
}
*cursor += length;
return glyph;
}
static bool glyphIsBlank(uint32_t glyph) {
return glyph <= ' ' || glyph == 0x7F || glyph == 0xA0 || glyph == 0x2800;
}
static uint8_t paletteAdd(FFSpinArt* art, const char* params, uint32_t length) {
if (length == 0 || (length == 1 && params[0] == '0')) {
return 0;
}
for (uint32_t i = FF_SPIN_PALETTE_LOGO_SLOTS; i < art->paletteCount; ++i) {
if (art->palette[i].length == length && memcmp(art->palette[i].chars, params, length) == 0) {
return (uint8_t) i;
}
}
if (art->paletteCount >= FF_SPIN_PALETTE_MAX) {
return 0;
}
ffStrbufSetNS(&art->palette[art->paletteCount], length, params);
return (uint8_t) art->paletteCount++;
}
// Walks fastfetch logo text ($1..$9 colors, `$$`, tabs, SGR escapes). With
// `fill` false it only measures the grid.
static void artWalk(FFSpinArt* art, const char* text, bool fill, uint32_t* outWidth, uint32_t* outHeight) {
uint32_t row = 0, col = 0, width = 0;
uint8_t color = 1; // fastfetch starts every logo in $1
const char* p = text;
while (*p) {
if (*p == '\r') {
++p;
continue;
}
if (*p == '\n') {
if (col > width) width = col;
++row;
col = 0;
++p;
continue;
}
if (*p == '\t') {
col += 4;
++p;
continue;
}
if (*p == '\e') {
if (p[1] != '[') {
++p;
continue;
}
const char* params = p + 2;
const char* end = params;
while ((*end >= '0' && *end <= '9') || *end == ';') {
++end;
}
if (*end == 'm' && fill) {
color = paletteAdd(art, params, (uint32_t) (end - params));
}
p = *end ? end + 1 : end;
continue;
}
if (*p == '$') {
if (p[1] >= '1' && p[1] <= '9') {
color = (uint8_t) (p[1] - '0');
p += 2;
continue;
}
if (p[1] == '$') {
++p; // `$$` is a literal dollar sign
}
}
uint32_t glyph = utf8Decode(&p);
if (fill && !glyphIsBlank(glyph)) {
size_t index = (size_t) row * art->width + col;
art->glyphs[index] = glyph;
art->colors[index] = color;
}
++col;
}
if (col > width) width = col;
if (col > 0) ++row;
*outWidth = width;
*outHeight = row;
}
// Drops blank rows and columns around the art, so it spins around its center.
static bool artCrop(FFSpinArt* art) {
uint32_t minX = UINT32_MAX, minY = UINT32_MAX, maxX = 0, maxY = 0;
for (uint32_t y = 0; y < art->height; ++y) {
for (uint32_t x = 0; x < art->width; ++x) {
if (art->glyphs[(size_t) y * art->width + x] == 0) continue;
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
}
if (minX == UINT32_MAX) {
return false;
}
uint32_t width = maxX - minX + 1, height = maxY - minY + 1;
if (width == art->width && height == art->height) {
return true;
}
uint32_t* glyphs = (uint32_t*) malloc((size_t) width * height * sizeof(uint32_t));
uint8_t* colors = (uint8_t*) malloc((size_t) width * height);
if (!glyphs || !colors) {
free(glyphs);
free(colors);
return false;
}
for (uint32_t y = 0; y < height; ++y) {
size_t from = (size_t) (minY + y) * art->width + minX;
memcpy(glyphs + (size_t) y * width, art->glyphs + from, width * sizeof(uint32_t));
memcpy(colors + (size_t) y * width, art->colors + from, width);
}
free(art->glyphs);
free(art->colors);
art->glyphs = glyphs;
art->colors = colors;
art->width = width;
art->height = height;
return true;
}
bool ffSpinArtParse(FFSpinArt* art, const char* text, const FFstrbuf* colors, uint32_t colorCount) {
memset(art, 0, sizeof(*art));
art->paletteCount = FF_SPIN_PALETTE_LOGO_SLOTS;
for (uint32_t i = 0; colors && i < colorCount && i + 1 < FF_SPIN_PALETTE_LOGO_SLOTS; ++i) {
ffStrbufSet(&art->palette[i + 1], &colors[i]);
}
uint32_t width, height;
artWalk(art, text, false, &width, &height);
if (width == 0 || height == 0) {
return false;
}
art->glyphs = (uint32_t*) calloc((size_t) width * height, sizeof(uint32_t));
art->colors = (uint8_t*) calloc((size_t) width * height, 1);
if (!art->glyphs || !art->colors) {
return false;
}
art->width = width;
art->height = height;
artWalk(art, text, true, &width, &height);
return artCrop(art);
}
void ffSpinArtDestroy(FFSpinArt* art) {
free(art->glyphs);
free(art->colors);
for (uint32_t i = 0; i < FF_SPIN_PALETTE_MAX; ++i) {
ffStrbufDestroy(&art->palette[i]);
}
memset(art, 0, sizeof(*art));
}
bool ffSpinFrameInit(FFSpinFrame* frame, uint32_t width, uint32_t height) {
memset(frame, 0, sizeof(*frame));
if (width == 0 || height == 0) {
return false;
}
frame->glyphs = (uint32_t*) calloc((size_t) width * height, sizeof(uint32_t));
frame->colors = (uint8_t*) calloc((size_t) width * height, 1);
if (!frame->glyphs || !frame->colors) {
ffSpinFrameDestroy(frame);
return false;
}
frame->width = width;
frame->height = height;
return true;
}
void ffSpinFrameDestroy(FFSpinFrame* frame) {
free(frame->glyphs);
free(frame->colors);
memset(frame, 0, sizeof(*frame));
}
static void frameClear(FFSpinFrame* frame) {
memset(frame->glyphs, 0, (size_t) frame->width * frame->height * sizeof(uint32_t));
memset(frame->colors, 0, (size_t) frame->width * frame->height);
}
void ffSpinRotationMatrix(float yaw, float tilt, float out[3][3], float outInverse[3][3]) {
float cy = cosf(yaw), sy = sinf(yaw);
float cx = cosf(tilt), sx = sinf(tilt);
// Rx(tilt) * Ry(yaw): spin around the logo's vertical axis, then lean it.
const float m[3][3] = {
{ cy, 0, sy },
{ sx * sy, cx, -sx * cy },
{ -cx * sy, sx, cx * cy },
};
for (uint32_t i = 0; i < 3; ++i) {
for (uint32_t j = 0; j < 3; ++j) {
out[i][j] = m[i][j];
outInverse[j][i] = m[i][j];
}
}
}
bool ffSpinRotationIsOrthonormal(const float m[3][3], float epsilon) {
for (uint32_t i = 0; i < 3; ++i) {
for (uint32_t j = 0; j < 3; ++j) {
float dot = m[0][i] * m[0][j] + m[1][i] * m[1][j] + m[2][i] * m[2][j];
if (fabsf(dot - (i == j ? 1.0f : 0.0f)) > epsilon) {
return false;
}
}
}
float det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
return fabsf(det - 1.0f) <= epsilon;
}
uint32_t ffSpinMirrorGlyph(uint32_t glyph) {
static const uint32_t pairs[][2] = {
{ '/', '\\' }, { '(', ')' }, { '[', ']' }, { '{', '}' }, { '<', '>' },
{ 'b', 'd' }, { 'p', 'q' },
{ 0x258C, 0x2590 }, { 0x2598, 0x259D }, { 0x2596, 0x2597 }, { 0x259B, 0x259C },
{ 0x2599, 0x259F }, { 0x259A, 0x259E }, { 0x25E2, 0x25E3 }, { 0x25E4, 0x25E5 },
{ 0x2571, 0x2572 }, { 0x250C, 0x2510 }, { 0x2514, 0x2518 }, { 0x251C, 0x2524 },
{ 0x256D, 0x256E }, { 0x2570, 0x256F }, { 0x2554, 0x2557 }, { 0x255A, 0x255D },
};
for (uint32_t i = 0; i < sizeof(pairs) / sizeof(pairs[0]); ++i) {
if (glyph == pairs[i][0]) return pairs[i][1];
if (glyph == pairs[i][1]) return pairs[i][0];
}
if (glyph >= 0x2800 && glyph <= 0x28FF) {
// Braille: swap the left dot column (1,2,3,7) with the right one (4,5,6,8).
uint32_t dots = glyph - 0x2800;
return 0x2800 + (((dots & 0x07) << 3) | ((dots & 0x38) >> 3) | ((dots & 0x40) << 1) | ((dots & 0x80) >> 1));
}
return glyph;
}
void ffSpinFrameSize(const FFSpinArt* art, float tilt, uint32_t* width, uint32_t* height) {
SpinGeometry g = spinGeometry(art);
float maxX = (float) art->width * 0.5f, maxY = (float) art->height * 0.5f;
for (uint32_t step = 0; step < 96; ++step) {
float rot[3][3], inv[3][3];
ffSpinRotationMatrix((float) step * 2.0f * (float) M_PI / 96.0f, tilt, rot, inv);
for (uint32_t corner = 0; corner < 8; ++corner) {
float p[3] = { corner & 1 ? g.hx : -g.hx, corner & 2 ? g.hy : -g.hy, corner & 4 ? g.hz : -g.hz };
float c[3];
for (uint32_t i = 0; i < 3; ++i) {
c[i] = rot[i][0] * p[0] + rot[i][1] * p[1] + rot[i][2] * p[2];
}
float scale = g.focal / (g.camera - c[2]);
maxX = fmaxf(maxX, fabsf(c[0]) * scale / FF_SPIN_CELL_ASPECT);
maxY = fmaxf(maxY, fabsf(c[1]) * scale);
}
}
// A cell is drawn when the ray through its center hits, and the outermost
// cell centers sit half a cell inside the frame edge.
float marginX = ceilf(maxX - (float) art->width * 0.5f - 0.5f);
float marginY = ceilf(maxY - (float) art->height * 0.5f - 0.5f);
*width = art->width + 2 * (marginX > 0 ? (uint32_t) marginX : 0);
*height = art->height + 2 * (marginY > 0 ? (uint32_t) marginY : 0);
}
void ffSpinFramePutArt(FFSpinFrame* frame, const FFSpinArt* art) {
frameClear(frame);
if (frame->width < art->width || frame->height < art->height) {
return;
}
uint32_t left = (frame->width - art->width) / 2;
uint32_t top = (frame->height - art->height) / 2;
for (uint32_t y = 0; y < art->height; ++y) {
size_t to = (size_t) (top + y) * frame->width + left;
size_t from = (size_t) y * art->width;
memcpy(frame->glyphs + to, art->glyphs + from, art->width * sizeof(uint32_t));
memcpy(frame->colors + to, art->colors + from, art->width);
}
}
typedef enum SpinHit {
SPIN_HIT_NONE,
SPIN_HIT_FRONT,
SPIN_HIT_BACK,
SPIN_HIT_WALL,
} SpinHit;
// Casts one ray (object space) through the extruded art. On a hit, `cell` is
// the art cell and `normal` the object-space normal of the surface.
static SpinHit castRay(const FFSpinArt* art, const SpinGeometry* g, const float o[3], const float d[3], uint32_t* cell, float normal[3]) {
const float lo[3] = { -g->hx, -g->hy, -g->hz };
const float hi[3] = { g->hx, g->hy, g->hz };
float tNear = 0.0f, tFar = INFINITY;
int axis = -1;
for (int a = 0; a < 3; ++a) {
if (fabsf(d[a]) < 1e-12f) {
if (o[a] < lo[a] || o[a] > hi[a]) return SPIN_HIT_NONE;
continue;
}
float t1 = (lo[a] - o[a]) / d[a];
float t2 = (hi[a] - o[a]) / d[a];
if (t1 > t2) {
float t = t1;
t1 = t2;
t2 = t;
}
if (t1 > tNear) {
tNear = t1;
axis = a;
}
if (t2 < tFar) tFar = t2;
if (tNear > tFar) return SPIN_HIT_NONE;
}
if (axis < 0) return SPIN_HIT_NONE;
// Grid coordinates: x in columns from the left, y in rows from the top.
const int32_t w = (int32_t) art->width, h = (int32_t) art->height;
float gx = (o[0] + d[0] * tNear + g->hx) / FF_SPIN_CELL_ASPECT;
float gy = g->hy - (o[1] + d[1] * tNear);
float dgx = d[0] / FF_SPIN_CELL_ASPECT;
float dgy = -d[1];
int32_t cx = (int32_t) floorf(gx), cy = (int32_t) floorf(gy);
if (cx < 0) cx = 0;
if (cx >= w) cx = w - 1;
if (cy < 0) cy = 0;
if (cy >= h) cy = h - 1;
normal[0] = normal[1] = normal[2] = 0.0f;
if (art->glyphs[(size_t) cy * art->width + (uint32_t) cx]) {
*cell = (uint32_t) cy * art->width + (uint32_t) cx;
normal[axis] = d[axis] > 0 ? -1.0f : 1.0f;
if (axis == 2) return d[2] < 0 ? SPIN_HIT_FRONT : SPIN_HIT_BACK;
return SPIN_HIT_WALL;
}
// Walk the grid cell by cell (Amanatides-Woo) until the ray leaves the slab.
int32_t stepX = dgx > 0 ? 1 : -1, stepY = dgy > 0 ? 1 : -1;
float tMaxX = dgx != 0 ? tNear + ((float) (dgx > 0 ? cx + 1 : cx) - gx) / dgx : INFINITY;
float tMaxY = dgy != 0 ? tNear + ((float) (dgy > 0 ? cy + 1 : cy) - gy) / dgy : INFINITY;
float tDeltaX = dgx != 0 ? fabsf(1.0f / dgx) : INFINITY;
float tDeltaY = dgy != 0 ? fabsf(1.0f / dgy) : INFINITY;
while (true) {
bool alongX = tMaxX < tMaxY;
if ((alongX ? tMaxX : tMaxY) > tFar) return SPIN_HIT_NONE;
if (alongX) {
cx += stepX;
tMaxX += tDeltaX;
if (cx < 0 || cx >= w) return SPIN_HIT_NONE;
} else {
cy += stepY;
tMaxY += tDeltaY;
if (cy < 0 || cy >= h) return SPIN_HIT_NONE;
}
if (art->glyphs[(size_t) cy * art->width + (uint32_t) cx]) {
*cell = (uint32_t) cy * art->width + (uint32_t) cx;
if (alongX) {
normal[0] = (float) -stepX;
} else {
normal[1] = (float) stepY; // grid rows grow downward
}
return SPIN_HIT_WALL;
}
}
}
void ffSpinRenderArt(const FFSpinArt* art, float yaw, float tilt, FFSpinFrame* frame) {
frameClear(frame);
SpinGeometry g = spinGeometry(art);
float rot[3][3], inv[3][3];
ffSpinRotationMatrix(yaw, tilt, rot, inv);
const float origin[3] = { inv[0][2] * g.camera, inv[1][2] * g.camera, inv[2][2] * g.camera };
const uint32_t rampLast = (uint32_t) (sizeof(FFSpinWallRamp) / sizeof(FFSpinWallRamp[0])) - 1;
for (uint32_t y = 0; y < frame->height; ++y) {
float v = (float) frame->height * 0.5f - ((float) y + 0.5f);
for (uint32_t x = 0; x < frame->width; ++x) {
float u = ((float) x + 0.5f - (float) frame->width * 0.5f) * FF_SPIN_CELL_ASPECT;
float d[3];
for (uint32_t i = 0; i < 3; ++i) {
d[i] = inv[i][0] * u + inv[i][1] * v - inv[i][2] * g.focal;
}
uint32_t cell = 0;
float n[3];
SpinHit hit = castRay(art, &g, origin, d, &cell, n);
if (hit == SPIN_HIT_NONE) continue;
size_t index = (size_t) y * frame->width + x;
frame->colors[index] = art->colors[cell];
if (hit == SPIN_HIT_FRONT) {
frame->glyphs[index] = art->glyphs[cell];
} else if (hit == SPIN_HIT_BACK) {
frame->glyphs[index] = ffSpinMirrorGlyph(art->glyphs[cell]);
} else {
float diffuse = 0.0f;
for (uint32_t i = 0; i < 3; ++i) {
diffuse += (rot[i][0] * n[0] + rot[i][1] * n[1] + rot[i][2] * n[2]) * FFSpinLight[i];
}
float luminance = 0.15f + 0.85f * fmaxf(diffuse, 0.0f);
frame->glyphs[index] = FFSpinWallRamp[(uint32_t) (luminance * (float) rampLast + 0.5f)];
}
}
}
}
static void appendSgr(FFstrbuf* out, const FFSpinArt* art, uint8_t color, bool bold) {
ffStrbufAppendS(out, bold ? "\e[0;1" : "\e[0");
if (color < art->paletteCount && art->palette[color].length > 0) {
ffStrbufAppendC(out, ';');
ffStrbufAppend(out, &art->palette[color]);
}
ffStrbufAppendC(out, 'm');
}
void ffSpinAppendCells(FFstrbuf* out, const FFSpinFrame* frame, const FFSpinArt* art, uint32_t index, uint32_t count, bool color, bool bold) {
int32_t active = -1;
for (uint32_t i = index; i < index + count; ++i) {
uint32_t glyph = frame->glyphs[i];
if (glyph == 0) {
ffStrbufAppendC(out, ' ');
continue;
}
if (color && (int32_t) frame->colors[i] != active) {
active = frame->colors[i];
appendSgr(out, art, frame->colors[i], bold);
}
ffStrbufAppendUtf32CodePoint(out, glyph);
}
if (active >= 0) {
ffStrbufAppendS(out, "\e[0m");
}
}
void ffSpinFrameToAnsi(const FFSpinFrame* frame, const FFSpinArt* art, bool color, bool bold, bool trimRight, FFstrbuf* out) {
for (uint32_t y = 0; y < frame->height; ++y) {
uint32_t length = frame->width;
if (trimRight) {
// Empty cells at the end of a line are not part of the art. Printing
// them would pad every line to the frame width and make the output
// differ from the other text logo types.
while (length > 0 && frame->glyphs[(size_t) y * frame->width + length - 1] == 0) {
--length;
}
}
ffSpinAppendCells(out, frame, art, y * frame->width, length, color, bold);
ffStrbufAppendC(out, '\n');
}
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "common/FFstrbuf.h"
#include <stdbool.h>
#include <stdint.h>
#define FF_SPIN_PALETTE_MAX 32
// Palette slots 1..9 hold the logo's $1..$9 colors; raw SGR sequences found in
// custom logo files get the slots after that. Slot 0 is the terminal default.
#define FF_SPIN_PALETTE_LOGO_SLOTS 10
// A logo's ASCII art as a grid of cells. The renderer extrudes it into a slab.
typedef struct FFSpinArt {
uint32_t width;
uint32_t height;
uint32_t* glyphs; // Unicode code point per cell, 0 = empty
uint8_t* colors; // palette index per cell
uint32_t paletteCount;
FFstrbuf palette[FF_SPIN_PALETTE_MAX]; // SGR parameters, e.g. "1;34"
} FFSpinArt;
typedef struct FFSpinFrame {
uint32_t width;
uint32_t height;
uint32_t* glyphs;
uint8_t* colors;
} FFSpinFrame;
// `colors` holds the logo's $1..$9 colors (may be nullptr). Returns false for
// empty art or when memory runs out.
bool ffSpinArtParse(FFSpinArt* art, const char* text, const FFstrbuf* colors, uint32_t colorCount);
void ffSpinArtDestroy(FFSpinArt* art);
bool ffSpinFrameInit(FFSpinFrame* frame, uint32_t width, uint32_t height);
void ffSpinFrameDestroy(FFSpinFrame* frame);
// Frame size that keeps the art inside the frame at every yaw for this tilt.
// The art is centered; the extra space is the same on both sides.
void ffSpinFrameSize(const FFSpinArt* art, float tilt, uint32_t* width, uint32_t* height);
// Copies the art face-on into the center of the frame, exactly as authored.
void ffSpinFramePutArt(FFSpinFrame* frame, const FFSpinArt* art);
void ffSpinRenderArt(const FFSpinArt* art, float yaw, float tilt, FFSpinFrame* frame);
void ffSpinRotationMatrix(float yaw, float tilt, float out[3][3], float outInverse[3][3]);
bool ffSpinRotationIsOrthonormal(const float matrix[3][3], float epsilon);
uint32_t ffSpinMirrorGlyph(uint32_t glyph);
// Appends `count` cells starting at `index` with colors, ending in a reset.
void ffSpinAppendCells(FFstrbuf* out, const FFSpinFrame* frame, const FFSpinArt* art, uint32_t index, uint32_t count, bool color, bool bold);
// One line per frame row, each ending in '\n'. `trimRight` drops the empty cells at the end of a
// row, which is what the other text logo types print; a frame that is redrawn as it turns must
// keep its full width instead, or the information column would move with the angle.
void ffSpinFrameToAnsi(const FFSpinFrame* frame, const FFSpinArt* art, bool color, bool bold, bool trimRight, FFstrbuf* out);
+167
View File
@@ -0,0 +1,167 @@
#include "logo/spin/spin.h"
#include "common/io.h"
#include "common/strutil.h"
#include "common/time.h"
#include "logo/logo.h"
#include "logo/spin/render.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Degrees per frame when neither speed option is set.
#define SPIN_DEFAULT_DEGREES_PER_FRAME 10.0f
// One step of the turn. `logo.spin.speedPerRound` wins over
// `logo.spin.speedPerFrame` when both are set; 0 means unset for either.
static float spinStepDegrees(void) {
const FFOptionsLogoSpin* spin = &instance.config.logo.spin;
if (spin->speedPerRound != 0.0f) {
// Seconds per revolution: follow the clock instead of counting frames,
// so the turn keeps its pace even when a refresh takes longer than the
// interval asked for.
FFLogoSpinState* state = &instance.state.logoSpin;
const double now = ffTimeGetTick();
const double elapsedMs = now - state->lastDrawMs;
state->lastDrawMs = now;
return (float) (360.0 / (double) spin->speedPerRound * elapsedMs / 1000.0);
}
if (spin->speedPerFrame != 0.0f) {
return spin->speedPerFrame;
}
return SPIN_DEFAULT_DEGREES_PER_FRAME;
}
static bool spinIsAuto(const FFstrbuf* source) {
return source->length == 0 || ffStrbufIgnCaseEqualS(source, "auto");
}
static const FFlogo* spinBuiltin(const FFstrbuf* source, FFLogoSize size) {
return spinIsAuto(source) ? ffLogoGetBuiltinDetected(size) : ffLogoGetBuiltinForName(source, size);
}
static bool spinLoadFile(const FFstrbuf* source, FFstrbuf* text) {
FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
if (ffPathExpandEnv(source->chars, &path) && ffAppendFileBuffer(path.chars, text)) {
return text->length > 0;
}
return ffAppendFileBuffer(source->chars, text) && text->length > 0;
}
void ffSpinLogoPrint(void) {
FFOptionsLogo* options = &instance.config.logo;
FFLogoSpinState* state = &instance.state.logoSpin;
instance.state.logoWidth = 0;
instance.state.logoHeight = 0;
if (!state->started) {
// The first frame starts where `logo.spin.angle` says, and every
// `--watch` refresh turns it by one step.
state->started = true;
state->degrees = fmodf(options->spin.angle, 360.0f);
if (state->degrees < 0.0f) {
state->degrees += 360.0f;
}
}
const float spinAngle = state->degrees * (float) M_PI / 180.0f;
// The source is the detected OS (`auto`), a builtin logo name, or a file
// in fastfetch's logo syntax, like the other text logo types.
FF_STRBUF_AUTO_DESTROY fileText = ffStrbufCreate();
const FFlogo* logo = spinBuiltin(&options->source, FF_LOGO_SIZE_NORMAL);
if (!logo) {
logo = spinBuiltin(&options->source, FF_LOGO_SIZE_UNKNOWN);
}
if (!logo && !spinLoadFile(&options->source, &fileText)) {
if (instance.config.display.showErrors) {
fprintf(stderr, "Logo (spin): no builtin logo or readable file named %s\n", options->source.chars);
}
}
// `logo.color.N` on top of the logo's own colors; a file logo borrows the
// detected logo's palette, as the other text logo types do.
ffLogoApplyColors(logo ? logo : ffLogoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), true);
FFSpinArt art = {};
if (!ffSpinArtParse(&art, logo ? logo->lines : fileText.chars, options->colors, FASTFETCH_LOGO_MAX_COLORS)) {
ffSpinArtDestroy(&art); // parse may have allocated before it gave up
return;
}
const float tilt = options->spin.tilt * (float) M_PI / 180.0f;
uint32_t width = art.width;
uint32_t height = art.height;
// With `--watch` the frame has to stay the same size from the first draw on,
// or the information column would move when the logo starts turning.
const bool animated = instance.state.dynamicInterval > 0;
if (animated || spinAngle != 0.0f || tilt != 0.0f) {
// A turned slab sticks out of the art's box, so the frame has to make
// room for its widest angle or the logo would be clipped.
ffSpinFrameSize(&art, tilt, &width, &height);
}
FFSpinFrame frame = {};
if (!ffSpinFrameInit(&frame, width, height)) {
ffSpinArtDestroy(&art);
return;
}
if (spinAngle == 0.0f && tilt == 0.0f) {
ffSpinFramePutArt(&frame, &art); // face on: exactly the original art
} else {
ffSpinRenderArt(&art, spinAngle, tilt, &frame);
}
// Hand the frame to the upstream logo pipeline: it pads, positions and
// measures the art, and its line cache is what puts the modules next to
// the logo. `false`: the frame already carries its own SGR sequences.
FF_STRBUF_AUTO_DESTROY ansi = ffStrbufCreateA(width * height * 4);
ffSpinFrameToAnsi(&frame, &art, !instance.config.display.pipe, instance.config.display.brightColor, !animated, &ansi);
// The serialiser ends every row with a newline; the last one would add an
// empty line to the logo and change the output from the other logo types.
if (ansi.length > 0 && ansi.chars[ansi.length - 1] == '\n') {
ansi.chars[--ansi.length] = '\0';
}
// A resize changes the frame size, and the cells outside the new frame
// would keep showing the previous one.
if (state->printed && (width != state->printedWidth || height != state->printedHeight)) {
fputs("\e[2J\e[H", stdout);
}
// `ffLogoPrintChars` writes the art with write(2) for `--logo-position top`,
// while the caller's cursor moves go through stdio; flush so the art lands
// where the cursor was just moved to.
fflush(stdout);
ffLogoPrintChars(ansi.chars, false);
ffSpinFrameDestroy(&frame);
ffSpinArtDestroy(&art);
state->printed = true;
state->printedWidth = width;
state->printedHeight = height;
state->lastDrawMs = ffTimeGetTick(); // start measuring the next step from now
}
bool ffSpinTick(void) {
FFLogoSpinState* state = &instance.state.logoSpin;
if (!state->printed) {
return false;
}
const float step = spinStepDegrees();
if (step == 0.0f) {
return false; // a logo that does not turn does not have to be redrawn
}
// The angle is wrapped instead of growing, or a long `--watch` session
// would lose precision in the sine and cosine.
float degrees = state->degrees + step;
state->degrees = degrees - floorf(degrees / 360.0f) * 360.0f;
return true;
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
// The spin logo: the builtin logo (or a text file in logo syntax) drawn as an
// extruded slab. It has no timer and reads no input of its own; the `--watch`
// loop calls `ffSpinTick` once per refresh, which is what makes it turn.
// Draws the current frame. Called from `ffLogoPrint`.
void ffSpinLogoPrint(void);
// Advances the rotation by one `--watch` refresh and returns true when the logo
// has to be drawn again, so a still logo or any other logo type costs a single
// comparison per refresh. The step is `logo.spin.speedPerFrame` degrees, or
// whatever `logo.spin.speedPerRound` makes of the time since the last frame.
bool ffSpinTick(void);
+88
View File
@@ -23,6 +23,22 @@ static bool logoTypeIsImage(FFLogoType type) {
}
#endif
// The numeric `--logo-spin-*` options: a plain number with a range, so a typo is
// rejected instead of producing a nonsensical frame.
static float logoParseFloat(const char* key, const char* value, float min, float max) {
if (value == nullptr) {
fprintf(stderr, "Error: usage: %s <number>\n", key);
exit(477);
}
char* end = nullptr;
double parsed = strtod(value, &end);
if (end == value || *end != '\0' || parsed < min || parsed > max) {
fprintf(stderr, "Error: %s must be a number between %g and %g\n", key, min, max);
exit(477);
}
return (float) parsed;
}
// --sixel, --kitty, --kitty-direct, --kitty-icat and --iterm. They keep a branch of their own in
// every build so that the error a build without image logos gives names the flag that was used,
// rather than it being rejected as an unknown option. --chafa is not among them: it is gated on
@@ -70,6 +86,10 @@ void ffOptionsInitLogo(FFOptionsLogo* options) {
options->cache = FF_LOGO_CACHE_ON;
options->position = FF_LOGO_POSITION_LEFT;
options->animationFrame = FF_LOGO_ANIMATION_FRAME_FIRST;
options->spin.speedPerFrame = 0.0f; // 0: not set, see `ffSpinTick`
options->spin.speedPerRound = 0.0f; // 0: not set, see `ffSpinTick`
options->spin.tilt = 0.0f;
options->spin.angle = 0.0f;
#if FF_HAVE_CHAFA
options->chafaFgOnly = false;
@@ -118,6 +138,7 @@ bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, cons
{ "iterm", FF_LOGO_TYPE_IMAGE_ITERM },
{ "chafa", FF_LOGO_TYPE_IMAGE_CHAFA },
{ "raw", FF_LOGO_TYPE_IMAGE_RAW },
{ "spin", FF_LOGO_TYPE_SPIN },
{ "none", FF_LOGO_TYPE_NONE },
{},
});
@@ -184,6 +205,14 @@ bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, cons
});
} else if (ffStrEqualsIgnCase(subKey, "animation-frame")) {
options->animationFrame = ffOptionParseInt32(key, value);
} else if (ffStrEqualsIgnCase(subKey, "spin-speed-per-frame")) {
options->spin.speedPerFrame = logoParseFloat(key, value, -360.0f, 360.0f);
} else if (ffStrEqualsIgnCase(subKey, "spin-speed-per-round")) {
options->spin.speedPerRound = logoParseFloat(key, value, -3600.0f, 3600.0f);
} else if (ffStrEqualsIgnCase(subKey, "spin-tilt")) {
options->spin.tilt = logoParseFloat(key, value, -80.0f, 80.0f);
} else if (ffStrEqualsIgnCase(subKey, "spin-angle")) {
options->spin.angle = logoParseFloat(key, value, 0.0f, 360.0f);
} else {
return false;
}
@@ -323,6 +352,7 @@ const char* ffOptionsParseLogoJsonConfig(FFOptionsLogo* options, yyjson_val* roo
{ "iterm", FF_LOGO_TYPE_IMAGE_ITERM },
{ "chafa", FF_LOGO_TYPE_IMAGE_CHAFA },
{ "raw", FF_LOGO_TYPE_IMAGE_RAW },
{ "spin", FF_LOGO_TYPE_SPIN },
{ "none", FF_LOGO_TYPE_NONE },
{},
});
@@ -436,6 +466,50 @@ const char* ffOptionsParseLogoJsonConfig(FFOptionsLogo* options, yyjson_val* roo
}
options->animationFrame = (int32_t) yyjson_get_sint(val);
continue;
} else if (unsafe_yyjson_equals_str(key, "spin")) {
if (!yyjson_is_obj(val)) {
return "Property 'logo.spin' must be an object";
}
yyjson_val *spinKey, *spinVal;
size_t spinIdx, spinMax;
yyjson_obj_foreach (val, spinIdx, spinMax, spinKey, spinVal) {
if (unsafe_yyjson_equals_str(spinKey, "speed")) {
if (!yyjson_is_obj(spinVal)) {
return "Property 'logo.spin.speed' must be an object";
}
yyjson_val *speedKey, *speedVal;
size_t speedIdx, speedMax;
yyjson_obj_foreach (spinVal, speedIdx, speedMax, speedKey, speedVal) {
if (unsafe_yyjson_equals_str(speedKey, "perFrame")) {
if (!yyjson_is_num(speedVal) || yyjson_get_num(speedVal) < -360 || yyjson_get_num(speedVal) > 360) {
return "Property 'logo.spin.speed.perFrame' must be a number between -360 and 360";
}
options->spin.speedPerFrame = (float) yyjson_get_num(speedVal);
} else if (unsafe_yyjson_equals_str(speedKey, "perRound")) {
if (!yyjson_is_num(speedVal) || yyjson_get_num(speedVal) < -3600 || yyjson_get_num(speedVal) > 3600) {
return "Property 'logo.spin.speed.perRound' must be a number between -3600 and 3600";
}
options->spin.speedPerRound = (float) yyjson_get_num(speedVal);
} else {
return "Unknown logo.spin.speed key";
}
}
} else if (unsafe_yyjson_equals_str(spinKey, "tilt")) {
if (!yyjson_is_num(spinVal) || yyjson_get_num(spinVal) < -80 || yyjson_get_num(spinVal) > 80) {
return "Property 'logo.spin.tilt' must be a number between -80 and 80";
}
options->spin.tilt = (float) yyjson_get_num(spinVal);
} else if (unsafe_yyjson_equals_str(spinKey, "angle")) {
if (!yyjson_is_num(spinVal) || yyjson_get_num(spinVal) < 0 || yyjson_get_num(spinVal) > 360) {
return "Property 'logo.spin.angle' must be a number between 0 and 360";
}
options->spin.angle = (float) yyjson_get_num(spinVal);
} else {
return "Unknown logo.spin key";
}
}
continue;
} else if (unsafe_yyjson_equals_str(key, "chafa")) {
#if FF_HAVE_CHAFA
if (!yyjson_is_obj(val)) {
@@ -568,6 +642,9 @@ void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) {
case FF_LOGO_TYPE_IMAGE_RAW:
yyjson_mut_obj_add_str(doc, obj, "type", "raw");
break;
case FF_LOGO_TYPE_SPIN:
yyjson_mut_obj_add_str(doc, obj, "type", "spin");
break;
default:
yyjson_mut_obj_add_str(doc, obj, "type", "auto");
break;
@@ -575,6 +652,17 @@ void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) {
yyjson_mut_obj_add_str(doc, obj, "source", options->source.chars);
if (options->type == FF_LOGO_TYPE_SPIN) {
yyjson_mut_val* spin = yyjson_mut_obj(doc);
yyjson_mut_val* speed = yyjson_mut_obj(doc);
yyjson_mut_obj_add_real(doc, speed, "perFrame", options->spin.speedPerFrame);
yyjson_mut_obj_add_real(doc, speed, "perRound", options->spin.speedPerRound);
yyjson_mut_obj_add_val(doc, spin, "speed", speed);
yyjson_mut_obj_add_real(doc, spin, "tilt", options->spin.tilt);
yyjson_mut_obj_add_real(doc, spin, "angle", options->spin.angle);
yyjson_mut_obj_add_val(doc, obj, "spin", spin);
}
{
yyjson_mut_val* color = yyjson_mut_obj(doc);
for (int i = 0; i < FASTFETCH_LOGO_MAX_COLORS; i++) {
+9
View File
@@ -21,9 +21,17 @@ typedef enum FFLogoType: uint8_t {
FF_LOGO_TYPE_IMAGE_ITERM, // image file, printed as iterm graphics protocol
FF_LOGO_TYPE_IMAGE_CHAFA, // image file, printed as ascii art using libchafa
FF_LOGO_TYPE_IMAGE_RAW, // image file, printed as raw binary string
FF_LOGO_TYPE_SPIN, // builtin logo (or a text file) drawn as a 3D slab, one turn step per `--watch` refresh
FF_LOGO_TYPE_NONE, // `--logo none`, but still applies colors to the system information output (unless `--pipe` is set)
} FFLogoType;
typedef struct FFOptionsLogoSpin {
float speedPerFrame; // degrees the logo turns per drawn frame; negative turns the other way, 0: not set
float speedPerRound; // seconds one revolution takes; negative turns the other way, 0: not set (wins over speedPerFrame)
float tilt; // camera tilt in degrees
float angle; // rotation of the first frame in degrees; drawn as is without `--watch`
} FFOptionsLogoSpin;
typedef enum FFLogoPosition: uint8_t {
FF_LOGO_POSITION_LEFT,
FF_LOGO_POSITION_TOP,
@@ -47,6 +55,7 @@ typedef struct FFOptionsLogo {
FFstrbuf source;
FFLogoType type;
FFLogoPosition position;
FFOptionsLogoSpin spin;
FFstrbuf colors[FASTFETCH_LOGO_MAX_COLORS];
uint32_t width;
uint32_t height;
+224
View File
@@ -0,0 +1,224 @@
#include "fastfetch.h"
#include "logo/logo.h"
#include "logo/spin/render.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// Unlike assert(), this also checks in release builds.
#define VERIFY(expression) \
do { \
if (!(expression)) { \
fprintf(stderr, "spin test failed [%d]: %s\n", __LINE__, #expression); \
exit(1); \
} \
} while (0)
static bool isWallGlyph(uint32_t glyph) {
return glyph >= 0x2591 && glyph <= 0x2593 ? true : glyph == 0x2588; // ░▒▓█
}
static void testRotation(void) {
const float angles[] = { 0.0f, 0.25f, (float) M_PI / 2.0f, (float) M_PI, 5.0f };
const float tilts[] = { 0.0f, 0.2f, -0.4f, 1.0f };
for (uint32_t i = 0; i < ARRAY_SIZE(angles); ++i) {
for (uint32_t j = 0; j < ARRAY_SIZE(tilts); ++j) {
float rotation[3][3], inverse[3][3];
ffSpinRotationMatrix(angles[i], tilts[j], rotation, inverse);
VERIFY(ffSpinRotationIsOrthonormal(rotation, 1e-4f));
for (uint32_t r = 0; r < 3; ++r) {
for (uint32_t c = 0; c < 3; ++c) {
VERIFY(fabsf(rotation[r][c] - inverse[c][r]) < 1e-5f);
}
}
}
}
}
static void testParse(void) {
FFstrbuf colors[2];
ffStrbufInitStatic(&colors[0], "31");
ffStrbufInitStatic(&colors[1], "1;34");
FFSpinArt art;
// Leading blank rows and columns are cropped; `$$` is a literal dollar.
VERIFY(ffSpinArtParse(&art, "\n $1ab\n $2c$$\xe2\x96\x88\n", colors, 2));
VERIFY(art.width == 3 && art.height == 2);
VERIFY(art.glyphs[0] == 'a' && art.colors[0] == 1);
VERIFY(art.glyphs[1] == 'b' && art.colors[1] == 1);
VERIFY(art.glyphs[2] == 0);
VERIFY(art.glyphs[3] == 'c' && art.colors[3] == 2);
VERIFY(art.glyphs[4] == '$');
VERIFY(art.glyphs[5] == 0x2588);
VERIFY(ffStrbufEqualS(&art.palette[2], "1;34"));
ffSpinArtDestroy(&art);
// Raw SGR escapes in custom logo files get their own palette slots.
VERIFY(ffSpinArtParse(&art, "\e[38;5;208mx\e[0my", nullptr, 0));
VERIFY(art.width == 2 && art.height == 1);
VERIFY(art.colors[0] >= FF_SPIN_PALETTE_LOGO_SLOTS);
VERIFY(ffStrbufEqualS(&art.palette[art.colors[0]], "38;5;208"));
VERIFY(art.colors[1] == 0);
ffSpinArtDestroy(&art);
VERIFY(!ffSpinArtParse(&art, " \n \t \n", nullptr, 0));
ffSpinArtDestroy(&art);
VERIFY(!ffSpinArtParse(&art, "", nullptr, 0));
ffSpinArtDestroy(&art);
}
static void testMirror(void) {
VERIFY(ffSpinMirrorGlyph('/') == '\\');
VERIFY(ffSpinMirrorGlyph('\\') == '/');
VERIFY(ffSpinMirrorGlyph('(') == ')');
VERIFY(ffSpinMirrorGlyph('o') == 'o');
VERIFY(ffSpinMirrorGlyph(0x2801) == 0x2808); // braille dot 1 -> dot 4
VERIFY(ffSpinMirrorGlyph(0x2840) == 0x2880); // braille dot 7 -> dot 8
for (uint32_t glyph = 0x2800; glyph <= 0x28FF; ++glyph) {
VERIFY(ffSpinMirrorGlyph(ffSpinMirrorGlyph(glyph)) == glyph);
}
}
static const char testLogo[] =
"$1 /\\\n"
"$1 / \\\n"
"$1 /\\ \\\n"
"$2 / __ \\\n"
"$2/__/ \\__\\";
static void testRenderFaces(void) {
FFstrbuf colors[2];
ffStrbufInitStatic(&colors[0], "36");
ffStrbufInitStatic(&colors[1], "34");
FFSpinArt art;
VERIFY(ffSpinArtParse(&art, testLogo, colors, 2));
uint32_t width, height;
ffSpinFrameSize(&art, 0.0f, &width, &height);
VERIFY(width >= art.width && height >= art.height);
VERIFY((width - art.width) % 2 == 0 && (height - art.height) % 2 == 0);
uint32_t left = (width - art.width) / 2, top = (height - art.height) / 2;
FFSpinFrame frame;
VERIFY(ffSpinFrameInit(&frame, width, height));
// Face-on, every cell of the art shows up unchanged at its place.
ffSpinRenderArt(&art, 0.0f, 0.0f, &frame);
for (uint32_t y = 0; y < art.height; ++y) {
for (uint32_t x = 0; x < art.width; ++x) {
uint32_t glyph = art.glyphs[y * art.width + x];
if (!glyph) continue;
size_t index = (size_t) (top + y) * width + left + x;
VERIFY(frame.glyphs[index] == glyph);
VERIFY(frame.colors[index] == art.colors[y * art.width + x]);
}
}
// Turned around, the back face shows the art mirrored, glyphs included.
ffSpinRenderArt(&art, (float) M_PI, 0.0f, &frame);
for (uint32_t y = 0; y < art.height; ++y) {
for (uint32_t x = 0; x < art.width; ++x) {
uint32_t glyph = art.glyphs[y * art.width + x];
if (!glyph) continue;
size_t index = (size_t) (top + y) * width + left + (art.width - 1 - x);
VERIFY(frame.glyphs[index] == ffSpinMirrorGlyph(glyph));
}
}
// Edge-on, only a thin band of edge shading is left.
ffSpinRenderArt(&art, (float) M_PI / 2.0f, 0.0f, &frame);
uint32_t usedColumns = 0;
for (uint32_t x = 0; x < width; ++x) {
bool used = false;
for (uint32_t y = 0; y < height; ++y) {
uint32_t glyph = frame.glyphs[y * width + x];
if (glyph) {
used = true;
VERIFY(isWallGlyph(glyph));
}
}
usedColumns += used;
}
VERIFY(usedColumns > 0 && usedColumns <= 4);
ffSpinFrameDestroy(&frame);
ffSpinArtDestroy(&art);
}
// Renders into a frame one cell larger on every side than ffSpinFrameSize
// asks for; the extra ring must stay empty at every angle.
static void verifyFits(const FFSpinArt* art, float tilt) {
uint32_t width, height;
ffSpinFrameSize(art, tilt, &width, &height);
FFSpinFrame frame;
VERIFY(ffSpinFrameInit(&frame, width + 2, height + 2));
for (uint32_t step = 0; step < 48; ++step) {
ffSpinRenderArt(art, (float) step * 2.0f * (float) M_PI / 48.0f, tilt, &frame);
for (uint32_t x = 0; x < frame.width; ++x) {
VERIFY(frame.glyphs[x] == 0);
VERIFY(frame.glyphs[(size_t) (frame.height - 1) * frame.width + x] == 0);
}
for (uint32_t y = 0; y < frame.height; ++y) {
VERIFY(frame.glyphs[(size_t) y * frame.width] == 0);
VERIFY(frame.glyphs[(size_t) y * frame.width + frame.width - 1] == 0);
}
}
ffSpinFrameDestroy(&frame);
}
static void testBuiltinLogos(void) {
uint32_t count = 0;
for (uint32_t letter = 0; letter < 26; ++letter) {
for (const FFlogo* logo = ffLogoBuiltins[letter]; *logo->names; ++logo) {
FFSpinArt art;
VERIFY(ffSpinArtParse(&art, logo->lines, nullptr, 0));
VERIFY(art.width > 0 && art.height > 0);
verifyFits(&art, 0.0f);
if (count % 16 == 0) {
verifyFits(&art, 0.4f);
}
ffSpinArtDestroy(&art);
++count;
}
}
VERIFY(count > 400);
}
static void testAnsiOutput(void) {
FFstrbuf colors[1];
ffStrbufInitStatic(&colors[0], "35");
FFSpinArt art;
VERIFY(ffSpinArtParse(&art, "a\xe2\x96\x88 b", colors, 1));
FFSpinFrame frame;
VERIFY(ffSpinFrameInit(&frame, art.width, art.height));
ffSpinFramePutArt(&frame, &art);
FF_STRBUF_AUTO_DESTROY plain = ffStrbufCreate();
ffSpinFrameToAnsi(&frame, &art, false, false, true, &plain);
VERIFY(ffStrbufEqualS(&plain, "a\xe2\x96\x88 b\n"));
FF_STRBUF_AUTO_DESTROY colored = ffStrbufCreate();
ffSpinFrameToAnsi(&frame, &art, true, true, true, &colored);
VERIFY(strstr(colored.chars, "\e[0;1;35ma") != nullptr);
VERIFY(strstr(colored.chars, "\e[0m\n") != nullptr);
ffSpinFrameDestroy(&frame);
ffSpinArtDestroy(&art);
}
int main(void) {
testRotation();
testParse();
testMirror();
testRenderFaces();
testBuiltinLogos();
testAnsiOutput();
puts("spin tests passed");
return 0;
}