Repository navigation
Expand file tree
/
Copy pathboids.c
More file actions
5501 lines (5144 loc) · 255 KB
/
Copy pathboids.c
File metadata and controls
5501 lines (5144 loc) · 255 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* Feature test macros must precede every include. */
#define _XOPEN_SOURCE 700
#define _DEFAULT_SOURCE
#define _DARWIN_C_SOURCE
#include <errno.h>
#include <langinfo.h>
#include <limits.h>
#include <locale.h>
#include <math.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include "cells.h"
#include "font.h"
#include "gif.h"
#include "kitty_graphics.h"
#include "options.h"
#include "picture.h"
#include "png.h"
#include "sign.h"
#include "spatial_grid.h"
#include "sprite_png.h"
enum {
/* Sixty rotations, six degrees apart. It was ninety, and at four degrees a
* fifteen pixel bird moves less than a pixel between frames; with wing beats
* and a second layer there are four times the images to build, and six
* degrees is still smoother than any sprite sheet a game ever shipped. */
ROTATION_FRAMES = 60,
/* A power of two so a rounded angle wraps with one mask. At 4096 entries the
* samples are 0.088 degrees apart, and sine plus cosine as floats occupy one
* typical 32 KB L1 data cache. */
TRIG_LOOKUP_SIZE = 4096,
TRIG_LOOKUP_MASK = TRIG_LOOKUP_SIZE - 1,
/* Wings out, wings half, wings folded: three pictures, beaten in a cycle of
* four so the flap goes out and back. */
WING_PHASES = 3,
WING_CYCLE = 4,
/* Near and far. A far bird is smaller, dimmer and slower, flocks only with
* other far birds, and is drawn underneath: two planes, and the parallax
* between them is what makes a flat screen read as a sky with depth in it. */
LAYERS = 2,
MAX_PALETTE_SHADES = 8,
/* Three is as many as a five step ramp can tell apart: at four the two nearest
* shades are closer to each other than two birds are wide. */
MAX_FLOCKS = 3,
MOUSE_REACH = 120, /* Pixels from the pointer within which a bird feels it. */
DEFAULT_VISION_NOTCH = 6,
DEFAULT_TURNING_NOTCH = 8,
/* Only every sixteenth bird leaves one, because a tail behind all of them is
* three times the bandwidth for a picture that reads as mud. Fifty comets in
* a flock of eight hundred is what says "moving" in a still frame. */
TRAIL_EVERY = 4,
TRAIL_LENGTH = 3,
INTRO_SECONDS = 3,
/* Four is as many as the sky holds: past that they overlap each other, crowd
* the flock into the edges and stop reading as separate animals. */
MAX_HAWKS = 4,
HAWK_REACH = 150,
HAWK_COMMITMENT_FRAMES = 40, /* Sixty-hertz frames; converted to seconds below. */
HAWK_GIVE_UP = 200, /* Pixels past which a reconsidered chase is dropped. */
HAWK_STALK = 340, /* And how far off it looks for the next bird. */
HAWK_PASS_FRAMES = 14, /* Sixty-hertz frames; converted to seconds below. */
HAWK_SPACING = 150, /* Pixels two hawks try to keep between them. */
/* A GIF's delay is in hundredths of a second, so the rates it can express are
* 100/1, 100/2, 100/3 and so on. Viewers also clamp anything under two
* hundredths up to a tenth of a second, which puts the real ceiling at fifty:
* sixty is simply not a rate a GIF has. */
MAX_RECORD_FPS = 50,
AUTOPILOT_PERIOD = 4, /* Seconds between one slider moving and the next. */
OUTRO_FRAMES_AT_SIXTY = 40,
FRAME_ANGLE = 360 / ROTATION_FRAMES,
SPRITE_SUPERSAMPLE = 6,
SPRITE_WORK_MAX = 256,
MIN_BIRD_SIZE = 4,
MAX_BIRD_SIZE = 64,
MAX_BIRDS = 4096,
INPUT_BUFFER_SIZE = 100,
DEFAULT_COLS = 80,
DEFAULT_ROWS = 24,
DEFAULT_CELL_WIDTH = 8,
DEFAULT_CELL_HEIGHT = 16,
/* Edge bands the flock turns away from, as a fraction of the viewport: a
* third on the sides and the top, half of that at the bottom. */
TURN_BAND_DIVISOR = 3,
BOTTOM_BAND_DIVISOR = 6,
/* Sixty is the rate, not a setting: every speed and turn in the program is a
* per second quantity divided by it, and a picture a frame terminal is the
* one thing that lowers it. A cast may be recorded at up to twice that. */
FRAME_RATE = 60,
MAX_CAST_FPS = 120,
DEFAULT_SPEED = 40,
DEFAULT_BIRD_SIZE = 30,
SPATIAL_CELL_SIZE = 12,
/* Perception is tuned as a radius in pixels rather than in whole grid cells,
* which is what lets it share the twelve notch travel: the cell scan derives
* from it, and the distance test was always the exact radius anyway. Sixty
* pixels is the five cell ceiling it had before. */
MIN_VISION_RADIUS = 12,
MAX_VISION_RADIUS = 60,
DEFAULT_VISION_RADIUS = 36,
MAX_VISION_CELLS = MAX_VISION_RADIUS / SPATIAL_CELL_SIZE,
/* The parameter panel, anchored to the top left corner. Its size in cells is
* fixed: it follows the longest parameter name and the bar, never the
* terminal. Below the minimum viewport it is dropped and the flock keeps
* everything; the minimums leave a corridor to the right of the panel and
* one underneath it. */
LEGEND_COLUMNS = 38,
/* Ten rows with one flock; with more there is a slider for how much they
* avoid each other, and it only exists when there is somebody to avoid. */
LEGEND_ROWS = 10,
LEGEND_MAX_ROWS = LEGEND_ROWS + 1,
/* One notch a keypress, so this is also the number of steps every parameter
* travels through, from its floor to its ceiling. */
LEGEND_BAR_CELLS = 12,
LEGEND_NAME_WIDTH = 10,
LEGEND_VALUE_WIDTH = 5,
/* The panel is 38 by 11 cells at most and the flock may not enter it. At the
* smallest terminal it used to appear in it covered half the screen, and half
* the flock was squeezed off the edges of what was left: it needs to be a
* quarter of the room at most, not a half, so it waits for a window it fits
* inside. */
LEGEND_MIN_COLS = 76,
LEGEND_MIN_ROWS = 22,
LEGEND_LINE_MAX = 128,
SPAWN_ATTEMPTS = 32
};
/* The same scale as the original bottom edge turn: large enough that it settles
* the direction on its own, whatever the flocking terms are doing. */
static const double LEGEND_PUSH = 100000.0;
/* How hard an edge pushes once a bird reaches the screen's own edge, against a
* flocking sum of one to four. Twelve turns them well inside the band and still
* leaves them the whole screen to use; much more and the flock plays in a box. */
static const double EDGE_FIRM = 12.0;
/* And how much harder each further band-width of straying costs. */
static const double ESCAPE_PENALTY = 8.0;
/* Heavy enough to bend a flock that is busy flocking, light enough that it bends
* rather than shatters. */
static const double MOUSE_WEIGHT = 4.0;
/* A hawk frightens a bird; it does not get to throw it off the screen. At six
* the flee beat the edge even at the screen's own edge, and four birds in fifty
* were outside the frame at any moment with four hawks up. */
static const double HAWK_WEIGHT = 3.0;
/* A shade slower than the flock when it is only cruising, so it has to dive to
* catch anything: a hawk that outruns the birds at rest never has to commit, and
* the moment it commits is the moment worth watching. */
static const double HAWK_SPEED = 0.90;
/* Sharper than a bird's bank, because a raptor is more agile, but a limit all the
* same: without one it turned forty degrees a frame and read as a glitch. A fifth
* of a radian looked calm and never caught anything: the turning circle was wider
* than the flock, so every miss became a long trip to a wall and back. Half a
* radian still only landed one chase in eight; at a whole one it lands half of
* them, and the wall is reached a third as often. */
static const double HAWK_TURN = 1.0;
/* That is per step at sixty a second. It follows elapsed flight time so the hawk
* remains the same animal when frames arrive faster or slower, and when the speed
* slider flies everything faster or slower. */
#define HAWK_TURN_PER_FRAME() (HAWK_TURN * FRAME_RATE * flight_seconds())
/* At most the distance a bird covers in three sixty-hertz steps. */
static const double HAWK_LEAD_DISTANCE = DEFAULT_SPEED * 3.0;
/* Inside the dive it accelerates and stops leading: a bird that flees is only a
* tenth slower than a cruising hawk, so without this the chase never closes and
* there is no moment to watch. */
static const double HAWK_DIVE = 90.0;
/* Wing beats a second, for the birds; a starling manages about eight, and six
* reads as effort without reading as panic. A bird glides now and then — wings
* out and still for half a second or so — because a flock in which every wing is
* always beating looks like a machine. */
static const double WING_HZ = 6.0;
static const double GLIDE_CHANCE = 0.06; /* Per beat completed. */
static const double GLIDE_SECONDS_MIN = 0.4, GLIDE_SECONDS_MAX = 1.2;
/* How the far layer differs from the near: smaller, slower — the parallax — and
* dimmed toward the ground, which is what distance does to colour. */
static const double FAR_SHARE = 0.35;
static const double FAR_SIZE = 0.62;
static const double FAR_PACE = 0.72;
static const double FAR_DIM = 0.40; /* How far toward the ground its tint goes. */
/* What a wing at each phase does to the span: seen from above, a beat is the
* wings foreshortening, not a new drawing. */
static const double WING_SPAN[WING_PHASES] = {1.0, 0.72, 0.45};
static const int WING_SEQUENCE[WING_CYCLE] = {0, 1, 2, 1};
/* Tails: three ghosts behind every fourth bird, each fainter and a touch smaller
* than the last, so a tail is a fade and not a queue. */
static const double TRAIL_ALPHA[TRAIL_LENGTH] = {0.40, 0.25, 0.12};
static const double TRAIL_SIZE = 0.85;
static const double HAWK_DIVE_SPEED = 1.45;
/* How much of the flee is sideways rather than straight away. Purely radial and
* the flock bursts like a firework and is gone; with a curl to it the birds peel
* around the hawk and close up behind, which is the shape people watch for. */
static const double HAWK_SWIRL = 0.9;
/* What one hawk's company is worth to another, against a chase of one. */
static const double HAWK_APART = 1.2;
/* And what a wall is worth: more than the chase, or it follows a bird into the
* edge and bounces off it. */
static const double HAWK_WALL = 2.5;
/* How firmly a hawk is turned from the text of a sign (see hawk_sign_vector). */
static const double HAWK_SIGN = 0.75;
/* Flocking is local — a bird sees sixty pixels at most — so nothing in the three
* rules keeps a flock together as a body across a whole screen. The leash is the
* missing long range term: nothing at all within a flock's own width of its
* centre, and a pull that grows outside it. The width grows with the flock, as
* the square root of its birds, twelve pixels to the root, which is about the
* width a flock that size takes up with no leash at all. A fixed 150 was
* narrower than a flock of five hundred, so the leash pulled on its whole rim
* and it wound itself into a mill: 1000 birds in two flocks turned 37 times a
* minute, 4096 in three 45 times. With the width grown they turn two to four. */
static const int FLOCK_LEASH = 150; /* The narrowest it gets, for a small flock. */
static const double LEASH_PER_ROOT_BIRD = 12.0;
static const double LEASH_WEIGHT = 2.5;
/* A breeze the whole flock leans into. Enough to shape it, not enough to carry
* it off: at the top notch it is about a third of the alignment weight. */
static const double WIND_WEIGHT = 0.5;
/* Needed in the config initializer, so macros rather than constants. */
#define DEFAULT_SEPARATION_W 0.005
#define DEFAULT_ALIGNMENT_W 1.5
/* Not a slider any more. Dragging it from end to end moved the flock's own
* measure of itself — neighbours within sixty pixels — by four and a half, where
* simply removing the term moves it by eleven: the force does something, the
* knob did not, and it was costing a row of a six row panel and two of the
* fourteen keys. */
#define COHESION_W 0.01
#define DEFAULT_BOUNDARY_W 0.2
static const double BOUNDARY_MIN = 0.01;
static const double SEPARATION_MIN = 0.001;
static const double ALIGNMENT_MIN = 0.1;
/* Each ceiling is placed so that the default lands exactly on the fourth of
* twelve notches, a third along the bar. There are no step constants any more:
* a step is one notch, which is a twelfth of the travel by construction, and
* that is what makes a keypress worth exactly one cell of bar. */
#define DEFAULT_NOTCH 4
#define NOTCH_CEILING(minimum, default_value) ((minimum) + 3 * ((default_value) - (minimum)))
static const double BOUNDARY_MAX = NOTCH_CEILING(0.01, DEFAULT_BOUNDARY_W);
static const double SEPARATION_MAX = NOTCH_CEILING(0.001, DEFAULT_SEPARATION_W);
static const double ALIGNMENT_MAX = NOTCH_CEILING(0.1, DEFAULT_ALIGNMENT_W);
/* Flight speed, as a factor on the pace everything else was tuned at. It scales
* how far a bird flies in a second and, with it, how far it may turn in that
* second, so the path a bird traces is the one the edges, the panel and the
* hawks were tuned on, flown faster or slower: without the turn a fast flock
* swung wide into the edges and a slow one spun on the spot. A fifth of the pace
* is slow motion; at the top a frame is flown in three steps (see fly), which
* keeps eight hundred birds near a millisecond a frame. A fifth a notch, from a
* fifth at notch zero to thirteen fifths at the top, so the panel prints each
* one as it is. The default is the second notch, two fifths: the flock starts
* slow enough to follow one bird with the eye, and v/V is there for more. */
#define DEFAULT_PACE_NOTCH 1
#define DEFAULT_PACE 0.4
static const double PACE_STEP = 0.2;
/* How much one flock avoids another, with two or more of them.
*
* On the fourth notch, the default, a flock keeps to its own kind and to nothing
* else: it flies where it likes, and meets and crosses the others. It used to be
* sent a room away from them as well, to a home of its own, and a flock sent
* home flies round it: two flocks turned twelve times a minute where one flock
* alone turns twice. Measured over six runs of a minute and a half.
*
* Below it the others become kin by halves, a half a notch, until at the bottom
* a bird aligns with and closes on every bird it sees: one flock in two or three
* colours, each bird's nearest neighbour a stranger as often as chance says. The
* leash that keeps a flock together and the pace that tells flocks apart let go
* with it, or the colours sort themselves out again.
*
* Above it each flock is sent a room away from the others, up to twice the room
* it used to keep, and a bird steers away from the strangers it can see, which
* is what makes two flocks that meet part around each other rather than pass
* through. At the top a stranger in sight weighs what the flock's own heading
* does by default. The panel shows the whole bar as a factor, a quarter a notch. */
static const double AVOID_WEIGHT_MAX = 1.5;
#define ALT_SCREEN_ON "\033[?1049h"
#define ALT_SCREEN_OFF "\033[?1049l"
#define CURSOR_HIDE "\033[?25l"
#define CURSOR_SHOW "\033[?25h"
#define SYNC_UPDATE_END "\033[?2026l"
/* Any event tracking plus SGR coordinates: 1003 reports plain motion as well as
* clicks, and 1006 lifts the 223 column ceiling of the original encoding. */
#define MOUSE_REPORT_ON "\033[?1003h\033[?1006h"
#define MOUSE_REPORT_OFF "\033[?1006l\033[?1003l"
#define KITTY_FREE_IMAGES "\033_Ga=d,d=A,q=2\033\\"
typedef struct {
double x, y;
} vector_t;
typedef struct {
float cosine, sine;
} trig_entry_t;
static trig_entry_t trig_lookup_table[TRIG_LOOKUP_SIZE];
/* Built once before any flock is run. The simulation keeps its continuous
* double direction; only the unit vector read hundreds of thousands of times by
* the neighbour loop is rounded to the nearest table entry. */
static void trig_lookup_init(void) {
for (int i = 0; i < TRIG_LOOKUP_SIZE; i++) {
double angle = (double)i * 2 * M_PI / TRIG_LOOKUP_SIZE;
trig_lookup_table[i].cosine = (float)cos(angle);
trig_lookup_table[i].sine = (float)sin(angle);
}
}
static trig_entry_t trig_lookup(double angle) {
double scaled = angle * (TRIG_LOOKUP_SIZE / (2 * M_PI));
int nearest = (int)(scaled + (scaled >= 0 ? 0.5 : -0.5));
return trig_lookup_table[(unsigned)nearest & TRIG_LOOKUP_MASK];
}
typedef struct {
double x, y, direction;
int frame;
int shade; /* Index into the palette, and half of the image id. */
int flock; /* Which flock it reads: separation ignores this, the rest does not. */
int layer; /* Near or far; the two never see each other. */
int wing; /* Where in the beat it is: an index into WING_SEQUENCE. */
double wing_clock;
double gliding; /* Seconds of wings held out and still. */
double trail_x[TRAIL_LENGTH], trail_y[TRAIL_LENGTH];
int trail_at, trail_held;
double scattered; /* Seconds a bird of a sign stays away from its place. */
} bird_t;
typedef struct {
uint8_t *data;
size_t length;
} image_frame_t;
typedef struct {
int width, height, cols, rows;
int cell_width, cell_height, turn_x, turn_y, turn_bottom;
int legend_width, legend_height; /* The panel in pixels, zero when there is none. */
} screen_t;
typedef struct {
int birds, bird_size, palette, flocks;
int trails, hawks, shape;
int turning_notch;
double speed; /* Pixels a bird covers this frame, at the chosen pace. */
double base_speed; /* The same at pace one, which is what the way out flies at. */
double pace; /* The speed slider's factor; one is the shipped flock. */
int vision_cells, vision_radius, vision_radius_squared;
double separation, alignment, boundary;
/* Notch positions, zero to LEGEND_BAR_CELLS. These are the state the keys
* move; every value above is derived from them, which is what makes one
* keypress exactly one notch of bar rather than nearly one. */
int boundary_notch, separation_notch, alignment_notch;
int vision_notch, pace_notch;
int avoid_notch;
double avoid_kinship; /* How much of kin a stranger is: one at the bottom, then halves. */
double avoid_room; /* The room between flock homes, as a share of 2 * FLOCK_LEASH. */
double avoid_weight; /* And the weight of a bird's wariness of strangers. */
} config_t;
static config_t config = {
.birds = 800,
.speed = DEFAULT_SPEED,
.base_speed = DEFAULT_SPEED,
.pace = DEFAULT_PACE,
.bird_size = 0, /* not given: settle_the_bird_size makes it 30 */
.palette = 0,
.flocks = 1,
.turning_notch = DEFAULT_TURNING_NOTCH,
.vision_cells = DEFAULT_VISION_RADIUS / SPATIAL_CELL_SIZE,
.vision_radius = DEFAULT_VISION_RADIUS,
.vision_radius_squared = DEFAULT_VISION_RADIUS * DEFAULT_VISION_RADIUS,
.separation = DEFAULT_SEPARATION_W,
.alignment = DEFAULT_ALIGNMENT_W,
.boundary = DEFAULT_BOUNDARY_W,
.boundary_notch = DEFAULT_NOTCH,
.separation_notch = DEFAULT_NOTCH,
.alignment_notch = DEFAULT_NOTCH,
/* Twelve to sixty pixels in steps of four: thirty six is the sixth notch. */
.vision_notch = DEFAULT_VISION_NOTCH,
.pace_notch = DEFAULT_PACE_NOTCH,
.avoid_notch = DEFAULT_NOTCH,
};
static screen_t screen;
static int legend_enabled; /* Hidden until --panel or h asks for it. */
/*
* How the frame reaches the screen.
*
* Braille, in every terminal, unless asked otherwise: the frame is rendered to
* pixels exactly as it is for a recording, and the pixels are read back as
* braille — eight dots a cell, the finest thing text can do — or as sextants or
* half blocks, each cell in the colour of the bird in it. Kitty's graphics
* protocol draws real sprites, and only when --render kitty asks for it: Kitty
* and Ghostty place them right, and elsewhere what it does is undefined. Other
* terminals answer for the protocol and then place nothing, too few birds or
* the wrong ones, so no terminal is guessed at.
*/
typedef enum {
RENDER_UNSET = -1, /* Not asked for: braille live, sprites in a recording. */
RENDER_KITTY,
RENDER_BRAILLE,
RENDER_SEXTANTS, /* Solid two by three blocks: bolder than dots, needs a 2020 font. */
RENDER_BLOCKS,
} render_mode_t;
static const char *const RENDER_NAMES[] = {"kitty", "braille", "sextants", "blocks", NULL};
static int render_mode = RENDER_UNSET;
/* --depth: a second plane of birds further off. The default is the one. */
static int deep_look;
static int drawing_with_text(void) {
return render_mode == RENDER_BRAILLE || render_mode == RENDER_SEXTANTS ||
render_mode == RENDER_BLOCKS;
}
static cells_style_t text_style(void) {
if (render_mode == RENDER_SEXTANTS) return CELLS_SEXTANTS;
if (render_mode == RENDER_BLOCKS) return CELLS_BLOCKS;
return CELLS_BRAILLE;
}
/* Where the pointer is, in pixels, and whether it has ever been seen. The
* terminal reports cells, so the position is the middle of the cell it names:
* that is as precise as the protocol gets. */
static struct {
int present;
double x, y;
double moved_at; /* On the run's clock: a pointer that is still scatters no sign. */
} mouse;
/* A monotonic clock for everything that animates on its own: the frame counter
* for anything that wants to act every so many frames, the seconds for anything
* that has to look the same whatever the frame rate. */
static struct {
long frame;
double seconds;
} clock_state;
/* How much real or recorded time the next simulation step represents. A normal
* live frame is about 1/60 s, a picture renderer about 1/30 s, and an unlocked
* frame whatever elapsed since the previous one. */
static double frame_seconds = 1.0 / FRAME_RATE;
/* How much flying the next step represents: the frame's time at the pace the
* speed slider asks for. What a bird or a hawk covers, what it may turn through
* and how long a hawk holds a chase run on this. The wing beats, the clocks and
* the way out run on the frame's own time: six beats a second reads as effort
* at any speed, and at two and a half times it would strobe. */
static double flight_seconds(void) {
return frame_seconds * config.pace;
}
/* Paused holds the simulation still but keeps drawing and reading keys, so the
* panel still answers and a single step is possible. Stepping is one frame of
* motion granted while paused. */
static int paused;
static int step_once;
static int population_changed;
/* Whether the panel is currently on screen. The panel is anchored at the origin
* and constant in cells, so it never leaves text behind by moving: the only row
* ever needing an erase is one it occupied before being switched off. Clearing
* the whole screen would take the uploaded sprites with it. */
static int legend_drawn;
static struct termios saved_termios;
static volatile sig_atomic_t terminal_is_raw;
static volatile sig_atomic_t terminal_restored;
static volatile sig_atomic_t alt_screen_is_on;
static volatile sig_atomic_t sprites_uploaded;
static void write_all(const void *data, size_t length) {
const char *bytes = data;
while (length > 0) {
ssize_t written = write(STDOUT_FILENO, bytes, length);
if (written < 0) {
if (errno == EINTR) continue;
return;
}
if (written == 0) return;
bytes += written;
length -= (size_t)written;
}
}
static void restore_terminal(void) {
if (terminal_restored) return;
terminal_restored = 1;
if (terminal_is_raw) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_termios);
terminal_is_raw = 0;
}
if (!alt_screen_is_on) return; /* The probe failed before we took the screen. */
/* The sprites were uploaded once and outlive the frames that placed them:
* the lowercase delete every frame sends clears placements only. Uppercase
* frees every image left without one, so the terminal is not holding a few
* megabytes of birds after they have flown. */
if (sprites_uploaded) write_all(KITTY_FREE_IMAGES, sizeof(KITTY_FREE_IMAGES) - 1);
write_all(MOUSE_REPORT_OFF, sizeof(MOUSE_REPORT_OFF) - 1);
write_all(SYNC_UPDATE_END, sizeof(SYNC_UPDATE_END) - 1);
write_all(CURSOR_SHOW, sizeof(CURSOR_SHOW) - 1);
write_all(ALT_SCREEN_OFF, sizeof(ALT_SCREEN_OFF) - 1);
}
static void signal_handler(int signal_number) {
restore_terminal();
_exit(128 + signal_number);
}
static void install_signal_handlers(void) {
static const int signals[] = {SIGINT, SIGTERM, SIGHUP, SIGQUIT,
SIGSEGV, SIGFPE, SIGBUS, SIGABRT};
struct sigaction action;
memset(&action, 0, sizeof(action));
action.sa_handler = signal_handler;
action.sa_flags = (int)SA_RESETHAND;
sigemptyset(&action.sa_mask);
for (size_t i = 0; i < sizeof(signals) / sizeof(*signals); i++)
sigaction(signals[i], &action, NULL);
/* A reader that goes away, as head does, is an error to report rather than a
* death: SIGPIPE's default kills the process before the terminal is put back,
* and leaves the shell without echo. Ignored, the write fails with EPIPE and
* the program leaves through exit, which restores it. */
action.sa_handler = SIG_IGN;
action.sa_flags = 0;
sigaction(SIGPIPE, &action, NULL);
}
/*
* The terminal's replies, and the key reader that must not take them for keys.
*
* A question to the terminal is answered with an escape sequence, and what makes
* the answer whole depends on the kind of sequence it is: an OSC string ends at a
* bell or at ST (ESC \), a DCS at ST, a CSI at its final byte. It does not end at
* a letter inside it: the colour cc in rgb:cc/00/00 is a 'c', and a reply that was
* cut off there lost its colour. The same goes for the key reader, which has to
* know where a string ends to leave it alone, since a reply that came late is
* read there, and the letters in it are commands.
*/
typedef enum { REPLY_NONE, REPLY_PARTIAL, REPLY_COMPLETE } reply_progress_t;
/* The first reply in `bytes`: where its escape is, where it ends, and which kind of
* sequence it is (the byte after the escape). Anything before the escape is a key
* that was typed while the question was out. */
static reply_progress_t read_a_reply(const char *bytes, size_t length, size_t *first, size_t *last,
char *kind) {
size_t at = 0;
while (at < length && bytes[at] != '\033') at++;
if (at >= length) return REPLY_NONE;
*first = at;
if (at + 1 >= length) return REPLY_PARTIAL;
*kind = bytes[at + 1];
if (*kind == '[') {
for (size_t i = at + 2; i < length; i++)
if ((unsigned char)bytes[i] >= 0x40 && (unsigned char)bytes[i] <= 0x7e) {
*last = i + 1;
return REPLY_COMPLETE;
}
return REPLY_PARTIAL;
}
if (*kind == ']' || *kind == 'P') {
for (size_t i = at + 2; i < length; i++) {
if (bytes[i] == '\a' && *kind == ']') {
*last = i + 1;
return REPLY_COMPLETE;
}
if (bytes[i] != '\033') continue;
if (i + 1 >= length) return REPLY_PARTIAL;
if (bytes[i + 1] == '\\') {
*last = i + 2;
return REPLY_COMPLETE;
}
}
return REPLY_PARTIAL;
}
*last = at + 2; /* An escape and a byte: a key with alt, and no reply. */
return REPLY_COMPLETE;
}
/* Where the key reader is in what it is reading, kept between reads because a
* sequence may be split across them. A string, an OSC or a DCS, is read to its end
* and thrown away. It is given up if it runs on past any reply this program asks
* for, or if nothing more of it comes for a moment: alt with ] or P begins one too,
* and the keys typed after that are not part of it. */
enum { INPUT_NORMAL, INPUT_ESCAPE, INPUT_SEQUENCE, INPUT_STRING, INPUT_STRING_ESCAPE };
static int input_state = INPUT_NORMAL;
static char input_string_kind;
static size_t input_string_length;
static struct timespec input_string_at;
static const size_t INPUT_STRING_LONGEST = 256;
static const double INPUT_STRING_PATIENCE = 0.25;
/* When the escape that the lock screen is waiting on came, or nothing at all: a
* negative second says there is none to wait for. */
static struct timespec input_escape_at = {-1, 0};
/* A lock screen past its grace waits to see whether an escape is the Escape key. */
static void wait_on_an_escape(int goes_at_a_key) {
if (goes_at_a_key)
clock_gettime(CLOCK_MONOTONIC, &input_escape_at);
else
input_escape_at.tv_sec = -1;
}
static void begin_a_string(char kind) {
input_state = INPUT_STRING;
input_string_kind = kind;
input_string_length = 0;
clock_gettime(CLOCK_MONOTONIC, &input_string_at);
}
/* Sends a request and collects the reply to it, or whatever comes back until the
* deadline. The colour queries ask the terminal a question it may simply not
* answer, and may not hang the startup path waiting for a reply that is never
* coming. Raw mode has to be on already, or the reply would be echoed and held
* until a newline.
*
* The reply is the one that answers the question: for a request that names what it
* asks (OSC 11, OSC 4 entry 3) a complete reply to another question is a late
* answer to an earlier one, and is dropped rather than taken for this. A reply
* that has begun and is not all there when the time is up is not an answer, and
* the key reader is told to throw away the rest of it when it comes. Returns the
* length of the reply, which is all that is in the buffer, or 0. */
static size_t terminal_query(const char *request, size_t request_length, char *reply,
size_t reply_size, int milliseconds) {
struct timespec start, now;
size_t length = 0;
char wanted[32];
size_t wanted_length = 0;
if (reply_size == 0) return 0;
reply[0] = '\0';
if (request_length > 2 && request[0] == '\033' && request[1] == ']') {
const char *mark = memchr(request, '?', request_length);
if (mark != NULL && (size_t)(mark - request) < sizeof(wanted)) {
wanted_length = (size_t)(mark - request);
memcpy(wanted, request, wanted_length);
}
}
write_all(request, request_length);
clock_gettime(CLOCK_MONOTONIC, &start);
for (;;) {
clock_gettime(CLOCK_MONOTONIC, &now);
long spent = (now.tv_sec - start.tv_sec) * 1000L + (now.tv_nsec - start.tv_nsec) / 1000000L;
if (spent >= milliseconds) break;
struct pollfd wait = {.fd = STDIN_FILENO, .events = POLLIN};
int ready = poll(&wait, 1, (int)(milliseconds - spent));
if (ready < 0) {
if (errno == EINTR) continue;
break;
}
if (ready == 0) break;
ssize_t got = read(STDIN_FILENO, reply + length, reply_size - 1 - length);
if (got <= 0) break;
length += (size_t)got;
reply[length] = '\0';
size_t first, last;
char kind = 0;
while (read_a_reply(reply, length, &first, &last, &kind) == REPLY_COMPLETE) {
if (wanted_length == 0 || (last - first >= wanted_length &&
memcmp(reply + first, wanted, wanted_length) == 0)) {
memmove(reply, reply + first, last - first);
length = last - first;
reply[length] = '\0';
return length;
}
memmove(reply, reply + last, length - last);
length -= last;
reply[length] = '\0';
}
if (length + 1 >= reply_size) break;
}
size_t first, last;
char kind = 0;
if (read_a_reply(reply, length, &first, &last, &kind) == REPLY_PARTIAL && first + 1 < length &&
(kind == ']' || kind == 'P'))
begin_a_string(kind);
reply[0] = '\0';
return 0;
}
/* What a live run draws with: braille unless something else was asked for. */
static int live_render_mode(void) {
return render_mode == RENDER_UNSET ? RENDER_BRAILLE : render_mode;
}
static void enter_alt_screen(void) {
write_all(ALT_SCREEN_ON, sizeof(ALT_SCREEN_ON) - 1);
write_all(CURSOR_HIDE, sizeof(CURSOR_HIDE) - 1);
write_all(MOUSE_REPORT_ON, sizeof(MOUSE_REPORT_ON) - 1);
alt_screen_is_on = 1;
}
static int enter_terminal(void) {
struct termios raw;
if (tcgetattr(STDIN_FILENO, &raw) < 0) return -1;
saved_termios = raw;
raw.c_iflag &= (tcflag_t) ~(tcflag_t)(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
raw.c_oflag &= (tcflag_t) ~(tcflag_t)OPOST;
raw.c_cflag |= CS8;
raw.c_lflag &= (tcflag_t) ~(tcflag_t)(ECHO | ICANON | IEXTEN);
raw.c_cc[VSUSP] = _POSIX_VDISABLE;
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) return -1;
terminal_is_raw = 1;
return 0;
}
/* Kept proportional to the viewport: a fixed pixel distance covers a short
* terminal entirely and pins the whole flock against one edge. */
static void update_turn_distances(void) {
screen.turn_x = screen.width / TURN_BAND_DIVISOR;
screen.turn_y = screen.height / TURN_BAND_DIVISOR;
screen.turn_bottom = screen.height / BOTTOM_BAND_DIVISOR;
if (screen.turn_x < 1) screen.turn_x = 1;
if (screen.turn_y < 1) screen.turn_y = 1;
if (screen.turn_bottom < 1) screen.turn_bottom = 1;
}
/* The panel takes a corner rather than a row, so the flyable area stays an L and
* no other derivation has to shrink: the flock keeps the full width below the
* panel and the full height beside it, and is kept out of the corner by a force
* instead of by a bound. */
static int legend_rows(void) {
return config.flocks > 1 ? LEGEND_MAX_ROWS : LEGEND_ROWS;
}
static void measure_legend(void) {
screen.legend_width = screen.legend_height = 0;
if (!legend_enabled) return;
if (screen.cols < LEGEND_MIN_COLS || screen.rows < LEGEND_MIN_ROWS) return;
screen.legend_width = LEGEND_COLUMNS * screen.cell_width;
screen.legend_height = legend_rows() * screen.cell_height;
}
/* Split out of the ioctl query so the tests drive the real derivation. */
/* Defined with the other notch arithmetic; needed here because the step a bird
* takes is capped by the size of the screen it is taking it on. */
static void update_speed(void);
static void apply_screen_size(int cols, int rows, int pixel_width, int pixel_height) {
screen.cols = cols > 0 ? cols : DEFAULT_COLS;
screen.rows = rows > 0 ? rows : DEFAULT_ROWS;
screen.width = pixel_width;
screen.height = pixel_height;
if (screen.width <= 0 || screen.height <= 0) {
screen.width = screen.cols * DEFAULT_CELL_WIDTH;
screen.height = screen.rows * DEFAULT_CELL_HEIGHT;
}
screen.cell_width = screen.width / screen.cols;
screen.cell_height = screen.height / screen.rows;
if (screen.cell_width < 1) screen.cell_width = 1;
if (screen.cell_height < 1) screen.cell_height = 1;
/* Before the distances, because the panel's turn zone is the panel grown by
* one frame of travel and the frame of travel depends on the screen. */
update_speed();
measure_legend();
update_turn_distances();
}
static void update_screen_dimensions(void) {
struct winsize size;
memset(&size, 0, sizeof(size));
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0) memset(&size, 0, sizeof(size));
apply_screen_size(size.ws_col, size.ws_row, size.ws_xpixel, size.ws_ypixel);
}
/*
* The terminal's own colours, asked for rather than guessed.
*
* OSC 4 names a palette entry, OSC 10 and 11 the foreground and background. The
* answer comes back as rgb:RRRR/GGGG/BBBB, four hex digits a channel, and a
* terminal that does not implement the query simply says nothing. A ramp is then
* built between the most saturated answer and the background, which is what
* makes a screenshot match the poster's own setup: the single thing that decides
* whether a terminal toy looks native or looks imported.
*/
static uint8_t theme_tints[5][3];
static int theme_is_known;
static int parse_osc_colour(const char *reply, uint8_t rgb[3]) {
const char *at = strstr(reply, "rgb:");
unsigned r, g, b;
if (at == NULL) return 0;
if (sscanf(at + 4, "%4x/%4x/%4x", &r, &g, &b) != 3) return 0;
/* Four hex digits a channel is the usual answer, but some terminals send
* two; scale whichever came back down to a byte. */
const char *slash = strchr(at + 4, '/');
int digits = slash ? (int)(slash - (at + 4)) : 4;
int shift = digits >= 4 ? 8 : 0;
rgb[0] = (uint8_t)(r >> shift);
rgb[1] = (uint8_t)(g >> shift);
rgb[2] = (uint8_t)(b >> shift);
return 1;
}
static int ask_colour(const char *request, uint8_t rgb[3]) {
char reply[128];
if (terminal_query(request, strlen(request), reply, sizeof(reply), 60) == 0) return 0;
return parse_osc_colour(reply, rgb);
}
static int saturation_of(const uint8_t rgb[3]) {
int high = rgb[0] > rgb[1] ? rgb[0] : rgb[1];
int low = rgb[0] < rgb[1] ? rgb[0] : rgb[1];
if (rgb[2] > high) high = rgb[2];
if (rgb[2] < low) low = rgb[2];
return high - low;
}
/* The relative luminance the WCAG contrast ratio is built on. Used to keep the
* flock off the terminal's own background, which is the one colour a bird must
* never be. */
static double luminance_of(const uint8_t rgb[3]) {
double channel[3];
for (int c = 0; c < 3; c++) {
double v = rgb[c] / 255.0;
channel[c] = v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
}
return 0.2126 * channel[0] + 0.7152 * channel[1] + 0.0722 * channel[2];
}
static double contrast_between(const uint8_t a[3], const uint8_t b[3]) {
double high = luminance_of(a), low = luminance_of(b);
if (high < low) {
double swap = high;
high = low;
low = swap;
}
return (high + 0.05) / (low + 0.05);
}
/* Five steps from the accent towards the background, stopping well short of it.
* Over four steps the last shade *was* the background, byte for byte: a fifth of
* the flock was painted in the colour of the sky and simply did not exist, and
* with three flocks up a whole flock went missing. Over seven, the far end still
* reads as distance and the worst case across the common colour schemes keeps a
* contrast of 1.8 against the ground instead of 1.0. */
static void ramp_between(const uint8_t from[3], const uint8_t to[3]) {
for (int i = 0; i < 5; i++)
for (int c = 0; c < 3; c++)
theme_tints[i][c] = (uint8_t)(from[c] + (to[c] - from[c]) * i / 7);
}
static int learn_the_theme(void) {
uint8_t accent[3] = {0, 0, 0}, background[3] = {0, 0, 0};
double best = -1;
/* The background first, because the accent is chosen against it. */
if (!ask_colour("\033]11;?\033\\", background)) {
/* No background: fade towards black, which is the common case. */
background[0] = background[1] = background[2] = 0;
}
/* Entries one to six are the terminal's own reds through cyans, which is
* where a colour scheme keeps its character. The most saturated of them is
* not always the one to take: on stock xterm that is pure blue on black,
* which is both the dimmest colour on the screen and the ugliest. Saturation
* times contrast picks the colour with character that can also be seen. */
for (int entry = 1; entry <= 6; entry++) {
char request[32];
uint8_t rgb[3];
snprintf(request, sizeof(request), "\033]4;%d;?\033\\", entry);
if (!ask_colour(request, rgb)) continue;
double score = saturation_of(rgb) * contrast_between(rgb, background);
if (score > best) {
best = score;
memcpy(accent, rgb, sizeof(accent));
}
}
if (best < 0) return 0;
ramp_between(accent, background);
theme_is_known = 1;
return 1;
}
/*
* A palette is a list of tints applied to the one embedded sprite. The first
* entry of every palette is the sprite untouched, so a bird with no shade of
* its own looks exactly as it always did.
*/
typedef struct {
const char *name;
const char *help;
int shades;
const uint8_t (*tints)[3];
png_tint_mode_t mode;
} palette_t;
static const uint8_t EMBER_TINTS[][3] = {
{255, 214, 138}, {255, 176, 66}, {247, 122, 41}, {224, 74, 39}, {173, 44, 51},
};
static const uint8_t ICE_TINTS[][3] = {
{226, 246, 255}, {160, 220, 250}, {96, 176, 236}, {58, 122, 206}, {44, 74, 158},
};
static const uint8_t ACID_TINTS[][3] = {
{238, 255, 176}, {186, 244, 96}, {118, 214, 74}, {54, 176, 108}, {26, 122, 106},
};
static const uint8_t MATRIX_TINTS[][3] = {
{198, 255, 198}, {120, 246, 120}, {54, 210, 70}, {26, 150, 48}, {12, 92, 30},
};
static const uint8_t AURORA_TINTS[][3] = {
{206, 255, 222}, {110, 240, 170}, {44, 204, 170}, {60, 140, 210}, {110, 84, 200},
};
/* A ramp is read in order, not by brightness: one flock is coloured along it by
* heading, a shade at a time as a bird turns, and several flocks from its ends
* inwards. So prism goes round the rainbow instead of from light to dark, and
* potion is two hues that meet with nothing between them. */
static const uint8_t PRISM_TINTS[][3] = {
{255, 92, 92}, {255, 196, 64}, {96, 220, 110}, {80, 160, 255}, {176, 110, 255},
};
static const uint8_t POTION_TINTS[][3] = {
{170, 255, 110}, {72, 214, 104}, {206, 160, 255}, {160, 104, 240}, {118, 64, 206},
};
static const uint8_t DUSK_TINTS[][3] = {
{255, 214, 170}, {255, 148, 120}, {232, 86, 136}, {160, 70, 170}, {92, 64, 168},
};
static const uint8_t ASH_TINTS[][3] = {
{244, 244, 246}, {206, 208, 214}, {164, 168, 178}, {124, 128, 140}, {88, 92, 104},
};
/* What the embedded sprite is actually painted, for the palettes that leave it
* alone: a hawk still has to stand off that. */
static const uint8_t SPRITE_OWN_COLOUR[3] = {237, 28, 36};
static const palette_t PALETTES[] = {
{"theme", "the terminal's own colours, asked for at startup", 5,
(const uint8_t (*)[3])theme_tints, PNG_TINT_REPLACE},
{"ember", "embers, pale gold to deep red", 5, EMBER_TINTS, PNG_TINT_REPLACE},
{"ice", "ice, white through to deep blue", 5, ICE_TINTS, PNG_TINT_REPLACE},
{"acid", "acid, lime through to teal", 5, ACID_TINTS, PNG_TINT_REPLACE},
{"matrix", "the green of the film it is named after", 5, MATRIX_TINTS, PNG_TINT_REPLACE},
{"aurora", "the northern lights, mint through to violet", 5, AURORA_TINTS, PNG_TINT_REPLACE},
{"prism", "light through a prism, red to violet", 5, PRISM_TINTS, PNG_TINT_REPLACE},
{"potion", "two potions that will not mix, green and violet", 5, POTION_TINTS,
PNG_TINT_REPLACE},
{"dusk", "the sky at dusk, peach through to indigo", 5, DUSK_TINTS, PNG_TINT_REPLACE},
{"ash", "ash, white through to slate grey", 5, ASH_TINTS, PNG_TINT_REPLACE},
};
enum { PALETTE_COUNT = sizeof(PALETTES) / sizeof(*PALETTES) };
static const char *PALETTE_NAMES[PALETTE_COUNT + 1];
/* A PNG of somebody's own, if they gave one: it keeps its own colours, which is
* the whole reason for drawing one. Every palette replaces the colour it is
* given, so the flock used to come out flat ember whatever the artwork was. */
static const char *sprite_path;
/* --picture: a PNG the flock draws. Its colours, cut down to a ramp of the
* program's own kind, are the palette of the run unless --color was given, and
* then the picture's light and dark pick shades of that. See the sign, below. */
static const char *picture_path;
static png_image_t picture_image;
static int picture_colours_in_use;
static int palette_was_asked_for;
static uint8_t picture_tints[MAX_PALETTE_SHADES][3];
static palette_t picture_palette = {"picture", "the colours of the picture", 0, NULL,
PNG_TINT_REPLACE};
/* Named rather than numbered: the table's order is a presentation choice and
* should not be load bearing. */
static int palette_named(const char *name) {
for (int i = 0; i < PALETTE_COUNT; i++)
if (strcmp(PALETTES[i].name, name) == 0) return i;
return 0;
}
static int palette_follows_the_theme(void) {
return !picture_colours_in_use && strcmp(PALETTES[config.palette].name, "theme") == 0;
}
#define FALLBACK_PALETTE palette_named("ember")
static void name_the_palettes(void) {
for (int i = 0; i < PALETTE_COUNT; i++) PALETTE_NAMES[i] = PALETTES[i].name;
PALETTE_NAMES[PALETTE_COUNT] = NULL;
}
static const palette_t *palette(void) {
return picture_colours_in_use ? &picture_palette : &PALETTES[config.palette];
}
static int palette_shades(void) {
if (sprite_path != NULL) return 1; /* One set, untinted, as it was drawn. */
return palette()->shades;
}
/* A hawk is not one of the flock's shades. What has to read instantly is that
* this one is different.
*
* It used to be a dark silhouette, which is what a hawk looks like against the
* sky and what nothing looks like against a black terminal: barely a twentieth of
* a stop above the background, invisible in every recording. Scarlet fixed that
* everywhere except the warm ramps, where scarlet is just another ember — against
* ember the contrast was 1.16, which is no contrast at all. So there are three
* colours and the palette picks: whichever of them stands furthest from the