use embedded_3dgfx::DrawPrimitive;
use embedded_3dgfx::K3dengine;
use embedded_3dgfx::billboard::Billboard;
use embedded_3dgfx::draw::{draw_zbuffered, draw_zbuffered_with_textures as draw_tex};
use embedded_3dgfx::mesh::{Geometry, K3dMesh, RenderMode};
use embedded_3dgfx::texture::{Texture, TextureManager};
use embedded_3dgfx::transform_anim::{AnimationPlayer, TransformKeyframe, TransformTrack};
use embedded_3dgfx::tween::{Easing, Tween, scale_rgb565};
use embedded_graphics::mono_font::{MonoTextStyle, ascii::FONT_6X10};
use embedded_graphics::text::Text;
use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
use embedded_graphics_core::prelude::*;
use embedded_graphics_simulator::{
OutputSettingsBuilder, SimulatorDisplay, SimulatorEvent, Window, sdl2::Keycode,
};
use nalgebra::{Point3, Vector3};
const W: u32 = 96;
const H: u32 = 64;
const MENU_COUNT: usize = 5;
const FADE_DURATION: f32 = 0.28;
const LOGO_INTRO: &[TransformKeyframe] = &[
TransformKeyframe::new(0.0, [0.0, -1.8, 0.0], 0.0, 0.0, 0.0, 0.9),
TransformKeyframe::new(0.5, [0.0, -0.4, 0.0], 0.0, 0.0, 0.0, 1.0),
TransformKeyframe::new(1.4, [0.0, 0.0, 0.0], 0.0, 0.0, 6.28, 1.1),
];
const MENU_X: f32 = 0.0;
const MENU_Y: [f32; MENU_COUNT] = [0.85, 0.42, 0.0, -0.42, -0.85];
const MENU_ITEM_SCALE: f32 = 0.65;
const MENU_SLIDE_FROM: f32 = -1.8;
const LOGO_MENU_X: f32 = -0.55;
const LOGO_MENU_Y: f32 = 1.2;
const LOGO_MENU_SIZE: f32 = 0.55;
const MENU_LABELS: [&str; MENU_COUNT] = ["PLAY", "OPTS", "SND ", "HUD ", "QUIT"];
const MENU_COLORS: [Rgb565; MENU_COUNT] = [
Rgb565::CSS_GREEN,
Rgb565::CSS_YELLOW,
Rgb565::CSS_CYAN,
Rgb565::CSS_MAGENTA,
Rgb565::CSS_RED,
];
static LOGO_TEX_DATA: [Rgb565; 256] = make_logo_tex_data();
const fn make_logo_tex_data() -> [Rgb565; 256] {
let mut d = [Rgb565::BLACK; 256];
let mut y = 0;
while y < 16 {
let mut x = 0;
while x < 16 {
let dx = x as i32 - 7;
let dy = y as i32 - 7;
let dist_sq = dx * dx + dy * dy;
let idx = y * 16 + x;
if dist_sq >= 16 && dist_sq <= 36 {
d[idx] = Rgb565::new(0, 31, 31);
}
if (dx.abs() <= 1 && dy.abs() <= 5) || (dy.abs() <= 1 && dx.abs() <= 5) {
d[idx] = Rgb565::new(4, 40, 48);
}
if dist_sq <= 4 {
d[idx] = Rgb565::new(8, 60, 62);
}
x += 1;
}
y += 1;
}
d
}
static DIAMOND_VERTS: [[f32; 3]; 5] = [
[0.0, 0.22, 0.0],
[-0.2, 0.0, 0.0],
[0.2, 0.0, 0.0],
[0.0, 0.0, 0.2],
[0.0, 0.0, -0.2],
];
static DIAMOND_FACES: [[usize; 3]; 6] = [
[0, 1, 3],
[0, 3, 2],
[0, 2, 4],
[0, 4, 1],
[1, 4, 3],
[2, 3, 4],
];
#[derive(Clone, Copy, PartialEq, Eq)]
enum Screen {
Boot,
Transition,
Menu,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum FadeMode {
None,
Out { next: Screen },
In,
}
fn make_menu_item(y: f32, color: Rgb565) -> K3dMesh<'static> {
let geometry = Geometry {
vertices: &DIAMOND_VERTS,
faces: &DIAMOND_FACES,
colors: &[],
lines: &[],
normals: &[],
vertex_normals: &[],
uvs: &[],
texture_id: None,
};
let mut mesh = K3dMesh::new(geometry);
mesh.set_render_mode(RenderMode::Lines);
mesh.set_color(color);
mesh.set_position(MENU_SLIDE_FROM, y, 0.0);
mesh.set_scale(MENU_ITEM_SCALE);
mesh
}
fn make_menu_items() -> [K3dMesh<'static>; MENU_COUNT] {
[
make_menu_item(MENU_Y[0], MENU_COLORS[0]),
make_menu_item(MENU_Y[1], MENU_COLORS[1]),
make_menu_item(MENU_Y[2], MENU_COLORS[2]),
make_menu_item(MENU_Y[3], MENU_COLORS[3]),
make_menu_item(MENU_Y[4], MENU_COLORS[4]),
]
}
fn place_menu_items(items: &mut [K3dMesh<'static>], x: f32) {
for (i, item) in items.iter_mut().enumerate() {
item.set_position(x, MENU_Y[i], 0.0);
}
}
fn apply_billboard_from_sample(billboard: &mut Billboard, sample: &embedded_3dgfx::SampledTransform, size_mul: f32) {
billboard.position = Point3::new(
sample.position[0],
sample.position[1],
sample.position[2],
);
billboard.size = sample.scale * size_mul;
billboard.rotation = sample.yaw;
}
fn park_logo_billboard(billboard: &mut Billboard) {
billboard.position = Point3::new(LOGO_MENU_X, LOGO_MENU_Y, 0.0);
billboard.size = LOGO_MENU_SIZE;
billboard.rotation = 0.0;
}
fn apply_brightness(display: &mut SimulatorDisplay<Rgb565>, factor: f32) {
let factor = factor.clamp(0.0, 1.0);
for y in 0..H {
for x in 0..W {
let p = Point::new(x as i32, y as i32);
let c = scale_rgb565(display.get_pixel(p), factor);
Pixel(p, c).draw(display).unwrap();
}
}
}
fn render_textured_billboard(
engine: &K3dengine,
billboard: &Billboard,
texture_id: u32,
texture_manager: &TextureManager<4>,
display: &mut SimulatorDisplay<Rgb565>,
zbuffer: &mut [u32],
) {
let camera_up = Vector3::y();
let quad = billboard.generate_quad(engine.camera.position, camera_up);
let uvs: [[f32; 2]; 4] = [[0.0, 1.0], [1.0, 1.0], [1.0, 0.0], [0.0, 0.0]];
let faces = Billboard::get_triangles();
for face in faces {
if let Some([p1, p2, p3]) = engine.transform_points(&face, &quad, engine.camera.vp_matrix) {
draw_tex(
DrawPrimitive::TexturedTriangleWithDepth {
points: [p1.xy(), p2.xy(), p3.xy()],
depths: [p1.z as f32, p2.z as f32, p3.z as f32],
uvs: [uvs[face[0]], uvs[face[1]], uvs[face[2]]],
texture_id,
},
display,
zbuffer,
W as usize,
texture_manager,
None,
None,
);
}
}
}
fn render_scene(
engine: &K3dengine,
screen: Screen,
logo_billboard: &Billboard,
logo_tex: u32,
texture_manager: &TextureManager<4>,
menu_items: &[K3dMesh<'static>],
display: &mut SimulatorDisplay<Rgb565>,
zbuffer: &mut [u32],
) {
match screen {
Screen::Boot => {
render_textured_billboard(
engine,
logo_billboard,
logo_tex,
texture_manager,
display,
zbuffer,
);
}
Screen::Transition | Screen::Menu => {
for item in menu_items {
engine.render(core::iter::once(item), |prim| {
draw_zbuffered(prim, display, zbuffer, W as usize);
});
}
render_textured_billboard(
engine,
logo_billboard,
logo_tex,
texture_manager,
display,
zbuffer,
);
}
}
}
fn start_fade_out(fade_mode: &mut FadeMode, fade_tween: &mut Tween, next: Screen) {
*fade_mode = FadeMode::Out { next };
*fade_tween = Tween::new(1.0, 0.0, FADE_DURATION, Easing::EaseInCubic);
}
fn start_fade_in(fade_mode: &mut FadeMode, fade_tween: &mut Tween) {
*fade_mode = FadeMode::In;
*fade_tween = Tween::new(0.0, 1.0, FADE_DURATION, Easing::EaseOutCubic);
}
fn reset_demo(
menu_items: &mut [K3dMesh<'static>; MENU_COUNT],
logo_player: &mut AnimationPlayer<'_>,
menu_slide: &mut Tween,
highlight_tween: &mut Tween,
fade_mode: &mut FadeMode,
fade_tween: &mut Tween,
screen: &mut Screen,
selected: &mut usize,
logo_billboard: &mut Billboard,
zbuffer: &mut [u32],
) {
*menu_items = make_menu_items();
logo_player.reset();
logo_player.set_playing(true);
menu_slide.reset();
highlight_tween.reset();
*fade_mode = FadeMode::None;
fade_tween.reset();
*screen = Screen::Boot;
*selected = 0;
*logo_billboard = Billboard::new(Point3::new(0.0, -1.8, 0.0), 0.9, Rgb565::CSS_CYAN);
zbuffer.fill(u32::MAX);
}
fn main() {
let output_settings = OutputSettingsBuilder::new().scale(6).build();
let mut display = SimulatorDisplay::<Rgb565>::new(Size::new(W, H));
let mut window = Window::new("Boot/Menu 96x64 — SPACE UP/DOWN R ESC", &output_settings);
let mut engine = K3dengine::new(W as u16, H as u16);
engine.camera.set_near_far(0.2, 10.0);
engine.camera.set_fovy(std::f32::consts::FRAC_PI_3);
engine.camera.set_position(Point3::new(0.0, 0.0, 3.4));
engine.camera.set_target(Point3::new(0.0, 0.0, 0.0));
let mut texture_manager = TextureManager::<4>::new();
let logo_tex = texture_manager
.add_texture(Texture::new(&LOGO_TEX_DATA, 16, 16))
.unwrap();
let mut logo_billboard = Billboard::new(Point3::new(0.0, -1.8, 0.0), 0.9, Rgb565::CSS_CYAN);
let mut menu_items = make_menu_items();
let intro_track = TransformTrack::new(LOGO_INTRO, false);
let mut logo_player = AnimationPlayer::new(intro_track);
let mut menu_slide = Tween::new(MENU_SLIDE_FROM, MENU_X, 0.4, Easing::Smoothstep);
let mut highlight_tween = Tween::new(MENU_ITEM_SCALE, 1.0, 0.12, Easing::EaseOutCubic);
let mut fade_tween = Tween::new(1.0, 1.0, 0.0, Easing::Linear);
let mut fade_mode = FadeMode::None;
let mut screen = Screen::Boot;
let mut selected: usize = 0;
let mut zbuffer = [u32::MAX; (W as usize) * (H as usize)];
let mut last_frame = std::time::Instant::now();
println!("Boot/menu @ {W}x{H} — bitmap logo + 5 items + fades");
println!("SPACE: skip | UP/DOWN: select | R: restart | ESC: quit");
display.clear(Rgb565::BLACK).unwrap();
apply_billboard_from_sample(&mut logo_billboard, &logo_player.sample(), 1.0);
render_scene(
&engine,
Screen::Boot,
&logo_billboard,
logo_tex,
&texture_manager,
&menu_items,
&mut display,
&mut zbuffer,
);
window.update(&display);
'running: loop {
let dt = last_frame.elapsed().as_secs_f32();
last_frame = std::time::Instant::now();
for event in window.events() {
match event {
SimulatorEvent::KeyDown { keycode, .. } => match keycode {
Keycode::Escape => break 'running,
Keycode::Space => match screen {
Screen::Boot | Screen::Transition => {
screen = Screen::Menu;
menu_slide.elapsed = menu_slide.duration;
place_menu_items(&mut menu_items, MENU_X);
park_logo_billboard(&mut logo_billboard);
logo_player.set_playing(false);
start_fade_in(&mut fade_mode, &mut fade_tween);
}
Screen::Menu => {}
},
Keycode::Up => {
if screen == Screen::Menu && selected > 0 {
selected -= 1;
highlight_tween.reset();
}
}
Keycode::Down => {
if screen == Screen::Menu && selected + 1 < MENU_COUNT {
selected += 1;
highlight_tween.reset();
}
}
Keycode::R => reset_demo(
&mut menu_items,
&mut logo_player,
&mut menu_slide,
&mut highlight_tween,
&mut fade_mode,
&mut fade_tween,
&mut screen,
&mut selected,
&mut logo_billboard,
&mut zbuffer,
),
_ => {}
},
SimulatorEvent::Quit => break 'running,
_ => {}
}
}
if fade_mode == FadeMode::None {
match screen {
Screen::Boot => {
logo_player.advance(dt);
apply_billboard_from_sample(&mut logo_billboard, &logo_player.sample(), 1.0);
if logo_player.is_done() {
start_fade_out(&mut fade_mode, &mut fade_tween, Screen::Transition);
}
}
Screen::Transition => {
menu_slide.advance(dt);
place_menu_items(&mut menu_items, menu_slide.value());
park_logo_billboard(&mut logo_billboard);
if menu_slide.is_done() {
start_fade_out(&mut fade_mode, &mut fade_tween, Screen::Menu);
}
}
Screen::Menu => {
park_logo_billboard(&mut logo_billboard);
highlight_tween.advance(dt);
for (i, item) in menu_items.iter_mut().enumerate() {
let scale = if i == selected {
if highlight_tween.is_done() {
1.0
} else {
highlight_tween.value()
}
} else {
MENU_ITEM_SCALE
};
item.set_scale(scale);
}
}
}
}
match fade_mode {
FadeMode::Out { next } => {
fade_tween.advance(dt);
if fade_tween.is_done() {
screen = next;
match screen {
Screen::Transition => {
menu_slide.reset();
park_logo_billboard(&mut logo_billboard);
logo_player.set_playing(false);
}
Screen::Menu => {
place_menu_items(&mut menu_items, MENU_X);
for (i, item) in menu_items.iter_mut().enumerate() {
item.set_scale(if i == selected { 1.0 } else { MENU_ITEM_SCALE });
}
}
Screen::Boot => {}
}
start_fade_in(&mut fade_mode, &mut fade_tween);
}
}
FadeMode::In => {
fade_tween.advance(dt);
if fade_tween.is_done() {
fade_mode = FadeMode::None;
}
}
FadeMode::None => {}
}
display.clear(Rgb565::BLACK).unwrap();
zbuffer.fill(u32::MAX);
render_scene(
&engine,
screen,
&logo_billboard,
logo_tex,
&texture_manager,
&menu_items,
&mut display,
&mut zbuffer,
);
let brightness = match fade_mode {
FadeMode::None => 1.0,
FadeMode::Out { .. } | FadeMode::In => fade_tween.value(),
};
if brightness < 1.0 {
apply_brightness(&mut display, brightness);
}
let (label, color) = match screen {
Screen::Boot => ("BOOT", Rgb565::CSS_CYAN),
Screen::Transition => ("LOAD", Rgb565::CSS_DARK_GRAY),
Screen::Menu => (MENU_LABELS[selected], MENU_COLORS[selected]),
};
let hud = MonoTextStyle::new(&FONT_6X10, color);
Text::new(label, Point::new(4, 54), hud)
.draw(&mut display)
.unwrap();
window.update(&display);
std::thread::sleep(std::time::Duration::from_millis(16));
}
}