Initial setup pachinko

This commit is contained in:
Matthew Deville 2026-09-14 18:19:58 +02:00
parent 3f379750a6
commit 1f2dff7ced
13 changed files with 2367 additions and 1764 deletions

3727
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,5 +3,7 @@
resolver = "3"
[profile.release]
lto = "thin"
lto = true
opt-level = 'z'
panic = "abort"
strip = true

View file

@ -2,10 +2,14 @@ FROM rust:1 AS builder
WORKDIR /usr/src/website
COPY . .
RUN apt-get update \
&& apt-get install -y --no-install-recommends binaryen brotli \
&& rm -rf /var/lib/apt/lists/*
RUN rustup target add wasm32-unknown-unknown
RUN cargo install wasm-bindgen-cli
RUN ./build-wasm.sh
RUN ./build-wasm.sh release
RUN cargo build --release -p server
FROM debian:trixie-slim

View file

@ -4,9 +4,5 @@
version = "0.1.0"
[dependencies]
bevy = { version = "0.18", default-features = false, features = [
"3d",
"tonemapping_luts",
"web",
"webgl2",
] }
avian2d = "0.7"
bevy = { version = "0.19", default-features = false, features = ["2d"] }

View file

@ -1,8 +1,15 @@
use bevy::prelude::*;
use avian2d::prelude::*;
use bevy::{
input::common_conditions::input_just_pressed,
prelude::*,
window::{PrimaryWindow, WindowResized},
};
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
.insert_resource(ClearColor(Color::srgb_u8(26, 26, 46)))
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
fit_canvas_to_parent: true,
prevent_default_event_handling: false,
@ -10,38 +17,157 @@ fn main() {
..default()
}),
..default()
}))
}),
PhysicsPlugins::default(),
))
.insert_resource(Time::<Physics>::default().with_relative_speed(5.0))
.add_systems(Startup, setup)
.add_systems(
Update,
(
update_screen_edges,
spawn_ball.run_if(input_just_pressed(MouseButton::Left)),
),
)
.run();
}
#[derive(Component, Clone, Copy)]
enum ScreenEdge {
Top,
Bottom,
Left,
Right,
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut materials: ResMut<Assets<ColorMaterial>>,
window: Query<&Window, With<PrimaryWindow>>,
) {
commands.spawn((
Mesh3d(meshes.add(Circle::new(4.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
));
commands.spawn(Camera2d);
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
Transform::from_xyz(0.0, 0.5, 0.0),
));
commands.spawn((
PointLight {
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0),
));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
));
if let Ok(window) = window.single() {
spawn_screen_edges(&mut commands, &mut meshes, &mut materials, window.size());
}
}
#[derive(Component, Clone, Copy)]
struct Ball;
fn spawn_ball(
mouse_button: Res<ButtonInput<MouseButton>>,
window: Single<&Window, With<PrimaryWindow>>,
camera_query: Single<(&Camera, &GlobalTransform)>,
time: Res<Time>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
const CIRCLE_RADIUS: f32 = 5.0;
let (camera, camera_transform) = *camera_query;
if mouse_button.just_pressed(MouseButton::Left)
&& let Some(cursor_position) = window.cursor_position()
&& let Ok(world_position) = camera.viewport_to_world_2d(camera_transform, cursor_position)
{
commands.spawn((
RigidBody::Dynamic,
Collider::circle(CIRCLE_RADIUS),
Restitution::PERFECTLY_ELASTIC.with_combine_rule(CoefficientCombine::Max),
Mesh2d(meshes.add(Circle::new(CIRCLE_RADIUS))),
MeshMaterial2d(materials.add(Color::hsl(time.elapsed_secs().sin() * 360.0, 1.0, 0.5))),
Transform::from_xyz(world_position.x, world_position.y, 0.0),
Ball,
));
}
}
fn spawn_screen_edges(
commands: &mut Commands,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<ColorMaterial>,
size: Vec2,
) {
const EDGE_COLOR: Color = Color::srgb_u8(255, 255, 255);
let material = materials.add(EDGE_COLOR);
for edge in [
ScreenEdge::Top,
ScreenEdge::Bottom,
ScreenEdge::Left,
ScreenEdge::Right,
] {
let (collider, transform, rect_size) = edge_shape(edge, size);
commands.spawn((
RigidBody::Static,
collider,
transform,
edge,
Mesh2d(meshes.add(Rectangle::from_size(rect_size))),
MeshMaterial2d(material.clone()),
));
}
}
fn edge_shape(edge: ScreenEdge, size: Vec2) -> (Collider, Transform, Vec2) {
const EDGE_THICKNESS: f32 = 5.0;
let half = size / 2.0;
let thickness = EDGE_THICKNESS;
match edge {
ScreenEdge::Top => {
let rect = Vec2::new(size.x, thickness);
(
Collider::rectangle(rect.x, rect.y),
Transform::from_xyz(0.0, half.y - thickness / 2.0, 0.0),
rect,
)
}
ScreenEdge::Bottom => {
let rect = Vec2::new(size.x, thickness);
(
Collider::rectangle(rect.x, rect.y),
Transform::from_xyz(0.0, -half.y + thickness / 2.0, 0.0),
rect,
)
}
ScreenEdge::Left => {
let rect = Vec2::new(thickness, size.y);
(
Collider::rectangle(rect.x, rect.y),
Transform::from_xyz(-half.x + thickness / 2.0, 0.0, 0.0),
rect,
)
}
ScreenEdge::Right => {
let rect = Vec2::new(thickness, size.y);
(
Collider::rectangle(rect.x, rect.y),
Transform::from_xyz(half.x - thickness / 2.0, 0.0, 0.0),
rect,
)
}
}
}
fn update_screen_edges(
mut resized: PopulatedMessageReader<WindowResized>,
mut edges: Query<(&ScreenEdge, &mut Collider, &mut Transform, &Mesh2d)>,
mut meshes: ResMut<Assets<Mesh>>,
) {
for event in resized.read() {
let size = Vec2::new(event.width, event.height);
for (edge, mut collider, mut transform, mesh2d) in &mut edges {
let (new_collider, new_transform, rect_size) = edge_shape(*edge, size);
*collider = new_collider;
*transform = new_transform;
if let Some(mut mesh) = meshes.get_mut(&mesh2d.0) {
*mesh = Rectangle::from_size(rect_size).into();
}
}
}
}

View file

@ -14,8 +14,22 @@ fi
cargo build $CARGO_FLAGS --target wasm32-unknown-unknown -p app
mkdir -p static
wasm-bindgen --out-name 3dscene \
# Drop stale bindgen / compression outputs from prior builds.
rm -f static/2dscene*
wasm-bindgen --out-name 2dscene \
--out-dir static \
--target web \
"target/wasm32-unknown-unknown/$PROFILE_DIR/app.wasm"
if [ "$PROFILE" != "dev" ]; then
wasm-opt -Oz --all-features -o static/2dscene_bg.wasm static/2dscene_bg.wasm
# Precompress assets Axum will negotiate via Accept-Encoding.
for f in static/2dscene_bg.wasm static/2dscene.js; do
gzip -9 -k -f "$f"
brotli -q 11 -f -k "$f"
done
fi
cp web/index.html static/

View file

@ -1,30 +1,30 @@
{
"nodes": {
"flake-utils": {
"flake-parts": {
"inputs": {
"systems": "systems"
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"lastModified": 1788450739,
"narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1771369470,
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
"lastModified": 1789149629,
"narHash": "sha256-H6GwaZzZf+4npqv0tph94w9tZddSjFjmQrVsW0z78uk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0182a361324364ae3f436a63005877674cf45efb",
"rev": "eaad089433ca2bb662274377d33df3d0e51ef28b",
"type": "github"
},
"original": {
@ -34,9 +34,24 @@
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1788057806,
"narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
@ -48,11 +63,11 @@
]
},
"locked": {
"lastModified": 1771556776,
"narHash": "sha256-zKprqMQDl3xVfhSSYvgru1IGXjFdxryWk+KqK0I20Xk=",
"lastModified": 1789372034,
"narHash": "sha256-mu6atvdBHYpDsFhJkPtSsS6auSWAnWONVJMCvpL0900=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "8b3f46b8a6d17ab46e533a5e3d5b1cc2ff228860",
"rev": "a5f41d15ba0e9442542c5324fbccd7a87105a90d",
"type": "github"
},
"original": {
@ -60,21 +75,6 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View file

@ -2,41 +2,46 @@
description = "Bevy WASM website";
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
outputs = inputs@{
flake-parts,
nixpkgs,
rust-overlay,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
perSystem = { pkgs, system, ... }:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
rustPkgs = import nixpkgs {
inherit system overlays;
};
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" ];
targets = [ "wasm32-unknown-unknown" ];
};
rustToolchain = rustPkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
in
{
devShells.default = pkgs.mkShell {
buildInputs = [
rustToolchain
pkgs.wasm-bindgen-cli
pkgs.binaryen
pkgs.brotli
pkgs.just
];
};
}
);
};
};
}

36
justfile Normal file
View file

@ -0,0 +1,36 @@
# List available recipes
default:
@just --list
# Build the Bevy app to WASM (release by default; use `just build dev` for debug)
build profile="release":
./build-wasm.sh {{profile}}
# Build WASM in debug mode
build-dev:
./build-wasm.sh dev
# Build the HTTP server (release)
build-server:
cargo build --release -p server
# Build WASM + server for production
build-all: build build-server
# Run the server (serves ./static on :3000)
serve:
cargo run -p server
# Build WASM (release) then run the server
run: build
cargo run --release -p server
# Type-check the workspace
check:
cargo clippy --target wasm32-unknown-unknown -p app
cargo clippy --target x86_64-unknown-linux-gnu -p server
# Remove build artifacts
clean:
rm -rf static
cargo clean

3
rust-toolchain.toml Normal file
View file

@ -0,0 +1,3 @@
[toolchain]
channel = "1.98.1"
targets = ["wasm32-unknown-unknown", "x86_64-unknown-linux-gnu"]

View file

@ -6,4 +6,4 @@
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.6", features = ["fs"] }
tower-http = { version = "0.7", features = ["fs"] }

View file

@ -3,9 +3,13 @@ use tower_http::services::ServeDir;
#[tokio::main]
async fn main() {
let static_files = ServeDir::new("static")
.precompressed_gzip()
.precompressed_br();
let app = Router::new()
.route("/health", get(|| async { "OK" }))
.fallback_service(ServeDir::new("static"));
.fallback_service(static_files);
println!("Listening on http://0.0.0.0:3000");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();

View file

@ -4,21 +4,17 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Scene</title>
<title>Personal Website</title>
<style>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #1a1a2e;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
</style>
@ -27,7 +23,7 @@
<body>
<canvas id="bevy-canvas"></canvas>
<script type="module">
import init from './3dscene.js';
import init from './2dscene.js';
init();
</script>
</body>