correct sprite cropping
This commit is contained in:
@@ -2,24 +2,67 @@ use crate::models::dolls::DollDto;
|
|||||||
use crate::services::sprite_recolor;
|
use crate::services::sprite_recolor;
|
||||||
use image::{imageops::FilterType, RgbaImage};
|
use image::{imageops::FilterType, RgbaImage};
|
||||||
use petpet::{encode_gif, generate};
|
use petpet::{encode_gif, generate};
|
||||||
|
use rand::prelude::IndexedRandom;
|
||||||
|
|
||||||
|
const SPRITE_SIZE: u32 = 32;
|
||||||
|
const SPRITE_FRAMES: &[(u32, u32)] = &[
|
||||||
|
(160, 125),
|
||||||
|
(32, 125),
|
||||||
|
(192, 0),
|
||||||
|
(224, 0),
|
||||||
|
(32, 0),
|
||||||
|
(0, 0),
|
||||||
|
(0, 127),
|
||||||
|
(32, 127),
|
||||||
|
(224, 126),
|
||||||
|
(224, 96),
|
||||||
|
(224, 64),
|
||||||
|
(160, 0),
|
||||||
|
(160, 127),
|
||||||
|
(160, 126),
|
||||||
|
(224, 0),
|
||||||
|
(224, 127),
|
||||||
|
(32, 126),
|
||||||
|
(32, 125),
|
||||||
|
(0, 126),
|
||||||
|
(0, 125),
|
||||||
|
(160, 0),
|
||||||
|
(160, 127),
|
||||||
|
(192, 127),
|
||||||
|
(192, 126),
|
||||||
|
(224, 125),
|
||||||
|
(32, 126),
|
||||||
|
(192, 125),
|
||||||
|
(224, 127),
|
||||||
|
(160, 126),
|
||||||
|
(160, 125),
|
||||||
|
(224, 0),
|
||||||
|
(224, 127),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn crop_random_sprite(img: &RgbaImage) -> RgbaImage {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let &(coord_x, coord_y) = SPRITE_FRAMES
|
||||||
|
.choose(&mut rng)
|
||||||
|
.expect("SPRITE_FRAMES must not be empty");
|
||||||
|
image::imageops::crop_imm(img, coord_x, coord_y, SPRITE_SIZE, SPRITE_SIZE).to_image()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn encode_pet_doll_gif_base64(doll: DollDto) -> Result<String, String> {
|
pub fn encode_pet_doll_gif_base64(doll: DollDto) -> Result<String, String> {
|
||||||
let body_color = &doll.configuration.color_scheme.body;
|
let body_color = &doll.configuration.color_scheme.body;
|
||||||
let outline_color = &doll.configuration.color_scheme.outline;
|
let outline_color = &doll.configuration.color_scheme.outline;
|
||||||
|
|
||||||
// Get recolored image
|
|
||||||
let img: RgbaImage = sprite_recolor::get_recolored_image(body_color, outline_color)
|
let img: RgbaImage = sprite_recolor::get_recolored_image(body_color, outline_color)
|
||||||
.map_err(|e| format!("Failed to recolor image: {}", e))?;
|
.map_err(|e| format!("Failed to recolor image: {}", e))?;
|
||||||
|
|
||||||
// Generate petpet frames
|
let random_sprite = crop_random_sprite(&img);
|
||||||
let frames = generate(img, FilterType::Lanczos3, None)
|
|
||||||
|
let frames = generate(random_sprite, FilterType::Lanczos3, None)
|
||||||
.map_err(|e| format!("Failed to generate petpet frames: {}", e))?;
|
.map_err(|e| format!("Failed to generate petpet frames: {}", e))?;
|
||||||
|
|
||||||
// Encode to GIF
|
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
encode_gif(frames, &mut output, 10).map_err(|e| format!("Failed to encode GIF: {}", e))?;
|
encode_gif(frames, &mut output, 10).map_err(|e| format!("Failed to encode GIF: {}", e))?;
|
||||||
|
|
||||||
// Base64
|
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
Ok(general_purpose::STANDARD.encode(&output))
|
Ok(general_purpose::STANDARD.encode(&output))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
let showFullscreenModal = $state(false);
|
let showFullscreenModal = $state(false);
|
||||||
let fullscreenImageSrc = $state("");
|
let fullscreenImageSrc = $state("");
|
||||||
let headpatSenderId = $state<string | null>(null);
|
let headpatSenderId = $state<string | null>(null);
|
||||||
|
let headpatTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
// Queue for pending headpats (when modal is already showing)
|
// Queue for pending headpats (when modal is already showing)
|
||||||
let headpatQueue = $state<Array<{ userId: string; content: string }>>([]);
|
let headpatQueue = $state<Array<{ userId: string; content: string }>>([]);
|
||||||
@@ -41,12 +42,23 @@
|
|||||||
fullscreenImageSrc = `data:image/gif;base64,${next.content}`;
|
fullscreenImageSrc = `data:image/gif;base64,${next.content}`;
|
||||||
headpatSenderId = next.userId;
|
headpatSenderId = next.userId;
|
||||||
showFullscreenModal = true;
|
showFullscreenModal = true;
|
||||||
|
scheduleHeadpatDismiss();
|
||||||
} else {
|
} else {
|
||||||
fullscreenImageSrc = "";
|
fullscreenImageSrc = "";
|
||||||
headpatSenderId = null;
|
headpatSenderId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleHeadpatDismiss() {
|
||||||
|
if (headpatTimer) {
|
||||||
|
clearTimeout(headpatTimer);
|
||||||
|
}
|
||||||
|
headpatTimer = setTimeout(() => {
|
||||||
|
showFullscreenModal = false;
|
||||||
|
headpatTimer = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
function getHeadpatSenderName(userId: string | null): string {
|
function getHeadpatSenderName(userId: string | null): string {
|
||||||
if (!userId) return "";
|
if (!userId) return "";
|
||||||
const friend = getFriendById(userId);
|
const friend = getFriendById(userId);
|
||||||
@@ -57,32 +69,44 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
for (const [userId, interaction] of $receivedInteractions) {
|
for (const [userId, interaction] of $receivedInteractions) {
|
||||||
if (interaction.type === INTERACTION_TYPE_HEADPAT) {
|
if (interaction.type === INTERACTION_TYPE_HEADPAT) {
|
||||||
// Deduplicate: replace existing headpat from same user instead of queueing
|
if (showFullscreenModal) {
|
||||||
const existingIndex = headpatQueue.findIndex((h) => h.userId === userId);
|
// Queue the headpat for later (deduplicate by replacing existing from same user)
|
||||||
if (existingIndex >= 0) {
|
const existingIndex = headpatQueue.findIndex((h) => h.userId === userId);
|
||||||
headpatQueue[existingIndex] = { userId, content: interaction.content };
|
if (existingIndex >= 0) {
|
||||||
} else if (showFullscreenModal) {
|
headpatQueue[existingIndex] = { userId, content: interaction.content };
|
||||||
headpatQueue.push({ userId, content: interaction.content });
|
} else {
|
||||||
|
headpatQueue.push({ userId, content: interaction.content });
|
||||||
|
}
|
||||||
|
scheduleHeadpatDismiss();
|
||||||
} else {
|
} else {
|
||||||
|
// Show immediately and clear from store
|
||||||
clearInteraction(userId);
|
clearInteraction(userId);
|
||||||
fullscreenImageSrc = `data:image/gif;base64,${interaction.content}`;
|
fullscreenImageSrc = `data:image/gif;base64,${interaction.content}`;
|
||||||
headpatSenderId = userId;
|
headpatSenderId = userId;
|
||||||
showFullscreenModal = true;
|
showFullscreenModal = true;
|
||||||
|
scheduleHeadpatDismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear headpat interaction when modal closes, then process next in queue
|
// When modal closes, process next headpat in queue
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!showFullscreenModal && headpatSenderId) {
|
if (!showFullscreenModal && headpatSenderId) {
|
||||||
clearInteraction(headpatSenderId);
|
|
||||||
headpatSenderId = null;
|
headpatSenderId = null;
|
||||||
// Process next headpat in queue
|
|
||||||
processNextHeadpat();
|
processNextHeadpat();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
return () => {
|
||||||
|
if (headpatTimer) {
|
||||||
|
clearTimeout(headpatTimer);
|
||||||
|
headpatTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function getFriendById(userId: string) {
|
function getFriendById(userId: string) {
|
||||||
const friend = $appData?.friends?.find((f) => f.friend?.id === userId);
|
const friend = $appData?.friends?.find((f) => f.friend?.id === userId);
|
||||||
return friend?.friend;
|
return friend?.friend;
|
||||||
|
|||||||
@@ -20,22 +20,6 @@
|
|||||||
senderName = "",
|
senderName = "",
|
||||||
}: { imageSrc: string; visible: boolean; senderName?: string } = $props();
|
}: { imageSrc: string; visible: boolean; senderName?: string } = $props();
|
||||||
|
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (visible) {
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
visible = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (timer) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if visible}
|
{#if visible}
|
||||||
|
|||||||
Reference in New Issue
Block a user