|
|
|
|
|
|
|
|
function wrapLines(tune, lineBreaks, barNumbers) { |
|
|
if (!lineBreaks || tune.lines.length === 0) |
|
|
return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var lines = tune.deline({lineBreaks: false}); |
|
|
var linesBreakElements = findLineBreaks(lines, lineBreaks); |
|
|
|
|
|
tune.lines = addLineBreaks(lines, linesBreakElements, barNumbers); |
|
|
tune.lineBreaks = linesBreakElements; |
|
|
} |
|
|
|
|
|
function addLineBreaks(lines, linesBreakElements, barNumbers) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var outputLines = []; |
|
|
var lastKeySig = []; |
|
|
var lastStem = []; |
|
|
var currentBarNumber = 1; |
|
|
for (var i = 0; i < linesBreakElements.length; i++) { |
|
|
var action = linesBreakElements[i]; |
|
|
if (lines[action.ogLine].staff) { |
|
|
var inputStaff = lines[action.ogLine].staff[action.staff]; |
|
|
if (!outputLines[action.line]) { |
|
|
outputLines[action.line] = {staff: []} |
|
|
} |
|
|
if (!outputLines[action.line].staff[action.staff]) { |
|
|
outputLines[action.line].staff[action.staff] = {voices: []}; |
|
|
if (barNumbers !== undefined && action.staff === 0 && action.line > 0) { |
|
|
outputLines[action.line].staff[action.staff].barNumber = currentBarNumber; |
|
|
} |
|
|
var keys = Object.keys(inputStaff) |
|
|
for (var k = 0; k < keys.length; k++) { |
|
|
var skip = keys[k] === "voices"; |
|
|
if (keys[k] === "meter" && action.line !== 0) |
|
|
skip = true; |
|
|
if (!skip) |
|
|
outputLines[action.line].staff[action.staff][keys[k]] = inputStaff[keys[k]]; |
|
|
} |
|
|
if (lastKeySig[action.staff]) |
|
|
outputLines[action.line].staff[action.staff].key = lastKeySig[action.staff]; |
|
|
|
|
|
} |
|
|
if (!outputLines[action.line].staff[action.staff].voices[action.voice]) { |
|
|
outputLines[action.line].staff[action.staff].voices[action.voice] = []; |
|
|
} |
|
|
outputLines[action.line].staff[action.staff].voices[action.voice] = |
|
|
lines[action.ogLine].staff[action.staff].voices[action.voice].slice(action.start, action.end+1); |
|
|
if (lastStem[action.staff*10+action.voice]) |
|
|
outputLines[action.line].staff[action.staff].voices[action.voice].unshift({el_type: "stem", direction: lastStem[action.staff*10+action.voice].direction}) |
|
|
var currVoice = outputLines[action.line].staff[action.staff].voices[action.voice]; |
|
|
for (var kk = currVoice.length-1; kk >= 0; kk--) { |
|
|
if (currVoice[kk].el_type === "key") { |
|
|
lastKeySig[action.staff] = { |
|
|
root: currVoice[kk].root, |
|
|
acc: currVoice[kk].acc, |
|
|
mode: currVoice[kk].mode, |
|
|
accidentals: currVoice[kk].accidentals.filter(function (acc) { return acc.acc !== 'natural' }) |
|
|
}; |
|
|
break; |
|
|
} |
|
|
} |
|
|
for (kk = currVoice.length-1; kk >= 0; kk--) { |
|
|
if (currVoice[kk].el_type === "stem") { |
|
|
lastStem[action.staff*10+action.voice] = { |
|
|
direction: currVoice[kk].direction, |
|
|
}; |
|
|
break; |
|
|
} |
|
|
} |
|
|
if (barNumbers !== undefined && action.staff === 0 && action.voice === 0) { |
|
|
for (kk = 0; kk < currVoice.length; kk++) { |
|
|
if (currVoice[kk].el_type === 'bar') { |
|
|
currentBarNumber++ |
|
|
if (kk === currVoice.length-1) |
|
|
delete currVoice[kk].barNumber |
|
|
else |
|
|
currVoice[kk].barNumber = currentBarNumber |
|
|
} |
|
|
} |
|
|
} |
|
|
} else { |
|
|
outputLines[action.line] = lines[action.ogLine]; |
|
|
} |
|
|
} |
|
|
|
|
|
for (var ii = 0; ii < outputLines.length; ii++) { |
|
|
if (outputLines[ii].staff) { |
|
|
outputLines[ii].staff = outputLines[ii].staff.filter(function (el) { |
|
|
return el != null; |
|
|
}); |
|
|
} |
|
|
} |
|
|
return outputLines; |
|
|
} |
|
|
|
|
|
|
|
|
function findLineBreaks(lines, lineBreakArray) { |
|
|
|
|
|
|
|
|
|
|
|
var lineBreakIndexes = []; |
|
|
var lbai = 0; |
|
|
var lineCounter = 0; |
|
|
var outputLine = 0; |
|
|
for (var i = 0; i < lines.length; i++) { |
|
|
var line = lines[i]; |
|
|
if (line.staff) { |
|
|
var lineStart = lineCounter; |
|
|
var lineBreaks = lineBreakArray[lbai]; |
|
|
lbai++; |
|
|
for (var j = 0; j < line.staff.length; j++) { |
|
|
var staff = line.staff[j]; |
|
|
for (var k = 0; k < staff.voices.length; k++) { |
|
|
outputLine = lineStart; |
|
|
var measureNumber = 0; |
|
|
var lbi = 0; |
|
|
var voice = staff.voices[k]; |
|
|
var start = 0; |
|
|
for (var e = 0; e < voice.length; e++) { |
|
|
var el = voice[e]; |
|
|
|
|
|
if (el.el_type === 'bar') { |
|
|
if (lineBreaks[lbi] === measureNumber) { |
|
|
lineBreakIndexes.push({ ogLine: i, line: outputLine, staff: j, voice: k, start: start, end: e}) |
|
|
start = e + 1; |
|
|
outputLine++; |
|
|
lineCounter = Math.max(lineCounter, outputLine) |
|
|
lbi++; |
|
|
} |
|
|
measureNumber++; |
|
|
|
|
|
} |
|
|
} |
|
|
lineBreakIndexes.push({ ogLine: i, line: outputLine, staff: j, voice: k, start: start, end: voice.length}) |
|
|
outputLine++; |
|
|
lineCounter = Math.max(lineCounter, outputLine) |
|
|
} |
|
|
} |
|
|
} else { |
|
|
lineBreakIndexes.push({ ogLine: i, line: outputLine }) |
|
|
outputLine++; |
|
|
lineCounter = Math.max(lineCounter, outputLine) |
|
|
} |
|
|
} |
|
|
return lineBreakIndexes; |
|
|
} |
|
|
|
|
|
|
|
|
function freeFormLineBreaks(widths, lineBreakPoint) { |
|
|
var lineBreaks = []; |
|
|
var totals = []; |
|
|
var totalThisLine = 0; |
|
|
|
|
|
|
|
|
for (var i = 0; i < widths.length; i++) { |
|
|
var width = widths[i]; |
|
|
var attemptedWidth = totalThisLine + width; |
|
|
if (attemptedWidth < lineBreakPoint) |
|
|
totalThisLine = attemptedWidth; |
|
|
else { |
|
|
|
|
|
var oldDistance = lineBreakPoint - totalThisLine; |
|
|
var newDistance = attemptedWidth - lineBreakPoint; |
|
|
if (oldDistance < newDistance && totalThisLine > 0) { |
|
|
lineBreaks.push(i - 1); |
|
|
totals.push(Math.round(totalThisLine - width)); |
|
|
totalThisLine = width; |
|
|
} else { |
|
|
if (i < widths.length-1) { |
|
|
lineBreaks.push(i); |
|
|
totals.push(Math.round(totalThisLine)); |
|
|
totalThisLine = 0; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
totals.push(Math.round(totalThisLine)); |
|
|
return { lineBreaks: lineBreaks, totals: totals }; |
|
|
} |
|
|
|
|
|
function clone(arr) { |
|
|
var newArr = []; |
|
|
for (var i = 0; i < arr.length; i++) |
|
|
newArr.push(arr[i]); |
|
|
return newArr; |
|
|
} |
|
|
|
|
|
function oneTry(measureWidths, idealWidths, accumulator, lineAccumulator, lineWidths, lastVariance, highestVariance, currLine, lineBreaks, startIndex, otherTries) { |
|
|
for (var i = startIndex; i < measureWidths.length; i++) { |
|
|
var measureWidth = measureWidths[i]; |
|
|
accumulator += measureWidth; |
|
|
lineAccumulator += measureWidth; |
|
|
var thisVariance = Math.abs(accumulator - idealWidths[currLine]); |
|
|
var varianceIsClose = Math.abs(thisVariance - lastVariance) < idealWidths[0] / 10; |
|
|
if (varianceIsClose) { |
|
|
if (thisVariance < lastVariance) { |
|
|
|
|
|
var newWidths = clone(lineWidths); |
|
|
var newBreaks = clone(lineBreaks); |
|
|
newBreaks.push(i-1); |
|
|
newWidths.push(lineAccumulator - measureWidth); |
|
|
otherTries.push({ |
|
|
accumulator: accumulator, |
|
|
lineAccumulator: measureWidth, |
|
|
lineWidths: newWidths, |
|
|
lastVariance: Math.abs(accumulator - idealWidths[currLine+1]), |
|
|
highestVariance: Math.max(highestVariance, lastVariance), |
|
|
currLine: currLine+1, |
|
|
lineBreaks: newBreaks, |
|
|
startIndex: i+1}); |
|
|
} else if (thisVariance > lastVariance && i < measureWidths.length-1) { |
|
|
|
|
|
newWidths = clone(lineWidths); |
|
|
newBreaks = clone(lineBreaks); |
|
|
|
|
|
|
|
|
otherTries.push({ |
|
|
accumulator: accumulator, |
|
|
lineAccumulator: lineAccumulator, |
|
|
lineWidths: newWidths, |
|
|
lastVariance: thisVariance, |
|
|
highestVariance: Math.max(highestVariance, thisVariance), |
|
|
currLine: currLine, |
|
|
lineBreaks: newBreaks, |
|
|
startIndex: i+1}); |
|
|
} |
|
|
} |
|
|
if (thisVariance > lastVariance) { |
|
|
lineBreaks.push(i - 1); |
|
|
currLine++; |
|
|
highestVariance = Math.max(highestVariance, lastVariance); |
|
|
lastVariance = Math.abs(accumulator - idealWidths[currLine]); |
|
|
lineWidths.push(lineAccumulator - measureWidth); |
|
|
lineAccumulator = measureWidth; |
|
|
} else { |
|
|
lastVariance = thisVariance; |
|
|
} |
|
|
} |
|
|
lineWidths.push(lineAccumulator); |
|
|
} |
|
|
|
|
|
function optimizeLineWidths(widths, lineBreakPoint, lineBreaks, explanation) { |
|
|
|
|
|
var numLines = Math.ceil(widths.total / lineBreakPoint); |
|
|
|
|
|
|
|
|
var idealWidth = Math.floor(widths.total / numLines); |
|
|
|
|
|
|
|
|
var idealWidths = []; |
|
|
for (var i = 0; i < numLines; i++) |
|
|
idealWidths.push(idealWidth*(i+1)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var otherTries = []; |
|
|
otherTries.push({ |
|
|
accumulator: 0, |
|
|
lineAccumulator: 0, |
|
|
lineWidths: [], |
|
|
lastVariance: 999999, |
|
|
highestVariance: 0, |
|
|
currLine: 0, |
|
|
lineBreaks: [], |
|
|
startIndex: 0}); |
|
|
var index = 0; |
|
|
while (index < otherTries.length) { |
|
|
oneTry(widths.measureWidths, |
|
|
idealWidths, |
|
|
otherTries[index].accumulator, |
|
|
otherTries[index].lineAccumulator, |
|
|
otherTries[index].lineWidths, |
|
|
otherTries[index].lastVariance, |
|
|
otherTries[index].highestVariance, |
|
|
otherTries[index].currLine, |
|
|
otherTries[index].lineBreaks, |
|
|
otherTries[index].startIndex, |
|
|
otherTries); |
|
|
index++; |
|
|
} |
|
|
for (i = 0; i < otherTries.length; i++) { |
|
|
var otherTry = otherTries[i]; |
|
|
otherTry.variances = []; |
|
|
otherTry.aveVariance = 0; |
|
|
for (var j = 0; j < otherTry.lineWidths.length; j++) { |
|
|
var lineWidth = otherTry.lineWidths[j]; |
|
|
otherTry.variances.push(lineWidth - idealWidths[0]); |
|
|
otherTry.aveVariance += Math.abs(lineWidth - idealWidths[0]); |
|
|
} |
|
|
otherTry.aveVariance = otherTry.aveVariance / otherTry.lineWidths.length; |
|
|
explanation.attempts.push({ type: "optimizeLineWidths", lineBreaks: otherTry.lineBreaks, variances: otherTry.variances, aveVariance: otherTry.aveVariance, widths: widths.measureWidths }); |
|
|
} |
|
|
var smallest = 9999999; |
|
|
var smallestIndex = -1; |
|
|
for (i = 0; i < otherTries.length; i++) { |
|
|
otherTry = otherTries[i]; |
|
|
if (otherTry.aveVariance < smallest) { |
|
|
smallest = otherTry.aveVariance; |
|
|
smallestIndex = i; |
|
|
} |
|
|
} |
|
|
return { failed: false, lineBreaks: otherTries[smallestIndex].lineBreaks, variance: otherTries[smallestIndex].highestVariance }; |
|
|
} |
|
|
|
|
|
function fixedMeasureLineBreaks(widths, lineBreakPoint, preferredMeasuresPerLine) { |
|
|
var lineBreaks = []; |
|
|
var totals = []; |
|
|
var thisWidth = 0; |
|
|
var failed = false; |
|
|
for (var i = 0; i < widths.length; i++) { |
|
|
thisWidth += widths[i]; |
|
|
if (thisWidth > lineBreakPoint) { |
|
|
failed = true; |
|
|
} |
|
|
if (i % preferredMeasuresPerLine === (preferredMeasuresPerLine-1)) { |
|
|
if (i !== widths.length-1) |
|
|
lineBreaks.push(i); |
|
|
totals.push(Math.round(thisWidth)); |
|
|
thisWidth = 0; |
|
|
} |
|
|
} |
|
|
return { failed: failed, totals: totals, lineBreaks: lineBreaks }; |
|
|
} |
|
|
|
|
|
function getRevisedTuneParams(lineBreaks, staffWidth, params) { |
|
|
|
|
|
var revisedParams = { |
|
|
lineBreaks: lineBreaks, |
|
|
staffwidth: staffWidth |
|
|
}; |
|
|
for (var key in params) { |
|
|
if (params.hasOwnProperty(key) && key !== 'wrap' && key !== 'staffwidth') { |
|
|
revisedParams[key] = params[key]; |
|
|
} |
|
|
} |
|
|
|
|
|
return { revisedParams: revisedParams }; |
|
|
} |
|
|
|
|
|
function calcLineWraps(tune, widths, params) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (widths.length === 0 || params.staffwidth < widths[0].left) { |
|
|
return { |
|
|
reParse: false, |
|
|
explanation: "Staff width is narrower than the margin", |
|
|
revisedParams: params |
|
|
}; |
|
|
} |
|
|
var scale = params.scale ? Math.max(params.scale, 0.1) : 1; |
|
|
var minSpacing = params.wrap.minSpacing ? Math.max(parseFloat(params.wrap.minSpacing), 1) : 1; |
|
|
var minSpacingLimit = params.wrap.minSpacingLimit ? Math.max(parseFloat(params.wrap.minSpacingLimit), 1) : minSpacing - 0.1; |
|
|
var maxSpacing = params.wrap.maxSpacing ? Math.max(parseFloat(params.wrap.maxSpacing), 1) : undefined; |
|
|
if (params.wrap.lastLineLimit && !maxSpacing) |
|
|
maxSpacing = Math.max(parseFloat(params.wrap.lastLineLimit), 1); |
|
|
|
|
|
var preferredMeasuresPerLine = params.wrap.preferredMeasuresPerLine ? Math.max(parseInt(params.wrap.preferredMeasuresPerLine, 10), 0) : undefined; |
|
|
|
|
|
var accumulatedLineBreaks = []; |
|
|
var explanations = []; |
|
|
for (var s = 0; s < widths.length; s++) { |
|
|
var section = widths[s]; |
|
|
var usableWidth = params.staffwidth - section.left; |
|
|
var lineBreakPoint = usableWidth / minSpacing / scale; |
|
|
var minLineSize = usableWidth / maxSpacing / scale; |
|
|
var allowableVariance = usableWidth / minSpacingLimit / scale; |
|
|
var explanation = { |
|
|
widths: section, |
|
|
lineBreakPoint: lineBreakPoint, |
|
|
minLineSize: minLineSize, |
|
|
attempts: [], |
|
|
staffWidth: params.staffwidth, |
|
|
minWidth: Math.round(allowableVariance) |
|
|
}; |
|
|
|
|
|
|
|
|
var lineBreaks = null; |
|
|
if (preferredMeasuresPerLine) { |
|
|
var f = fixedMeasureLineBreaks(section.measureWidths, lineBreakPoint, preferredMeasuresPerLine); |
|
|
explanation.attempts.push({ |
|
|
type: "Fixed Measures Per Line", |
|
|
preferredMeasuresPerLine: preferredMeasuresPerLine, |
|
|
lineBreaks: f.lineBreaks, |
|
|
failed: f.failed, |
|
|
totals: f.totals |
|
|
}); |
|
|
if (!f.failed) |
|
|
lineBreaks = f.lineBreaks; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!lineBreaks) { |
|
|
var ff = freeFormLineBreaks(section.measureWidths, lineBreakPoint); |
|
|
explanation.attempts.push({type: "Free Form", lineBreaks: ff.lineBreaks, totals: ff.totals}); |
|
|
lineBreaks = ff.lineBreaks; |
|
|
|
|
|
|
|
|
if (lineBreaks.length > 0 && section.measureWidths.length < 25) { |
|
|
|
|
|
|
|
|
ff = optimizeLineWidths(section, lineBreakPoint, lineBreaks, explanation); |
|
|
explanation.attempts.push({ |
|
|
type: "Optimize", |
|
|
failed: ff.failed, |
|
|
reason: ff.reason, |
|
|
lineBreaks: ff.lineBreaks, |
|
|
totals: ff.totals |
|
|
}); |
|
|
if (!ff.failed) |
|
|
lineBreaks = ff.lineBreaks; |
|
|
} |
|
|
} |
|
|
accumulatedLineBreaks.push(lineBreaks); |
|
|
explanations.push(explanation); |
|
|
} |
|
|
|
|
|
var staffWidth = params.staffwidth; |
|
|
var ret = getRevisedTuneParams(accumulatedLineBreaks, staffWidth, params); |
|
|
ret.explanation = explanations; |
|
|
ret.reParse = true; |
|
|
return ret; |
|
|
} |
|
|
|
|
|
module.exports = { wrapLines: wrapLines, calcLineWraps: calcLineWraps }; |
|
|
|