mirror of
https://github.com/rsms/inter.git
synced 2024-12-01 04:20:46 +03:00
603 lines
17 KiB
HTML
603 lines
17 KiB
HTML
---
|
||
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 how to best use Inter UI.
|
||
You simply provide the optical font size,
|
||
and the tracking and leading is calculated for you through the following
|
||
formula:
|
||
</p>
|
||
<p>
|
||
<formula>
|
||
tracking =
|
||
<const>a</const> + <const>b</const> ×
|
||
<const title="Base of natural logarithm; ≈2.718">e</const><sup>(<const>c</const> × <const>z</const>)</sup>
|
||
</formula>
|
||
<formula>
|
||
leading = round(<num data-binding="var-l">1.4</num> × <const>z</const>)
|
||
</formula>
|
||
<formula title="Values for Inter UI">
|
||
<g><const title="Constant a">a</const> = <num data-binding="var-a">-0.016</num></g>
|
||
<g><const title="Constant b">b</const> = <num data-binding="var-b">0.21</num></g>
|
||
<g><const title="Constant c">c</const> = <num data-binding="var-c">-0.18</num></g>
|
||
<g><const>z</const> = font size</g>
|
||
</formula>
|
||
</p>
|
||
<p class="wide-window">
|
||
The controls below can be used to play around with the
|
||
<const>a</const>, <const>b</const> and <const>c</const> constants to discover
|
||
how they affect tracking.
|
||
</p>
|
||
<p class="narrow-window">
|
||
<small>View this on a larger screen to enable interactive control.</small>
|
||
</p>
|
||
</div></div>
|
||
|
||
<div class="white full-width row with-sidebar">
|
||
|
||
<div class="samples">
|
||
<p 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>
|
||
<span contenteditable class="content">
|
||
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.
|
||
</span>
|
||
</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="semi-bold" default>Semi Bold</option>
|
||
<option value="semi-bold-italic">Semi Bold Italic</option>
|
||
<option value="bold" default>Bold</option>
|
||
<option value="bold-italic">Bold Italic</option>
|
||
<option value="extra-bold" default>Extra Bold</option>
|
||
<option value="extra-bold-italic">Extra 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="when-selection without-top-margin">
|
||
<h3 class="when-selection">CSS</h3>
|
||
<textarea class="when-selection" readonly id="code-output"></textarea>
|
||
|
||
<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
|
||
|
||
function round(num, prec) {
|
||
return parseFloat(num.toFixed(prec))
|
||
}
|
||
|
||
// 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({
|
||
// 2018-09-28
|
||
6: 0.054,
|
||
7: 0.042,
|
||
8: 0.033,
|
||
9: 0.025,
|
||
10: 0.018,
|
||
11: 0.012,
|
||
12: 0.008,
|
||
13: 0.004,
|
||
14: 0,
|
||
15: -0.002,
|
||
16: -0.005,
|
||
17: -0.007,
|
||
18: -0.008,
|
||
20: -0.011,
|
||
24: -0.014,
|
||
30: -0.016,
|
||
40: -0.017,
|
||
})
|
||
|
||
|
||
// Variables for constants involved in Dynamic Metrics
|
||
|
||
// var a = -0.012, b = 0.23, c = -0.21; // di=0.002514 on set-2018-02-18
|
||
// var a = -0.013, b = 0.251, c = -0.222 // di=0.001742 on set-2018-02-18
|
||
// var a = -0.015, b = 0.283, c = -0.23; // di=0.00221 on set-2018-02-18
|
||
// var a = -0.0149, b = 0.298, c = -0.23; // di=0.000484 on set-2018-02-19
|
||
// var a = -0.018, b = 0.21, c = -0.18; // di=0.000532 on set-2018-02-20
|
||
var a = -0.017, b = 0.202, c = -0.175; // di=0.000247 on 2018-09-28
|
||
|
||
var l = 1.4
|
||
|
||
|
||
// _InterUIDynamicTracking is a version of InterUIDynamicTracking that
|
||
// uses some global variables that are adjustable.
|
||
//
|
||
function _InterUIDynamicTracking(fontSize, weightClass) {
|
||
// Note: weightClass is currently unused
|
||
//
|
||
// 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.contentEl = $('.content', 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()
|
||
|
||
// listen for focus events on the editable content
|
||
var s = this
|
||
this.contentEl.addEventListener(
|
||
'focus',
|
||
function(){ s.onReceivedFocus() },
|
||
{passive:true, capture:false}
|
||
)
|
||
this.contentEl.addEventListener(
|
||
'blur',
|
||
function(){ s.onLostFocus() },
|
||
{passive:true, capture:false}
|
||
)
|
||
}
|
||
|
||
Sample.prototype.onReceivedFocus = function() {}
|
||
Sample.prototype.onLostFocus = function() {}
|
||
|
||
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.cssProperties = function() {
|
||
return [
|
||
['font-size', round(this.fontSize, 3) + 'px'],
|
||
['letter-spacing', round(this.tracking, 3) + 'em'],
|
||
['line-height', round(this.lineHeight, 3) + '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 samplesEl = $('.samples')
|
||
var sampleTemplate
|
||
var samples = [] // Sample[]
|
||
var focusedSample = null // Sample | null
|
||
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() {
|
||
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))
|
||
|
||
// connect focus events
|
||
var onSampleReceivedFocus = function() { setSelectedSample(this) }
|
||
samples.forEach(function(s) {
|
||
s.onReceivedFocus = onSampleReceivedFocus
|
||
})
|
||
|
||
// add to dom in one go
|
||
addChildren(samplesEl, samples.map(function(s) { return s.rootEl }))
|
||
}
|
||
|
||
function setSelectedSample(sample) {
|
||
if (focusedSample !== sample) {
|
||
if (focusedSample) {
|
||
focusedSample.rootEl.classList.remove('selected')
|
||
}
|
||
if (sample) {
|
||
sample.rootEl.classList.add('selected')
|
||
}
|
||
focusedSample = sample
|
||
updateSelection()
|
||
}
|
||
}
|
||
|
||
function updateSample(sample) {
|
||
sample.setFontSize(sample.fontSize) // updates derived values
|
||
sample.render()
|
||
}
|
||
|
||
function updateSamples() {
|
||
samples.forEach(updateSample)
|
||
updateIdealMatches()
|
||
updateGraphPlot()
|
||
updateCodeView()
|
||
}
|
||
|
||
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)
|
||
})
|
||
if (focusedSample) {
|
||
var graphedFontSize = Math.min(24, focusedSample.fontSize) // clamp to [-inf,24]
|
||
graph.plotPoints([
|
||
[graphedFontSize, focusedSample.tracking]
|
||
], 'rgb(45, 143, 255)')
|
||
}
|
||
}
|
||
|
||
var codeOutput = $('#code-output')
|
||
codeOutput.addEventListener('focus', function(ev) {
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
codeOutput.select()
|
||
}, {passive:false,capture:true})
|
||
|
||
codeOutput.addEventListener('pointerdown', function(ev) {
|
||
// TODO: don't do this if codeOutput is focused
|
||
ev.preventDefault()
|
||
ev.stopPropagation()
|
||
codeOutput.select()
|
||
document.execCommand("copy")
|
||
HUDNotification.show('Copied to clipboard')
|
||
}, {passive:false,capture:true})
|
||
|
||
function updateCodeView() {
|
||
var s = ''
|
||
if (focusedSample) {
|
||
var props = focusedSample.cssProperties()
|
||
props.forEach(function(prop, i) {
|
||
var name = prop[0], value = prop[1]
|
||
s += name + ': ' + value + ';'
|
||
if (i != props.length-1) {
|
||
s += '\n'
|
||
}
|
||
})
|
||
}
|
||
codeOutput.value = s
|
||
}
|
||
|
||
function updateSelection() {
|
||
var controlsEl = $('.controls')
|
||
if (focusedSample) {
|
||
controlsEl.classList.add('has-selected-sample')
|
||
} else {
|
||
controlsEl.classList.remove('has-selected-sample')
|
||
}
|
||
|
||
updateGraphPlot()
|
||
updateCodeView()
|
||
}
|
||
|
||
|
||
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 = samplesEl.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-semi-bold')
|
||
cl.remove('font-style-semi-bold-italic')
|
||
cl.remove('font-style-bold')
|
||
cl.remove('font-style-bold-italic')
|
||
cl.remove('font-style-extra-bold')
|
||
cl.remove('font-style-extra-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('[data-binding]')
|
||
|
||
// double-click base tracking to reset
|
||
$('input[type="range"][data-binding="base-tracking"]').addEventListener(
|
||
'dblclick',
|
||
function(ev) { bindings.setValue('base-tracking', 0) }
|
||
)
|
||
|
||
;[
|
||
['var-a',a],
|
||
['var-b',b],
|
||
['var-c',c],
|
||
['var-l',l],
|
||
['base-tracking', 0]
|
||
].forEach(p => {
|
||
let bindname = p[0], defval = p[1]
|
||
$$('input[type="range"][data-binding="'+bindname+'"]').forEach(e => {
|
||
e.addEventListener('dblclick', ev =>
|
||
bindings.setValue(bindname, defval))
|
||
})
|
||
})
|
||
|
||
// allow editing of ideal values
|
||
idealValuesTextArea.addEventListener('input', function(ev) {
|
||
setIdealValues(parseValues(idealValuesTextArea.value))
|
||
updateSamples()
|
||
})
|
||
|
||
// listen for clicks onto "background", to deselect any selected sample
|
||
document.body.addEventListener('pointerdown', function(ev){
|
||
if (
|
||
ev.target === document.body ||
|
||
ev.target === samplesEl ||
|
||
ev.target.classList && ev.target.classList.contains('row')
|
||
) {
|
||
setSelectedSample(null)
|
||
}
|
||
})
|
||
|
||
|
||
// start
|
||
initSamples()
|
||
updateSamples()
|
||
|
||
})();</script>
|