diff --git a/kayak_font/Cargo.toml b/kayak_font/Cargo.toml
index 4d787f23c6be0227b4cc403bdb348ca24e826b82..4b5ffdfdd09c3886eee2cb843f572d2e63bdeeab 100644
--- a/kayak_font/Cargo.toml
+++ b/kayak_font/Cargo.toml
@@ -8,6 +8,8 @@ edition = "2021"
 [dependencies]
 anyhow = { version = "1.0" }
 bevy = { git = "https://github.com/bevyengine/bevy", rev = "38c7d5eb9e81ab8e1aec03673599b25a9aa0c69c" }
+bytemuck = "1.7.2"
+crevice = { git = "https://github.com/bevyengine/bevy", rev = "38c7d5eb9e81ab8e1aec03673599b25a9aa0c69c" }
 serde = "1.0"
 serde_json = "1.0"
 serde_path_to_error = "0.1"
diff --git a/kayak_font/examples/bevy.rs b/kayak_font/examples/bevy.rs
index 25968860628eed43893d69a43184350cbedf8a9c..1fa0e5a6b101202f235dcda432d6b1a28fcb78ff 100644
--- a/kayak_font/examples/bevy.rs
+++ b/kayak_font/examples/bevy.rs
@@ -1,13 +1,31 @@
 use bevy::{
+    math::Vec2,
     prelude::{App as BevyApp, AssetServer, Commands, Handle, Res},
+    render2::{camera::OrthographicCameraBundle, color::Color},
     window::WindowDescriptor,
     PipelinedDefaultPlugins,
 };
 use kayak_font::{KayakFont, KayakFontPlugin};
 
+mod renderer;
+use renderer::FontRenderPlugin;
+use renderer::Text;
+
 fn startup(mut commands: Commands, asset_server: Res<AssetServer>) {
+    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
+
     let font_handle: Handle<KayakFont> = asset_server.load("roboto.kayak_font");
-    dbg!(font_handle);
+
+    commands
+        .spawn()
+        .insert(Text {
+            color: Color::WHITE,
+            content: "Hello World!".into(),
+            font_size: 32.0,
+            position: Vec2::new(5.0, 5.0),
+            size: Vec2::new(100.0, 100.0),
+        })
+        .insert(font_handle);
 }
 
 fn main() {
@@ -20,6 +38,7 @@ fn main() {
         })
         .add_plugins(PipelinedDefaultPlugins)
         .add_plugin(KayakFontPlugin)
+        .add_plugin(FontRenderPlugin)
         .add_startup_system(startup)
         .run();
 }
diff --git a/kayak_font/examples/renderer/extract.rs b/kayak_font/examples/renderer/extract.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2900f192f62848298ebb1c930c4f133928fa84fe
--- /dev/null
+++ b/kayak_font/examples/renderer/extract.rs
@@ -0,0 +1,47 @@
+use bevy::{
+    prelude::{Assets, Commands, Handle, Query, Res},
+    sprite2::Rect,
+};
+use kayak_font::{CoordinateSystem, KayakFont};
+
+use super::{
+    pipeline::{ExtractCharBundle, ExtractedChar},
+    Text,
+};
+
+pub fn extract(
+    mut commands: Commands,
+    fonts: Res<Assets<KayakFont>>,
+    texts: Query<(&Text, &Handle<KayakFont>)>,
+) {
+    let mut extracted_texts = Vec::new();
+
+    for (text, font_handle) in texts.iter() {
+        if let Some(font) = fonts.get(font_handle) {
+            let layouts = font.get_layout(
+                CoordinateSystem::PositiveYUp,
+                text.position,
+                &text.content,
+                text.font_size,
+            );
+
+            for layout in layouts {
+                extracted_texts.push(ExtractCharBundle {
+                    extracted_quad: ExtractedChar {
+                        font_handle: Some(font_handle.clone()),
+                        rect: Rect {
+                            min: layout.position,
+                            max: layout.position + layout.size,
+                        },
+                        color: text.color,
+                        vertex_index: 0,
+                        char_id: font.get_char_id(layout.content).unwrap(),
+                        z_index: 0.0,
+                    },
+                });
+            }
+        }
+    }
+
+    commands.spawn_batch(extracted_texts);
+}
diff --git a/kayak_font/examples/renderer/mod.rs b/kayak_font/examples/renderer/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ca7cc81a07904a8172663f116efec9b1843e803a
--- /dev/null
+++ b/kayak_font/examples/renderer/mod.rs
@@ -0,0 +1,63 @@
+use bevy::{
+    core_pipeline::Transparent2d,
+    prelude::{Assets, HandleUntyped, Plugin, Res, ResMut},
+    reflect::TypeUuid,
+    render2::{
+        render_asset::RenderAssets,
+        render_phase::DrawFunctions,
+        render_resource::Shader,
+        renderer::{RenderDevice, RenderQueue},
+        texture::Image,
+        RenderApp, RenderStage,
+    },
+};
+use kayak_font::FontTextureCache;
+
+use self::pipeline::{DrawUI, FontPipeline, QuadMeta};
+
+mod extract;
+pub mod pipeline;
+mod text;
+
+pub use text::*;
+
+pub const FONT_SHADER_HANDLE: HandleUntyped =
+    HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7604018236855288450);
+
+pub struct FontRenderPlugin;
+
+impl Plugin for FontRenderPlugin {
+    fn build(&self, app: &mut bevy::prelude::App) {
+        let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
+        let unified_shader = Shader::from_wgsl(include_str!("shader.wgsl"));
+        shaders.set_untracked(FONT_SHADER_HANDLE, unified_shader);
+
+        let render_app = app.sub_app(RenderApp);
+        render_app
+            .init_resource::<QuadMeta>()
+            .init_resource::<FontPipeline>()
+            .add_system_to_stage(RenderStage::Extract, extract::extract)
+            .add_system_to_stage(RenderStage::Prepare, pipeline::prepare_quads)
+            .add_system_to_stage(RenderStage::Queue, pipeline::queue_quads)
+            .add_system_to_stage(RenderStage::Queue, create_and_update_font_cache_texture);
+
+        let draw_quad = DrawUI::new(&mut render_app.world);
+
+        render_app
+            .world
+            .get_resource::<DrawFunctions<Transparent2d>>()
+            .unwrap()
+            .write()
+            .add(draw_quad);
+    }
+}
+
+fn create_and_update_font_cache_texture(
+    device: Res<RenderDevice>,
+    queue: Res<RenderQueue>,
+    pipeline: Res<FontPipeline>,
+    mut font_texture_cache: ResMut<FontTextureCache>,
+    images: Res<RenderAssets<Image>>,
+) {
+    font_texture_cache.process_new(&device, &queue, pipeline.into_inner(), &images);
+}
diff --git a/kayak_font/examples/renderer/pipeline.rs b/kayak_font/examples/renderer/pipeline.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8807dd337e41e3af1840c09c75701ae2eeae358a
--- /dev/null
+++ b/kayak_font/examples/renderer/pipeline.rs
@@ -0,0 +1,395 @@
+use bevy::{
+    core::FloatOrd,
+    core_pipeline::Transparent2d,
+    ecs::system::{
+        lifetimeless::{Read, SQuery, SRes},
+        SystemState,
+    },
+    math::{const_vec3, Mat4, Quat, Vec2, Vec3, Vec4},
+    prelude::{Bundle, Component, Entity, FromWorld, Handle, Query, Res, ResMut, World},
+    render2::{
+        color::Color,
+        render_phase::{Draw, DrawFunctions, RenderPhase, TrackedRenderPass},
+        render_resource::{
+            BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout,
+            BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendComponent,
+            BlendFactor, BlendOperation, BlendState, BufferBindingType, BufferSize, BufferUsages,
+            BufferVec, CachedPipelineId, ColorTargetState, ColorWrites, FragmentState, FrontFace,
+            MultisampleState, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineCache,
+            RenderPipelineDescriptor, Shader, ShaderStages, TextureFormat, TextureSampleType,
+            TextureViewDimension, VertexAttribute, VertexBufferLayout, VertexFormat, VertexState,
+            VertexStepMode,
+        },
+        renderer::{RenderDevice, RenderQueue},
+        texture::{BevyDefault, GpuImage},
+        view::{ViewUniformOffset, ViewUniforms},
+    },
+    sprite2::Rect,
+};
+use bytemuck::{Pod, Zeroable};
+use crevice::std140::AsStd140;
+use kayak_font::{FontRenderingPipeline, FontTextureCache, KayakFont};
+
+use super::FONT_SHADER_HANDLE;
+
+pub struct FontPipeline {
+    view_layout: BindGroupLayout,
+    pub(crate) font_image_layout: BindGroupLayout,
+    pipeline: CachedPipelineId,
+    empty_font_texture: (GpuImage, BindGroup),
+}
+
+const QUAD_VERTEX_POSITIONS: &[Vec3] = &[
+    const_vec3!([0.0, 0.0, 0.0]),
+    const_vec3!([1.0, 1.0, 0.0]),
+    const_vec3!([0.0, 1.0, 0.0]),
+    const_vec3!([0.0, 0.0, 0.0]),
+    const_vec3!([1.0, 0.0, 0.0]),
+    const_vec3!([1.0, 1.0, 0.0]),
+];
+
+impl FontRenderingPipeline for FontPipeline {
+    fn get_font_image_layout(&self) -> &BindGroupLayout {
+        &self.font_image_layout
+    }
+}
+
+impl FromWorld for FontPipeline {
+    fn from_world(world: &mut World) -> Self {
+        let world = world.cell();
+        let render_device = world.get_resource::<RenderDevice>().unwrap();
+        let mut pipeline_cache = world.get_resource_mut::<RenderPipelineCache>().unwrap();
+
+        let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
+            entries: &[BindGroupLayoutEntry {
+                binding: 0,
+                visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
+                ty: BindingType::Buffer {
+                    ty: BufferBindingType::Uniform,
+                    has_dynamic_offset: true,
+                    // TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
+                    // Context: https://github.com/LPGhatguy/crevice/issues/29
+                    min_binding_size: BufferSize::new(144),
+                },
+                count: None,
+            }],
+            label: Some("ui_view_layout"),
+        });
+
+        // Used by fonts
+        let font_image_layout =
+            render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
+                entries: &[
+                    BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: ShaderStages::FRAGMENT,
+                        ty: BindingType::Texture {
+                            multisampled: false,
+                            sample_type: TextureSampleType::Float { filterable: true },
+                            view_dimension: TextureViewDimension::D2Array,
+                        },
+                        count: None,
+                    },
+                    BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: ShaderStages::FRAGMENT,
+                        ty: BindingType::Sampler {
+                            comparison: false,
+                            filtering: true,
+                        },
+                        count: None,
+                    },
+                ],
+                label: Some("text_image_layout"),
+            });
+
+        let vertex_buffer_layout = VertexBufferLayout {
+            array_stride: 60,
+            step_mode: VertexStepMode::Vertex,
+            attributes: vec![
+                VertexAttribute {
+                    format: VertexFormat::Float32x3,
+                    offset: 0,
+                    shader_location: 0,
+                },
+                VertexAttribute {
+                    format: VertexFormat::Float32x4,
+                    offset: 12,
+                    shader_location: 1,
+                },
+                VertexAttribute {
+                    format: VertexFormat::Float32x4,
+                    offset: 28,
+                    shader_location: 2,
+                },
+                VertexAttribute {
+                    format: VertexFormat::Float32x4,
+                    offset: 44,
+                    shader_location: 3,
+                },
+            ],
+        };
+
+        let empty_font_texture = FontTextureCache::get_empty(&render_device, &font_image_layout);
+
+        let pipeline_desc = RenderPipelineDescriptor {
+            vertex: VertexState {
+                shader: FONT_SHADER_HANDLE.typed::<Shader>(),
+                entry_point: "vertex".into(),
+                shader_defs: vec![],
+                buffers: vec![vertex_buffer_layout],
+            },
+            fragment: Some(FragmentState {
+                shader: FONT_SHADER_HANDLE.typed::<Shader>(),
+                shader_defs: vec![],
+                entry_point: "fragment".into(),
+                targets: vec![ColorTargetState {
+                    format: TextureFormat::bevy_default(),
+                    blend: Some(BlendState {
+                        color: BlendComponent {
+                            src_factor: BlendFactor::SrcAlpha,
+                            dst_factor: BlendFactor::OneMinusSrcAlpha,
+                            operation: BlendOperation::Add,
+                        },
+                        alpha: BlendComponent {
+                            src_factor: BlendFactor::One,
+                            dst_factor: BlendFactor::One,
+                            operation: BlendOperation::Add,
+                        },
+                    }),
+                    write_mask: ColorWrites::ALL,
+                }],
+            }),
+            layout: Some(vec![view_layout.clone(), font_image_layout.clone()]),
+            primitive: PrimitiveState {
+                front_face: FrontFace::Ccw,
+                cull_mode: None,
+                polygon_mode: PolygonMode::Fill,
+                clamp_depth: false,
+                conservative: false,
+                topology: PrimitiveTopology::TriangleList,
+                strip_index_format: None,
+            },
+            depth_stencil: None,
+            multisample: MultisampleState {
+                count: 1,
+                mask: !0,
+                alpha_to_coverage_enabled: false,
+            },
+            label: Some("font_pipeline".into()),
+        };
+
+        FontPipeline {
+            pipeline: pipeline_cache.queue(pipeline_desc),
+            view_layout,
+            font_image_layout,
+            empty_font_texture,
+        }
+    }
+}
+
+#[derive(Debug, Bundle)]
+pub struct ExtractCharBundle {
+    pub(crate) extracted_quad: ExtractedChar,
+}
+
+#[derive(Debug, Component, Clone)]
+pub struct ExtractedChar {
+    pub rect: Rect,
+    pub color: Color,
+    pub vertex_index: usize,
+    pub char_id: u32,
+    pub z_index: f32,
+    pub font_handle: Option<Handle<KayakFont>>,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Pod, Zeroable)]
+struct QuadVertex {
+    pub position: [f32; 3],
+    pub color: [f32; 4],
+    pub uv: [f32; 4],
+    pub pos_size: [f32; 4],
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, AsStd140)]
+struct QuadType {
+    pub t: i32,
+}
+
+pub struct QuadMeta {
+    vertices: BufferVec<QuadVertex>,
+    view_bind_group: Option<BindGroup>,
+}
+
+impl Default for QuadMeta {
+    fn default() -> Self {
+        Self {
+            vertices: BufferVec::new(BufferUsages::VERTEX),
+            view_bind_group: None,
+        }
+    }
+}
+
+pub fn prepare_quads(
+    render_device: Res<RenderDevice>,
+    render_queue: Res<RenderQueue>,
+    mut sprite_meta: ResMut<QuadMeta>,
+    mut extracted_quads: Query<&mut ExtractedChar>,
+) {
+    let extracted_sprite_len = extracted_quads.iter_mut().len();
+    // don't create buffers when there are no quads
+    if extracted_sprite_len == 0 {
+        return;
+    }
+
+    sprite_meta.vertices.clear();
+    sprite_meta.vertices.reserve(
+        extracted_sprite_len * QUAD_VERTEX_POSITIONS.len(),
+        &render_device,
+    );
+
+    for (i, mut extracted_sprite) in extracted_quads.iter_mut().enumerate() {
+        let sprite_rect = extracted_sprite.rect;
+        let color = extracted_sprite.color.as_linear_rgba_f32();
+
+        let uv_min = Vec2::ZERO;
+        let uv_max = Vec2::ONE;
+
+        let bottom_left = Vec4::new(uv_min.x, uv_max.y, extracted_sprite.char_id as f32, 0.0);
+        let top_left = Vec4::new(uv_min.x, uv_min.y, extracted_sprite.char_id as f32, 0.0);
+        let top_right = Vec4::new(uv_max.x, uv_min.y, extracted_sprite.char_id as f32, 0.0);
+        let bottom_right = Vec4::new(uv_max.x, uv_max.y, extracted_sprite.char_id as f32, 0.0);
+
+        let uvs: [[f32; 4]; 6] = [
+            bottom_left.into(),
+            top_right.into(),
+            top_left.into(),
+            bottom_left.into(),
+            bottom_right.into(),
+            top_right.into(),
+        ];
+
+        extracted_sprite.vertex_index = i;
+        for (index, vertex_position) in QUAD_VERTEX_POSITIONS.iter().enumerate() {
+            let world = Mat4::from_scale_rotation_translation(
+                sprite_rect.size().extend(1.0),
+                Quat::default(),
+                sprite_rect.min.extend(0.0),
+            );
+            let final_position = (world * Vec3::from(*vertex_position).extend(1.0)).truncate();
+            sprite_meta.vertices.push(QuadVertex {
+                position: final_position.into(),
+                color,
+                uv: uvs[index],
+                pos_size: [
+                    sprite_rect.min.x,
+                    sprite_rect.min.y,
+                    sprite_rect.size().x,
+                    sprite_rect.size().y,
+                ],
+            });
+        }
+    }
+    sprite_meta
+        .vertices
+        .write_buffer(&render_device, &render_queue);
+}
+
+pub fn queue_quads(
+    draw_functions: Res<DrawFunctions<Transparent2d>>,
+    render_device: Res<RenderDevice>,
+    mut sprite_meta: ResMut<QuadMeta>,
+    view_uniforms: Res<ViewUniforms>,
+    quad_pipeline: Res<FontPipeline>,
+    mut extracted_sprites: Query<(Entity, &ExtractedChar)>,
+    mut views: Query<&mut RenderPhase<Transparent2d>>,
+) {
+    if let Some(view_binding) = view_uniforms.uniforms.binding() {
+        sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor {
+            entries: &[BindGroupEntry {
+                binding: 0,
+                resource: view_binding,
+            }],
+            label: Some("quad_view_bind_group"),
+            layout: &quad_pipeline.view_layout,
+        }));
+
+        let draw_quad = draw_functions.read().get_id::<DrawUI>().unwrap();
+        for mut transparent_phase in views.iter_mut() {
+            for (entity, quad) in extracted_sprites.iter_mut() {
+                transparent_phase.add(Transparent2d {
+                    draw_function: draw_quad,
+                    pipeline: quad_pipeline.pipeline,
+                    entity,
+                    sort_key: FloatOrd(quad.z_index),
+                });
+            }
+        }
+    }
+}
+
+pub struct DrawUI {
+    params: SystemState<(
+        SRes<QuadMeta>,
+        SRes<FontPipeline>,
+        SRes<RenderPipelineCache>,
+        SRes<FontTextureCache>,
+        SQuery<Read<ViewUniformOffset>>,
+        SQuery<Read<ExtractedChar>>,
+    )>,
+}
+
+impl DrawUI {
+    pub fn new(world: &mut World) -> Self {
+        Self {
+            params: SystemState::new(world),
+        }
+    }
+}
+
+impl Draw<Transparent2d> for DrawUI {
+    fn draw<'w>(
+        &mut self,
+        world: &'w World,
+        pass: &mut TrackedRenderPass<'w>,
+        view: Entity,
+        item: &Transparent2d,
+    ) {
+        let (quad_meta, unified_pipeline, pipelines, font_texture_cache, views, quads) =
+            self.params.get(world);
+
+        let view_uniform = views.get(view).unwrap();
+        let quad_meta = quad_meta.into_inner();
+        let extracted_quad = quads.get(item.entity).unwrap();
+        if let Some(pipeline) = pipelines.into_inner().get(item.pipeline) {
+            pass.set_render_pipeline(pipeline);
+            pass.set_vertex_buffer(0, quad_meta.vertices.buffer().unwrap().slice(..));
+            pass.set_bind_group(
+                0,
+                quad_meta.view_bind_group.as_ref().unwrap(),
+                &[view_uniform.offset],
+            );
+
+            let unified_pipeline = unified_pipeline.into_inner();
+            if let Some(font_handle) = extracted_quad.font_handle.as_ref() {
+                if let Some(image_bindings) =
+                    font_texture_cache.into_inner().get_binding(font_handle)
+                {
+                    pass.set_bind_group(1, image_bindings, &[]);
+                } else {
+                    pass.set_bind_group(1, &unified_pipeline.empty_font_texture.1, &[]);
+                }
+            } else {
+                pass.set_bind_group(1, &unified_pipeline.empty_font_texture.1, &[]);
+            }
+
+            pass.draw(
+                (extracted_quad.vertex_index * QUAD_VERTEX_POSITIONS.len()) as u32
+                    ..((extracted_quad.vertex_index + 1) * QUAD_VERTEX_POSITIONS.len()) as u32,
+                0..1,
+            );
+        }
+    }
+}
diff --git a/kayak_font/examples/renderer/shader.wgsl b/kayak_font/examples/renderer/shader.wgsl
new file mode 100644
index 0000000000000000000000000000000000000000..097890a21b4d880e4b2b3fe10a5c25ee0bfa411a
--- /dev/null
+++ b/kayak_font/examples/renderer/shader.wgsl
@@ -0,0 +1,52 @@
+[[block]]
+struct View {
+    view_proj: mat4x4<f32>;
+    world_position: vec3<f32>;
+};
+[[group(0), binding(0)]]
+var<uniform> view: View;
+
+struct VertexOutput {
+    [[builtin(position)]] position: vec4<f32>;
+    [[location(0)]] color: vec4<f32>;
+    [[location(1)]] uv: vec3<f32>;
+    [[location(2)]] pos: vec2<f32>;
+    [[location(3)]] size: vec2<f32>;
+    [[location(4)]] screen_position: vec2<f32>;
+    [[location(5)]] border_radius: f32;
+};
+
+[[stage(vertex)]]
+fn vertex(
+    [[location(0)]] vertex_position: vec3<f32>,
+    [[location(1)]] vertex_color: vec4<f32>,
+    [[location(2)]] vertex_uv: vec4<f32>,
+    [[location(3)]] vertex_pos_size: vec4<f32>,
+) -> VertexOutput {
+    var out: VertexOutput;
+    out.color = vertex_color;
+    out.pos = vertex_pos_size.xy;
+    out.position = view.view_proj * vec4<f32>(vertex_position, 1.0);
+    out.screen_position = (view.view_proj * vec4<f32>(vertex_position, 1.0)).xy;
+    out.uv = vertex_uv.xyz;
+    out.size = vertex_pos_size.zw;
+    out.border_radius = vertex_uv.w;
+    return out;
+}
+
+[[group(1), binding(0)]]
+var font_texture: texture_2d_array<f32>;
+[[group(1), binding(1)]]
+var font_sampler: sampler;
+
+[[stage(fragment)]]
+fn fragment(in: VertexOutput) -> [[location(0)]] vec4<f32> {
+    var px_range = 2.5;
+    var tex_dimensions = textureDimensions(font_texture);
+    var msdf_unit = vec2<f32>(px_range, px_range) / vec2<f32>(f32(tex_dimensions.x), f32(tex_dimensions.y));
+    var x = textureSample(font_texture, font_sampler, vec2<f32>(in.uv.x, in.uv.y), i32(in.uv.z)); 
+    var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b));
+    var sig_dist = (v - 0.5) * dot(msdf_unit, 0.5 / fwidth(in.uv.xy));
+    var a = clamp(sig_dist + 0.5, 0.0, 1.0);
+    return vec4<f32>(in.color.rgb, a);
+}
\ No newline at end of file
diff --git a/kayak_font/examples/renderer/text.rs b/kayak_font/examples/renderer/text.rs
new file mode 100644
index 0000000000000000000000000000000000000000..845e2e47696dd0b1b2d9f7d2969ca0b4fd5efad1
--- /dev/null
+++ b/kayak_font/examples/renderer/text.rs
@@ -0,0 +1,10 @@
+use bevy::{math::Vec2, prelude::Component, render2::color::Color};
+
+#[derive(Component)]
+pub struct Text {
+    pub content: String,
+    pub position: Vec2,
+    pub size: Vec2,
+    pub font_size: f32,
+    pub color: Color,
+}
diff --git a/kayak_font/src/font.rs b/kayak_font/src/font.rs
index b50fedf55a56ec26649d734ece2a59838a0e93be..000aeb6c1ef35d279553fc95f37e47f852dff3a4 100644
--- a/kayak_font/src/font.rs
+++ b/kayak_font/src/font.rs
@@ -25,6 +25,12 @@ pub struct LayoutRect {
     pub content: char,
 }
 
+#[derive(Debug, Clone, Copy)]
+pub enum CoordinateSystem {
+    PositiveYUp,
+    PositiveYDown,
+}
+
 impl KayakFont {
     pub fn new(sdf: Sdf, atlas_image: Handle<Image>) -> Self {
         Self {
@@ -46,7 +52,13 @@ impl KayakFont {
         self.char_ids.get(&c).and_then(|id| Some(*id))
     }
 
-    pub fn get_layout(&self, position: Vec2, content: &String, font_size: f32) -> Vec<LayoutRect> {
+    pub fn get_layout(
+        &self,
+        axis_alignment: CoordinateSystem,
+        position: Vec2,
+        content: &String,
+        font_size: f32,
+    ) -> Vec<LayoutRect> {
         let mut positions_and_size = Vec::new();
         let max_glyph_size = self.sdf.max_glyph_size();
         let font_ratio = font_size / self.sdf.atlas.size;
@@ -66,8 +78,13 @@ impl KayakFont {
                     None => (0.0, 0.0, 0.0, 0.0),
                 };
 
+                let shift_sign = match axis_alignment {
+                    CoordinateSystem::PositiveYDown => -1.0,
+                    CoordinateSystem::PositiveYUp => 1.0,
+                };
+
                 let position_x = position.x + x + left * font_size;
-                let position_y = (position.y + (-top * font_size)) + font_size;
+                let position_y = (position.y + (shift_sign * top * font_size)) + font_size;
 
                 positions_and_size.push(LayoutRect {
                     position: Vec2::new(position_x, position_y),