Skip to content

Instantly share code, notes, and snippets.

@aaronbarker
Created March 30, 2017 15:21
Show Gist options
  • Save aaronbarker/ccb985c09232e80bf3cf5024679d8f57 to your computer and use it in GitHub Desktop.
Save aaronbarker/ccb985c09232e80bf3cf5024679d8f57 to your computer and use it in GitHub Desktop.
Viewport Sized Typography Visualizer
<h1>Viewport Sized Typography Visualizer</h1>
<p>At work we are exploring the potential use of <a href="https://css-tricks.com/snippets/sass/viewport-sized-typography-minimum-maximum-sizes/">Viewport Sized Typography with Minimum and Maximum Sizes</a>. As part of this exploration we realized that the "fluid" portion of a given min/fluid/max set of sizes would be different for each combination. I wanted to make a visual reprsentation of how each set of sizes would overlap so we could make educated adjustments to the fluid portions and their overlaps to have the best experience possible.</p>
<div class="sizes">
<h2>Size properties</h2>
<div id="sizes__instructions" class="hide">
<p>Provide the min/fluid/max settings for as many size combinations as you would like to see how their ranges will overlap. You can easily adjust the fluid portion via the slider for each size and then see how the combined sets of numbers will overlap.</p>
<p>If you need a custom font that isn't installed, you can fork this pen, add custom fonts following <a href="https://blog.codepen.io/2012/07/18/custom-fonts-in-pens/">these instructions</a> and then use the new font name in the font fields below.</p>
</div>
<a href='#sizes__instructions' class='shower'>View Size Instructions</a>
<div class="sizes__sets">
<div class="sizes__set" id="set1">
Name: <input type="text" class="name" value="Set 1" /><br />
Min: <input type="text" class="min" value="38" />px<br />
Fluid: <input type="range" class="range" min="2.00" max="10.00" step=".01" value="5" /> (<span class="theVW"></span>)<br />
Max: <input type="text" class="max" value="70" />px<br />
Font(s): <input type="text" class="font" value="Helvetica" />
Weight: <select class="weight">
<option value="100">100</option>
<option value="200">200</option>
<option value="300">300</option>
<option value="400" selected>400</option>
<option value="500">500</option>
<option value="600">600</option>
<option value="700">700</option>
<option value="800">800</option>
<option value="900">900</option>
</select>
<a href="#d" class="sizes__delete">X</a>
</div>
</div>
<a href="#d" class="sizes__add">Add additional size set</a>
</div>
<section class="example">
<h2>Example</h2>
<div class="example__instructions hide" id="example__instructions">
<p>Move your mouse over the colored example row(s) below. Where your cursor is denotes the width of the browser. Below the colored row(s) will show an example of what the text sizes of each set would be at that width. This allows you to see the hierarchy of the text against other sizes at any width. Why show this via weird mousemove "width" instead of just resizing your browser? This gives a better visual representation of where sizes will shift amongst your different fluid type.</p>
<p>The left is the range of width where the min-font size will apply. The middle is the range where the fluid font size will apply (sometimes missing if min/max is the same). The right is the range where the max font size will apply.</p>
<p>Red lines denote where there is overlap of the same size across multiple examples. If there are multiple instances of the same size having an overlap on the same example, it shows as an incorrect single line between the multiple instances. It is a hopefully rare condition so since I barely got this working in the first place, I won't tackle unless it becomes a bigger thing.</p>
</div>
<a href='#example__instructions' class='shower'>View Example Instructions</a><br />
<a href='#d' class="preset" data-preset="38/6/70/Helvetica/400|26/4/38/Helvetica/400|20/3/26/Helvetica/400|18/3/18/Helvetica/400">Good example</a> - 4 sets of sizes that overlap well.<br />
<a href="#d" class="preset" data-preset="38/4.94/70/Helvetica/400|26/5.24/38/Helvetica/400|20/6.03/26/Helvetica/400|18/3/18/Helvetica/400">Bad example</a> - Same min/max as above, only the fluid portion changed from the good example and now sizes overlap poorly.
<div class="example__examples">
<div class="example__width-indicator">
<div class="example__width-x"></div>
<div class="example__width-x2"></div>
</div>
<div class="example__example" id="example1">
<div class="example__min">
<div class="example__inner-wrapper">
<!-- min font size: -->
<span class="example__size"></span><br/>
<span class="example__width"></span>
</div>
</div>
<div class="example__fluid">
<div class="example__inner-wrapper">
<!-- fluid font size: -->
<span class="example__size"></span><br/>
<span class="example__width"></span>
</div>
</div>
<div class="example__max">
<div class="example__inner-wrapper">
<!-- max font size: -->
<span class="example__size"></span><br/>
<span class="example__width"></span>
</div>
</div>
</div>
</div>
<h3>Sizes at mouse location</h3>
<div class="example__mouse-size">
</div>
</section>
<section class="documentation">
<h2>Loading Preset Data</h2>
<p>Any change you make is saved into <code>localStorage</code> to persist through editor changes and refreshes. If you want to send the current state to others, use the URL below.</p>
<p>URL to copy: <input type="text" class="preset-url" /></p>
</section>
<section class="experimental hide" id="experimental">
<h2>Experimental</h2>
<h3>Custom Font Code (css only)</h3>
<p>While there is a way to load <a href="https://blog.codepen.io/2012/07/18/custom-fonts-in-pens/">custom fonts</a> in a forked pen, this will allow you to load some custom font styling that is shareable and uses the original pen to get potential updates. This has the potential to add a LOT of data to the URL for the shareable version. Not sure where the limits will be reached, so be careful.</p>
<textarea class='custom-fonts'></textarea>
</section>
<a href='#experimental' class='shower'>View Experimental Stuff</a>
<section class="sizes">
<h2>All font sizes</h2>
<p>A visualization of all fixed (min/max) font sizes defined above.</p>
<div class="sizes__sizes">
</div>
</section>
/*
This isn't meant to be bulletproof. It isn't meant to be a demonstration of quality JS code. The tool in the browser is the objective, although I have tried to be semi clean in the JS anyway.
*/
const viewportVisualizer = {
lastX: 0,
smallestMin:0,
biggestMax:0,
setData:{},
fontSizes:[],
init() {
const self = this;
// hook up the "add additional size set" link
document.querySelector('.sizes__add').addEventListener('click', function() {
self.addSet();
});
// hookup the controls of the initial set
self.hookupControls(document.querySelector('.sizes__set'));
// hook up show instructions links
[].forEach.call(document.querySelectorAll('.shower'), function(curLink){
curLink.addEventListener('click', function(event){
event.preventDefault();
const target = document.querySelector(curLink.getAttribute('href'));
target.classList.remove('hide');
curLink.parentNode.removeChild(curLink);
});
});
// hook up preset links
[].forEach.call(document.querySelectorAll('.preset'), function(curLink){
curLink.addEventListener('click', function(event){
event.preventDefault();
viewportVisualizer.loadPreset(curLink.dataset.preset);
});
});
// check to see if any customFont stuff is in the URL or localStorage
self.customFont();
const customFontURL = getUrlParameter('custom-font');
let customFont;
if(customFontURL) {
customFont = customFontURL;
} else if (localStorage.getItem('custom-font')){
customFont = localStorage.getItem('custom-font');
}
self.loadCustomFont(customFont);
// check to see if any prests were declared in the URL or localstorage
let presets;
if(window.location.search.indexOf('presets') !== -1) {
presets = getUrlParameter('presets');
} else if (localStorage.getItem('presets')){
presets = localStorage.getItem('presets');
} else {
// load a good default for demo purposes
presets = '38/6/70/Helvetica/400|26/4/38/Helvetica/400|20/3/26/Helvetica/400|18/3/18/Helvetica/400'
}
self.loadPreset(presets);
// hide the delete links initially since there is only one.
self.hideDelete();
self.hoverStuff();
},
loadPreset(presets){
const sets = presets.split("|");
sets.forEach(function(set, index){
let curSet = document.querySelector('#set' + (index+1));
if(!curSet){
viewportVisualizer.addSet();
curSet = document.querySelector('#set' + (index+1));
}
// populate data
const data = set.split('/');
curSet.querySelector('.min').value = data[0];
curSet.querySelector('.range').value = data[1];
curSet.querySelector('.max').value = data[2];
curSet.querySelector('.font').value = data[3]||'Helvetica';
curSet.querySelector('.weight').value = data[4]||400;
curSet.querySelector('.name').value = data[5]||'Set '+(index+1);
});
viewportVisualizer.getResults();
viewportVisualizer.updateExampleStyle(viewportVisualizer.lastX);
},
getResults(){
const sizeSets = document.querySelectorAll('.sizes__set');
viewportVisualizer.smallestMin = 10000;
viewportVisualizer.biggestmax = 0;
viewportVisualizer.fontSizes = [];
[].forEach.call(sizeSets, function(curSet){
const curID = curSet.getAttribute('id');
const curNum = curID.match(/\d+/)[0];
const curExample = document.querySelector('#example'+curNum);
const curName = curSet.querySelector('.name').value;
const minVal = parseInt(curSet.querySelector('.min').value, 10);
const fluidVal = curSet.querySelector('.range').value;
const maxVal = parseInt(curSet.querySelector('.max').value, 10);
const fontVal = curSet.querySelector('.font').value;
const weightVal = curSet.querySelector('.weight').value;
// console.log(minVal, fluidVal, maxVal)
const minWidth = Math.round(parseInt(minVal, 10) / parseInt(fluidVal, 10) * 100, 2);
const maxWidth = Math.round(parseInt(maxVal, 10) / fluidVal * 100, 2);
const fluidRange = Math.round(maxWidth - minWidth, 2);
// console.log(minWidth, maxWidth, fluidRange)
// update the example
// get the elements we need to set data into
const minExample = curExample.querySelector('.example__min');
const fluidExample = curExample.querySelector('.example__fluid');
const maxExample = curExample.querySelector('.example__max');
const minExampleSize = minExample.querySelector('.example__size');
const fluidExampleSize = fluidExample.querySelector('.example__size');
const maxExampleSize = maxExample.querySelector('.example__size');
const minExampleWidth = minExample.querySelector('.example__width');
const fluidExampleWidth = fluidExample.querySelector('.example__width');
const maxExampleWidth = maxExample.querySelector('.example__width');
// save some data into the element for later use
curExample.min = minVal;
curExample.fluid = fluidVal;
curExample.max = maxVal;
curExample.minW = minWidth;
curExample.maxW = maxWidth;
curExample.set = curSet;
curExample.font = fontVal;
curExample.weight = weightVal;
curExample.curID = curID;
curExample.curName = curName;
viewportVisualizer.setData[curID] = {
minW: minWidth,
maxW: maxWidth,
weight: weightVal,
min: minVal,
max: maxVal
}
if(viewportVisualizer.fontSizes.indexOf(minVal) === -1){
viewportVisualizer.fontSizes.push(minVal);
}
if(viewportVisualizer.fontSizes.indexOf(maxVal) === -1){
viewportVisualizer.fontSizes.push(maxVal);
}
curExample.sizeAtX = function(x) {
if (x < minWidth) {
curSize = minVal;
} else if (x > maxWidth) {
curSize = maxVal;
} else if(x === minWidth && x === maxWidth) {
// special case when there is no fluid portion
curSize = minVal;
} else {
// console.log('fluid', x, minW, maxW, max, min)
const mouseInFluid = x - minWidth;
const fluidRange = (maxWidth - minWidth);
const percentageOfFluid = mouseInFluid/fluidRange;
const fluidFontRange = maxVal - minVal;
curSize = parseInt(minVal, 10) + parseInt(fluidFontRange * percentageOfFluid, 10);
}
return curSize;
};
// update the respective elements with their particular data
// minExampleSize.style.fontSize = minVal + 'px';
minExampleSize.innerHTML = minVal + 'px';
minExampleWidth.innerHTML = '0 - ' + minWidth + 'px';
// fluidExampleSize.style.fontSize = fluidVal + 'vw';
fluidExampleSize.innerHTML = fluidVal + 'vw';
curSet.querySelector('.theVW').innerHTML = fluidVal + 'vw';
fluidExampleWidth.innerHTML = minWidth + 'px - ' + maxWidth + 'px (' +fluidRange+ 'px total)';
// maxExampleSize.style.fontSize = maxVal + 'px';
maxExampleSize.innerHTML = maxVal + 'px';
maxExampleWidth.innerHTML = maxWidth + 'px - ∞';
// show visual of when each font size will apply
minExample.style.width = minWidth + 'px';
minExample.style.minWidth = minWidth + 'px';
fluidExample.style.width = (maxWidth - minWidth) + 'px';
fluidExample.style.minWidth = (maxWidth - minWidth) + 'px';
// maxExample.style.width = maxWidth + 'px'; // max needs to go to the edge of the screen (inifinity), so just set to a large number in the css
if(minWidth < viewportVisualizer.smallestMin) {
viewportVisualizer.smallestMin = minWidth;
// console.log('new min', minWidth);
}
if(maxWidth > viewportVisualizer.biggestMax) {
viewportVisualizer.biggestMax = maxWidth;
// console.log('new max', maxWidth);
}
});
viewportVisualizer.fontSizes.sort((a, b) => (a - b));
viewportVisualizer.showSizes();
viewportVisualizer.overlapCheck();
viewportVisualizer.updateExampleStyle(viewportVisualizer.lastX);
// update the URL with the latest values
const presets = viewportVisualizer.getData();
document.querySelector('.preset-url').value = `http://codepen.io/aaronbarker/pen/zZroxL?${ presets }`;
// console.log('set storage')
// localStorage.setItem('presets', presets);
// console.log(localStorage.getItem('presets'))
// history.replaceState({},'',)
},
addSet(){
const self = this;
// find the last set and duplicate it
const lastSet = document.querySelector('.sizes__set:last-of-type');
const lastSetID = lastSet.getAttribute('id');
const lastSetNum = lastSetID.match(/\d+/)[0];
const newSetNum = parseInt(lastSetNum,10) + 1;
const newSet = lastSet.cloneNode(true);
newSet.setAttribute('id', 'set' + newSetNum);
newSet.querySelector('.name').value = 'Set ' + newSetNum;
document.querySelector('.sizes__sets').appendChild(newSet);
self.hookupControls(newSet);
// find the last example and duplicate it
const lastExample = document.querySelector('.example__example:last-of-type');
const lastExampleID = lastExample.getAttribute('id');
const lastExampleNum = lastExampleID.match(/\d+/)[0];
const newExampleNum = parseInt(lastExampleNum,10) + 1;
const newExample = lastExample.cloneNode(true);
newExample.setAttribute('id', 'example' + newExampleNum);
document.querySelector('.example__examples').appendChild(newExample);
newSet.example = newExample;
self.hideDelete();
self.getResults();
},
removeSet(curSet){
const self = this;
const example = document.querySelector('#example'+ curSet.getAttribute('id').match(/\d+/)[0]);
curSet.parentNode.removeChild(curSet);
example.parentNode.removeChild(example);
self.hideDelete();
},
hideDelete(){
if(document.querySelectorAll('.sizes__set').length === 1) {
document.querySelector('body').classList.add('hide-delete');
} else {
document.querySelector('body').classList.remove('hide-delete');
}
},
hookupControls(curSet){
const self = this;
const min = curSet.querySelector('.min');
const range = curSet.querySelector('.range');
const max = curSet.querySelector('.max');
const font = curSet.querySelector('.font');
const weight = curSet.querySelector('.weight');
const name = curSet.querySelector('.name');
min.addEventListener('change', self.getResults);
range.addEventListener('input', self.getResults);
max.addEventListener('change', self.getResults);
font.addEventListener('change', self.getResults);
weight.addEventListener('change', self.getResults);
name.addEventListener('change', self.getResults);
curSet.querySelector('.sizes__delete').addEventListener('click', function(e){
e.preventDefault();
self.removeSet(curSet);
});
},
hoverStuff(){
const self = this;
const example = document.querySelector('.example__examples');
const visual = example.querySelector('.example__width-indicator');
const widthX = example.querySelector('.example__width-x');
const widthX2 = example.querySelector('.example__width-x2');
example.addEventListener('mousemove', function(event){
const x = event.clientX;
visual.style.left = (x - 20)+'px';
widthX.innerHTML = x+'px';
widthX2.innerHTML = x+'px';
self.updateExampleStyle(x);
// console.log('lastX to', x);
self.lastX = x;
});
},
overlapCheck(){
// oh man this thing is ugly. Comparing across multiple rows takes many loops (first document the sizes, then compare, then display results.... bah!!). Don't look directly at this function, it will make you think bad thoughts.
// find the smallest min size, which is where we will start comparing
// find the largest max size, which is where we will stop comparing
const examples = document.querySelectorAll('.example__example');
const setData = viewportVisualizer.setData;
const setDataKeys = Object.keys(setData);
const overlapRanges = {};
setDataKeys.forEach(function(key){
overlapRanges[key] = {};
});
// loop through each horizontal pixel we need to worry about
const megaTracker = {};
for(let x = viewportVisualizer.smallestMin; x <= viewportVisualizer.biggestMax; x++){
// look at each example row and see what the size is at the curent X
// if rows match, do something. Track the begining X
// if the row had a match going, but doesn't anymore, stop tracking it and record the final X
// there can be multiple matches per row, so need to store the start/stop in an way that allows multiple
megaTracker[x] = [];
[].forEach.call(examples, function(curExample){
megaTracker[x].push(curExample.sizeAtX(x));
});
// megaTracker[x] has a font size for each example
megaTracker[x].forEach(function(cur, index){
// console.log(cur)
megaTracker[x].forEach(function(cur2, index2){
// console.log(cur)
if(index !== index2 && cur === cur2){
// console.log('match at ' + x + ' for '+ cur + ' font size for set' + (index+1) + ' example');
if(!overlapRanges['set'+(index+1)][cur]){
overlapRanges['set'+(index+1)][cur] = {};
overlapRanges['set'+(index+1)][cur].start = x;
}
overlapRanges['set'+(index+1)][cur].end = x;
}
});
});
}
// console.log(overlapRanges);
Object.keys(overlapRanges).forEach(function(key){
// overlapRanges[key] = {};
curExample = document.querySelector('#example'+key.match(/\d+/)[0]);
// remove curent ranges
[].forEach.call(curExample.querySelectorAll('.overlap-range'),function(curRange){
curRange.parentNode.removeChild(curRange);
});
let ranges = "";
Object.keys(overlapRanges[key]).forEach(function(key2){
// overlapRanges[key] = {};
// console.log(key2);
// curExample = document.querySelector('#'+key);
const left = overlapRanges[key][key2].start-20;// minus 20 for the negative margin for the body padding
ranges += '<span class="overlap-range" data-overlap-size="'+key2+'" style="left:'+left+'px; width:'+(overlapRanges[key][key2].end - overlapRanges[key][key2].start)+'px"></span>';
});
// console.log(ranges)
curExample.insertAdjacentHTML('beforeend', ranges);
});
},
updateExampleStyle(x) {
const self = this;
// how to determine what size of each font is available at the point under the line
const examples = document.querySelectorAll('.example__example');
const examplesAtMouse = document.querySelector('.example__mouse-size');
let examplesAtMouseContent = '';
[].forEach.call(examples, function(curExample){
// const minW = curExample.minW;
// const maxW = curExample.maxW;
// const min = curExample.min;
// const max = curExample.max;
const font = curExample.font;
const weight = curExample.weight;
let curSize = curExample.sizeAtX(x);
// console.log('curSize', curSize);
const setName = curExample.curName;
examplesAtMouseContent += '<span style="font-size: '+ curSize +'px; font-family:'+ font+'; font-weight:' + weight + '" class="at-mouse-size" data-size="'+ curSize + '">'+ setName +': ' + curSize + 'px</span><br />';
});
examplesAtMouse.innerHTML = examplesAtMouseContent;
// loop through each at mouse location example to see if we have matches of size to call out
[].forEach.call(document.querySelectorAll('.at-mouse-size'), function(curExample){
const curSize = curExample.dataset.size;
const matching = document.querySelectorAll('[data-size="'+curSize+'"]');
if(matching.length > 1){
[].forEach.call(matching, function(curMatch){
curMatch.classList.add('match');
});
}
});
},
showSizes(){
const sizes = document.querySelector('.sizes__sizes');
sizes.innerHTML = '';
viewportVisualizer.fontSizes.forEach(function(curSize){
sizes.innerHTML += '<div class="sizes__size" style="font-size: ' + curSize + 'px">' + curSize + '</div>';
});
},
getData(){
let data = '';
const sizeSets = document.querySelectorAll('.sizes__set');
[].forEach.call(sizeSets, function(curSet){
data += `${ curSet.querySelector('.min').value }/${ curSet.querySelector('.range').value }/${ curSet.querySelector('.max').value }/${ curSet.querySelector('.font').value }/${ curSet.querySelector('.weight').value }/${ curSet.querySelector('.name').value }|`;
});
data = data.replace(/\|$/,'');
// console.log('saving data to LS', data)
localStorage.setItem('presets', data);
localStorage.setItem('custom-font', document.querySelector('.custom-fonts').value);
return 'presets='+encodeURIComponent(data)+'&custom-font='+encodeURIComponent(document.querySelector('.custom-fonts').value);
},
customFont(){
const customField = document.querySelector('.custom-fonts');
const head = document.querySelector('head');
const lsCustomFont = localStorage.getItem('custom-font');
const newStyle = document.createElement('style');
newStyle.classList.add('custom-font');
head.appendChild(newStyle);
customField.addEventListener('change', function(){
viewportVisualizer.loadCustomFont(customField.value);
viewportVisualizer.getResults();
});
// console.log('lsCustomFont', lsCustomFont);
if(lsCustomFont) {
viewportVisualizer.loadCustomFont(lsCustomFont);
customField.value = lsCustomFont;
}
},
loadCustomFont(val) {
document.querySelector('.custom-font').textContent = val;
localStorage.setItem('custom-font', val);
document.querySelector('.custom-fonts').value = val;
}
};
viewportVisualizer.init();
// experimenting with stuff, hopefully it doesn't break anything in the process
function getUrlParameter(name) {
name = name.replace(/[\[]/, '[').replace(/[\]]/, ']');// eslint-disable-line
const regex = new RegExp('[\?&]' + name + '=([^&#]*)');// eslint-disable-line
const results = regex.exec(location.search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}
$bodyPadding: 20px;
input[type="text"] {
width: 50px;
}
input.name,input.font {
width: 70%;
}
input.preset-url {
width: 60%;
}
.sizes {
&__sets {
display:flex;
}
&__set {
background: #f1f1f1;
padding: 10px;
margin-right: 10px;
margin-bottom: 10px;
position: relative;
h3 {
margin-top: 0;
}
}
&__delete {
position: absolute;
top:10px;
right:10px;
.hide-delete & {
display:none;
}
}
}
.example {
position: relative;
&__width-indicator {
width: 2px;
background: black;
height: calc(100% + 20px);
position: absolute;
margin-top: -10px;
pointer-events: none;
}
&__width-x,&__width-x2 {
position: absolute;
top:-15px;
transform:translateX(-50%);
background:black;
color:white;
padding: 2px;
}
&__width-x2 {
top: auto;
bottom: -15px;
}
&__examples {
position: relative;
margin-top: 30px;
margin-bottom: 30px;
}
&__example {
display: flex;
overflow: hidden;
margin-bottom: 10px;
margin-left: -$bodyPadding;
margin-right: -$bodyPadding;
}
&__inner-wrapper {
padding: 5px;
white-space: nowrap;
}
&__min {
background: #E0E4CC;
display: block;
// padding: 5px;
}
&__fluid {
background: #A7DBD8;
display: block;
// padding: 5px;
}
&__max {
background: #69D2E7;
display: block;
// padding: 5px;
width: 1000em;
}
&__width {
font-size: 14px;
}
}
.custom-fonts {
width: 100%;
height: 200px;
}
* {
box-sizing: border-box;
}
body {
padding: $bodyPadding;
}
.hide {
display: none;
}
.match {
color: red;
}
.overlap-range {
position: absolute;
background: red;
height: 3px;
min-width: 10px;
}

Viewport Sized Typography Visualizer

A way to visually represent a hierarchy of multiple font sizes using the viewport sizes so you can see how they relate to each other.

A Pen by Aaron Barker on CodePen.

License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment