├── img ├── 16.png ├── 48.png ├── 64.png ├── 128.png ├── apple.png ├── 440x280.png ├── 440x280.xcf ├── 1280x800.png └── GesturesImages.png ├── README.md ├── research ├── test.html ├── mini.html ├── responsive.html ├── viewer.html ├── minimal.html └── experiment.html ├── manifest.json ├── background.js ├── options.js ├── options.html └── swipe.js /img/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antimatter15/swipe-gesture/HEAD/img/16.png -------------------------------------------------------------------------------- /img/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antimatter15/swipe-gesture/HEAD/img/48.png -------------------------------------------------------------------------------- /img/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antimatter15/swipe-gesture/HEAD/img/64.png -------------------------------------------------------------------------------- /img/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antimatter15/swipe-gesture/HEAD/img/128.png -------------------------------------------------------------------------------- /img/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antimatter15/swipe-gesture/HEAD/img/apple.png -------------------------------------------------------------------------------- /img/440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antimatter15/swipe-gesture/HEAD/img/440x280.png -------------------------------------------------------------------------------- /img/440x280.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antimatter15/swipe-gesture/HEAD/img/440x280.xcf -------------------------------------------------------------------------------- /img/1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antimatter15/swipe-gesture/HEAD/img/1280x800.png -------------------------------------------------------------------------------- /img/GesturesImages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antimatter15/swipe-gesture/HEAD/img/GesturesImages.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Apple Safari for OS X Lion style Back/Forward Gestures for Chrome 2 | 3 | Basic principle: 4 | 5 | onmousewheel = function(e){ 6 | if(Math.abs(e.wheelDeltaX) > 100) //go foward/back 7 | } 8 | 9 | -------------------------------------------------------------------------------- /research/test.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Swipe Gesture", 3 | "version": "1.99", 4 | "manifest_version": 2, 5 | "description": "Add OSX-style multitouch gestures to Chromebooks", 6 | "icons": { 7 | "64": "img/64.png", 8 | "48": "img/48.png", 9 | "16": "img/16.png", 10 | "128": "img/128.png" 11 | }, 12 | "options_page": "options.html", 13 | "background": { 14 | "scripts": ["background.js"] 15 | }, 16 | "content_scripts": [ 17 | { 18 | "matches": [""], 19 | "js": ["swipe.js"] 20 | } 21 | ], 22 | "permissions": [ 23 | "storage", 24 | "tabs" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /research/mini.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /research/responsive.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /research/viewer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { 2 | if(request.action == "reload"){ 3 | chrome.tabs.reload(sender.tab.id); 4 | sendResponse('') 5 | }else if(request.action == "open-settings"){ 6 | chrome.tabs.create({url: "options.html"}) 7 | sendResponse('') 8 | }else if(request.action == "new-tab"){ 9 | chrome.tabs.create({}) 10 | sendResponse('') 11 | }else if(request.action == "next-tab" || request.action == 'previous-tab'){ 12 | var offset = (request.action == "next-tab") ? 1 : -1; 13 | chrome.windows.get(sender.tab.windowId, {populate: true}, function(e){ 14 | var tab = e.tabs[(sender.tab.index + e.tabs.length + offset) % e.tabs.length]; 15 | //wrap around! 16 | chrome.tabs.update(tab.id, { 17 | active: true 18 | }) 19 | }); 20 | sendResponse('') 21 | }else if(request.action == "back"){ 22 | sendResponse('history.go(-1)') 23 | }else if(request.action == "forward"){ 24 | sendResponse('history.go(1)') 25 | }else if(request.action == "close-tab"){ 26 | chrome.tabs.remove(sender.tab.id); 27 | sendResponse('') 28 | } 29 | console.log(sender) 30 | }); -------------------------------------------------------------------------------- /research/minimal.html: -------------------------------------------------------------------------------- 1 |

Start Scrolling

2 | 3 |
4 |
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | function $(sel){ 2 | var s = document.querySelectorAll(sel); 3 | return s.length == 1 ? s[0] : [].slice.call(s, 0) 4 | } 5 | 6 | $('#dev').addEventListener('change', function(){ 7 | $('#devmode').style.display = $('#dev').checked ? '' : 'none'; 8 | }) 9 | 10 | var options = [ "Disabled", "Forward", "Back", "Reload", "Next Tab", "Previous Tab", "Close Tab", "New Tab", 'Open Settings'] 11 | 12 | function setSelect(i){ 13 | var el = document.getElementById('a' + i); 14 | for(var j = 0; j < options.length; j++){ 15 | var slug = options[j].replace(/ /g, '-').toLowerCase(); 16 | el.options.add(new Option(options[j], slug)) 17 | } 18 | el.addEventListener('change', function(){ 19 | var obj = {}; 20 | obj['a' + i] = el.value; 21 | chrome.storage.local.set(obj, function(){ 22 | loadConfiguration(); 23 | notifyUpdates(); 24 | }) 25 | }) 26 | } 27 | 28 | for(var i = 0; i < 360; i += 45) setSelect(i); //configure each of the select things 29 | 30 | $('#reverse').addEventListener('change', function(){ 31 | var val = $('#reverse').checked; 32 | chrome.storage.local.set({INVERT_ARROW: val}) 33 | INVERT_ARROW = val; 34 | }) 35 | 36 | $('#thresh').addEventListener('change', function(){ 37 | var val = +$('#thresh').value; 38 | chrome.storage.local.set({LENGTH_THRESHOLD: val}); 39 | LENGTH_THRESHOLD = val; 40 | }) 41 | 42 | 43 | function updateConfiguration(){ 44 | $('#reverse').checked = INVERT_ARROW; 45 | $('#thresh').value = LENGTH_THRESHOLD; 46 | for(var i = 0; i < 360; i += 45){ 47 | document.getElementById("a" + i).value = ACTION_MAP[i] 48 | } 49 | 50 | } 51 | 52 | function notifyUpdates(){ 53 | chrome.windows.getAll({populate: true}, function(wins){ 54 | wins.forEach(function(win){ 55 | win.tabs.forEach(function(tab){ 56 | chrome.tabs.sendMessage(tab.id, {action: "update"}, function(e){ 57 | if(typeof e == 'undefined'){ 58 | console.log('err', chrome.extension.lastError 59 | ); 60 | // chrome.tabs.executeScript(tab.id, {file: "swipe.js"}); 61 | }else{ 62 | console.log("got response", tab.id, e) 63 | } 64 | }) 65 | }) 66 | }) 67 | }) 68 | } 69 | 70 | 71 | function showWord(word){ 72 | $('#modal').innerHTML = word; 73 | $('#modal').style.display = ''; 74 | setTimeout(function(){ 75 | $('#modal').style.opacity = '0.5'; 76 | }, 10) 77 | } 78 | 79 | $('#modal').addEventListener('webkitTransitionEnd', function(){ 80 | if(+$('#modal').style.opacity == 0){ 81 | $('#modal').style.display = 'none'; 82 | }else{ 83 | $('#modal').style.opacity = '0'; 84 | } 85 | }) 86 | 87 | function signalCompletion(){ 88 | var slugmap = {}; 89 | for(var j = 0; j < options.length; j++){ 90 | var slug = options[j].replace(/ /g, '-').toLowerCase(); 91 | slugmap[slug] = options[j] 92 | } 93 | showWord(slugmap[ACTION_MAP[direction]]) 94 | 95 | } -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 72 | Swipe Gesture 73 | 74 | 75 | 76 |
77 |

Swipe Gestures Settings

78 |
79 |
80 |
81 | 82 | Here's a picture gently stolen from Apple showing exactly what this extension enables. 83 |
84 |

85 | This page is a place where you can experiment with the extension and the settings which you can configure on this page. 86 |

87 | 88 |

89 | 90 |

91 |
92 | 93 | 94 |
95 |

96 |

97 | Simple Gestures 98 |
99 | 100 | 101 |
102 |
103 | 104 | 105 |
106 |
107 | 108 | 109 |
110 |
111 | 112 | 113 |
114 |
115 |
116 | Diagonal Gestures 117 |
118 | 119 | 120 |
121 |
122 | 123 | 124 |
125 |
126 | 127 | 128 |
129 |
130 | 131 | 132 |
133 |
134 |

135 |

136 | 137 |

138 | 143 | 144 |
145 |
146 |

147 | Made by @antimatter15. Follow me on Google+ or read my blog. Submit issues or contribute through Github. 148 |

149 |
150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /research/experiment.html: -------------------------------------------------------------------------------- 1 | 6 | 320 |





321 |
322 |
323 |

























324 |



















325 |
326 |

























327 |

























328 |

























-------------------------------------------------------------------------------- /swipe.js: -------------------------------------------------------------------------------- 1 | 2 | function signalCompletion(){ 3 | // console.log('asdfja9ofjawerjwoaer', ACTION_MAP[direction]) 4 | // document.body.style.backgroundColor = '#B9D3B9'; 5 | chrome.extension.sendMessage({ 6 | action: ACTION_MAP[direction] 7 | }, function(code){ 8 | if(code == ""){ 9 | var gc; 10 | while(gc = document.querySelector('.swipe-gestures-container-element')) 11 | gc.parentNode.removeChild(gc); 12 | }else{ 13 | eval(code); //arghhh super scary! 14 | } 15 | }); 16 | } 17 | function signalCancellation(){ 18 | // document.body.style.backgroundColor = '#D1A5A5'; 19 | } 20 | function signalScroll(){ 21 | console.log('you see me scrolling') //my front lawn 22 | //i know you're all thinking he's so white and nerdy 23 | // document.body.style.backgroundColor = '#D7DB7F'; 24 | } 25 | function signalEnd(){ 26 | console.log("DONE") 27 | // setTimeout(function(){ 28 | // document.body.style.backgroundColor = ''; 29 | // }, 500) 30 | } 31 | function transformLength(x){ //x is a positive real number 32 | // return Math.sqrt(x) 33 | // return 1 34 | return x 35 | } 36 | function updateConfiguration(){ 37 | console.log("config loaded") 38 | } 39 | 40 | chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { 41 | if(request.action == "update"){ 42 | loadConfiguration(); 43 | sendResponse("done") 44 | } 45 | }) 46 | 47 | var ACTION_MAP = { 48 | "0": "forward", 49 | "45": "disabled", 50 | "90": "disabled", 51 | "135": "disabled", 52 | "180": "back", 53 | "225": "previous-tab", 54 | "270": "open-settings", 55 | "315": "next-tab" 56 | } 57 | 58 | 59 | // var orientations = [180, 0, 315, 225, 90, 270, 135, 45]; 60 | var LENGTH_THRESHOLD = 500; 61 | var INVERT_ARROW = false; 62 | 63 | 64 | function loadConfiguration(){ 65 | chrome.storage.local.get(function(e){ 66 | console.log(e) 67 | INVERT_ARROW = e.INVERT_ARROW; 68 | LENGTH_THRESHOLD = e.LENGTH_THRESHOLD; 69 | Object.keys(ACTION_MAP).forEach(function(angle){ 70 | if(e["a"+angle]){ 71 | ACTION_MAP[angle] = e["a"+angle]; 72 | } 73 | }) 74 | updateConfiguration(); 75 | }) 76 | } 77 | 78 | loadConfiguration() 79 | 80 | var XY_SPLIT = 0.42; //50-50 is 50, .42 favors vertical axis as most trackpads have something like a 4-3 ratio 81 | 82 | function drawArrow(canvas, orientation){ 83 | //parameters of drawing an arrow 84 | var shaft_rad = 5, 85 | point_len = 15, 86 | head_rad = 15, 87 | shaft_len = 30; 88 | 89 | orientation = (2 * 360 + (orientation || 0)) % 360; 90 | 91 | canvas.width = 50; 92 | canvas.height = 50; 93 | 94 | var c = canvas.getContext('2d'); 95 | 96 | c.beginPath() 97 | c.fillStyle = 'white'; 98 | c.translate(25, 25) 99 | c.rotate(Math.PI/180 * orientation); 100 | 101 | var x = -(shaft_len + point_len) / 2, y = 0; 102 | 103 | c.moveTo(x, y - shaft_rad); 104 | c.lineTo(x + shaft_len, y - shaft_rad) // le shaft 105 | c.lineTo(x + shaft_len, y - head_rad) // arrow face 106 | c.lineTo(x + shaft_len + point_len, y) // le cusp 107 | c.lineTo(x + shaft_len, y + head_rad) // arrow face 108 | c.lineTo(x + shaft_len, y + shaft_rad) 109 | c.lineTo(x, y + shaft_rad); // le shaft 110 | c.lineTo(x, y - shaft_rad); // the butt 111 | c.fill(); 112 | } 113 | 114 | function createArrow(){ 115 | var container = document.createElement('div'); 116 | container.className = 'swipe-gestures-container-element'; 117 | 118 | container.style.position = 'fixed'; 119 | container.style.top = 0; 120 | container.style.left = 0; 121 | container.style.overflow = 'hidden'; 122 | container.style.width = innerWidth + 'px'; 123 | container.style.height = innerHeight + 'px'; 124 | container.style.zIndex = '9999999999999999'; 125 | 126 | var canvas = document.createElement('canvas'); 127 | canvas.style.opacity = 0; 128 | canvas.style.background = 'black'; 129 | canvas.style.borderRadius = '10px'; 130 | canvas.style.padding = '10px'; 131 | 132 | canvas.style.webkitTransitionProperty = 'opacity, -webkit-transform, transform, top, left, right, bottom'; 133 | canvas.style.webkitTransitionDuration = '0.5s, 0.5s, 0s, 0s, 0s, 0s' 134 | canvas.style.position = 'absolute'; 135 | canvas.style.zIndex = '9999999999999999'; 136 | 137 | 138 | container.appendChild(canvas); 139 | document.body.appendChild(container) 140 | return canvas; 141 | } 142 | 143 | function renderProgress(pct){ 144 | if(!navArrow) return; //nav arrow is necessary 145 | if(pct == 0){ 146 | navArrow.style.opacity = pct; 147 | }else{ 148 | navArrow.style.opacity = pct * 0.4 + 0.1; 149 | } 150 | 151 | if(INVERT_ARROW) pct = 1 - pct; 152 | 153 | var boxEdge = 50 + 10 * 2; //50px w/h + 10px padding 154 | 155 | //CAVEAT EMPTOR, the 45deg multiples render slightly off 156 | //but fixing that might incur moar code, ergo not doing it 157 | //not now at least, but if you feel like doing it, have fun 158 | 159 | var nW = ((innerWidth + boxEdge) * pct - boxEdge) + 'px'; 160 | var nH = ((innerHeight + boxEdge) * pct - boxEdge) + 'px'; 161 | // console.log(nW, nH) 162 | if(direction == 180 || direction == 0){ 163 | navArrow.style.top = (innerHeight / 2 - boxEdge / 2) + 'px'; 164 | }else if(direction == 270 || direction == 90){ 165 | navArrow.style.left = (innerWidth / 2 - boxEdge / 2) + 'px'; 166 | } 167 | //these are for the diags 168 | if(direction == 315 || direction == 225 || direction == 270){ // UP 169 | navArrow.style.bottom = nH; 170 | }else if(direction == 135 || direction == 45 || direction == 90){ //DOWN 171 | navArrow.style.top = nH; 172 | } 173 | if(direction == 315 || direction == 45 || direction == 0){ //RIGHT 174 | navArrow.style.left = nW; 175 | }else if(direction == 225 || direction == 135 || direction == 180){ //LEFT 176 | navArrow.style.right = nW; 177 | } 178 | } 179 | 180 | 181 | var scrollListened = []; 182 | var lastDetectedScroll = 0; 183 | var wheelBuffer = []; 184 | var lastWheelTimer = -1; 185 | var direction; 186 | var navArrow; 187 | var len = 0; 188 | var currentSession = -1; 189 | // 0 = unknown/not begun 190 | // 1 = scroll 191 | // 2 = MAGICALPONIES 192 | 193 | function detectScroll(e){ 194 | console.log("detected scroll on", e.target, e, +new Date) 195 | lastDetectedScroll = +new Date; 196 | removeListeners(); 197 | } 198 | 199 | function removeListeners(){ 200 | for(var i = 0; i < scrollListened.length; i++){ 201 | scrollListened[i].removeEventListener('scroll', detectScroll, false) 202 | } 203 | scrollListened = []; 204 | } 205 | 206 | function mouseWheel(x, y){ 207 | // if(navArrow && x % 10 == 0 && y % 10 == 0 && (Math.abs(x) > 100 || Math.abs(y) > 100)){ 208 | // navArrow.style.webkitTransitionDuration = '0.5s, 0.5s, 0.5s, 0.2s, 0.2s, 0.2s'; 209 | // } 210 | 211 | //dot product of direction and the stuff, why, i dont know im dumb 212 | var ty = (XY_SPLIT) * y, tx = (1 - XY_SPLIT) * x; 213 | var r_dir = direction / 180 * Math.PI; 214 | var r_cmp = Math.atan2(ty, tx); 215 | var mag = Math.sqrt(ty*ty + tx*tx); 216 | var dp = mag * Math.cos(r_cmp - r_dir); 217 | var ortho = mag * Math.sin(r_cmp - r_dir) 218 | 219 | // console.log(Math.round(xortho), Math.round(ortho)) 220 | //give transformLength a positive thing just cause 221 | len += transformLength(Math.abs(dp)) * (dp < 0 ? -1 : 1) - Math.abs(ortho / 3); //subtract ortho to punish deviation 222 | 223 | var pct = Math.max(0, Math.min(1, len / LENGTH_THRESHOLD)); 224 | // console.log(dp, len, pct); 225 | renderProgress(pct); 226 | 227 | if(pct >= 0.80){ 228 | //DONE! 229 | removeArrow(navArrow, true); 230 | if(currentSession != 3){ 231 | signalCompletion(); 232 | } 233 | 234 | currentSession = 3; 235 | } 236 | } 237 | 238 | function removeArrow(arrow, complete){ 239 | if(arrow && arrow.style){ 240 | if(complete){ 241 | arrow.style.webkitTransitionDuration = '0.5s, 0.5s, 0.2s, 0.2s, 0.2s, 0.2s' 242 | renderProgress(1) 243 | }else if(currentSession != 3){ 244 | arrow.style.webkitTransitionDuration = '0.5s, 0.5s, 0.5s, 0.5s, 0.5s, 0.5s'; 245 | renderProgress(0) 246 | arrow.style.webkitTransform = 'scale(2.0)'; 247 | signalCancellation() 248 | } 249 | 250 | arrow.addEventListener('webkitTransitionEnd', function(){ 251 | var gc; 252 | while(gc = document.querySelector('.swipe-gestures-container-element')) 253 | gc.parentNode.removeChild(gc); 254 | }) 255 | } 256 | if(arrow === navArrow){ 257 | navArrow = null; 258 | } 259 | } 260 | 261 | function endTrigger(){ 262 | signalEnd(); 263 | removeArrow(navArrow); 264 | currentSession = -1; 265 | wheelBuffer = []; 266 | len = 0; 267 | clearTimeout(lastWheelTimer); 268 | } 269 | 270 | function scrollTrigger(){ 271 | if(navArrow) navArrow.style.display = 'none'; 272 | removeArrow(navArrow); 273 | wheelBuffer = []; 274 | currentSession = 1; 275 | signalScroll(); 276 | var gc; 277 | while(gc = document.querySelector('.swipe-gestures-container-element')) 278 | gc.parentNode.removeChild(gc); 279 | } 280 | 281 | 282 | function wheelEvent(e){ 283 | var orientations = Object.keys(ACTION_MAP).filter(function(e){ 284 | return ACTION_MAP[e] != 'disabled' 285 | }).map(function(e){ 286 | return parseInt(e, 10) 287 | }); 288 | 289 | if(orientations.length == 0) return; 290 | 291 | clearTimeout(lastWheelTimer); 292 | 293 | //to convert wheelDeltas to cartesian, you have to flip it 294 | //because negative = scroll down 295 | 296 | var deltaX = -e.wheelDeltaX, deltaY = -e.wheelDeltaY; 297 | 298 | if(currentSession == 1){ 299 | lastWheelTimer = setTimeout(endTrigger, 600); 300 | }else if(currentSession == 3){ 301 | lastWheelTimer = setTimeout(endTrigger, 400); 302 | }else{ 303 | lastWheelTimer = setTimeout(endTrigger, 900); 304 | } 305 | 306 | if(currentSession == 0 || currentSession == -1){ 307 | wheelBuffer.push([deltaX, deltaY]); 308 | } 309 | if(currentSession == 2 || currentSession == 3){ 310 | if(len > LENGTH_THRESHOLD * 0.01){ 311 | e.preventDefault(); 312 | e.stopPropagation(); 313 | }else if(len < -0.1 * LENGTH_THRESHOLD){ 314 | scrollTrigger(); 315 | } 316 | } 317 | if(currentSession == 2){ 318 | mouseWheel(deltaX, deltaY); 319 | 320 | }else if(currentSession == -1){ 321 | removeListeners(); 322 | var el = document.elementFromPoint(e.clientX, e.clientY); 323 | do { 324 | // console.log("parent", el); 325 | scrollListened.push(el); 326 | el.addEventListener('scroll', detectScroll, false); 327 | } while (el = el.parentNode); 328 | var checkpointScroll = lastDetectedScroll; 329 | // isTriggering = 1; 330 | currentSession = 0; 331 | setTimeout(function(){ 332 | var orientation = 0; 333 | // console.log(wheelBuffer) 334 | var mag_sum = 0, ang_sum = 0; 335 | var angles = wheelBuffer.forEach(function(xy){ 336 | var x = xy[0], y = xy[1], mag = Math.sqrt(x * x + y * y); 337 | mag_sum += mag; 338 | ang_sum += ((2 * 360 + Math.atan2(y, x) / Math.PI * 180) % 360) * mag; 339 | }); 340 | var mean = ang_sum / Math.max(1, mag_sum); 341 | 342 | var closest = orientations.sort(function(a, b){ 343 | return Math.pow(mean - a, 2) - Math.pow(mean - b, 2) 344 | })[0]; 345 | // console.log(mean, stdev, closest); 346 | // console.log(wheelBuffer.length) 347 | if(checkpointScroll == lastDetectedScroll && 348 | wheelBuffer.length > 0 && 349 | // stdev < 15 && 350 | Math.abs(closest - mean) < 30 351 | ){ 352 | direction = closest; 353 | navArrow = createArrow(); 354 | drawArrow(navArrow, closest); 355 | renderProgress(0) 356 | currentSession = 2; 357 | wheelBuffer = []; 358 | 359 | }else{ 360 | scrollTrigger() 361 | } 362 | 363 | }, 100) 364 | // empirically, the margin is usually only about 3msecs 365 | // so having an order of magnitude's worth in leeway is probably 366 | // sufficient 367 | } 368 | } 369 | 370 | window.addEventListener('mousewheel', wheelEvent); --------------------------------------------------------------------------------