├── LICENSE ├── README.md ├── css ├── demo.css └── fokus48.png ├── favicon.ico ├── index.html └── js ├── fokus.js └── fokus.min.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012-2013 Hakim El Hattab, http://hakim.se 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fokus 2 | 3 | Fokus uses JavaScript to emphasize anything you select by covering the rest of the page with semi-transparent black. 4 | 5 | The library has no dependencies and weighs in at around 3kb. A <canvas> element is used to paint the cut-out cover. Works in most modern browsers except IE and touch devices. 6 | 7 | If you want to use this on your site simply include the [fokus.min.js](https://github.com/hakimel/Fokus/blob/master/js/fokus.min.js) script. 8 | 9 | [Check out the demo page](http://lab.hakim.se/fokus/). 10 | 11 | [Get the Fokus Chrome extension](https://chrome.google.com/webstore/detail/flkkpmjbbpijiedjdgnhkcgopgnflehe). 12 | [Get the Fokus Firefox extension](https://addons.mozilla.org/en-US/firefox/addon/fokus/) by @aaronraimist. 13 | 14 | ## History 15 | 16 | #### 0.5 17 | - Faster selection animation 18 | - Handle offset document element (``````) 19 | - Update selection immediately on scroll 20 | 21 | #### 0.4 22 | - Don't start selection on rightclick 23 | 24 | #### 0.3 25 | - Don't select elements of zero width and height 26 | - Don't select br nodes 27 | 28 | #### 0.2 29 | - Animate change in cleared area 30 | - Handle selection via keyboard 31 | 32 | #### 0.1 33 | - Initial release 34 | 35 | ## License 36 | 37 | MIT licensed 38 | 39 | Copyright (C) 2012-2013 Hakim El Hattab, http://hakim.se 40 | -------------------------------------------------------------------------------- /css/demo.css: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Styles for the Fokus demo page. 4 | * 5 | * @author Hakim El Hattab | http://hakim.se 6 | */ 7 | 8 | * { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | ::selection { 14 | background: #d1edf8; 15 | } 16 | 17 | html, 18 | body { 19 | height: 100%; 20 | } 21 | 22 | body { 23 | background: #eee; 24 | font-family: 'Lato', Helvetica, sans-serif; 25 | font-size: 16px; 26 | color: #222; 27 | } 28 | 29 | a { 30 | color: #c2575b; 31 | text-decoration: none; 32 | 33 | -webkit-transition: 0.15s color ease; 34 | -moz-transition: 0.15s color ease; 35 | -ms-transition: 0.15s color ease; 36 | -o-transition: 0.15s color ease; 37 | transition: 0.15s color ease; 38 | } 39 | a:hover { 40 | color: #f76f76; 41 | } 42 | 43 | h1, 44 | h2 { 45 | font-size: 24px; 46 | } 47 | 48 | .intro { 49 | display: inline-block; 50 | background: #eee; 51 | padding: 40px 60px; 52 | overflow-y: auto; 53 | 54 | -webkit-box-sizing: border-box; 55 | -moz-box-sizing: border-box; 56 | box-sizing: border-box; 57 | } 58 | .intro h1 { 59 | position: relative; 60 | } 61 | .intro h1:after { 62 | content: ''; 63 | position: absolute; 64 | display: inline-block; 65 | width: 48px; 66 | height: 48px; 67 | top: -9px; 68 | margin-left: 7px; 69 | 70 | background-image: url( 'fokus48.png' ); 71 | background-repeat: no-repeat; 72 | } 73 | .intro>article { 74 | width: 400px; 75 | } 76 | .intro p { 77 | margin: 10px 0 10px 0; 78 | font-size: 16px; 79 | line-height: 1.5em; 80 | } 81 | .intro small { 82 | display: block; 83 | margin-top: 10px; 84 | padding-top: 10px; 85 | color: #333; 86 | font-size: 0.85em; 87 | border-top: 1px dashed #ccc; 88 | 89 | -webkit-text-size-adjust: none; 90 | } 91 | .intro .sharing { 92 | margin-top: 20px; 93 | } 94 | 95 | .demo { 96 | border-top: 1px solid #ccc; 97 | padding: 40px 60px; 98 | background: #fff; 99 | } 100 | .demo>article { 101 | display: inline-block; 102 | max-width: 320px; 103 | vertical-align: top; 104 | margin: 0 20px 20px 0; 105 | line-height: 1.5em; 106 | } 107 | .demo>article.double { 108 | max-width: 640px; 109 | } 110 | .demo>article img { 111 | max-width: 100%; 112 | margin-bottom: 10px; 113 | } 114 | .demo>article h2 { 115 | margin-bottom: 10px; 116 | } 117 | .demo>article .image-link { 118 | display: block; 119 | } 120 | 121 | -------------------------------------------------------------------------------- /css/fokus48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/Fokus/4c50f4dfc860ecabc8d9833a8acb7746a3458cd1/css/fokus48.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakimel/Fokus/4c50f4dfc860ecabc8d9833a8acb7746a3458cd1/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fokus - Emphasized text-highlighting using JavaScript 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |

Fokus

25 |

26 | Fokus uses JavaScript to emphasize anything you select by covering the rest of the page with semi-transparent black. 27 |

28 |

29 | Try it out by selecting this paragraph or the sample content below. You'll see the entire page fade out while this text is highlighted. 30 |

31 |

32 | If you want to use Fokus on your site you just need to include the fokus.min.js script (3kb, no dependencies). 33 |

34 |

35 | Fokus is also available as a Chrome extension. 36 |

37 | 38 | Created by @hakimel / http://hakim.se 39 | 40 |
41 | 42 |
43 | 44 | 45 | 46 |
47 | 48 | Fork me on GitHub 49 |
50 | 51 | 52 | 53 | 54 |
55 |
56 |

Meny

57 | 58 | Meny 59 | 60 |

61 | An experimental CSS 3D fold-in menu. Works in any browser that supports CSS 3D transforms, that includes Mobile Safari so get your iPhone ready! 62 |

63 |
64 |
65 |

CSS Scroll Effects

66 | 67 | CSS3 Scroll Effects 68 | 69 |

70 | Decided it was time for some CSS tinkering again and ended up creating this set of CSS3 scrolling styles. Not intended for any practical use but the visuals are surprisingly impactful. 71 |

72 |

73 | This works by applying a 74 | future/past 75 | class to list items outside of the viewport as you scroll. Based on this class a variety of transforms are transitioned to via CSS. 76 |

77 |

78 | Thanks 79 | Paul Irish 80 | for improving the JavaScript performance! 81 |

82 |
83 |
84 |

Linjer

85 | 86 | Meny 87 | 88 |

89 | An interactive experiment based on the visuals originally created for 90 | Radar. 91 |

92 |
93 |
94 |

Avgrund

95 | 96 | Meny 97 | 98 |

99 | A conceptual modal which gives a sense of depth between the page and modal layers. 100 |

101 |
102 |
103 |

Radar

104 | 105 | Radar 106 | 107 |

108 | An audio-visual experiment that uses 109 | Audiolet 110 | to synthesize sound in real-time. The visuals are rendered on 111 | <canvas>. 112 |

113 |
114 |
115 |

Sinuous for iPhone & iPad

116 | 117 | Sinuous 118 | 119 |
120 |

121 | Almost two years after releasing the 122 | web version 123 | Sinuous has finally made its way to the App Store! The gameplay is very much alike the web but it ties into Game Center so that you can compete with friends. Oh, and keep and eye our for the new 'vortex'-boost! 124 |

125 |

126 | The game is still powered by JavaScript and rendered on HTML canvas. It relies on native code for audio and Game Center integration but that bit was easy thanks to the ever so lovely 127 | PhoneGap. I'm very happy with how well the controls translated to touch and I hope you will be too. 128 |

129 |

130 | This runs as smooth as butter on the iPad but the framerate is a bit shaky on the iPhone 4. I will be releasing an update soon to address that as well as some other improvements. 131 | Try it out! 132 |

133 |
134 |
135 |
136 |

forkit.js

137 | 138 | forkit.js 139 | 140 |

141 | An experimental animated ribbon which lets you drag down a curtain of additional content. A fun twist on the "Fork me on GitHub" banner! Created with JavaScript-controlled CSS3 transforms. 142 |

143 |
144 |
145 |

rvl.io

146 | 147 | rvl.io 148 | 149 |

150 | As 151 | reveal.js 152 | has grown more popular there has been a lot of requests for an online editor and 153 | rvl.io 154 | is the answer. rvl.io is a service for authoring, storing and presenting slide decks without having to touch the underlying HTML of reveal.js. It was created by myself (frontend) and 155 | @h__r__j 156 | (backend). 157 |

158 |
159 |
160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /js/fokus.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Fokus 0.5 3 | * http://lab.hakim.se/fokus 4 | * MIT licensed 5 | * 6 | * Copyright (C) 2012 Hakim El Hattab, http://hakim.se 7 | */ 8 | 9 | (function(){ 10 | 11 | // Padding around the selection 12 | var PADDING = 5; 13 | 14 | // Opacity of the overlay 15 | var OPACITY = 0.75; 16 | 17 | // Key modifier that needs to be held down for overlay to appear 18 | var MODIFIER = null; 19 | 20 | // The opaque overlay canvas 21 | var overlay, 22 | overlayContext, 23 | overlayAlpha = 0, 24 | 25 | // Reference to the redraw animation so it can be cancelled 26 | redrawAnimation, 27 | 28 | // Currently selected region 29 | selectedRegion = { left: 0, top: 0, right: 0, bottom: 0 }, 30 | 31 | // Currently cleared region 32 | clearedRegion = { left: 0, top: 0, right: 0, bottom: 0 }, 33 | 34 | // Currently pressed down key modifiers 35 | keyModifiers = { ctrl: false, shift: false, alt: false, cmd: false }; 36 | 37 | // choo choo! 38 | function initialize() { 39 | 40 | // Only initialize if the client is capable 41 | if( capable() && !window.__fokused ) { 42 | 43 | // Ensures that Fokus isn't initialized twice on the same page 44 | window.__fokused = true; 45 | 46 | overlay = document.createElement( 'canvas' ); 47 | overlayContext = overlay.getContext( '2d' ); 48 | 49 | // Place the canvas on top of everything 50 | overlay.style.position = 'fixed'; 51 | overlay.style.left = 0; 52 | overlay.style.top = 0; 53 | overlay.style.zIndex = 2147483647; 54 | overlay.style.pointerEvents = 'none'; 55 | overlay.style.background = 'transparent'; 56 | 57 | document.addEventListener( 'mousedown', onMouseDown, false ); 58 | document.addEventListener( 'keyup', onKeyPress, false ); 59 | document.addEventListener( 'keydown', onKeyPress, false ); 60 | document.addEventListener( 'scroll', onScroll, false ); 61 | document.addEventListener( 'DOMMouseScroll', onScroll, false ); 62 | window.addEventListener( 'resize', onWindowResize, false ); 63 | 64 | // Trigger an initial resize 65 | onWindowResize(); 66 | 67 | } 68 | 69 | } 70 | 71 | /** 72 | * Is this browser capable of running Fokus? 73 | */ 74 | function capable() { 75 | 76 | return !!( 77 | 'addEventListener' in document && 78 | 'pointerEvents' in document.body.style 79 | ); 80 | 81 | } 82 | 83 | /** 84 | * Redraws an animates the overlay. 85 | */ 86 | function redraw() { 87 | 88 | // Cache the response of this for re-use below 89 | var _hasSelection = hasSelection(); 90 | 91 | // Reset to a solid (less opacity) overlay fill 92 | overlayContext.clearRect( 0, 0, overlay.width, overlay.height ); 93 | overlayContext.fillStyle = 'rgba( 0, 0, 0, '+ overlayAlpha +' )'; 94 | overlayContext.fillRect( 0, 0, overlay.width, overlay.height ); 95 | 96 | if( _hasSelection ) { 97 | if( overlayAlpha < 0.1 ) { 98 | // Clear the selection instantly if we're just fading in 99 | clearedRegion = selectedRegion; 100 | } 101 | else { 102 | // Ease the cleared region towards the selected selection 103 | clearedRegion.left += ( selectedRegion.left - clearedRegion.left ) * 0.18; 104 | clearedRegion.top += ( selectedRegion.top - clearedRegion.top ) * 0.18; 105 | clearedRegion.right += ( selectedRegion.right - clearedRegion.right ) * 0.18; 106 | clearedRegion.bottom += ( selectedRegion.bottom - clearedRegion.bottom ) * 0.18; 107 | } 108 | } 109 | 110 | // Cut out the cleared region 111 | overlayContext.clearRect( 112 | clearedRegion.left - window.scrollX - PADDING, 113 | clearedRegion.top - window.scrollY - PADDING, 114 | ( clearedRegion.right - clearedRegion.left ) + ( PADDING * 2 ), 115 | ( clearedRegion.bottom - clearedRegion.top ) + ( PADDING * 2 ) 116 | ); 117 | 118 | // Fade in if there's a valid selection... 119 | if( _hasSelection ) { 120 | overlayAlpha += ( OPACITY - overlayAlpha ) * 0.08; 121 | } 122 | // ... otherwise fade out 123 | else { 124 | overlayAlpha = Math.max( ( overlayAlpha * 0.85 ) - 0.02, 0 ); 125 | } 126 | 127 | // Ensure there is no overlap 128 | cancelAnimationFrame( redrawAnimation ); 129 | 130 | // Continue so long as there is content selected or we are fading out 131 | if( _hasSelection || overlayAlpha > 0 ) { 132 | // Append the overlay if it isn't already in the DOM 133 | if( !overlay.parentNode ) document.body.appendChild( overlay ); 134 | 135 | // Stage a new animation frame 136 | redrawAnimation = requestAnimationFrame( redraw ); 137 | } 138 | else { 139 | document.body.removeChild( overlay ); 140 | } 141 | 142 | } 143 | 144 | /** 145 | * Steps through all selected nodes and updates the selected 146 | * region (bounds of selection). 147 | * 148 | * @param {Boolean} immediate flags if selection should happen 149 | * immediately, defaults to false which means the selection 150 | * rect animates into place 151 | */ 152 | function updateSelection( immediate ) { 153 | 154 | // Default to negative space 155 | var currentRegion = { left: Number.MAX_VALUE, top: Number.MAX_VALUE, right: 0, bottom: 0 }; 156 | 157 | var nodes = getSelectedNodes(); 158 | 159 | for( var i = 0, len = nodes.length; i < len; i++ ) { 160 | var node = nodes[i]; 161 | 162 | // Select parents of text nodes that have contents 163 | if( node.nodeName === '#text' && node.nodeValue.trim() ) { 164 | node = node.parentNode; 165 | } 166 | 167 | // Fetch the screen coordinates for this element 168 | var position = getScreenPosition( node ); 169 | 170 | var x = position.x, 171 | y = position.y, 172 | w = node.offsetWidth, 173 | h = node.offsetHeight; 174 | 175 | // 1. offsetLeft works 176 | // 2. offsetWidth works 177 | // 3. Element is larger than zero pixels 178 | // 4. Element is not
179 | if( node && typeof x === 'number' && typeof w === 'number' && ( w > 0 || h > 0 ) && !node.nodeName.match( /^br$/gi ) ) { 180 | currentRegion.left = Math.min( currentRegion.left, x ); 181 | currentRegion.top = Math.min( currentRegion.top, y ); 182 | currentRegion.right = Math.max( currentRegion.right, x + w ); 183 | currentRegion.bottom = Math.max( currentRegion.bottom, y + h ); 184 | } 185 | } 186 | 187 | // Don't update selection if a modifier is specified but not 188 | // pressed down, unless there's already a selected region 189 | if( !MODIFIER || MODIFIER === 'none' || keyModifiers[ MODIFIER ] || hasSelection() ) { 190 | selectedRegion = currentRegion; 191 | } 192 | 193 | // If flagged, update the cleared region immediately 194 | if( immediate ) { 195 | clearedRegion = selectedRegion; 196 | } 197 | 198 | // Start repainting if there is a selected region 199 | if( hasSelection() ) { 200 | redraw(); 201 | } 202 | 203 | } 204 | 205 | /** 206 | * Checks if a region is currently selected. 207 | */ 208 | function hasSelection() { 209 | 210 | return selectedRegion.left < selectedRegion.right && selectedRegion.top < selectedRegion.bottom; 211 | 212 | } 213 | 214 | function onMouseDown( event ) { 215 | 216 | // Don't start selection on right click 217 | if( event.which !== 3 ) { 218 | document.addEventListener( 'mousemove', onMouseMove, false ); 219 | document.addEventListener( 'mouseup', onMouseUp, false ); 220 | 221 | updateSelection(); 222 | } 223 | 224 | } 225 | 226 | function onMouseMove( event ) { 227 | 228 | updateSelection(); 229 | 230 | } 231 | 232 | function onMouseUp( event ) { 233 | 234 | document.removeEventListener( 'mousemove', onMouseMove, false ); 235 | document.removeEventListener( 'mouseup', onMouseUp, false ); 236 | 237 | setTimeout( updateSelection, 1 ); 238 | 239 | } 240 | 241 | function onKeyPress( event ) { 242 | keyModifiers.alt = event.altKey || event.altGraphKey; 243 | keyModifiers.ctrl = event.ctrlKey; 244 | keyModifiers.shift = event.shiftKey; 245 | keyModifiers.meta = event.metaKey; 246 | 247 | updateSelection(); 248 | 249 | } 250 | 251 | function onScroll( event ) { 252 | 253 | updateSelection( true ); 254 | 255 | } 256 | 257 | /** 258 | * Make sure the overlay canvas is always as wide and tall as 259 | * the current window. 260 | */ 261 | function onWindowResize( event ) { 262 | 263 | overlay.width = window.innerWidth; 264 | overlay.height = window.innerHeight; 265 | 266 | } 267 | 268 | /** 269 | * Helper methods for getting selected nodes, source: 270 | * http://stackoverflow.com/questions/7781963/js-get-array-of-all-selected-nodes-in-contenteditable-div 271 | */ 272 | function getSelectedNodes() { 273 | 274 | if (window.getSelection) { 275 | var sel = window.getSelection(); 276 | if (!sel.isCollapsed) { 277 | return getRangeSelectedNodes(sel.getRangeAt(0)); 278 | } 279 | } 280 | return []; 281 | 282 | } 283 | function getRangeSelectedNodes( range ) { 284 | 285 | var node = range.startContainer; 286 | var endNode = range.endContainer; 287 | 288 | // Special case for a range that is contained within a single node 289 | if (node == endNode) { 290 | if( node.nodeName === '#text' ) { 291 | return [node.parentNode]; 292 | } 293 | return [node]; 294 | } 295 | 296 | // Iterate nodes until we hit the end container 297 | var rangeNodes = []; 298 | while (node && node != endNode) { 299 | rangeNodes.push( node = nextNode(node) ); 300 | } 301 | 302 | // Add partially selected nodes at the start of the range 303 | node = range.startContainer; 304 | while (node && node != range.commonAncestorContainer) { 305 | rangeNodes.unshift(node); 306 | node = node.parentNode; 307 | } 308 | 309 | return rangeNodes; 310 | 311 | } 312 | function nextNode(node) { 313 | 314 | if (node.hasChildNodes()) { 315 | return node.firstChild; 316 | } else { 317 | while (node && !node.nextSibling) { 318 | node = node.parentNode; 319 | } 320 | if (!node) { 321 | return null; 322 | } 323 | return node.nextSibling; 324 | } 325 | 326 | } 327 | 328 | /** 329 | * Gets the x/y screen position of the target node, source: 330 | * http://www.quirksmode.org/js/findpos.html 331 | */ 332 | function getScreenPosition( node ) { 333 | var x = document.documentElement.offsetLeft, 334 | y = document.documentElement.offsetTop; 335 | 336 | if ( node.offsetParent ) { 337 | do { 338 | x += node.offsetLeft; 339 | y += node.offsetTop; 340 | } while ( node = node.offsetParent ); 341 | } 342 | 343 | return { x: x, y: y }; 344 | } 345 | 346 | /** 347 | * rAF polyfill. 348 | */ 349 | (function() { 350 | var lastTime = 0; 351 | var vendors = ['ms', 'moz', 'webkit', 'o']; 352 | for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 353 | window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; 354 | window.cancelAnimationFrame = 355 | window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; 356 | } 357 | 358 | if (!window.requestAnimationFrame) 359 | window.requestAnimationFrame = function(callback, element) { 360 | var currTime = new Date().getTime(); 361 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 362 | var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 363 | timeToCall); 364 | lastTime = currTime + timeToCall; 365 | return id; 366 | }; 367 | 368 | if (!window.cancelAnimationFrame) 369 | window.cancelAnimationFrame = function(id) { 370 | clearTimeout(id); 371 | }; 372 | }()); 373 | 374 | initialize(); 375 | 376 | })(); 377 | -------------------------------------------------------------------------------- /js/fokus.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Fokus 0.5 3 | * http://lab.hakim.se/fokus 4 | * MIT licensed 5 | * 6 | * Copyright (C) 2012 Hakim El Hattab, http://hakim.se 7 | */ 8 | (function(){function u(){var m=s();l.clearRect(0,0,a.width,a.height);l.fillStyle="rgba( 0, 0, 0, "+d+" )";l.fillRect(0,0,a.width,a.height);m&&(0.1>d?b=h:(b.left+=0.18*(h.left-b.left),b.top+=0.18*(h.top-b.top),b.right+=0.18*(h.right-b.right),b.bottom+=0.18*(h.bottom-b.bottom)));l.clearRect(b.left-window.scrollX-q,b.top-window.scrollY-q,b.right-b.left+2*q,b.bottom-b.top+2*q);d=m?d+0.08*(B-d):Math.max(0.85*d-0.02,0);cancelAnimationFrame(v);m||0