ui transforms

This commit is contained in:
griffi-gh 2024-03-02 00:33:02 +01:00
parent da61904a5a
commit b46db55f1b
6 changed files with 186 additions and 3 deletions

View file

@ -0,0 +1,63 @@
use std::f32::consts::PI;
use glam::{vec4, Vec2};
use hui::{
element::{
container::Container,
text::Text,
transformer::ElementTransformExt,
UiElementExt
},
layout::Alignment,
rectangle::Corners,
text::FontHandle,
size,
};
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
ui_main!(
init: |ui| {
let font = ui.add_font(include_bytes!("../assets/blink/Blink-ynYZ.otf"));
ui.push_font(font);
(std::time::Instant::now(),)
},
run: |ui, size, (instant,)| {
let elapsed_sec = instant.elapsed().as_secs_f32();
Container::default()
.with_background(Corners {
top_left: vec4(0.2, 0.2, 0.3, 1.),
top_right: vec4(0.3, 0.3, 0.4, 1.),
bottom_left: vec4(0.2, 0.3, 0.2, 1.),
bottom_right: vec4(0.5, 0.4, 0.4, 1.),
})
.with_size(size!(100%))
.with_align(Alignment::Center)
.with_children(|ui| {
Container::default()
.with_align((Alignment::Center, Alignment::Begin))
.with_padding(15.)
.with_gap(10.)
.with_corner_radius(8.)
.with_background((0., 0., 0., 0.5))
.with_children(|ui| {
Text::default()
.with_text("Did you know?")
.with_text_size(18)
.add_child(ui);
Text::default()
.with_text("You can die by jumping into the spike pit! :D\nCheck out the tutorial section for more tips.")
.with_text_size(24)
.with_font(FontHandle::default())
.add_child(ui);
})
.add_child(ui);
})
.transform()
.scale(Vec2::splat(elapsed_sec.sin() * 0.1 + 1.))
.rotate(elapsed_sec * PI / 4.)
.add_root(ui, size);
}
);

View file

@ -16,7 +16,7 @@ pub use corner_radius::RoundedCorners;
use std::borrow::Cow; use std::borrow::Cow;
use fontdue::layout::{Layout, CoordinateSystem, TextStyle}; use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
use glam::{Vec2, Vec4, vec2}; use glam::{vec2, Vec2, Affine2, Vec4};
//TODO: circle draw command //TODO: circle draw command
@ -51,6 +51,10 @@ pub enum UiDrawCommand {
///Font handle to use ///Font handle to use
font: FontHandle, font: FontHandle,
}, },
/// Push a transformation matrix to the stack
PushTransform(Affine2),
/// Pop a transformation matrix from the stack
PopTransform,
} }
/// List of draw commands /// List of draw commands
@ -91,9 +95,45 @@ pub struct UiDrawCall {
impl UiDrawCall { impl UiDrawCall {
/// Tesselate the UI and build a complete draw plan from a list of draw commands /// Tesselate the UI and build a complete draw plan from a list of draw commands
pub(crate) fn build(draw_commands: &UiDrawCommandList, atlas: &mut TextureAtlasManager, text_renderer: &mut TextRenderer) -> Self { pub(crate) fn build(draw_commands: &UiDrawCommandList, atlas: &mut TextureAtlasManager, text_renderer: &mut TextRenderer) -> Self {
let mut trans_stack = Vec::new();
let mut draw_call = UiDrawCall::default(); let mut draw_call = UiDrawCall::default();
for command in &draw_commands.commands { for command in &draw_commands.commands {
match command { match command {
UiDrawCommand::PushTransform(trans) => {
//Take note of the current index, and the transformation matrix\
//We will actually apply the transformation matrix when we pop it,
//to all vertices between the current index and the index we pushed
trans_stack.push((trans, draw_call.vertices.len() as u32));
},
UiDrawCommand::PopTransform => {
//Pop the transformation matrix and apply it to all vertices between the current index and the index we pushed
let (&trans, idx) = trans_stack.pop().expect("Unbalanced push/pop transform");
//If Push is immediately followed by a pop (which is dumb but possible), we don't need to do anything
//(this can also happen if push and pop are separated by a draw command that doesn't add any vertices, like a text command with an empty string)
if idx == draw_call.vertices.len() as u32 {
continue
}
//Kinda a hack:
//We want to apply the transform aronnd the center, so we need to compute the center of the vertices
//We won't actually do that, we will compute the center of the bounding box of the vertices
let mut min = Vec2::splat(std::f32::INFINITY);
let mut max = Vec2::splat(std::f32::NEG_INFINITY);
for v in &draw_call.vertices[idx as usize..] {
min = min.min(v.position);
max = max.max(v.position);
}
//TODO: make the point of transform configurable
let center = (min + max) / 2.;
//Apply trans mtx to all vertices between idx and the current index
for v in &mut draw_call.vertices[idx as usize..] {
v.position -= center;
v.position = trans.transform_point2(v.position);
v.position += center;
}
},
UiDrawCommand::Rectangle { position, size, color, texture, rounded_corners } => { UiDrawCommand::Rectangle { position, size, color, texture, rounded_corners } => {
let uvs = texture let uvs = texture
.map(|x| atlas.get_uv(x)) .map(|x| atlas.get_uv(x))
@ -257,6 +297,8 @@ impl UiDrawCall {
feature = "pixel_perfect_text", feature = "pixel_perfect_text",
not(feature = "pixel_perfect") not(feature = "pixel_perfect")
))] { ))] {
//Round the position of the vertices to the nearest pixel, unless any transformations are active
if trans_stack.is_empty() {
for vtx in &mut draw_call.vertices[(vidx as usize)..] { for vtx in &mut draw_call.vertices[(vidx as usize)..] {
vtx.position = vtx.position.round() vtx.position = vtx.position.round()
} }
@ -265,6 +307,7 @@ impl UiDrawCall {
} }
} }
} }
}
#[cfg(feature = "pixel_perfect")] #[cfg(feature = "pixel_perfect")]
draw_call.vertices.iter_mut().for_each(|v| { draw_call.vertices.iter_mut().for_each(|v| {
v.position = v.position.round() v.position = v.position.round()

View file

@ -12,3 +12,8 @@ pub mod progress_bar;
#[cfg(feature = "builtin_elements")] #[cfg(feature = "builtin_elements")]
pub mod text; pub mod text;
#[cfg(feature = "builtin_elements")]
pub mod transformer;
//TODO add: Button, Checkbox, Dropdown, Input, Radio, Slider, Textarea, Toggle, etc.

View file

@ -1,3 +1,7 @@
//TODO this thing?
//not sure if this is a good idea...
//but having the ability to add a click event to any element would be nice, and this is a naive way to do it
// use crate::element::{UiElement, MeasureContext, ProcessContext}; // use crate::element::{UiElement, MeasureContext, ProcessContext};
// pub struct Interactable<T: UiElement> { // pub struct Interactable<T: UiElement> {

View file

@ -0,0 +1,68 @@
use glam::{Affine2, Vec2};
use crate::{
draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, measure::Response
};
pub struct Transformer {
pub transform: Affine2,
pub element: Box<dyn UiElement>,
}
impl Transformer {
pub fn new(element: Box<dyn UiElement>) -> Self {
Self {
transform: Affine2::IDENTITY,
element,
}
}
pub fn translate(mut self, v: impl Into<Vec2>) -> Self {
self.transform *= Affine2::from_translation(v.into());
self
}
pub fn scale(mut self, v: impl Into<Vec2>) -> Self {
self.transform *= Affine2::from_scale(v.into());
self
}
pub fn rotate(mut self, radians: f32) -> Self {
self.transform *= Affine2::from_angle(radians);
self
}
}
impl UiElement for Transformer {
fn measure(&self, ctx: MeasureContext) -> Response {
self.element.measure(ctx)
}
fn process(&self, ctx: ProcessContext) {
ctx.draw.add(UiDrawCommand::PushTransform(self.transform));
//This is stupid:
self.element.process(ProcessContext {
measure: ctx.measure,
state: ctx.state,
layout: ctx.layout,
draw: ctx.draw,
text_measure: ctx.text_measure,
current_font: ctx.current_font,
});
ctx.draw.add(UiDrawCommand::PopTransform);
}
}
pub trait ElementTransformExt {
fn transform(self) -> Transformer;
}
impl<T: UiElement + 'static> ElementTransformExt for T {
/// Wrap the element in a [`Transformer`]
///
/// This allows you to apply various transformations to the element, such as translation, rotation, or scaling\
/// Use sparingly, as this is an experimental feature and may not work as expected\
/// Transform is applied around the center of the element's bounding box.
fn transform(self) -> Transformer {
Transformer::new(Box::new(self))
}
}