diff options
Diffstat (limited to 'docs/dynmetrics')
-rw-r--r-- | docs/dynmetrics/index.css | 262 | ||||
-rw-r--r-- | docs/dynmetrics/index.html | 522 |
2 files changed, 784 insertions, 0 deletions
diff --git a/docs/dynmetrics/index.css b/docs/dynmetrics/index.css new file mode 100644 index 000000000..c7c98d351 --- /dev/null +++ b/docs/dynmetrics/index.css @@ -0,0 +1,262 @@ +body { + padding-bottom: 0; +} + +const { + display: inline; + font-size: 1.2em; + font-style: italic; + font-family: 'Times New Roman', Times, serif; +} + +sup { + /*background:lightpink;*/ + display: inline-block; + font-size:0.92em; + position: relative; + top:-0.4em; + letter-spacing: 0.001em; + vertical-align: baseline; +} + +.row.first { + padding-bottom:1em; +} + +formula { + display: inline-flex; + align-items: center; + background: white; + border-radius: 5px; + padding: 0 1em; + line-height: 3em; + height: 3em; + overflow: hidden; + margin-right: 1em; + margin-bottom: 1em; +} +.row.white formula { + background: #f5f5f5; +} +formula:last-child { + margin-right: 0; +} +formula.code { + white-space: pre; + font-family: "SFMono-Regular", Menlo, Consolas, Inconsolata, monospace; + font-size:0.96em; +} + formula > * { + margin: 0 0.2em 0 0.2em; + } + formula > const { + margin-bottom: 0.11em; + } + formula > sup { + margin-left: 0; + } + + +.samples { + display: flex; + flex-wrap: wrap; + overflow: auto; + overflow-wrap: break-word; + word-break: break-word; +} + .samples .sample { + /*background: lightpink;*/ + color: #111; + flex: 0 1 auto; + outline: none; + margin-right: 50px; + margin-bottom: 50px; + } + .samples .sample .di { + display: block; + background-color: #ccc; + height: 1px; + width: 100%; + margin-bottom: 8px; + } + .samples .sample .di > span { + display: block; + background-color: #333; + height: 100%; + } + .samples .sample .di.match > span { + background-color: #0d3; + } + .samples .sample .di.unavailable { + background-color: #eee; + } + .samples .sample .di.unavailable > span { + visibility: hidden; + } + .samples .sample .info { + display: block; + font-size: 11px !important; + line-height: 11px; + margin-bottom: 9px; + color: #bbb; + } + + +.font-style-regular { font-weight:400 !important; font-style:normal !important; } +.font-style-italic { font-weight:400 !important; font-style:italic !important; } +.font-style-medium { font-weight:500 !important; font-style:normal !important; } +.font-style-medium-italic { font-weight:500 !important; font-style:italic !important; } +.font-style-bold { font-weight:700 !important; font-style:normal !important; } +.font-style-bold-italic { font-weight:700 !important; font-style:italic !important; } +.font-style-black { font-weight:900 !important; font-style:normal !important; } +.font-style-black-italic { font-weight:900 !important; font-style:italic !important; } + +.row.with-sidebar { + padding: 0; +} + .row.with-sidebar > *:first-child { + flex: 1 1 auto; + padding: 50px 0 0 50px; /* note: samples have 50px right margin */ + } + .row.with-sidebar > .sidebar { + flex: 0 0 auto; + } + +div.controls { + box-sizing: border-box; + width: 250px; + max-width: 250px; + flex: 0 0 auto; + padding: 10px 0; + border-left: 4px solid #f4f4f4; + display: flex; + flex-direction: column; + overflow: hidden; +} +div.controls hr { + border: none; + height: 2px; + background: #f4f4f4; + margin-top: 10px; + margin-bottom: 10px; +} +div.controls hr.without-bottom-margin { margin-bottom: 0; } +div.controls hr.without-top-margin { margin-top: 0; } +div.controls hr.without-margins { margin: 0; } +div.controls .control { + display: flex; + justify-content: space-between; + align-items: center; + overflow: hidden; + height: 30px; + margin: 0 16px; +} +div.controls > h3 { + margin: 0 16px; +} +div.controls > textarea { + border: none; + padding:16px; + height: 400px; + font-family: "SFMono-Regular", Menlo, Consolas, Inconsolata, monospace; + outline: none; +} +div.controls .control > * { + /*max-width: 50%;*/ + flex: 1 1 auto; + margin:0; + margin-right: 16px; + box-sizing: border-box; +} +div.controls .control > :last-child { + margin-right: 0; +} +div.controls .control > select { + min-width: 6em; + align-items: center; + justify-content: center; +} +div.controls .control > input, +div.controls .control > select { + width: 0; + outline: none; +} +div.controls .control > input[type="number"], +div.controls .control > input[type="text"] { + background: none; + border: none; + padding: 4px 0; + font-size: 13px; +} +div.controls .control > input[type="number"] { + max-width: 60px; + -moz-font-feature-settings: 'calt' 1, 'zero' 1, 'tnum' 1; + -ms-font-feature-settings: 'calt' 1, 'zero' 1, 'tnum' 1; + -o-font-feature-settings: 'calt' 1, 'zero' 1, 'tnum' 1; + -webkit-font-feature-settings: 'calt' 1, 'zero' 1, 'tnum' 1; + font-feature-settings: 'calt' 1, 'zero' 1, 'tnum' 1; +} +div.controls .control > input[type=number]::-webkit-inner-spin-button, +div.controls .control > input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +div.controls .control > input[type="number"][readonly] { + max-width: 40px; +} +div.controls .control > input.wide[type="number"] { + max-width: 100%; +} +div.controls .control > input[type="range"] { + /*max-width: 80%;*/ + flex: 1 1 auto; + display: block; +} +div.controls .control > img.icon, +div.controls .control > label { + font-family: georgia, serif; + font-style: italic; + line-height: 16px; + color: black; + width: 16px; + height: 16px; + flex: 0 0 auto; + margin-right: 16px; + opacity: 0.6; +} +div.controls canvas { + height: 200px; +} + +.row.small-window { + margin-top:0; + padding-top:0; +} + +@media only screen and (min-width: 541px) { + .small-window { + display: none; + } +} + +@media only screen and (max-width: 540px) { + + .row.with-sidebar { + overflow: auto; + } + + div.controls { + display: none; + } + div.controls .graphplot, + div.controls hr.without-top-margin, + div.controls h3, + div.controls #ideal-values + { + display: none; + } + + .row.with-sidebar { + flex-direction: column; + } +} diff --git a/docs/dynmetrics/index.html b/docs/dynmetrics/index.html new file mode 100644 index 000000000..21bd95d82 --- /dev/null +++ b/docs/dynmetrics/index.html @@ -0,0 +1,522 @@ +--- +layout: default +title: Dynamic Metrics +--- +{% + +capture url_root + %}{% if site.safe == false %}/{% else %}/inter/{% endif +%}{% +endcapture %}{% + +for file in site.static_files %}{% + assign _path = file.path | remove_first: "/inter" %}{% + if _path == "/dynmetrics/index.css" %}{% + assign index_css_v = file.modified_time | date: "%Y%m%d%H%M%S" %}{% + elsif _path == "/res/bindings.js" %}{% + assign bindings_js_v = file.modified_time | date: "%Y%m%d%H%M%S" %}{% + elsif _path == "/res/graphplot.js" %}{% + assign graphplot_js_v = file.modified_time | date: "%Y%m%d%H%M%S" %}{% + endif %}{% +endfor + +%} +<link rel="stylesheet" href="index.css?v={{ index_css_v }}"> +<script src="{{url_root}}res/bindings.js?v={{ bindings_js_v }}"></script> +<script src="{{url_root}}res/graphplot.js?v={{ graphplot_js_v }}"></script> + +<div class="row first"><div> + <h1>Dynamic Metrics</h1> + <p> + There's of course no absolute right or wrong when it comes to expressing + yourself with typography, but Inter UI Dynamic Metrics provides guidelines + for <em>good</em> typography. + You simply provide the optical font size, and the tracking and leading + (letter spacing and line height) will be calculated using the following + formula: + </p> + <p> + <formula> + tracking = + <const>a</const> + <const>b</const> × + <const>e</const><sup>(<const>c</const> × fontSize)</sup> + </formula> + <formula> + leading = <const>l</const> × fontSize + </formula> + <formula> + <const>e</const> ≈ <num>2.718</num> + </formula> + </p> + <p class="small-window"> + <small>View this on a larger screen to enable interactive control.</small> + </p> +</div></div> + +<!-- <div class="row small-window"><div> + Hello +</div></div> --> + +<div class="white full-width row with-sidebar"> + + <div class="samples"> + <p contenteditable class="sample"> + <span class="di"><span></span></span> + <span class="info" + title="size, tracking, (ideal tracking) — progress bar shows distance from ideal" + >15 0.0 ( 0.0 )</span> + Such a riot of sea and wind strews the whole extent of beach with whatever has been lost or thrown overboard, or torn out of sunken ships. Many a man has made a good week’s work in a single day by what he has found while walking along the Beach when the surf was down. + </p> + </div> + + <div class="sidebar controls"> + <div class="control"> + <label title="Number of ideal matches">ni</label> + <input title="Number of ideal matches" type="number" readonly data-binding="ideal-count"> + <label title="Distance from ideal">di</label> + <input title="Distance from ideal" type="number" class="wide" readonly data-binding="ideal-distance"> + </div> + <div class="control"> + <img title="Style" class="icon" src="../samples/icons/style.svg"> + <select data-binding="style"> + <option value="regular" default>Regular</option> + <option value="italic">Italic</option> + <option value="medium" default>Medium</option> + <option value="medium-italic">Medium Italic</option> + <option value="bold" default>Bold</option> + <option value="bold-italic">Bold Italic</option> + <option value="black" default>Black</option> + <option value="black-italic">Black Italic</option> + </select> + </div> + <div class="control"> + <img title="Base tracking" class="icon" src="../samples/icons/letter-spacing.svg"> + <input type="range" min="-0.05" max="0.06" step="0.001" data-binding="base-tracking"> + <input type="number" min="-0.15" max="1" step="0.001" data-binding="base-tracking"> + </div> + <hr> + <div class="control"> + <label title="Constant a">a</label> + <input type="range" min="-0.05" max="0" step="0.001" data-binding="var-a"> + <input type="number" min="-0.05" max="0" step="0.0001" data-binding="var-a"> + </div> + <div class="control"> + <label title="Constant b">b</label> + <input type="range" min="0" max="1" step="0.01" data-binding="var-b"> + <input type="number" min="0" max="1" step="0.001" data-binding="var-b"> + </div> + <div class="control"> + <label title="Constant c">c</label> + <input type="range" min="-1" max="0" step="0.01" data-binding="var-c"> + <input type="number" min="-1" max="0" step="0.001" data-binding="var-c"> + </div> + <hr> + <div class="control"> + <label title="Constant l controls line height">l</label> + <input type="range" min="1" max="2" step="0.1" data-binding="var-l"> + <input type="number" min="1" max="2" step="0.1" data-binding="var-l"> + </div> + <hr class="without-bottom-margin"> + <canvas class="graphplot">Canvas not Supported</canvas> + <hr class="without-top-margin"> + <h3>Ideal values</h3> + <textarea id="ideal-values"></textarea> + <hr class="without-top-margin"> + </div> + +</div> + + +<script type="text/javascript">(function(){ + +// internal variables that are user-controllable, but are not part of +// Dynamic Metrics +var baseTracking = 0 +var weightClass = 400 + +// Ideal values designed one by one, by hand. +// font size => tracking +var idealValues = {} +var idealValuesList = [] +var idealValuesTextArea = $('textarea#ideal-values') + +function setIdealValues(valueMap) { + idealValues = valueMap + idealValuesList = [] + Object.keys(idealValues).forEach(function(fontSize) { + idealValuesList.push([fontSize, idealValues[fontSize]]) + }) + if (idealValuesTextArea.value == '') { + idealValuesTextArea.value = idealValuesList.map(function(t){ + return t[0] + '\t' + t[1] + }).join('\n') + } +} + +function parseValues(s) { + var values = {} + s = s.replace(/^[\s\r\t\n]+|[\s\r\t\n]+$/g, '') // trim whitespace + s.split(/\s*\r?\n\s*/).map(function(line) { + var t = line.split(/\s+/) + if (t.length == 2) { + t[0] = parseInt(t[0]) + t[1] = parseFloat(t[1]) + values[t[0]] = t[1] + } + }) + return values +} + +setIdealValues({ + // set-2018-02-20 + 6: 0.055, + 7: 0.044, + 8: 0.034, + 9: 0.024, + 10: 0.018, + 11: 0.012, + 12: 0.007, + 13: 0.003, + 14: 0.001, + 15: -0.002, + 16: -0.004, + 17: -0.006, + 18: -0.008, + 20: -0.01, + 24: -0.013, +}) + +// setIdealValues({ +// // set-2018-02-19 +// 6: 0.059, +// 7: 0.043, +// 8: 0.032, +// 9: 0.022, +// 10: 0.014, +// 11: 0.009, +// 12: 0.004, +// 13: 0, +// 14: -0.003, +// 15: -0.005, +// 16: -0.0075, +// 17: -0.0084, +// 18: -0.0095, +// 20: -0.012, +// 24: -0.014, +// }) + +// setIdealValues({ +// // set-2018-02-18 +// 6: 0.06, +// 7: 0.04, +// 8: 0.03, +// 9: 0.02, +// 10: 0.01, +// 11: 0.005, +// 12: 0.002, +// 13: 0.001, +// 14: 0, +// 15: -0.002, +// 16: -0.005, +// 17: -0.007, +// 18: -0.009, +// 20: -0.011, +// 24: -0.011, +// }) + + +// Variables for constants involved in Dynamic Metrics +var a = -0.013 +// -0.0119 +// -0.0101 +// -0.0092 +// -0.012 +// -0.013 + +var b = 0.23 +// 0.455 +// 0.421 +// 0.26 +// 0.23 +// 0.251 + +var c = -0.21 +// -0.29 +// -0.23 +// -0.21 +// -0.222 + +// a = -0.012; b = 0.23; c = -0.21; // di=0.002514 on set-2018-02-18 +// a = -0.013; b = 0.251; c = -0.222 // di=0.001742 on set-2018-02-18 +// a = -0.015; b = 0.283; c = -0.23; // di=0.00221 on set-2018-02-18 +// a = -0.0149; b = 0.298; c = -0.23; // di=0.000484 on set-2018-02-19 +a = -0.016; b = 0.21; c = -0.18; // di=0.000532 on set-2018-02-20 + + +// these have a short di, but stray far away from larger font sizes +// a = -0.0077 +// b = 0.377 + +var l = 1.4 + + +// InterUIDynamicTracking takes the font size in points or pixels and returns +// the compensating tracking in EM. +// +function InterUIDynamicTracking(fontSize, weightClass) { + // if (!weightClass) { -- currently unused + // weightClass = 400 + // } + // + // y = -0.01021241 + 0.3720623 * e ^ (-0.2808687 * x) + // [6-35] 0.05877 .. -0.0101309 (13==0; stops growing around 30-35) + // See https://gist.github.com/rsms/8efdbca5f8145a584ed08a7c3d6e5788 + // + return a + b * Math.pow(Math.E, c * fontSize) + // [6 - 38] 0.05798 .. -0.01099 (midpoint = 12.533) + + // y = 0.025 - (ln(x) * 0.01) + // return 0.025 - Math.log(fontSize) * 0.01 +} + + +function InterUIDynamicLineHeight(fontSize, weightClass) { + return Math.round(fontSize * l) +} + + +var NPOS = Number.MAX_SAFE_INTEGER + +function Sample(fontSize) { + this.rootEl = sampleTemplate.cloneNode(true) + this.infoEl = $('.info', this.rootEl) + this.diEl = $('.di', this.rootEl) + this.diEl.classList.add('unavailable') + this.style = this.rootEl.style + this.maxBoxWidth = 0 + this.fontSize = 0 + this.tracking = 0 + this.lineHeight = 0 + this.idealTracking = NPOS + this.matchesIdeal = false + this.setFontSize(fontSize) + this.render() +} + +Sample.prototype.idealDistance = function(fontSize) { + // returns the distance from the ideal (or NPOS if no "ideal" data available) + if (this.idealTracking == NPOS) { + return NPOS + } + return ( + Math.max(this.tracking, this.idealTracking) - + Math.min(this.tracking, this.idealTracking) + ) +} + +Sample.prototype.setFontSize = function(fontSize) { + this.fontSize = fontSize + this.tracking = baseTracking + InterUIDynamicTracking(fontSize, weightClass) + this.lineHeight = InterUIDynamicLineHeight(fontSize, weightClass) + this.maxBoxWidth = Math.round(fontSize * (this.tracking + 1) * 25) + + var idealTracking = idealValues[this.fontSize] + if (idealTracking === undefined) { + if (this.idealTracking != NPOS) { + this.diEl.classList.add('unavailable') + } + this.idealTracking = NPOS + this.matchesIdeal = false + } else { + if (this.idealTracking == NPOS) { + this.diEl.classList.remove('unavailable') + } + this.idealTracking = idealTracking + // match to a precision of 3 + this.matchesIdeal = this.tracking.toFixed(3) == idealTracking.toFixed(3) + var di = 1 + if (this.matchesIdeal) { + this.diEl.classList.add('match') + } else { + this.diEl.classList.remove('match') + di = 1 - Math.min(1, this.idealDistance() * 50) + } + this.diEl.firstChild.style.width = (di * this.maxBoxWidth) + 'px' + } +} + +Sample.prototype.render = function() { + this.style.fontSize = this.fontSize + 'px' + this.style.letterSpacing = this.tracking + 'em' + this.style.lineHeight = this.lineHeight + 'px' + + var SP = '\u00A0\u00A0\u00A0' + this.infoEl.innerText = ( + this.fontSize + + SP + this.tracking.toFixed(3) + + // SP + this.lineHeight.toFixed(3) + + ( + this.idealTracking != NPOS ? ( + SP + '( ' + this.idealTracking + + (this.matchesIdeal ? ' \u2713' : '') + + ' )' + ) : '' + ) + ) + + this.style.maxWidth = this.maxBoxWidth + 'px' +} + + +var bindings = new Bindings() +var sampleTemplate +var samples = [] // Sample[] +var graph = new GraphPlot($('canvas.graphplot')) +graph.setOrigin(-0.2, 0.8) +graph.setScale(0.049, 5) + +function addChildren(targetNode, children) { + targetNode.style.visibility = 'hidden' + var i = 0; + for (; i < children.length; i++) { + targetNode.appendChild(children[i]) + } + targetNode.style.visibility = null +} + +function initSamples() { + var samplesEl = $('.samples') + sampleTemplate = $('.sample', samplesEl) + samplesEl.removeChild(sampleTemplate) + + // small and medium sizes + var i, fontSize = 6, endFontSize = 16 + for (; fontSize <= endFontSize; fontSize++) { + samples.push(new Sample(fontSize)) + } + + // a few select large samples + samples.push(new Sample(17)) + samples.push(new Sample(18)) + samples.push(new Sample(20)) + samples.push(new Sample(24)) + samples.push(new Sample(30)) + samples.push(new Sample(40)) + + // add to dom in one go + addChildren(samplesEl, samples.map(function(s) { return s.rootEl })) +} + + +function updateSample(sample) { + sample.setFontSize(sample.fontSize) // updates derived values + sample.render() +} + +function updateSamples() { + samples.forEach(updateSample) + updateIdealMatches() + updateGraphPlot() +} + +function updateIdealMatches() { + // ideal-distance + var idealCount = 0 + var distance = 0 + var ndistances = 0 + samples.forEach(function(sample) { + if (sample.matchesIdeal) { + idealCount++ + } + var di = sample.idealDistance() + if (di != NPOS) { + distance += di + ndistances++ + } + }) + + distance = distance / ndistances + + bindings.setValue('ideal-distance', distance.toFixed(6)) + bindings.setValue('ideal-count', idealCount) +} + +function updateGraphPlot() { + graph.clear() + graph.plotLine(idealValuesList, '#0d3') + graph.plotf(function(x) { + return InterUIDynamicTracking(x, weightClass) + }) +} + + +bindings.configure('base-tracking', 0, 'float', function (tracking) { + baseTracking = tracking + updateSamples() +}) + +bindings.configure('var-a', a, 'float', function (v) { + a = v + updateSamples() +}) + +bindings.configure('var-b', b, 'float', function (v) { + b = v + updateSamples() +}) + +bindings.configure('var-c', c, 'float', function (v) { + c = v + updateSamples() +}) + +bindings.configure('var-l', l, 'float', function (v) { + l = v + updateSamples() +}) + +bindings.configure('ideal-count', 0, 'int', function (v) {}) +bindings.configure('ideal-distance', 0, 'float', function (v) {}) + +bindings.configure('style', null, null, function (style) { + var cl = $('.samples').classList + cl.remove('font-style-regular') + cl.remove('font-style-italic') + cl.remove('font-style-medium') + cl.remove('font-style-medium-italic') + cl.remove('font-style-bold') + cl.remove('font-style-bold-italic') + cl.remove('font-style-black') + cl.remove('font-style-black-italic') + cl.add('font-style-' + style) + weightClass = ( + style.indexOf('black') != -1 ? 900 : + style.indexOf('bold') != -1 ? 700 : + style.indexOf('medium') != -1 ? 500 : + 400 + ) + updateSamples() +}) + + +bindings.bindAllInputs('.control input') +bindings.bindAllInputs('.control select') + +// double-click base tracking to reset +$('input[type="range"][data-binding="base-tracking"]').addEventListener( + 'dblclick', + function(ev) { bindings.setValue('base-tracking', 0) } +) + +// allow editing of ideal values +idealValuesTextArea.addEventListener('input', function(ev) { + setIdealValues(parseValues(idealValuesTextArea.value)) + updateSamples() +}) + + +// start +initSamples() +updateSamples() + +})();</script> |