1use crate::Texture;
2use theframework::prelude::*;
3
4#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug)]
6pub enum AvatarDirection {
7 Front,
8 Back,
9 Left,
10 Right,
11}
12
13#[derive(Serialize, Deserialize, Clone, Debug)]
15pub struct AvatarAnimationFrame {
16 pub texture: Texture,
17 #[serde(default)]
18 pub weapon_main_anchor: Option<(i16, i16)>,
19 #[serde(default)]
20 pub weapon_off_anchor: Option<(i16, i16)>,
21}
22
23impl AvatarAnimationFrame {
24 pub fn new(texture: Texture) -> Self {
25 Self {
26 texture,
27 weapon_main_anchor: None,
28 weapon_off_anchor: None,
29 }
30 }
31}
32
33#[derive(Deserialize)]
34#[serde(untagged)]
35enum AvatarFrameSerde {
36 Texture(Texture),
37 Frame(AvatarAnimationFrame),
38}
39
40fn deserialize_avatar_frames<'de, D>(deserializer: D) -> Result<Vec<AvatarAnimationFrame>, D::Error>
41where
42 D: serde::Deserializer<'de>,
43{
44 let frames = Vec::<AvatarFrameSerde>::deserialize(deserializer)?;
45 Ok(frames
46 .into_iter()
47 .map(|f| match f {
48 AvatarFrameSerde::Texture(texture) => AvatarAnimationFrame::new(texture),
49 AvatarFrameSerde::Frame(frame) => frame,
50 })
51 .collect())
52}
53
54#[derive(Serialize, Deserialize, Clone, Debug)]
55pub struct AvatarPerspective {
56 pub direction: AvatarDirection,
57 #[serde(default, deserialize_with = "deserialize_avatar_frames")]
58 pub frames: Vec<AvatarAnimationFrame>,
59 #[serde(default)]
60 pub weapon_main_anchor: Option<(i16, i16)>,
61 #[serde(default)]
62 pub weapon_off_anchor: Option<(i16, i16)>,
63}
64
65impl Default for AvatarPerspective {
66 fn default() -> Self {
67 Self {
68 direction: AvatarDirection::Front,
69 frames: vec![],
70 weapon_main_anchor: None,
71 weapon_off_anchor: None,
72 }
73 }
74}
75
76#[derive(Serialize, Deserialize, Clone, Debug)]
78pub struct AvatarAnimation {
79 pub id: Uuid,
80 pub name: String,
81 #[serde(default = "AvatarAnimation::default_speed")]
83 pub speed: f32,
84 pub perspectives: Vec<AvatarPerspective>,
85}
86
87impl Default for AvatarAnimation {
88 fn default() -> Self {
89 Self {
90 id: Uuid::new_v4(),
91 name: "Unnamed".to_string(),
92 speed: 1.0,
93 perspectives: vec![],
94 }
95 }
96}
97
98impl AvatarAnimation {
99 fn default_speed() -> f32 {
100 1.0
101 }
102}
103
104#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug, Hash)]
106pub enum AvatarPerspectiveCount {
107 One,
109 Four,
111}
112
113impl Default for AvatarPerspectiveCount {
114 fn default() -> Self {
115 Self::One
116 }
117}
118
119#[derive(Serialize, Deserialize, Clone, Debug)]
121pub struct Avatar {
122 pub id: Uuid,
123 pub name: String,
124 pub resolution: u16,
125 pub perspective_count: AvatarPerspectiveCount,
126 pub animations: Vec<AvatarAnimation>,
127}
128
129impl Default for Avatar {
130 fn default() -> Self {
131 Self {
132 id: Uuid::new_v4(),
133 name: "Unnamed".to_string(),
134 resolution: 24,
135 perspective_count: AvatarPerspectiveCount::One,
136 animations: vec![],
137 }
138 }
139}
140
141impl Avatar {
142 pub fn set_resolution(&mut self, new_resolution: u16) {
144 if new_resolution == self.resolution || new_resolution == 0 {
145 return;
146 }
147 let size = new_resolution as usize;
148 for animation in &mut self.animations {
149 for perspective in &mut animation.perspectives {
150 for frame in &mut perspective.frames {
151 frame.texture = frame.texture.resized(size, size);
152 }
153 }
154 }
155 self.resolution = new_resolution;
156 }
157
158 pub fn set_perspective_count(&mut self, count: AvatarPerspectiveCount) {
160 if count == self.perspective_count {
161 return;
162 }
163
164 let size = self.resolution as usize;
165
166 let needed: &[AvatarDirection] = match count {
167 AvatarPerspectiveCount::One => &[AvatarDirection::Front],
168 AvatarPerspectiveCount::Four => &[
169 AvatarDirection::Front,
170 AvatarDirection::Back,
171 AvatarDirection::Left,
172 AvatarDirection::Right,
173 ],
174 };
175
176 for anim in &mut self.animations {
177 let frame_count = anim
179 .perspectives
180 .first()
181 .map(|p| p.frames.len())
182 .unwrap_or(1)
183 .max(1);
184
185 for dir in needed {
187 if !anim.perspectives.iter().any(|p| p.direction == *dir) {
188 let frames = (0..frame_count)
189 .map(|_| {
190 AvatarAnimationFrame::new(Texture::new(
191 vec![0; size * size * 4],
192 size,
193 size,
194 ))
195 })
196 .collect();
197 anim.perspectives.push(AvatarPerspective {
198 direction: *dir,
199 frames,
200 weapon_main_anchor: None,
201 weapon_off_anchor: None,
202 });
203 }
204 }
205
206 anim.perspectives.retain(|p| needed.contains(&p.direction));
208 }
209
210 self.perspective_count = count;
211 }
212
213 pub fn set_animation_frame_count(&mut self, animation_id: &Uuid, count: usize) {
216 let count = count.max(1);
217 let size = self.resolution as usize;
218
219 if let Some(anim) = self.animations.iter_mut().find(|a| a.id == *animation_id) {
220 let needed: &[AvatarDirection] = match self.perspective_count {
222 AvatarPerspectiveCount::One => &[AvatarDirection::Front],
223 AvatarPerspectiveCount::Four => &[
224 AvatarDirection::Front,
225 AvatarDirection::Back,
226 AvatarDirection::Left,
227 AvatarDirection::Right,
228 ],
229 };
230
231 for dir in needed {
233 if !anim.perspectives.iter().any(|p| p.direction == *dir) {
234 anim.perspectives.push(AvatarPerspective {
235 direction: *dir,
236 frames: vec![],
237 weapon_main_anchor: None,
238 weapon_off_anchor: None,
239 });
240 }
241 }
242
243 for perspective in &mut anim.perspectives {
245 let current = perspective.frames.len();
246 if count > current {
247 for _ in current..count {
248 perspective
249 .frames
250 .push(AvatarAnimationFrame::new(Texture::new(
251 vec![0; size * size * 4],
252 size,
253 size,
254 )));
255 }
256 } else if count < current {
257 perspective.frames.truncate(count);
258 }
259 }
260 }
261 }
262
263 pub fn get_animation_frame_count(&self, animation_id: &Uuid) -> usize {
265 self.animations
266 .iter()
267 .find(|a| a.id == *animation_id)
268 .and_then(|a| a.perspectives.first())
269 .map(|p| p.frames.len())
270 .unwrap_or(0)
271 }
272}
273
274#[derive(Clone, Copy, Debug)]
276pub struct AvatarMarkerColors {
277 pub skin_light: [u8; 4],
278 pub skin_dark: [u8; 4],
279 pub torso: [u8; 4],
280 pub arms: [u8; 4],
281 pub legs: [u8; 4],
282 pub hair: [u8; 4],
283 pub eyes: [u8; 4],
284 pub hands: [u8; 4],
285 pub feet: [u8; 4],
286}
287
288impl Default for AvatarMarkerColors {
289 fn default() -> Self {
290 Self {
291 skin_light: [255, 224, 189, 255],
292 skin_dark: [205, 133, 63, 255],
293 torso: [70, 90, 140, 255],
294 arms: [85, 105, 155, 255],
295 legs: [50, 60, 90, 255],
296 hair: [70, 50, 30, 255],
297 eyes: [30, 80, 120, 255],
298 hands: [255, 210, 170, 255],
299 feet: [80, 70, 60, 255],
300 }
301 }
302}
303
304#[derive(Clone, Copy, Debug, PartialEq, Eq)]
306pub struct AvatarShadingOptions {
307 pub enabled: bool,
309 pub skin_enabled: bool,
311}
312
313impl Default for AvatarShadingOptions {
314 fn default() -> Self {
315 Self {
316 enabled: true,
317 skin_enabled: false,
318 }
319 }
320}
321
322#[derive(Clone, Debug)]
324pub struct AvatarBuildOutput {
325 pub size: u32,
326 pub rgba: Vec<u8>,
327}
328
329pub struct AvatarBuildRequest<'a> {
331 pub avatar: &'a Avatar,
332 pub animation_name: Option<&'a str>,
333 pub direction: AvatarDirection,
334 pub frame_index: usize,
335 pub marker_colors: AvatarMarkerColors,
336 pub shading: AvatarShadingOptions,
337}
338
339pub struct AvatarBuilder;
342
343impl AvatarBuilder {
344 pub fn build_current_stub(req: AvatarBuildRequest<'_>) -> Option<AvatarBuildOutput> {
345 let anim = req
346 .animation_name
347 .and_then(|name| {
348 req.avatar
349 .animations
350 .iter()
351 .find(|a| a.name.eq_ignore_ascii_case(name))
352 })
353 .or_else(|| req.avatar.animations.first())?;
354
355 let persp = anim
356 .perspectives
357 .iter()
358 .find(|p| p.direction == req.direction)
359 .or_else(|| {
360 anim.perspectives
361 .iter()
362 .find(|p| p.direction == AvatarDirection::Front)
363 })
364 .or_else(|| anim.perspectives.first())?;
365
366 if persp.frames.is_empty() {
367 return None;
368 }
369
370 let frame = persp.frames.get(req.frame_index % persp.frames.len())?;
371
372 let target_size = frame.texture.width.max(frame.texture.height);
374 let mut rgba = if frame.texture.width == frame.texture.height {
375 frame.texture.data.clone()
376 } else {
377 frame.texture.resized(target_size, target_size).data
378 };
379
380 Self::recolor_markers(&mut rgba, req.marker_colors, req.shading, target_size);
381
382 Some(AvatarBuildOutput {
383 size: target_size as u32,
384 rgba,
385 })
386 }
387
388 fn recolor_markers(
389 rgba: &mut [u8],
390 colors: AvatarMarkerColors,
391 shading: AvatarShadingOptions,
392 size: usize,
393 ) {
394 const SKIN_LIGHT: [u8; 3] = [255, 0, 255];
395 const SKIN_DARK: [u8; 3] = [200, 0, 200];
396 const TORSO: [u8; 3] = [0, 0, 255];
397 const ARMS: [u8; 3] = [0, 120, 255];
398 const LEGS: [u8; 3] = [0, 255, 0];
399 const HAIR: [u8; 3] = [255, 255, 0];
400 const EYES: [u8; 3] = [0, 255, 255];
401 const HANDS: [u8; 3] = [255, 128, 0];
402 const FEET: [u8; 3] = [255, 80, 0];
403
404 let skin_light_ramp = Self::build_shade_ramp(colors.skin_light);
405 let skin_dark_ramp = Self::build_shade_ramp(colors.skin_dark);
406 let torso_ramp = Self::build_shade_ramp(colors.torso);
407 let arms_ramp = Self::build_shade_ramp(colors.arms);
408 let legs_ramp = Self::build_shade_ramp(colors.legs);
409 let hair_ramp = Self::build_shade_ramp(colors.hair);
410 let eyes_ramp = Self::build_shade_ramp(colors.eyes);
411 let hands_ramp = Self::build_shade_ramp(colors.hands);
412 let feet_ramp = Self::build_shade_ramp(colors.feet);
413
414 let mut min_y = [usize::MAX; 9];
416 let mut max_y = [0usize; 9];
417 for (i, px) in rgba.chunks_exact(4).enumerate() {
418 if px[3] == 0 || size == 0 {
419 continue;
420 }
421 let src = [px[0], px[1], px[2]];
422 let channel = if src == SKIN_LIGHT {
423 Some(0usize)
424 } else if src == SKIN_DARK {
425 Some(1usize)
426 } else if src == TORSO {
427 Some(2usize)
428 } else if src == ARMS {
429 Some(3usize)
430 } else if src == LEGS {
431 Some(4usize)
432 } else if src == HAIR {
433 Some(5usize)
434 } else if src == EYES {
435 Some(6usize)
436 } else if src == HANDS {
437 Some(7usize)
438 } else if src == FEET {
439 Some(8usize)
440 } else {
441 None
442 };
443 if let Some(channel) = channel {
444 let y = i / size;
445 min_y[channel] = min_y[channel].min(y);
446 max_y[channel] = max_y[channel].max(y);
447 }
448 }
449
450 for (i, px) in rgba.chunks_exact_mut(4).enumerate() {
451 if px[3] == 0 || size == 0 {
452 continue;
453 }
454 let x = i % size;
455 let y = i / size;
456 let src = [px[0], px[1], px[2]];
457 let (ramp, channel_seed) = if src == SKIN_LIGHT {
458 (&skin_light_ramp, 0u32)
459 } else if src == SKIN_DARK {
460 (&skin_dark_ramp, 1u32)
461 } else if src == TORSO {
462 (&torso_ramp, 2u32)
463 } else if src == ARMS {
464 (&arms_ramp, 3u32)
465 } else if src == LEGS {
466 (&legs_ramp, 4u32)
467 } else if src == HAIR {
468 (&hair_ramp, 5u32)
469 } else if src == EYES {
470 (&eyes_ramp, 6u32)
471 } else if src == HANDS {
472 (&hands_ramp, 7u32)
473 } else if src == FEET {
474 (&feet_ramp, 8u32)
475 } else {
476 continue;
477 };
478 let channel = channel_seed as usize;
479 let y0 = min_y[channel];
480 let y1 = max_y[channel];
481 let yf_local = if y0 == usize::MAX || y1 <= y0 {
482 0.5
483 } else {
484 (y.saturating_sub(y0)) as f32 / (y1 - y0) as f32
485 };
486 let is_skin = channel <= 1;
487 let use_ramp = shading.enabled && (shading.skin_enabled || !is_skin);
488 let shade_idx = if use_ramp {
489 Self::shade_index_for_pixel(x, y, yf_local, channel_seed)
490 } else {
491 1 };
493 px.copy_from_slice(&ramp[shade_idx]);
494 }
495 }
496
497 #[inline]
498 fn build_shade_ramp(base: [u8; 4]) -> [[u8; 4]; 4] {
499 [
501 Self::modulate_rgb(base, 1.18),
502 Self::modulate_rgb(base, 1.00),
503 Self::modulate_rgb(base, 0.82),
504 Self::modulate_rgb(base, 0.64),
505 ]
506 }
507
508 #[inline]
509 fn modulate_rgb(base: [u8; 4], factor: f32) -> [u8; 4] {
510 let r = (base[0] as f32 * factor).clamp(0.0, 255.0) as u8;
511 let g = (base[1] as f32 * factor).clamp(0.0, 255.0) as u8;
512 let b = (base[2] as f32 * factor).clamp(0.0, 255.0) as u8;
513 [r, g, b, base[3]]
514 }
515
516 #[inline]
517 fn shade_index_for_pixel(x: usize, y: usize, yf_local: f32, channel_seed: u32) -> usize {
518 const BAYER4: [f32; 16] = [
520 0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0,
521 ];
522 let d = BAYER4[(y & 3) * 4 + (x & 3)] / 15.0; let yf = yf_local.clamp(0.0, 1.0); let channel_bias = (channel_seed % 3) as f32 * 0.03;
525 let t = (yf * 2.7 + d * 0.6 + channel_bias).clamp(0.0, 3.0);
526 t as usize
527 }
528}