Init
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
1017
Cargo.lock
generated
Normal file
1017
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "nekosprite"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gif = "0.14.1"
|
||||||
|
image = "0.25.9"
|
||||||
190
src/main.rs
Normal file
190
src/main.rs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
use image::{Rgba, RgbaImage};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, BufReader, BufWriter, Write};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("=== Neko Sprite Recolor Tool ===\n");
|
||||||
|
|
||||||
|
// Get input path
|
||||||
|
println!("Enter input GIF path:");
|
||||||
|
let input_path = read_line();
|
||||||
|
|
||||||
|
// Get output path
|
||||||
|
println!("Enter output GIF path:");
|
||||||
|
let output_path = read_line();
|
||||||
|
|
||||||
|
// Get target color
|
||||||
|
println!("Enter target color (hex, e.g., FF5733):");
|
||||||
|
let color_input = read_line();
|
||||||
|
let target_color = parse_hex_color(&color_input).expect("Invalid hex color");
|
||||||
|
|
||||||
|
// Ask about texture
|
||||||
|
println!("Apply texture/noise? (y/n):");
|
||||||
|
let texture_input = read_line();
|
||||||
|
let apply_texture = texture_input.to_lowercase().starts_with('y');
|
||||||
|
|
||||||
|
println!("\nProcessing...");
|
||||||
|
|
||||||
|
match recolor_gif(&input_path, &output_path, target_color, apply_texture) {
|
||||||
|
Ok(_) => println!("✓ Successfully created: {}", output_path),
|
||||||
|
Err(e) => eprintln!("✗ Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_line() -> String {
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin()
|
||||||
|
.read_line(&mut input)
|
||||||
|
.expect("Failed to read line");
|
||||||
|
input.trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_hex_color(hex: &str) -> Result<Rgba<u8>, String> {
|
||||||
|
let hex = hex.trim_start_matches('#');
|
||||||
|
if hex.len() != 6 {
|
||||||
|
return Err("Hex color must be 6 characters".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid hex")?;
|
||||||
|
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid hex")?;
|
||||||
|
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid hex")?;
|
||||||
|
|
||||||
|
Ok(Rgba([r, g, b, 255]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recolor_gif(
|
||||||
|
input_path: &str,
|
||||||
|
output_path: &str,
|
||||||
|
target_color: Rgba<u8>,
|
||||||
|
apply_texture: bool,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Open and decode input GIF
|
||||||
|
let file = File::open(input_path)?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let mut decoder = gif::DecodeOptions::new();
|
||||||
|
decoder.set_color_output(gif::ColorOutput::RGBA);
|
||||||
|
let mut decoder = decoder.read_info(reader)?;
|
||||||
|
|
||||||
|
// Get GIF properties
|
||||||
|
let width = decoder.width();
|
||||||
|
let height = decoder.height();
|
||||||
|
|
||||||
|
// Prepare output encoder
|
||||||
|
let output_file = File::create(output_path)?;
|
||||||
|
let writer = BufWriter::new(output_file);
|
||||||
|
let mut encoder = gif::Encoder::new(writer, width, height, &[])?;
|
||||||
|
encoder.set_repeat(gif::Repeat::Infinite)?;
|
||||||
|
|
||||||
|
let mut frame_count = 0;
|
||||||
|
|
||||||
|
// Process each frame
|
||||||
|
while let Some(frame) = decoder.read_next_frame()? {
|
||||||
|
frame_count += 1;
|
||||||
|
print!("\rProcessing frame {}...", frame_count);
|
||||||
|
io::stdout().flush()?;
|
||||||
|
|
||||||
|
// Convert frame buffer to RgbaImage
|
||||||
|
let mut img = RgbaImage::from_raw(width as u32, height as u32, frame.buffer.to_vec())
|
||||||
|
.ok_or("Failed to create image from frame")?;
|
||||||
|
|
||||||
|
// Recolor the image
|
||||||
|
recolor_image(&mut img, target_color, apply_texture);
|
||||||
|
|
||||||
|
// Create output frame with same timing
|
||||||
|
let mut pixel_data = img.into_raw();
|
||||||
|
let mut output_frame = gif::Frame::from_rgba_speed(width, height, &mut pixel_data, 10);
|
||||||
|
output_frame.delay = frame.delay;
|
||||||
|
output_frame.dispose = frame.dispose;
|
||||||
|
|
||||||
|
encoder.write_frame(&output_frame)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n✓ Processed {} frames", frame_count);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recolor_image(img: &mut RgbaImage, target_color: Rgba<u8>, apply_texture: bool) {
|
||||||
|
// Generate color palette with variations if texture is enabled
|
||||||
|
let color_palette = if apply_texture {
|
||||||
|
generate_color_palette(target_color, 7)
|
||||||
|
} else {
|
||||||
|
vec![target_color]
|
||||||
|
};
|
||||||
|
|
||||||
|
for (x, y, pixel) in img.enumerate_pixels_mut() {
|
||||||
|
let Rgba([r, g, b, a]) = *pixel;
|
||||||
|
|
||||||
|
// Skip fully transparent pixels
|
||||||
|
if a == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate brightness (0-255)
|
||||||
|
let brightness = (r as u16 + g as u16 + b as u16) / 3;
|
||||||
|
|
||||||
|
// Determine pixel type based on brightness
|
||||||
|
if brightness < 64 {
|
||||||
|
// Very dark pixels (outlines) - keep original
|
||||||
|
continue;
|
||||||
|
} else if brightness > 200 {
|
||||||
|
// White/light pixels (body) - apply target color
|
||||||
|
let chosen_color = if apply_texture {
|
||||||
|
pick_color_from_palette(&color_palette, x, y)
|
||||||
|
} else {
|
||||||
|
target_color
|
||||||
|
};
|
||||||
|
*pixel = chosen_color;
|
||||||
|
} else {
|
||||||
|
// Gray pixels (anti-aliasing/shadows) - blend with target color
|
||||||
|
// Blend factor based on how gray it is (0.0 = keep original, 1.0 = full target)
|
||||||
|
let blend_factor = (brightness as f32 - 64.0) / (200.0 - 64.0);
|
||||||
|
let blend_factor = blend_factor.clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
let chosen_color = if apply_texture {
|
||||||
|
pick_color_from_palette(&color_palette, x, y)
|
||||||
|
} else {
|
||||||
|
target_color
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_r =
|
||||||
|
(r as f32 * (1.0 - blend_factor) + chosen_color[0] as f32 * blend_factor) as u8;
|
||||||
|
let new_g =
|
||||||
|
(g as f32 * (1.0 - blend_factor) + chosen_color[1] as f32 * blend_factor) as u8;
|
||||||
|
let new_b =
|
||||||
|
(b as f32 * (1.0 - blend_factor) + chosen_color[2] as f32 * blend_factor) as u8;
|
||||||
|
|
||||||
|
*pixel = Rgba([new_r, new_g, new_b, a]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_color_palette(base_color: Rgba<u8>, count: usize) -> Vec<Rgba<u8>> {
|
||||||
|
let mut palette = Vec::with_capacity(count);
|
||||||
|
let Rgba([r, g, b, a]) = base_color;
|
||||||
|
|
||||||
|
// Generate darker and brighter variations
|
||||||
|
let variation_range = 25; // How much to vary (+/- this value)
|
||||||
|
let step = (variation_range * 2) / (count - 1).max(1);
|
||||||
|
|
||||||
|
for i in 0..count {
|
||||||
|
let offset = -(variation_range as i32) + (i as i32 * step as i32);
|
||||||
|
|
||||||
|
let new_r = (r as i32 + offset).clamp(0, 255) as u8;
|
||||||
|
let new_g = (g as i32 + offset).clamp(0, 255) as u8;
|
||||||
|
let new_b = (b as i32 + offset).clamp(0, 255) as u8;
|
||||||
|
|
||||||
|
palette.push(Rgba([new_r, new_g, new_b, a]));
|
||||||
|
}
|
||||||
|
|
||||||
|
palette
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_color_from_palette(palette: &[Rgba<u8>], x: u32, y: u32) -> Rgba<u8> {
|
||||||
|
// Use a deterministic "random" selection based on pixel position
|
||||||
|
// This creates consistent noise pattern across frames
|
||||||
|
let hash = x
|
||||||
|
.wrapping_mul(374761393)
|
||||||
|
.wrapping_add(y.wrapping_mul(668265263));
|
||||||
|
let index = (hash % palette.len() as u32) as usize;
|
||||||
|
palette[index]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user