This website runs on Jekyll and it’s deployed to Amazon S3. This is a guide on how I run development and staging environments, gzip and upload only modified files.
Production and Development configs
On production, I combine all my JS files into one and then minify it with Google Closure Compiler.
On development, I include non-minified JS files one-by-one using plain-old script elements. I also include extra debug scrips.
I use different Jekyll configs for production and development: _config.yml for production deployment dev_config.yml for local development
dev_config.yml is generated by merging _dev_config.yml (starting with underscore) into _config.yml by:
./tolya_deployer dev_config
Jekyll and Modified Time
After building Jekyll, I run a script to preserve modified time of unchanged files.
Let’s say a publish a new post. This results in the following files being modified:
/jekyll-on-amazon-s3/index.html
/index.html
However, Jekyll regenerated the entire website including copying non-modified files. In our example, source/main.css hasn’t changed, but Jekyll copied it and set modified time to the last build time.
I wrote a script that reverts modified time of Jekyll-copied files back to the one set on the source file. The script looks at public/main.css and checks if it matches source/main.css. When it does, it transfers modified time from source file to the generated file.
./tolya_deployer jekyll
Git
The build directory, public/, is a separate Git repository. After each deploy I make a commit.
What’s the point of it?
I parse git status --porcelain=v1 output to see what files were added, deleted, or modified. Then I gzip only added and modified files, then copy them to staging/. I don’t gzip unchanged or removed files; I copy them to staging/ unchanged.
I gzip files locally. I use Zopfli with high iteration count for minimal file size. This takes time. The early version of my deployment script gzipped all files before each deploy — it was painfully slow.
Git workflow has several added benefits: git status and git diff to see what about to get deployed. git log to see deployment history.
--cf-invalidate create Amazon CloudFront request specifically with changed paths.
--no-mime-magic don’t look at the content of the file to guess MIME-type. Deciding MIME-type based on file extension is more predictable.
--acl-public make files accessible via HTTP.
s3cmd sync doesn’t know which files are gzipped and which aren’t. When gzipping, I store each path in an array. For each of these files, I set Content-Encoding: gzip header:
I tried using the official Amazon S3 & CloudFront CLI but it didn’t seem to provide a convenient way to invalidate only CloudFront files that I changed on S3.
Here’s the source code of my deployment script. Feel free to ask questions and share your Jekyll or Amazon S3 tips!
var Temperature = Backbone.Model.extend({
defaults: {
celsius: 0
},
fahrenheit: function(value) {
if (typeof value == 'undefined') {
return c2f(this.get('celsius'))
}
this.set('celsius', f2c(value))
}
})
var TemperatureView = Backbone.View.extend({
el: document.getElementById('tc-backbone'),
model: new Temperature(),
events: {
'input .celsius': 'updateCelsius',
'input .fahrenheit': 'updateFahrenheit'
},
initialize: function() {
this.listenTo(this.model, 'change:celsius', this.render)
this.render()
},
render: function() {
this.$('.celsius').val(this.model.get('celsius'))this.$('.fahrenheit').val(this.model.fahrenheit())
},
updateCelsius: function(event) {
this.model.set('celsius', event.target.value)
},
updateFahrenheit: function(event) {
this.model.fahrenheit(event.target.value)
}
})
var temperatureView = new TemperatureView()
Temperature is our model. Note that it only stores °C values, it doesn’t store °F.
We can always convert one to another so there is no need to store both.
View→Model→View Blowback
Changing the value in the text field moves the cursor to the end. The problem is that data flows
from an input field to a model, and then back to the same input field, overriding the current value
even if it’s exactly the same.
React.js doesn’t have Backbone’s problem with moving the cursor position. Its virtual DOM,
a layer between the actual DOM and React’s state, prevents React from unnecessary DOM changes.
setState schedules re-rendering on next requestAnimationFrame.
render method updates the virtual DOM, calculates the difference between
the current and the previous virtual DOM objects, and applies the changes to the actual DOM.
However, here is another bug (Backbone has it too):
Double Conversion
Instead of 2 we get 1.9999999999999964, because:
c2f(f2c(2)) === 1.9999999999999964
The problem is in the double conversion: Fahrenheits to Celsius, and then back to Fahrenheits.
In many programming languages, including JavaScript, all arithmetic operations are performed in floating point,
and floating point operations aren't necessarily precise.
Backbone doesn’t support two-way data binding out of the box. It’s the only library here that overwrites the currently edited input field with the same value.
React, Angular and Meteor all support two-way data binding. Although, in my example React needed a little extra logic to handle conversion errors.
Fortunately, none of the mentioned libraries go into an infinite loop updating values back and forth between model and view.
On retina screens CSS pixels don’t match device (physical) pixels.
A device pixel ratio of 2 means one CSS pixel equals two device pixels.
Here are few ways to draw a one-device-pixel border.
An external image. It’s only 51 bytes and it can be inlined using Data URI. You’d need to fire up Photoshop (or whatever you use) to change the border color, which isn’t very convenient.
Multiple background images
background:
linear-gradient(180deg, black, black 50%, transparent 50%) top left / 100% 1px no-repeat,
linear-gradient(90deg, black, black 50%, transparent 50%) top right / 1px 100% no-repeat,
linear-gradient(0, black, black 50%, transparent 50%) bottom right / 100% 1px no-repeat,
linear-gradient(-90deg, black, black 50%, transparent 50%) bottom left / 1px 100% no-repeat;
I performed 3 experiments to address some of the problems of Flying Focus.
Focus Snail
Flying Focus received good critique on Hacker News.
This demo seems to move faster than eye tracking speed, and the animation appears to skip or flicker, and it go [sic] too fast to follow, especially if the widgets are far apart.
True. I tried to increase animation duration but it seemed dull. Focus Snail doesn’t have this problem since it leaves a “trail”, which becomes longer for controls that are spread out, thus easier to notice.
Focus Zoom
Good idea, but the particular visualization (moving shadow) is a bit annoying.
I would go with something less in-your-face. Maybe a light shadow that blinks couple of
times on focus? I don't think the directional aspect of this is very important. Users
just need to know where the focus is now, not where it came from.
I agree, but I don’t like the blinking shadow idea. I experimented with Focus Zoom, which magnifies the focused element then gradually returns it to its original size.
This prototype only changes the size of the focus box, keeping the size of the focused element intact.
It’s not as aesthetically appealing as Focus Zoom, but it works well with inline elements.
I hope my experiments will serve as inspiration to implement similar effects on an OS level.
I implemented the prototypes using web technologies such as CSS, JS,
and SVG (for Focus Snail) only because those are tools I’m familiar with.
There is no real reason to just limit them to the web.
(function() {
var dummies = document.querySelectorAll('#keyboard-focus-demo .dummy');
var currentEffect = 'focusSnail';
var prevFocused = null;
window.addEventListener('load', function() {
selectEffect('focusSnail');
}, false);
window['playFocusDemo'] = function(playButton) {
playButton.disabled = true;
focusSnail.enabled = false;
focusZoom.enabled = false;
focusHug.enabled = false;
function set(name) {
setUI(name);
currentEffect = name;
}
var list = [].slice.call(dummies, 0);
function nextInQueue(next) {
var inputs = list.slice(0, 3).concat(list[rand(3, 5)], list.slice(6));
playQueue(inputs, next);
}
prevFocused = playButton;
sequence([
function(next) {
set('focusSnail');
doFocus(id('focus-snail_toggler'));
next();
},
500,
nextInQueue,
500,
function(next) {
set('focusZoom');
doFocus(id('focus-zoom_toggler'));
next();
},
500,
nextInQueue,
500,
function(next) {
set('focusHug');
doFocus(id('focus-hug_toggler'));
next();
},
500,
nextInQueue,
500,
function() {
playButton.disabled = false;
selectEffect('focusSnail');
doFocus(id('focus-snail_toggler'));
}
]);
};
window['selectEffectFromUI'] = function selectEffectFromUI(name) {
window[name].enabled = true;
var names = ['focusSnail', 'focusZoom', 'focusHug'];
for (var i = 0; i < names.length; i++) {
if (names[i] !== name) {
console.info(names[i]);
window[names[i]].enabled = false;
}
}
};
function selectEffect(name) {
selectEffectFromUI(name);
setUI(name);
}
function setUI(name) {
window[name].enabled = true;
var radios = document.querySelectorAll('[name="focus_radio"]');
for (var i = 0; i < radios.length; i++) {
var radio = radios[i];
if (radio.value === name) {
radio.checked = true;
break;
}
}
}
function doFocus(current) {
current.focus();
var fn = window[currentEffect].trigger;
if (fn.length === 2) {
fn(prevFocused, current);
} else {
fn(current);
}
prevFocused = current;
}
function playQueue(inputs, onFinish) {
inputs = [].slice.call(inputs, 3);
shuffle(inputs);
var length = inputs.length;
function next(i) {
if (i >= length) {
onFinish();
return;
}
var current = inputs[i];
doFocus(current);
setTimeout(function() {
next(i + 1);
}, 400);
}
next(0);
}
function sequence(list) {
var length = list.length;
var i = 0;
function next() {
if (i >= length) {
return;
}
var current = list[i];
i++;
if (typeof current == 'function') {
current(next);
} else if (typeof current == 'number') {
setTimeout(next, current);
}
}
next();
}
function shuffle(array) {
var m = array.length, t, i;
while (m) {
i = Math.floor(Math.random() * m--);
t = array[m];
array[m] = array[i];
array[i] = t;
}
}
function rand(low, high) {
return low + Math.round((high - low) * Math.random());
}
function id(name) {
return document.getElementById(name);
}
})();
Keyboard navigation has a major downside: it’s not clear where the focus moves upon pressing the Tab key.
Animation makes the transition more apparent.
How?
Flying Focus creates an element that is moving when the focus event happens.
outline: 5px auto -webkit-focus-ring-color makes the element look like it has a focus. It only works in Safari and Chrome.
Firefox doesn’t support outline-style: auto so box-shadow is used instead:
Previously, I actually focused on the moving element. It affected focus and blur events on web pages, making some dropdown menus unusable. Faking the focus solves the problem.
Flying Focus is available as a standalone JS file that you can include on your website.
Chrome and Safari extensions enable Flying Focus on all websites.
It’s all free and open-source.
Open /etc/hosts in Sublime or your favourite text editor and add:
127.0.0.1 n12v.dev
fe80::1%lo0 n12v.dev
fe80::1%lo0 is an IPv6 address. Without it your local site might run too slow.
Check if it works correctly:
ping n12v.dev
PING n12v.dev (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.040 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.071 ms
Press ctrl C to stop it. If you see ping: cannot resolve n12v.dev: Unknown host then run
sudo dscacheutil -flushcache
Apache
Mac OS X Mountain Lion ships with Apache 2.2.22. You don’t need to install it.
Open /etc/apache2/extra/httpd-vhosts.conf and add the following code to the bottom
function setTransition(element, value) {
element.style.webkitTransition = element.style.mozTransition = element.style.transition = value;
}
function fromAuto_run() {
var element = document.getElementById('from-auto')
element.style.width = getComputedStyle(element).width
element.offsetWidth; // force repaint
setTransition(element, 'width .5s ease-in-out')
element.style.width = '200px'
setTimeout(function() {
setTransition(element, '')
}, 500);
}
function fromAuto_reset() {
var element = document.getElementById('from-auto');
element.style.width = '';
}
function toAuto_run() {
var element = document.getElementById('to-auto');
var prevWidth = element.style.width;
element.style.width = 'auto';
var endWidth = getComputedStyle(element).width;
element.style.width = prevWidth;
element.offsetWidth; // force repaint
setTransition(element, 'width .5s ease-in-out');
element.style.width = endWidth;
element.addEventListener('transitionend', function transitionEnd(event) {
if (event.propertyName == 'width') {
setTransition(element, '');
element.style.width = 'auto';
element.removeEventListener('transitionend', transitionEnd, false)
}
}, false)
}
function toAuto_reset() {
var element = document.getElementById('to-auto');
element.style.width = '200px';
}
To auto
width: 200px
width: auto
var prevWidth = element.style.width
element.style.width = 'auto'
var endWidth = getComputedStyle(element).width
element.style.width = prevWidth
element.offsetWidth // force repaint
element.style.transition = 'width .5s ease-in-out'
element.style.width = endWidth
element.addEventListener('transitionend', function transitionEnd(event) {
if (event.propertyName == 'width') {
element.style.transition = ''
element.style.width = 'auto'
element.removeEventListener('transitionend', transitionEnd, false)
}
}, false)
That’s a lot of code for a simple transition and haven’t even covered vendor prefixes to make it work in Firefox, WebKit, IE and Opera. To automate this I use jquery.transit.
jquery.transit
jquery.transit is a jQuery plugin that provides neat JS API for creating animations using CSS transitions.
I forked jquery.transit to make it work with transitions from/to auto values.
In the example above I set max-width to 4000px. Who has such a wide screen, right?
Here goes the problem:
The width of the orange box in your browser is currently Xpx.
Transition from 160px to 4000px takes 1 second.
Transition from 160px to Xpx takes (X-160)/4000 second; animation is 4000/(X-160)× faster than expected.
Transition from Xpx to 160px is delayed for 1-((X-160)/4000) second; that’s the time to go from 4000px to Xpx which has no visible effect whatsoever.
Resize you browser window, see how values change. Narrower the window, more screwed up the animation.
(function() {
var MIN = 160;
var MAX = 4000;
window.maxWidth_toFixed = function() {
document.getElementById('to-from-max-width').style.maxWidth = MIN + 'px';
};
window.maxWidth_toAuto = function() {
document.getElementById('to-from-max-width').style.maxWidth = MAX + 'px';
};
function round(x) {
return Math.round(x * 100) / 100;
}
function updateWidth() {
var el = document.getElementById('to-from-max-width_wrap');
var width = el.offsetWidth;
var w = document.querySelectorAll('.article_css-transition-to-from-auto_width');
for (var i = 0; i < w.length; i++) {
var item = w[i];
if (!item.dataset.formula) {
item.dataset.formula = item.textContent;
}
var expression = item.dataset.formula.replace(/X/g, width);
var value = eval(expression);
item.textContent = round(value);
}
}
updateWidth();
window.addEventListener('resize', function() {
updateWidth();
}, false);
})();