├── .gitignore ├── demo ├── .babelrc ├── postcss.config.js ├── src │ ├── index.js │ └── styles │ │ └── main.css ├── webpack.config.js ├── static │ ├── index.html │ └── main.css └── package.json ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | example 3 | -------------------------------------------------------------------------------- /demo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-class-properties", 4 | "transform-object-rest-spread", 5 | ['module-resolver', { 6 | 'root': ['.'], 7 | 'alias': {} 8 | }] 9 | ], 10 | "presets": [ 11 | "es2015", 12 | "react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | const p = process.env.NODE_ENV === 'production' 2 | 3 | module.exports = { 4 | plugins: [ 5 | require('postcss-import'), 6 | require('postcss-nested'), 7 | require('postcss-cssnext'), 8 | require('postcss-discard-comments'), 9 | require('postcss-reporter'), 10 | p ? require('cssnano') : '' 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | /** imports */ 2 | import rosin from '../../index.js' 3 | 4 | /** hot module reloading */ 5 | if (module.hot && process && process.env.NODE_ENV !== 'production') { 6 | module.hot.accept() 7 | } 8 | 9 | /** application code */ 10 | const root = document.getElementById('root') 11 | 12 | const swiper = rosin(root) 13 | 14 | swiper.on('drag', arg => { 15 | console.log(arg) 16 | root.innerHTML = `
${JSON.stringify(arg, null, '  ')}
` 17 | }) 18 | -------------------------------------------------------------------------------- /demo/src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import 'svbstrate/src/lib/reset'; 2 | @import 'svbstrate/src/lib/display'; 3 | @import 'svbstrate/src/lib/positioning'; 4 | @import 'svbstrate/src/lib/flexbox'; 5 | @import 'svbstrate/src/lib/align'; 6 | @import 'svbstrate/src/lib/spacing'; 7 | @import 'svbstrate/src/lib/buttons'; 8 | @import 'svbstrate/src/lib/forms'; 9 | @import 'svbstrate/src/lib/lists'; 10 | @import 'svbstrate/src/lib/typography'; 11 | @import 'svbstrate/src/lib/z-index'; 12 | 13 | body { 14 | height: 80vh; 15 | } 16 | 17 | #root { 18 | width: 50vh; 19 | height: 50vh; 20 | position: absolute; 21 | margin: auto; 22 | top: 0; 23 | bottom: 0; 24 | left: 0; 25 | right: 0; 26 | background: tomato; 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rosin", 3 | "version": "2.0.2", 4 | "description": "Touch detection library", 5 | "source": "index.js", 6 | "module": "dist/rosin.es.js", 7 | "main": "dist/rosin.js", 8 | "umd:main": "dist/rosin.umd.js", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "build": "microbundle build", 14 | "watch": "microbundle watch" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+ssh://git@github.com/estrattonbailey/rosin.git" 19 | }, 20 | "author": "estrattonbailey", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/estrattonbailey/rosin/issues" 24 | }, 25 | "homepage": "https://github.com/estrattonbailey/rosin#readme", 26 | "devDependencies": { 27 | "microbundle": "^0.4.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /demo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const p = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | devtool: 'cheap-module-source-map', 7 | entry: path.join(__dirname, 'src/index.js'), 8 | output: { 9 | path: path.join(__dirname, 'static'), 10 | filename: 'index.js', 11 | publicPath: '/' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /node_modules/, 18 | include: path.join(__dirname, 'src'), 19 | loaders: ['babel-loader'] 20 | }, 21 | ] 22 | }, 23 | resolve: { 24 | alias: { 25 | '@': path.join(__dirname, 'src') 26 | } 27 | }, 28 | plugins: p ? [] : [ 29 | new webpack.HotModuleReplacementPlugin() 30 | ], 31 | devServer: { 32 | hot: true, 33 | contentBase: path.join(__dirname, 'static'), 34 | publicPath: '/', 35 | compress: true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demo/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | @estrattonbailey/frame 29 | 30 | 31 |
32 |
hello
33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frame", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "concurrently 'npm run watch:css' 'npm run serve'", 8 | "serve": "webpack-dev-server --open", 9 | "build": "npm run build:js && npm run build:css", 10 | "build:js": "NODE_ENV=production webpack --progress -p", 11 | "build:css": "NODE_ENV=production postcss src/styles/main.css -o static/main.css", 12 | "watch:css": "NODE_ENV=development postcss src/styles/main.css -w -o static/main.css" 13 | }, 14 | "author": "estrattonbailey", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "babel-core": "^6.25.0", 18 | "babel-eslint": "^7.2.3", 19 | "babel-loader": "^7.0.0", 20 | "babel-plugin-module-resolver": "^2.7.1", 21 | "babel-plugin-transform-class-properties": "^6.24.1", 22 | "babel-plugin-transform-object-assign": "^6.22.0", 23 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 24 | "babel-preset-es2015": "^6.24.1", 25 | "babel-preset-react": "^6.24.1", 26 | "concurrently": "^3.6.1", 27 | "cssnano": "^3.10.0", 28 | "postcss": "^6.0.23", 29 | "postcss-cli": "^4.1.1", 30 | "postcss-cssnext": "^2.11.0", 31 | "postcss-discard-comments": "^2.0.4", 32 | "postcss-import": "^10.0.0", 33 | "postcss-nested": "^2.0.2", 34 | "standard": "^10.0.2", 35 | "standard-loader": "^6.0.1", 36 | "webpack": "^2.6.1", 37 | "webpack-dev-server": "^2.11.5" 38 | }, 39 | "dependencies": { 40 | "svbstrate": "^4.1.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rosin 2 | 3 | A tiny single-finger tap and swipe library. Works with touch and/or mouse 4 | events. **700 bytes gzipped.** 5 | 6 | ## Install 7 | 8 | ``` 9 | npm i rosin --save 10 | ``` 11 | 12 | # Usage 13 | 14 | ```javascript 15 | import rosin from "rosin"; 16 | 17 | const swiper = rosin(document.body); 18 | ``` 19 | 20 | Emitted values are _relative_ to the DOM node you instantiated on. 21 | 22 | ```javascript 23 | swiper.on("tap", ({ x, y }, e) => {}); 24 | swiper.on("mouseup", ({ x, y }, e) => {}); 25 | swiper.on("mousedown", ({ x, y }, e) => {}); 26 | 27 | /** Fired once on each swipe */ 28 | swiper.on("left", ({ x, y }, e) => {}); 29 | swiper.on("right", ({ x, y }, e) => {}); 30 | swiper.on("up", ({ x, y }, e) => {}); 31 | swiper.on("down", ({ x, y }, e) => {}); 32 | ``` 33 | 34 | Drag events emit a different payload. It looks like this: 35 | 36 | ``` 37 | { 38 | ix, // initial X coordinate 39 | iy, // initial Y coordinate 40 | dx, // delta (change) in X coordinate 41 | dy, // delta (change) in Y coordinate 42 | x, // current X coordinate 43 | y, // current Y coordinate 44 | } 45 | ``` 46 | 47 | ```javascript 48 | /** Fired on every animation frame */ 49 | swiper.on("drag", (coords, e) => {}); 50 | swiper.on("dragLeft", (coords, e) => {}); 51 | swiper.on("dragRight", (coords, e) => {}); 52 | swiper.on("dragUp", (coords, e) => {}); 53 | swiper.on("dragDown", (coords, e) => {}); 54 | ``` 55 | 56 | Each emitter also returns a function to destroy itself: 57 | 58 | ```javascript 59 | const tapListener = swiper.on("tap", () => {}); 60 | 61 | tapListener(); // destroy listener 62 | ``` 63 | 64 | To destroy the entire instance: 65 | 66 | ```javascript 67 | swiper.destroy(); 68 | ``` 69 | 70 | ## License 71 | 72 | MIT License © [Eric Bailey](https://estrattonbailey.com) 73 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const abs = Math.abs 2 | 3 | function coord (e, y) { 4 | return e.touches ? e.touches[0][y ? 'pageY' : 'pageX'] : e[y ? 'clientY' : 'clientX'] 5 | } 6 | 7 | function coords (ctx, e) { 8 | const { top, bottom, left, right } = ctx.getBoundingClientRect() 9 | const x = coord(e) - left 10 | const y = coord(e, 1) - top 11 | 12 | return { 13 | x, 14 | y, 15 | px: x / (right - left), 16 | py: y / (bottom - top) 17 | } 18 | } 19 | 20 | export default function rosin (ctx) { 21 | let dir 22 | let x 23 | let y 24 | let focus = false 25 | let dragging = false 26 | 27 | const fns = {} 28 | 29 | function start (e) { 30 | if (e.target === ctx || ctx.contains(e.target)) { 31 | focus = true 32 | 33 | const pos = coords(ctx, e) 34 | 35 | x = pos.x 36 | y = pos.y 37 | 38 | emit('start', { x, y }, e) 39 | } 40 | } 41 | 42 | function end (e) { 43 | if (focus) { 44 | emit('end', e) 45 | 46 | if (dragging) { 47 | emit('dragEnd', e) 48 | } else { 49 | emit('tap', { x, y }, e) 50 | } 51 | } 52 | 53 | cancel() 54 | } 55 | 56 | function move (e) { 57 | if (focus) { 58 | const cancel = e.preventDefault.bind(e) 59 | const pos = coords(ctx, e) 60 | const deltaX = pos.x - x 61 | const deltaY = pos.y - y 62 | const travelX = abs(deltaX) 63 | const travelY = abs(deltaY) 64 | const horizontal = travelX > travelY 65 | 66 | if (travelX < 10 && travelY < 10) return 67 | 68 | const payload = { 69 | px: pos.px, 70 | py: pos.py, 71 | ix: x, 72 | iy: y, 73 | dx: deltaX, 74 | dy: deltaY, 75 | x: x + deltaX, 76 | y: y + deltaY, 77 | } 78 | 79 | dragging = true 80 | 81 | fns.drag && cancel() 82 | 83 | emit('drag', payload, e) 84 | 85 | if (deltaX > 0 && horizontal) { 86 | fns.dragRight && cancel() 87 | 88 | emit('dragRight', payload, e) 89 | 90 | if (dir !== 'right') { 91 | emit('right', { x, y }, e) 92 | dir = 'right' 93 | } 94 | } else if (deltaX < 0 && horizontal) { 95 | fns.dragLeft && cancel() 96 | 97 | emit('dragLeft', payload, e) 98 | 99 | if (dir !== 'left') { 100 | emit('left', { x, y }, e) 101 | dir = 'left' 102 | } 103 | } else if (deltaY > 0 && !horizontal) { 104 | fns.dragDown && cancel() 105 | 106 | emit('dragDown', payload, e) 107 | 108 | if (dir !== 'down') { 109 | emit('down', { x, y }, e) 110 | dir = 'down' 111 | } 112 | } else if (deltaY < 0 && !horizontal) { 113 | fns.dragUp && cancel() 114 | 115 | emit('dragUp', payload, e) 116 | 117 | if (dir !== 'up') { 118 | emit('up', { x, y }, e) 119 | dir = 'up' 120 | } 121 | } 122 | } 123 | } 124 | 125 | function emit (type, payload, event) { 126 | if (fns[type]) for (let i = 0; i < fns[type].length; i++) fns[type][i](payload, event) 127 | } 128 | 129 | function cancel () { 130 | dir = null 131 | x = null 132 | y = null 133 | focus = false 134 | } 135 | 136 | window.addEventListener('mousedown', start) 137 | window.addEventListener('touchstart', start) 138 | 139 | window.addEventListener('mouseup', end) 140 | window.addEventListener('touchend', end) 141 | 142 | window.addEventListener('mousemove', move) 143 | window.addEventListener('touchmove', move) 144 | 145 | window.addEventListener('touchcancel', cancel) 146 | 147 | return { 148 | on (type, fn) { 149 | fns[type] = fns[type] || [] 150 | fns[type].indexOf(fn) < 0 && fns[type].push(fn) 151 | return () => fns[type].splice(fns[type].indexOf(fn), 1) 152 | }, 153 | destroy () { 154 | window.removeEventListener('mousedown', start) 155 | window.removeEventListener('touchstart', start) 156 | 157 | window.removeEventListener('mouseup', end) 158 | window.removeEventListener('touchend', end) 159 | 160 | window.removeEventListener('mousemove', move) 161 | window.removeEventListener('touchmove', move) 162 | 163 | window.removeEventListener('touchcancel', cancel) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /demo/static/main.css: -------------------------------------------------------------------------------- 1 | html, body{ margin:0 } 2 | *, 3 | *::before, 4 | *::after{ 5 | box-sizing:border-box; 6 | } 7 | a, 8 | button, 9 | [role="button"], 10 | input, 11 | label, 12 | select, 13 | textarea{ 14 | -ms-touch-action:manipulation; 15 | touch-action:manipulation; 16 | } 17 | .block{ display:block } 18 | .inline-block{ display:inline-block } 19 | .rel{ position:relative } 20 | .abs{ position:absolute } 21 | .fix{ position:fixed } 22 | .fill, .top{ top:0 } 23 | .fill, .bottom{ bottom:0 } 24 | .fill, .left{ left:0 } 25 | .fill, .right{ right:0 } 26 | .x{ width:100% } 27 | .y{ height:100% } 28 | .f{ display:-ms-flexbox; display:flex } 29 | .fw{ -ms-flex-wrap:wrap; flex-wrap:wrap } 30 | .ais{ -ms-flex-align:start; align-items:flex-start } 31 | .aie{ -ms-flex-align:end; align-items:flex-end } 32 | .aic{ -ms-flex-align:center; align-items:center } 33 | .aib{ -ms-flex-align:baseline; align-items:baseline } 34 | .jcs{ -ms-flex-pack:start; justify-content:flex-start } 35 | .jce{ -ms-flex-pack:end; justify-content:flex-end } 36 | .jcc{ -ms-flex-pack:center; justify-content:center } 37 | .jcb{ -ms-flex-pack:justify; justify-content:space-between } 38 | .fa{ 39 | -ms-flex:1 1 auto; 40 | flex:1 1 auto; 41 | min-width:0; 42 | min-height:0; 43 | } 44 | .al{ text-align:left } 45 | .ac{ text-align:center } 46 | .ar{ text-align:right } 47 | .aj{ text-align:justify } 48 | .mxa, .ma{ margin-left:auto } 49 | .mxa, .ma{ margin-right:auto } 50 | .mya, .ma{ margin-top:auto } 51 | .mya, .ma{ margin-bottom:auto; } 52 | .pt0, .py0, .p0{ padding-top:0 } 53 | .pb0, .py0, .p0{ padding-bottom:0 } 54 | .mt0, .my0, .m0{ margin-top:0 } 55 | .mb0, .my0, .m0{ margin-bottom:0 } 56 | .mt025, .my025, .m025{ margin-top:0.25em } 57 | .mb025, .my025, .m025{ margin-bottom:0.25em } 58 | .ml025, .mx025, .m025{ margin-left:0.25em } 59 | .mr025, .mx025, .m025{ margin-right:0.25em } 60 | .pt025, .py025, .p025{ padding-top:0.25em } 61 | .pb025, .py025, .p025{ padding-bottom:0.25em } 62 | .pl025, .px025, .p025{ padding-left:0.25em } 63 | .pr025, .px025, .p025{ padding-right:0.25em } 64 | .mt05, .my05, .m05{ margin-top:0.5em } 65 | .mb05, .my05, .m05{ margin-bottom:0.5em } 66 | .ml05, .mx05, .m05{ margin-left:0.5em } 67 | .mr05, .mx05, .m05{ margin-right:0.5em } 68 | .pt05, .py05, .p05{ padding-top:0.5em } 69 | .pb05, .py05, .p05{ padding-bottom:0.5em } 70 | .pl05, .px05, .p05{ padding-left:0.5em } 71 | .pr05, .px05, .p05{ padding-right:0.5em } 72 | .mt075, .my075, .m075{ margin-top:0.75em } 73 | .mb075, .my075, .m075{ margin-bottom:0.75em } 74 | .ml075, .mx075, .m075{ margin-left:0.75em } 75 | .mr075, .mx075, .m075{ margin-right:0.75em } 76 | .pt075, .py075, .p075{ padding-top:0.75em } 77 | .pb075, .py075, .p075{ padding-bottom:0.75em } 78 | .pl075, .px075, .p075{ padding-left:0.75em } 79 | .pr075, .px075, .p075{ padding-right:0.75em } 80 | .mt1, .my1, .m1{ margin-top:1em } 81 | .mb1, .my1, .m1{ margin-bottom:1em } 82 | .ml1, .mx1, .m1{ margin-left:1em } 83 | .mr1, .mx1, .m1{ margin-right:1em } 84 | .pt1, .py1, .p1{ padding-top:1em } 85 | .pb1, .py1, .p1{ padding-bottom:1em } 86 | .pl1, .px1, .p1{ padding-left:1em } 87 | .pr1, .px1, .p1{ padding-right:1em } 88 | button{ 89 | border:0; 90 | border-radius:0; 91 | display:inline-block; 92 | cursor:pointer; 93 | -webkit-appearance:none; 94 | } 95 | button.button, 96 | .button[role="button"], 97 | input.button[type="submit"]{ 98 | background-color:#000; 99 | color:white; 100 | padding:0.5em 1.5em; 101 | } 102 | form{ 103 | margin:0; 104 | } 105 | input, 106 | textarea, 107 | select{ 108 | display:inline-block; 109 | outline:0; 110 | border-radius:0; 111 | border:1px solid #000; 112 | position:relative; 113 | font-size:inherit; 114 | background-color:transparent; 115 | } 116 | textarea{ 117 | max-width:100%; 118 | overflow:auto; 119 | resize:vertical; 120 | } 121 | ol, ul{ 122 | list-style:none; 123 | padding:0; 124 | margin:0; 125 | } 126 | ul.list, 127 | ol.list{ 128 | padding-left:2em; 129 | } 130 | ol.list{ 131 | list-style:decimal; 132 | } 133 | ul.list{ 134 | list-style:disc; 135 | } 136 | html, body{ 137 | color:#000; 138 | font-family:-apple-system, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, BlinkMacSystemFont, sans-serif; 139 | font-size:100%; 140 | line-height:1.7; 141 | font-weight:400; 142 | -webkit-font-smoothing:antialiased; 143 | } 144 | h1, h2, h3, h4, h5, h6{ 145 | margin:0; 146 | } 147 | .s1, 148 | h1, .h1{ 149 | font-size:64px; 150 | font-size:4rem; 151 | } 152 | h1, .h1{ 153 | line-height:1.1; 154 | } 155 | .s2, 156 | h2, .h2{ 157 | font-size:48px; 158 | font-size:3rem; 159 | } 160 | h2, .h2{ 161 | line-height:1.3; 162 | } 163 | .s3, 164 | h3, .h3{ 165 | font-size:32px; 166 | font-size:2rem; 167 | } 168 | h3, .h3{ 169 | line-height:1.5; 170 | } 171 | .s4, 172 | h4, .h4{ 173 | font-size:24px; 174 | font-size:1.5rem; 175 | } 176 | h4, .h4{ 177 | line-height:1.5; 178 | } 179 | .s5, 180 | h5, .h5{ 181 | font-size:16px; 182 | font-size:1rem; 183 | } 184 | h5, .h5{ 185 | line-height:1.6; 186 | } 187 | .s6, 188 | h6, .h6{ 189 | font-size:14px; 190 | font-size:0.875rem; 191 | } 192 | h6, .h6{ 193 | line-height:1.6; 194 | } 195 | .s0, 196 | p, .p{ 197 | font-size:16px; 198 | font-size:1rem; 199 | } 200 | p, .p{ 201 | line-height:1.7; 202 | } 203 | p{ 204 | margin:1em 0; 205 | } 206 | a{ 207 | color:inherit; 208 | } 209 | hr{ 210 | display:block; 211 | border:0; 212 | margin:0; 213 | height:1px; 214 | width:100%; 215 | background-color:currentColor; 216 | color:inherit; 217 | } 218 | small, .small{ 219 | font-size:0.75em; 220 | } 221 | strong, .b{ 222 | font-weight:bold; 223 | } 224 | em, .i{ 225 | font-style:italic; 226 | } 227 | .caps{ 228 | text-transform:uppercase; 229 | } 230 | .no-under{ 231 | text-decoration:none; 232 | } 233 | .z0{ z-index:0 } 234 | .z1{ z-index:100 } 235 | .z2{ z-index:200 } 236 | .z3{ z-index:300 } 237 | .z4{ z-index:400 } 238 | .z5{ z-index:500 } 239 | .z6{ z-index:600 } 240 | .z7{ z-index:700 } 241 | .z8{ z-index:800 } 242 | .z9{ z-index:900 } 243 | .z10{ z-index:1000 } 244 | body{ 245 | height:80vh; 246 | } 247 | #root{ 248 | width:50vh; 249 | height:50vh; 250 | position:absolute; 251 | margin:auto; 252 | top:0; 253 | bottom:0; 254 | left:0; 255 | right:0; 256 | background:tomato; 257 | } 258 | --------------------------------------------------------------------------------