Skip to main content

canvas_app/
canvas_app.rs

1use operad::native::{NativeWindowOptions, NativeWindowResult};
2use operad::{
3    root_style, widgets, CanvasContent, CanvasRenderProgram, ColorRgba, LayoutStyle, StrokeStyle,
4    TextStyle, UiDocument, UiNode, UiSize, UiVisual, WidgetAction,
5};
6
7fn main() -> NativeWindowResult {
8    operad::native::run_app_with(
9        NativeWindowOptions::new("Canvas app")
10            .with_min_size(520.0, 380.0)
11            .with_tick_action("runtime.tick")
12            .with_tick_rate_hz(60.0),
13        CanvasApp::default(),
14        CanvasApp::update,
15        CanvasApp::view,
16    )
17}
18
19#[derive(Default)]
20struct CanvasApp {
21    phase: f32,
22}
23
24impl CanvasApp {
25    fn update(&mut self, action: WidgetAction) {
26        if action
27            .binding
28            .action_id()
29            .is_some_and(|id| id.as_str() == "runtime.tick")
30        {
31            self.phase = (self.phase + 0.01) % 1.0;
32        }
33    }
34
35    fn view(&self, viewport: UiSize) -> UiDocument {
36        let mut ui = UiDocument::new(root_style(viewport.width, viewport.height));
37        let panel = ui.add_child(
38            ui.root(),
39            UiNode::container(
40                "canvas.app",
41                LayoutStyle::column()
42                    .with_width_percent(1.0)
43                    .with_height_percent(1.0)
44                    .with_padding(16.0)
45                    .with_gap(10.0),
46            )
47            .with_visual(UiVisual::panel(ColorRgba::new(13, 17, 23, 255), None, 0.0)),
48        );
49        widgets::label(
50            &mut ui,
51            panel,
52            "canvas.title",
53            "WGPU canvas",
54            heading(),
55            LayoutStyle::new().with_width_percent(1.0).with_height(32.0),
56        );
57        let mut options = widgets::CanvasOptions::default()
58            .with_accessibility_label("Animated shader canvas")
59            .with_aspect_ratio(16.0 / 9.0);
60        options.layout = LayoutStyle::new()
61            .with_width_percent(1.0)
62            .with_height(0.0)
63            .with_flex_grow(1.0);
64        options.visual = UiVisual::panel(
65            ColorRgba::new(18, 22, 28, 255),
66            Some(StrokeStyle::new(ColorRgba::new(58, 68, 84, 255), 1.0)),
67            4.0,
68        );
69        widgets::canvas(
70            &mut ui,
71            panel,
72            "canvas.preview",
73            CanvasContent::new("canvas.preview").program(shader(self.phase)),
74            options,
75        );
76        ui
77    }
78}
79
80fn shader(phase: f32) -> CanvasRenderProgram {
81    CanvasRenderProgram::wgsl(
82        r#"
83override PHASE: f32 = 0.0;
84
85struct VertexOutput {
86    @builtin(position) position: vec4<f32>,
87    @location(0) uv: vec2<f32>,
88};
89
90@vertex
91fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
92    let positions = array<vec2<f32>, 3>(
93        vec2<f32>(-1.0, -1.0),
94        vec2<f32>(3.0, -1.0),
95        vec2<f32>(-1.0, 3.0),
96    );
97    let position = positions[vertex_index];
98    var output: VertexOutput;
99    output.position = vec4<f32>(position, 0.0, 1.0);
100    output.uv = position * 0.5 + vec2<f32>(0.5, 0.5);
101    return output;
102}
103
104@fragment
105fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
106    let p = input.uv * 2.0 - vec2<f32>(1.0, 1.0);
107    let wave = 0.5 + 0.5 * sin((p.x + p.y + PHASE * 6.28318) * 5.0);
108    let glow = 1.0 / (1.0 + dot(p, p) * 3.0);
109    let color = vec3<f32>(0.10, 0.32, 0.72) * glow + vec3<f32>(0.22, 0.70, 0.56) * wave * 0.35;
110    return vec4<f32>(color, 1.0);
111}
112"#,
113    )
114    .label("template.canvas")
115    .constant("PHASE", phase as f64)
116    .clear_color(Some(ColorRgba::new(18, 22, 28, 255)))
117}
118
119fn heading() -> TextStyle {
120    TextStyle {
121        font_size: 22.0,
122        line_height: 30.0,
123        color: ColorRgba::WHITE,
124        ..TextStyle::default()
125    }
126}