UNPKG

satie

Version:

A sheet music renderer for the web

164 lines (163 loc) 7.41 kB
/** * This file is part of Satie music engraver <https://github.com/jnetterf/satie>. * Copyright (C) Joshua Netterfield <joshua.ca> 2015 - present. * * Satie is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Satie is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Satie. If not, see <http://www.gnu.org/licenses/>. */ "use strict"; var lodash_1 = require("lodash"); var private_util_1 = require("./private_util"); var private_chordUtil_1 = require("./private_chordUtil"); var private_part_1 = require("./private_part"); var UNDERFILLED_EXPANSION_WEIGHT = 0.1; /** * Evaluates S(t), the logistic function. Used to create aesthetic transitions. * For example, the upper half of the logistic function is used to compute how much * spacing should be on the final line of a song. */ function logistic(t) { return 1 / (1 + Math.exp(-t)); } /** * Lays out measures within a bar & justifies. * * @returns new end of line */ function justify(options, bounds, measures) { if (options.singleLineMode && !options.fixedMeasureWidth) { // Skip: note that in this case, we set the shortestCount in each measure to half what it // needs to be to fake justification in each measure. return measures; } var x = bounds.left + lodash_1.reduce(measures, function (sum, measure) { return sum + measure.width; }, 0); // Check for underfilled bars var underfilled = lodash_1.map(measures, function (measure, idx) { var attr = measures[idx].attributes; var firstPart = private_part_1.scoreParts(options.header.partList)[0].id; var divs = private_chordUtil_1.barDivisions(attr[firstPart][1]); var maxDivs = measure.maxDivisions; return maxDivs < divs; }); var smallest = Number.POSITIVE_INFINITY; lodash_1.forEach(measures, function (measure, measureIdx) { var maxIdx = lodash_1.max(lodash_1.map(measure.elements, function (el) { return el.length; })); lodash_1.times(maxIdx, function (j) { for (var i = 0; i < measure.elements.length; ++i) { if (measure.elements[i][j].expandPolicy !== "none") { if (measure.elements[i][j].model && measure.elements[i][j].model.divCount) { smallest = Math.min(measure.elements[i][j].model.divCount, smallest); } } } }); }); // x > enX is possible if a single bar's minimum size exceeds maxX, or if our // guess for a measure width was too liberal. In either case, we're shortening // the measure width here, and our partial algorithm doesn't work with negative // padding. var partial = x < bounds.right && options.lineIndex + 1 === options.lineCount; var underfilledCount = 0; var expandableCount = lodash_1.reduce(measures, function (memo, measure$, idx) { // Precondition: all layouts at a given index have the same "expandable" value. return lodash_1.reduce(lodash_1.last(measure$.elements), function (memo, element$) { if (underfilled[idx] && element$.expandPolicy !== "none") { ++underfilledCount; } if (!element$.model || !element$.model.divCount) { return memo; } var expandBy = 0; if (element$.expandPolicy !== "none") { expandBy = (Math.log(element$.model.divCount) - Math.log(smallest) + 1); } return memo + expandBy * (underfilled[idx] ? UNDERFILLED_EXPANSION_WEIGHT : 1.0); }, memo); }, 0); var avgExpansion; if (!expandableCount) { avgExpansion = 0; } else if (partial) { var expansionRemainingGuess = bounds.right - 3 - x; var avgExpansionGuess = expansionRemainingGuess / (expandableCount + (1 - UNDERFILLED_EXPANSION_WEIGHT) * underfilledCount); var weight = logistic((avgExpansionGuess - bounds.right / 80) / 20) * 2 / 3; avgExpansion = (1 - weight) * avgExpansionGuess; } else { var exp = bounds.right - x; avgExpansion = exp / expandableCount; } var anyExpandable = false; var totalExpCount = 0; var lineExpansion = 0; lodash_1.forEach(measures, function (measure, measureIdx) { measure.originX += lineExpansion; var measureExpansion = 0; var maxIdx = lodash_1.max(lodash_1.map(measure.elements, function (el) { return el.length; })); if (options.fixedMeasureWidth) { var expandable_1 = lodash_1.times(maxIdx, function (j) { var expand = false; for (var i = 0; i < measure.elements.length; ++i) { if (measure.elements[i][j].expandPolicy !== "none") { expand = true; } } return expand; }); var count = expandable_1.filter(function (n) { return n; }).length; var expansionPerElement_1 = (options.fixedMeasureWidth - measure.width) / count; lodash_1.times(maxIdx, function (j) { for (var i = 0; i < measure.elements.length; ++i) { measure.elements[i][j].x += measureExpansion; } if (expandable_1[j]) { measureExpansion += expansionPerElement_1; } }); } else { lodash_1.times(maxIdx, function (j) { for (var i = 0; i < measure.elements.length; ++i) { measure.elements[i][j].x += measureExpansion; } var expandOne = false; var minRatio = private_util_1.MAX_SAFE_INTEGER; for (var i = 0; i < measure.elements.length; ++i) { if (measure.elements[i][j].expandPolicy !== "none") { anyExpandable = true; if (!measure.elements[i][j].model || !measure.elements[i][j].model.divCount) { continue; } var divCount = measure.elements[i][j].model.divCount; var ratio = (Math.log(divCount) - Math.log(smallest) + 1) * (underfilled[measureIdx] ? UNDERFILLED_EXPANSION_WEIGHT : 1.0); minRatio = Math.min(minRatio, ratio); expandOne = true; } } if (expandOne) { // FIXME: We can overshoot, like on Lily 23f. measureExpansion += avgExpansion * minRatio; totalExpCount += minRatio; } }); } measure.width += measureExpansion; lineExpansion += measureExpansion; }); return measures; } Object.defineProperty(exports, "__esModule", { value: true }); exports.default = justify;