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}