| <!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> |
| |