<script lang="ts"> import { twJoin } from "tailwind-merge" import { Store, Stores, UIEventSource } from "../../../Logic/UIEventSource" /** * Given the available floors, shows an elevator to pick a single one * * This is but the input element, the logic of handling the filter is in 'LevelSelector' */ export let floors: Store<string[]> export let value: UIEventSource<string> const HEIGHT = 40 let initialIndex = Math.max(0, floors?.data?.findIndex((f) => f === value?.data) ?? 0) let index: UIEventSource<number> = new UIEventSource<number>(initialIndex) let forceIndex: number | undefined = undefined let top = Math.max(0, initialIndex) * HEIGHT let elevator: HTMLImageElement let mouseDown = false let container: HTMLElement $: { if (top > 0 || forceIndex !== undefined) { index.setData(closestFloorIndex()) value.setData(floors.data[forceIndex ?? closestFloorIndex()]) } } function unclick() { mouseDown = false } function click() { mouseDown = true } function closestFloorIndex() { return Math.min(floors.data.length - 1, Math.max(0, Math.round(top / HEIGHT))) } function onMove(e: { movementY: number }) { if (mouseDown) { forceIndex = undefined const containerY = container.clientTop const containerMax = containerY + (floors.data.length - 1) * HEIGHT top = Math.min(Math.max(0, top + e.movementY), containerMax) } } let momentum = 0 function stabilize() { // Automatically move the elevator to the closes floor if (mouseDown) { return } const target = (forceIndex ?? index.data) * HEIGHT let diff = target - top if (diff > 1) { diff /= 3 } const sign = Math.sign(diff) momentum = momentum + sign let diffR = Math.min(Math.abs(momentum), forceIndex !== undefined ? 9 : 3, Math.abs(diff)) momentum = Math.sign(momentum) * Math.min(diffR, Math.abs(momentum)) top += sign * diffR if (index.data === forceIndex) { forceIndex = undefined } top = Math.max(top, 0) } Stores.Chronic(50).addCallback((_) => stabilize()) floors.addCallback((floors) => { forceIndex = floors.findIndex((s) => s === value.data) }) let image: HTMLImageElement $: { if (image) { let lastY = 0 image.ontouchstart = (e: TouchEvent) => { mouseDown = true lastY = e.changedTouches[0].clientY } image.ontouchmove = (e) => { const y = e.changedTouches[0].clientY console.log(y) const movementY = y - lastY lastY = y onMove({ movementY }) } image.ontouchend = unclick } } </script> <div bind:this={container} class="relative" style={`height: calc(${HEIGHT}px * ${$floors.length}); width: 96px`} > <div class="absolute right-0 h-full w-min"> {#each $floors as floor, i} <button style={`height: ${HEIGHT}px; width: ${HEIGHT}px`} class={twJoin( "content-box m-0 flex items-center justify-center border-2 border-gray-300", i === (forceIndex ?? $index) && "selected" )} on:click={() => { forceIndex = i }} > {floor} </button> {/each} </div> <div style={`width: ${HEIGHT}px`}> <img bind:this={image} class="draggable" draggable="false" on:mousedown={click} src="./assets/svg/elevator.svg" style={`top: ${top}px;`} /> </div> </div> <svelte:window on:mousemove={onMove} on:mouseup={unclick} /> <style> .draggable { user-select: none; cursor: move; position: absolute; user-drag: none; height: 72px; margin-top: -15px; margin-bottom: -15px; margin-left: -18px; -webkit-user-drag: none; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; } </style>