Initial public khodo snapshot
ci / checks (push) Successful in 38m58s

This commit is contained in:
khodowany
2026-09-04 19:30:11 -05:00
commit 4f9a269f85
99 changed files with 59585 additions and 0 deletions
+632
View File
@@ -0,0 +1,632 @@
#include <vamp_zero/score.hpp>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include <raylib.h>
#include <vamp_style/font.hpp>
#include <vamp_zero/color.hpp>
namespace {
constexpr auto whole_note_ticks = std::int64_t{16384};
constexpr auto half_note_ticks = std::int64_t{8192};
constexpr auto quarter_note_ticks = std::int64_t{4096};
constexpr auto eighth_note_ticks = std::int64_t{2048};
enum class duration_base {
WHOLE,
HALF,
QUARTER,
EIGHTH,
};
struct duration_shape {
duration_base base{duration_base::QUARTER};
std::int32_t dots{};
std::int64_t ticks{quarter_note_ticks};
};
struct smufl_box {
std::float32_t west{};
std::float32_t south{};
std::float32_t east{};
std::float32_t north{};
};
struct smufl_anchor {
std::float32_t x{};
std::float32_t y{};
};
struct notehead_instance {
std::float32_t center_y{};
luma::point origin{};
};
constexpr auto glyph_notehead_whole = std::int32_t{0xe0a2};
constexpr auto glyph_notehead_half = std::int32_t{0xe0a3};
constexpr auto glyph_notehead_black = std::int32_t{0xe0a4};
constexpr auto glyph_notehead_slash = std::int32_t{0xe101};
constexpr auto glyph_rest_whole = std::int32_t{0xe4e3};
constexpr auto glyph_rest_half = std::int32_t{0xe4e4};
constexpr auto glyph_rest_quarter = std::int32_t{0xe4e5};
constexpr auto glyph_rest_eighth = std::int32_t{0xe4e6};
constexpr auto glyph_flag_eighth_up = std::int32_t{0xe240};
constexpr auto glyph_flag_eighth_down = std::int32_t{0xe241};
constexpr auto glyph_augmentation_dot = std::int32_t{0xe1e7};
constexpr auto glyph_accidental_flat = std::int32_t{0xe260};
constexpr auto glyph_accidental_natural = std::int32_t{0xe261};
constexpr auto glyph_accidental_sharp = std::int32_t{0xe262};
auto to_raylib(luma::color color) -> Color {
return {
.r = color.r,
.g = color.g,
.b = color.b,
.a = color.a,
};
}
auto encode_utf8(std::int32_t codepoint) -> std::string {
auto output = std::string{};
if (codepoint <= 0x7f) {
output.push_back(static_cast<std::char_t>(codepoint));
} else if (codepoint <= 0x7ff) {
output.push_back(static_cast<std::char_t>(0xc0 | ((codepoint >> 6) & 0x1f)));
output.push_back(static_cast<std::char_t>(0x80 | (codepoint & 0x3f)));
} else if (codepoint <= 0xffff) {
output.push_back(static_cast<std::char_t>(0xe0 | ((codepoint >> 12) & 0x0f)));
output.push_back(static_cast<std::char_t>(0x80 | ((codepoint >> 6) & 0x3f)));
output.push_back(static_cast<std::char_t>(0x80 | (codepoint & 0x3f)));
} else {
output.push_back(static_cast<std::char_t>(0xf0 | ((codepoint >> 18) & 0x07)));
output.push_back(static_cast<std::char_t>(0x80 | ((codepoint >> 12) & 0x3f)));
output.push_back(static_cast<std::char_t>(0x80 | ((codepoint >> 6) & 0x3f)));
output.push_back(static_cast<std::char_t>(0x80 | (codepoint & 0x3f)));
}
return output;
}
auto music_font(std::float32_t size) -> Font {
auto const key = std::max(std::int32_t{1}, static_cast<std::int32_t>(std::ceil(size)));
static auto fonts = std::unordered_map<std::int32_t, Font>{};
if (auto found = fonts.find(key); found != fonts.end()) {
return found->second;
}
auto codepoints = std::vector<std::int32_t>{
glyph_notehead_whole,
glyph_notehead_half,
glyph_notehead_black,
glyph_notehead_slash,
glyph_rest_whole,
glyph_rest_half,
glyph_rest_quarter,
glyph_rest_eighth,
glyph_flag_eighth_up,
glyph_flag_eighth_down,
glyph_augmentation_dot,
glyph_accidental_flat,
glyph_accidental_natural,
glyph_accidental_sharp,
};
auto path = std::string{vamp_style::score_music_font_path};
auto loaded = LoadFontEx(path.c_str(), key, codepoints.data(), static_cast<std::int32_t>(codepoints.size()));
if (loaded.texture.id == 0) {
return GetFontDefault();
}
auto inserted = fonts.emplace(key, loaded);
return inserted.first->second;
}
auto glyph_box(std::int32_t codepoint) -> smufl_box {
switch (codepoint) {
case glyph_notehead_whole:
return {.west = 0.0F, .south = -0.744684F, .east = 1.520666F, .north = 0.66F};
case glyph_notehead_half:
return {.west = 0.0F, .south = -0.696F, .east = 1.336F, .north = 0.7F};
case glyph_notehead_black:
return {.west = 0.0F, .south = -0.656F, .east = 1.336186F, .north = 0.656F};
case glyph_notehead_slash:
return {.west = 0.0F, .south = -1.016F, .east = 1.952F, .north = 1.016F};
case glyph_rest_whole:
return {.west = 0.003563F, .south = -0.48F, .east = 2.0F, .north = 0.056F};
case glyph_rest_half:
return {.west = 0.0F, .south = 0.0F, .east = 2.073599F, .north = 0.648F};
case glyph_rest_quarter:
return {.west = -0.001514F, .south = -1.66F, .east = 1.052F, .north = 1.66F};
case glyph_rest_eighth:
return {.west = 0.0F, .south = -1.04F, .east = 1.156F, .north = 1.044F};
case glyph_flag_eighth_up:
return {.west = 0.0F, .south = -3.276F, .east = 1.044F, .north = 0.0F};
case glyph_flag_eighth_down:
return {.west = 0.0F, .south = 0.0F, .east = 1.044F, .north = 3.276F};
case glyph_augmentation_dot:
return {.west = 0.0F, .south = -0.228F, .east = 0.384F, .north = 0.228F};
case glyph_accidental_flat:
return {.west = 0.004F, .south = -0.832F, .east = 0.836F, .north = 1.888F};
case glyph_accidental_natural:
return {.west = 0.0F, .south = -1.824F, .east = 0.854F, .north = 1.848F};
case glyph_accidental_sharp:
return {.west = -0.088F, .south = -1.536F, .east = 1.56F, .north = 1.532F};
default:
return {};
}
}
auto glyph_center_x(std::int32_t codepoint) -> std::float32_t {
auto box = glyph_box(codepoint);
return (box.west + box.east) * 0.5F;
}
auto glyph_center_y(std::int32_t codepoint) -> std::float32_t {
auto box = glyph_box(codepoint);
return (box.south + box.north) * 0.5F;
}
auto glyph_width(std::int32_t codepoint, std::float32_t staff_spacing) -> std::float32_t {
auto box = glyph_box(codepoint);
return (box.east - box.west) * staff_spacing;
}
auto draw_music_origin(std::int32_t codepoint, luma::point origin, std::float32_t staff_spacing, luma::color color) -> void {
auto font_size = staff_spacing * 4.0F;
auto font = music_font(font_size);
auto glyph = GetGlyphInfo(font, codepoint);
auto scale = font_size / std::max(1.0F, static_cast<std::float32_t>(font.baseSize));
auto box = glyph_box(codepoint);
auto screen_left = origin.x + box.west * staff_spacing;
auto screen_top = origin.y - box.north * staff_spacing;
auto draw_x = screen_left - static_cast<std::float32_t>(glyph.offsetX) * scale + static_cast<std::float32_t>(font.glyphPadding) * scale;
auto draw_y = screen_top - static_cast<std::float32_t>(glyph.offsetY) * scale + static_cast<std::float32_t>(font.glyphPadding) * scale;
auto text = encode_utf8(codepoint);
DrawTextEx(font, text.c_str(), {.x = draw_x, .y = draw_y}, font_size, 0.0F, to_raylib(color));
}
auto origin_for_center(std::int32_t codepoint, std::float32_t center_x, std::float32_t center_y, std::float32_t staff_spacing) -> luma::point {
return {
.x = center_x - glyph_center_x(codepoint) * staff_spacing,
.y = center_y + glyph_center_y(codepoint) * staff_spacing,
};
}
auto draw_music_centered(std::int32_t codepoint, std::float32_t center_x, std::float32_t center_y, std::float32_t staff_spacing, luma::color color) -> void {
draw_music_origin(codepoint, origin_for_center(codepoint, center_x, center_y, staff_spacing), staff_spacing, color);
}
auto draw_music_centered_at_origin_y(std::int32_t codepoint, std::float32_t center_x, std::float32_t origin_y, std::float32_t staff_spacing, luma::color color) -> void {
draw_music_origin(codepoint, {.x = center_x - glyph_center_x(codepoint) * staff_spacing, .y = origin_y}, staff_spacing, color);
}
auto draw_music_left_of(std::int32_t codepoint, std::float32_t right_x, std::float32_t center_y, std::float32_t staff_spacing, luma::color color) -> void {
auto box = glyph_box(codepoint);
auto origin = luma::point{
.x = right_x - box.east * staff_spacing,
.y = center_y + glyph_center_y(codepoint) * staff_spacing,
};
draw_music_origin(codepoint, origin, staff_spacing, color);
}
auto anchor_point(luma::point origin, smufl_anchor anchor, std::float32_t staff_spacing) -> luma::point {
return {
.x = origin.x + anchor.x * staff_spacing,
.y = origin.y - anchor.y * staff_spacing,
};
}
auto stem_up_anchor(std::int32_t notehead) -> smufl_anchor {
switch (notehead) {
case glyph_notehead_half:
return {.x = 1.312F, .y = 0.284F};
case glyph_notehead_slash:
return {.x = 1.952F, .y = 0.884F};
case glyph_notehead_black:
default:
return {.x = 1.336F, .y = 0.288F};
}
}
auto stem_down_anchor(std::int32_t notehead) -> smufl_anchor {
switch (notehead) {
case glyph_notehead_half:
return {.x = 0.0F, .y = -0.252F};
case glyph_notehead_slash:
return {.x = 0.0F, .y = -0.928F};
case glyph_notehead_black:
default:
return {.x = 0.0F, .y = -0.236F};
}
}
auto draw_eighth_flag(std::bool_t stem_up, std::float32_t stem_x, std::float32_t stem_tip_y, std::float32_t staff_spacing, luma::color color) -> void {
auto codepoint = stem_up ? glyph_flag_eighth_up : glyph_flag_eighth_down;
auto anchor = stem_up ? smufl_anchor{.x = 0.0F, .y = -0.176F} : smufl_anchor{.x = 0.0F, .y = 0.176F};
auto origin = luma::point{
.x = stem_x - anchor.x * staff_spacing,
.y = stem_tip_y + anchor.y * staff_spacing,
};
draw_music_origin(codepoint, origin, staff_spacing, color);
}
auto text_style(std::int32_t scale) -> luma::text_style {
auto size = 14.0F;
if (scale == 2) {
size = 22.0F;
} else if (scale >= 3) {
size = 30.0F;
}
return {
.font = std::string{vamp_style::score_text_font_path},
.size = size,
.line_height = size + 5.0F,
};
}
auto draw_text(luma::draw_context& context, std::float32_t x, std::float32_t y, std::string const& text, std::int32_t scale, vamp_style::color color, std::float32_t max_width) -> void {
if (text.empty() || max_width <= 0.0F) {
return;
}
auto style = text_style(scale);
context.push_clip({.x = x, .y = y, .width = max_width, .height = style.line_height});
context.draw_text(text, {.x = x, .y = y}, style, vamp_zero::to_luma(color));
context.pop_clip();
}
auto duration_for(std::int64_t ticks) -> duration_shape {
if (ticks >= whole_note_ticks) {
return {.base = duration_base::WHOLE, .dots = 0, .ticks = whole_note_ticks};
}
if (ticks >= half_note_ticks + quarter_note_ticks) {
return {.base = duration_base::HALF, .dots = 1, .ticks = half_note_ticks + quarter_note_ticks};
}
if (ticks >= half_note_ticks) {
return {.base = duration_base::HALF, .dots = 0, .ticks = half_note_ticks};
}
if (ticks >= quarter_note_ticks + eighth_note_ticks) {
return {.base = duration_base::QUARTER, .dots = 1, .ticks = quarter_note_ticks + eighth_note_ticks};
}
if (ticks >= quarter_note_ticks) {
return {.base = duration_base::QUARTER, .dots = 0, .ticks = quarter_note_ticks};
}
return {.base = duration_base::EIGHTH, .dots = 0, .ticks = eighth_note_ticks};
}
auto spacing_ticks(vamp_zero::score_note const& note) -> std::int64_t {
return std::max<std::int64_t>(1, note.duration_ticks);
}
auto display_ticks(vamp_zero::score_note const& note) -> std::int64_t {
return note.display_ticks <= 0 ? note.duration_ticks : note.display_ticks;
}
auto measure_ticks(vamp_zero::score_measure const& measure) -> std::int64_t {
auto total = std::int64_t{};
for (auto const& note : measure.notes) {
total += spacing_ticks(note);
}
return std::max(whole_note_ticks, total);
}
auto has_stem(duration_shape duration) -> std::bool_t {
return duration.base != duration_base::WHOLE;
}
auto has_flag(duration_shape duration) -> std::bool_t {
return duration.base == duration_base::EIGHTH;
}
auto letter_index(std::char_t letter) -> std::int32_t {
switch (letter) {
case 'c':
case 'C':
return 0;
case 'd':
case 'D':
return 1;
case 'e':
case 'E':
return 2;
case 'f':
case 'F':
return 3;
case 'g':
case 'G':
return 4;
case 'a':
case 'A':
return 5;
case 'b':
case 'B':
return 6;
default:
return 0;
}
}
auto pitch_y(vamp_zero::score_pitch const& pitch, std::float32_t staff_top, std::float32_t staff_spacing) -> std::float32_t {
constexpr auto e4_index = std::int32_t{30};
auto pitch_index = pitch.octave * 7 + letter_index(pitch.letter);
auto bottom_line = staff_top + staff_spacing * 4.0F;
return bottom_line - static_cast<std::float32_t>(pitch_index - e4_index) * (staff_spacing * 0.5F);
}
auto accidental_glyph(std::string const& accidental) -> std::optional<std::int32_t> {
if (accidental == "#" || accidental == "sharp") {
return glyph_accidental_sharp;
}
if (accidental == "b" || accidental == "flat") {
return glyph_accidental_flat;
}
if (accidental == "natural" || accidental == "n") {
return glyph_accidental_natural;
}
return std::nullopt;
}
auto draw_curve(luma::draw_context& context, luma::point start, luma::point control, luma::point end, std::float32_t thickness, luma::color color) -> void {
constexpr auto segments = std::int32_t{10};
auto previous = start;
for (auto index = std::int32_t{1}; index <= segments; ++index) {
auto t = static_cast<std::float32_t>(index) / static_cast<std::float32_t>(segments);
auto omt = 1.0F - t;
auto current = luma::point{
.x = omt * omt * start.x + 2.0F * omt * t * control.x + t * t * end.x,
.y = omt * omt * start.y + 2.0F * omt * t * control.y + t * t * end.y,
};
context.stroke_line(previous, current, thickness, color);
previous = current;
}
}
auto draw_staff(luma::draw_context& context, std::float32_t x, std::float32_t y, std::float32_t width, std::float32_t spacing) -> void {
auto ink = vamp_zero::to_luma(vamp_style::paper_ink);
for (auto line = std::int32_t{}; line < 5; ++line) {
auto yy = y + static_cast<std::float32_t>(line) * spacing;
context.stroke_line({.x = x, .y = yy}, {.x = x + width, .y = yy}, 1.15F, ink);
}
}
auto draw_duration_dot(luma::draw_context& context, std::float32_t x, std::float32_t y, std::float32_t spacing, luma::color ink) -> void {
static_cast<void>(context);
draw_music_centered(glyph_augmentation_dot, x, y, spacing, ink);
}
auto dot_y_for(std::float32_t note_y, std::float32_t staff_top, std::float32_t spacing) -> std::float32_t {
auto half_step = (note_y - staff_top) / (spacing * 0.5F);
auto rounded = std::round(half_step);
if (std::abs(half_step - rounded) < 0.08F && static_cast<std::int32_t>(rounded) % 2 == 0) {
return note_y - spacing * 0.5F;
}
return note_y;
}
auto draw_ledger_lines(luma::draw_context& context, std::float32_t x, std::float32_t note_y, std::float32_t staff_top, std::float32_t spacing, luma::color ink) -> void {
auto bottom = staff_top + spacing * 4.0F;
if (note_y < staff_top) {
for (auto yy = staff_top - spacing; yy >= note_y - 0.1F; yy -= spacing) {
context.stroke_line({.x = x - 12.0F, .y = yy}, {.x = x + 12.0F, .y = yy}, 1.1F, ink);
}
} else if (note_y > bottom) {
for (auto yy = bottom + spacing; yy <= note_y + 0.1F; yy += spacing) {
context.stroke_line({.x = x - 12.0F, .y = yy}, {.x = x + 12.0F, .y = yy}, 1.1F, ink);
}
}
}
auto draw_rest(luma::draw_context& context, duration_shape duration, std::float32_t x, std::float32_t staff_top, std::float32_t spacing) -> void {
static_cast<void>(context);
auto ink = vamp_zero::to_luma(vamp_style::paper_ink);
auto center = staff_top + spacing * 2.0F;
auto codepoint = glyph_rest_quarter;
auto origin_y = center;
auto centered = false;
switch (duration.base) {
case duration_base::WHOLE:
codepoint = glyph_rest_whole;
origin_y = staff_top + spacing;
break;
case duration_base::HALF:
codepoint = glyph_rest_half;
origin_y = staff_top + spacing * 2.0F;
break;
case duration_base::QUARTER:
codepoint = glyph_rest_quarter;
centered = true;
break;
case duration_base::EIGHTH:
codepoint = glyph_rest_eighth;
centered = true;
break;
}
if (centered) {
draw_music_centered(codepoint, x, center, spacing, ink);
} else {
draw_music_centered_at_origin_y(codepoint, x, origin_y, spacing, ink);
}
if (duration.dots != 0) {
draw_duration_dot(context, x + glyph_width(codepoint, spacing) * 0.5F + spacing, center, spacing, ink);
}
}
auto notehead_codepoint(duration_shape duration, std::bool_t slash) -> std::int32_t {
if (slash) {
return glyph_notehead_slash;
}
switch (duration.base) {
case duration_base::WHOLE:
return glyph_notehead_whole;
case duration_base::HALF:
return glyph_notehead_half;
case duration_base::QUARTER:
case duration_base::EIGHTH:
return glyph_notehead_black;
}
return glyph_notehead_black;
}
auto draw_notehead(duration_shape duration, std::bool_t slash, std::float32_t x, std::float32_t y, std::float32_t spacing, luma::color ink) -> notehead_instance {
auto codepoint = notehead_codepoint(duration, slash);
auto origin = origin_for_center(codepoint, x, y, spacing);
draw_music_origin(codepoint, origin, spacing, ink);
return {.center_y = y, .origin = origin};
}
auto draw_note(luma::draw_context& context, vamp_zero::score_note const& note, std::float32_t x, std::float32_t staff_top, std::float32_t spacing, std::float32_t next_x, std::bool_t slash) -> void {
if (note.hidden) {
return;
}
auto duration = duration_for(display_ticks(note));
if (note.kind == "rest") {
draw_rest(context, duration, x, staff_top, spacing);
return;
}
auto ink = vamp_zero::to_luma(vamp_style::paper_ink);
auto center = staff_top + spacing * 2.0F;
auto bottom = staff_top + spacing * 4.0F;
auto low_y = center;
auto high_y = center;
auto heads = std::vector<notehead_instance>{};
if (slash || note.pitches.empty()) {
heads.push_back(draw_notehead(duration, slash, x, center, spacing, ink));
low_y = center;
high_y = center;
}
if (!slash) {
for (auto const& pitch : note.pitches) {
auto yy = std::clamp(pitch_y(pitch, staff_top, spacing), staff_top - spacing * 2.0F, bottom + spacing * 2.0F);
low_y = std::max(low_y, yy);
high_y = std::min(high_y, yy);
}
for (auto const& pitch : note.pitches) {
auto yy = std::clamp(pitch_y(pitch, staff_top, spacing), staff_top - spacing * 2.0F, bottom + spacing * 2.0F);
draw_ledger_lines(context, x, yy, staff_top, spacing, ink);
if (auto accidental = accidental_glyph(pitch.accidental)) {
auto head_origin = origin_for_center(notehead_codepoint(duration, false), x, yy, spacing);
auto head_left = head_origin.x + glyph_box(notehead_codepoint(duration, false)).west * spacing;
draw_music_left_of(*accidental, head_left - spacing * 0.65F, yy, spacing, ink);
}
heads.push_back(draw_notehead(duration, false, x, yy, spacing, ink));
}
}
auto notehead = notehead_codepoint(duration, slash);
if (has_stem(duration) && !heads.empty()) {
auto stem_up = (low_y + high_y) * 0.5F >= center;
if (stem_up) {
auto base_origin = origin_for_center(notehead, x, low_y, spacing);
auto base = anchor_point(base_origin, stem_up_anchor(notehead), spacing);
auto stem_top = std::min(high_y, center) - spacing * 3.6F;
context.stroke_line(base, {.x = base.x, .y = stem_top}, 1.6F, ink);
if (has_flag(duration)) {
draw_eighth_flag(true, base.x, stem_top, spacing, ink);
}
} else {
auto base_origin = origin_for_center(notehead, x, high_y, spacing);
auto base = anchor_point(base_origin, stem_down_anchor(notehead), spacing);
auto stem_bottom = std::max(low_y, center) + spacing * 3.6F;
context.stroke_line(base, {.x = base.x, .y = stem_bottom}, 1.6F, ink);
if (has_flag(duration)) {
draw_eighth_flag(false, base.x, stem_bottom, spacing, ink);
}
}
}
if (duration.dots != 0) {
auto dot_origin = origin_for_center(notehead, x, low_y, spacing);
auto dot_x = dot_origin.x + glyph_box(notehead).east * spacing + spacing * 0.85F;
draw_duration_dot(context, dot_x, dot_y_for(low_y, staff_top, spacing), spacing, ink);
}
if (note.tie_to_next) {
draw_curve(context, {.x = x + 7.0F, .y = low_y + 9.0F}, {.x = (x + next_x) * 0.5F, .y = low_y + 19.0F}, {.x = next_x - 7.0F, .y = low_y + 9.0F}, 1.4F, ink);
}
}
auto note_x(std::float32_t start_x, std::float32_t width, std::int64_t total_ticks, std::int64_t elapsed_ticks, std::int64_t duration_ticks) -> std::float32_t {
auto center_ticks = static_cast<std::float32_t>(elapsed_ticks) + static_cast<std::float32_t>(duration_ticks) * 0.5F;
auto ratio = std::clamp(center_ticks / static_cast<std::float32_t>(total_ticks), 0.0F, 1.0F);
return start_x + width * ratio;
}
auto draw_measure(luma::draw_context& context, vamp_zero::score_measure const& measure, luma::rect bounds, std::size_t measure_number) -> void {
constexpr auto spacing = std::float32_t{8.0F};
auto staff_top = bounds.y + 52.0F;
draw_text(context, bounds.x + 5.0F, bounds.y + 1.0F, std::to_string(measure_number), 1, vamp_style::muted, 42.0F);
if (!measure.section_label.empty()) {
draw_text(context, bounds.x + 28.0F, bounds.y - 2.0F, measure.section_label, 2, vamp_style::selection, bounds.width - 32.0F);
}
if (!measure.time_signature.empty() && measure_number == 1U) {
draw_text(context, bounds.x + 8.0F, staff_top + 2.0F, measure.time_signature, 1, vamp_style::paper_ink, 42.0F);
}
auto notation_x = bounds.x + (measure_number == 1U ? 43.0F : 24.0F);
auto notation_width = std::max(24.0F, bounds.width - (notation_x - bounds.x) - 16.0F);
auto total_ticks = measure_ticks(measure);
auto elapsed = std::int64_t{};
auto previous_chord = std::string{};
for (auto index = std::size_t{}; index < measure.notes.size(); ++index) {
auto const& note = measure.notes[index];
auto duration_ticks = spacing_ticks(note);
auto x = note_x(notation_x, notation_width, total_ticks, elapsed, duration_ticks);
auto next_duration = index + 1U < measure.notes.size() ? spacing_ticks(measure.notes[index + 1U]) : duration_ticks;
auto next_x = note_x(notation_x, notation_width, total_ticks, elapsed + duration_ticks, next_duration);
if (!note.hidden && !note.chord_symbol.empty() && note.chord_symbol != previous_chord) {
draw_text(context, x - 18.0F, bounds.y + 24.0F, note.chord_symbol, 2, vamp_style::paper_ink, std::min(92.0F, bounds.width - (x - bounds.x)));
previous_chord = note.chord_symbol;
}
draw_note(context, note, x, staff_top, spacing, next_x, note.notehead == "slash");
if (!note.hidden && !note.lyric.empty()) {
draw_text(context, x - 12.0F, staff_top + 46.0F, note.lyric, 1, vamp_style::muted, 66.0F);
}
elapsed += duration_ticks;
}
}
}
auto vamp_zero::draw_score_page(luma::draw_context& context, luma::rect bounds, vamp_zero::score_document const& score, std::size_t page_index) -> void {
context.fill_rect(bounds, vamp_zero::to_luma(vamp_style::paper));
context.stroke_rect(bounds, 2.0F, vamp_zero::to_luma(vamp_style::rule));
context.push_clip(bounds);
auto margin = std::float32_t{28.0F};
auto header_y = bounds.y + 16.0F;
draw_text(context, bounds.x + margin, header_y, score.title, 3, vamp_style::paper_ink, bounds.width * 0.55F);
draw_text(context, bounds.x + bounds.width - 220.0F, header_y + 2.0F, score.part, 3, vamp_style::selection, 190.0F);
draw_text(context, bounds.x + margin, header_y + 34.0F, score.composer, 1, vamp_style::muted, bounds.width * 0.6F);
draw_text(context, bounds.x + bounds.width - 122.0F, header_y + 39.0F, "PAGE " + std::to_string(page_index + 1U), 1, vamp_style::muted, 110.0F);
auto content_x = bounds.x + margin;
auto content_y = bounds.y + 84.0F;
auto content_width = bounds.width - margin * 2.0F;
auto system_height = (bounds.height - 108.0F) / static_cast<std::float32_t>(vamp_zero::score_systems_per_page);
auto measure_width = content_width / static_cast<std::float32_t>(vamp_zero::score_measures_per_system);
auto first_measure = page_index * vamp_zero::score_measures_per_page;
for (auto system = std::size_t{}; system < vamp_zero::score_systems_per_page; ++system) {
auto system_y = content_y + static_cast<std::float32_t>(system) * system_height;
auto staff_top = system_y + 52.0F;
draw_staff(context, content_x, staff_top, content_width, 8.0F);
for (auto measure = std::size_t{}; measure < vamp_zero::score_measures_per_system; ++measure) {
auto measure_index = first_measure + system * vamp_zero::score_measures_per_system + measure;
auto x = content_x + static_cast<std::float32_t>(measure) * measure_width;
context.stroke_line({.x = x, .y = staff_top}, {.x = x, .y = staff_top + 32.0F}, 1.2F, vamp_zero::to_luma(vamp_style::paper_ink));
if (measure_index < score.measures.size()) {
draw_measure(context, score.measures[measure_index], {.x = x, .y = system_y, .width = measure_width, .height = system_height}, measure_index + 1U);
}
}
context.stroke_line({.x = content_x + content_width, .y = staff_top}, {.x = content_x + content_width, .y = staff_top + 32.0F}, 2.0F, vamp_zero::to_luma(vamp_style::paper_ink));
}
context.pop_clip();
}