use crate::blit::{Blitter, PixelFmt, Rect, Surface};
pub trait EffectSink {
fn target_width(&self) -> u32;
fn target_height(&self) -> u32;
fn target_format(&self) -> PixelFmt;
fn fill(&mut self, rect: Rect, color: u32);
fn blit(&mut self, src: &Surface<'_>, src_area: Rect, dst_pos: (i32, i32));
fn blend_a8_row(&mut self, alphas: &[u8], dst_pos: (i32, i32), fg_color: u32);
}
pub struct SubSink<'a> {
inner: &'a mut dyn EffectSink,
offset: (i32, i32),
width: u32,
height: u32,
}
impl<'a> SubSink<'a> {
#[inline]
pub fn new(inner: &'a mut dyn EffectSink, offset: (i32, i32), width: u32, height: u32) -> Self {
Self {
inner,
offset,
width,
height,
}
}
}
impl<'a> EffectSink for SubSink<'a> {
fn target_width(&self) -> u32 {
self.width
}
fn target_height(&self) -> u32 {
self.height
}
fn target_format(&self) -> PixelFmt {
self.inner.target_format()
}
fn fill(&mut self, rect: Rect, color: u32) {
let rect = Rect {
x: rect.x + self.offset.0,
y: rect.y + self.offset.1,
w: rect.w,
h: rect.h,
};
self.inner.fill(rect, color);
}
fn blit(&mut self, src: &Surface<'_>, src_area: Rect, dst_pos: (i32, i32)) {
self.inner.blit(
src,
src_area,
(dst_pos.0 + self.offset.0, dst_pos.1 + self.offset.1),
);
}
fn blend_a8_row(&mut self, alphas: &[u8], dst_pos: (i32, i32), fg_color: u32) {
self.inner.blend_a8_row(
alphas,
(dst_pos.0 + self.offset.0, dst_pos.1 + self.offset.1),
fg_color,
);
}
}
pub trait Effect {
fn is_active(&self) -> bool;
fn activate(&mut self);
fn deactivate(&mut self);
fn tick(&mut self);
fn draw(&mut self, sink: &mut dyn EffectSink);
}
pub trait EffectExt: Effect {
fn paint<B: Blitter>(&mut self, blitter: &mut B, dst: &mut Surface<'_>) {
let mut sink = BlitterSink::new(blitter, dst);
self.draw(&mut sink);
}
}
impl<T: Effect + ?Sized> EffectExt for T {}
pub struct BlitterSink<'a, 'b, B: Blitter> {
blitter: &'a mut B,
dst: &'a mut Surface<'b>,
}
impl<'a, 'b, B: Blitter> BlitterSink<'a, 'b, B> {
#[inline]
pub fn new(blitter: &'a mut B, dst: &'a mut Surface<'b>) -> Self {
Self { blitter, dst }
}
}
impl<'a, 'b, B: Blitter> EffectSink for BlitterSink<'a, 'b, B> {
fn target_width(&self) -> u32 {
self.dst.width
}
fn target_height(&self) -> u32 {
self.dst.height
}
fn target_format(&self) -> PixelFmt {
self.dst.format
}
fn fill(&mut self, rect: Rect, color: u32) {
self.blitter.fill(self.dst, rect, color);
}
fn blit(&mut self, src: &Surface<'_>, src_area: Rect, dst_pos: (i32, i32)) {
self.blitter.blit(src, src_area, self.dst, dst_pos);
}
fn blend_a8_row(&mut self, alphas: &[u8], dst_pos: (i32, i32), fg_color: u32) {
blend_a8_row_inline(self.dst, alphas, dst_pos, fg_color);
}
}
pub fn blend_a8_row_inline(
dst: &mut Surface<'_>,
alphas: &[u8],
dst_pos: (i32, i32),
fg_color: u32,
) {
if dst.format != PixelFmt::Argb8888 {
return;
}
let (dx, dy) = dst_pos;
if dy < 0 || (dy as u32) >= dst.height {
return;
}
let dst_stride = dst.stride;
let dst_w = dst.width as i32;
let row_start = (dy as usize) * dst_stride;
let fg_r = (fg_color >> 16) & 0xFF;
let fg_g = (fg_color >> 8) & 0xFF;
let fg_b = fg_color & 0xFF;
for (i, &alpha_byte) in alphas.iter().enumerate() {
let alpha = alpha_byte as u32;
if alpha == 0 {
continue;
}
let x = dx + i as i32;
if x < 0 || x >= dst_w {
continue;
}
let dst_off = row_start + (x as usize) * 4;
if dst_off + 4 > dst.buf.len() {
break;
}
let bg_b = dst.buf[dst_off] as u32;
let bg_g = dst.buf[dst_off + 1] as u32;
let bg_r = dst.buf[dst_off + 2] as u32;
let inv = 255 - alpha;
dst.buf[dst_off] = ((fg_b * alpha + bg_b * inv) / 255) as u8;
dst.buf[dst_off + 1] = ((fg_g * alpha + bg_g * inv) / 255) as u8;
dst.buf[dst_off + 2] = ((fg_r * alpha + bg_r * inv) / 255) as u8;
dst.buf[dst_off + 3] = 0xFF;
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct CrawlParams {
pub viewport_w: u32,
pub viewport_h: u32,
pub frame_hz: u32,
pub pixels_per_sec: u32,
pub line_spacing_px: u16,
pub text_color_argb: u32,
pub background_scroll_divisor: u16,
pub perspective_top_width: u16,
pub perspective_bottom_width: u16,
}
impl CrawlParams {
pub const fn star_crawl_disco(viewport_w: u32, viewport_h: u32, frame_hz: u32) -> Self {
Self {
viewport_w,
viewport_h,
frame_hz,
pixels_per_sec: 40,
line_spacing_px: 36,
text_color_argb: 0xFFFF_D700,
background_scroll_divisor: 3,
perspective_top_width: 360,
perspective_bottom_width: 600,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn star_crawl_disco_matches_hardware_baseline() {
let p = CrawlParams::star_crawl_disco(720, 480, 57);
assert_eq!(p.viewport_w, 720);
assert_eq!(p.viewport_h, 480);
assert_eq!(p.frame_hz, 57);
assert_eq!(p.pixels_per_sec, 40);
assert_eq!(p.line_spacing_px, 36);
assert_eq!(p.text_color_argb, 0xFFFF_D700);
assert_eq!(p.background_scroll_divisor, 3);
assert_eq!(p.perspective_top_width, 360);
assert_eq!(p.perspective_bottom_width, 600);
}
#[test]
fn crawl_params_is_copy() {
let a = CrawlParams::star_crawl_disco(720, 480, 57);
let b = a;
assert_eq!(a, b);
}
}