1use crate::{
2 context::DrawContext,
3 sprite::SpriteTexture,
4 utils::{Drawable, ShaderRef, Vertex},
5};
6use smallvec::SmallVec;
7use spitfire_core::Triangle;
8use spitfire_glow::{
9 graphics::{Graphics, GraphicsBatch},
10 renderer::{GlowBlending, GlowUniformValue},
11};
12use std::{borrow::Cow, collections::HashMap};
13use vek::{Mat4, Quaternion, Rect, Rgba, Transform, Vec2, Vec3};
14
15#[derive(Debug, Default, Clone, Copy)]
16pub struct NineSliceMargins {
17 pub left: f32,
18 pub right: f32,
19 pub top: f32,
20 pub bottom: f32,
21}
22
23impl NineSliceMargins {
24 pub fn clamp(self) -> Self {
25 Self {
26 left: self.left.clamp(0.0, 1.0),
27 right: self.right.clamp(0.0, 1.0),
28 top: self.top.clamp(0.0, 1.0),
29 bottom: self.bottom.clamp(0.0, 1.0),
30 }
31 }
32
33 pub fn fit_to_size(self, size: Vec2<f32>) -> Self {
34 let mut result = self;
35 let width = result.left + result.right;
36 let height = result.top + result.bottom;
37 if width > size.x {
38 result.left = result.left / width * size.x;
39 result.right = result.right / width * size.x;
40 }
41 if height > size.x {
42 result.top = result.top / height * size.y;
43 result.bottom = result.bottom / height * size.y;
44 }
45 result
46 }
47}
48
49impl From<f32> for NineSliceMargins {
50 fn from(value: f32) -> Self {
51 Self {
52 left: value,
53 right: value,
54 top: value,
55 bottom: value,
56 }
57 }
58}
59
60impl From<[f32; 2]> for NineSliceMargins {
61 fn from([hor, ver]: [f32; 2]) -> Self {
62 Self {
63 left: hor,
64 right: hor,
65 top: ver,
66 bottom: ver,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
72pub struct NineSliceSprite {
73 pub shader: Option<ShaderRef>,
74 pub textures: SmallVec<[SpriteTexture; 4]>,
75 pub uniforms: HashMap<Cow<'static, str>, GlowUniformValue>,
76 pub region: Rect<f32, f32>,
77 pub page: f32,
78 pub margins_source: NineSliceMargins,
79 pub margins_target: NineSliceMargins,
80 pub frame_only: bool,
81 pub tint: Rgba<f32>,
82 pub transform: Transform<f32, f32, f32>,
83 pub size: Option<Vec2<f32>>,
84 pub pivot: Vec2<f32>,
85 pub blending: Option<GlowBlending>,
86 pub screen_space: bool,
87}
88
89impl Default for NineSliceSprite {
90 fn default() -> Self {
91 Self {
92 shader: Default::default(),
93 textures: Default::default(),
94 uniforms: Default::default(),
95 region: Rect::new(0.0, 0.0, 1.0, 1.0),
96 page: Default::default(),
97 margins_source: Default::default(),
98 margins_target: Default::default(),
99 frame_only: false,
100 tint: Rgba::white(),
101 transform: Default::default(),
102 size: Default::default(),
103 pivot: Default::default(),
104 blending: Default::default(),
105 screen_space: Default::default(),
106 }
107 }
108}
109
110impl NineSliceSprite {
111 pub fn single(texture: SpriteTexture) -> Self {
112 Self {
113 textures: vec![texture].into(),
114 ..Default::default()
115 }
116 }
117
118 pub fn shader(mut self, value: ShaderRef) -> Self {
119 self.shader = Some(value);
120 self
121 }
122
123 pub fn texture(mut self, value: SpriteTexture) -> Self {
124 self.textures.push(value);
125 self
126 }
127
128 pub fn uniform(mut self, key: Cow<'static, str>, value: GlowUniformValue) -> Self {
129 self.uniforms.insert(key, value);
130 self
131 }
132
133 pub fn region_page(mut self, region: Rect<f32, f32>, page: f32) -> Self {
134 self.region = region;
135 self.page = page;
136 self
137 }
138
139 pub fn margins_source(mut self, margins: NineSliceMargins) -> Self {
140 self.margins_source = margins;
141 self
142 }
143
144 pub fn margins_target(mut self, margins: NineSliceMargins) -> Self {
145 self.margins_target = margins;
146 self
147 }
148
149 pub fn frame_only(mut self, value: bool) -> Self {
150 self.frame_only = value;
151 self
152 }
153
154 pub fn tint(mut self, value: Rgba<f32>) -> Self {
155 self.tint = value;
156 self
157 }
158
159 pub fn transform(mut self, value: Transform<f32, f32, f32>) -> Self {
160 self.transform = value;
161 self
162 }
163
164 pub fn position(mut self, value: Vec2<f32>) -> Self {
165 self.transform.position = value.into();
166 self
167 }
168
169 pub fn orientation(mut self, value: Quaternion<f32>) -> Self {
170 self.transform.orientation = value;
171 self
172 }
173
174 pub fn rotation(mut self, angle_radians: f32) -> Self {
175 self.transform.orientation = Quaternion::rotation_z(angle_radians);
176 self
177 }
178
179 pub fn scale(mut self, value: Vec2<f32>) -> Self {
180 self.transform.scale = Vec3::new(value.x, value.y, 1.0);
181 self
182 }
183
184 pub fn size(mut self, value: Vec2<f32>) -> Self {
185 self.size = Some(value);
186 self
187 }
188
189 pub fn pivot(mut self, value: Vec2<f32>) -> Self {
190 self.pivot = value;
191 self
192 }
193
194 pub fn blending(mut self, value: GlowBlending) -> Self {
195 self.blending = Some(value);
196 self
197 }
198
199 pub fn screen_space(mut self, value: bool) -> Self {
200 self.screen_space = value;
201 self
202 }
203}
204
205impl Drawable for NineSliceSprite {
206 fn draw(&self, context: &mut DrawContext, graphics: &mut Graphics<Vertex>) {
207 let batch = GraphicsBatch {
208 shader: context.shader(self.shader.as_ref()),
209 uniforms: self
210 .uniforms
211 .iter()
212 .map(|(k, v)| (k.clone(), v.to_owned()))
213 .chain(std::iter::once((
214 "u_projection_view".into(),
215 GlowUniformValue::M4(
216 if self.screen_space {
217 graphics.main_camera.screen_matrix()
218 } else {
219 graphics.main_camera.world_matrix()
220 }
221 .into_col_array(),
222 ),
223 )))
224 .chain(self.textures.iter().enumerate().map(|(index, texture)| {
225 (texture.sampler.clone(), GlowUniformValue::I1(index as _))
226 }))
227 .collect(),
228 textures: self
229 .textures
230 .iter()
231 .filter_map(|texture| {
232 Some((context.texture(Some(&texture.texture))?, texture.filtering))
233 })
234 .collect(),
235 blending: self.blending.unwrap_or_else(|| context.top_blending()),
236 scissor: None,
237 };
238 let transform = Mat4::from(context.top_transform()) * Mat4::from(self.transform);
239 let size = self
240 .size
241 .or_else(|| {
242 batch
243 .textures
244 .first()
245 .map(|(texture, _)| Vec2::new(texture.width() as _, texture.height() as _))
246 })
247 .unwrap_or_default();
248 let offset = size * self.pivot;
249 let color = self.tint.into_array();
250 let margins_source = self.margins_source.clamp();
251 let margins_target = self.margins_target.fit_to_size(size);
252 let plf = 0.0;
253 let plc = margins_target.left;
254 let prc = size.x - margins_target.right;
255 let prf = size.x;
256 let ptf = 0.0;
257 let ptc = margins_target.top;
258 let pbc = size.y - margins_target.bottom;
259 let pbf = size.y;
260 let tlf = self.region.x;
261 let tlc = self.region.x + self.region.w * margins_source.left;
262 let trc = self.region.x + (1.0 - margins_source.right) * self.region.w;
263 let trf = self.region.x + self.region.w;
264 let ttf = self.region.y;
265 let ttc = self.region.y + self.region.h * margins_source.top;
266 let tbc = self.region.y + (1.0 - margins_source.bottom) * self.region.h;
267 let tbf = self.region.y + self.region.h;
268 graphics.stream.batch_optimized(batch);
269 graphics.stream.transformed(
270 |stream| unsafe {
271 stream.extend_triangles(
272 true,
273 [
274 Triangle { a: 0, b: 1, c: 5 },
275 Triangle { a: 5, b: 4, c: 0 },
276 Triangle { a: 1, b: 2, c: 6 },
277 Triangle { a: 6, b: 5, c: 1 },
278 Triangle { a: 2, b: 3, c: 7 },
279 Triangle { a: 7, b: 6, c: 2 },
280 Triangle { a: 4, b: 5, c: 9 },
281 Triangle { a: 9, b: 8, c: 4 },
282 ],
283 );
284 if !self.frame_only {
285 stream.extend_triangles(
286 true,
287 [
288 Triangle { a: 5, b: 6, c: 10 },
289 Triangle { a: 10, b: 9, c: 5 },
290 ],
291 );
292 }
293 stream.extend_triangles(
294 true,
295 [
296 Triangle { a: 6, b: 7, c: 11 },
297 Triangle { a: 11, b: 10, c: 6 },
298 Triangle { a: 8, b: 9, c: 13 },
299 Triangle { a: 13, b: 12, c: 8 },
300 Triangle { a: 9, b: 10, c: 14 },
301 Triangle { a: 14, b: 13, c: 9 },
302 Triangle {
303 a: 10,
304 b: 11,
305 c: 15,
306 },
307 Triangle {
308 a: 15,
309 b: 14,
310 c: 10,
311 },
312 ],
313 );
314 stream.extend_vertices([
315 Vertex {
316 position: [plf, ptf],
317 uv: [tlf, ttf, self.page],
318 color,
319 },
320 Vertex {
321 position: [plc, ptf],
322 uv: [tlc, ttf, self.page],
323 color,
324 },
325 Vertex {
326 position: [prc, ptf],
327 uv: [trc, ttf, self.page],
328 color,
329 },
330 Vertex {
331 position: [prf, ptf],
332 uv: [trf, ttf, self.page],
333 color,
334 },
335 Vertex {
336 position: [plf, ptc],
337 uv: [tlf, ttc, self.page],
338 color,
339 },
340 Vertex {
341 position: [plc, ptc],
342 uv: [tlc, ttc, self.page],
343 color,
344 },
345 Vertex {
346 position: [prc, ptc],
347 uv: [trc, ttc, self.page],
348 color,
349 },
350 Vertex {
351 position: [prf, ptc],
352 uv: [trf, ttc, self.page],
353 color,
354 },
355 Vertex {
356 position: [plf, pbc],
357 uv: [tlf, tbc, self.page],
358 color,
359 },
360 Vertex {
361 position: [plc, pbc],
362 uv: [tlc, tbc, self.page],
363 color,
364 },
365 Vertex {
366 position: [prc, pbc],
367 uv: [trc, tbc, self.page],
368 color,
369 },
370 Vertex {
371 position: [prf, pbc],
372 uv: [trf, tbc, self.page],
373 color,
374 },
375 Vertex {
376 position: [plf, pbf],
377 uv: [tlf, tbf, self.page],
378 color,
379 },
380 Vertex {
381 position: [plc, pbf],
382 uv: [tlc, tbf, self.page],
383 color,
384 },
385 Vertex {
386 position: [prc, pbf],
387 uv: [trc, tbf, self.page],
388 color,
389 },
390 Vertex {
391 position: [prf, pbf],
392 uv: [trf, tbf, self.page],
393 color,
394 },
395 ]);
396 },
397 |vertex| {
398 let point = transform.mul_point(Vec2::from(vertex.position) - offset);
399 vertex.position[0] = point.x;
400 vertex.position[1] = point.y;
401 },
402 );
403 }
404}