1use crate::color::Color;
28use objc2::rc::Retained;
29use objc2::runtime::AnyObject;
30use objc2_core_foundation::{CFRetained, CGPoint, CGRect, CGSize};
31use objc2_core_graphics::{
32 CGBitmapContextCreate, CGBitmapContextCreateImage, CGColor, CGColorSpace, CGContext, CGImage,
33 CGImageAlphaInfo,
34};
35use objc2_foundation::NSArray;
36use objc2_quartz_core::{
37 kCAEmitterLayerAdditive, kCAEmitterLayerBackToFront, kCAEmitterLayerCircle,
38 kCAEmitterLayerCuboid, kCAEmitterLayerLine, kCAEmitterLayerOldestFirst,
39 kCAEmitterLayerOldestLast, kCAEmitterLayerOutline, kCAEmitterLayerPoint, kCAEmitterLayerPoints,
40 kCAEmitterLayerRectangle, kCAEmitterLayerSphere, kCAEmitterLayerSurface,
41 kCAEmitterLayerUnordered, kCAEmitterLayerVolume, CAEmitterCell, CAEmitterLayer,
42};
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
50pub enum EmitterShape {
51 #[default]
53 Point,
54 Line,
56 Rectangle,
58 Circle,
60 Cuboid,
62 Sphere,
64}
65
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
68pub enum EmitterMode {
69 #[default]
71 Points,
72 Outline,
74 Surface,
76 Volume,
78}
79
80#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
82pub enum RenderMode {
83 #[default]
85 Unordered,
86 OldestFirst,
88 OldestLast,
90 BackToFront,
92 Additive,
94}
95
96#[derive(Clone, Debug)]
98pub enum ParticleImage {
99 SoftGlow(u32),
101 Circle(u32),
103 Star { size: u32, points: u32 },
105 Spark(u32),
107}
108
109impl ParticleImage {
110 pub fn soft_glow(size: u32) -> Self {
112 Self::SoftGlow(size)
113 }
114
115 pub fn circle(size: u32) -> Self {
117 Self::Circle(size)
118 }
119
120 pub fn star(size: u32, points: u32) -> Self {
124 Self::Star { size, points }
125 }
126
127 pub fn spark(size: u32) -> Self {
131 Self::Spark(size)
132 }
133
134 pub fn to_cgimage(&self) -> CFRetained<CGImage> {
136 match self {
137 Self::SoftGlow(size) => create_soft_glow_image(*size as usize),
138 Self::Circle(size) => create_circle_image(*size as usize),
139 Self::Star { size, points } => create_star_image(*size as usize, *points as usize),
140 Self::Spark(size) => create_spark_image(*size as usize),
141 }
142 }
143}
144
145pub struct CAEmitterCellBuilder {
153 birth_rate: f32,
154 lifetime: f32,
155 lifetime_range: f32,
156 velocity: f64,
157 velocity_range: f64,
158 emission_longitude: f64,
159 emission_range: f64,
160 scale: f64,
161 scale_range: f64,
162 scale_speed: f64,
163 alpha_speed: f32,
164 spin: f64,
165 spin_range: f64,
166 acceleration: (f64, f64),
167 color: Option<Color>,
168 image: Option<ParticleImage>,
169}
170
171impl CAEmitterCellBuilder {
172 pub fn new() -> Self {
174 Self {
175 birth_rate: 1.0,
176 lifetime: 1.0,
177 lifetime_range: 0.0,
178 velocity: 0.0,
179 velocity_range: 0.0,
180 emission_longitude: 0.0,
181 emission_range: 0.0,
182 scale: 1.0,
183 scale_range: 0.0,
184 scale_speed: 0.0,
185 alpha_speed: 0.0,
186 spin: 0.0,
187 spin_range: 0.0,
188 acceleration: (0.0, 0.0),
189 color: None,
190 image: None,
191 }
192 }
193
194 pub fn birth_rate(mut self, rate: f32) -> Self {
196 self.birth_rate = rate;
197 self
198 }
199
200 pub fn lifetime(mut self, seconds: f32) -> Self {
202 self.lifetime = seconds;
203 self
204 }
205
206 pub fn lifetime_range(mut self, range: f32) -> Self {
208 self.lifetime_range = range;
209 self
210 }
211
212 pub fn velocity(mut self, v: f64) -> Self {
214 self.velocity = v;
215 self
216 }
217
218 pub fn velocity_range(mut self, range: f64) -> Self {
220 self.velocity_range = range;
221 self
222 }
223
224 pub fn emission_longitude(mut self, radians: f64) -> Self {
226 self.emission_longitude = radians;
227 self
228 }
229
230 pub fn emission_toward(mut self, from: (f64, f64), target: (f64, f64)) -> Self {
234 let dx = target.0 - from.0;
235 let dy = target.1 - from.1;
236 self.emission_longitude = dy.atan2(dx);
237 self
238 }
239
240 pub fn emission_range(mut self, radians: f64) -> Self {
245 self.emission_range = radians;
246 self
247 }
248
249 pub fn scale(mut self, s: f64) -> Self {
251 self.scale = s;
252 self
253 }
254
255 pub fn scale_range(mut self, range: f64) -> Self {
257 self.scale_range = range;
258 self
259 }
260
261 pub fn scale_speed(mut self, speed: f64) -> Self {
263 self.scale_speed = speed;
264 self
265 }
266
267 pub fn alpha_speed(mut self, speed: f32) -> Self {
269 self.alpha_speed = speed;
270 self
271 }
272
273 pub fn spin(mut self, radians_per_sec: f64) -> Self {
275 self.spin = radians_per_sec;
276 self
277 }
278
279 pub fn spin_range(mut self, range: f64) -> Self {
281 self.spin_range = range;
282 self
283 }
284
285 pub fn acceleration(mut self, x: f64, y: f64) -> Self {
289 self.acceleration = (x, y);
290 self
291 }
292
293 pub fn color(mut self, color: impl Into<Color>) -> Self {
303 self.color = Some(color.into());
304 self
305 }
306
307 pub fn color_rgb(mut self, r: f64, g: f64, b: f64) -> Self {
309 self.color = Some(Color::rgb(r, g, b));
310 self
311 }
312
313 pub fn color_rgba(mut self, r: f64, g: f64, b: f64, a: f64) -> Self {
315 self.color = Some(Color::rgba(r, g, b, a));
316 self
317 }
318
319 pub fn image(mut self, img: ParticleImage) -> Self {
321 self.image = Some(img);
322 self
323 }
324
325 pub fn build(self) -> Retained<CAEmitterCell> {
327 let cell = CAEmitterCell::new();
328
329 cell.setBirthRate(self.birth_rate);
330 cell.setLifetime(self.lifetime);
331 cell.setLifetimeRange(self.lifetime_range);
332 cell.setVelocity(self.velocity);
333 cell.setVelocityRange(self.velocity_range);
334 cell.setEmissionLongitude(self.emission_longitude);
335 cell.setEmissionRange(self.emission_range);
336 cell.setScale(self.scale);
337 cell.setScaleRange(self.scale_range);
338 cell.setScaleSpeed(self.scale_speed);
339 cell.setAlphaSpeed(self.alpha_speed);
340 cell.setSpin(self.spin);
341 cell.setSpinRange(self.spin_range);
342 cell.setXAcceleration(self.acceleration.0);
343 cell.setYAcceleration(self.acceleration.1);
344
345 if let Some(color) = self.color {
346 let cgcolor: CFRetained<CGColor> = color.into();
347 cell.setColor(Some(&cgcolor));
348 }
349
350 if let Some(img) = self.image {
351 let cgimage = img.to_cgimage();
352 unsafe {
353 let image_obj: &AnyObject = std::mem::transmute(&*cgimage);
354 cell.setContents(Some(image_obj));
355 }
356 }
357
358 cell
359 }
360}
361
362impl Default for CAEmitterCellBuilder {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368pub struct CAEmitterLayerBuilder {
387 position: (f64, f64),
388 size: (f64, f64),
389 shape: EmitterShape,
390 mode: EmitterMode,
391 render_mode: RenderMode,
392 birth_rate: f32,
393 cells: Vec<Retained<CAEmitterCell>>,
394}
395
396impl CAEmitterLayerBuilder {
397 pub fn new() -> Self {
399 Self {
400 position: (0.0, 0.0),
401 size: (0.0, 0.0),
402 shape: EmitterShape::Point,
403 mode: EmitterMode::Points,
404 render_mode: RenderMode::Unordered,
405 birth_rate: 1.0,
406 cells: Vec::new(),
407 }
408 }
409
410 pub fn position(mut self, x: f64, y: f64) -> Self {
412 self.position = (x, y);
413 self
414 }
415
416 pub fn size(mut self, width: f64, height: f64) -> Self {
418 self.size = (width, height);
419 self
420 }
421
422 pub fn shape(mut self, shape: EmitterShape) -> Self {
424 self.shape = shape;
425 self
426 }
427
428 pub fn mode(mut self, mode: EmitterMode) -> Self {
430 self.mode = mode;
431 self
432 }
433
434 pub fn render_mode(mut self, mode: RenderMode) -> Self {
436 self.render_mode = mode;
437 self
438 }
439
440 pub fn birth_rate(mut self, rate: f32) -> Self {
445 self.birth_rate = rate;
446 self
447 }
448
449 pub fn particle<F>(mut self, configure: F) -> Self
461 where
462 F: FnOnce(CAEmitterCellBuilder) -> CAEmitterCellBuilder,
463 {
464 let builder = CAEmitterCellBuilder::new();
465 let configured = configure(builder);
466 self.cells.push(configured.build());
467 self
468 }
469
470 pub fn cell(mut self, cell: Retained<CAEmitterCell>) -> Self {
474 self.cells.push(cell);
475 self
476 }
477
478 pub fn build(self) -> Retained<CAEmitterLayer> {
480 let emitter = CAEmitterLayer::new();
481
482 emitter.setEmitterPosition(CGPoint::new(self.position.0, self.position.1));
483 emitter.setEmitterSize(CGSize::new(self.size.0, self.size.1));
484 emitter.setBirthRate(self.birth_rate);
485
486 unsafe {
488 let shape = match self.shape {
489 EmitterShape::Point => kCAEmitterLayerPoint,
490 EmitterShape::Line => kCAEmitterLayerLine,
491 EmitterShape::Rectangle => kCAEmitterLayerRectangle,
492 EmitterShape::Circle => kCAEmitterLayerCircle,
493 EmitterShape::Cuboid => kCAEmitterLayerCuboid,
494 EmitterShape::Sphere => kCAEmitterLayerSphere,
495 };
496 emitter.setEmitterShape(shape);
497 }
498
499 unsafe {
501 let mode = match self.mode {
502 EmitterMode::Points => kCAEmitterLayerPoints,
503 EmitterMode::Outline => kCAEmitterLayerOutline,
504 EmitterMode::Surface => kCAEmitterLayerSurface,
505 EmitterMode::Volume => kCAEmitterLayerVolume,
506 };
507 emitter.setEmitterMode(mode);
508 }
509
510 unsafe {
512 let render = match self.render_mode {
513 RenderMode::Unordered => kCAEmitterLayerUnordered,
514 RenderMode::OldestFirst => kCAEmitterLayerOldestFirst,
515 RenderMode::OldestLast => kCAEmitterLayerOldestLast,
516 RenderMode::BackToFront => kCAEmitterLayerBackToFront,
517 RenderMode::Additive => kCAEmitterLayerAdditive,
518 };
519 emitter.setRenderMode(render);
520 }
521
522 if !self.cells.is_empty() {
524 let cells = NSArray::from_retained_slice(&self.cells);
525 emitter.setEmitterCells(Some(&cells));
526 }
527
528 emitter
529 }
530}
531
532impl Default for CAEmitterLayerBuilder {
533 fn default() -> Self {
534 Self::new()
535 }
536}
537
538pub struct PointBurstBuilder {
551 position: (f64, f64),
552 birth_rate: f32,
553 lifetime: f32,
554 lifetime_range: f32,
555 velocity: f64,
556 velocity_range: f64,
557 scale: f64,
558 scale_range: f64,
559 scale_speed: f64,
560 alpha_speed: f32,
561 color: Option<Color>,
562 image: Option<ParticleImage>,
563 render_mode: RenderMode,
564}
565
566impl PointBurstBuilder {
567 pub fn new(x: f64, y: f64) -> Self {
569 Self {
570 position: (x, y),
571 birth_rate: 100.0,
572 lifetime: 5.0,
573 lifetime_range: 1.0,
574 velocity: 100.0,
575 velocity_range: 20.0,
576 scale: 0.1,
577 scale_range: 0.02,
578 scale_speed: 0.0,
579 alpha_speed: 0.0,
580 color: None,
581 image: None,
582 render_mode: RenderMode::Additive,
583 }
584 }
585
586 pub fn birth_rate(mut self, rate: f32) -> Self {
588 self.birth_rate = rate;
589 self
590 }
591
592 pub fn lifetime(mut self, seconds: f32) -> Self {
594 self.lifetime = seconds;
595 self
596 }
597
598 pub fn lifetime_range(mut self, range: f32) -> Self {
600 self.lifetime_range = range;
601 self
602 }
603
604 pub fn velocity(mut self, v: f64) -> Self {
606 self.velocity = v;
607 self
608 }
609
610 pub fn velocity_range(mut self, range: f64) -> Self {
612 self.velocity_range = range;
613 self
614 }
615
616 pub fn scale(mut self, s: f64) -> Self {
618 self.scale = s;
619 self
620 }
621
622 pub fn scale_range(mut self, range: f64) -> Self {
624 self.scale_range = range;
625 self
626 }
627
628 pub fn scale_speed(mut self, speed: f64) -> Self {
630 self.scale_speed = speed;
631 self
632 }
633
634 pub fn alpha_speed(mut self, speed: f32) -> Self {
636 self.alpha_speed = speed;
637 self
638 }
639
640 pub fn color(mut self, color: impl Into<Color>) -> Self {
649 self.color = Some(color.into());
650 self
651 }
652
653 pub fn color_rgb(mut self, r: f64, g: f64, b: f64) -> Self {
655 self.color = Some(Color::rgb(r, g, b));
656 self
657 }
658
659 pub fn color_rgba(mut self, r: f64, g: f64, b: f64, a: f64) -> Self {
661 self.color = Some(Color::rgba(r, g, b, a));
662 self
663 }
664
665 pub fn image(mut self, img: ParticleImage) -> Self {
667 self.image = Some(img);
668 self
669 }
670
671 pub fn render_mode(mut self, mode: RenderMode) -> Self {
673 self.render_mode = mode;
674 self
675 }
676
677 pub fn build(self) -> Retained<CAEmitterLayer> {
679 use std::f64::consts::PI;
680
681 let image = self.image.unwrap_or_else(|| ParticleImage::soft_glow(64));
682
683 CAEmitterLayerBuilder::new()
684 .position(self.position.0, self.position.1)
685 .shape(EmitterShape::Point)
686 .render_mode(self.render_mode)
687 .particle(|p| {
688 let mut builder = p
689 .birth_rate(self.birth_rate)
690 .lifetime(self.lifetime)
691 .lifetime_range(self.lifetime_range)
692 .velocity(self.velocity)
693 .velocity_range(self.velocity_range)
694 .emission_range(PI * 2.0) .scale(self.scale)
696 .scale_range(self.scale_range)
697 .scale_speed(self.scale_speed)
698 .alpha_speed(self.alpha_speed)
699 .image(image);
700
701 if let Some(color) = self.color {
702 builder = builder.color(color);
703 }
704
705 builder
706 })
707 .build()
708 }
709}
710
711fn create_soft_glow_image(size: usize) -> CFRetained<CGImage> {
717 let color_space = CGColorSpace::new_device_rgb().expect("Failed to create color space");
718
719 let context = unsafe {
720 CGBitmapContextCreate(
721 std::ptr::null_mut(),
722 size,
723 size,
724 8,
725 size * 4,
726 Some(&color_space),
727 CGImageAlphaInfo::PremultipliedLast.0,
728 )
729 }
730 .expect("Failed to create bitmap context");
731
732 let center = (size / 2) as f64;
734 let radius = center;
735
736 for r in (1..=size / 2).rev() {
737 let alpha = 1.0 - (r as f64 / radius);
738 CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, alpha);
739 CGContext::fill_ellipse_in_rect(
740 Some(&context),
741 CGRect::new(
742 CGPoint::new(center - r as f64, center - r as f64),
743 CGSize::new(r as f64 * 2.0, r as f64 * 2.0),
744 ),
745 );
746 }
747
748 CGBitmapContextCreateImage(Some(&context)).expect("Failed to create image")
749}
750
751fn create_circle_image(size: usize) -> CFRetained<CGImage> {
753 let color_space = CGColorSpace::new_device_rgb().expect("Failed to create color space");
754
755 let context = unsafe {
756 CGBitmapContextCreate(
757 std::ptr::null_mut(),
758 size,
759 size,
760 8,
761 size * 4,
762 Some(&color_space),
763 CGImageAlphaInfo::PremultipliedLast.0,
764 )
765 }
766 .expect("Failed to create bitmap context");
767
768 CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, 1.0);
770 CGContext::fill_ellipse_in_rect(
771 Some(&context),
772 CGRect::new(
773 CGPoint::new(0.0, 0.0),
774 CGSize::new(size as f64, size as f64),
775 ),
776 );
777
778 CGBitmapContextCreateImage(Some(&context)).expect("Failed to create image")
779}
780
781fn create_star_image(size: usize, points: usize) -> CFRetained<CGImage> {
783 use std::f64::consts::PI;
784
785 let color_space = CGColorSpace::new_device_rgb().expect("Failed to create color space");
786
787 let context = unsafe {
788 CGBitmapContextCreate(
789 std::ptr::null_mut(),
790 size,
791 size,
792 8,
793 size * 4,
794 Some(&color_space),
795 CGImageAlphaInfo::PremultipliedLast.0,
796 )
797 }
798 .expect("Failed to create bitmap context");
799
800 let center = size as f64 / 2.0;
801 let outer_radius = center * 0.95;
802 let inner_radius = center * 0.4;
803 let points = points.max(3); let angle_step = PI / points as f64;
808
809 for i in 0..(points * 2) {
810 let is_outer = i % 2 == 0;
811 let radius = if is_outer { outer_radius } else { inner_radius };
812 let angle = -PI / 2.0 + (i as f64) * angle_step;
813
814 let x = center + radius * angle.cos();
815 let y = center + radius * angle.sin();
816
817 let steps = 20;
819 for s in 0..steps {
820 let t = s as f64 / steps as f64;
821 let px = center + (x - center) * t;
822 let py = center + (y - center) * t;
823 let alpha = 1.0 - t * 0.5; CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, alpha);
826 let dot_size = 2.0 + (1.0 - t) * 2.0;
827 CGContext::fill_ellipse_in_rect(
828 Some(&context),
829 CGRect::new(
830 CGPoint::new(px - dot_size / 2.0, py - dot_size / 2.0),
831 CGSize::new(dot_size, dot_size),
832 ),
833 );
834 }
835 }
836
837 for r in (1..=(size / 6)).rev() {
839 let alpha = 1.0 - (r as f64 / (size as f64 / 6.0)) * 0.3;
840 CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, alpha);
841 CGContext::fill_ellipse_in_rect(
842 Some(&context),
843 CGRect::new(
844 CGPoint::new(center - r as f64, center - r as f64),
845 CGSize::new(r as f64 * 2.0, r as f64 * 2.0),
846 ),
847 );
848 }
849
850 CGBitmapContextCreateImage(Some(&context)).expect("Failed to create image")
851}
852
853fn create_spark_image(size: usize) -> CFRetained<CGImage> {
855 let color_space = CGColorSpace::new_device_rgb().expect("Failed to create color space");
856
857 let width = size;
859 let height = size / 3;
860
861 let context = unsafe {
862 CGBitmapContextCreate(
863 std::ptr::null_mut(),
864 width,
865 height,
866 8,
867 width * 4,
868 Some(&color_space),
869 CGImageAlphaInfo::PremultipliedLast.0,
870 )
871 }
872 .expect("Failed to create bitmap context");
873
874 let center_x = width as f64 / 2.0;
875 let center_y = height as f64 / 2.0;
876
877 for x in 0..width {
879 let dx = (x as f64 - center_x) / center_x;
880 let x_alpha = 1.0 - dx.abs().powf(1.5); for y in 0..height {
883 let dy = (y as f64 - center_y) / center_y;
884 let y_alpha = 1.0 - dy.abs().powf(2.0); let alpha = (x_alpha * y_alpha).max(0.0);
887 if alpha > 0.01 {
888 CGContext::set_rgb_fill_color(Some(&context), 1.0, 1.0, 1.0, alpha);
889 CGContext::fill_rect(
890 Some(&context),
891 CGRect::new(CGPoint::new(x as f64, y as f64), CGSize::new(1.0, 1.0)),
892 );
893 }
894 }
895 }
896
897 CGBitmapContextCreateImage(Some(&context)).expect("Failed to create image")
898}