render sliders
6
Cargo.lock
generated
|
@ -558,6 +558,7 @@ dependencies = [
|
|||
"ggez",
|
||||
"libosu",
|
||||
"log",
|
||||
"num",
|
||||
"stderrlog",
|
||||
]
|
||||
|
||||
|
@ -1130,9 +1131,8 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
|
|||
|
||||
[[package]]
|
||||
name = "libosu"
|
||||
version = "0.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1211582bccd34afcda71e9a99d542d6880750b6c04f5506989a7042c5f06284"
|
||||
version = "0.0.12"
|
||||
source = "git+https://github.com/iptq/libosu?rev=7acc09af59b789f78c21c259d4e1e246e9cf0b08#7acc09af59b789f78c21c259d4e1e246e9cf0b08"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
|
|
@ -13,6 +13,10 @@ members = [
|
|||
anyhow = "1.0.37"
|
||||
bass-sys = { path = "bass-sys" }
|
||||
ggez = "0.5.1"
|
||||
libosu = "0.0.11"
|
||||
log = "0.4.11"
|
||||
stderrlog = "0.5.0"
|
||||
num = "0.3.1"
|
||||
|
||||
[dependencies.libosu]
|
||||
git = "https://github.com/iptq/libosu"
|
||||
rev = "7acc09af59b789f78c21c259d4e1e246e9cf0b08"
|
||||
|
|
2
bass-sys/.ignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
/linux
|
||||
/win
|
|
@ -1,8 +1,21 @@
|
|||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "linux")]
|
||||
fn load_bass() {
|
||||
let mut project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
project_dir.push("linux");
|
||||
project_dir.push("bass24");
|
||||
project_dir.push("x64");
|
||||
|
||||
println!("cargo:rustc-link-search={}", project_dir.to_str().unwrap());
|
||||
println!("cargo:rustc-link-lib=bass");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn load_bass() {
|
||||
let mut project_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
project_dir.push("win");
|
||||
project_dir.push("bass24");
|
||||
project_dir.push("c");
|
||||
project_dir.push("x64");
|
||||
|
@ -10,3 +23,7 @@ fn main() {
|
|||
println!("cargo:rustc-link-search={}", project_dir.to_str().unwrap());
|
||||
println!("cargo:rustc-link-lib=bass");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
load_bass();
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
439740
beatmaps/notch-hell/Halozy - Kikoku Doukoku Jigokuraku (Hollow Wings).osb
Normal file
BIN
beatmaps/notch-hell/Kikoku Doukoku Jigokuraku.jpg
Normal file
After Width: | Height: | Size: 129 KiB |
BIN
beatmaps/notch-hell/Kikoku Doukoku Jigokuraku.mp3
Normal file
BIN
beatmaps/notch-hell/drum-hitclap.wav
Normal file
BIN
beatmaps/notch-hell/drum-hitfinish.wav
Normal file
BIN
beatmaps/notch-hell/drum-hitnormal.wav
Normal file
BIN
beatmaps/notch-hell/drum-hitwhistle.wav
Normal file
BIN
beatmaps/notch-hell/normal-hitclap.wav
Normal file
BIN
beatmaps/notch-hell/normal-hitfinish.wav
Normal file
BIN
beatmaps/notch-hell/normal-hitnormal.wav
Normal file
BIN
beatmaps/notch-hell/normal-hitwhistle.wav
Normal file
BIN
beatmaps/notch-hell/sb/effect/b.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
beatmaps/notch-hell/sb/effect/core.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
beatmaps/notch-hell/sb/effect/flame.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
beatmaps/notch-hell/sb/effect/pixel.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
beatmaps/notch-hell/sb/effect/t.png
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
beatmaps/notch-hell/soft-hitclap.wav
Normal file
BIN
beatmaps/notch-hell/soft-hitfinish.wav
Normal file
BIN
beatmaps/notch-hell/soft-hitnormal.wav
Normal file
BIN
beatmaps/notch-hell/soft-hitwhistle.wav
Normal file
BIN
beatmaps/notch-hell/soft-sliderslide.wav
Normal file
BIN
beatmaps/notch-hell/soft-sliderslide2.wav
Normal file
BIN
beatmaps/notch-hell/soft-sliderslide3.wav
Normal file
BIN
beatmaps/notch-hell/soft-sliderslide4.wav
Normal file
BIN
beatmaps/notch-hell/soft-slidertick.wav
Normal file
BIN
beatmaps/notch-hell/soft-slidertick2.wav
Normal file
BIN
beatmaps/notch-hell/soft-slidertick3.wav
Normal file
BIN
beatmaps/notch-hell/soft-slidertick4.wav
Normal file
4
run.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
export LD_LIBRARY_PATH=$(pwd)/bass-sys/linux/bass24/x64
|
||||
echo $LD_LIBRARY_PATH
|
||||
exec cargo run "$@"
|
1
rust-toolchain
Normal file
|
@ -0,0 +1 @@
|
|||
nightly
|
47
src/game.rs
|
@ -5,13 +5,15 @@ use std::path::Path;
|
|||
use anyhow::Result;
|
||||
use ggez::{
|
||||
event::{EventHandler, KeyCode, KeyMods},
|
||||
graphics::{self, DrawMode, DrawParam, FillOptions, StrokeOptions, FilterMode, Mesh, Text, WHITE},
|
||||
nalgebra::Point2,
|
||||
graphics::{
|
||||
self, DrawMode, DrawParam, FillOptions, FilterMode, Mesh, Rect, StrokeOptions, Text, WHITE,
|
||||
},
|
||||
Context, GameError, GameResult,
|
||||
};
|
||||
use libosu::{Beatmap, HitObject};
|
||||
use libosu::{Beatmap, HitObject, HitObjectKind};
|
||||
|
||||
use crate::audio::{AudioEngine, Sound};
|
||||
use crate::slider_render::render_slider;
|
||||
|
||||
pub struct Game {
|
||||
is_playing: bool,
|
||||
|
@ -26,6 +28,7 @@ impl Game {
|
|||
let audio_engine = AudioEngine::new()?;
|
||||
let beatmap = Beatmap::default();
|
||||
let hit_objects = Vec::new();
|
||||
|
||||
Ok(Game {
|
||||
is_playing: false,
|
||||
audio_engine,
|
||||
|
@ -60,6 +63,9 @@ impl Game {
|
|||
}
|
||||
|
||||
fn priv_draw(&mut self, ctx: &mut Context) -> Result<()> {
|
||||
// TODO: lol
|
||||
const EDITOR_SCREEN: Rect = Rect::new(112.0, 84.0, 800.0, 600.0);
|
||||
|
||||
graphics::clear(ctx, [0.0, 0.0, 0.0, 1.0].into());
|
||||
|
||||
let time = self.song.as_ref().unwrap().position()?;
|
||||
|
@ -76,15 +82,42 @@ impl Game {
|
|||
}
|
||||
}
|
||||
|
||||
info!("# hitobjects: {} / {}", visible_hitobjects.len(), self.beatmap.hit_objects.len());
|
||||
let osupx_scale_x = EDITOR_SCREEN.w / 512.0;
|
||||
let osupx_scale_y = EDITOR_SCREEN.h / 384.0;
|
||||
let cs_osupx = 54.4 - 4.48 * self.beatmap.difficulty.circle_size;
|
||||
let cs_real = cs_osupx * osupx_scale_x;
|
||||
|
||||
for ho in visible_hitobjects.iter() {
|
||||
let ho_time = (ho.start_time.0 as f64) / 1000.0;
|
||||
let circ = Mesh::new_circle(ctx, DrawMode::Fill(FillOptions::default()), [ho.pos.0 as f32, ho.pos.1 as f32], 10.0, 1.0, WHITE)?;
|
||||
let pos = [
|
||||
EDITOR_SCREEN.x + osupx_scale_x * ho.pos.0 as f32,
|
||||
EDITOR_SCREEN.y + osupx_scale_y * ho.pos.1 as f32,
|
||||
];
|
||||
|
||||
if let HitObjectKind::Slider(_) = ho.kind {
|
||||
render_slider(ctx, EDITOR_SCREEN, &self.beatmap, ho)?;
|
||||
}
|
||||
|
||||
let circ = Mesh::new_circle(
|
||||
ctx,
|
||||
DrawMode::Fill(FillOptions::default()),
|
||||
pos,
|
||||
cs_real,
|
||||
1.0,
|
||||
WHITE,
|
||||
)?;
|
||||
graphics::draw(ctx, &circ, DrawParam::default())?;
|
||||
|
||||
let time_diff = ho_time - time;
|
||||
let approach_r = 10.0 * (1.0 + 2.0 * time_diff as f32 / 0.75);
|
||||
let approach = Mesh::new_circle(ctx, DrawMode::Stroke(StrokeOptions::default()), [ho.pos.0 as f32, ho.pos.1 as f32], approach_r, 1.0, WHITE)?;
|
||||
let approach_r = cs_real * (1.0 + 2.0 * time_diff as f32 / 0.75);
|
||||
let approach = Mesh::new_circle(
|
||||
ctx,
|
||||
DrawMode::Stroke(StrokeOptions::default()),
|
||||
pos,
|
||||
approach_r,
|
||||
1.0,
|
||||
WHITE,
|
||||
)?;
|
||||
graphics::draw(ctx, &approach, DrawParam::default())?;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![feature(vec_into_raw_parts)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate anyhow;
|
||||
#[macro_use]
|
||||
|
@ -6,6 +8,10 @@ extern crate bass_sys as bass;
|
|||
|
||||
mod audio;
|
||||
mod game;
|
||||
mod math;
|
||||
mod slider_render;
|
||||
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
use ggez::{
|
||||
|
@ -29,7 +35,8 @@ fn main() -> Result<()> {
|
|||
|
||||
let (ctx, event_loop) = &mut cb.build()?;
|
||||
let mut game = Game::new()?;
|
||||
game.load_beatmap("happy-time/Nanamori-chu Goraku-bu - Happy Time wa Owaranai (Cut Ver.) (-Keitaro) [Osu's Expert].osu")?;
|
||||
let path = env::args().nth(1).unwrap();
|
||||
game.load_beatmap(path)?;
|
||||
event::run(ctx, event_loop, &mut game)?;
|
||||
|
||||
Ok(())
|
||||
|
|
35
src/math.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use libosu::Point;
|
||||
use num::Float;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Math<T>(PhantomData<T>);
|
||||
|
||||
impl<T: Float> Math<T> {
|
||||
pub fn circumcircle(p1: Point<T>, p2: Point<T>, p3: Point<T>) -> (Point<T>, T) {
|
||||
let (x1, y1) = (p1.0, p1.1);
|
||||
let (x2, y2) = (p2.0, p2.1);
|
||||
let (x3, y3) = (p3.0, p3.1);
|
||||
|
||||
let two = num::cast::<_, T>(2.0).unwrap();
|
||||
let d = two.mul_add(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2), T::zero());
|
||||
let ux = ((x1 * x1 + y1 * y1) * (y2 - y3)
|
||||
+ (x2 * x2 + y2 * y2) * (y3 - y1)
|
||||
+ (x3 * x3 + y3 * y3) * (y1 - y2))
|
||||
/ d;
|
||||
let uy = ((x1 * x1 + y1 * y1) * (x3 - x2)
|
||||
+ (x2 * x2 + y2 * y2) * (x1 - x3)
|
||||
+ (x3 * x3 + y3 * y3) * (x2 - x1))
|
||||
/ d;
|
||||
|
||||
let center = Point(ux, uy);
|
||||
(center, Self::distance(center, p1))
|
||||
}
|
||||
|
||||
pub fn distance(p1: Point<T>, p2: Point<T>) -> T {
|
||||
let dx = p2.0 - p1.0;
|
||||
let dy = p2.1 - p1.1;
|
||||
(dx * dx + dy * dy).sqrt()
|
||||
}
|
||||
}
|
228
src/slider_render.rs
Normal file
|
@ -0,0 +1,228 @@
|
|||
use std::collections::{HashMap, VecDeque};
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use ggez::{
|
||||
conf::NumSamples,
|
||||
graphics::{
|
||||
self, Canvas, Color, DrawMode, DrawParam, LineCap, LineJoin, Mesh, Rect, StrokeOptions,
|
||||
},
|
||||
nalgebra::Point2,
|
||||
Context,
|
||||
};
|
||||
use libosu::{Beatmap, HitObject, HitObjectKind, Point, SliderSplineKind};
|
||||
|
||||
use crate::math::Math;
|
||||
|
||||
pub fn render_slider(
|
||||
ctx: &mut Context,
|
||||
rect: Rect,
|
||||
beatmap: &Beatmap,
|
||||
slider: &HitObject,
|
||||
) -> Result<()> {
|
||||
let mut control_points = vec![slider.pos];
|
||||
let slider_info = match &slider.kind {
|
||||
HitObjectKind::Slider(info) => info,
|
||||
_ => panic!("retard"),
|
||||
};
|
||||
control_points.extend(&slider_info.control);
|
||||
|
||||
let spline = get_spline(&slider_info.kind, &control_points, slider_info.pixel_length);
|
||||
|
||||
let osupx_scale_x = rect.w as f64 / 512.0;
|
||||
let osupx_scale_y = rect.h as f64 / 384.0;
|
||||
let cs_osupx = 54.4 - 4.48 * beatmap.difficulty.circle_size as f64;
|
||||
let cs_real = cs_osupx * osupx_scale_x;
|
||||
|
||||
let (mut boundx, mut boundy, mut boundw, mut boundh) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
|
||||
let spline_mapped = spline
|
||||
.iter()
|
||||
.map(|point| {
|
||||
let (x, y) = (point.0, point.1);
|
||||
boundx = boundx.min(x - cs_osupx);
|
||||
boundy = boundy.min(y - cs_osupx);
|
||||
boundw = boundw.max(x + cs_osupx - boundx);
|
||||
boundh = boundh.max(y + cs_osupx - boundy);
|
||||
|
||||
let x2 = rect.x as f64 + osupx_scale_x * x;
|
||||
let y2 = rect.y as f64 + osupx_scale_y * y;
|
||||
[x2 as f32, y2 as f32].into()
|
||||
})
|
||||
.collect::<Vec<Point2<f32>>>();
|
||||
|
||||
let opts = StrokeOptions::default()
|
||||
.with_line_cap(LineCap::Round)
|
||||
.with_line_join(LineJoin::Round)
|
||||
.with_line_width(cs_real as f32 * 2.0);
|
||||
let mesh = Mesh::new_polyline(ctx, DrawMode::Stroke(opts), &spline_mapped, graphics::WHITE)?;
|
||||
graphics::draw(ctx, &mesh, DrawParam::default())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_spline(
|
||||
kind: &SliderSplineKind,
|
||||
control_points: &[Point<i32>],
|
||||
pixel_length: f64,
|
||||
) -> Vec<Point<f64>> {
|
||||
// no matter what, if there's 2 control points, it's linear
|
||||
let mut kind = kind.clone();
|
||||
if control_points.len() == 2 {
|
||||
kind = SliderSplineKind::Linear;
|
||||
}
|
||||
|
||||
let points = control_points
|
||||
.iter()
|
||||
.map(|p| Point(p.0 as f64, p.1 as f64))
|
||||
.collect();
|
||||
match kind {
|
||||
SliderSplineKind::Linear => points,
|
||||
SliderSplineKind::Perfect => {
|
||||
let (p1, p2, p3) = (points[0], points[1], points[2]);
|
||||
let (center, radius) = Math::circumcircle(p1, p2, p3);
|
||||
|
||||
// find the t-values of the start and end of the slider
|
||||
let t0 = (center.1 - p1.1).atan2(p1.0 - center.0);
|
||||
let mut t1 = (center.1 - p3.1).atan2(p3.0 - center.0);
|
||||
|
||||
// make sure t0 is less than t1
|
||||
let mut mid = (center.1 - p2.1).atan2(p2.0 - center.0);
|
||||
while mid < t0 {
|
||||
mid += 2.0 * std::f64::consts::PI
|
||||
}
|
||||
while t1 < mid {
|
||||
t1 += 2.0 * std::f64::consts::PI
|
||||
}
|
||||
|
||||
// circumference is 2*pi*r, slider length over circumference is length/(2*pi*r)
|
||||
let direction_unit = (t1 - t0) / (t1 - t0).abs();
|
||||
let new_t1 = t0 + direction_unit * (pixel_length / radius);
|
||||
|
||||
let mut t = t0;
|
||||
let mut c = Vec::new();
|
||||
loop {
|
||||
if !((new_t1 >= t0 && t < new_t1) || (new_t1 < t0 && t > new_t1)) {
|
||||
break;
|
||||
}
|
||||
|
||||
let rel = Point(t.cos() * radius, -t.sin() * radius);
|
||||
c.push(center + rel);
|
||||
|
||||
t += (new_t1 - t0) / pixel_length;
|
||||
}
|
||||
c
|
||||
}
|
||||
SliderSplineKind::Bezier => {
|
||||
let mut idx = 0;
|
||||
let mut whole = Vec::new();
|
||||
for i in 1..points.len() {
|
||||
if points[i].0 == points[i - 1].0 && points[i].1 == points[i - 1].1 {
|
||||
let spline = calculate_bezier(&points[idx..i - 1]);
|
||||
whole.extend(spline);
|
||||
idx = i;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let spline = calculate_bezier(&points[idx..]);
|
||||
whole.extend(spline);
|
||||
whole
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
type P = Point<f64>;
|
||||
type V<T> = (*mut T, usize, usize);
|
||||
fn calculate_bezier(points: &[P]) -> Vec<P> {
|
||||
let points = points.to_vec();
|
||||
let mut output = Vec::new();
|
||||
let n = points.len() - 1;
|
||||
let last = points[n];
|
||||
|
||||
let mut to_flatten = VecDeque::new();
|
||||
let mut free_buffers = VecDeque::new();
|
||||
|
||||
to_flatten.push_back(points.into_raw_parts());
|
||||
let mut p = n;
|
||||
let buf1 = vec![Point(0.0, 0.0); p + 1].into_raw_parts();
|
||||
let buf2 = vec![Point(0.0, 0.0); p * 2 + 1].into_raw_parts();
|
||||
|
||||
let left_child = buf2;
|
||||
while !to_flatten.is_empty() {
|
||||
let parent = to_flatten.pop_front().unwrap();
|
||||
let parent_slice = unsafe { std::slice::from_raw_parts_mut(parent.0, parent.1) };
|
||||
|
||||
if bezier_flat_enough(parent_slice) {
|
||||
bezier_approximate(parent_slice, &mut output, buf1, buf2, p + 1);
|
||||
free_buffers.push_front(parent);
|
||||
continue;
|
||||
}
|
||||
|
||||
let right_child = if free_buffers.is_empty() {
|
||||
let buf = vec![Point(0.0, 0.0); p + 1];
|
||||
buf.into_raw_parts()
|
||||
} else {
|
||||
free_buffers.pop_front().unwrap()
|
||||
};
|
||||
bezier_subdivide(parent_slice, left_child, right_child, buf1, p + 1);
|
||||
|
||||
let left_child = unsafe { std::slice::from_raw_parts(left_child.0, left_child.1) };
|
||||
for i in 0..p + 1 {
|
||||
parent_slice[i] = left_child[i];
|
||||
}
|
||||
|
||||
to_flatten.push_front(right_child);
|
||||
to_flatten.push_front(parent);
|
||||
}
|
||||
|
||||
output.push(last);
|
||||
output
|
||||
}
|
||||
|
||||
const TOLERANCE: f64 = 0.25;
|
||||
fn bezier_flat_enough(curve: &[P]) -> bool {
|
||||
for i in 1..(curve.len() - 1) {
|
||||
let p = curve[i - 1] - curve[i] * 2.0 + curve[i + 1];
|
||||
if p.0 * p.0 + p.1 * p.1 > TOLERANCE * TOLERANCE / 4.0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn bezier_approximate(curve: &[P], output: &mut Vec<P>, buf1: V<P>, buf2: V<P>, count: usize) {
|
||||
let l = buf2;
|
||||
let r = buf1;
|
||||
bezier_subdivide(curve, l, r, buf1, count);
|
||||
|
||||
let l = unsafe { std::slice::from_raw_parts_mut(l.0, l.1) };
|
||||
let r = unsafe { std::slice::from_raw_parts_mut(r.0, r.1) };
|
||||
for i in 0..(count - 1) {
|
||||
l[count + i] = r[i + 1];
|
||||
}
|
||||
output.push(curve[0]);
|
||||
|
||||
for i in 1..(count - 1) {
|
||||
let idx = 2 * i;
|
||||
let p = (l[idx - 1] + l[idx] * 2.0 + l[idx + 1]) * 0.25;
|
||||
output.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
fn bezier_subdivide(curve: &[P], l: V<P>, r: V<P>, subdiv: V<P>, count: usize) {
|
||||
let midpoints = unsafe { std::slice::from_raw_parts_mut(subdiv.0, subdiv.1) };
|
||||
for i in 0..count {
|
||||
midpoints[i] = curve[i];
|
||||
}
|
||||
|
||||
let l = unsafe { std::slice::from_raw_parts_mut(l.0, l.1) };
|
||||
let r = unsafe { std::slice::from_raw_parts_mut(r.0, r.1) };
|
||||
for i in 0..count {
|
||||
l[i] = midpoints[0];
|
||||
r[count - i - 1] = midpoints[count - i - 1];
|
||||
for j in 0..(count - i - 1) {
|
||||
midpoints[j] = (midpoints[j] + midpoints[j + 1]) * 0.5;
|
||||
}
|
||||
}
|
||||
}
|