alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates client src gui mod.rs
mod layout;
mod menu;
mod progression;
use bytemuck::{Pod, Zeroable};
use common::inventory::ContainerKind;
use common::item::{Item, ItemStack};
use common::world::texture;
use glam::{Mat4, Vec3};
use crate::assets::RgbaImage;
use crate::interaction::{Interaction, Screen};
use crate::menu::contains;
use crate::player::LocalPlayer;
use crate::render::item;
pub use layout::{button_at, slot_at};
const ATLAS: i32 = 0;
pub const WIDGETS: i32 = 3;
pub const ICONS: i32 = 4;
pub const ITEMS: i32 = 5;
pub const FONT: i32 = 6;
pub const INVENTORY: i32 = 7;
pub const CRAFTING: i32 = 8;
pub const CHEST: i32 = 9;
pub const FURNACE: i32 = 10;
const DISPENSER: i32 = crate::assets::entity_texture("gui/trap.png");
const SIGN: i32 = crate::assets::entity_texture("item/sign.png");
const PUMPKIN_BLUR: i32 = crate::assets::entity_texture("misc/pumpkinblur.png");
const VIGNETTE: i32 = crate::assets::entity_texture("misc/vignette.png");
/// Texture flags of the GUI shader.
const BLUR: i32 = 1 << 16;
const DARKEN: i32 = 1 << 17;
/// Drawn with the inverting blend of the crosshair.
const INVERT: i32 = 1 << 18;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct Vertex {
position: [f32; 2],
uv: [f32; 2],
color: u32,
texture: i32,
}
pub struct Geometry {
pub vertices: Vec<Vertex>,
pub size: [f32; 2],
/// `ScaledResolution.scaledWidthD/scaledHeightD`: the unrounded size the GUI is projected with.
pub projection: [f32; 2],
pub portrait: Option<Portrait>,
/// The crosshair's vertices, drawn with `GL_ONE_MINUS_DST_COLOR`, `GL_ONE_MINUS_SRC_COLOR`.
pub crosshair: std::ops::Range<u32>,
widths: [u8; 256],
/// `GuiIngame.prevVignetteBrightness`.
vignette: f32,
translations: rustc_hash::FxHashMap<String, String>,
}
#[derive(Clone, Copy)]
pub struct Portrait {
pub origin: [f32; 2],
pub cursor: [f32; 2],
pub split: u32,
}
/// What the full-screen overlays depend on besides the player.
#[derive(Clone, Copy)]
pub struct Overlay {
/// The light at the player's eyes for the vignette, which only fancy graphics draw.
pub brightness: Option<f32>,
pub partial: f32,
/// `isInsideOfMaterial(Material.water)`: the air meter shows.
pub underwater: bool,
}
pub fn scaled_size([width, height]: [u32; 2], gui_scale: u8) -> ([f32; 2], f32) {
let mut scale = 1;
while (gui_scale == 0 || scale < u32::from(gui_scale))
&& width / (scale + 1) >= 320
&& height / (scale + 1) >= 240
{
scale += 1;
}
(
[width.div_ceil(scale) as f32, height.div_ceil(scale) as f32],
scale as f32,
)
}
impl Geometry {
pub fn new(font: &RgbaImage, translations: rustc_hash::FxHashMap<String, String>) -> Self {
let widths = std::array::from_fn(|i| {
if i == 32 {
return 4;
}
(0..8)
.rev()
.find(|&x| {
(0..8).any(|y| {
let px = (i % 16 * 8 + x) * font.width as usize / 128;
let py = (i / 16 * 8 + y) * font.height as usize / 128;
font.pixels[(py * font.width as usize + px) * 4 + 2] != 0
})
})
.map_or(1, |x| x as u8 + 2)
});
Self {
vertices: Vec::with_capacity(12000),
size: [0.0; 2],
projection: [0.0; 2],
portrait: None,
crosshair: 0..0,
widths,
vignette: 1.0,
translations,
}
}
fn quad(&mut self, points: [[f32; 2]; 4], uv: [f32; 4], texture: i32, color: u32) {
let [u, v, w, h] = uv;
let uvs = [[u, v], [u + w, v], [u + w, v + h], [u, v + h]];
for i in [0, 1, 2, 0, 2, 3] {
self.vertices.push(Vertex {
position: points[i],
uv: uvs[i],
color,
texture,
});
}
}
fn tinted(&mut self, rect: [f32; 4], uv: [f32; 4], texture: i32, color: u32) {
let size = if texture == FONT { 128.0 } else { 256.0 };
self.quad(corners(rect), uv.map(|v| v / size), texture, color);
}
fn sprite(&mut self, rect: [f32; 4], uv: [f32; 4], texture: i32) {
self.tinted(rect, uv, texture, 0xffffffff);
}
/// A 9 by 9 sprite of `gui/icons.png`.
fn icon(&mut self, [x, y]: [f32; 2], [u, v]: [f32; 2]) {
self.sprite([x, y, 9.0, 9.0], [u, v, 9.0, 9.0], ICONS);
}
fn fill(&mut self, rect: [f32; 4], color: u32) {
self.tinted(rect, [0.0; 4], -1, color);
}
/// `FontRenderer.drawString` takes whole GUI pixels; fractional origins would distort the glyphs.
fn text(&mut self, text: &str, x: f32, y: f32, mut color: u32, shadow: bool) {
let (x, y) = (x.floor(), y.floor());
// `FontRenderer.drawString`: nearly transparent text is drawn opaque
if color & 0xfc000000 == 0 {
color |= 0xff000000;
}
if shadow {
self.text_run(text, x + 1.0, y + 1.0, color, true);
}
self.text_run(text, x, y, color, false);
}
fn text_run(&mut self, text: &str, x: f32, y: f32, color: u32, shadow: bool) {
for (ch, offset, color) in self.layout(text, color).0 {
let color = if shadow {
(color & 0xff000000) | ((color & 0xfcfcfc) >> 2)
} else {
color
};
self.tinted(
[x + offset, y, 7.99, 7.99],
cell(ch, 8.0, 7.99),
FONT,
color,
);
}
}
fn text_width(&self, text: &str) -> f32 {
let mut width = 0.0;
let mut chars = text.chars();
while let Some(ch) = chars.next() {
if ch == '§' {
chars.next();
} else {
width += f32::from(self.widths[usize::from(glyph(ch))]);
}
}
width
}
/// The full-screen effects under the HUD: the vignette in fancy graphics, the view through a
/// pumpkin, and the swirl of a portal.
fn overlays(&mut self, interaction: &Interaction, player: &LocalPlayer, overlay: Overlay) {
let [w, h] = self.size;
let screen = [[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]];
let whole = [0.0, 0.0, 1.0, 1.0];
if let Some(brightness) = overlay.brightness {
let darkness = (1.0 - brightness).clamp(0.0, 1.0);
self.vignette =
(f64::from(self.vignette) + f64::from(darkness - self.vignette) * 0.01) as f32;
self.quad(screen, whole, VIGNETTE | BLUR | DARKEN, gray(self.vignette));
}
let helmet = interaction.inventory.armor[3].map(|stack| stack.item);
if !player.third_person && helmet == Some(Item::from_block(common::world::Block::PUMPKIN)) {
self.quad(screen, whole, PUMPKIN_BLUR | BLUR, 0xffffffff);
}
let portal = &player.entity.portal;
let mut time = portal.prev_time + (portal.time - portal.prev_time) * overlay.partial;
if time > 0.0 {
if time < 1.0 {
time *= time;
time *= time;
time = time * 0.8 + 0.2;
}
let tile = texture::inventory_face(common::world::Block::PORTAL, 0, 0);
let alpha = ((time * 255.0) as u32) << 24;
self.quad(
screen,
cell(tile, 1.0 / 16.0, 1.0 / 16.0),
ATLAS,
alpha | 0xffffff,
);
}
}
/// The F3 screen: renderer and world counters, then the player's position and facing.
pub fn debug(&mut self, info: &[String], position: &[String]) {
for (i, line) in info.iter().enumerate() {
self.text(line, 2.0, 2.0 + i as f32 * 10.0, 0xffffffff, true);
}
for (i, line) in position.iter().enumerate() {
self.text(line, 2.0, 64.0 + i as f32 * 8.0, 0xffe0e0e0, true);
}
}
/// The record message above the hotbar, cycling through colors as it fades.
pub fn record(&mut self, message: &str, ticks: f32) {
let alpha = ((ticks * 256.0 / 20.0) as i32).min(255);
if alpha <= 0 {
return;
}
let [w, h] = self.size;
let [r, g, b] = hsb_to_rgb(ticks / 50.0, 0.7, 0.6);
let color = (alpha as u32) << 24 | r | g << 8 | b << 16;
let x = self.centered_x(message, w / 2.0);
self.text(message, x, (h - 48.0).floor() - 4.0, color, false);
}
/// Text laid out like `FontRenderer`, starting in `color`: each glyph with its x offset
/// and the color its `§` codes give it, and the width of the whole line.
pub fn layout(&self, text: &str, mut color: u32) -> (Vec<(u8, f32, u32)>, f32) {
let mut x = 0.0;
let mut glyphs = Vec::with_capacity(text.len());
let mut chars = text.chars();
while let Some(ch) = chars.next() {
if ch == '§'
&& let Some(code) = chars.next()
{
color = code_color(code, color);
continue;
}
let ch = glyph(ch);
glyphs.push((ch, x, color));
x += f32::from(self.widths[usize::from(ch)]);
}
(glyphs, x)
}
/// The atlas rectangle of a glyph in `font/default.png`, as normalized u, v, width, height.
pub fn glyph_uv(ch: u8) -> [f32; 4] {
cell(ch, 1.0 / 16.0, 7.99 / 128.0)
}
/// `GuiEditSign.drawScreen`: the sign seen from the front, its lines written on it, with
/// arrows blinking around the line being edited.
fn sign_editor(&mut self, editor: &crate::menu::SignEditor, blink: bool) {
let [w, _] = self.size;
let center = (w / 2.0).floor();
let scale = 93.75;
let post = editor.sign.block == common::world::Block::SIGN;
// Height of the sign's origin below the top of the screen, in blocks
let origin = if post { 1.3125 } else { 1.625 };
let rect = |x0: f32, x1: f32, y0: f32, y1: f32| {
[
center + x0 * scale,
(origin + y0) * scale,
(x1 - x0) * scale,
(y1 - y0) * scale,
]
};
let texture = |[u, v, uw, vh]: [f32; 4]| [u / 64.0, v / 32.0, uw / 64.0, vh / 32.0];
let board = corners(rect(-0.5, 0.5, -14.0 / 24.0, -2.0 / 24.0));
self.quad(board, texture([2.0, 2.0, 24.0, 12.0]), SIGN, 0xffffffff);
if post {
let stick = corners(rect(-1.0 / 24.0, 1.0 / 24.0, -2.0 / 24.0, 12.0 / 24.0));
self.quad(stick, texture([2.0, 16.0, 2.0, 14.0]), SIGN, 0xffffffff);
}
let unit = scale / 90.0;
let top = (origin - 1.0 / 3.0) * scale;
for (i, line) in editor.lines.iter().enumerate() {
let line = if blink && i == editor.line {
format!("> {line} <")
} else {
line.clone()
};
let (glyphs, width) = self.layout(&line, 0xff000000);
let left = center - (width / 2.0).floor() * unit;
let y = top + (i as f32 * 10.0 - 20.0) * unit;
for (ch, offset, color) in glyphs {
let size = 7.99 * unit;
let points = corners([left + offset * unit, y, size, size]);
self.quad(points, Self::glyph_uv(ch), FONT, color);
}
}
}
fn centered(&mut self, text: &str, x: f32, y: f32) {
self.text(text, self.centered_x(text, x), y, 0xffffffff, true);
}
/// `drawCenteredString`'s left edge, with its integer halving.
fn centered_x(&self, text: &str, x: f32) -> f32 {
x.floor() - (self.text_width(text) / 2.0).floor()
}
fn item(&mut self, stack: ItemStack, x: f32, y: f32) {
self.item_icon(stack, x, y);
self.item_overlay(stack, x, y);
}
/// `RenderItem.renderItemIntoGUI`.
fn item_icon(&mut self, stack: ItemStack, x: f32, y: f32) {
if let Some(block) = stack.item.block() {
let texture = texture::inventory_face(block, stack.damage as u8, 0);
if item::in_3d(block) {
let transform = Mat4::from_translation(Vec3::new(x + 8.0, y + 8.0, 0.0))
* Mat4::from_scale(Vec3::new(10.0, 10.0, -10.0))
* Mat4::from_rotation_x(210_f32.to_radians())
* Mat4::from_rotation_y(-45_f32.to_radians());
let mut faces: Vec<_> = item::faces(block, stack.damage as u8)
.into_iter()
.map(|face| {
let points = face.points.map(|p| transform.transform_point3(p));
(face, points)
})
.collect();
faces.sort_by(|a, b| {
let depth = |p: &[Vec3; 4]| p.iter().map(|p| p.z).sum::<f32>();
depth(&a.1).total_cmp(&depth(&b.1))
});
for (face, points) in faces {
let a = points[1] - points[0];
let b = points[2] - points[0];
if a.x * b.y - a.y * b.x >= 0.0 {
continue;
}
let color = item::color(item::tint(block, stack.damage as u8), face.shade);
for i in [0, 2, 1, 0, 3, 2] {
self.vertices.push(Vertex {
position: [points[i].x, points[i].y],
uv: face.uv[i],
texture: 0,
color,
});
}
}
} else {
self.sprite([x, y, 16.0, 16.0], cell(texture, 16.0, 15.99), ATLAS);
}
} else if stack.item.properties().is_some() {
self.sprite(
[x, y, 16.0, 16.0],
cell(item::icon(stack), 16.0, 16.0),
ITEMS,
);
}
}
/// `RenderItem.renderItemOverlayIntoGUI`: the stack size and the damage bar.
fn item_overlay(&mut self, stack: ItemStack, x: f32, y: f32) {
if stack.count > 1 {
let count = stack.count.to_string();
let left = x + 17.0 - self.text_width(&count);
self.text(&count, left, y + 9.0, 0xffffffff, true);
}
if stack.is_damaged() {
let fraction = stack.damage as f32 / stack.max_damage() as f32;
let width = (13.0 - fraction * 13.0).round();
let green = (255.0 - fraction * 255.0).round() as u32;
self.fill([x + 2.0, y + 13.0, 13.0, 2.0], 0xff000000);
let shade = 0xff000000 | ((255 - green) / 4) | (63 << 8);
self.fill([x + 2.0, y + 13.0, 12.0, 1.0], shade);
let color = 0xff000000 | (255 - green) | green << 8;
self.fill([x + 2.0, y + 13.0, width, 1.0], color);
}
}
pub fn build(
&mut self,
interaction: &Interaction,
player: &LocalPlayer,
extent: [u32; 2],
gui_scale: u8,
overlay: Overlay,
) {
let scale = self.begin(extent, gui_scale);
let [w, h] = self.size;
self.overlays(interaction, player, overlay);
let rect = [w / 2.0 - 91.0, h - 22.0, 182.0, 22.0];
self.sprite(rect, [0.0, 0.0, 182.0, 22.0], WIDGETS);
let selected = w / 2.0 - 92.0 + interaction.inventory.selected as f32 * 20.0;
self.sprite(
[selected, h - 23.0, 24.0, 22.0],
[0.0, 22.0, 24.0, 22.0],
WIDGETS,
);
let start = self.vertices.len() as u32;
let rect = [w / 2.0 - 7.0, h / 2.0 - 7.0, 16.0, 16.0];
self.sprite(rect, [0.0, 0.0, 16.0, 16.0], ICONS | INVERT);
self.crosshair = start..self.vertices.len() as u32;
for (i, stack) in interaction.inventory.main[..9].iter().enumerate() {
if let Some(stack) = stack {
let (x, y) = (w / 2.0 - 88.0 + i as f32 * 20.0, h - 19.0);
let start = self.vertices.len();
self.item_icon(*stack, x, y);
// `GuiIngame.renderInventorySlot`: a picked up stack pops
let pop = f32::from(interaction.pops[i]) - overlay.partial;
if pop > 0.0 {
let stretch = 1.0 + pop / 5.0;
let scale = [1.0 / stretch, (stretch + 1.0) / 2.0];
let pivot = [x + 8.0, y + 12.0];
for vertex in &mut self.vertices[start..] {
for axis in 0..2 {
vertex.position[axis] =
pivot[axis] + (vertex.position[axis] - pivot[axis]) * scale[axis];
}
}
}
self.item_overlay(*stack, x, y);
}
}
let health = player.entity.living.as_ref().map_or(20, |l| l.health);
let previous_health = player.entity.living.as_ref().map_or(20, |l| l.prev_health);
let flash = player.entity.hearts_life >= 10 && player.entity.hearts_life / 3 % 2 == 1;
let mut random =
common::JavaRandom::new(i64::from(player.entity.ticks_existed.wrapping_mul(312871)));
let armor = interaction.inventory.armor_value();
for i in 0..10 {
let x = w / 2.0 - 91.0 + i as f32 * 8.0;
let y = h - 32.0
+ if health <= 4 {
random.next_int(2) as f32
} else {
0.0
};
self.icon([x, y], [if flash { 25.0 } else { 16.0 }, 0.0]);
if flash && previous_health > i * 2 {
let u = if previous_health == i * 2 + 1 {
79.0
} else {
70.0
};
self.icon([x, y], [u, 0.0]);
}
if health > i * 2 {
self.icon([x, y], [if health == i * 2 + 1 { 61.0 } else { 52.0 }, 0.0]);
}
if armor > 0 {
let u = match armor.cmp(&(i * 2 + 1)) {
std::cmp::Ordering::Greater => 34.0,
std::cmp::Ordering::Equal => 25.0,
std::cmp::Ordering::Less => 16.0,
};
self.icon([w / 2.0 + 82.0 - i as f32 * 8.0, h - 32.0], [u, 9.0]);
}
}
if overlay.underwater {
let full = ((player.entity.air - 2) as f64 * 10.0 / 300.0).ceil() as i32;
let bubbles = (player.entity.air as f64 * 10.0 / 300.0).ceil() as i32;
for i in 0..bubbles {
let u = if i < full { 16.0 } else { 25.0 };
self.icon([w / 2.0 - 91.0 + i as f32 * 8.0, h - 41.0], [u, 18.0]);
}
}
let timer = player.entity.sleep.timer;
if timer > 0 {
let fade = if timer > 100 {
1.0 - (timer - 100) as f32 / 10.0
} else {
timer as f32 / 100.0
};
self.fill([0.0, 0.0, w, h], ((220.0 * fade) as u32) << 24 | 0x101010);
}
let cursor = interaction.cursor.map(|v| (v / scale).floor());
match interaction.screen {
Some(Screen::Inventory) => self.container(interaction, cursor),
Some(Screen::Death) => {
// `GuiGameOver.drawScreen`
self.gradient([0.0, 0.0, w, h], 0x60000050, 0xa0303080);
let start = self.vertices.len();
self.centered("Game over!", (w as i32 / 2 / 2) as f32, 30.0);
for vertex in &mut self.vertices[start..] {
vertex.position = vertex.position.map(|v| v * 2.0);
}
self.centered("Score: &e0", w / 2.0, 100.0);
for (i, text) in ["Respawn", "Title menu"].into_iter().enumerate() {
let rect = layout::button_rect(self.size, Screen::Death, i);
let hover = contains(rect, cursor);
let v = if hover { 86.0 } else { 66.0 };
self.sprite(rect, [0.0, v, 200.0, 20.0], WIDGETS);
let color = if hover { 0xffa0ffff } else { 0xffe0e0e0 };
let x = self.centered_x(text, w / 2.0);
self.text(text, x, rect[1] + 6.0, color, true);
}
}
None => {}
}
}
fn container(&mut self, interaction: &Interaction, cursor: [f32; 2]) {
let kind = interaction.container.kind;
let [x, y, w, h] = layout::container_rect(self.size, kind);
// `GuiScreen.drawDefaultBackground` in a world
let screen = [0.0, 0.0, self.size[0], self.size[1]];
self.gradient(screen, 0xc0101010, 0xd0101010);
let texture = match kind {
ContainerKind::Inventory => INVENTORY,
ContainerKind::Workbench => CRAFTING,
ContainerKind::Chest { .. } => CHEST,
ContainerKind::Furnace => FURNACE,
ContainerKind::Dispenser => DISPENSER,
};
if let ContainerKind::Chest { slots } = kind {
let top = (slots / 9 * 18 + 17) as f32;
self.sprite([x, y, w, top], [0.0, 0.0, w, top], texture);
self.sprite([x, y + top, w, 96.0], [0.0, 126.0, w, 96.0], texture);
} else {
self.sprite([x, y, w, h], [0.0, 0.0, w, h], texture);
}
if kind == ContainerKind::Inventory {
self.portrait = Some(Portrait {
origin: [x + 51.0, y + 75.0],
cursor,
split: self.vertices.len() as u32,
});
self.text("Crafting", x + 86.0, y + 16.0, 0xff404040, false);
} else {
let (title, left) = match kind {
ContainerKind::Dispenser => ("Dispenser", 60.0),
ContainerKind::Furnace => (&*interaction.title, 60.0),
ContainerKind::Workbench => (&*interaction.title, 28.0),
_ => (&*interaction.title, 8.0),
};
self.text(title, x + left, y + 6.0, 0xff404040, false);
self.text("Inventory", x + 8.0, y + h - 94.0, 0xff404040, false);
}
if kind == ContainerKind::Furnace {
let [cook, burn, total] = interaction.bars;
if burn > 0 {
let flame = f32::from(burn) * 12.0 / f32::from(total.max(1));
let flame = flame.floor().clamp(0.0, 12.0);
let size = [14.0, flame + 2.0];
let rect = [x + 56.0, y + 48.0 - flame, size[0], size[1]];
self.sprite(rect, [176.0, 12.0 - flame, size[0], size[1]], FURNACE);
}
let width = (f32::from(cook) * 24.0 / 200.0).floor() + 1.0;
let rect = [x + 79.0, y + 34.0, width, 16.0];
self.sprite(rect, [176.0, 14.0, width, 16.0], FURNACE);
}
let mut hovered = None;
for index in 0..kind.slot_count() {
let [sx, sy] = layout::slot_position(kind, index);
let container = &interaction.container;
let item = container.get(&interaction.inventory, &interaction.external, index);
if let Some(item) = item {
self.item(item, x + sx, y + sy);
}
if contains([x + sx - 1.0, y + sy - 1.0, 18.0, 18.0], cursor) {
self.fill([x + sx, y + sy, 16.0, 16.0], 0x80ffffff);
hovered = hovered.or(Some(item));
}
}
if let Some(item) = interaction.container.carried {
self.item(item, cursor[0] - 8.0, cursor[1] - 8.0);
return;
}
let name = hovered
.flatten()
.and_then(|stack| stack.translation_key())
.and_then(|key| self.translations.get(&format!("{key}.name")))
.map(|name| name.trim().to_owned());
if let Some(name) = name.filter(|name| !name.is_empty()) {
let [tx, ty] = [cursor[0] + 12.0, cursor[1] - 12.0];
let width = self.text_width(&name);
self.fill([tx - 3.0, ty - 3.0, width + 6.0, 14.0], 0xc0000000);
self.text(&name, tx, ty, 0xffffffff, true);
}
}
}
fn corners([x, y, w, h]: [f32; 4]) -> [[f32; 2]; 4] {
[[x, y], [x + w, y], [x + w, y + h], [x, y + h]]
}
/// The rectangle of cell `index` in a 16 by 16 grid of `step` sized cells.
fn cell(index: u8, step: f32, size: f32) -> [f32; 4] {
[
f32::from(index % 16) * step,
f32::from(index / 16) * step,
size,
size,
]
}
fn gray(level: f32) -> u32 {
0xff000000 | ((level * 255.0) as u32 * 0x010101)
}
fn glyph(ch: char) -> u8 {
if ch.is_ascii() {
return ch as u8;
}
const EXTENDED: &str = "⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜø£Ø×ƒáíóúñѪº¿®¬½¼¡«»";
EXTENDED
.chars()
.position(|c| c == ch)
.map_or(b'?', |index| 127 + index as u8)
}
/// `java.awt.Color.HSBtoRGB`.
fn hsb_to_rgb(hue: f32, saturation: f32, brightness: f32) -> [u32; 3] {
let h = (hue - hue.floor()) * 6.0;
let f = h - h.floor();
let p = brightness * (1.0 - saturation);
let q = brightness * (1.0 - saturation * f);
let t = brightness * (1.0 - saturation * (1.0 - f));
let (r, g, b) = match h as i32 {
0 => (brightness, t, p),
1 => (q, brightness, p),
2 => (p, brightness, t),
3 => (p, q, brightness),
4 => (t, p, brightness),
_ => (brightness, p, q),
};
[r, g, b].map(|c| (c * 255.0 + 0.5) as u32)
}
/// The color of a `§` formatting code, keeping the alpha of `color`.
fn code_color(code: char, color: u32) -> u32 {
let code = code.to_digit(16).unwrap_or(15);
let light = (code >> 3) * 85;
let red = (code >> 2 & 1) * 170 + light + if code == 6 { 85 } else { 0 };
let green = (code >> 1 & 1) * 170 + light;
let blue = (code & 1) * 170 + light;
(color & 0xff000000) | red | green << 8 | blue << 16
}