Chord tinting

This commit is contained in:
Stepland 2023-07-18 03:13:46 +02:00
parent 054a608009
commit 3e863eaed7
14 changed files with 305 additions and 90 deletions

View File

@ -11,6 +11,7 @@
- Claps and Beats ticks should now be perfectly synced !
- Playfield
- Pressing `F` displays free buttons (highlights buttons where a new note would create a collision in red)
- Chords can now be displayed with a customizable color (Go to `Settings > Playfield`)
- Linear View
- notes can be selected by dragging a rectangle with the mouse
- new settings

View File

@ -0,0 +1,48 @@
uniform sampler2D texture;
uniform vec4 tint;
uniform float mix_amount;
float get_hsy_lightness(in vec3 color) {
return 0.299*color.r + 0.587*color.g + 0.114*color.b;
}
void add_hsy_lightness(inout vec3 color, float light) {
color.r += light;
color.g += light;
color.b += light;
float l = get_hsy_lightness(color);
float n = min(color.r, min(color.g, color.b));
float x = max(color.r, max(color.g, color.b));
if(n < 0.0) {
float iln = 1.0 / (l-n);
color.r = l + ((color.r-l) * l) * iln;
color.g = l + ((color.g-l) * l) * iln;
color.b = l + ((color.b-l) * l) * iln;
}
if(x > 1.0 && (x-l) > 1e-6) {
float il = 1.0 - l;
float ixl = 1.0 / (x - l);
color.r = l + ((color.r-l) * il) * ixl;
color.g = l + ((color.g-l) * il) * ixl;
color.b = l + ((color.b-l) * il) * ixl;
}
}
void set_hsy_lightness(inout vec3 color, float light) {
add_hsy_lightness(color, light - get_hsy_lightness(color));
}
void set_hsy_color(in vec3 src, inout vec3 dest) {
float lum = get_hsy_lightness(dest);
dest = src;
set_hsy_lightness(dest, lum);
}
void main() {
// lookup the pixel in the texture
vec4 pixel = texture2D(texture, gl_TexCoord[0].xy);
vec3 tinted_pixel = vec3(pixel);
set_hsy_color(tint.rgb, tinted_pixel);
vec3 mixed_rgb_pixel = mix(pixel.rgb, tinted_pixel, mix_amount);
gl_FragColor = gl_Color * vec4(mixed_rgb_pixel, pixel.a);
}

View File

@ -285,6 +285,24 @@ Interval<Fraction> ChartState::visible_beats(const sf::Time& playback_position,
void ChartState::update_visible_notes(const sf::Time& playback_position, const better::Timing& timing) {
const auto bounds = visible_beats(playback_position, timing);
visible_notes = chart.notes->between(bounds);
std::map<Fraction, unsigned int> counts;
std::for_each(
visible_notes.cbegin(),
visible_notes.cend(),
[&](const auto& it){
counts[it.second.get_time()] += 1;
}
);
visible_chords.clear();
std::for_each(
counts.begin(),
counts.end(),
[&](const auto& it){
if (it.second > 1) {
visible_chords.insert(it.first);
}
}
);
};
void ChartState::toggle_note(

View File

@ -58,6 +58,7 @@ struct ChartState {
Interval<Fraction> visible_beats(const sf::Time& playback_position, const better::Timing& timing);
void update_visible_notes(const sf::Time& playback_position, const better::Timing& timing);
better::Notes visible_notes;
std::set<Fraction> visible_chords;
void toggle_note(
const sf::Time& playback_position,

View File

@ -1,5 +1,6 @@
#include "config.hpp"
#include <algorithm>
#include <filesystem>
#include <SFML/Config.hpp>
@ -8,6 +9,7 @@
#include <toml++/toml.h>
#include <variant>
#include "color.hpp"
#include "linear_view_colors.hpp"
#include "marker.hpp"
#include "nowide/fstream.hpp"
@ -145,6 +147,9 @@ void config::Windows::load_from_v1_0_0_table(const toml::table& tbl) {
if (auto val = windows_table["show_playfield"].value<bool>()) {
show_playfield = *val;
}
if (auto val = windows_table["show_playfield_settings"].value<bool>()) {
show_playfield_settings = *val;
}
if (auto val = windows_table["show_file_properties"].value<bool>()) {
show_file_properties = *val;
}
@ -189,6 +194,7 @@ void config::Windows::load_from_v1_0_0_table(const toml::table& tbl) {
void config::Windows::dump_as_v1_0_0(toml::table& tbl) {
tbl.insert_or_assign("windows", toml::table{
{"show_playfield", show_playfield},
{"show_playfield_settings", show_playfield_settings},
{"show_file_properties", show_file_properties},
{"show_status", show_status},
{"show_playback_status", show_playback_status},
@ -206,6 +212,25 @@ void config::Windows::dump_as_v1_0_0(toml::table& tbl) {
}
void config::Playfield::load_from_v1_0_0_table(const toml::table& tbl) {
const auto playfield_table = tbl["playfield"];
if (auto val = playfield_table["color_chords"].value<bool>()) {
color_chords = *val;
}
load_color(playfield_table["chord_color"], chord_color);
if (auto val = playfield_table["chord_color_mix_amount"].value<float>()) {
chord_color_mix_amount = std::clamp(*val, 0.0f, 1.0f);
}
}
void config::Playfield::dump_as_v1_0_0(toml::table& tbl) {
tbl.insert_or_assign("playfield", toml::table{
{"color_chords", color_chords},
{"chord_color", dump_color(chord_color)},
{"chord_color_mix_amount", chord_color_mix_amount},
});
}
config::Config::Config(const std::filesystem::path& settings) :
config_path(settings / "config.toml")
{
@ -245,6 +270,7 @@ toml::table config::Config::dump_as_v1_0_0() {
editor.dump_as_v1_0_0(tbl);
sound.dump_as_v1_0_0(tbl);
windows.dump_as_v1_0_0(tbl);
playfield.dump_as_v1_0_0(tbl);
return tbl;
}
@ -266,4 +292,5 @@ void config::Config::load_from_v1_0_0_table(const toml::table& tbl) {
editor.load_from_v1_0_0_table(tbl);
sound.load_from_v1_0_0_table(tbl);
windows.load_from_v1_0_0_table(tbl);
playfield.load_from_v1_0_0_table(tbl);
}

View File

@ -1,5 +1,6 @@
#pragma once
#include <SFML/Graphics/Color.hpp>
#include <SFML/System/Time.hpp>
#include <filesystem>
@ -58,6 +59,7 @@ namespace config {
struct Windows {
bool show_playfield = true;
bool show_playfield_settings = false;
bool show_file_properties = false;
bool show_status = false;
bool show_playback_status = true;
@ -76,6 +78,15 @@ namespace config {
void dump_as_v1_0_0(toml::table& tbl);
};
struct Playfield {
bool color_chords = false;
sf::Color chord_color = sf::Color{110, 200, 250, 255};
float chord_color_mix_amount = 1.0f;
void load_from_v1_0_0_table(const toml::table& tbl);
void dump_as_v1_0_0(toml::table& tbl);
};
/* RAII-style class that holds settings we wish to save on disk and saves
them upon being destroyed */
class Config {
@ -90,6 +101,7 @@ namespace config {
Editor editor;
Sound sound;
Windows windows;
Playfield playfield;
private:
void load_from_v1_0_0_table(const toml::table& tbl);

View File

@ -25,6 +25,7 @@
#include <SFML/System/Vector2.hpp>
#include <tinyfiledialogs.h>
#include "config.hpp"
#include "custom_sfml_audio/synced_sound_streams.hpp"
#include "widgets/linear_view.hpp"
#include "better_metadata.hpp"
@ -54,6 +55,7 @@ EditorState::EditorState(const std::filesystem::path& assets_, config::Config& c
playfield(assets_),
linear_view(LinearView{assets_, config_}),
show_playfield(config_.windows.show_playfield),
show_playfield_settings(config_.windows.show_playfield_settings),
show_file_properties(config_.windows.show_file_properties),
show_status(config_.windows.show_status),
show_playback_status(config_.windows.show_playback_status),
@ -89,6 +91,7 @@ EditorState::EditorState(
playfield(assets_),
linear_view(LinearView{assets_, config_}),
show_playfield(config_.windows.show_playfield),
show_playfield_settings(config_.windows.show_playfield_settings),
show_file_properties(config_.windows.show_file_properties),
show_status(config_.windows.show_status),
show_playback_status(config_.windows.show_playback_status),
@ -494,7 +497,6 @@ void EditorState::display_playfield(const Markers::marker_type& opt_marker, Judg
float squareSize = ImGui::GetWindowSize().x / 4.f;
float TitlebarHeight = ImGui::GetWindowSize().y - ImGui::GetWindowSize().x;
int ImGuiIndex = 0;
if (chart_state and opt_marker) {
const auto& marker = **opt_marker;
@ -514,26 +516,39 @@ void EditorState::display_playfield(const Markers::marker_type& opt_marker, Judg
auto display = VariantVisitor {
[&, this](const better::TapNote& tap_note){
auto note_offset = (this->current_time() - this->time_at(tap_note.get_time()));
const auto t = marker.at(markerEndingState, note_offset);
auto t = marker.at(markerEndingState, note_offset);
if (t) {
ImGui::SetCursorPos({
tap_note.get_position().get_x() * squareSize,
TitlebarHeight + tap_note.get_position().get_y() * squareSize
});
ImGui::PushID(ImGuiIndex);
ImGui::Image(*t, {squareSize, squareSize});
ImGui::PopID();
++ImGuiIndex;
if (config.playfield.color_chords and chart_state->visible_chords.contains(tap_note.get_time())) {
playfield.draw_chord_tap_note(
tap_note,
current_time(),
*applicable_timing,
marker,
markerEndingState,
config.playfield
);
} else {
ImGui::SetCursorPos({
tap_note.get_position().get_x() * squareSize,
TitlebarHeight + tap_note.get_position().get_y() * squareSize
});
ImGui::Image(*t, {squareSize, squareSize});
}
}
},
[&, this](const better::LongNote& long_note){
std::optional<config::Playfield> chord_config;
if (config.playfield.color_chords and chart_state->visible_chords.contains(long_note.get_time())) {
chord_config = config.playfield;
}
this->playfield.draw_long_note(
long_note,
current_time(),
*applicable_timing,
marker,
markerEndingState
markerEndingState,
chord_config
);
},
};
@ -545,7 +560,9 @@ void EditorState::display_playfield(const Markers::marker_type& opt_marker, Judg
ImGui::SetCursorPos({0, TitlebarHeight});
ImGui::Image(playfield.long_note.layer);
ImGui::SetCursorPos({0, TitlebarHeight});
ImGui::Image(playfield.marker_layer);
ImGui::Image(playfield.long_note_marker_layer);
ImGui::SetCursorPos({0, TitlebarHeight});
ImGui::Image(playfield.chord_marker_layer);
}
// Display button grid
@ -621,10 +638,7 @@ void EditorState::display_playfield(const Markers::marker_type& opt_marker, Judg
int x = i % 4;
int y = i / 4;
ImGui::SetCursorPos({x * squareSize, TitlebarHeight + y * squareSize});
ImGui::PushID(ImGuiIndex);
ImGui::Image(playfield.note_collision, {squareSize, squareSize});
ImGui::PopID();
++ImGuiIndex;
}
}
@ -635,10 +649,7 @@ void EditorState::display_playfield(const Markers::marker_type& opt_marker, Judg
note.get_position().get_x() * squareSize,
TitlebarHeight + note.get_position().get_y() * squareSize
});
ImGui::PushID(ImGuiIndex);
ImGui::Image(playfield.note_selected, {squareSize, squareSize});
ImGui::PopID();
++ImGuiIndex;
}
}
}
@ -646,9 +657,18 @@ void EditorState::display_playfield(const Markers::marker_type& opt_marker, Judg
ImGui::End();
};
/*
Display all metadata in an editable form
*/
void EditorState::display_playfield_settings() {
if (ImGui::Begin("Playfield Settings", &show_playfield_settings, ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::Checkbox("Color Chords", &config.playfield.color_chords);
feis::DisabledIf(not config.playfield.color_chords, [&](){
feis::ColorEdit4("Chord Color", config.playfield.chord_color);
ImGui::SliderFloat("Chord Color Mix Amount", &config.playfield.chord_color_mix_amount, 0.0f, 1.0f);
});
}
ImGui::End();
}
// Display all metadata in an editable form
void EditorState::display_file_properties() {
if (ImGui::Begin(
"File Properties",
@ -1100,16 +1120,11 @@ void EditorState::display_linear_view() {
void EditorState::display_sound_settings() {
if (ImGui::Begin("Sound Settings", &show_sound_settings)) {
if (ImGui::TreeNodeEx("Music", ImGuiTreeNodeFlags_DefaultOpen)) {
const auto music_exists = music.has_value();
if (not music_exists) {
ImGui::BeginDisabled();
}
if (ImGui::SliderInt("Volume##Music", &config.sound.music_volume, 0, 10)) {
set_volume(config.sound.music_volume);
}
if (not music_exists) {
ImGui::EndDisabled();
}
feis::DisabledIf(not music.has_value(), [&](){
if (ImGui::SliderInt("Volume##Music", &config.sound.music_volume, 0, 10)) {
set_volume(config.sound.music_volume);
}
});
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Beat Tick", ImGuiTreeNodeFlags_DefaultOpen)) {

View File

@ -145,6 +145,9 @@ public:
bool& show_playfield;
void display_playfield(const Markers::marker_type& marker, Judgement markerEndingState);
bool& show_playfield_settings;
void display_playfield_settings();
bool& show_file_properties;
void display_file_properties();

View File

@ -68,6 +68,17 @@ namespace feis {
bool SquareButton(const char* text);
void ColorSquare(const sf::Color& color);
template<typename Callback>
void DisabledIf(const bool disabled, const Callback& cb) {
if (disabled) {
ImGui::BeginDisabled();
}
cb();
if (disabled) {
ImGui::EndDisabled();
}
}
}
namespace colors {

View File

@ -452,6 +452,9 @@ int main() {
if (editor_state->chart_state and editor_state->show_playfield) {
editor_state->display_playfield(markers.get_chosen_marker(), markerEndingState);
}
if (editor_state->show_playfield_settings) {
editor_state->display_playfield_settings();
}
if (editor_state->show_linear_view) {
editor_state->display_linear_view();
}
@ -730,6 +733,9 @@ int main() {
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Settings", editor_state.has_value())) {
if (ImGui::MenuItem("Playfield##Settings")) {
editor_state->show_playfield_settings = true;
}
if (ImGui::MenuItem("Sound")) {
editor_state->show_sound_settings = true;
}

View File

@ -1,5 +1,10 @@
#include "playfield.hpp"
#include <filesystem>
#include <stdexcept>
#include <variant>
#include "better_note.hpp"
#include "config.hpp"
#include "toolbox.hpp"
#include "utf8_strings.hpp"
@ -8,8 +13,19 @@ const std::string texture_file = "textures/edit_textures/game_front_edit_tex_1.t
Playfield::Playfield(std::filesystem::path assets_folder) :
long_note(assets_folder / "textures" / "long"),
texture_path(assets_folder / texture_file)
{
if (!base_texture.load_from_path(texture_path)) {
{
if (sf::Shader::isAvailable()) {
chord_tint_shader.emplace();
const auto shader_path = assets_folder / "shaders" / "chord_tint.frag";
if (not std::filesystem::is_regular_file(shader_path)) {
throw std::runtime_error(fmt::format("File {} does not exist", path_to_utf8_encoded_string(shader_path)));
}
if (not chord_tint_shader->load_from_path(assets_folder / "shaders" / "chord_tint.frag", sf::Shader::Fragment)) {
throw std::runtime_error(fmt::format("Could not open fragment shader {}", path_to_utf8_encoded_string(shader_path)));
};
chord_tint_shader->setUniform("texture", sf::Shader::CurrentTexture);
}
if (not base_texture.load_from_path(texture_path)) {
std::cerr << "Unable to load texture " << texture_path;
throw std::runtime_error("Unable to load texture " + path_to_utf8_encoded_string(texture_path));
}
@ -27,48 +43,36 @@ Playfield::Playfield(std::filesystem::path assets_folder) :
note_collision.setTexture(base_texture);
note_collision.setTextureRect({576, 0, 192, 192});
if (!marker_layer.create(400, 400)) {
std::cerr << "Unable to create Playfield's markerLayer";
throw std::runtime_error("Unable to create Playfield's markerLayer");
if (not long_note_marker_layer.create(400, 400)) {
throw std::runtime_error("Unable to create Playfield's long_note_marker_layer");
}
marker_layer.setSmooth(true);
long_note_marker_layer.setSmooth(true);
if (!long_note.layer.create(400, 400)) {
std::cerr << "Unable to create Playfield's longNoteLayer";
throw std::runtime_error("Unable to create Playfield's longNoteLayer");
if (not long_note.layer.create(400, 400)) {
throw std::runtime_error("Unable to create Playfield's long_note.laye");
}
long_note.layer.setSmooth(true);
// why do we do this here ?
long_note.backgroud.setTexture(*long_note.marker.background_at(sf::Time::Zero));
long_note.outline.setTexture(*long_note.marker.outline_at(sf::Time::Zero));
long_note.highlight.setTexture(*long_note.marker.highlight_at(sf::Time::Zero));
long_note.tail.setTexture(*long_note.marker.tail_at(sf::Time::Zero));
long_note.triangle.setTexture(*long_note.marker.triangle_at(sf::Time::Zero));
if (not chord_marker_layer.create(400, 400)) {
throw std::runtime_error("Unable to create Playfield's chord_marker_layer");
}
chord_marker_layer.setSmooth(true);
}
void Playfield::resize(unsigned int width) {
if (long_note.layer.getSize() != sf::Vector2u(width, width)) {
if (!long_note.layer.create(width, width)) {
std::cerr << "Unable to resize Playfield's longNoteLayer";
throw std::runtime_error(
"Unable to resize Playfield's longNoteLayer");
const auto _resize = [](auto& tex, unsigned int width){
if (tex.getSize() != sf::Vector2u(width, width)) {
if (not tex.create(width, width)) {
throw std::runtime_error("Unable to resize Playfield texture");
}
tex.setSmooth(true);
}
long_note.layer.setSmooth(true);
}
tex.clear(sf::Color::Transparent);
};
long_note.layer.clear(sf::Color::Transparent);
if (marker_layer.getSize() != sf::Vector2u(width, width)) {
if (!marker_layer.create(width, width)) {
std::cerr << "Unable to resize Playfield's markerLayer";
throw std::runtime_error(
"Unable to resize Playfield's markerLayer");
}
marker_layer.setSmooth(true);
}
marker_layer.clear(sf::Color::Transparent);
_resize(long_note.layer, width);
_resize(long_note_marker_layer, width);
_resize(chord_marker_layer, width);
}
void Playfield::draw_tail_and_receptor(
@ -196,33 +200,31 @@ void Playfield::draw_long_note(
const sf::Time& playback_position,
const better::Timing& timing,
const Marker& marker,
const Judgement& markerEndingState
const Judgement& marker_ending_state,
const std::optional<config::Playfield>& chord_config
) {
draw_tail_and_receptor(note, playback_position, timing);
const float square_size = static_cast<float>(long_note.layer.getSize().x) / 4;
const auto note_time = timing.time_at(note.get_time());
const auto note_offset = playback_position - note_time;
const auto tail_end = timing.time_at(note.get_end());
if (playback_position < tail_end) {
// Before or During the long note
// Display the beginning marker
auto t = marker.at(markerEndingState, note_offset);
if (t) {
const float x_scale = square_size / t->getTextureRect().width;
const float y_scale = square_size / t->getTextureRect().height;
t->setScale(x_scale, y_scale);
t->setPosition(
note.get_position().get_x() * square_size,
note.get_position().get_y() * square_size
);
marker_layer.draw(*t);
const auto offset = [&](){
if (playback_position < tail_end) {
return playback_position - note_time;
} else {
return playback_position - tail_end;
}
}();
if (chord_config.has_value()) {
draw_chord_tap_note(
offset,
note.get_position(),
marker,
marker_ending_state,
*chord_config
);
} else {
const auto tail_end_offset = playback_position - tail_end;
auto t = marker.at(markerEndingState, tail_end_offset);
auto t = marker.at(marker_ending_state, offset);
if (t) {
const float x_scale = square_size / t->getTextureRect().width;
const float y_scale = square_size / t->getTextureRect().height;
@ -231,7 +233,54 @@ void Playfield::draw_long_note(
note.get_position().get_x() * square_size,
note.get_position().get_y() * square_size
);
marker_layer.draw(*t);
long_note_marker_layer.draw(*t);
}
}
}
void Playfield::draw_chord_tap_note(
const better::TapNote& note,
const sf::Time& playback_position,
const better::Timing& timing,
const Marker& marker,
const Judgement& marker_ending_state,
const config::Playfield& chord_config
) {
const auto note_time = timing.time_at(note.get_time());
const auto note_offset = playback_position - note_time;
draw_chord_tap_note(
note_offset,
note.get_position(),
marker,
marker_ending_state,
chord_config
);
}
void Playfield::draw_chord_tap_note(
const sf::Time& offset,
const better::Position& position,
const Marker& marker,
const Judgement& marker_ending_state,
const config::Playfield& chord_config
) {
const float square_size = static_cast<float>(chord_marker_layer.getSize().x) / 4;
auto t = marker.at(marker_ending_state, offset);
if (t) {
const float x_scale = square_size / t->getTextureRect().width;
const float y_scale = square_size / t->getTextureRect().height;
t->setScale(x_scale, y_scale);
t->setPosition(
position.get_x() * square_size,
position.get_y() * square_size
);
if (chord_tint_shader) {
chord_tint_shader->setUniform("tint", sf::Glsl::Vec4(chord_config.chord_color));
chord_tint_shader->setUniform("mix_amount", chord_config.chord_color_mix_amount);
chord_marker_layer.draw(*t, &chord_tint_shader.value());
} else {
t->setColor(chord_config.chord_color);
chord_marker_layer.draw(*t);
}
}
}

View File

@ -9,6 +9,7 @@
#include "better_note.hpp"
#include "better_timing.hpp"
#include "config.hpp"
#include "ln_marker.hpp"
#include "marker.hpp"
#include "utf8_sfml_redefinitions.hpp"
@ -22,8 +23,8 @@ public:
sf::Sprite note_selected;
sf::Sprite note_collision;
sf::RenderTexture marker_layer;
sf::Sprite marker_sprite;
sf::RenderTexture long_note_marker_layer;
sf::RenderTexture chord_marker_layer;
struct LongNote {
template<typename ...Ts>
@ -50,12 +51,32 @@ public:
void draw_long_note(
const better::LongNote& note,
const sf::Time& playback_position,
const better::Timing& timing,
const Marker& marker,
const Judgement& marker_ending_state,
const std::optional<config::Playfield>& config
);
void draw_chord_tap_note(
const better::TapNote& note,
const sf::Time& playbackPosition,
const better::Timing& timing,
const Marker& marker,
const Judgement& markerEndingState
const Judgement& markerEndingState,
const config::Playfield& config
);
private:
void draw_chord_tap_note(
const sf::Time& offset,
const better::Position& position,
const Marker& marker,
const Judgement& marker_ending_state,
const config::Playfield& config
);
const std::filesystem::path texture_path;
std::optional<feis::Shader> chord_tint_shader;
};

View File

@ -19,12 +19,13 @@ namespace feis {
template<class T>
class UTF8Loader : public T {
public:
bool load_from_path(const std::filesystem::path& file) {
template<typename... Ts>
bool load_from_path(const std::filesystem::path& file, const Ts&... args) {
UTF8FileInputStream f;
if (not f.open(file)) {
return false;
}
return this->loadFromStream(f);
return this->loadFromStream(f, args...);
}
};

View File

@ -3,6 +3,7 @@
#include <SFML/Audio.hpp>
#include <SFML/Audio/InputSoundFile.hpp>
#include <SFML/Audio/SoundBuffer.hpp>
#include <SFML/Graphics/Shader.hpp>
#include <SFML/Graphics/Texture.hpp>
#include "utf8_sfml.hpp"
@ -12,4 +13,5 @@ namespace feis {
using InputSoundFile = feis::UTF8Streamer<sf::InputSoundFile>;
using SoundBuffer = feis::UTF8Loader<sf::SoundBuffer>;
using Texture = feis::UTF8Loader<sf::Texture>;
using Shader = feis::UTF8Loader<sf::Shader>;
}