🚀 Init public commit
3
.cargo/config.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[build]
|
||||||
|
target-dir = "target/cargo"
|
||||||
|
rustflags = ["-A", "unused_imports"]
|
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target/
|
5528
Cargo.lock
generated
Normal file
78
Cargo.toml
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/*"]
|
||||||
|
resolver = "3"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
name = "mascully_website"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[workspace.metadata.crane]
|
||||||
|
name = "mascully_website"
|
||||||
|
|
||||||
|
[[workspace.metadata.leptos]]
|
||||||
|
name = "mascully_website"
|
||||||
|
bin-package = "mascully_website_server"
|
||||||
|
lib-package = "mascully_website_frontend"
|
||||||
|
assets-dir = "crates/frontend/assets"
|
||||||
|
style-file = "crates/frontend/assets/css/styles.css"
|
||||||
|
site-root = "target/site/mascully_website"
|
||||||
|
server-fn-prefix = "/custom/prefix"
|
||||||
|
# disable-server-fn-hash = true
|
||||||
|
# server-fn-mod-path = true
|
||||||
|
site-addr = "127.0.0.1:3002"
|
||||||
|
# hash-files = true
|
||||||
|
# disable-erase-components = true
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
async-trait = "0.1.74"
|
||||||
|
axum = "0.8.4"
|
||||||
|
comrak = "0.29.0"
|
||||||
|
console_error_panic_hook = { version = "0.1.7" }
|
||||||
|
console_log = { version = "1.0" }
|
||||||
|
dotenvy = { version = "0.15.6" }
|
||||||
|
encase = { version = "0.11.1", features = ["glam"] }
|
||||||
|
futures = { version = "0.3.31" }
|
||||||
|
glam = { version = "0.30.0" }
|
||||||
|
gloo = { version = "0.11.0", features = ["timers"] }
|
||||||
|
gloo-net = { version = "0.6", features = ["http"] }
|
||||||
|
js-sys = "0.3.77"
|
||||||
|
leptos = { version = "0.8.4", features = ["nightly"] }
|
||||||
|
leptos_axum = "0.8.4"
|
||||||
|
leptos_dom = { version = "0.8.4" }
|
||||||
|
leptos_meta = { version = "0.8.4" }
|
||||||
|
leptos_router = { version = "0.8.4" }
|
||||||
|
log = "0.4.17"
|
||||||
|
mime_guess = "2.0.4"
|
||||||
|
reqwest = { version = "0.12.8", features = [
|
||||||
|
"json",
|
||||||
|
"rustls-tls",
|
||||||
|
], default-features = false }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0.128" }
|
||||||
|
simple_logger = { version = "5.0.0" }
|
||||||
|
tokio = { version = "1.33.0", features = ["full"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] }
|
||||||
|
wasm-bindgen = { version = "=0.2.100" }
|
||||||
|
wasm-bindgen-futures = "0.4.50"
|
||||||
|
web-sys = { version = "0.3.77", features = [
|
||||||
|
"HtmlCanvasElement",
|
||||||
|
"Window",
|
||||||
|
"Document",
|
||||||
|
"Element",
|
||||||
|
"DomRect",
|
||||||
|
"Performance",
|
||||||
|
"WebGl2RenderingContext",
|
||||||
|
"WebGlBuffer",
|
||||||
|
"WebGlVertexArrayObject",
|
||||||
|
"WebGlProgram",
|
||||||
|
"WebGlShader",
|
||||||
|
"WebGlUniformLocation",
|
||||||
|
] }
|
||||||
|
wgpu = { version = "26.0.1", features = ["webgl"] }
|
||||||
|
tower = { version = "0.5.2", features = ["full"] }
|
||||||
|
tower-http = { version = "0.6.6", features = ["full"] }
|
||||||
|
indoc = "2.0.6"
|
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Markus Scully
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
39
README.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Mascully Website
|
||||||
|
|
||||||
|
My personal website. Hosted at [mascully.com](https://mascully.com). The currently hosted version may not always exactly reflect the contents of this repo.
|
||||||
|
|
||||||
|
Built using [Leptos](https://leptos.dev/) with [Axum](https://github.com/tokio-rs/axum).
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
### Nix
|
||||||
|
|
||||||
|
Run directly with
|
||||||
|
```bash
|
||||||
|
nix develop -c cargo-leptos serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Develop with
|
||||||
|
```bash
|
||||||
|
nix develop
|
||||||
|
cargo-leptos watch
|
||||||
|
```
|
||||||
|
|
||||||
|
I'm still working on getting the flake working, so for now you can't use `nix run`. I also haven't figured out how to bundle it as a standalone binary yet.
|
||||||
|
|
||||||
|
### Other systems
|
||||||
|
|
||||||
|
- Install Rust nightly.
|
||||||
|
- Install [Cargo Leptos](https://github.com/leptos-rs/cargo-leptos).
|
||||||
|
- `cargo-leptos serve`
|
||||||
|
|
||||||
|
## Code structure
|
||||||
|
|
||||||
|
- `crates/app`: The actual website.
|
||||||
|
- `crates/frontend`: Images, fonts, CSS.
|
||||||
|
- `crates/server`: A minimal Axum server for serving pages.
|
||||||
|
- `flake.nix`: This doesn't work; ignore it.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
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
|
@ -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
|
@ -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
|
@ -0,0 +1 @@
|
||||||
|
|
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>
|
||||||
|
}
|
||||||
|
}
|
15
crates/frontend/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "mascully_website_frontend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mascully_website_app = { path = "../app", default-features = false, features = ["hydrate"] }
|
||||||
|
leptos = { workspace = true, features = [ "hydrate" ] }
|
||||||
|
log.workspace = true
|
||||||
|
wasm-bindgen.workspace = true
|
||||||
|
console_log.workspace = true
|
||||||
|
console_error_panic_hook.workspace = true
|
BIN
crates/frontend/assets/carplexity_1.png
Normal file
After Width: | Height: | Size: 597 KiB |
443
crates/frontend/assets/css/styles.css
Normal file
|
@ -0,0 +1,443 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "Space Grotesk";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("/fonts/space_grotesk_variable.woff2") format("woff2-variations");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "IBM Plex Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("/fonts/ibm_plex_sans_variable.ttf") format("truetype-variations");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Zed Mono";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("/fonts/zed_mono_variable.woff2") format("woff2-variations");
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--body-bg: oklch(15% 0.02 240); /* darker background */
|
||||||
|
--main-bg: oklch(20% 0 0 / 0.7); /* more transparency */
|
||||||
|
--text-color: oklch(80% 0 0);
|
||||||
|
--link-color: oklch(70% 0.15 240); /* lighter blue for links */
|
||||||
|
--link-hover-color: oklch(80% 0.15 240); /* blue */
|
||||||
|
--border-color: oklch(40% 0 0);
|
||||||
|
--post-description-color: oklch(60% 0 0);
|
||||||
|
--header-bg: oklch(15% 0.02 240); /* same as body background */
|
||||||
|
--input-bg: oklch(25% 0 0);
|
||||||
|
--button-bg: oklch(30% 0 0);
|
||||||
|
--button-hover-bg: oklch(35% 0 0);
|
||||||
|
--bg-tile: url("/tile_1_dark.png");
|
||||||
|
--bg-color: oklch(20% 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light {
|
||||||
|
--navbar-opacity: 0.5;
|
||||||
|
--navbar-opacity-hover: 1;
|
||||||
|
--transition-time: 0.3s;
|
||||||
|
--body-bg: oklch(98% 0 0); /* white */
|
||||||
|
--main-bg: oklch(100% 0 0 / 0.85); /* white with transparency */
|
||||||
|
--text-color: oklch(0% 0 0); /* black */
|
||||||
|
--link-color: oklch(50% 0.15 240); /* blue for links */
|
||||||
|
--link-hover-color: oklch(60% 0.15 240); /* blue */
|
||||||
|
--border-color: oklch(90% 0 0);
|
||||||
|
--post-description-color: oklch(60% 0 0);
|
||||||
|
--header-bg: oklch(98% 0 0); /* same as body background */
|
||||||
|
--input-bg: oklch(95% 0 0);
|
||||||
|
--button-bg: oklch(90% 0 0);
|
||||||
|
--button-hover-bg: oklch(85% 0 0);
|
||||||
|
--bg-tile: url("/tile_1_light.png");
|
||||||
|
--bg-color: oklch(100% 0 0); /* white background */
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
background-image: var(--bg-tile);
|
||||||
|
background-repeat: repeat;
|
||||||
|
color: var(--text-color);
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: "IBM Plex Sans", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 1rem 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: xx-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
opacity: var(--navbar-opacity);
|
||||||
|
transition: opacity var(--transition-time) ease;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-rows: max-content;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
padding: 1rem;
|
||||||
|
align-content: start;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar:hover {
|
||||||
|
opacity: var(--navbar-opacity-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav ul {
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--link-color);
|
||||||
|
transition: color var(--transition-time) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--link-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
background-color: var(--main-bg);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-family: "Space Grotesk", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Posts page styling */
|
||||||
|
.posts-page {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-page header {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview h2 {
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview h2 a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: color var(--transition-time) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview h2 a:hover {
|
||||||
|
color: var(--link-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-description {
|
||||||
|
color: var(--post-description-color);
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Plot component styling */
|
||||||
|
.plot-container {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--main-bg);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-canvas {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background-color: oklch(100% 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-demo-post {
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-demo-post header {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-section h2 {
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header styling */
|
||||||
|
.site-header {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
background-color: var(--header-bg);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input {
|
||||||
|
font-family: "Zed Mono", monospace;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input::placeholder {
|
||||||
|
color: var(--post-description-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switcher {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-button {
|
||||||
|
background-color: var(--button-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--transition-time) ease;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-button:hover {
|
||||||
|
background-color: var(--button-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background-color: var(--header-bg);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
min-width: 80px;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dropdown.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: background-color var(--transition-time) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option:hover {
|
||||||
|
background-color: var(--button-hover-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-option.active {
|
||||||
|
background-color: var(--button-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Projects page styling */
|
||||||
|
.projects-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project_listing {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--main-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 2fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-image {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-text {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-links {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 3;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
grid-auto-flow: column;
|
||||||
|
justify-items: center;
|
||||||
|
align-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav ul {
|
||||||
|
grid-auto-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot-canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switcher {
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-image {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-text {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-links {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 4;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
BIN
crates/frontend/assets/endolingual_1.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
crates/frontend/assets/fonts/ibm_plex_sans_variable.ttf
Normal file
BIN
crates/frontend/assets/fonts/space_grotesk_variable.woff2
Normal file
BIN
crates/frontend/assets/fonts/zed_mono_variable.woff2
Normal file
BIN
crates/frontend/assets/icon_1.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
crates/frontend/assets/icon_2.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
crates/frontend/assets/parva_1.png
Normal file
After Width: | Height: | Size: 5.1 MiB |
BIN
crates/frontend/assets/tile_1_dark.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
crates/frontend/assets/tile_1_light.png
Normal file
After Width: | Height: | Size: 79 KiB |
11
crates/frontend/src/lib.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use mascully_website_app::App;
|
||||||
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn hydrate() {
|
||||||
|
_ = console_log::init_with_level(log::Level::Debug);
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
leptos::mount::hydrate_body(App);
|
||||||
|
}
|
11
crates/grid-shader/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "grid-shader"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
env_logger = "0.11.6"
|
||||||
|
log = "0.4.25"
|
||||||
|
wgpu = "26.0.1"
|
||||||
|
winit = "0.30.11"
|
32
crates/grid-shader/src/lib.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// use winit::{
|
||||||
|
// event::*,
|
||||||
|
// event_loop::EventLoop,
|
||||||
|
// keyboard::{KeyCode, PhysicalKey},
|
||||||
|
// };
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
pub fn run() {
|
||||||
|
// env_logger::init();
|
||||||
|
// let event_loop = EventLoop::new().unwrap();
|
||||||
|
// let window = WindowBuilder::new().build(&event_loop).unwrap();
|
||||||
|
|
||||||
|
// let _ = event_loop.run(move |event, control_flow| match event {
|
||||||
|
// Event::WindowEvent {
|
||||||
|
// ref event,
|
||||||
|
// window_id,
|
||||||
|
// } if window_id == window.id() => match event {
|
||||||
|
// WindowEvent::CloseRequested
|
||||||
|
// | WindowEvent::KeyboardInput {
|
||||||
|
// event:
|
||||||
|
// KeyEvent {
|
||||||
|
// state: ElementState::Pressed,
|
||||||
|
// physical_key: PhysicalKey::Code(KeyCode::Escape),
|
||||||
|
// ..
|
||||||
|
// },
|
||||||
|
// ..
|
||||||
|
// } => control_flow.exit(),
|
||||||
|
// _ => {}
|
||||||
|
// },
|
||||||
|
// _ => {}
|
||||||
|
// });
|
||||||
|
}
|
27
crates/server/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "mascully_website_server"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mascully_website_app = { path = "../app", default-features = false, features = [
|
||||||
|
"ssr",
|
||||||
|
] }
|
||||||
|
leptos = { workspace = true, features = ["ssr"] }
|
||||||
|
leptos_meta.workspace = true
|
||||||
|
leptos_router.workspace = true
|
||||||
|
leptos_dom.workspace = true
|
||||||
|
leptos_axum.workspace = true
|
||||||
|
gloo-net.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
simple_logger.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
dotenvy.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tower.workspace = true
|
||||||
|
tower-http.workspace = true
|
23
crates/server/src/main.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use leptos::{logging::log, prelude::*};
|
||||||
|
use leptos_axum::{LeptosRoutes, generate_route_list};
|
||||||
|
use mascully_website_app::{App, shell};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
let conf = get_configuration(None).unwrap();
|
||||||
|
let addr = conf.leptos_options.site_addr;
|
||||||
|
let leptos_options = conf.leptos_options;
|
||||||
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.leptos_routes(&leptos_options, routes, {
|
||||||
|
let leptos_options = leptos_options.clone();
|
||||||
|
move || shell(leptos_options.clone())
|
||||||
|
})
|
||||||
|
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||||
|
.with_state(leptos_options);
|
||||||
|
|
||||||
|
log!("listening on http://{}", &addr);
|
||||||
|
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||||
|
axum::serve(listener, app.into_make_service()).await.unwrap();
|
||||||
|
}
|
116
flake.lock
generated
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752946753,
|
||||||
|
"narHash": "sha256-g5uP3jIj+STUcfTJDKYopxnSijs2agRg13H0SGL5iE4=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "544d09fecc8c2338542c57f3f742f1a0c8c71e13",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752561972,
|
||||||
|
"narHash": "sha256-pPqES/udciKmKo422mfwRQ3YzjUCVyCTOsgZYA1xh+g=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "d17ca03c15660ecb8e5a01ca34e441f594feec62",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1751413152,
|
||||||
|
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752446735,
|
||||||
|
"narHash": "sha256-Nz2vtUEaRB/UjvPfuhHpez060P/4mvGpXW4JCDIboA4=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a421ac6595024edcfbb1ef950a3712b89161c359",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1751159883,
|
||||||
|
"narHash": "sha256-urW/Ylk9FIfvXfliA1ywh75yszAbiTEVgpPeinFyVZo=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"rev": "14a40a1d7fb9afa4739275ac642ed7301a9ba1ab",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752475356,
|
||||||
|
"narHash": "sha256-V2nHrCJ0/Pv30j8NWJ4GfDlaNzfkOdYI0jS69GdVpq8=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "e10d64eb402a25a32d9f1ef60cacc89d82a01b85",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
137
flake.nix
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
description = "Flake for building mascully with Crane";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||||
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
|
fenix = {
|
||||||
|
url = "github:nix-community/fenix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
crane.url = "github:ipetkov/crane";
|
||||||
|
};
|
||||||
|
|
||||||
|
nixConfig = { };
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs@{
|
||||||
|
flake-parts,
|
||||||
|
nixpkgs,
|
||||||
|
fenix,
|
||||||
|
crane,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
debug = true;
|
||||||
|
systems = nixpkgs.lib.systems.flakeExposed;
|
||||||
|
|
||||||
|
perSystem =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
self',
|
||||||
|
inputs',
|
||||||
|
pkgs,
|
||||||
|
system,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config.allowUnfree = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
rustToolchain = fenix.packages.${system}.combine ([
|
||||||
|
(fenix.packages.${system}.complete.withComponents [
|
||||||
|
"cargo"
|
||||||
|
"clippy"
|
||||||
|
"rust-src"
|
||||||
|
"rustc"
|
||||||
|
"rustfmt"
|
||||||
|
])
|
||||||
|
fenix.packages.${system}.complete.rust-analyzer
|
||||||
|
fenix.packages.${system}.targets.wasm32-unknown-unknown.latest.rust-std
|
||||||
|
]);
|
||||||
|
|
||||||
|
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||||
|
|
||||||
|
# extraPackages = with pkgs; [
|
||||||
|
# cargo-leptos
|
||||||
|
# binaryen
|
||||||
|
# leptosfmt
|
||||||
|
# ];
|
||||||
|
|
||||||
|
commonArgs = {
|
||||||
|
# src = craneLib.cleanCargoSource ./.;
|
||||||
|
src = ./.;
|
||||||
|
strictDeps = true;
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
cargo-leptos
|
||||||
|
leptosfmt
|
||||||
|
binaryen
|
||||||
|
];
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
cargo-leptos
|
||||||
|
leptosfmt
|
||||||
|
binaryen
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
leptosOptions = builtins.elemAt (builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.metadata.leptos 0;
|
||||||
|
|
||||||
|
|
||||||
|
# --profile=${leptosOptions.lib-profile-release}
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly (
|
||||||
|
commonArgs
|
||||||
|
// {
|
||||||
|
# if work is duplicated by the `mascullyWebsiteServer` package, update these
|
||||||
|
# commands from the logs of `cargo leptos build --release -vvv`
|
||||||
|
buildPhaseCargoCommand = ''
|
||||||
|
cargo build --package=${leptosOptions.lib-package} --lib --target-dir=/build/source/target/front --target=wasm32-unknown-unknown --no-default-features
|
||||||
|
cargo build --package=${leptosOptions.bin-package} --no-default-features --release
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
mascullyWebsiteServer = craneLib.buildPackage (
|
||||||
|
commonArgs
|
||||||
|
// {
|
||||||
|
pname = "mascully_website_server";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
nativeBuildInputs = commonArgs.nativeBuildInputs ++ [ pkgs.makeWrapper ];
|
||||||
|
|
||||||
|
buildPhaseCargoCommand = ''
|
||||||
|
RUST_BACKTRACE=full cargo-leptos build --release -vvv
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhaseCommand = ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
ls target/
|
||||||
|
pwd
|
||||||
|
cp target/server/release/mascully_website_server $out/bin/
|
||||||
|
# cp target/release/hash.txt $out/bin/
|
||||||
|
cp -r target/site $out/bin/
|
||||||
|
wrapProgram $out/bin/mascully_website_server --set LEPTOS_SITE_ROOT $out/bin/site
|
||||||
|
'';
|
||||||
|
|
||||||
|
doCheck = false;
|
||||||
|
doNotPostBuildInstallCargoBinaries = true;
|
||||||
|
cargoArtifacts = cargoArtifacts;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = rec {
|
||||||
|
inherit mascullyWebsiteServer;
|
||||||
|
default = mascullyWebsiteServer;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = craneLib.devShell {
|
||||||
|
inputsFrom = [ mascullyWebsiteServer ];
|
||||||
|
# packages = extraPackages;
|
||||||
|
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
2
rust-analyzer.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[rustfmt]
|
||||||
|
overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"]
|
2
rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
edition = "2024"
|
||||||
|
max_width = 120
|