|
<!DOCTYPE html> |
|
<html lang="ko"> |
|
<head> |
|
<meta charset="utf-8"> |
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, target-densitydpi=medium-dpi"> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"> |
|
<script src="../../node_modules/rxjs/bundles/rxjs.umd.js"></script> |
|
<style> |
|
.view { |
|
overflow: hidden; |
|
} |
|
|
|
.view .container { |
|
white-space: nowrap; |
|
padding: 0px; |
|
list-style: none; |
|
font-size: 0; |
|
} |
|
|
|
.view .panel { |
|
width: 100%; |
|
min-height: 200px; |
|
display: inline-block; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<div id="carousel" class="view"> |
|
<ul class="container"> |
|
<li class="panel" style="background-color:lightgreen"> |
|
</li> |
|
<li class="panel" style="background-color:lightpink"> |
|
</li> |
|
<li class="panel" style="background-color:royalblue"> |
|
</li> |
|
<li class="panel" style="background-color:darkred"> |
|
</li> |
|
</ul> |
|
</div> |
|
|
|
<script> |
|
const THRESHOLD = 30; |
|
const DEFAULT_DURATION = 300; |
|
const $view = document.getElementById("carousel"); |
|
const $container = $view.querySelector(".container"); |
|
const PANEL_COUNT = $container.querySelectorAll(".panel").length; |
|
const SUPPORT_TOUCH = "ontouchstart" in window; |
|
const EVENTS = { |
|
start: SUPPORT_TOUCH ? "touchstart" : "mousedown", |
|
move: SUPPORT_TOUCH ? "touchmove" : "mousemove", |
|
end: SUPPORT_TOUCH ? "touchend" : "mouseup" |
|
}; |
|
|
|
const { fromEvent, merge, of, defer, animationFrameScheduler, interval, concat } = rxjs; |
|
const { |
|
map, |
|
startWith, |
|
switchMap, |
|
takeUntil, |
|
reduce, |
|
share, |
|
first, |
|
scan, |
|
withLatestFrom, |
|
takeWhile |
|
} = rxjs.operators; |
|
|
|
function animation(from, to, duration) { |
|
return defer(() => { |
|
const scheduler = animationFrameScheduler; |
|
const start = scheduler.now(); |
|
const interval$ = interval(0, scheduler) |
|
.pipe( |
|
map(() => (scheduler.now() - start) / duration), |
|
takeWhile(rate => rate < 1) |
|
); |
|
return concat(interval$, of(1)) |
|
.pipe( |
|
map(rate => from + (to - from) * rate) |
|
); |
|
}); |
|
} |
|
|
|
function toPos(obs$) { |
|
return obs$.pipe( |
|
map(v => SUPPORT_TOUCH ? v.changedTouches[0].pageX : v.pageX) |
|
); |
|
} |
|
|
|
function translateX(posX) { |
|
$container.style.transform = `translate3d(${posX}px, 0, 0)`; |
|
} |
|
|
|
const start$ = fromEvent($view, EVENTS.start).pipe(toPos); |
|
const move$ = fromEvent($view, EVENTS.move).pipe(toPos); |
|
const end$ = fromEvent($view, EVENTS.end); |
|
|
|
const size$ = fromEvent(window, "resize").pipe( |
|
startWith(0), |
|
map(event => $view.clientWidth) |
|
) |
|
|
|
const drag$ = start$.pipe( |
|
switchMap(start => { |
|
return move$.pipe( |
|
map(move => move - start), |
|
takeUntil(end$) |
|
); |
|
}), |
|
share(), |
|
map(distance => ({ distance })) |
|
) |
|
|
|
const drop$ = drag$.pipe( |
|
switchMap(drag => { |
|
return end$.pipe( |
|
map(event => drag), |
|
first() |
|
) |
|
}), |
|
withLatestFrom(size$, (drag, size) => { |
|
return { ...drag, size }; |
|
}) |
|
); |
|
|
|
const carousel$ = merge(drag$, drop$) |
|
.pipe( |
|
scan((store, {distance, size}) => { |
|
const updateStore = { |
|
from: -(store.index * store.size) + distance |
|
}; |
|
|
|
if (size === undefined) { // drag 시점 |
|
updateStore.to = updateStore.from; |
|
} else { // drop 시점 |
|
let tobeIndex = store.index; |
|
if (Math.abs(distance) >= THRESHOLD) { |
|
tobeIndex = distance < 0 ? |
|
Math.min(tobeIndex + 1, PANEL_COUNT - 1) : |
|
Math.max(tobeIndex - 1, 0); |
|
} |
|
updateStore.index = tobeIndex; |
|
updateStore.to = -(tobeIndex * size); |
|
updateStore.size = size; |
|
} |
|
return { ...store, ...updateStore }; |
|
}, { |
|
from: 0, |
|
to: 0, |
|
index: 0, |
|
size: 0, |
|
}), |
|
switchMap(({from, to}) => from === to ? |
|
of(to) : animation(from, to, DEFAULT_DURATION)) |
|
); |
|
|
|
carousel$.subscribe(pos => { |
|
console.log("캐로셀 데이터", pos); |
|
translateX(pos); |
|
}); |
|
</script> |
|
</body> |
|
</html> |