Compare commits

..

7 Commits

Author SHA1 Message Date
Mark cb68e9d376
Added configurable land dialog 2024-01-25 21:58:41 -08:00
Mark f184a40877
Minor edits 2024-01-25 21:25:50 -08:00
Mark 699e62a1c8
Added get_mouse_pos 2024-01-25 21:25:38 -08:00
Mark f70f79e96a
Added last run to renderinput 2024-01-25 21:25:21 -08:00
Mark f85c2e082a
Minor edits 2024-01-25 21:25:03 -08:00
Mark 0e4dae6215
Added animation to UI 2024-01-25 21:24:37 -08:00
Mark 616e252ae3
Minor edit 2024-01-25 21:22:53 -08:00
13 changed files with 563 additions and 156 deletions

View File

@ -93,16 +93,15 @@ file = "ui/landscape-mask.png"
[sprite."ui::planet::button"] [sprite."ui::planet::button"]
start_at = "off:top" start_at = "off:top"
random_start_frame = false
section.off.top = "stop" section.off.top = "stop"
section.off.bot = "stop" section.off.bot = "stop"
section.off.timing.fps = 60 section.off.timing.duration = 0.5
section.off.frames = ["ui/planet-button-off.png"] section.off.frames = ["ui/planet-button-off.png"]
section.on.top = "stop" section.on.top = "stop"
section.on.bot = "stop" section.on.bot = "stop"
section.on.timing.fps = 60 section.on.timing.duration = 0.5
section.on.frames = ["ui/planet-button-on.png"] section.on.frames = ["ui/planet-button-on.png"]
[sprite."effect::blaster"] [sprite."effect::blaster"]

18
content/ui.toml Normal file
View File

@ -0,0 +1,18 @@
[ui.landed]
frame.sprite = "ui::planet"
frame.pos = [0.0, 0.0]
frame.dim = [800.0, 800.0]
landscape.mask = "ui::landscapemask"
landscape.pos = [32.0, 75.0]
landscape.dim = [344.0, 173.0]
landscape.loc_div = 512
button.sprite = "ui::planet::button"
button.pos = [356, 90]
button.dim = [113.569, 20]
button.loc_div = 512
button.on_mouse_enter.edge = "on:top"
button.on_mouse_enter.duration = 0.1
button.on_mouse_leave.edge = "off:top"
button.on_mouse_leave.duration = 0.1

View File

@ -33,6 +33,7 @@ mod syntax {
use crate::{ use crate::{
config, config,
part::{effect, faction, outfit, ship, sprite, system}, part::{effect, faction, outfit, ship, sprite, system},
ui,
}; };
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -44,6 +45,7 @@ mod syntax {
pub faction: Option<HashMap<String, faction::syntax::Faction>>, pub faction: Option<HashMap<String, faction::syntax::Faction>>,
pub effect: Option<HashMap<String, effect::syntax::Effect>>, pub effect: Option<HashMap<String, effect::syntax::Effect>>,
pub config: Option<config::syntax::Config>, pub config: Option<config::syntax::Config>,
pub ui: Option<ui::syntax::Ui>,
} }
fn merge_hashmap<K, V>( fn merge_hashmap<K, V>(
@ -81,6 +83,7 @@ mod syntax {
faction: None, faction: None,
effect: None, effect: None,
config: None, config: None,
ui: None,
} }
} }
@ -96,6 +99,7 @@ mod syntax {
.with_context(|| "while merging factions")?; .with_context(|| "while merging factions")?;
merge_hashmap(&mut self.effect, other.effect) merge_hashmap(&mut self.effect, other.effect)
.with_context(|| "while merging effects")?; .with_context(|| "while merging effects")?;
if self.config.is_some() { if self.config.is_some() {
if other.config.is_some() { if other.config.is_some() {
bail!("invalid content dir, multiple config tables") bail!("invalid content dir, multiple config tables")
@ -103,6 +107,15 @@ mod syntax {
} else { } else {
self.config = other.config; self.config = other.config;
} }
if self.ui.is_some() {
if other.ui.is_some() {
bail!("invalid content dir, multiple ui tables")
}
} else {
self.ui = other.ui;
}
return Ok(()); return Ok(());
} }
} }
@ -159,6 +172,7 @@ pub struct Content {
factions: Vec<Faction>, factions: Vec<Faction>,
effects: Vec<Effect>, effects: Vec<Effect>,
config: Config, config: Config,
ui: Option<Ui>,
} }
// Loading methods // Loading methods
@ -227,6 +241,7 @@ impl Content {
factions: Vec::new(), factions: Vec::new(),
effects: Vec::new(), effects: Vec::new(),
sprite_index: HashMap::new(), sprite_index: HashMap::new(),
ui: None,
}; };
// TODO: enforce sprite and image limits // TODO: enforce sprite and image limits
@ -239,6 +254,7 @@ impl Content {
&mut content, &mut content,
)?; )?;
} }
if root.effect.is_some() { if root.effect.is_some() {
part::effect::Effect::build( part::effect::Effect::build(
root.effect.take().unwrap(), root.effect.take().unwrap(),
@ -247,6 +263,10 @@ impl Content {
)?; )?;
} }
if root.ui.is_some() {
part::ui::Ui::build(root.ui.take().unwrap(), &mut build_context, &mut content)?;
}
// Order below this line does not matter // Order below this line does not matter
if root.ship.is_some() { if root.ship.is_some() {
part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?; part::ship::Ship::build(root.ship.take().unwrap(), &mut build_context, &mut content)?;
@ -353,4 +373,9 @@ impl Content {
pub fn get_config(&self) -> &Config { pub fn get_config(&self) -> &Config {
return &self.config; return &self.config;
} }
/// Get ui configuration
pub fn get_ui(&self) -> &Ui {
return self.ui.as_ref().unwrap();
}
} }

View File

@ -8,6 +8,7 @@ pub(crate) mod outfitspace;
pub(crate) mod ship; pub(crate) mod ship;
pub(crate) mod sprite; pub(crate) mod sprite;
pub(crate) mod system; pub(crate) mod system;
pub(crate) mod ui;
pub use config::Config; pub use config::Config;
pub use effect::*; pub use effect::*;
@ -20,3 +21,4 @@ pub use ship::{
}; };
pub use sprite::*; pub use sprite::*;
pub use system::{System, SystemObject}; pub use system::{System, SystemObject};
pub use ui::*;

View File

@ -0,0 +1,250 @@
use anyhow::{Context, Result};
use nalgebra::{Point2, Vector2};
use crate::{handle::SpriteHandle, Content, ContentBuildContext, SectionEdge};
pub(crate) mod syntax {
use crate::{sprite::syntax::SectionEdge, Content, ContentBuildContext};
use anyhow::{bail, Context, Result};
use nalgebra::{Point2, Vector2};
use serde::Deserialize;
// Raw serde syntax structs.
// These are never seen by code outside this crate.
#[derive(Debug, Deserialize)]
pub struct Ui {
pub landed: UiLanded,
}
#[derive(Debug, Deserialize)]
pub struct UiLanded {
pub frame: UiSprite,
pub landscape: UiSprite,
pub button: UiSprite,
}
#[derive(Debug, Deserialize)]
pub struct UiSprite {
pub sprite: Option<String>,
pub pos: [f32; 2],
pub dim: [f32; 2],
pub loc_div: Option<f32>,
pub mask: Option<String>,
pub on_mouse_enter: Option<EdgeSpec>,
pub on_mouse_leave: Option<EdgeSpec>,
}
#[derive(Debug, Deserialize)]
pub struct EdgeSpec {
pub edge: SectionEdge,
pub duration: f32,
}
impl UiSprite {
pub fn build(
self,
build_context: &ContentBuildContext,
ct: &Content,
// If true, this ui sprite will be positioned relative to another
is_child: bool,
// If true, fail if self.sprite is missing.
// If false, fail if self.sprite exists.
// This is false for sprites that may change---for example, planet landscapes
should_have_sprite: bool,
) -> Result<super::UiSpriteConfig> {
let sprite = {
if should_have_sprite {
if self.sprite.is_none() {
bail!("no sprite given, but expected a value")
}
match ct.sprite_index.get(self.sprite.as_ref().unwrap()) {
None => bail!("ui sprite `{}` doesn't exist", self.sprite.unwrap()),
Some(t) => Some(*t),
}
} else {
if self.sprite.is_some() {
bail!("got a sprite, but didn't expect one")
}
None
}
};
let mask = if let Some(mask) = self.mask {
Some(match ct.sprite_index.get(&mask) {
None => bail!("mask `{}` doesn't exist", mask),
Some(t) => *t,
})
} else {
None
};
let on_mouse_enter = {
if let Some(x) = self.on_mouse_enter {
if sprite.is_none() {
bail!("got `on_mouse_enter` on a ui element with no fixed sprite")
}
Some(
x.edge
.resolve_as_edge(sprite.unwrap(), build_context, x.duration)
.with_context(|| format!("failed to resolve mouse enter edge"))?,
)
} else {
None
}
};
let on_mouse_leave = {
if let Some(x) = self.on_mouse_leave {
if sprite.is_none() {
bail!("got `on_mouse_leave` on a ui element with no fixed sprite")
}
Some(
x.edge
.resolve_as_edge(sprite.unwrap(), build_context, x.duration)
.with_context(|| format!("failed to resolve mouse leave edge"))?,
)
} else {
None
}
};
let d = self.loc_div.unwrap_or(1.0);
let rect = {
if is_child {
super::UiSpriteRect::Relative {
pos: Point2::new(self.pos[0], self.pos[1]) / d,
dim: Vector2::new(self.dim[0], self.dim[1]) / d,
}
} else {
super::UiSpriteRect::Absolute {
pos: Point2::new(self.pos[0], self.pos[1]) / d,
dim: Vector2::new(self.dim[0], self.dim[1]) / d,
}
}
};
return Ok(super::UiSpriteConfig {
sprite,
mask,
on_mouse_enter,
on_mouse_leave,
rect,
});
}
}
}
/// UI Configuration
#[derive(Debug, Clone)]
pub struct Ui {
/// Landed interface frame
pub landed_frame: UiSpriteConfig,
/// Landed interface image
pub landed_landscape: UiSpriteConfig,
/// Test button
pub landed_button: UiSpriteConfig,
}
/// A UI sprite's position
#[derive(Debug, Clone)]
pub enum UiSpriteRect {
/// Positioning relative to a parent sprite
Relative {
// Note that both pos and dim include transparent pixels,
// of this sprite AND its parent.
// We use the top left corner here because that's how inkscape
// positions its objects. This makes it very easy to compute position.
// TODO: maybe add anchors here too?
/// The position of this sprite's northeast corner, relative to its parent's NE corner.
/// Positive X is right, positive Y is down.
pos: Point2<f32>,
/// This sprite's w and h, as a fraction of the parent's width and height.
dim: Vector2<f32>,
},
/// Absolute positioning
Absolute {
/// The position of the center of this sprite, in logical pixels,
/// with 0, 0 at the center of the screen
pos: Point2<f32>,
/// This sprite's w and h, in logical pixels.
dim: Vector2<f32>,
},
}
impl UiSpriteRect {
/// Get this rectangle's position
pub fn get_pos(&self) -> &Point2<f32> {
match self {
Self::Relative { pos, .. } => pos,
Self::Absolute { pos, .. } => pos,
}
}
/// Get this rectangle's dimensions
pub fn get_dim(&self) -> &Vector2<f32> {
match self {
Self::Relative { dim, .. } => dim,
Self::Absolute { dim, .. } => dim,
}
}
}
/// A single UI sprite instance
#[derive(Debug, Clone)]
pub struct UiSpriteConfig {
/// The sprite to show
pub sprite: Option<SpriteHandle>,
/// The mask to use
pub mask: Option<SpriteHandle>,
/// This sprite's position and size
pub rect: UiSpriteRect,
/// Animation edge to take when mouse enters this sprite
pub on_mouse_enter: Option<SectionEdge>,
/// Animation edge to take when mouse leaves this sprite
pub on_mouse_leave: Option<SectionEdge>,
}
impl crate::Build for Ui {
type InputSyntaxType = syntax::Ui;
fn build(
ui: Self::InputSyntaxType,
build_context: &mut ContentBuildContext,
ct: &mut Content,
) -> Result<()> {
ct.ui = Some(Ui {
landed_frame: ui
.landed
.frame
.build(build_context, ct, false, true)
.with_context(|| format!("in ui config (frame)"))?,
landed_landscape: ui
.landed
.landscape
.build(build_context, ct, true, false)
.with_context(|| format!("in ui config (image)"))?,
landed_button: ui
.landed
.button
.build(build_context, ct, true, true)
.with_context(|| format!("in ui config (button)"))?,
});
return Ok(());
}
}

View File

@ -126,7 +126,12 @@ impl SpriteAutomaton {
fn take_edge(&mut self, ct: &Content, e: SectionEdge) { fn take_edge(&mut self, ct: &Content, e: SectionEdge) {
let sprite = ct.get_sprite(self.sprite); let sprite = ct.get_sprite(self.sprite);
let current_section = sprite.get_section(self.current_section); let current_section = sprite.get_section(self.current_section);
let last_direction = self.current_direction;
let last = match self.current_direction {
AnimDirection::Stop => self.next_texture,
AnimDirection::Down => self.next_texture,
AnimDirection::Up => self.last_texture,
};
match e { match e {
SectionEdge::Stop => { SectionEdge::Stop => {
@ -198,12 +203,6 @@ impl SpriteAutomaton {
} }
} }
let last = match last_direction {
AnimDirection::Stop => self.last_texture,
AnimDirection::Down => self.next_texture,
AnimDirection::Up => self.last_texture,
};
match self.current_direction { match self.current_direction {
AnimDirection::Stop => { AnimDirection::Stop => {
let current_section = sprite.get_section(self.current_section); let current_section = sprite.get_section(self.current_section);

View File

@ -21,6 +21,7 @@ use nalgebra::Vector2;
use std::{ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Instant,
}; };
use winit::{ use winit::{
event::{Event, KeyboardInput, WindowEvent}, event::{Event, KeyboardInput, WindowEvent},
@ -132,6 +133,7 @@ fn try_main() -> Result<()> {
); );
let mut phys_img = PhysImage::new(); let mut phys_img = PhysImage::new();
let mut last_run = Instant::now();
event_loop.run(move |event, _, control_flow| { event_loop.run(move |event, _, control_flow| {
match event { match event {
@ -143,9 +145,11 @@ fn try_main() -> Result<()> {
ct: &content, ct: &content,
phys_img: &phys_img, phys_img: &phys_img,
player: &player, player: &player,
time_since_last_run: last_run.elapsed().as_secs_f32(),
current_system: SystemHandle { index: 0 }, current_system: SystemHandle { index: 0 },
timing: game.get_timing().clone(), timing: game.get_timing().clone(),
}; };
last_run = Instant::now();
match gpu.render(render_input) { match gpu.render(render_input) {
Ok(_) => {} Ok(_) => {}

View File

@ -112,6 +112,11 @@ impl InputStatus {
self.v_scroll self.v_scroll
} }
/// Get the current mouse position
pub fn get_mouse_pos(&self) -> PhysicalPosition<f32> {
self.mouse_position
}
/// Is the player pressing the "turn left" key? /// Is the player pressing the "turn left" key?
pub fn pressed_left(&self) -> bool { pub fn pressed_left(&self) -> bool {
self.key_left self.key_left

View File

@ -18,11 +18,15 @@ struct VertexInput {
struct VertexOutput { struct VertexOutput {
@builtin(position) position: vec4<f32>, @builtin(position) position: vec4<f32>,
@location(0) texture_coords: vec2<f32>, @location(0) tween: f32,
@location(1) texture_index: u32, @location(1) texture_index_a: u32,
@location(2) mask_coords: vec2<f32>, @location(2) texture_coords_a: vec2<f32>,
@location(3) mask_index: vec2<u32>, @location(3) texture_index_b: u32,
@location(4) color_transform: vec4<f32>, @location(4) texture_coords_b: vec2<f32>,
@location(5) color: vec4<f32>,
@location(6) mask_coords: vec2<f32>,
@location(7) mask_index: vec2<u32>,
} }
@group(0) @binding(0) @group(0) @binding(0)
@ -80,31 +84,67 @@ fn vertex_main(
vertex: VertexInput, vertex: VertexInput,
instance: InstanceInput, instance: InstanceInput,
) -> VertexOutput { ) -> VertexOutput {
var out: VertexOutput;
// TODO: this will break if we try to use texture 0. // TODO: this will break if we try to use texture 0.
// implement animations for ui sprites & fix that here. // implement animations for ui sprites & fix that here.
let pos = transform_vertex( // Pick texture size by the size of the visible texture
// (texture index 0 is special, it's the "hidden" texture)
if instance.texture_index.x == 0u && instance.texture_index.y == 0u {
out.position = vec4<f32>(0.0, 0.0, 0.0, 1.0);
} else if instance.texture_index.x == 0u {
out.position = transform_vertex(
instance,
vertex.position,
instance.texture_index.y,
);
} else if instance.texture_index.y == 0u {
out.position = transform_vertex(
instance, instance,
vertex.position, vertex.position,
instance.texture_index.x, instance.texture_index.x,
); );
} else {
out.position = transform_vertex(
instance,
vertex.position,
instance.texture_index.x,
);
}
var out: VertexOutput; out.color = instance.color_transform;
out.position = pos; out.tween = instance.texture_fade;
out.color_transform = instance.color_transform;
// Texture 0 is special, it's the empty texture
// TODO: function to get texture from sprite if instance.texture_index.x == 0u {
// Pick texture frame out.texture_index_a = 0u;
out.texture_coords_a = vec2(0.0, 0.0);
} else {
let t = global_atlas[instance.texture_index.x]; let t = global_atlas[instance.texture_index.x];
out.texture_index = u32(t.atlas_texture); out.texture_index_a = t.atlas_texture;
out.texture_coords = vec2(t.xpos, t.ypos); out.texture_coords_a = vec2(t.xpos, t.ypos);
if vertex.texture_coords.x == 1.0 { if vertex.texture_coords.x == 1.0 {
out.texture_coords = vec2(out.texture_coords.x + t.width, out.texture_coords.y); out.texture_coords_a = out.texture_coords_a + vec2(t.width, 0.0);
} }
if vertex.texture_coords.y == 1.0 { if vertex.texture_coords.y == 1.0 {
out.texture_coords = vec2(out.texture_coords.x, out.texture_coords.y + t.height); out.texture_coords_a = out.texture_coords_a + vec2(0.0, t.height);
}
}
if instance.texture_index.y == 0u {
out.texture_index_b = u32(0u);
out.texture_coords_b = vec2(0.0, 0.0);
} else {
let b = global_atlas[instance.texture_index.y];
out.texture_index_b = u32(b.atlas_texture);
out.texture_coords_b = vec2(b.xpos, b.ypos);
if vertex.texture_coords.x == 1.0 {
out.texture_coords_b = out.texture_coords_b + vec2(b.width, 0.0);
}
if vertex.texture_coords.y == 1.0 {
out.texture_coords_b = out.texture_coords_b + vec2(0.0, b.height);
}
} }
// Pick mask image if mask is enabled // Pick mask image if mask is enabled
@ -142,12 +182,40 @@ fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
).a; ).a;
} }
var color: vec4<f32> = textureSampleLevel( var texture_a: vec4<f32> = vec4(0.0, 0.0, 0.0, 0.0);
texture_array[in.texture_index], if !(
(in.texture_index_a == 0u) &&
(in.texture_coords_a.x == 0.0) &&
(in.texture_coords_a.y == 0.0)
) {
texture_a = textureSampleLevel(
texture_array[in.texture_index_a],
sampler_array[0], sampler_array[0],
in.texture_coords, in.texture_coords_a,
0.0 0.0
).rgba * in.color_transform; ).rgba;
}
var texture_b: vec4<f32> = vec4(0.0, 0.0, 0.0, 0.0);
if !(
(in.texture_index_b == 0u) &&
(in.texture_coords_b.x == 0.0) &&
(in.texture_coords_b.y == 0.0)
) {
texture_b = textureSampleLevel(
texture_array[in.texture_index_b],
sampler_array[0],
in.texture_coords_b,
0.0
).rgba;
}
var color: vec4<f32> = mix(
texture_a,
texture_b,
in.tween
) * in.color;
// Apply mask and discard fully transparent pixels // Apply mask and discard fully transparent pixels
color = vec4(color.rgb, color.a *mask); color = vec4(color.rgb, color.a *mask);

View File

@ -25,6 +25,9 @@ pub struct RenderInput<'a> {
/// The current time, in seconds /// The current time, in seconds
pub current_time: f32, pub current_time: f32,
/// The amount of time that has passed since the last frame was drawn
pub time_since_last_run: f32,
/// Game content /// Game content
pub ct: &'a Content, pub ct: &'a Content,

View File

@ -10,7 +10,9 @@ pub(super) struct Planet {
// UI elements // UI elements
planet_desc: UiTextArea, planet_desc: UiTextArea,
planet_name: UiTextArea, planet_name: UiTextArea,
sprite: UiSprite, frame: UiSprite,
landscape: UiSprite,
button: UiSprite,
/// What object we're displaying currently. /// What object we're displaying currently.
/// Whenever this changes, we need to reflow text. /// Whenever this changes, we need to reflow text.
@ -21,32 +23,13 @@ pub(super) struct Planet {
impl Planet { impl Planet {
pub fn new(ct: &Content, state: &mut RenderState) -> Self { pub fn new(ct: &Content, state: &mut RenderState) -> Self {
let mut sprite = UiSprite::new( let frame = UiSprite::from(ct, &ct.get_ui().landed_frame);
ct.get_sprite_handle("ui::planet"), let button = UiSprite::from(ct, &ct.get_ui().landed_button);
None, let landscape = UiSprite::from_with_sprite(
SpriteRect { ct,
pos: Point2::new(0.0, 0.0), &ct.get_ui().landed_landscape,
dim: Vector2::new(800.0, 800.0),
},
);
sprite.add_child_under(Box::new(UiSprite::new(
ct.get_sprite_handle("ui::landscape::test"), ct.get_sprite_handle("ui::landscape::test"),
Some(ct.get_sprite_handle("ui::landscapemask")), );
SpriteRect {
pos: Point2::new(32.0, 75.0) / 512.0,
dim: Vector2::new(344.0, 173.0) / 512.0,
},
)));
sprite.add_child_under(Box::new(UiSprite::new(
ct.get_sprite_handle("ui::planet::button"),
None,
SpriteRect {
pos: Point2::new(375.0, 90.0) / 512.0,
dim: Vector2::new(113.569, 20.0) / 512.0,
},
)));
let s = Self { let s = Self {
// height of element in logical pixels // height of element in logical pixels
@ -55,9 +38,9 @@ impl Planet {
planet_desc: UiTextArea::new( planet_desc: UiTextArea::new(
ct, ct,
state, state,
ct.get_sprite_handle("ui::planet"), frame.get_sprite(),
Point2::new(0.0, 0.0), Point2::new(0.0, 0.0),
800.0, frame.get_height(),
SpriteRect { SpriteRect {
pos: Point2::new(25.831, 284.883) / 512.0, pos: Point2::new(25.831, 284.883) / 512.0,
dim: Vector2::new(433.140, 97.220) / 512.0, dim: Vector2::new(433.140, 97.220) / 512.0,
@ -70,9 +53,9 @@ impl Planet {
planet_name: UiTextArea::new( planet_name: UiTextArea::new(
ct, ct,
state, state,
ct.get_sprite_handle("ui::planet"), frame.get_sprite(),
Point2::new(0.0, 0.0), Point2::new(0.0, 0.0),
800.0, frame.get_height(),
SpriteRect { SpriteRect {
pos: Point2::new(165.506, 82.0) / 512.0, pos: Point2::new(165.506, 82.0) / 512.0,
dim: Vector2::new(74.883, 17.0) / 512.0, dim: Vector2::new(74.883, 17.0) / 512.0,
@ -82,9 +65,9 @@ impl Planet {
Align::Center, Align::Center,
), ),
// TODO: use both dimensions, frame,
// not just height landscape,
sprite, button,
}; };
return s; return s;
@ -134,8 +117,16 @@ impl Planet {
self.reflow(planet, state); self.reflow(planet, state);
} }
self.button.step(input, state);
self.landscape.step(input, state);
self.frame.step(input, state);
// Draw elements // Draw elements
self.sprite.push_to_buffer(input, state); self.button
.push_to_buffer(input, state, Some(self.frame.get_rect(input)));
self.landscape
.push_to_buffer(input, state, Some(self.frame.get_rect(input)));
self.frame.push_to_buffer(input, state, None);
} }
pub fn get_textarea(&self, input: &RenderInput, state: &RenderState) -> [TextArea; 2] { pub fn get_textarea(&self, input: &RenderInput, state: &RenderState) -> [TextArea; 2] {

View File

@ -9,6 +9,7 @@ use nalgebra::{Point2, Vector2};
use crate::{RenderInput, RenderState}; use crate::{RenderInput, RenderState};
/// Represents a rectangular region inside a sprite. /// Represents a rectangular region inside a sprite.
#[derive(Debug, Clone, Copy)]
pub(crate) struct SpriteRect { pub(crate) struct SpriteRect {
/// The position of the top-left corner of this rectangle, in fractional units. /// The position of the top-left corner of this rectangle, in fractional units.
/// (0.0 is left edge of sprite, 1.0 is right edge) /// (0.0 is left edge of sprite, 1.0 is right edge)
@ -19,6 +20,18 @@ pub(crate) struct SpriteRect {
pub dim: Vector2<f32>, pub dim: Vector2<f32>,
} }
impl SpriteRect {
/// Northeast corner of this rect
pub fn ne_corner(&self) -> Point2<f32> {
self.pos + Vector2::new(-self.dim.x, self.dim.y) / 2.0
}
/// Southwest corner of this rect
pub fn sw_corner(&self) -> Point2<f32> {
self.pos + Vector2::new(self.dim.x, -self.dim.y) / 2.0
}
}
pub(super) trait UiElement { pub(super) trait UiElement {
fn push_to_buffer_child( fn push_to_buffer_child(
&self, &self,

View File

@ -1,119 +1,145 @@
use galactica_content::SpriteHandle; use galactica_content::{Content, SectionEdge, SpriteAutomaton, SpriteHandle, UiSpriteConfig};
use galactica_util::to_radians; use galactica_util::to_radians;
use nalgebra::{Point2, Vector2}; use nalgebra::{Point2, Vector2};
use super::{SpriteRect, UiElement}; use super::SpriteRect;
use crate::{vertexbuffer::types::UiInstance, PositionAnchor, RenderInput, RenderState}; use crate::{vertexbuffer::types::UiInstance, PositionAnchor, RenderInput, RenderState};
pub struct UiSprite { pub struct UiSprite {
sprite: SpriteHandle, pub anim: SpriteAutomaton,
mask: Option<SpriteHandle>, mask: Option<SpriteHandle>,
rect: SpriteRect, rect: SpriteRect,
children_under: Vec<Box<dyn UiElement>>, has_mouse: bool,
children_above: Vec<Box<dyn UiElement>>,
on_mouse_enter: Option<SectionEdge>,
on_mouse_leave: Option<SectionEdge>,
} }
impl UiSprite { impl UiSprite {
pub fn new(sprite: SpriteHandle, mask: Option<SpriteHandle>, rect: SpriteRect) -> Self { pub fn from(ct: &Content, ui: &UiSpriteConfig) -> Self {
Self::from_with_sprite(ct, ui, ui.sprite.unwrap())
}
pub fn from_with_sprite(ct: &Content, ui: &UiSpriteConfig, sprite: SpriteHandle) -> Self {
if ui.sprite.is_none() {
unreachable!("called `UiSprite.from()` on a UiSprite with a None sprite!")
}
return Self { return Self {
sprite, anim: SpriteAutomaton::new(ct, sprite),
mask, mask: ui.mask,
rect, rect: SpriteRect {
children_under: Vec::new(), pos: *ui.rect.get_pos(),
children_above: Vec::new(), dim: *ui.rect.get_dim(),
},
has_mouse: false,
on_mouse_enter: ui.on_mouse_enter,
on_mouse_leave: ui.on_mouse_leave,
}; };
} }
/// Add a child under this sprite pub fn step(&mut self, input: &RenderInput, state: &RenderState) {
pub fn add_child_under(&mut self, child: Box<dyn UiElement>) { if self.contains_mouse(input, state, Some(self.get_rect(input)))
self.children_under.push(child); && !self.has_mouse
&& self.on_mouse_enter.is_some()
{
self.has_mouse = true;
self.anim.jump_to(input.ct, self.on_mouse_enter.unwrap())
} }
/// Add a child above this sprite if !self.contains_mouse(input, state, Some(self.get_rect(input)))
//pub fn add_child_above(&mut self, child: Box<dyn UiElement>) { && self.has_mouse
// self.children_above.push(child); && self.on_mouse_leave.is_some()
//} {
self.has_mouse = false;
self.anim.jump_to(input.ct, self.on_mouse_leave.unwrap())
}
/// Add this image to the gpu sprite buffer self.anim.step(input.ct, input.time_since_last_run);
pub fn push_to_buffer(&self, input: &RenderInput, state: &mut RenderState) { }
pub fn contains_mouse(
&self,
input: &RenderInput,
state: &RenderState,
parent: Option<SpriteRect>,
) -> bool {
let rect = self.get_relative_rect(input, parent);
let fac = state.window.scale_factor() as f32;
let window_size = Vector2::new(
state.window_size.width as f32 / fac,
state.window_size.height as f32 / fac,
);
let pos = input.player.input.get_mouse_pos();
let mouse_pos = Vector2::new(
pos.x / fac - window_size.x / 2.0,
window_size.y / 2.0 - pos.y / fac,
);
let ne = rect.ne_corner();
let sw = rect.sw_corner();
return (mouse_pos.y < ne.y && mouse_pos.y > sw.y)
&& (mouse_pos.x > ne.x && mouse_pos.x < sw.x);
}
pub fn get_rect(&self, input: &RenderInput) -> SpriteRect {
let pos = Point2::new(self.rect.pos.x, self.rect.pos.y); let pos = Point2::new(self.rect.pos.x, self.rect.pos.y);
let dim = Vector2::new( let dim = Vector2::new(
self.rect.dim.y * input.ct.get_sprite(self.sprite).aspect, self.rect.dim.y * input.ct.get_sprite(self.anim.get_sprite()).aspect,
self.rect.dim.y, self.rect.dim.y,
); );
for c in &self.children_under { return SpriteRect { dim, pos };
c.push_to_buffer_child(input, state, pos, dim);
} }
let sprite = input.ct.get_sprite(self.sprite); pub fn get_relative_rect(&self, input: &RenderInput, parent: Option<SpriteRect>) -> SpriteRect {
let texture_a = sprite.get_first_frame(); // ANIMATE if let Some(parent) = parent {
let zero = Point2::new(
parent.pos.x - (parent.dim.x / 2.0),
parent.pos.y + (parent.dim.y / 2.0),
);
state.push_ui_buffer(UiInstance { let mut pos = zero
anchor: PositionAnchor::CC.to_int(), + Vector2::new(
position: pos.into(), self.rect.pos.x * parent.dim.x,
angle: to_radians(90.0), -self.rect.pos.y * parent.dim.y,
size: dim.y, );
color: [1.0, 1.0, 1.0, 1.0], let dim = Vector2::new(
texture_index: [texture_a, texture_a], self.rect.dim.x * parent.dim.x,
texture_fade: 1.0, self.rect.dim.y * parent.dim.y,
mask_index: self );
.mask
.map(|x| {
let sprite = input.ct.get_sprite(x);
let texture_b = sprite.get_first_frame(); // ANIMATE
[1, texture_b]
})
.unwrap_or([0, 0]),
});
for c in &self.children_above { pos += Vector2::new(dim.x, -dim.y) / 2.0;
c.push_to_buffer_child(input, state, pos, dim);
} return SpriteRect { dim, pos };
} else {
return self.get_rect(input);
} }
} }
impl UiElement for UiSprite { /// Add this image to the gpu sprite buffer
/// Add this image to the gpu sprite buffer, pub fn push_to_buffer(
/// as a child of another sprite
fn push_to_buffer_child(
&self, &self,
input: &RenderInput, input: &RenderInput,
state: &mut RenderState, state: &mut RenderState,
parent_pos: Point2<f32>, parent: Option<SpriteRect>,
parent_size: Vector2<f32>,
) { ) {
let zero = Point2::new( let rect = self.get_relative_rect(input, parent.clone());
parent_pos.x - (parent_size.x / 2.0),
parent_pos.y + (parent_size.y / 2.0),
);
let pos = zero
+ Vector2::new(
self.rect.pos.x * parent_size.x,
-self.rect.pos.y * parent_size.y,
);
let dim = Vector2::new(
self.rect.dim.x * parent_size.x,
self.rect.dim.y * parent_size.y,
);
for c in &self.children_under {
c.push_to_buffer_child(input, state, pos, dim);
}
let sprite = input.ct.get_sprite(self.sprite);
let texture_a = sprite.get_first_frame(); // ANIMATE
// TODO: use both dimensions,
// not just height
let anim_state = self.anim.get_texture_idx();
state.push_ui_buffer(UiInstance { state.push_ui_buffer(UiInstance {
anchor: PositionAnchor::CNw.to_int(), anchor: PositionAnchor::CC.to_int(),
position: pos.into(), position: rect.pos.into(),
angle: to_radians(90.0), angle: to_radians(90.0),
size: dim.y, size: rect.dim.y,
color: [1.0, 1.0, 1.0, 1.0], color: [1.0, 1.0, 1.0, 1.0],
texture_index: [texture_a, texture_a], texture_index: anim_state.texture_index(),
texture_fade: 1.0, texture_fade: anim_state.fade,
mask_index: self mask_index: self
.mask .mask
.map(|x| { .map(|x| {
@ -123,9 +149,13 @@ impl UiElement for UiSprite {
}) })
.unwrap_or([0, 0]), .unwrap_or([0, 0]),
}); });
}
for c in &self.children_above { pub fn get_sprite(&self) -> SpriteHandle {
c.push_to_buffer_child(input, state, pos, dim); self.anim.get_sprite()
} }
pub fn get_height(&self) -> f32 {
self.rect.dim.y
} }
} }