|
| 1 | +//! Golden: Pixel Preview (Normal vs Pixelated) |
| 2 | +//! |
| 3 | +//! Renders the same simple scene at a high zoom twice: |
| 4 | +//! - Normal rendering (smooth, re-rasterized vectors) |
| 5 | +//! - Pixel Preview 1x (downsample then nearest-neighbor upscale) |
| 6 | +//! |
| 7 | +//! Output: `crates/grida-canvas/goldens/pixel_preview.png` |
| 8 | +
|
| 9 | +use cg::cg::prelude::*; |
| 10 | +use cg::node::{ |
| 11 | + factory::NodeFactory, |
| 12 | + scene_graph::{Parent, SceneGraph}, |
| 13 | + schema::*, |
| 14 | +}; |
| 15 | +use cg::runtime::{ |
| 16 | + camera::Camera2D, |
| 17 | + scene::{Backend, FrameFlushResult, Renderer, RendererOptions}, |
| 18 | +}; |
| 19 | +use skia_safe::{surfaces, Color, Font, Paint as SkPaint, Rect}; |
| 20 | + |
| 21 | +fn build_scene() -> Scene { |
| 22 | + let nf = NodeFactory::new(); |
| 23 | + |
| 24 | + // A small stroked rectangle (1px stroke) so pixelation is obvious when zoomed. |
| 25 | + let mut rect = nf.create_rectangle_node(); |
| 26 | + rect.transform = math2::transform::AffineTransform::new(0.0, 0.0, 0.0); |
| 27 | + rect.size = Size { |
| 28 | + width: 6.0, |
| 29 | + height: 6.0, |
| 30 | + }; |
| 31 | + rect.set_fill(Paint::Solid(SolidPaint { |
| 32 | + color: CGColor::from_rgba(240, 240, 240, 255), |
| 33 | + blend_mode: BlendMode::default(), |
| 34 | + active: true, |
| 35 | + })); |
| 36 | + rect.strokes = Paints::new([Paint::Solid(SolidPaint::BLACK)]); |
| 37 | + rect.stroke_width = StrokeWidth::Uniform(1.0); |
| 38 | + |
| 39 | + // A stroked ellipse to make jagged curves obvious in pixel preview. |
| 40 | + let mut ellipse = nf.create_ellipse_node(); |
| 41 | + ellipse.transform = math2::transform::AffineTransform::new(8.0, 0.0, 0.0); |
| 42 | + ellipse.size = Size { |
| 43 | + width: 6.0, |
| 44 | + height: 6.0, |
| 45 | + }; |
| 46 | + ellipse.fills = Paints::new([Paint::Solid(SolidPaint { |
| 47 | + color: CGColor::from_rgba(240, 240, 240, 255), |
| 48 | + blend_mode: BlendMode::default(), |
| 49 | + active: true, |
| 50 | + })]); |
| 51 | + ellipse.strokes = Paints::new([Paint::Solid(SolidPaint::BLACK)]); |
| 52 | + ellipse.stroke_width = SingularStrokeWidth(Some(1.0)); |
| 53 | + |
| 54 | + let mut graph = SceneGraph::new(); |
| 55 | + graph.append_children( |
| 56 | + vec![Node::Rectangle(rect), Node::Ellipse(ellipse)], |
| 57 | + Parent::Root, |
| 58 | + ); |
| 59 | + |
| 60 | + Scene { |
| 61 | + name: "Pixel Preview Golden".to_string(), |
| 62 | + graph, |
| 63 | + background_color: Some(CGColor::WHITE), |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +fn render_with_pixel_preview(scale: u8) -> skia_safe::Image { |
| 68 | + let (w, h) = (256u32, 256u32); |
| 69 | + let scene = build_scene(); |
| 70 | + |
| 71 | + let mut camera = Camera2D::new(Size { |
| 72 | + width: w as f32, |
| 73 | + height: h as f32, |
| 74 | + }); |
| 75 | + // High zoom so the effect is obvious (but still recognizable). |
| 76 | + camera.set_zoom(24.0); |
| 77 | + camera.set_center(7.0, 3.0); |
| 78 | + |
| 79 | + let mut renderer = Renderer::new_with_options( |
| 80 | + Backend::new_from_raster(w as i32, h as i32), |
| 81 | + None, |
| 82 | + camera, |
| 83 | + RendererOptions { |
| 84 | + use_embedded_fonts: true, |
| 85 | + }, |
| 86 | + ); |
| 87 | + |
| 88 | + renderer.load_scene(scene); |
| 89 | + renderer.set_pixel_preview_scale(scale); |
| 90 | + renderer.queue_unstable(); |
| 91 | + |
| 92 | + match renderer.flush() { |
| 93 | + FrameFlushResult::OK(_) => {} |
| 94 | + _ => panic!("expected rendered frame"), |
| 95 | + } |
| 96 | + |
| 97 | + let surface = unsafe { &mut *renderer.backend.get_surface() }; |
| 98 | + let image = surface.image_snapshot(); |
| 99 | + renderer.free(); |
| 100 | + image |
| 101 | +} |
| 102 | + |
| 103 | +fn main() { |
| 104 | + let normal = render_with_pixel_preview(0); |
| 105 | + let pixel_1x = render_with_pixel_preview(1); |
| 106 | + |
| 107 | + let padding = 16.0; |
| 108 | + let label_h = 18.0; |
| 109 | + let out_w = (normal.width() as f32 * 2.0 + padding * 3.0) as i32; |
| 110 | + let out_h = (normal.height() as f32 + padding * 2.0 + label_h) as i32; |
| 111 | + |
| 112 | + let mut surface = surfaces::raster_n32_premul((out_w, out_h)).expect("surface"); |
| 113 | + let canvas = surface.canvas(); |
| 114 | + canvas.clear(Color::WHITE); |
| 115 | + |
| 116 | + let left_x = padding; |
| 117 | + let top_y = padding + label_h; |
| 118 | + let right_x = padding * 2.0 + normal.width() as f32; |
| 119 | + |
| 120 | + // Draw images |
| 121 | + canvas.draw_image(&normal, (left_x, top_y), None); |
| 122 | + canvas.draw_image(&pixel_1x, (right_x, top_y), None); |
| 123 | + |
| 124 | + // Labels |
| 125 | + let font = Font::new( |
| 126 | + cg::fonts::embedded::typeface(cg::fonts::embedded::geistmono::BYTES), |
| 127 | + 12.0, |
| 128 | + ); |
| 129 | + let mut paint = SkPaint::default(); |
| 130 | + paint.set_color(Color::BLACK); |
| 131 | + paint.set_anti_alias(true); |
| 132 | + canvas.draw_str("Normal", (left_x, padding + 12.0), &font, &paint); |
| 133 | + canvas.draw_str("Pixel Preview 1x", (right_x, padding + 12.0), &font, &paint); |
| 134 | + |
| 135 | + // Frame separators (optional) |
| 136 | + let mut border = SkPaint::default(); |
| 137 | + border.set_color(Color::from_argb(255, 220, 220, 220)); |
| 138 | + border.set_style(skia_safe::paint::Style::Stroke); |
| 139 | + border.set_stroke_width(1.0); |
| 140 | + canvas.draw_rect( |
| 141 | + Rect::from_xywh(left_x, top_y, normal.width() as f32, normal.height() as f32), |
| 142 | + &border, |
| 143 | + ); |
| 144 | + canvas.draw_rect( |
| 145 | + Rect::from_xywh(right_x, top_y, normal.width() as f32, normal.height() as f32), |
| 146 | + &border, |
| 147 | + ); |
| 148 | + |
| 149 | + let image = surface.image_snapshot(); |
| 150 | + let data = image |
| 151 | + .encode(None, skia_safe::EncodedImageFormat::PNG, None) |
| 152 | + .expect("encode"); |
| 153 | + std::fs::write( |
| 154 | + concat!(env!("CARGO_MANIFEST_DIR"), "/goldens/pixel_preview.png"), |
| 155 | + data.as_bytes(), |
| 156 | + ) |
| 157 | + .expect("write png"); |
| 158 | +} |
| 159 | + |
0 commit comments