Repository navigation
Theory Colour
Every colour that means something about a process or a CPU core lives in one 174-line file,
src/palette.rs: four kind colours, three state
colours, three core colours, eight district colours, four stone colours, and two arithmetic helpers, mix and
tint. This page asks what those numbers are, what space the arithmetic happens in, what it costs there, and
how well a viewer can tell the colours apart, including a viewer with colour-vision deficiency. The numbers
are computed from the actual palette values. It ends with the blackbody colour table that
src/blackbody.rs now provides
(#32) and the one change still planned, a colour-blind-safe
palette (#104).
Rows: normal vision; protanopia, deuteranopia, tritanopia (Machado et al. 2009, severity 1.0, section 5).
Below: a mix of teal and amber in the encoded space the code uses, against the same blend in linear light,
with the relative luminance of each step. Colour-vision simulations are indicative.
Colour on a computer display began as a matter of what the cathode-ray tube happened to do. The tube's brightness is roughly a power of the drive voltage, so a byte that was meant to be linear in intensity was not, and the exponent near 2.2 became a convention rather than a design. sRGB wrote that convention down. A proposal dated 5 November 1996 by Michael Stokes and Ricardo Motta of Hewlett-Packard and Matthew Anderson and Srinivasan Chandrasekar of Microsoft asked for a standard default colour space for operating systems, printers and the web, "based on a calibrated colorimetric RGB color space well suited to Cathode Ray Tube (CRT) monitors, television, scanners, digital cameras, and printing systems". The IEC published it as IEC 61966-2-1 in October 1999 (amended in 2003); a small numerical error caused by rounding was corrected on the way, and the viewing conditions were clarified. The W3C's copy of the proposal now carries a note that the document is obsolete and kept for historical purposes only.
The question of what a temperature looks like is older. The colour of heated bodies was a working fact for smiths long before Planck gave the spectrum of a blackbody in 1900. Priest described something like correlated colour temperature in 1923, and Davis introduced the name in 1931, defining it on the Planckian locus, the curve a blackbody's chromaticity follows as its temperature rises. The CIE later fixed the chromaticity diagram in which the nearest point on that locus is sought: the 1960 uniform-chromaticity-scale diagram. The tabulated colour-matching functions that turn a spectrum into a chromaticity are the CIE 1931 standard observer; Wyman, Sloan and Shirley (2013) published a multi-lobe Gaussian fit to them, which is what lets blackbody.rs carry a generator and no tabulated CIE data.
Colour-vision deficiency was studied as a medical and, soon, a military problem. Shinobu Ishihara of the University of Tokyo published his plates in 1917 (some sources say he began them in 1916) at the request of the Japanese military, who wanted to screen conscripts; the plates rely on pseudo-isochromatic pairs that look alike to a dichromat and differ to everyone else. The computational counterpart is Brettel, Viénot and Mollon (1997), who simulated dichromat appearance on a screen, and Machado, Oliveira and Fernandes (2009), who extended it to partial loss in a single matrix on linear RGB, which is the model section 5 uses. Okabe and Ito (2008) turned the same concern into a palette of colours chosen to survive it.
The yardsticks of section 1 are of the same period. CIEDE2000 was adopted by the CIE in 2001 and its implementation notes are Sharma, Wu and Dalal (2005); WCAG 2.0 (2008) set the contrast ratio, and WCAG 2.1 (2018) added the 3:1 threshold for graphical objects that section 2 applies to the kernel colour.
This section is standard colour science, independent of isotop.
A display byte is not proportional to light. The sRGB standard (IEC 61966-2-1) defines the transfer function
that maps an encoded value
Over most of its range it is close to a power law of exponent 2.2. Physics (adding two lights, blurring,
averaging, anti-aliasing, lighting) is linear in light, so it is correct on
and the CIE lightness is
About one man in twelve has a reduced or absent response of one of the three cone classes: L (protan), M
(deutan) or S (tritan). A dichromat's colours collapse onto a surface in colour space; the simulation of
Machado, Oliveira and Fernandes (2009) models that as a
All are render::Color = [u8; 3], sRGB-encoded bytes.
| constant | RGB | hex | contrast on Dusk top / bottom | on Space | on Sea top | meaning | ||
|---|---|---|---|---|---|---|---|---|
KERNEL |
104, 122, 156 | #687a9c |
0.193 | 51.0 | 4.49 / 4.25 | 4.62 | 2.27 | kernel threads: slate |
SYSTEM |
72, 183, 199 | #48b7c7 |
0.394 | 69.0 | 8.20 / 7.78 | 8.45 | 4.16 | system services: teal |
SESSION |
236, 178, 92 | #ecb25c |
0.504 | 76.3 | 10.25 / 9.72 | 10.56 | 5.19 | the user's session: amber |
CONTAINER |
178, 136, 232 | #b288e8 |
0.329 | 64.1 | 7.01 / 6.64 | 7.22 | 3.55 | containers: violet |
STATE_ZOMBIE |
213, 97, 126 | #d5617e |
0.242 | 56.3 | 5.40 / 5.12 | 5.56 | 2.74 | state Z |
STATE_STOPPED |
225, 161, 75 | #e1a14b |
0.420 | 70.9 | 8.69 / 8.24 | 8.95 | 4.40 | state T or t |
STATE_BLOCKED |
222, 94, 73 | #de5e49 |
0.240 | 56.1 | 5.36 / 5.09 | 5.53 | 2.72 | state D |
UNKNOWN_CORE |
140, 160, 196 | #8ca0c4 |
0.347 | 65.5 | 7.34 / 6.96 | 7.56 | 3.72 | a core whose kind is unknown |
PERFORMANCE = SESSION and EFFICIENCY = SYSTEM: performance cores are the session's amber, efficiency
cores the system's teal. DISTRICTS is eight colours, cycled by group % 8 for the City: teal
(72, 183, 199), blue (122, 145, 229), orchid (190, 135, 215), tan (211, 159, 91), sea green (98, 185, 149), rose
(212, 116, 140), pale blue (149, 171, 213), olive (166, 185, 104). stone_color(kind) gives the Necropolis
stones, deliberately duller: slate (104, 112, 132), green granite (112, 138, 134), sandstone (166, 142, 108),
lilac marble (134, 118, 160).
The backdrops (render::shared, in 0 to 255 units): Space is flat (5, 8, 18); Dusk runs from (8, 13, 25) at the
top to (14, 20, 33) at the bottom; Sea from (24, 72, 96) at the top to (6, 22, 38); Void is black. Their
luminances are 0.0025, 0.0041 to 0.0070, 0.057 to 0.0075 and 0. All kind colours are well above 4:1 on Dusk and
Space; slate (KERNEL) is the weakest, 4.25:1 on the lighter bottom of Dusk, and 2.27:1 on the bright top of the
Sea backdrop, below the 3:1 graphical-object threshold. The kind colours are jittered darker (section 3.2),
which lowers the contrast further: the darkest jittered slate, (89, 104, 134), has
Functions:
-
state_color(state):Zzombie,Tortstopped,Dblocked, else none. A state colour overrides the kind colour. -
process_color(process, group)(City): the state colour, orDISTRICTS[group % 8]. -
kind_color(process)(Orbit, Ripple, Flow, Cores, Cells, Strata, Globe, Coop, Ptolemy, Tesseract, Murmuration, Horizon): the state colour, else the kind base brightness-scaled per pid (section 3.2). State colours are not jittered. -
core_color(kind)andcore_name(kind)("performance", "efficiency", "unknown"). - Colours used by only one view stay in that view (for example Reef's own palette), and the terminal
overlay colours (
Tone::Quiet150, 172, 198;Bright214, 226, 240;Tag255, 226, 170 on 12, 19, 31; the popup border 92, 122, 158 on 12, 19, 31; the status text 180, 199, 218) are interminal.rs, outsidepalette.rs.
Color is an encoded byte triple, and neither rasterizer applies or removes a transfer function. The CPU
works on the bytes directly (Frame::blend, Frame::ball, tint). The GPU render target is
wgpu::TextureFormat::Rgba8Unorm, not the ...Srgb variant that would encode on write and decode on read, and
its alpha blending is the plain BlendState::ALPHA_BLENDING on those values; the sphere shader reproduces the
CPU's integer arithmetic (clamp(floor(color * 255 * light), 0, 255)). So every operation below is an
operation on encoded values. That is a design choice with consequences, which this section quantifies. (The
CPU/GPU agreement it preserves is described on GPU and parity.)
tint(color, scale) is (v as f32 * scale).clamp(0.0, 255.0) as u8 per channel: a multiplication of the
encoded value, saturating at 255, truncating. Because an encoded value is approximately SESSION (relative luminance 0.5045):
scale |
result | pure |
||
|---|---|---|---|---|
| 0.50 | 118, 89, 46 | 0.112 | 0.222 | 0.218 |
| 0.72 | 169, 128, 66 | 0.243 | 0.481 | 0.485 |
| 0.86 | 202, 153, 79 | 0.359 | 0.712 | 0.718 |
| 1.00 | 236, 178, 92 | 0.504 | 1.000 | 1.000 |
| 1.10 | 255, 195, 101 | 0.612 | 1.214 | 1.233 |
| 1.20 | 255, 213, 110 | 0.700 | 1.387 | 1.493 |
For SPHERE_AMBIENT, SPHERE_LIGHT_*),
is a tint with a scale in
kind_color scales its base by 0.86 + 0.24 * j, where j = (hash::knuth(pid) >> 24) / 255 takes 256 values in
| kind | darkest | brightest |
|
luminance ratio |
|---|---|---|---|---|
KERNEL |
89, 104, 134 | 114, 134, 171 | 12.0 | 1.71 |
SYSTEM |
61, 157, 171 | 79, 201, 218 | 12.4 | 1.73 |
SESSION |
202, 153, 79 | 255, 195, 101 | 12.1 | 1.71 |
CONTAINER |
153, 116, 199 | 195, 149, 255 | 12.5 | 1.72 |
A jitter of about 12
The jitter hashes the pid alone (// pid only: kept for pixel stability, see A33), so a reused pid gets its
predecessor's shade; see Determinism.
mix(a, b, t) is per channel (a + (b - a) * t) as u8: linear interpolation of the encoded bytes,
truncated. It is exact at the ends (t = 0 gives a, t = 1 gives b, because the arithmetic is exact
on small integers) and extrapolates, then saturates, outside
Midpoints are darker than a physical mixture. Averaging light is averaging
| pair, |
mix result |
mix
|
linear-light blend |
|
ratio |
|---|---|---|---|---|---|
| white, black | 127, 127, 127 | 0.212 | 188, 188, 188 | 0.503 | 0.42 |
| red, green | 127, 127, 0 | 0.197 | 188, 188, 0 | 0.467 | 0.42 |
SESSION, CONTAINER
|
207, 157, 162 | 0.400 | 210, 159, 180 | 0.418 | 0.96 |
SYSTEM, KERNEL
|
88, 152, 177 | 0.277 | 90, 156, 179 | 0.292 | 0.95 |
SYSTEM, SESSION ( |
154, 180, 145 | 0.416 | 179, 181, 157 | 0.451 | 0.92 |
The palette's colours were chosen close in lightness, so mix loses only 4 to 8 percent of the blended
luminance; between very different lightnesses it loses more than half (white against black, the figure's
bottom panel). Gradients, fades and glow falloffs between a bright and a dark colour dip in the middle, which
reads as a faint outline around bright objects on dark grounds. It is also what the GPU draws too, so the
two renderers agree.
Truncation is biased. as u8 rounds toward zero, so the expected error is
Frame::blend(x, y, color, weight) is channel += (target - channel) * weight, truncated: the same
encoded-space lerp, used by halos (strength * (1 - d)^2 falloff), rays and the pressure haze. The sky haze
is added in 0 to 255 units (min(base + haze * density, 255)) before the floor. None of these are physically
additive in light; they are compositing operators on display values, consistent between the backends.
-
Hue carries kind, lightness carries identity noise. The four kind colours have
$L^*$ of 51 (slate), 64 (violet), 69 (teal) and 76 (amber): three of them within 12 units of each other, so lightness is a poor channel for kind even before the jitter. The two core colours are the teal and the amber. - Slate is a ground colour. At 4.25:1 on Dusk bottom and 2.27:1 on the Sea backdrop it recedes, which suits kernel threads (many, uninteresting) and fails for any view that puts them on the lighter backdrops.
-
Amber is the lightest of the four (
$L^*$ 76, luminance 0.50), the user's own processes, which fits the mapping; it is also nearly the colour of the stopped state (section 5), which does not. -
Simultaneous contrast and adaptation are not modelled: colours are chosen against the dark backdrops
only, and the selection ring (237, 220, 154) is contrasted with them only by being bright: against
SESSIONits contrast is 1.38:1 ($\Delta E_{00}$ 14.2), the weakest of the kinds; againstKERNEL3.15:1.
All pairs among the eight colours of the table (four kinds, three states, the unknown core), by
| vision | closest pairs among all eight ( |
|---|---|
| normal |
SESSION-STOPPED 4.5; KERNEL-UNKNOWN_CORE 13.1; CONTAINER-UNKNOWN_CORE 14.0; ZOMBIE-BLOCKED 16.3 |
| protanopia |
SESSION-STOPPED 4.6; SYSTEM-UNKNOWN_CORE 5.8; CONTAINER-UNKNOWN_CORE 10.1; KERNEL-ZOMBIE 11.0 |
| deuteranopia |
SYSTEM-UNKNOWN_CORE 1.6; SESSION-STOPPED 3.8; CONTAINER-UNKNOWN_CORE 8.0; SYSTEM-CONTAINER 8.7 |
| tritanopia |
SESSION-STOPPED 4.4; ZOMBIE-BLOCKED 4.5; KERNEL-UNKNOWN_CORE 13.0 |
Reading the table:
- The four kind colours alone are well separated: the minimum over their six pairs is 18.2 (normal, slate against violet), 14.7 (protan), 8.7 (deutan, teal against violet) and 20.9 (tritan, slate against teal). For comparison the seven chromatic colours of the Okabe-Ito palette (Okabe and Ito 2008, built for exactly this purpose) have minima of 21.7, 12.3, 11.5 and 11.1. The shipped kinds are already competitive with a purpose-built palette, which is why the issue calls for checking rather than redesigning them.
-
STATE_STOPPEDcollides withSESSIONfor everyone: (225, 161, 75) against (236, 178, 92),$\Delta E_{00}$ 4.5. Worse,kind_colorjitters the session amber by 0.86 to 1.10, and the stopped colour lies inside that range: the nearest jittered session shade is (220, 165, 85), at$\Delta E_{00}$ 2.3 from the stopped colour, and for 63 percent of the 256 jitter values the difference is under 5, for 23 percent under 3. In Orbit, Ripple, Flow and every other view that useskind_color, a stopped process in the user's session is, by colour alone, indistinguishable from a running one, although the state colour exists to tell them apart. -
UNKNOWN_COREis a steel blue between slate and teal, and a deuteranope sees it 1.6 away from the efficiency-core teal. It only matters if both appear in one view (a part-hybrid machine reporting some cores as unknown). -
ZOMBIEandBLOCKED(rose against red-orange) are 16.3 apart normally and 4.5 for a tritanope. - The eight district colours are a categorical scale beyond what hue alone can carry: their closest pair is 8.6 apart normally, and 2.3 (protan), 2.4 (deutan), 3.7 (tritan) under simulation. Districts are spatially separate blocks of plots, which is what rescues the City.
Planned, not yet implemented. Specified in issue #104 ("F10: Colour-blind safe palette"). There is no
--paletteoption inmain.rs.
The issue asks for --palette default|safe; the active palette is chosen once at startup so that no view takes
a palette argument. The safe palette is to be chosen "by measurement, not by eye": the four kind colours, the
core performance/efficiency colours and the three state colours must stay distinguishable under protanopia,
deuteranopia and tritanopia simulation (Machado et al. 2009 matrices) with a minimum pairwise safe_palette_is_distinguishable_under_common_colour_vision_deficiencies)
so later edits cannot regress it. The default palette's pixels must not change (the golden hashes hold with
--palette default), and legends that name colours ("teal = session") are to be generated from palette.rs.
Section 5 shows what the test will find on the shipped palette: the stopped/session pair already fails any
threshold above about 4.5, and the unknown-core/system pair fails under deuteranopia. That is a concrete
reason the check should include the state against kind pairs, which the issue's list does.
A star's colour is its temperature. src/blackbody.rs
is a pure module mapping MINIMUM_KELVIN, MAXIMUM_KELVIN) to a colour. The Horizon view
draws its accretion disk and its stars with blackbody::linear and blackbody::color
(src/horizon/disk.rs); View HR uses it as well. This section gives the physics, then the
recipe as the module's header states it, then a check of the table.
Physics. A blackbody at temperature
The recipe in the module. Multi-lobe Gaussian fit of the CIE 1931 2-degree observer by Wyman, Sloan and
Shirley (2013) (the constants of the lobes are in the test generated, which the table must match); integrate 360 to 830 nm in 1 nm steps in f64;
convert with the IEC 61966-2-1 matrix to linear sRGB; clip negative channels to 0; divide by the largest
channel; store 256 entries at color(kelvin, brightness), scale by the
brightness then apply the sRGB transfer function and round (encode), because frames hold sRGB-encoded bytes
and linear values must never go straight into a Color. This is exactly the point of section 3.1: the table is
in linear light, the frame is in encoded bytes, and color is where a blackbody crosses from one to the other.
There is no chromatic adaptation: the colour is that of the light itself, white point and all.
A check of the recipe. The prototype technique in docs/mockups/code/hr.py is the recipe in NumPy. I
re-implemented it in the standard library (the same lobe constants, 1 nm steps, same matrix) and computed the
following; the module's own table gives the same two end rows (1 000 K [255, 47, 0], 40 000 K
[158, 184, 255], pinned by blackbody_colours_match_published_values):
|
|
linear, max-normalised | encoded | hex | |
|---|---|---|---|---|
| 1 000 | 1, 0.029, 0 | 255, 47, 0 | #ff2f00 |
0.233 |
| 2 000 | 1, 0.266, 0.008 | 255, 141, 21 | #ff8d15 |
0.404 |
| 3 000 | 1, 0.484, 0.155 | 255, 185, 110 | #ffb96e |
0.570 |
| 4 000 | 1, 0.659, 0.379 | 255, 212, 166 | #ffd4a6 |
0.711 |
| 5 000 | 1, 0.795, 0.630 | 255, 231, 208 | #ffe7d0 |
0.827 |
| 6 500 | 1, 0.945, 0.993 | 255, 249, 254 | #fff9fe |
0.960 |
| 10 000 | 0.610, 0.695, 1 | 205, 217, 255 | #cdd9ff |
0.699 |
| 20 000 | 0.407, 0.536, 1 | 171, 193, 255 | #abc1ff |
0.542 |
| 40 000 | 0.344, 0.480, 1 | 158, 184, 255 | #9eb8ff |
0.489 |
Validation against an independent source: the CIE chromaticity
The module's test compares six temperatures (2 000 to 20 000 K) with a published table of blackbody colours (Mitchell Charity's "What color is a blackbody?", the CIE 1931 2-degree observer) within 6 levels per channel. That table is chromatically adapted and the module's is not, so at 1 000 K and 40 000 K the green channel differs by 9 and 8 levels; the test pins those two rows instead of tolerating them.
What the table shows about the design:
-
Below about 1 910 K the blue channel is negative before clipping: the blackbody colour is outside the sRGB
gamut, and clipping to zero (as the recipe specifies) is a hue shift. At 1 000 K the raw blue is
$-0.022$ of the red. - Normalising to the brightest channel throws away brightness, but the colours' luminance is not constant: 0.23 at 1 000 K, 0.96 at 6 500 K, 0.49 at 40 000 K. A view that wants brightness to encode luminosity (the HR diagram) must scale separately, and a view that does not will make mid-temperature stars look brightest.
-
At 6 500 K the colour is almost white
(255, 249, 254); the whole blue half of the locus is the pale band from#cdd9ffto#9eb8ff, which at the 8-bit resolution of the display is a modest range: about 96 of the 256 table steps (10 000 K to 40 000 K) lie within 47 levels of red and 33 of green.
# the City uses process_color (state or district), the others kind_color (state or kind with jitter)
./target/release/isotop --demo --view city --time 12 --width 1200 --renderer cpu --output city.png
./target/release/isotop --demo --view orbit --time 12 --width 1200 --renderer cpu --output orbit.png
# tests that pin the palette
cargo test palette_kind_colours_match_their_previous_literals
cargo test mix_blends_each_channel_and_truncatesThe demo has a stopped process (index 9, 'T'), a zombie every 41st, and blocked ones, so every state colour
is on screen in a demo frame. Regenerate the figure with python3 docs/figures/colour-palette.py from the
repository root.
-
src/palette.rs: the constants,mix,tint,state_color,process_color,kind_color,core_color,core_name,stone_color,DISTRICTS. -
src/blackbody.rs:TABLE,linear,color,MINIMUM_KELVIN,MAXIMUM_KELVIN; its users are insrc/horizon/disk.rs. -
src/render.rs:Color,Sky,Frame::blend,Frame::ball(sphere light),Frame::halo,Frame::ray. -
src/render/shared.rs: sky colours,SPHERE_AMBIENT,SPHERE_LIGHT_*,SELECTION_COLOR. -
src/shaders.wgslandsrc/gpu.rs(COLOR = Rgba8Unorm). -
src/terminal.rs: the 24-bit overlay colours.
-
A linear-light
mix. Decode, blend, encode. Cost: two table lookups per channel. It removes the midpoint dip of section 3.3 and changes every golden hash; a candidate for a dedicated PR with before and after renders. -
Separate the stopped state from the session kind. Give
STATE_STOPPEDa colour outside the jitter range of every kind (for example a desaturated cool grey), or draw the state as a ring or hatch. Section 5 quantifies the collision. -
Jitter in lightness only along a perceptual axis. Replace
tintby a change of$L^*$ at fixed hue so that the jitter does not shift saturation when a channel clips at 255. -
Run the CVD test now. The planned test of #104 on the current palette would fail on the pairs
above; fixing the stopped/session pair is independent of the
--paletteswitch. -
Blackbody for CPU. View HR maps CPU to temperature for its stars; what are the perceptual
consequences of a luminance that is non-monotone in
$T$ (section 7)?
- IEC 61966-2-1:1999. Multimedia systems and equipment - Colour measurement and management - Part 2-1: Colour management - Default RGB colour space - sRGB.
- W3C. 2018. Web Content Accessibility Guidelines (WCAG) 2.1. (Relative luminance and contrast ratio, success criteria 1.4.3 and 1.4.11.)
- Sharma, G., Wu, W. and Dalal, E. N. 2005. "The CIEDE2000 color-difference formula: implementation notes, supplementary test data, and mathematical observations." Color Research and Application 30(1): 21-30.
- Machado, G. M., Oliveira, M. M. and Fernandes, L. A. F. 2009. "A physiologically-based model for simulation of color vision deficiency." IEEE Transactions on Visualization and Computer Graphics 15(6): 1291-1298.
- Brettel, H., Vienot, F. and Mollon, J. D. 1997. "Computerized simulation of color appearance for dichromats." Journal of the Optical Society of America A 14(10): 2647-2655.
- Okabe, M. and Ito, K. 2008. "Color Universal Design (CUD): how to make figures and presentations that are friendly to colorblind people."
- Wyman, C., Sloan, P.-P. and Shirley, P. 2013. "Simple analytic approximations to the CIE XYZ color matching functions." Journal of Computer Graphics Techniques 2(2): 1-11.
- Kim, Y. S., Cho, B. H., Kim, S. H., Kim, M. J. and Ko, H. S. 2002. "Design of advanced color temperature control system for HDTV applications." Journal of the Korean Physical Society 41: 865-871 (unverified).
- Harrower, M. and Brewer, C. A. 2003. "ColorBrewer.org: an online tool for selecting colour schemes for maps." The Cartographic Journal 40(1): 27-37.
isotop — maximalism for its own sake. Source: tfpickard/isotop. This wiki is published from wiki/ in the repository; edit it there. This footer is decorative.
Philosophy · Gallery · Glossary
Theory
Overview
Sampling: Linux
Sampling: macOS
Background sampler
Model and rates
Life events
History
Journal
Scene and display list
Camera and projection
Rasterization
GPU and parity
Picking
Terminal graphics
Stable layout
Determinism
Simulation toolkit
Colour
Text safety
Mathematics
Fourier epicycles
Wave equation
Flocking
Spatial hashing
Stellar photometry
Lorentz and Schlegel
Reaction-diffusion
Spiking neurons
Schwarzschild
Views
City · Orbit · Ripple
Flow · Cores · Cells
Strata · Globe · Reef
Coop · Matrix
Ptolemy · Tesseract
Necropolis · Cortex
Horizon · Murmuration
Arbor · HR
Hive · Switchboard
Armorial · Lights
Planned
Mold · Chamber
Anthill · Plumbing
Morph · Ouija
Canyon · Die
Guides
Writing a view
Recipes
Headless renders
Contributing to the wiki