Skip to content

Commit

Permalink
feat: Engine supporting 1-level Marking Menu.
Browse files Browse the repository at this point in the history
  • Loading branch information
QuentinRoy committed Jul 20, 2017
1 parent f44b65e commit 39a04f1
Show file tree
Hide file tree
Showing 10 changed files with 470 additions and 4 deletions.
4 changes: 4 additions & 0 deletions build-config/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default {
pug(),
babel({ exclude: 'node_modules/**' })
],
external: ['rxjs'],
globals: {
'rxjs': 'Rx'
},
dest: './marking-menu.js',
sourceMap: true
};
279 changes: 279 additions & 0 deletions demo/Rx.min.js

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions demo/Rx.min.js.map

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
<div id="main"></div>


<script src="./Rx.min.js"></script>
<script src="../marking-menu.js"></script>
<script>
var items = Array.from({ length: 8 }).map((_, i) => `item ${i}`);
var mm = MarkingMenu(items, document.getElementById('main'), [250, 200]);
mm.setActiveByNearestAngle(87);
var main = document.getElementById('main');
var mm = MarkingMenu(items, main);
</script>
</body>

Expand Down
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
},
"author": "Quentin Roy <quentin@quentinroy.fr>",
"license": "MIT",
"dependencies": {
"rxjs": "^5.4.2"
},
"devDependencies": {
"ava": "^0.21.0",
"babel-plugin-external-helpers": "^6.22.0",
Expand Down
99 changes: 99 additions & 0 deletions src/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Observable } from 'rxjs';

// Create a custom pointer event from a touch event.
const createPEventFromTouchEvent = touchEvt => {
const touchList = Array.from(touchEvt.targetTouches);
const sumX = touchList.reduce((acc, t) => acc + t.clientX, 0);
const sumY = touchList.reduce((acc, t) => acc + t.clientY, 0);
const meanX = sumX / touchList.length;
const meanY = sumY / touchList.length;
return {
originalEvent: touchEvt,
clientX: meanX,
clientY: meanY
};
};

// Create a custom pointer from a mouse event.
const createPEventFromMouseEvent = mouseEvt => ({
originalEvent: mouseEvt,
clientX: mouseEvt.clientX,
clientY: mouseEvt.clientY
});

/**
* Create the marking menu controller.
* @param {HTMLElement} parentDOM the element where to listen for events.
*/
const createEngine = parentDOM => {
// Higher order observable tracking mouse drags.
const mouseDrag$ = Observable.fromEvent(parentDOM, 'mousedown')
.map(downEvt =>
Observable.fromEvent(parentDOM, 'mousemove')
// Make sure we include the first mouse down event.
.startWith(downEvt)
.takeUntil(Observable.fromEvent(parentDOM, 'mouseup'))
)
.map(o => o.map(createPEventFromMouseEvent));

// Higher order observable tracking touch drags.
const touchDrag$ = Observable.fromEvent(parentDOM, 'touchstart')
// Menu is supposed to have pointer-events: none so we can safely rely on
// targetTouches.
.filter(evt => evt.targetTouches.length === 1)
.map(firstEvent =>
Observable.fromEvent(parentDOM, 'touchmove')
.startWith(firstEvent)
.takeUntil(
Observable.merge(
Observable.fromEvent(parentDOM, 'touchend'),
Observable.fromEvent(parentDOM, 'touchcancel'),
Observable.fromEvent(parentDOM, 'touchstart')
).filter(evt => evt.targetTouches.length !== 1)
)
)
.map(o => o.map(createPEventFromTouchEvent));

// Higher order observable tracking drags.
const drag$ = Observable.merge(touchDrag$, mouseDrag$);

// Higher order observable tracking angular drags.
// Emits { center, alpha } where center is the drag start location
// and alpha is the angle of the center to current drag position vector.
const angleDrag$ = drag$.map(o =>
o.scan((acc, evt) => {
const center = acc
? acc.center
: { clientX: evt.clientX, clientY: evt.clientY };
const alpha =
Math.atan2(evt.clientY - center.clientY, evt.clientX - center.clientX) *
360 /
(2 * Math.PI);
return { center, alpha };
}, null)
);

const menuEvent$ = angleDrag$.exhaustMap(o =>
Observable.concat(
o.first().map(e => {
// Adjust the center position
const parentBCR = parentDOM.getBoundingClientRect();
const centerX = e.center.clientX - parentBCR.left;
const centerY = e.center.clientY - parentBCR.top;
return {
type: 'open',
position: [centerX, centerY]
};
}),
o.map(e => ({
type: 'change',
alpha: e.alpha
})),
Observable.from([{ type: 'close' }])
)
);

return menuEvent$;
};

export default createEngine;
31 changes: 30 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
export { default } from './menu';
import createEngine from './engine';
import createMenu from './menu';

export default (items, parentDOM) => {
let menu = null;
// Create the engine.
const menuEvent$ = createEngine(parentDOM);
// Open the menu in function of engine events.
menuEvent$.subscribe(evt => {
try {
switch (evt.type) {
case 'open':
menu = createMenu(items, parentDOM, evt.position);
break;
case 'change':
menu.setActiveByNearestAngle(evt.alpha);
break;
case 'close':
menu.remove();
menu = null;
break;
default:
throw new Error(`Invalid engine type: ${evt.type}`);
}
} catch (e) {
console.error(e);
throw e;
}
});
};
7 changes: 6 additions & 1 deletion src/menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ $line-thickness: 4px;
$line-color: $item-background;
$active-line-color: $active-item-background;
$middle-color: $active-line-color;
$first-item-angle: -90;
$first-item-angle: 0;

.marking-menu {
position: absolute;
pointer-events: none;

* {
pointer-events: none;
}
}

.marking-menu-item {
Expand Down
23 changes: 23 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,26 @@ export const mod = (a, n) => (a % n + n) % n;
* @return {Number} The (signed) delta between the two angles (in degrees).
*/
export const deltaAngle = (alpha, beta) => mod(beta - alpha + 180, 360) - 180;

/**
* Create an observer logging next, errors and complete "events".
* Handy to debug observable.
* @param {String} prefix a prefix to append before log messages.
*/
export const logObservable = prefix => {
const fixedPrefix = prefix ? `${prefix} ` : '';
return {
next(e) {
// eslint-disable-next-line no-console
console.log(`${fixedPrefix}next`, e);
},
error(e) {
// eslint-disable-next-line no-console
console.error(`${fixedPrefix}error`, e);
},
complete() {
// eslint-disable-next-line no-console
console.log(`${fixedPrefix}complete`);
}
};
};

0 comments on commit 39a04f1

Please sign in to comment.