🚀 Init public commit
This commit is contained in:
commit
62fbf6d17c
42 changed files with 7433 additions and 0 deletions
26
crates/app/Cargo.toml
Normal file
26
crates/app/Cargo.toml
Normal 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
78
crates/app/src/article.rs
Normal 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>
|
||||
}
|
||||
}
|
6
crates/app/src/command_line.rs
Normal file
6
crates/app/src/command_line.rs
Normal 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
30
crates/app/src/contact.rs
Normal 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
154
crates/app/src/lib.rs
Normal 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
43
crates/app/src/plot.rs
Normal 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
53
crates/app/src/posts.rs
Normal 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>
|
||||
}
|
||||
}
|
37
crates/app/src/posts/plot_demo.rs
Normal file
37
crates/app/src/posts/plot_demo.rs
Normal 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
122
crates/app/src/projects.rs
Normal 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,
|
||||
}
|
0
crates/app/src/security/bug_bounties.rs
Normal file
0
crates/app/src/security/bug_bounties.rs
Normal file
31
crates/app/src/security/pgp_master.txt
Normal file
31
crates/app/src/security/pgp_master.txt
Normal 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
5
crates/app/src/sigil.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use leptos::{prelude::*, svg::Svg};
|
||||
|
||||
pub fn website_icon() -> impl IntoView {
|
||||
view! { "test" }
|
||||
}
|
175
crates/app/src/theme_switcher.rs
Normal file
175
crates/app/src/theme_switcher.rs
Normal 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>
|
||||
}
|
||||
}
|
1
crates/app/src/version.rs
Normal file
1
crates/app/src/version.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
183
crates/app/src/wgpu_renderer.rs
Normal file
183
crates/app/src/wgpu_renderer.rs
Normal 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>
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue