blob: 4ad10f3f95183b6f6a6d245db2627b5f70dd3990 [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<style>
body {
background-color: rgb(255, 255, 255);
}
.row {
position: absolute;
}
.tile {
display: inline-block;
width: 200px;
height: 200px;
margin: 30px;
background-color: rgb(255, 100, 100);
}
</style>
</head>
<body>
<div class="row">
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
<div class="tile"></div>
</div>
<script>
const TRANSLATE_VELOCITY_PX_S = 800;
const TILE_WIDTH_PX = 260;
// A class that keeps track of the current state of "transform: translateX()"
// animations for a given div, and sets up linear smooth scrolling while
// keys are being held via CSS transitions that are only updated at sparse
// discrete moments (e.g. when the user first presses a key, or if the
// key is still pressed when the animation is halfway completed).
function RowAnimationController(rowDiv) {
var self = this;
// The div that we are controlling.
self.rowDiv = rowDiv;
// A set of keycodes that are currently in the pressed state. This may
// be updated externally, but its state will only be re-inspected when
// onKeyStatusesChanged() is called.
self.keyStatuses = new Set([]);
// The origin of translateX() for the currently playing transition.
self.sourceTranslatePx = 0.0;
// The target of translateX() for the currently playing transition, which is
// the current position if the transition is complete.
self.targetTranslatePx = 0.0;
// The time that the current transition was set at.
self.setTargetTimeMs = 0;
self.onKeyDown = function(keyCode) {
if (!self.keyStatuses.has(keyCode)) {
self.keyStatuses.add(keyCode);
makeUpdateStateFromKeyStatusRequestAnimationFrame();
}
}
self.onKeyUp = function(keyCode) {
if (self.keyStatuses.has(keyCode)) {
self.keyStatuses.delete(keyCode);
makeUpdateStateFromKeyStatusRequestAnimationFrame();
}
}
function makeUpdateStateFromKeyStatusRequestAnimationFrame() {
// We require all animation processing and updating to happen within a
// requestAnimationFrame call so that we can access the exact times
// (provided as the requestAnimationFrame callback's parameter) that will
// be used by the renderer when interpolating the animations/transitions.
window.requestAnimationFrame(
(timeMs) => updateStateFromKeyStatus(timeMs));
}
function updateStateFromKeyStatus(timeMs) {
if (self.keyStatuses.has(37)) {
// Left arrow key is pressed.
processKeyDown(-1.0, timeMs);
}
if (self.keyStatuses.has(39)) {
// Right arrow key is pressed.
processKeyDown(1.0, timeMs);
}
}
function processKeyDown(dir, timeMs) {
// If the tile is not yet half-way towards its destination, just
// leave it be.
if (translateDiffPx(timeMs) * dir > TILE_WIDTH_PX / 2.0) {
// Nothing to do, tile is already happily scrolling away.
return;
} else {
// It's time to start or continue an animation.
startTransition(
self.targetTranslatePx + TILE_WIDTH_PX * dir, timeMs);
// Setup a timer that will go off when we have transitioned halfway
// to the next tile, to see if a key is still pressed. If so, we
// want to update the animation to animate to the next tile after that.
var timeMsToHalfTile =
(Math.abs(currentTranslatePx(timeMs) - self.targetTranslatePx) -
TILE_WIDTH_PX / 2.0) /
(TRANSLATE_VELOCITY_PX_S / 1000.0);
timeMsToHalfTile = Math.max(timeMsToHalfTile, 0);
window.setTimeout(makeUpdateStateFromKeyStatusRequestAnimationFrame,
timeMsToHalfTile);
}
}
function translateDiffPx(timeMs) {
return self.targetTranslatePx - currentTranslatePx(timeMs);
}
// Triggers the transition, by making an adjustment to the |rowDiv|'s
// style.
function startTransition(newTargetTranslatePx, timeMs) {
self.sourceTranslatePx = currentTranslatePx(timeMs);
self.targetTranslatePx = newTargetTranslatePx;
self.setTargetTimeMs = timeMs;
var transitionTimeMs =
Math.abs(self.targetTranslatePx - self.sourceTranslatePx) /
(TRANSLATE_VELOCITY_PX_S / 1000.0);
// Compute and apply the new CSS which will trigger (or continue) the
// scrolling animation.
self.rowDiv.style.transition =
transitionTimeMs + 'ms linear transform';
self.rowDiv.style.transform =
'translateX(' + self.targetTranslatePx + 'px)';
// It's expected that this console.log output will appear sparsely,
// indicating that this logic is not actually being executed each frame.
console.log('Setting transform to: ' + self.rowDiv.style.transform);
}
function currentTranslatePx(timeMs) {
var diffTimeS = (timeMs - self.setTargetTimeMs) / 1000.0;
// Are we going left (-1) or right (+1)?
var direction =
Math.sign(self.targetTranslatePx - self.sourceTranslatePx);
var unsignedDiffTranslate = diffTimeS * TRANSLATE_VELOCITY_PX_S;
// If we would otherwise have moved past our target, just return the
// target.
if (unsignedDiffTranslate >
Math.abs(self.targetTranslatePx - self.sourceTranslatePx)) {
return self.targetTranslatePx;
}
return unsignedDiffTranslate * direction + self.sourceTranslatePx;
}
}
function main() {
var row_animation_controller =
new RowAnimationController(document.getElementsByClassName('row')[0]);
document.addEventListener('keydown', function (e) {
row_animation_controller.onKeyDown(e.keyCode);
});
document.addEventListener('keyup', function (e) {
row_animation_controller.onKeyUp(e.keyCode);
});
}
window.addEventListener('load', () => { main(); });
</script>
</body>
</html>