OverviewThis WebsiteHome LabTrading BotEcosimProcedural Generation
Source Code

src/pages/projects/orbit_camera.rs

use leptos::*;
use leptos_meta::{Meta, Title};

use super::data::ALL_PROJECTS;

#[component]
pub fn OrbitCamera() -> impl IntoView {
    let project = ALL_PROJECTS
        .iter()
        .find(|p| p.slug == "orbit-camera")
        .unwrap();
    let skills_view = project
        .skills
        .iter()
        .map(|&s| view! { <li>{s}</li> })
        .collect::<Vec<_>>();

    view! {
        <Title text="Orbit Camera – Peter Pinto"/>
        <Meta name="description" content="A reusable Bevy plugin for orbital camera control with trait-based input abstraction, quaternion rotation, and distance-aware speed falloff."/>
        <div class="page">
            <span class="eyebrow">"Projects"</span>
            <h1>"Orbit " <em style="font-style:italic; color: var(--accent)">"Camera"</em></h1>
            <p class="lead">
                "A reusable Bevy plugin for orbital camera control. Input handling is
                fully decoupled from camera logic via a trait, making it straightforward
                to bind any combination of keyboard, mouse, gamepad, or custom input."
            </p>

            <ul class="skills-list" style="margin-top: 1.5rem;">
                {skills_view}
            </ul>

            <hr class="divider"/>

            // ── Input Abstraction ─────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Design"</span>
                <h2>"Trait-Based Input"</h2>
                <p>
                    "The plugin is generic over an "
                    <code class="inline-code">"OrbitCameraInput"</code>
                    " trait. Any type that implements this trait can drive the camera —
                    default mouse controls, a gamepad, keyboard arrows, or a custom input
                    reader that pulls from a network socket. The camera system itself
                    never touches "
                    <code class="inline-code">"Res<ButtonInput<KeyCode>>"</code>
                    " directly; it only sees normalised orbit/zoom/roll deltas."
                </p>
                <p style="margin-top: 1rem;">
                    "This pattern means the same "
                    <code class="inline-code">"OrbitCameraPlugin<I>"</code>
                    " can be dropped into any project and configured for the project's
                    input conventions without forking or patching the plugin."
                </p>
            </section>

            <hr class="divider"/>

            // ── Rotation ──────────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Mathematics"</span>
                <h2>"Quaternion Rotation"</h2>
                <p>
                    "All rotations use quaternions exclusively — no Euler angles. This
                    prevents gimbal lock regardless of orbit angle, which matters when
                    the camera is used to orbit a planet where the user can reach any
                    viewing angle including directly over the poles."
                </p>
                <p style="margin-top: 1rem;">
                    "Two rotation modes are supported: "
                    <code class="inline-code">"GlobalY"</code>
                    " (turntable — yaw around the world up axis, pitch around the camera's
                    local X) and "
                    <code class="inline-code">"CameraRelative"</code>
                    " (free-look — both axes relative to the camera). The former is natural
                    for viewing a model; the latter for flying around a scene."
                </p>
            </section>

            <hr class="divider"/>

            // ── Speed Falloff ─────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"Feel"</span>
                <h2>"Speed Falloff"</h2>
                <p>
                    "Zoom speed decreases as the camera approaches its minimum radius,
                    providing a natural \"resistance\" feeling rather than abruptly hitting
                    a hard stop. Orbit speed scales proportionally with the current radius
                    so that a given mouse movement always sweeps the same arc length on
                    the focus sphere — the camera feels equally responsive when zoomed in
                    close or pulled back to a wide view."
                </p>
            </section>

            <hr class="divider"/>

            // ── Usage ─────────────────────────────────────────────
            <section class="project-section">
                <span class="eyebrow">"API"</span>
                <h2>"Usage"</h2>
                <p>
                    "Adding the camera to a Bevy app is a few lines:"
                </p>
                <div class="code-block" style="margin-top: 1.25rem;">
                    <pre><code>"app.add_plugins(OrbitCameraPlugin::<MouseInput>::default());

commands.spawn((
    Camera3dBundle::default(),
    OrbitCamera {
        focus: Vec3::ZERO,
        radius: 10.0,
        ..default()
    },
));"</code></pre>
                </div>
                <p style="margin-top: 1rem;">
                    "Swapping to a custom input scheme means changing the type parameter
                    and providing an implementation of "
                    <code class="inline-code">"OrbitCameraInput"</code>
                    " — the camera behaviour is unchanged."
                </p>
            </section>
        </div>
    }
}