Skip to content

Commit 90309d5

Browse files
committed
feat: added bar lines in phrasemeaker
Signed-off-by: Shirshendu R Tripathi <[email protected]>
1 parent 5742a6e commit 90309d5

File tree

1 file changed

+307
-0
lines changed

1 file changed

+307
-0
lines changed

js/widgets/phrasemaker.js

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4266,6 +4266,313 @@ class PhraseMaker {
42664266
}
42674267
}
42684268
}
4269+
4270+
this._updateMeasureBoundaries();
4271+
}
4272+
4273+
/**
4274+
* Updates visual barlines based on the active meter so measure starts are easy to spot.
4275+
* @private
4276+
*/
4277+
_updateMeasureBoundaries() {
4278+
const columnCount = this._getColumnCount();
4279+
if (columnCount === 0) {
4280+
return;
4281+
}
4282+
4283+
const meterInfo = this._getMeterInfo();
4284+
if (!meterInfo || !meterInfo.beatUnit || meterInfo.beatUnit <= 0) {
4285+
return;
4286+
}
4287+
4288+
const beatsPerMeasure = meterInfo.beatsPerMeasure;
4289+
const beatUnit = meterInfo.beatUnit;
4290+
const EPSILON = 1e-6;
4291+
let beatAccumulator = 0;
4292+
const measureBoundaries = new Set([0]);
4293+
4294+
for (let column = 0; column < columnCount; column++) {
4295+
const duration = this._getColumnDuration(column);
4296+
if (duration === null) {
4297+
continue;
4298+
}
4299+
4300+
beatAccumulator += duration / beatUnit;
4301+
4302+
if (beatAccumulator > beatsPerMeasure - EPSILON) {
4303+
while (beatAccumulator >= beatsPerMeasure - EPSILON) {
4304+
measureBoundaries.add(column + 1);
4305+
beatAccumulator -= beatsPerMeasure;
4306+
}
4307+
}
4308+
4309+
if (Math.abs(beatAccumulator) < EPSILON) {
4310+
beatAccumulator = 0;
4311+
}
4312+
}
4313+
4314+
measureBoundaries.delete(columnCount);
4315+
4316+
const measureBorder = `2px solid ${this._resolveBarlineColor()}`;
4317+
4318+
const applyBorder = (row) => {
4319+
if (!row || !row.cells) {
4320+
return;
4321+
}
4322+
4323+
let columnIndex = 0;
4324+
for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) {
4325+
const cell = row.cells[cellIdx];
4326+
if (!cell) {
4327+
columnIndex += 1;
4328+
continue;
4329+
}
4330+
4331+
const span = cell.colSpan || 1;
4332+
4333+
cell.style.borderLeft = "";
4334+
4335+
if (measureBoundaries.has(columnIndex)) {
4336+
cell.style.borderLeft = measureBorder;
4337+
}
4338+
4339+
columnIndex += span;
4340+
}
4341+
};
4342+
4343+
this._rows.forEach(applyBorder);
4344+
applyBorder(this._noteValueRow);
4345+
applyBorder(this._tupletNoteValueRow);
4346+
applyBorder(this._tupletValueRow);
4347+
4348+
if (this.lyricsON) {
4349+
const lyricsRow = docById("lyricRow");
4350+
if (lyricsRow) {
4351+
const nestedTable = lyricsRow.querySelector("table");
4352+
if (nestedTable && nestedTable.rows.length > 0) {
4353+
applyBorder(nestedTable.rows[0]);
4354+
}
4355+
}
4356+
}
4357+
}
4358+
4359+
/**
4360+
* Returns the number of rhythm columns currently displayed in the grid.
4361+
* @returns {number}
4362+
* @private
4363+
*/
4364+
_getColumnCount() {
4365+
for (let i = 0; i < this._rows.length; i++) {
4366+
const row = this._rows[i];
4367+
if (row && row.cells && row.cells.length > 0) {
4368+
return row.cells.length;
4369+
}
4370+
}
4371+
4372+
if (this._noteValueRow && this._noteValueRow.cells) {
4373+
let total = 0;
4374+
for (let i = 0; i < this._noteValueRow.cells.length; i++) {
4375+
total += this._noteValueRow.cells[i].colSpan || 1;
4376+
}
4377+
return total;
4378+
}
4379+
4380+
return 0;
4381+
}
4382+
4383+
/**
4384+
* Retrieves the duration of a column, expressed as a fraction of a whole note.
4385+
* @param {number} index - Column index.
4386+
* @returns {?number}
4387+
* @private
4388+
*/
4389+
_getColumnDuration(index) {
4390+
for (let i = 0; i < this._rows.length; i++) {
4391+
const row = this._rows[i];
4392+
if (!row || !row.cells || row.cells.length <= index) {
4393+
continue;
4394+
}
4395+
4396+
const duration = this._parseDurationFromCell(row.cells[index], false);
4397+
if (duration !== null) {
4398+
return duration;
4399+
}
4400+
}
4401+
4402+
if (this._noteValueRow && this._noteValueRow.cells) {
4403+
let columnPointer = 0;
4404+
for (let cellIdx = 0; cellIdx < this._noteValueRow.cells.length; cellIdx++) {
4405+
const cell = this._noteValueRow.cells[cellIdx];
4406+
const span = cell.colSpan || 1;
4407+
4408+
if (index >= columnPointer && index < columnPointer + span) {
4409+
const duration = this._parseDurationFromCell(cell, true);
4410+
if (duration !== null) {
4411+
return duration;
4412+
}
4413+
break;
4414+
}
4415+
4416+
columnPointer += span;
4417+
}
4418+
}
4419+
4420+
return null;
4421+
}
4422+
4423+
/**
4424+
* Parses a duration value from a cell element.
4425+
* @param {HTMLElement} cell - The cell to inspect.
4426+
* @param {boolean} fromNoteValueRow - Whether the cell comes from the note value row.
4427+
* @returns {?number}
4428+
* @private
4429+
*/
4430+
_parseDurationFromCell(cell, fromNoteValueRow) {
4431+
if (!cell) {
4432+
return null;
4433+
}
4434+
4435+
const alt = cell.getAttribute("alt");
4436+
if (alt !== null) {
4437+
let parsed = parseFloat(alt);
4438+
if (!Number.isNaN(parsed) && parsed > 0) {
4439+
if (fromNoteValueRow && parsed >= 1) {
4440+
parsed = 1 / parsed;
4441+
}
4442+
if (parsed > 0) {
4443+
return parsed;
4444+
}
4445+
}
4446+
4447+
if (typeof alt === "string" && alt.includes("/")) {
4448+
const parts = alt.split("/");
4449+
if (parts.length === 2) {
4450+
const numerator = parseFloat(parts[0]);
4451+
const denominator = parseFloat(parts[1]);
4452+
if (
4453+
!Number.isNaN(numerator) &&
4454+
!Number.isNaN(denominator) &&
4455+
denominator !== 0
4456+
) {
4457+
return numerator / denominator;
4458+
}
4459+
}
4460+
}
4461+
}
4462+
4463+
const id = cell.getAttribute("id");
4464+
if (id !== null) {
4465+
let parsed = parseFloat(id);
4466+
if (!Number.isNaN(parsed) && parsed > 0) {
4467+
if (fromNoteValueRow && parsed >= 1) {
4468+
parsed = 1 / parsed;
4469+
}
4470+
if (parsed > 0) {
4471+
return parsed;
4472+
}
4473+
}
4474+
}
4475+
4476+
return null;
4477+
}
4478+
4479+
/**
4480+
* Retrieves meter information for the active turtle.
4481+
* @returns {{beatsPerMeasure: number, beatUnit: number}}
4482+
* @private
4483+
*/
4484+
_getMeterInfo() {
4485+
let beatsPerMeasure = 4;
4486+
let noteValuePerBeat = 4;
4487+
4488+
if (
4489+
this.activity &&
4490+
this.activity.turtles &&
4491+
typeof this.activity.turtles.ithTurtle === "function"
4492+
) {
4493+
const turtle = this.activity.turtles.ithTurtle(0);
4494+
if (turtle && turtle.singer) {
4495+
if (
4496+
Number.isFinite(turtle.singer.beatsPerMeasure) &&
4497+
turtle.singer.beatsPerMeasure > 0
4498+
) {
4499+
beatsPerMeasure = turtle.singer.beatsPerMeasure;
4500+
}
4501+
4502+
if (
4503+
Number.isFinite(turtle.singer.noteValuePerBeat) &&
4504+
turtle.singer.noteValuePerBeat > 0
4505+
) {
4506+
noteValuePerBeat = turtle.singer.noteValuePerBeat;
4507+
}
4508+
}
4509+
}
4510+
4511+
beatsPerMeasure = Math.max(1, Math.round(beatsPerMeasure));
4512+
4513+
let beatUnit = noteValuePerBeat >= 1 ? 1 / noteValuePerBeat : noteValuePerBeat;
4514+
if (!Number.isFinite(beatUnit) || beatUnit <= 0) {
4515+
beatUnit = 1 / 4;
4516+
}
4517+
4518+
return {
4519+
beatsPerMeasure,
4520+
beatUnit
4521+
};
4522+
}
4523+
4524+
/**
4525+
* Determines the barline color that best contrasts with the current theme.
4526+
* @returns {string}
4527+
* @private
4528+
*/
4529+
_resolveBarlineColor() {
4530+
if (!platformColor || !platformColor.background) {
4531+
return "#303030";
4532+
}
4533+
4534+
return this._isDarkColor(platformColor.background) ? "#E0E0E0" : "#303030";
4535+
}
4536+
4537+
/**
4538+
* Determines if a hex color string represents a dark tone.
4539+
* @param {string} color - Hex color string (e.g., #303030).
4540+
* @returns {boolean}
4541+
* @private
4542+
*/
4543+
_isDarkColor(color) {
4544+
if (typeof color !== "string") {
4545+
return false;
4546+
}
4547+
4548+
let hex = color.trim();
4549+
if (hex.startsWith("#")) {
4550+
hex = hex.slice(1);
4551+
} else {
4552+
return false;
4553+
}
4554+
4555+
if (hex.length === 3) {
4556+
hex = hex
4557+
.split("")
4558+
.map((char) => char + char)
4559+
.join("");
4560+
}
4561+
4562+
if (hex.length !== 6) {
4563+
return false;
4564+
}
4565+
4566+
const r = parseInt(hex.slice(0, 2), 16);
4567+
const g = parseInt(hex.slice(2, 4), 16);
4568+
const b = parseInt(hex.slice(4, 6), 16);
4569+
4570+
if ([r, g, b].some((value) => Number.isNaN(value))) {
4571+
return false;
4572+
}
4573+
4574+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
4575+
return luminance < 0.5;
42694576
}
42704577

42714578
/**

0 commit comments

Comments
 (0)