Files
uv-k1-k5v3-firmware-custom/App/apps/plasma/plasma_app.c
T
2026-09-29 19:45:36 +02:00

326 lines
13 KiB
C

/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Plasma — overlay app. Full-screen demoscene effects on the 1-bit 128x64 LCD,
* all drawn from one scalar field per pixel:
* - scrolled sines: horizontal (x), vertical (y) and diagonal (x+y);
* - up to three radial centres drifting on Lissajous paths, each adding
* either a sine ripple of the squared distance (plasma rings, Fresnel zone
* plates whose interference gives moire) or a 1/r^2 falloff (metaballs).
* Because (x-cx)^2 + (y-cy)^2 is separable, every term comes from per-column
* and per-row tables, so each of the 8192 pixels costs only a handful of adds.
* The field is shown as sweeping bands, an ordered 4x4 Bayer stipple or iso
* contours, optionally inverted. A title pill in the top-left corner spells
* out each change for a moment ("SCENE 8/9: BLOBS", "SPEED 3/8", ...). Pure compute + framebuffer, no radio. The tables live
* on app_main's stack (~2 KiB kept out of the 4 KiB overlay) and no division
* is linked.
*
* Keys: 1-9 scene · UP/DOWN speed · STAR bands/stipple/contour · 0 invert ·
* F auto-cycle · MENU pause · EXIT quit. Scene, look and speed persist.
*/
#include <stdint.h>
#include <stdbool.h>
#include "../app_api.h"
#include "plasma_assets.h" /* generated by gen_assets.py */
#define W 128
#define H 64
#define NCENTRE 3u
#define SPEED_MAX 8u
#define AUTO_FRAMES 400u /* frames per scene in auto-cycle */
#define HUD_MS 1500u /* HUD display time after a change */
#define CFG_MAGIC 0xA7u
enum { MODE_BANDS, MODE_DITHER, MODE_CONTOUR };
enum { FLAG_INV = 1u, FLAG_AUTO = 2u };
/* Title kinds, in T_LABEL order; INVERT/AUTO follow the FLAG_* bit order. */
enum { HUD_SCENE, HUD_MODE, HUD_SPEED, HUD_INVERT, HUD_AUTO, HUD_PAUSE };
typedef struct {
uint8_t magic, scene, mode, speed, flags;
} config_t;
/* Centre c follows phase = tc * K + O on each axis, with its own multipliers
* and offsets so the metaballs never move in lockstep. Centre 0 keeps the
* original plasma drift (x at 1 step, y at 2 steps per frame). */
static const uint8_t centre_kx[NCENTRE] = { 1, 2, 1 };
static const uint8_t centre_ky[NCENTRE] = { 2, 1, 3 };
static const uint8_t centre_ox[NCENTRE] = { 0, 85, 170 };
static const uint8_t centre_oy[NCENTRE] = { 0, 40, 200 };
/* All the state in one struct: one base address for every access. */
struct globals {
const app_api_t *api;
uint32_t hud_until; /* ticks_ms() deadline of the HUD */
uint8_t scene, mode, speed, flags;
uint8_t hud; /* HUD_* kind of the last change */
bool paused;
};
static struct globals g;
#define A (g.api)
static void show_hud(uint8_t kind)
{
g.hud = kind;
g.hud_until = A->ticks_ms() + HUD_MS;
}
/* Append one asset string (without its NUL). */
static char *put_text(char *d, uint16_t offset)
{
char w[TEXT_MAX];
A->asset_read(offset, w, sizeof(w));
for (const char *s = w; *s; s++)
*d++ = *s;
return d;
}
/* Title pill in the top-left corner of the status line, as in the other apps:
* the last change spelled out for HUD_MS, then "PAUSE" while paused.
* Longest: "SCENE 8/9: CLASSIC", 18 chars, 72 px. */
static void draw_hud(void)
{
uint8_t kind = g.hud;
if ((int32_t)(g.hud_until - A->ticks_ms()) <= 0) {
if (!g.paused)
return;
kind = HUD_PAUSE;
}
char s[24];
char *d = put_text(s, T_LABEL + kind * T_LABEL_STRIDE);
switch (kind) {
case HUD_SCENE: /* SCENE 8/9: BLOBS */
*d++ = (char)('1' + g.scene);
*d++ = '/';
*d++ = (char)('0' + NSCENE);
*d++ = ':';
*d++ = ' ';
d = put_text(d, T_SCENE + g.scene * T_SCENE_STRIDE);
break;
case HUD_MODE: /* RENDER: DITHER */
d = put_text(d, T_MODE + g.mode * T_MODE_STRIDE);
break;
case HUD_SPEED: /* SPEED 3/8 */
*d++ = (char)('0' + g.speed);
*d++ = '/';
*d++ = (char)('0' + SPEED_MAX);
break;
case HUD_INVERT: /* INVERT: ON */
case HUD_AUTO: /* AUTO CYCLE: OFF */
d = put_text(d, T_ONOFF + ((g.flags >> (kind - HUD_INVERT)) & 1u) * T_ONOFF_STRIDE);
break;
default: /* PAUSE */
break;
}
*d = '\0';
const uint8_t end = (uint8_t)(2u + (d - s) * 4); /* text starts at x = 2 */
for (uint8_t i = 0; i <= end + 1u; i++)
A->status_line[i] = 0; /* blank the pill area plus a 1 px margin on its right */
A->print_inverse(s, 2, 0, true, true, end);
}
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
A->backlight_on();
A->status_clear();
config_t cfg;
A->cfg_load((uint8_t *)&cfg, sizeof(cfg));
if (cfg.magic == CFG_MAGIC && cfg.scene < NSCENE && cfg.mode < NMODE &&
cfg.speed - 1u < SPEED_MAX) {
g.scene = cfg.scene;
g.mode = cfg.mode;
g.speed = cfg.speed;
g.flags = cfg.flags & (FLAG_INV | FLAG_AUTO);
} else {
g.speed = 3u; /* scene 0, bands, not inverted */
}
show_hud(HUD_SCENE);
/* Read-only tables, copied from the assets onto the stack. */
int8_t sine[SIN_LEN]; /* 32 * sin(2*pi*i/256) */
int8_t fall[FALL_LEN]; /* metaball falloff, by d^2 >> shift */
uint8_t bayer[BAYER_LEN]; /* idx = (y&3)*4 + (x&3), values 0..15 */
uint8_t scenes[NSCENE][SCENE_REC]; /* see gen_assets.py SCENES */
A->asset_read(SIN, sine, sizeof(sine));
A->asset_read(FALL, fall, sizeof(fall));
A->asset_read(BAYER, bayer, sizeof(bayer));
A->asset_read(SCENE, scenes, sizeof(scenes));
/* Per-frame separable tables. */
int8_t colA[W]; /* horizontal sine, by column x */
int8_t rowA[H]; /* vertical sine, by row y */
int8_t diagA[W + H]; /* diagonal sine, by (x + y) */
uint16_t sqx[NCENTRE][W]; /* (x - cx)^2 per radial centre */
uint16_t sqy[NCENTRE][H]; /* (y - cy)^2 */
uint8_t prevLvl[H]; /* previous column, contour mode */
uint8_t t1 = 0, t2 = 0, t3 = 0, tr = 0, tc = 0;
uint16_t autoCtr = 0;
bool running = true;
uint8_t prevKey = APP_KEY_INVALID;
while (running) {
uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
prevKey = APP_KEY_INVALID;
A->delay_ms(10);
A->backlight_update();
continue;
}
if (key == APP_KEY_WAKE)
key = APP_KEY_INVALID;
if (key != prevKey && key != APP_KEY_INVALID) {
A->backlight_on();
switch (key) {
case APP_KEY_EXIT: running = false; break;
case APP_KEY_UP:
case APP_KEY_DOWN: {
const int8_t direction = A->nav_dir(key);
if (direction > 0 && g.speed < SPEED_MAX) g.speed++;
if (direction < 0 && g.speed > 1u) g.speed--;
show_hud(HUD_SPEED);
break;
}
case APP_KEY_MENU:
g.paused = !g.paused;
show_hud(g.paused ? HUD_PAUSE : HUD_SCENE);
break;
case APP_KEY_STAR:
if (++g.mode >= NMODE) g.mode = 0;
show_hud(HUD_MODE);
break;
case APP_KEY_0:
g.flags ^= FLAG_INV;
show_hud(HUD_INVERT);
break;
case APP_KEY_F:
g.flags ^= FLAG_AUTO;
autoCtr = 0;
show_hud(HUD_AUTO);
break;
case APP_KEY_1: case APP_KEY_2: case APP_KEY_3:
case APP_KEY_4: case APP_KEY_5: case APP_KEY_6:
case APP_KEY_7: case APP_KEY_8: case APP_KEY_9:
g.scene = (uint8_t)(key - APP_KEY_1);
g.flags &= (uint8_t)~FLAG_AUTO;
show_hud(HUD_SCENE);
break;
default: break;
}
}
prevKey = key;
if (!running)
break;
const uint8_t *sc = scenes[g.scene];
const bool blob = sc[0] != 0u;
const uint8_t n = sc[1];
const uint8_t sx = sc[2], sy = sc[3], sd = sc[4];
const uint8_t rsh = sc[5];
const int gain = 1 << sc[6];
const uint8_t mode = g.mode;
const uint8_t inv = (g.flags & FLAG_INV) ? 0xFFu : 0u;
/* Drifting radial centres, then their separable squares. */
for (uint8_t c = 0; c < n; c++) {
const int cx = 64 + sine[(uint8_t)(tc * centre_kx[c] + centre_ox[c])]; /* 32..96 */
const int cy = 32 + (sine[(uint8_t)(tc * centre_ky[c] + centre_oy[c])] >> 1); /* 16..48 */
for (uint8_t x = 0; x < W; x++) {
const int dx = (int)x - cx;
sqx[c][x] = (uint16_t)(dx * dx);
}
for (uint8_t y = 0; y < H; y++) {
const int dy = (int)y - cy;
sqy[c][y] = (uint16_t)(dy * dy);
}
}
/* Linear sine terms; a zero scale drops the term. */
for (uint8_t x = 0; x < W; x++)
colA[x] = sx ? sine[(uint8_t)(x * sx + t1)] : 0;
for (uint8_t y = 0; y < H; y++)
rowA[y] = sy ? sine[(uint8_t)(y * sy + t2)] : 0;
for (uint8_t s = 0; s < W + H; s++)
diagA[s] = sd ? sine[(uint8_t)(s * sd + t3)] : 0;
/* Render all 8 pages: page 0 -> status line (top), pages 1..7 -> fb[0..6]. */
for (uint8_t x = 0; x < W; x++) {
const int8_t cxv = colA[x];
const uint8_t bx = x & 3u;
uint8_t up = 0; /* level of the pixel above */
for (uint8_t p = 0; p < 8u; p++) {
uint8_t byte = 0;
for (uint8_t b = 0; b < 8u; b++) {
const uint8_t y = (uint8_t)(p * 8u + b);
int v = cxv + rowA[y] + diagA[x + y];
for (uint8_t c = 0; c < n; c++) {
const uint16_t d = (uint16_t)((sqx[c][x] + sqy[c][y]) >> rsh);
v += blob ? fall[d > 255u ? 255u : d]
: sine[(uint8_t)(d + tr)];
}
v = v * gain + 128;
if (v < 0) v = 0;
if (v > 255) v = 255;
const uint8_t lvl = (uint8_t)v >> 4; /* 0..15 */
bool on;
if (mode == MODE_BANDS) {
on = (lvl & 1u) != 0u; /* sweeping stripes */
} else if (mode == MODE_DITHER) {
on = lvl > bayer[((y & 3u) << 2) | bx]; /* 4x4 stipple */
} else { /* iso contours */
on = (x && prevLvl[y] != lvl) || (y && up != lvl);
prevLvl[y] = lvl;
up = lvl;
}
if (on) byte = (uint8_t)(byte | (1u << b));
}
byte ^= inv;
if (p == 0) A->status_line[x] = byte;
else A->fb[p - 1][x] = byte;
}
}
draw_hud();
A->blit_status();
A->blit_full();
if (!g.paused) {
t1 += g.speed; t2 += (uint8_t)(g.speed + 1u); t3 += 1u;
tr += g.speed; tc += 1u;
if ((g.flags & FLAG_AUTO) && ++autoCtr >= AUTO_FRAMES) {
autoCtr = 0;
if (++g.scene >= NSCENE) g.scene = 0; /* wrap without a modulo */
show_hud(HUD_SCENE);
}
}
A->backlight_update();
}
cfg.magic = CFG_MAGIC;
cfg.scene = g.scene;
cfg.mode = g.mode;
cfg.speed = g.speed;
cfg.flags = g.flags;
A->cfg_save((const uint8_t *)&cfg, sizeof(cfg));
}