Skip to content

Commit b9240a1

Browse files
Merge pull request #509 from gridaco/canary
Grida Canvas - Pixel Preview
2 parents 2c2ef4d + fd07b7b commit b9240a1

File tree

27 files changed

+944
-52
lines changed

27 files changed

+944
-52
lines changed

crates/grida-canvas-wasm/lib/__test__/environment-node-api-spec-validation.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ const EXPECTED_FUNCTIONS = [
9191
{ name: "_devtools_rendering_set_show_hit_testing", paramCount: 2 },
9292
{ name: "_devtools_rendering_set_show_ruler", paramCount: 2 },
9393
{ name: "_runtime_renderer_set_cache_tile", paramCount: 2 },
94+
{ name: "_runtime_renderer_set_pixel_preview_scale", paramCount: 2 },
95+
{ name: "_runtime_renderer_set_pixel_preview_stable", paramCount: 2 },
9496
] as const;
9597

9698
// Expected Emscripten runtime methods

crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js

Lines changed: 2 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:16bc581263902e9085c4275cad54587f8018f81e1f63e74c667adb09b9b92b6a
3-
size 12867497
2+
oid sha256:a8761491f638c3b4591d3ab9c1d370eda5a29bfd327e2a935e0f9941c922b75b
3+
size 12873266

crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,5 +173,15 @@ declare namespace canvas {
173173
state: GridaCanvasApplicationPtr,
174174
enabled: boolean
175175
): void;
176+
177+
_runtime_renderer_set_pixel_preview_scale(
178+
state: GridaCanvasApplicationPtr,
179+
scale: number
180+
): void;
181+
182+
_runtime_renderer_set_pixel_preview_stable(
183+
state: GridaCanvasApplicationPtr,
184+
stable: boolean
185+
): void;
176186
}
177187
}

crates/grida-canvas-wasm/lib/modules/canvas.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,16 @@ export class Scene {
392392
this.module._runtime_renderer_set_cache_tile(this.appptr, enable);
393393
}
394394

395+
runtime_renderer_set_pixel_preview_scale(scale: number) {
396+
this._assertAlive();
397+
this.module._runtime_renderer_set_pixel_preview_scale(this.appptr, scale);
398+
}
399+
400+
runtime_renderer_set_pixel_preview_stable(stable: boolean) {
401+
this._assertAlive();
402+
this.module._runtime_renderer_set_pixel_preview_stable(this.appptr, stable);
403+
}
404+
395405
// ====================================================================================================
396406
// DEVTOOLS
397407
// ====================================================================================================

crates/grida-canvas-wasm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@grida/canvas-wasm",
3-
"version": "0.90.0-canary.2",
3+
"version": "0.90.0-canary.3",
44
"private": false,
55
"description": "WASM bindings for Grida Canvas",
66
"keywords": [

crates/grida-canvas-wasm/src/wasm_application.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,28 @@ pub unsafe extern "C" fn runtime_renderer_set_cache_tile(
587587
}
588588
}
589589

590+
#[no_mangle]
591+
/// js::_runtime_renderer_set_pixel_preview_scale
592+
pub unsafe extern "C" fn runtime_renderer_set_pixel_preview_scale(
593+
app: *mut UnknownTargetApplication,
594+
scale: u32,
595+
) {
596+
if let Some(app) = app.as_mut() {
597+
app.runtime_renderer_set_pixel_preview_scale((scale as u8).min(2));
598+
}
599+
}
600+
601+
#[no_mangle]
602+
/// js::_runtime_renderer_set_pixel_preview_stable
603+
pub unsafe extern "C" fn runtime_renderer_set_pixel_preview_stable(
604+
app: *mut UnknownTargetApplication,
605+
stable: bool,
606+
) {
607+
if let Some(app) = app.as_mut() {
608+
app.runtime_renderer_set_pixel_preview_stable(stable);
609+
}
610+
}
611+
590612
#[no_mangle]
591613
/// js::_devtools_rendering_set_show_fps_meter
592614
pub unsafe extern "C" fn devtools_rendering_set_show_fps_meter(
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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

Comments
 (0)