use crate::color::Color;
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2_core_foundation::{CFRetained, CGPoint, CGRect, CGSize};
use objc2_core_graphics::{
CGBitmapContextCreate, CGBitmapContextCreateImage, CGColor, CGColorSpace, CGContext, CGImage,
CGImageAlphaInfo,
};
use objc2_foundation::NSArray;
use objc2_quartz_core::{
kCAEmitterLayerAdditive, kCAEmitterLayerBackToFront, kCAEmitterLayerCircle,
kCAEmitterLayerCuboid, kCAEmitterLayerLine, kCAEmitterLayerOldestFirst,
kCAEmitterLayerOldestLast, kCAEmitterLayerOutline, kCAEmitterLayerPoint, kCAEmitterLayerPoints,
kCAEmitterLayerRectangle, kCAEmitterLayerSphere, kCAEmitterLayerSurface,
kCAEmitterLayerUnordered, kCAEmitterLayerVolume, CAEmitterCell, CAEmitterLayer,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum EmitterShape {
#[default]
Point,
Line,
Rectangle,
Circle,
Cuboid,
Sphere,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum EmitterMode {
#[default]
Points,
Outline,
Surface,
Volume,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum RenderMode {
#[default]
Unordered,
OldestFirst,
OldestLast,
BackToFront,
Additive,
}
#[derive(Clone, Debug)]
pub enum ParticleImage {
SoftGlow(u32),
Circle(u32),
Star { size: u32, points: u32 },
Spark(u32),
}
impl ParticleImage {
pub fn soft_glow(size: u32) -> Self {
Self::SoftGlow(size)
}
pub fn circle(size: u32) -> Self {
Self::Circle(size)
}
pub fn star(size: u32, points: u32) -> Self {
Self::Star { size, points }
}
pub fn spark(size: u32) -> Self {
Self::Spark(size)
}
pub fn to_cgimage(&self) -> CFRetained<CGImage> {
match self {
Self::SoftGlow(size) => create_soft_glow_image(*size as usize),
Self::Circle(size) => create_circle_image(*size as usize),
Self::Star { size, points } => create_star_image(*size as usize, *points as usize),
Self::Spark(size) => create_spark_image(*size as usize),
}
}
}
pub struct CAEmitterCellBuilder {
birth_rate: f32,
lifetime: f32,
lifetime_range: f32,
velocity: f64,
velocity_range: f64,
emission_longitude: f64,
emission_range: f64,
scale: f64,
scale_range: f64,
scale_speed: f64,
alpha_speed: f32,
spin: f64,
spin_range: f64,
acceleration: (f64, f64),
color: Option<Color>,
image: Option<ParticleImage>,
}
impl CAEmitterCellBuilder {
pub fn new() -> Self {
Self {
birth_rate: 1.0,
lifetime: 1.0,
lifetime_range: 0.0,
velocity: 0.0,
velocity_range: 0.0,
emission_longitude: 0.0,
emission_range: 0.0,
scale: 1.0,
scale_range: 0.0,
scale_speed: 0.0,
alpha_speed: 0.0,
spin: 0.0,
spin_range: 0.0,
acceleration: (0.0, 0.0),
color: None,
image: None,
}
}
pub fn birth_rate(mut self, rate: f32) -> Self {
self.birth_rate = rate;
self
}
pub fn lifetime(mut self, seconds: f32) -> Self {
self.lifetime = seconds;
self
}
pub fn lifetime_range(mut self, range: f32) -> Self {
self.lifetime_range = range;
self
}
pub fn velocity(mut self, v: f64) -> Self {
self.velocity = v;
self
}
pub fn velocity_range(mut self, range: f64) -> Self {
self.velocity_range = range;
self
}
pub fn emission_longitude(mut self, radians: f64) -> Self {
self.emission_longitude = radians;
self
}
pub fn emission_toward(mut self, from: (f64, f64), target: (f64, f64)) -> Self {
let dx = target.0 - from.0;
let dy = target.1 - from.1;
self.emission_longitude = dy.atan2(dx);
self
}
pub fn emission_range(mut self, radians: f64) -> Self {
self.emission_range = radians;
self
}
pub fn scale(mut self, s: f64) -> Self {
self.scale = s;
self
}
pub fn scale_range(mut self, range: f64) -> Self {
self.scale_range = range;
self
}
pub fn scale_speed(mut self, speed: f64) -> Self {
self.scale_speed = speed;
self
}
pub fn alpha_speed(mut self, speed: f32) -> Self {
self.alpha_speed = speed;
self
}
pub fn spin(mut self, radians_per_sec: f64) -> Self {
self.spin = radians_per_sec;
self
}
pub fn spin_range(mut self, range: f64) -> Self {
self.spin_range = range;
self
}
pub fn acceleration(mut self, x: f64, y: f64) -> Self {
self.acceleration = (x, y);
self
}
pub fn color(mut self, color: impl Into<Color>) -> Self {
self.color = Some(color.into());
self
}
pub fn color_rgb(mut self, r: f64, g: f64, b: f64) -> Self {
self.color = Some(Color::rgb(r, g, b));
self
}
pub fn color_rgba(mut self, r: f64, g: f64, b: f64, a: f64) -> Self {
self.color = Some(Color::rgba(r, g, b, a));
self
}
pub fn image(mut self, img: ParticleImage) -> Self {
self.image = Some(img);
self
}
pub fn build(self) -> Retained<CAEmitterCell> {
let cell = CAEmitterCell::new();
cell.setBirthRate(self.birth_rate);
cell.setLifetime(self.lifetime);
cell.setLifetimeRange(self.lifetime_range);
cell.setVelocity(self.velocity);
cell.setVelocityRange(self.velocity_range);
cell.setEmissionLongitude(self.emission_longitude);
cell.setEmissionRange(self.emission_range);
cell.setScale(self.scale);
cell.setScaleRange(self.scale_range);
cell.setScaleSpeed(self.scale_speed);
cell.setAlphaSpeed(self.alpha_speed);
cell.setSpin(self.spin);
cell.setSpinRange(self.spin_range);
cell.setXAcceleration(self.acceleration.0);
cell.setYAcceleration(self.acceleration.1);
if let Some(color) = self.color {
let cgcolor: CFRetained<CGColor> = color.into();
cell.setColor(Some(&cgcolor));
}
if let Some(img) = self.image {
let cgimage = img.to_cgimage();
unsafe {
let image_obj: &AnyObject = std::mem::transmute(&*cgimage);
cell.setContents(Some(image_obj));
}
}
cell
}
}
impl Default for CAEmitterCellBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct CAEmitterLayerBuilder {
position: (f64, f64),
size: (f64, f64),
shape: EmitterShape,
mode: EmitterMode,
render_mode: RenderMode,
birth_rate: f32,
cells: Vec<Retained<CAEmitterCell>>,
}
impl CAEmitterLayerBuilder {
pub fn new() -> Self {
Self {
position: (0.0, 0.0),
size: (0.0, 0.0),
shape: EmitterShape::Point,
mode: EmitterMode::Points,
render_mode: RenderMode::Unordered,
birth_rate: 1.0,
cells: Vec::new(),
}
}
pub fn position(mut self, x: f64, y: f64) -> Self {
self.position = (x, y);
self
}
pub fn size(mut self, width: f64, height: f64) -> Self {
self.size = (width, height);
self
}
pub fn shape(mut self, shape: EmitterShape) -> Self {
self.shape = shape;
self
}
pub fn mode(mut self, mode: EmitterMode) -> Self {
self.mode = mode;
self
}
pub fn render_mode(mut self, mode: RenderMode) -> Self {
self.render_mode = mode;
self
}
pub fn birth_rate(mut self, rate: f32) -> Self {
self.birth_rate = rate;
self
}
pub fn particle<F>(mut self, configure: F) -> Self
where
F: FnOnce(CAEmitterCellBuilder) -> CAEmitterCellBuilder,
{
let builder = CAEmitterCellBuilder::new();
let configured = configure(builder);
self.cells.push(configured.build());
self
}
pub fn cell(mut self, cell: Retained<CAEmitterCell>) -> Self {
self.cells.push(cell);
self
}
pub fn build(self) -> Retained<CAEmitterLayer> {
let emitter = CAEmitterLayer::new();
emitter.setEmitterPosition(CGPoint::new(self.position.0, self.position.1));
emitter.setEmitterSize(CGSize::new(self.size.0, self.size.1));
emitter.setBirthRate(self.birth_rate);
unsafe {
let shape = match self.shape {
EmitterShape::Point => kCAEmitterLayerPoint,
EmitterShape::Line => kCAEmitterLayerLine,
EmitterShape::Rectangle => kCAEmitterLayerRectangle,
EmitterShape::Circle => kCAEmitterLayerCircle,
EmitterShape::Cuboid => kCAEmitterLayerCuboid,
EmitterShape::Sphere => kCAEmitterLayerSphere,
};
emitter.setEmitterShape(shape);
}
unsafe {
let mode = match self.mode {
EmitterMode::Points => kCAEmitterLayerPoints,
EmitterMode::Outline => kCAEmitterLayerOutline,
EmitterMode::Surface => kCAEmitterLayerSurface,
EmitterMode::Volume => kCAEmitterLayerVolume,
};
emitter.setEmitterMode(mode);
}
unsafe {
let render = match self.render_mode {
RenderMode::Unordered => kCAEmitterLayerUnordered,
RenderMode::OldestFirst => kCAEmitterLayerOldestFirst,
RenderMode::OldestLast => kCAEmitterLayerOldestLast,
RenderMode::BackToFront => kCAEmitterLayerBackToFront,
RenderMode::Additive => kCAEmitterLayerAdditive,
};
emitter.setRenderMode(render);
}
if !self.cells.is_empty() {
let cells = NSArray::from_retained_slice(&self.cells);
emitter.setEmitterCells(Some(&cells));
}
emitter
}
}
impl Default for CAEmitterLayerBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct PointBurstBuilder {
position: (f64, f64),
birth_rate: f32,
lifetime: f32,
lifetime_range: f32,
velocity: f64,
velocity_range: f64,
scale: f64,
scale_range: f64,
scale_speed: f64,
alpha_speed: f32,
color: Option<Color>,
image: Option<ParticleImage>,
render_mode: RenderMode,
}
impl PointBurstBuilder {
pub fn new(x: f64, y: f64) -> Self {
Self {
position: (x, y),
birth_rate: 100.0,
lifetime: 5.0,
lifetime_range: 1.0,
velocity: 100.0,
velocity_range: 20.0,
scale: 0.1,
scale_range: 0.02,
scale_speed: 0.0,
alpha_speed: 0.0,
color: None,
image: None,
render_mode: RenderMode::Additive,
}
}
pub fn birth_rate(mut self, rate: f32) -> Self {
self.birth_rate = rate;
self
}
pub fn lifetime(mut self, seconds: f32) -> Self {
self.lifetime = seconds;
self
}
pub fn lifetime_range(mut self, range: f32) -> Self {
self.lifetime_range = range;
self
}
pub fn velocity(mut self, v: f64) -> Self {
self.velocity = v;
self
}
pub fn velocity_range(mut self, range: f64) -> Self {
self.velocity_range = range;
self
}
pub fn scale(mut self, s: f64) -> Self {
self.scale = s;
self
}
pub fn scale_range(mut self, range: f64) -> Self {
self.scale_range = range;
self
}
pub fn scale_speed(mut self, speed: f64) -> Self {
self.scale_speed = speed;
self
}
pub fn alpha_speed(mut self, speed: f32) -> Self {
self.alpha_speed = speed;
self
}
pub fn color(mut self, color: impl Into<Color>) -> Self {
self.color = Some(color.into());
self
}
pub fn color_rgb(mut self, r: f64, g: f64, b: f64) -> Self {
self.color = Some(Color::rgb(r, g, b));
self
}
pub fn color_rgba(mut self, r: f64, g: f64, b: f64, a: f64) -> Self {
self.color = Some(Color::rgba(r, g, b, a));
self
}
pub fn image(mut self, img: ParticleImage) -> Self {
self.image = Some(img);
self
}
pub fn render_mode(mut self, mode: RenderMode) -> Self {
self.render_mode = mode;
self
}
pub fn build(self) -> Retained<CAEmitterLayer> {
use std::f64::consts::PI;
let image = self.image.unwrap_or_else(|| ParticleImage::soft_glow(64));
CAEmitterLayerBuilder::new()
.position(self.position.0, self.position.1)
.shape(EmitterShape::Point)
.render_mode(self.render_mode)
.particle(|p| {
let mut builder = p
.birth_rate(self.birth_rate)
.lifetime(self.lifetime)
.lifetime_range(self.lifetime_range)
.velocity(self.velocity)
.velocity_range(self.velocity_range)
.emission_range(PI * 2.0) .scale(self.scale)
.scale_range(self.scale_range)
.scale_speed(self.scale_speed)
.alpha_speed(self.alpha_speed)
.image(image);
if let Some(color) = self.color {
builder = builder.color(color);
}
builder
})
.build()
}
}
fn create_soft_glow_image(size: usize) -> CFRetained<CGImage> {
let color_space = CGColorSpace::new_device_rgb().expect("Failed to create color space");
let context = unsafe {
CGBitmapContextCreate(
std::ptr::null_mut(),
size,
size,
8,
size * 4,
Some(&color_space),
CGImageAlphaInfo::PremultipliedLast.0,
)
}
.expect("Failed to create bitmap context");
let center = (size / 2) as f64;
let radius = center;
for r in (1..=size / 2).rev() {
let alpha = 1.0 - (r as f64 / radius);
CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, alpha);
CGContext::fill_ellipse_in_rect(
Some(&context),
CGRect::new(
CGPoint::new(center - r as f64, center - r as f64),
CGSize::new(r as f64 * 2.0, r as f64 * 2.0),
),
);
}
CGBitmapContextCreateImage(Some(&context)).expect("Failed to create image")
}
fn create_circle_image(size: usize) -> CFRetained<CGImage> {
let color_space = CGColorSpace::new_device_rgb().expect("Failed to create color space");
let context = unsafe {
CGBitmapContextCreate(
std::ptr::null_mut(),
size,
size,
8,
size * 4,
Some(&color_space),
CGImageAlphaInfo::PremultipliedLast.0,
)
}
.expect("Failed to create bitmap context");
CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, 1.0);
CGContext::fill_ellipse_in_rect(
Some(&context),
CGRect::new(
CGPoint::new(0.0, 0.0),
CGSize::new(size as f64, size as f64),
),
);
CGBitmapContextCreateImage(Some(&context)).expect("Failed to create image")
}
fn create_star_image(size: usize, points: usize) -> CFRetained<CGImage> {
use std::f64::consts::PI;
let color_space = CGColorSpace::new_device_rgb().expect("Failed to create color space");
let context = unsafe {
CGBitmapContextCreate(
std::ptr::null_mut(),
size,
size,
8,
size * 4,
Some(&color_space),
CGImageAlphaInfo::PremultipliedLast.0,
)
}
.expect("Failed to create bitmap context");
let center = size as f64 / 2.0;
let outer_radius = center * 0.95;
let inner_radius = center * 0.4;
let points = points.max(3);
let angle_step = PI / points as f64;
for i in 0..(points * 2) {
let is_outer = i % 2 == 0;
let radius = if is_outer { outer_radius } else { inner_radius };
let angle = -PI / 2.0 + (i as f64) * angle_step;
let x = center + radius * angle.cos();
let y = center + radius * angle.sin();
let steps = 20;
for s in 0..steps {
let t = s as f64 / steps as f64;
let px = center + (x - center) * t;
let py = center + (y - center) * t;
let alpha = 1.0 - t * 0.5;
CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, alpha);
let dot_size = 2.0 + (1.0 - t) * 2.0;
CGContext::fill_ellipse_in_rect(
Some(&context),
CGRect::new(
CGPoint::new(px - dot_size / 2.0, py - dot_size / 2.0),
CGSize::new(dot_size, dot_size),
),
);
}
}
for r in (1..=(size / 6)).rev() {
let alpha = 1.0 - (r as f64 / (size as f64 / 6.0)) * 0.3;
CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, alpha);
CGContext::fill_ellipse_in_rect(
Some(&context),
CGRect::new(
CGPoint::new(center - r as f64, center - r as f64),
CGSize::new(r as f64 * 2.0, r as f64 * 2.0),
),
);
}
CGBitmapContextCreateImage(Some(&context)).expect("Failed to create image")
}
fn create_spark_image(size: usize) -> CFRetained<CGImage> {
let color_space = CGColorSpace::new_device_rgb().expect("Failed to create color space");
let width = size;
let height = size / 3;
let context = unsafe {
CGBitmapContextCreate(
std::ptr::null_mut(),
width,
height,
8,
width * 4,
Some(&color_space),
CGImageAlphaInfo::PremultipliedLast.0,
)
}
.expect("Failed to create bitmap context");
let center_x = width as f64 / 2.0;
let center_y = height as f64 / 2.0;
for x in 0..width {
let dx = (x as f64 - center_x) / center_x;
let x_alpha = 1.0 - dx.abs().powf(1.5);
for y in 0..height {
let dy = (y as f64 - center_y) / center_y;
let y_alpha = 1.0 - dy.abs().powf(2.0);
let alpha = (x_alpha * y_alpha).max(0.0);
if alpha > 0.01 {
CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, alpha);
CGContext::fill_rect(
Some(&context),
CGRect::new(CGPoint::new(x as f64, y as f64), CGSize::new(1.0, 1.0)),
);
}
}
}
CGBitmapContextCreateImage(Some(&context)).expect("Failed to create image")
}