petpet headpat init

This commit is contained in:
2026-02-20 02:23:01 +08:00
parent 62a1c1b672
commit ea8f41f852
12 changed files with 162 additions and 78 deletions

View File

@@ -1,5 +1,5 @@
use serde_json::json;
use tracing::{error, info};
use tracing::error;
use crate::{
lock_r, models::interaction::SendInteractionDto, services::ws::WS_EVENT, state::FDOLL,
@@ -25,23 +25,12 @@ pub async fn send_interaction(dto: SendInteractionDto) -> Result<(), String> {
// The DTO structure matches what the server expects:
// { recipientUserId, content, type } (handled by serde rename_all="camelCase")
// Note: The `type` field in DTO is mapped to `type_` in Rust struct but serialized as `type`
// due to camelCase renaming (if we rely on TS-RS output) or manual renaming.
// Wait, `type` is a reserved keyword in Rust so we used `type_`.
// We need to ensure serialization maps `type_` to `type`.
// However, serde's `rename_all = "camelCase"` handles `recipient_user_id` -> `recipientUserId`.
// It does NOT automatically handle `type_` -> `type`.
// We should add `#[serde(rename = "type")]` to the `type_` field in the model.
// I will fix the model first to ensure correct serialization.
let payload = json!({
"recipientUserId": dto.recipient_user_id,
"content": dto.content,
"type": dto.type_
});
info!("Sending interaction via WS: {:?}", payload);
// Blocking emission because rust_socketio::Client::emit is synchronous/blocking
// but we are in an async context. Ideally we spawn_blocking.
let spawn_result = tauri::async_runtime::spawn_blocking(move || {

View File

@@ -13,6 +13,7 @@ pub mod interaction;
pub mod presence_modules;
pub mod scene;
pub mod sprite_recolor;
pub mod petpet;
pub mod welcome;
pub mod ws;

View File

@@ -0,0 +1,25 @@
use crate::models::dolls::DollDto;
use crate::services::sprite_recolor;
use image::{imageops::FilterType, RgbaImage};
use petpet::{encode_gif, generate};
pub fn encode_pet_doll_gif_base64(doll: DollDto) -> Result<String, String> {
let body_color = &doll.configuration.color_scheme.body;
let outline_color = &doll.configuration.color_scheme.outline;
// Get recolored image
let img: RgbaImage = sprite_recolor::get_recolored_image(body_color, outline_color)
.map_err(|e| format!("Failed to recolor image: {}", e))?;
// Generate petpet frames
let frames = generate(img, FilterType::Lanczos3, None)
.map_err(|e| format!("Failed to generate petpet frames: {}", e))?;
// Encode to GIF
let mut output = Vec::new();
encode_gif(frames, &mut output, 10).map_err(|e| format!("Failed to encode GIF: {}", e))?;
// Base64
use base64::{engine::general_purpose, Engine as _};
Ok(general_purpose::STANDARD.encode(&output))
}

View File

@@ -226,3 +226,30 @@ fn pick_color_from_palette(palette: &[Rgba<u8>], x: u32, y: u32) -> Rgba<u8> {
// SAFETY: index is guaranteed to be < palette.len() due to modulo operation
unsafe { *palette.get_unchecked(index) }
}
pub fn get_recolored_image(
white_color_hex: &str,
black_color_hex: &str,
) -> Result<RgbaImage, Box<dyn std::error::Error>> {
let white_color = parse_hex_color(white_color_hex)?;
let black_color = parse_hex_color(black_color_hex)?;
// Get pre-decoded GIF data
let decoded_gif = get_decoded_gif();
// Take first frame
let frame = &decoded_gif.frames[0];
let mut img = RgbaImage::from_raw(
decoded_gif.width as u32,
decoded_gif.height as u32,
frame.pixels.clone(),
)
.ok_or("Failed to create image from frame")?;
// Recolor the image
let white_palette = generate_color_palette(white_color, 7);
let black_palette = generate_color_palette(black_color, 7);
recolor_image(&mut img, &white_palette, &black_palette, true);
Ok(img)
}