🚀 Init public commit

This commit is contained in:
Markus Scully 2025-07-27 00:42:40 +03:00
commit 62fbf6d17c
Signed by: mascully
GPG key ID: 93CA5814B698101C
42 changed files with 7433 additions and 0 deletions

26
crates/app/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "mascully_website_app"
version.workspace = true
edition.workspace = true
[features]
default = ["hydrate"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]
[dependencies]
leptos.workspace = true
leptos_meta.workspace = true
leptos_router.workspace = true
leptos_axum = { workspace = true, optional = true }
wgpu.workspace = true
web-sys.workspace = true
encase.workspace = true
glam.workspace = true
log.workspace = true
gloo.workspace = true
wasm-bindgen.workspace = true
js-sys.workspace = true
wasm-bindgen-futures.workspace = true
serde.workspace = true
indoc.workspace = true

78
crates/app/src/article.rs Normal file
View file

@ -0,0 +1,78 @@
use leptos::prelude::*;
use leptos_router::{components::A, hooks::use_params_map};
use crate::wgpu_renderer::WgpuTriangle;
#[derive(Clone)]
pub struct ArticleMetadata {
pub title: &'static str,
pub slug: &'static str,
pub description: &'static str,
}
#[component]
pub fn ArticlePage() -> impl IntoView {
let params = use_params_map();
// let slug = move || params.with(|params| params.get("slug").unwrap_or_default());
// let article_content = move || match slug().as_str() {
// "creating-an-actually-perfect-grid-shader" => view! {
// <div class="article grid-shader-article">
// <h1>"Creating an actually perfect grid shader"</h1>
// <div class="article-metadata">
// <p class="date">"March 9, 2024"</p>
// </div>
// <p>"Grid shaders are essential for many visualization and design applications, but creating the perfect grid involves some nuanced considerations."</p>
// <h2>"Basic Triangle Rendering"</h2>
// <p>"Before we get to grids, let's start with a basic triangle rendering implementation using WebGL:"</p>
// <div class="visualization">
// <WgpuTriangle />
// </div>
// <h2>"Building the Grid Shader"</h2>
// <p>"The key to a perfect grid shader lies in the precision of the grid lines and the handling of coordinate spaces."</p>
// <h2>"Avoiding Common Pitfalls"</h2>
// <p>"Many grid implementations suffer from aliasing issues or precision problems at certain scales."</p>
// <h2>"Conclusion"</h2>
// <p>"With the right approach, you can create grid shaders that look perfect at any scale."</p>
// </div>
// },
// "minimal-example-article" => view! {
// <div class="article minimal-article">
// <h1>"Minimal Example Article"</h1>
// <div class="article-metadata">
// <p class="date">"March 9, 2024"</p>
// </div>
// <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nisl eget ultricies tincidunt, nisl nisl aliquam nisl, eget ultricies nisl nisl eget nisl."</p>
// <h2>"Section One"</h2>
// <p>"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."</p>
// <h2>"Section Two"</h2>
// <p>"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."</p>
// </div>
// },
// _ => view! {
// <div class="article-not-found">
// <h1>"Article Not Found"</h1>
// <p>"The article you're looking for doesn't exist."</p>
// <A href="/">"Return to Home"</A>
// </div>
// },
// };
view! {
<div class="article-page">
<nav class="article-nav">
<A href="/">"← Back to Home"</A>
</nav>
// {article_content}
</div>
}
}

View file

@ -0,0 +1,6 @@
use leptos::prelude::*;
#[component]
pub fn CommandLine() -> impl IntoView {
view! { <input class="terminal-input" type="text" placeholder="$ " /> }
}

30
crates/app/src/contact.rs Normal file
View file

@ -0,0 +1,30 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::prelude::*;
#[component]
pub fn Contact() -> impl IntoView {
view! {
<section>
<h1>"Contact"</h1>
<section>
<span>"Email: "</span>
<a href="mailto:contact@mascully.com">"contact@mascully.com"</a>
</section>
<section>
<span>"GitHub: "</span>
<a href="https://github.com/mascully">"mascully"</a>
</section>
<section>
<span>"Matrix: "</span>
<a href="matrix:u/blue_flycatcher:matrix.org">"@blue_flycatcher:matrix.org"</a>
</section>
</section>
<section>
<span>"PGP:"</span>
<pre>{include_str!("security/pgp_master.txt")}</pre>
</section>
}
}

154
crates/app/src/lib.rs Normal file
View file

@ -0,0 +1,154 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::hydration::{AutoReload, HydrationScripts};
use leptos::prelude::*;
use leptos_meta::{Link, MetaTags, Stylesheet, Title, provide_meta_context};
use leptos_router::components::{A, FlatRoutes, Route, Router, Routes, RoutingProgress};
use leptos_router::{ParamSegment, StaticSegment, path};
use std::time::Duration;
use contact::Contact;
use posts::{Posts, plot_demo::PlotDemo};
use projects::Projects;
use theme_switcher::SiteHeader;
mod command_line;
mod contact;
mod plot;
mod posts;
mod projects;
mod sigil;
mod theme_switcher;
mod version;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<AutoReload options=options.clone() />
<HydrationScripts options />
<MetaTags />
</head>
<body>
<App />
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
view! {
<Stylesheet href="/css/styles.css" />
<Link rel="icon" href="/icon_2.png" />
<Link
rel="preload"
href="/fonts/space_grotesk_variable.woff2"
as_="font"
type_="font/woff2"
crossorigin="anonymous"
/>
<Link
rel="preload"
href="/fonts/ibm_plex_sans_variable.ttf"
as_="font"
type_="font/ttf"
crossorigin="anonymous"
/>
<Link
rel="preload"
href="/fonts/zed_mono_variable.woff2"
as_="font"
type_="font/woff2"
crossorigin="anonymous"
/>
<Title text="mascully.com" />
<Root>
<SiteHeader />
<div class="content-wrapper">
<div class="main-content">
<Router>
<aside class="sidebar">
<nav class="main-nav">
<ul>
<li>
<A href="/">"Home"</A>
</li>
<li>
<A href="/posts">"Posts"</A>
</li>
<li>
<A href="/projects">"Projects"</A>
</li>
<li>
<hr />
</li>
<li>
<a href="https://git.mascully.com/mascully?tab=repositories">
"Git"
</a>
</li>
<li>
<A href="/contact">"Contact"</A>
</li>
<li>
<A href="/hire_me">"Hire me"</A>
</li>
</ul>
</nav>
</aside>
<main>
<Routes fallback=NotFound>
<Route path=path!("/") view=HomePage />
<Route path=path!("/posts") view=Posts />
<Route path=path!("/posts/plot_demo") view=PlotDemo />
<Route path=path!("/projects") view=Projects />
<Route path=path!("/contact") view=Contact />
<Route path=path!("/hire_me") view=HireMe />
</Routes>
</main>
</Router>
</div>
</div>
</Root>
}
}
#[component]
fn Root(children: Children) -> impl IntoView {
view! { <div id="root">{children()}</div> }
}
#[component]
fn NotFound() -> impl IntoView {
view! { <h1>404</h1> }
}
#[component]
fn HomePage() -> impl IntoView {
view! {
<p>"Hi. I'm Markus. I mostly do full stack development with Rust and Nix."</p>
<p>
"You can check out some of my projects "<A href="/projects">"here"</A>" or on my "<A href="https://git.mascully.com/mascully?tab=repositories">"Git forge"</A>", including the "
<A href="https://git.mascully.com/mascully/mascully_website">
"source code for this website"
</A>"."
</p>
}
}
#[component]
fn HireMe() -> impl IntoView {
view! {
<p>"I am available for freelancing work (remote, European time zone)."</p>
<p>"I can work comfortably with most tech stacks in most domains."</p>
<p>"Email me at "<a href="mailto:contact@mascully.com">"contact@mascully.com"</a>.</p>
}
}

43
crates/app/src/plot.rs Normal file
View file

@ -0,0 +1,43 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::prelude::*;
use wasm_bindgen::prelude::*;
use web_sys::HtmlCanvasElement;
#[derive(Clone, Debug)]
pub struct Camera {
// TODO: Add camera fields (position, zoom, etc.)
}
impl Default for Camera {
fn default() -> Self {
Self {
// TODO: Initialize default camera values
}
}
}
#[derive(Clone, Debug)]
pub struct Element {
// TODO: Add element fields (position, shape, color, etc.)
}
#[component]
pub fn Plot(camera: Camera, elements: Vec<Element>) -> impl IntoView {
let canvas_ref = NodeRef::<leptos::html::Canvas>::new();
// TODO: Implement rendering logic
// - Set up canvas context
// - Handle resize events
// - Render elements based on camera view
// - Handle user interactions (pan, zoom, etc.)
view! {
<div class="plot-container">
<canvas node_ref=canvas_ref class="plot-canvas" width="800" height="600">
"Your browser does not support the HTML5 Canvas element."
</canvas>
</div>
}
}

53
crates/app/src/posts.rs Normal file
View file

@ -0,0 +1,53 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::prelude::*;
use leptos_router::components::A;
pub mod plot_demo;
#[derive(Clone)]
pub struct PostMeta {
pub slug: &'static str,
pub title: &'static str,
pub description: &'static str,
}
fn get_all_posts() -> Vec<PostMeta> {
vec![
// PostMeta {
// slug: "plot_demo",
// title: "Interactive Plot Demo",
// description: "A demonstration of the Plot component with interactive graphics rendering.",
// },
]
}
#[component]
pub fn Posts() -> impl IntoView {
let posts = get_all_posts();
view! {
<section>
<header>
<h1>"Posts"</h1>
</header>
"I haven't published anything yet. Try again later."
<section class="posts-list">
{posts
.into_iter()
.map(|post| {
view! {
<article class="post-preview">
<h2>
<A href=format!("/posts/{}", post.slug)>{post.title}</A>
</h2>
<p class="post-description">{post.description}</p>
</article>
}
})
.collect::<Vec<_>>()}
</section>
</section>
}
}

View file

@ -0,0 +1,37 @@
#![allow(unused)]
#![warn(unused_must_use)]
use crate::plot::{Camera, Element, Plot};
use leptos::prelude::*;
#[component]
pub fn PlotDemo() -> impl IntoView {
let camera = Camera::default();
// TODO: Create sample elements for the plot
let elements = vec![
Element {
// TODO: Initialize with sample data
},
Element {
// TODO: Initialize with sample data
},
];
view! {
<article class="plot-demo-post">
<header>
<h1>"Interactive Plot Demo"</h1>
<p class="post-description">
"A demonstration of the Plot component with interactive graphics rendering."
</p>
</header>
<section class="plot-section">
<h2>"Sample Plot"</h2>
<p>"This plot component will render interactive graphics:"</p>
<Plot camera=camera elements=elements />
<p>"TODO: Implement rendering logic, camera controls, and element interactions."</p>
</section>
</article>
}
}

122
crates/app/src/projects.rs Normal file
View file

@ -0,0 +1,122 @@
#![allow(unused)]
#![warn(unused_must_use)]
use indoc::indoc;
use leptos::prelude::*;
use leptos_router::components::A;
#[component]
pub fn Projects() -> impl IntoView {
view! {
<h1>"Personal projects"</h1>
<section class="projects-list">
<ProjectSummary image="/parva_1.png".into()>
<ProjectSummaryTitle slot>"LittleBigPlanet Computer"</ProjectSummaryTitle>
<ProjectSummaryMain slot>
<p>
"A 120Hz 24-bit computer built using logic gates inside the game "
<A href="https://en.wikipedia.org/wiki/LittleBigPlanet_3">
"LittleBigPlanet 3"
</A>" that I call \"Parva\"."
</p>
<p>
"There's been several iterations of it. Currently I'm trying to expand the memory from ~2kiB to 24kiB using "
<A href="https://www.youtube.com/watch?v=jzAx65my6N0">"this"</A>
" technique but I'm running into problems with non-determinism in how the game simulates the logic."
</p>
<p>
"In the process of building it, some other members of the LittleBigPlanet logic community and I did a lot of research into the game's mechanics. Some of the findings are documented "
<A href="https://github.com/Nomkid/littlebiglogic/blob/master/wiki/README.md">
"here"
</A>"."
</p>
<p>
"I will write up a blogpost about Parva and the other computers people have built in the game along with instructions on how to use them at some point."
</p>
</ProjectSummaryMain>
<ProjectSummaryLinks slot>
<a href="https://wiki.littlebigcomputer.com">"Wiki"</a>
<a href="https://littlebigcomputer.com">"Assembler"</a>
<a href="https://git.mascully.com/mascully/littlebigcomputer/src/branch/master/programs/parva_0.1">
"Example assembly programs"
</a>
</ProjectSummaryLinks>
</ProjectSummary>
<ProjectSummary image="/carplexity_1.png".into()>
<ProjectSummaryTitle slot>"Carplexity"</ProjectSummaryTitle>
<ProjectSummaryMain slot>
<p>
"Very early in development. A physics car game built using Bevy meant to be similar to Rocket League."
</p>
<p>
"360 ticks per second instead of 120, so less input lag, which is basically the only thing that matters."
</p>
</ProjectSummaryMain>
<ProjectSummaryLinks slot>
<A href="https://carplexity.com">"Website"</A>
</ProjectSummaryLinks>
</ProjectSummary>
<ProjectSummary image="/endolingual_1.png".into()>
<ProjectSummaryTitle slot>"Endolingual"</ProjectSummaryTitle>
<ProjectSummaryMain slot>
<p>"Compile-time string localization for Rust."</p>
// <pre>
// {
// indoc! {
// r#"
// let current_language = get_current_language();
// let my_string = translate!("Enter your password: ");
// println!("{}", my_string[current_language]);
// "#
// }
// }
// </pre>
// <p>
// "You can try this out by clicking the translate button at the top right of this website. All of the translation"
// </p>
</ProjectSummaryMain>
<ProjectSummaryLinks slot>
<A href="https://github.com/mascully/endolingual">"Repo"</A>
</ProjectSummaryLinks>
</ProjectSummary>
</section>
}
}
#[component]
pub fn ProjectSummary(
project_summary_title: ProjectSummaryTitle,
project_summary_main: ProjectSummaryMain,
project_summary_links: ProjectSummaryLinks,
image: String,
) -> impl IntoView {
view! {
<div class="project-grid">
<h2 class="project-title">{(project_summary_title.children)()}</h2>
<picture class="project-image">
<img
src=image
// srcset={image.concat(" x1")}
loading="lazy"
/>
</picture>
<div class="project-text">{(project_summary_main.children)()}</div>
<div class="project-links">{(project_summary_links.children)()}</div>
</div>
}
}
#[slot]
pub struct ProjectSummaryTitle {
children: ChildrenFn,
}
#[slot]
pub struct ProjectSummaryLinks {
children: ChildrenFn,
}
#[slot]
pub struct ProjectSummaryMain {
children: ChildrenFn,
}

View file

View file

@ -0,0 +1,31 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mJMEaHK66RMJKyQDAwIIAQENBAMEU30JORhgrFANgMzvqeaX/T1ZfA9FjLPVsJQx
MTrQ0m6oCBjqi4vSnMmB+AO9BhEGMnaJmrx1xf96eWNWv6OEBAwED6nxB+zxV1O2
ixh+LxMdJCXJfDDTb/DDNkna0nTfNRdjduqNYa5TfXQ56Jz4/srHwJQJg723nrXO
zeEc4wC0Hm1hc2N1bGx5IDxtYXJrdXNAbWFzY3VsbHkuY29tPojTBBMTCgA7FiEE
dGuDuzFFGDwlt8+KXne6BGBk3ZAFAmhyuukCGwMFCwkIBwICIgIGFQoJCAsCBBYC
AwECHgcCF4AACgkQXne6BGBk3ZBeMwIAjrmfwrh/3A0fFAKL8Ov/VzBdh9ErpLI8
MrD2GiFhGUNDZfSQ6pJsu/k9US0YM7HygNBVSs+VCiZIs91XIVcuewH/XOI2yKIG
y/e5r7G9cYm1i5C7tPfux4cI83gd14b368KlrxsrgyVh3RwC6mogB4FO+S0q9mMe
qzTlBXC/8Xxov7iXBGhyuukSCSskAwMCCAEBDQQDBJa9IbHmgiKRbm8r6JCVOCoi
4ZRI4ms90IaI0wctMlhMntnbJFWgW+/SoHuV9fBfIYbuN3L77d6wYA0xhi59UT85
qfTia06Kd6q6KJvHjCLN2nFYHECFU0Qz4Q8U4q+WD2nStYylI8BZixEih7dYzyjh
g7GmA+d7dNCBqiR6aiViAwEKCYi4BBgTCgAgFiEEdGuDuzFFGDwlt8+KXne6BGBk
3ZAFAmhyuukCGwwACgkQXne6BGBk3ZCk2AH6A/w7IvHl4qRZJm+8rjMAgRUf9YyT
XZO2ndkzOHFXsfWIRi/FKd7qP5MXokBRAfwe07zFy4f6rrhhb6XPCKPARAH+OAon
wgnE5CWj8wvUgZ0vjImAp4PlLvF1hhMN3Eb4ptT5ytfTo8Cssgu+Et6YpKoQh2uz
yTjBNEAgyvGT/ORQaLiTBGiEg5UTCSskAwMCCAEBDQQDBBSg23AZV79tF5upAgT5
pLoAXPkMLU5daER2JUElNX+bzSnEoDp6OyMgVawYsXePaCa7k9PUm6PPQU9L2bpi
NxwoUKhISZ+BID87trh2dRMSjta4JU+4SbR8uRz4OlJShjupb2M1MycFLHJ5Z4QS
0tz7DW5OUEZ/XuvpIl9sPy44iQF1BBgTCgAmFiEEdGuDuzFFGDwlt8+KXne6BGBk
3ZAFAmiEg5UCGwIFCQGYxmoAwQkQXne6BGBk3ZC2IAQZEwoAHRYhBEytVj5gfxiX
tnSKV5PKWBS2mBAcBQJohIOVAAoJEJPKWBS2mBActqQCAJSLMh4z9JgpY++JIQfg
pz7NRXOYNWKmGco6OD/msZps4ZoW9AyI8TMGwtioq0cPV7dztsm07JLQteezhIPd
fjAB/2CUnzTmVXzIJmP4LflwaASkVaDx6B5ZmUo+hrgDJ2ka/fSkUUowUboQOTvV
i8q5QxFYMmrG3GHTuLgUYkEPGceyMQIAmCz5SfRmEmKxHQTPhymIgbKT073vkmtI
uy3F+/YolAjKangwm5EPf+huOnddxUEbqlSoxoMM2Zs1rSMubsvnKQH/c+VBdyGR
AtTRFhprNN9qb6F9SRX5F/lOgCuReDRAKAW+FqFWQs1wgBDYm/Y/SA4h4OKzMt09
lUHKVwcI4MRRHw==
=fxmK
-----END PGP PUBLIC KEY BLOCK-----

5
crates/app/src/sigil.rs Normal file
View file

@ -0,0 +1,5 @@
use leptos::{prelude::*, svg::Svg};
pub fn website_icon() -> impl IntoView {
view! { "test" }
}

View file

@ -0,0 +1,175 @@
#![allow(unused)]
#![warn(unused_must_use)]
use leptos::html::Input;
use leptos::prelude::*;
#[cfg(feature = "hydrate")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "hydrate")]
use web_sys::{Storage, window};
use crate::command_line::CommandLine;
#[derive(Clone, Copy, PartialEq)]
pub enum Theme {
Light,
Dark,
Auto,
}
impl Theme {
fn as_str(&self) -> &'static str {
match self {
Theme::Light => "light",
Theme::Dark => "dark",
Theme::Auto => "auto",
}
}
fn from_str(s: &str) -> Self {
match s {
"light" => Theme::Light,
"dark" => Theme::Dark,
_ => Theme::Auto,
}
}
}
#[cfg(feature = "hydrate")]
fn get_stored_theme() -> Theme {
if let Some(window) = window()
&& let Ok(Some(storage)) = window.local_storage()
&& let Ok(Some(theme_str)) = storage.get_item("theme")
{
return Theme::from_str(&theme_str);
}
Theme::Auto
}
#[cfg(not(feature = "hydrate"))]
fn get_stored_theme() -> Theme {
Theme::Auto
}
#[cfg(feature = "hydrate")]
fn set_stored_theme(theme: Theme) {
if let Some(window) = window()
&& let Ok(Some(storage)) = window.local_storage()
{
let _ = storage.set_item("theme", theme.as_str());
}
}
#[cfg(not(feature = "hydrate"))]
fn set_stored_theme(_theme: Theme) {
// No-op on server side
}
#[cfg(feature = "hydrate")]
fn apply_theme(theme: Theme) {
if let Some(window) = window()
&& let Some(document) = window.document()
&& let Some(html) = document.document_element()
{
let class_list = html.class_list();
let _ = class_list.remove_2("light", "dark");
match theme {
Theme::Light => {
let _ = class_list.add_1("light");
}
Theme::Dark => {
let _ = class_list.add_1("dark");
}
Theme::Auto => {}
}
}
}
#[cfg(not(feature = "hydrate"))]
fn apply_theme(_theme: Theme) {
// No-op on server side
}
#[component]
pub fn ThemeSwitcher() -> impl IntoView {
let (current_theme, set_current_theme) = signal(Theme::Auto);
let (dropdown_open, set_dropdown_open) = signal(false);
// Load theme from localStorage and apply it on client side only
Effect::new(move |_| {
#[cfg(feature = "hydrate")]
{
let stored_theme = get_stored_theme();
set_current_theme.set(stored_theme);
apply_theme(stored_theme);
}
});
// Apply theme when it changes (client side only)
Effect::new(move |_| {
#[cfg(feature = "hydrate")]
{
let theme = current_theme.get();
apply_theme(theme);
}
});
let toggle_dropdown = move |_| {
set_dropdown_open.update(|open| *open = !*open);
};
let select_theme = move |theme: Theme| {
set_current_theme.set(theme);
#[cfg(feature = "hydrate")]
set_stored_theme(theme);
set_dropdown_open.set(false);
};
let theme_icon = move || match current_theme.get() {
Theme::Light => "☀️",
Theme::Dark => "🌙",
Theme::Auto => "🌓",
};
view! {
<div class="theme-switcher">
<button class="theme-button" on:click=toggle_dropdown>
{theme_icon}
</button>
<div class="theme-dropdown" class:hidden=move || !dropdown_open.get()>
<button
class="theme-option"
class:active=move || current_theme.get() == Theme::Light
on:click=move |_| select_theme(Theme::Light)
>
"Light"
</button>
<button
class="theme-option"
class:active=move || current_theme.get() == Theme::Dark
on:click=move |_| select_theme(Theme::Dark)
>
"Dark"
</button>
<button
class="theme-option"
class:active=move || current_theme.get() == Theme::Auto
on:click=move |_| select_theme(Theme::Auto)
>
"Auto"
</button>
</div>
</div>
}
}
#[component]
pub fn SiteHeader() -> impl IntoView {
view! {
<header class="site-header">
<CommandLine />
<ThemeSwitcher />
</header>
}
}

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,183 @@
use leptos::prelude::*;
use log::*;
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{HtmlCanvasElement, WebGl2RenderingContext as GL, WebGlProgram, WebGlShader};
// Vertex shader source
const VERTEX_SHADER_SRC: &str = r#"#version 300 es
precision mediump float;
void main() {
// Define the vertices of a triangle
const vec2 positions[3] = vec2[3](
vec2(0.0, 0.5),
vec2(-0.5, -0.5),
vec2(0.5, -0.5)
);
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}
"#;
// Fragment shader source
const FRAGMENT_SHADER_SRC: &str = r#"#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
"#;
// Implementation for WASM/browser targets
fn set_up_wgpu_triangle(canvas_id: &str) {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
// Check if document is ready
if let Some(element) = document.get_element_by_id(canvas_id) {
let canvas = element.dyn_into::<HtmlCanvasElement>().unwrap();
if let Err(e) = set_up_webgl(&canvas) {
error!("WebGL setup failed: {:?}", e);
} else {
info!("WebGL triangle renderer initialized");
}
}
}
// Sets up WebGL and renders a triangle
fn set_up_webgl(canvas: &HtmlCanvasElement) -> Result<(), JsValue> {
// Get WebGL context
let context = canvas.get_context("webgl2")?.unwrap().dyn_into::<GL>()?;
// Set clear color and viewport
context.clear_color(0.1, 0.2, 0.3, 1.0);
context.clear(GL::COLOR_BUFFER_BIT);
// Create shaders
let vert_shader = compile_shader(&context, GL::VERTEX_SHADER, VERTEX_SHADER_SRC)?;
let frag_shader = compile_shader(&context, GL::FRAGMENT_SHADER, FRAGMENT_SHADER_SRC)?;
// Create program
let program = link_program(&context, &vert_shader, &frag_shader)?;
context.use_program(Some(&program));
// Create empty vertex array object (required for core profile)
let vao = context
.create_vertex_array()
.ok_or("Could not create vertex array object")?;
context.bind_vertex_array(Some(&vao));
// Start rendering
render_loop(context);
Ok(())
}
// Compiles a shader from source
fn compile_shader(context: &GL, shader_type: u32, source: &str) -> Result<WebGlShader, String> {
let shader = context
.create_shader(shader_type)
.ok_or_else(|| String::from("Unable to create shader object"))?;
context.shader_source(&shader, source);
context.compile_shader(&shader);
if context
.get_shader_parameter(&shader, GL::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(shader)
} else {
Err(context
.get_shader_info_log(&shader)
.unwrap_or_else(|| String::from("Unknown error creating shader")))
}
}
// Links a shader program
fn link_program(
context: &GL,
vert_shader: &WebGlShader,
frag_shader: &WebGlShader,
) -> Result<WebGlProgram, String> {
let program = context
.create_program()
.ok_or_else(|| String::from("Unable to create shader program"))?;
context.attach_shader(&program, vert_shader);
context.attach_shader(&program, frag_shader);
context.link_program(&program);
if context
.get_program_parameter(&program, GL::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
Ok(program)
} else {
Err(context
.get_program_info_log(&program)
.unwrap_or_else(|| String::from("Unknown error creating program")))
}
}
// Sets up an animation loop
fn render_loop(context: GL) {
// Create a render function
let f = Rc::new(RefCell::new(None));
let g = f.clone();
*g.borrow_mut() = Some(Closure::new(move || {
// Clear the canvas
context.clear_color(0.1, 0.2, 0.3, 1.0);
context.clear(GL::COLOR_BUFFER_BIT);
// Draw the triangle (3 vertices starting at index 0)
context.draw_arrays(GL::TRIANGLES, 0, 3);
// Request next frame
request_animation_frame(f.borrow().as_ref().unwrap());
}));
// Start the render loop
request_animation_frame(g.borrow().as_ref().unwrap());
}
// Helper function to request animation frame
fn request_animation_frame(f: &Closure<dyn FnMut()>) {
web_sys::window()
.unwrap()
.request_animation_frame(f.as_ref().unchecked_ref())
.expect("should register `requestAnimationFrame` OK");
}
#[component]
pub fn WgpuTriangle() -> impl IntoView {
// Use a canvas with a fixed ID
let canvas_id = "triangle-canvas";
// create_effect(move |_| {
// // This will only run client-side code
// if cfg!(target_arch = "wasm32") {
// set_up_wgpu_triangle(canvas_id);
// }
// });
view! {
<div class="triangle-container" style="width: 100%; height: 100%">
<canvas
id=canvas_id
width="800"
height="600"
style="width: 100%; height: 100%; border: 1px solid #ccc"
></canvas>
</div>
}
}