mirror of
https://github.com/RGBCube/Site
synced 2025-08-01 13:37:49 +00:00
Add routes, minification, asset serving (BROKEN), error handlers
This commit is contained in:
parent
c1af80979d
commit
c3d45b6ba4
20 changed files with 926 additions and 29 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,14 +1,19 @@
|
||||||
*
|
*
|
||||||
|
|
||||||
!src/
|
!src/
|
||||||
|
!src/errors/
|
||||||
|
!src/errors/internal_server_error/
|
||||||
|
!src/errors/not_found/
|
||||||
!src/page/
|
!src/page/
|
||||||
!src/page/cube/
|
!src/page/cube/
|
||||||
!src/routes/
|
!src/routes/
|
||||||
!src/routes/_404/
|
|
||||||
!src/routes/index/
|
!src/routes/index/
|
||||||
|
|
||||||
!.gitignore
|
!.gitignore
|
||||||
|
|
||||||
|
!*.gif
|
||||||
|
!*.woff2
|
||||||
|
|
||||||
!*.css
|
!*.css
|
||||||
!*.js
|
!*.js
|
||||||
!*.rs
|
!*.rs
|
||||||
|
|
12
build.rs
Normal file
12
build.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
println!("cargo:rerun-if-changed=src");
|
||||||
|
println!("cargo:rerun-if-changed=src.tar");
|
||||||
|
|
||||||
|
Command::new("tar")
|
||||||
|
.args(["-czf", "src.tar", "src"])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to create tar archive");
|
||||||
|
}
|
94
src/asset.rs
Normal file
94
src/asset.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
use maud::{
|
||||||
|
html,
|
||||||
|
Markup,
|
||||||
|
PreEscaped,
|
||||||
|
Render,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::minify;
|
||||||
|
|
||||||
|
pub fn extension_of(path: &str) -> Option<&str> {
|
||||||
|
path.rsplit_once(".").map(|(_base, extension)| extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Js {
|
||||||
|
Shared(&'static str),
|
||||||
|
Owned(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Js {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
match self {
|
||||||
|
Self::Shared(path) => {
|
||||||
|
html! {
|
||||||
|
script src=(format!("/assets/{}", minify::insert_min(path))) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::Owned(content) => {
|
||||||
|
html! {
|
||||||
|
script {
|
||||||
|
(minify::js(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod js {
|
||||||
|
macro_rules! owned {
|
||||||
|
($path:literal) => {
|
||||||
|
crate::asset::Js::Owned(::embed::string!($path))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use owned;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Css {
|
||||||
|
Shared(&'static str),
|
||||||
|
Owned(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Css {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
match self {
|
||||||
|
Self::Shared(path) => {
|
||||||
|
html! {
|
||||||
|
link rel="stylesheet" type="text/css" href=(format!("/assets/{}", minify::insert_min(path)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::Owned(content) => {
|
||||||
|
html! {
|
||||||
|
style {
|
||||||
|
(minify::css(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod css {
|
||||||
|
macro_rules! owned {
|
||||||
|
($path:literal) => {
|
||||||
|
crate::asset::Css::Owned(::embed::string!($path).to_string())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use owned;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct File(pub &'static str);
|
||||||
|
|
||||||
|
impl Render for File {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
PreEscaped(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for File {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
format!("/assets/{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
41
src/errors/internal_server_error/mod.rs
Normal file
41
src/errors/internal_server_error/mod.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use std::array;
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
dev::ServiceResponse,
|
||||||
|
middleware::ErrorHandlerResponse,
|
||||||
|
};
|
||||||
|
use maud::html;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
asset,
|
||||||
|
page::cube,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn handler<B: 'static>(
|
||||||
|
response: ServiceResponse<B>,
|
||||||
|
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||||
|
let (request, response) = response.into_parts();
|
||||||
|
|
||||||
|
let response = response.set_body(
|
||||||
|
cube::create(
|
||||||
|
asset::Css::Shared("not-found.css"),
|
||||||
|
array::from_fn(|_| {
|
||||||
|
(html! {
|
||||||
|
div class="frame" { "error" }
|
||||||
|
div class="square black" {}
|
||||||
|
div class="square red" {}
|
||||||
|
div class="square red" {}
|
||||||
|
div class="square black" {}
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(ErrorHandlerResponse::Response(
|
||||||
|
ServiceResponse::new(request, response)
|
||||||
|
.map_into_boxed_body()
|
||||||
|
.map_into_right_body(),
|
||||||
|
))
|
||||||
|
}
|
16
src/errors/mod.rs
Normal file
16
src/errors/mod.rs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
mod internal_server_error;
|
||||||
|
mod not_found;
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
http::StatusCode,
|
||||||
|
middleware::ErrorHandlers,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn handler<B: 'static>() -> ErrorHandlers<B> {
|
||||||
|
ErrorHandlers::new()
|
||||||
|
.handler(StatusCode::NOT_FOUND, not_found::handler)
|
||||||
|
.handler(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
internal_server_error::handler,
|
||||||
|
)
|
||||||
|
}
|
41
src/errors/not_found/mod.rs
Normal file
41
src/errors/not_found/mod.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use std::array;
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
dev::ServiceResponse,
|
||||||
|
middleware::ErrorHandlerResponse,
|
||||||
|
};
|
||||||
|
use maud::html;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
asset,
|
||||||
|
page::cube,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn handler<B: 'static>(
|
||||||
|
response: ServiceResponse<B>,
|
||||||
|
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||||
|
let (request, response) = response.into_parts();
|
||||||
|
|
||||||
|
let response = response.set_body(
|
||||||
|
cube::create(
|
||||||
|
asset::Css::Shared("not-found.css"),
|
||||||
|
array::from_fn(|_| {
|
||||||
|
(html! {
|
||||||
|
div class="frame" { "404" }
|
||||||
|
div class="square black" {}
|
||||||
|
div class="square magenta" {}
|
||||||
|
div class="square magenta" {}
|
||||||
|
div class="square black" {}
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(ErrorHandlerResponse::Response(
|
||||||
|
ServiceResponse::new(request, response)
|
||||||
|
.map_into_boxed_body()
|
||||||
|
.map_into_right_body(),
|
||||||
|
))
|
||||||
|
}
|
33
src/errors/not_found/not-found.css
Normal file
33
src/errors/not_found/not-found.css
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
.face {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
box-shadow: 0 0 10px #AAAAAA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magenta {
|
||||||
|
background-color: magenta;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
color: black;
|
||||||
|
}
|
23
src/main.rs
23
src/main.rs
|
@ -1,10 +1,14 @@
|
||||||
#![feature(lazy_cell)]
|
#![feature(lazy_cell)]
|
||||||
|
|
||||||
|
mod asset;
|
||||||
|
mod errors;
|
||||||
|
mod minify;
|
||||||
mod page;
|
mod page;
|
||||||
// mod routes;
|
mod routes;
|
||||||
|
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
main as async_main,
|
main as async_main,
|
||||||
|
middleware,
|
||||||
App,
|
App,
|
||||||
HttpServer,
|
HttpServer,
|
||||||
};
|
};
|
||||||
|
@ -31,12 +35,17 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.target(env_logger::Target::Stdout)
|
.target(env_logger::Target::Stdout)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
HttpServer::new(App::new) //|| App::new().route(routes::index))
|
HttpServer::new(|| {
|
||||||
.bind(("0.0.0.0", args.port))
|
App::new()
|
||||||
.with_context(|| format!("Failed to bind to 0.0.0.0:{port}", port = args.port))?
|
.wrap(middleware::Logger::default())
|
||||||
.run()
|
.wrap(errors::handler())
|
||||||
.await
|
.service(routes::handler())
|
||||||
.with_context(|| "Failed to run HttpServer")?;
|
})
|
||||||
|
.bind(("0.0.0.0", args.port))
|
||||||
|
.with_context(|| format!("Failed to bind to 0.0.0.0:{}", args.port))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
.with_context(|| "Failed to run HttpServer")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
73
src/minify.rs
Normal file
73
src/minify.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
use std::{
|
||||||
|
env::temp_dir,
|
||||||
|
fs::File,
|
||||||
|
hash::{
|
||||||
|
BuildHasher,
|
||||||
|
RandomState,
|
||||||
|
},
|
||||||
|
io::Write,
|
||||||
|
};
|
||||||
|
|
||||||
|
use minify_js::{
|
||||||
|
Session,
|
||||||
|
TopLevelMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::asset::extension_of;
|
||||||
|
|
||||||
|
pub const MINIFIABLE: &[&str] = &[".js", ".css"];
|
||||||
|
|
||||||
|
pub fn is_minifiable(path: &str) -> bool {
|
||||||
|
MINIFIABLE.iter().any(|extension| path.ends_with(extension))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_min(path: &str) -> String {
|
||||||
|
match path.rsplit_once(".") {
|
||||||
|
Some((base, extension)) => format!("{base}.min.{extension}"),
|
||||||
|
None => format!("{path}.min"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generic(path: &str, content: &[u8]) -> Vec<u8> {
|
||||||
|
match extension_of(path) {
|
||||||
|
Some("js") => js(&String::from_utf8(content.to_vec()).unwrap()).into_bytes(),
|
||||||
|
Some("css") => css(&String::from_utf8(content.to_vec()).unwrap()).into_bytes(),
|
||||||
|
_ => content.to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn js(content: &str) -> String {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
minify_js::minify(
|
||||||
|
&Session::new(),
|
||||||
|
TopLevelMode::Module,
|
||||||
|
content.as_bytes(),
|
||||||
|
&mut output,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
String::from_utf8(output)
|
||||||
|
.map_err(|error| {
|
||||||
|
let hash = RandomState::new()
|
||||||
|
.hash_one(error.clone().into_bytes())
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let path = temp_dir().join(hash);
|
||||||
|
|
||||||
|
let mut file = File::create(&path).unwrap();
|
||||||
|
file.write_all(&error.into_bytes()).unwrap();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"Failed to create a String from minified JavaScript code. The minified code has \
|
||||||
|
been written to {}",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn css(content: &str) -> String {
|
||||||
|
// TODO
|
||||||
|
content.to_string()
|
||||||
|
}
|
BIN
src/page/BaiJamjuree700.woff2
Normal file
BIN
src/page/BaiJamjuree700.woff2
Normal file
Binary file not shown.
108
src/page/cube/cube.css
Normal file
108
src/page/cube/cube.css
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "Bai Jamjuree";
|
||||||
|
font-weight: 700;
|
||||||
|
src: url("/assets/BaiJamjuree700.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: #000000;
|
||||||
|
font-family: "Bai Jamjuree", sans;
|
||||||
|
font-size: 450%;
|
||||||
|
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
html {
|
||||||
|
font-size: 200%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 800px) and (min-width: 400px) {
|
||||||
|
html {
|
||||||
|
font-size: 300%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #000000;
|
||||||
|
text-decoration-line: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
|
||||||
|
width: min-content;
|
||||||
|
|
||||||
|
padding: 0 .3em;
|
||||||
|
border-radius: 1em;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
perspective: 15em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cube {
|
||||||
|
height: 5em;
|
||||||
|
width: 5em;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
transform: translateZ(-2.498em);
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
width: 5em;
|
||||||
|
height: 5em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.front {
|
||||||
|
transform: rotateY(0deg) translateZ(2.498em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
/* Guess what? Yeah, you guessed right. Safari can't render shit. */
|
||||||
|
transform: rotateX(89.99999999999999deg) translateZ(2.498em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
transform: rotateY(180deg) translateZ(2.498em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
transform: rotateX(-89.99999999999999deg) translateZ(2.498em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
transform: rotateY(89.99999999999999deg) translateZ(2.498em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
transform: rotateY(-89.99999999999999deg) translateZ(2.498em);
|
||||||
|
}
|
228
src/page/cube/cube.js
Normal file
228
src/page/cube/cube.js
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
class Vec3 {
|
||||||
|
constructor(x, y, z) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
|
||||||
|
static zero() {
|
||||||
|
return new Vec3(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
length() {
|
||||||
|
return Math.sqrt(this.x ** 2 + this.y ** 2 + this.z ** 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
scale(factor) {
|
||||||
|
this.x *= factor;
|
||||||
|
this.y *= factor;
|
||||||
|
this.z *= factor;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize() {
|
||||||
|
const length = this.length();
|
||||||
|
|
||||||
|
if (length != 0) {
|
||||||
|
this.x /= length;
|
||||||
|
this.y /= length;
|
||||||
|
this.z /= length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
static sub(v, t) {
|
||||||
|
return new Vec3(
|
||||||
|
v.x - t.x,
|
||||||
|
v.y - t.y,
|
||||||
|
v.z - t.z,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static sum(v, t) {
|
||||||
|
return new Vec3(
|
||||||
|
v.x + t.x,
|
||||||
|
v.y + t.y,
|
||||||
|
v.z + t.z,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Quat {
|
||||||
|
constructor(x, y, z, w) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
this.w = w;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromAxis(axis) {
|
||||||
|
const angle = axis.length();
|
||||||
|
|
||||||
|
axis.normalize();
|
||||||
|
|
||||||
|
const half = angle / 2;
|
||||||
|
|
||||||
|
const sinHalf = Math.sin(half);
|
||||||
|
const cosHalf = Math.cos(half);
|
||||||
|
|
||||||
|
const x = axis.x * sinHalf;
|
||||||
|
const y = axis.y * sinHalf;
|
||||||
|
const z = axis.z * sinHalf;
|
||||||
|
const w = cosHalf;
|
||||||
|
|
||||||
|
return new Quat(x, y, z, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
static mul(q, r) {
|
||||||
|
return new Quat(
|
||||||
|
q.w * r.x + q.x * r.w + q.y * r.z - q.z * r.y,
|
||||||
|
q.w * r.y - q.x * r.z + q.y * r.w + q.z * r.x,
|
||||||
|
q.w * r.z + q.x * r.y - q.y * r.x + q.z * r.w,
|
||||||
|
q.w * r.w - q.x * r.x - q.y * r.y - q.z * r.z,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let friction = 3;
|
||||||
|
let sensitivity = 0.01;
|
||||||
|
let velocity = Vec3.zero();
|
||||||
|
|
||||||
|
const orientation = {
|
||||||
|
__cube: document.querySelector(".cube"),
|
||||||
|
__value: new Quat(0, 0, 0, 1),
|
||||||
|
|
||||||
|
set(value) {
|
||||||
|
this.__value = value;
|
||||||
|
|
||||||
|
const q = this.__value;
|
||||||
|
this.__cube.style.transform = `rotate3d(${q.x}, ${q.y}, ${q.z}, ${Math.acos(q.w) * 2}rad)`;
|
||||||
|
},
|
||||||
|
|
||||||
|
get() {
|
||||||
|
return this.__value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const mouse = {
|
||||||
|
down: false,
|
||||||
|
lastMove: -10000,
|
||||||
|
previous: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
let impulseThisFrame = Vec3.zero();
|
||||||
|
|
||||||
|
const handleUp = () => {
|
||||||
|
mouse.down = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mouseup", handleUp);
|
||||||
|
document.addEventListener("touchend", handleUp);
|
||||||
|
|
||||||
|
const handleDown = (event) => {
|
||||||
|
// Disables link dragging that occurs when spinning.
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
mouse.down = true;
|
||||||
|
|
||||||
|
velocity = Vec3.zero();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleDown);
|
||||||
|
document.addEventListener("touchstart", handleDown);
|
||||||
|
|
||||||
|
const handleMove = (event) => {
|
||||||
|
// Disables scrolling.
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!mouse.down) return;
|
||||||
|
|
||||||
|
const newMouse = new Vec3(event.clientX, event.clientY, 0);
|
||||||
|
|
||||||
|
const timeDelta = (window.performance.now() - mouse.lastMove) / 1000;
|
||||||
|
|
||||||
|
if (timeDelta > 0.1) {
|
||||||
|
// This is a fresh scroll.
|
||||||
|
mouse.previous = newMouse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = Vec3.sub(newMouse, mouse.previous);
|
||||||
|
|
||||||
|
mouse.previous = newMouse;
|
||||||
|
mouse.lastMove = window.performance.now();
|
||||||
|
|
||||||
|
const axis = new Vec3(-delta.y, delta.x, 0)
|
||||||
|
.normalize()
|
||||||
|
.scale(delta.length() * sensitivity);
|
||||||
|
|
||||||
|
impulseThisFrame = Vec3.sum(impulseThisFrame, axis);
|
||||||
|
|
||||||
|
const rotation = Quat.fromAxis(axis);
|
||||||
|
|
||||||
|
orientation.set(Quat.mul(rotation, orientation.get()));
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleMove);
|
||||||
|
document.addEventListener("touchmove", (event) => {
|
||||||
|
const delta = event.changedTouches[0];
|
||||||
|
|
||||||
|
event.clientX = delta.clientX;
|
||||||
|
event.clientY = delta.clientY;
|
||||||
|
|
||||||
|
handleMove(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastUpdate = 0;
|
||||||
|
|
||||||
|
const updateFrame = (timestamp) => {
|
||||||
|
if (lastUpdate == 0) lastUpdate = timestamp;
|
||||||
|
|
||||||
|
const delta = (timestamp - lastUpdate) / 1000;
|
||||||
|
lastUpdate = timestamp;
|
||||||
|
|
||||||
|
if (mouse.down) {
|
||||||
|
velocity = impulseThisFrame.scale(1 / delta);
|
||||||
|
impulseThisFrame = Vec3.zero();
|
||||||
|
} else {
|
||||||
|
const decay = Math.exp(-delta * friction);
|
||||||
|
|
||||||
|
const effectiveDelta = friction > 0 ? (1 - decay) / friction : delta;
|
||||||
|
|
||||||
|
let theta = effectiveDelta * velocity.length();
|
||||||
|
|
||||||
|
velocity.x *= decay;
|
||||||
|
velocity.y *= decay;
|
||||||
|
velocity.z *= decay;
|
||||||
|
|
||||||
|
if (friction > 0 && velocity.length() < 0.00001) {
|
||||||
|
theta += velocity.length() / friction;
|
||||||
|
|
||||||
|
velocity.x = 0;
|
||||||
|
velocity.y = 0;
|
||||||
|
velocity.z = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.performance.now() - mouse.lastMove > 10000) {
|
||||||
|
const impulse = new Vec3(1, 1, -1);
|
||||||
|
velocity = Vec3.sum(impulse.scale(effectiveDelta * 3), velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const axis = new Vec3(velocity.x, velocity.y, velocity.z)
|
||||||
|
.normalize()
|
||||||
|
.scale(theta);
|
||||||
|
|
||||||
|
const rotation = Quat.fromAxis(axis);
|
||||||
|
|
||||||
|
orientation.set(Quat.mul(rotation, orientation.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(updateFrame);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateFrame(0);
|
||||||
|
}
|
35
src/page/cube/mod.rs
Normal file
35
src/page/cube/mod.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use maud::{
|
||||||
|
html,
|
||||||
|
Markup,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
asset::Css,
|
||||||
|
page::asset,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates a pure HTML CSS and JS cube with 6 faces, the
|
||||||
|
/// order of the faces are as so:
|
||||||
|
///
|
||||||
|
/// front, top, back, bottom, right, left.
|
||||||
|
pub fn create(css: Css, faces: [Markup; 6]) -> Markup {
|
||||||
|
crate::page::create(
|
||||||
|
html! {
|
||||||
|
(asset::Css::Shared("cube.css"))
|
||||||
|
(css)
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
div class="scene" {
|
||||||
|
div class="cube" {
|
||||||
|
@for (name, content) in ["front", "top", "back", "bottom", "right", "left"].iter().zip(faces) {
|
||||||
|
div class=(format!("face {name}")) {
|
||||||
|
(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(asset::Js::Shared("cube.js"))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
18
src/page/elements.rs
Normal file
18
src/page/elements.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
use maud::{
|
||||||
|
html,
|
||||||
|
Markup,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates a meta tag with property and content.
|
||||||
|
pub fn property(name: &str, content: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
meta property=(name) content=(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a meta tag with name and content.
|
||||||
|
pub fn pname(name: &str, content: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
meta name=(name) content=(content);
|
||||||
|
}
|
||||||
|
}
|
BIN
src/page/icon.gif
Normal file
BIN
src/page/icon.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
|
@ -1,42 +1,31 @@
|
||||||
|
pub mod cube;
|
||||||
|
mod elements;
|
||||||
|
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use cargo_toml::Manifest;
|
use cargo_toml::Manifest;
|
||||||
|
pub use elements::*;
|
||||||
use maud::{
|
use maud::{
|
||||||
html,
|
html,
|
||||||
Markup,
|
Markup,
|
||||||
DOCTYPE,
|
DOCTYPE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::asset;
|
||||||
|
|
||||||
static MANIFEST: LazyLock<Manifest> = LazyLock::new(|| {
|
static MANIFEST: LazyLock<Manifest> = LazyLock::new(|| {
|
||||||
Manifest::from_str(&embed::string!("../../Cargo.toml"))
|
Manifest::from_str(&embed::string!("../../Cargo.toml"))
|
||||||
.with_context(|| "Failed to deserialize Cargo manifest.")
|
.with_context(|| "Failed to deserialize Cargo manifest")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
fn property(name: &str, content: &str) -> Markup {
|
|
||||||
html! {
|
|
||||||
meta property=(name) content=(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pname(name: &str, content: &str) -> Markup {
|
|
||||||
html! {
|
|
||||||
meta name=(name) content=(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an asset URL from the given asset path.
|
|
||||||
pub(crate) fn asset(path: &str) -> String {
|
|
||||||
format!("/assets/{path}")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a page with the given head and body.
|
/// Creates a page with the given head and body.
|
||||||
///
|
///
|
||||||
/// This is the most low level function for page creation
|
/// This is the most low level function for page creation
|
||||||
/// as all pages use this, as this function provides the
|
/// as all pages use this, as this function provides the
|
||||||
/// page title, OpenGraph and other information.
|
/// page title, OpenGraph and other information.
|
||||||
pub(crate) fn create(head: Markup, body: Markup) -> Markup {
|
pub fn create(head: Markup, body: Markup) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
|
|
||||||
|
@ -58,9 +47,9 @@ pub(crate) fn create(head: Markup, body: Markup) -> Markup {
|
||||||
(pname("description", description))
|
(pname("description", description))
|
||||||
(property("og:description", description))
|
(property("og:description", description))
|
||||||
|
|
||||||
link rel="icon" href=(asset("icon.gif")) type="image/gif";
|
link rel="icon" href=(asset::File("icon.gif")) type="image/gif";
|
||||||
|
|
||||||
(property("og:image", &asset("thumbnail.png")))
|
(property("og:image", &asset::File("thumbnail.png").to_string()))
|
||||||
(property("og:image:type", "image/png"))
|
(property("og:image:type", "image/png"))
|
||||||
(property("og:image:height", "1080"))
|
(property("og:image:height", "1080"))
|
||||||
(property("og:image:width", "600"))
|
(property("og:image:width", "600"))
|
||||||
|
|
77
src/routes/assets.rs
Normal file
77
src/routes/assets.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
io::{
|
||||||
|
Cursor,
|
||||||
|
Read,
|
||||||
|
},
|
||||||
|
sync::LazyLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
get,
|
||||||
|
web,
|
||||||
|
HttpResponse,
|
||||||
|
};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use tar::Archive;
|
||||||
|
|
||||||
|
use crate::minify;
|
||||||
|
|
||||||
|
const ASSET_EXTENSIONS: &[&str] = &[".js", ".css", ".woff2", ".gif"];
|
||||||
|
|
||||||
|
static ASSETS: LazyLock<HashMap<String, Bytes>> = LazyLock::new(|| {
|
||||||
|
let contents = embed::bytes!("../../src.tar");
|
||||||
|
let mut archive = Archive::new(Cursor::new(contents));
|
||||||
|
|
||||||
|
let mut assets = HashMap::new();
|
||||||
|
|
||||||
|
for entry in archive.entries().unwrap() {
|
||||||
|
let mut entry = if let Ok(entry) = entry {
|
||||||
|
entry
|
||||||
|
} else {
|
||||||
|
log::error!("fail");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = entry.path_bytes();
|
||||||
|
let path = String::from_utf8(path.to_vec()).unwrap();
|
||||||
|
|
||||||
|
if path.ends_with("/") || !ASSET_EXTENSIONS.iter().any(|ext| path.ends_with(ext)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = path.rsplit_once("/").unwrap_or(("", &path)).1;
|
||||||
|
|
||||||
|
let mut content = Vec::new();
|
||||||
|
entry.read_to_end(&mut content).unwrap();
|
||||||
|
|
||||||
|
if minify::is_minifiable(&path) {
|
||||||
|
let content = minify::generic(&path, &content);
|
||||||
|
|
||||||
|
log::info!("Minifying asset {path}");
|
||||||
|
assets.insert(minify::insert_min(path), Bytes::from(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Adding asset {path}");
|
||||||
|
assets.insert(path.to_string(), Bytes::from(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
assets
|
||||||
|
});
|
||||||
|
|
||||||
|
#[get("/assets/{name}")]
|
||||||
|
pub async fn handler(name: web::Path<String>) -> HttpResponse {
|
||||||
|
let name = name.into_inner();
|
||||||
|
|
||||||
|
if let Some(body) = ASSETS.get(&name) {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type(
|
||||||
|
mime_guess::from_path(&name)
|
||||||
|
.first_or_octet_stream()
|
||||||
|
.essence_str(),
|
||||||
|
)
|
||||||
|
.body(Bytes::clone(body))
|
||||||
|
} else {
|
||||||
|
HttpResponse::NotFound().into()
|
||||||
|
}
|
||||||
|
}
|
68
src/routes/index/index.css
Normal file
68
src/routes/index/index.css
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
.frame:hover {
|
||||||
|
background-color: #FFFF00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face::after {
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
content: "";
|
||||||
|
|
||||||
|
height: inherit;
|
||||||
|
width: inherit;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.front {
|
||||||
|
background: linear-gradient(to bottom, cyan, blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.front::after {
|
||||||
|
background: linear-gradient(to bottom, white, magenta);
|
||||||
|
mask-image: linear-gradient(to left, magenta, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
background: linear-gradient(to bottom, lime, cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top::after {
|
||||||
|
background: linear-gradient(to bottom, yellow, white);
|
||||||
|
mask-image: linear-gradient(to left, white, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
background: linear-gradient(to bottom, yellow, red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back::after {
|
||||||
|
background: linear-gradient(to bottom, lime, black);
|
||||||
|
mask-image: linear-gradient(to left, black, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
background: linear-gradient(to bottom, blue, black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom::after {
|
||||||
|
background: linear-gradient(to bottom, magenta, red);
|
||||||
|
mask-image: linear-gradient(to left, red, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
background: linear-gradient(to bottom, white, magenta);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right::after {
|
||||||
|
background: linear-gradient(to bottom, yellow, red);
|
||||||
|
mask-image: linear-gradient(to left, red, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
background: linear-gradient(to bottom, lime, black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left::after {
|
||||||
|
background: linear-gradient(to bottom, cyan, blue);
|
||||||
|
mask-image: linear-gradient(to left, blue, transparent);
|
||||||
|
}
|
37
src/routes/index/mod.rs
Normal file
37
src/routes/index/mod.rs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
use actix_web::get;
|
||||||
|
use maud::{
|
||||||
|
html,
|
||||||
|
Markup,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
asset,
|
||||||
|
page::cube,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
pub async fn handler() -> actix_web::Result<Markup> {
|
||||||
|
Ok(cube::create(
|
||||||
|
asset::css::owned!("index.css"),
|
||||||
|
[
|
||||||
|
html! {
|
||||||
|
a href="/contact" {
|
||||||
|
div class="frame" {
|
||||||
|
"contact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
a href="/github" {
|
||||||
|
div class="frame" {
|
||||||
|
"github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
html! {},
|
||||||
|
html! {},
|
||||||
|
html! {},
|
||||||
|
html! {},
|
||||||
|
],
|
||||||
|
))
|
||||||
|
}
|
13
src/routes/mod.rs
Normal file
13
src/routes/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
mod assets;
|
||||||
|
mod index;
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
web,
|
||||||
|
Scope,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn handler() -> Scope {
|
||||||
|
web::scope("")
|
||||||
|
.service(index::handler)
|
||||||
|
.service(assets::handler)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue