|
105 | 105 | under kortene i vannrett modus, og som et bånd tvers over ved landingslinja |
106 | 106 | i fallende modus, der kortene faller bak det. */ |
107 | 107 | .sband{ |
108 | | - position:relative;height:94px;overflow:visible; |
| 108 | + position:relative;height:94px;overflow:visible;touch-action:none;cursor:grab; |
109 | 109 | background:var(--paper); |
110 | 110 | border-top:3px solid var(--gold);border-bottom:1px solid rgba(28,23,18,.25); |
111 | 111 | } |
112 | 112 | .sband.inflow{margin:0 0 10px} |
| 113 | + .sband.grabbing{cursor:grabbing} |
| 114 | + .sband.grabbing .sseg{cursor:grabbing} |
113 | 115 | .sband.overlay{position:absolute;left:0;right:0;z-index:2;box-shadow:0 10px 26px rgba(0,0,0,.45)} |
114 | 116 | .swin{position:absolute;inset:0;overflow:hidden} |
115 | 117 | .slane{position:absolute;left:0;top:8px;height:78px;display:flex;will-change:transform} |
@@ -1809,7 +1811,7 @@ <h2 class="ptitle" id="ptitle">♪</h2> |
1809 | 1811 | bevegelse, slik at tonene faller nedover i takt med musikken. */ |
1810 | 1812 | const V_PLAYHEAD = 0.62; // landingslinja, andel ned i vinduet |
1811 | 1813 | const S_HEAD = 13; // notehodets avstand fra segmentets venstrekant |
1812 | | -let vY = 0, sSegs = [], beatStart = [], beatTotal = 0; |
| 1814 | +let vY = 0, sX = 0, sSegs = [], beatStart = [], beatTotal = 0; |
1813 | 1815 | /* Alle kortene er like høye. Varigheten leses av notebåndet under, og like |
1814 | 1816 | høye kort gjør at man ser flere av de kommende grepene. */ |
1815 | 1817 | function cardH() { return 86 + state.air * 24; } |
@@ -1844,9 +1846,31 @@ <h2 class="ptitle" id="ptitle">♪</h2> |
1844 | 1846 | function sLaneStyle(transition, x) { |
1845 | 1847 | const lane = $('slane'); |
1846 | 1848 | if (!lane) return; |
| 1849 | + sX = x; |
1847 | 1850 | lane.style.transition = transition; |
1848 | 1851 | lane.style.transform = 'translateX(' + x.toFixed(1) + 'px)'; |
1849 | 1852 | } |
| 1853 | +/* Hvilket taktslag som ligger på spillehodet ved en gitt forskyvning */ |
| 1854 | +function beatAtBandOffset(x) { |
| 1855 | + const w = ($('sband') && $('sband').clientWidth) || 1; |
| 1856 | + return (w / 2 - S_HEAD - x) / ppbS(); |
| 1857 | +} |
| 1858 | +/* Invers av beatAtIndexF: fra taktslag til (brøkdels) tonenummer */ |
| 1859 | +function indexAtBeat(beat) { |
| 1860 | + const n = state.events.length; |
| 1861 | + if (!n) return 0; |
| 1862 | + if (beat <= 0) return 0; |
| 1863 | + for (let i = 0; i < n; i++) { |
| 1864 | + const len = state.events[i].beats; |
| 1865 | + if (beat < beatStart[i] + len) return i + (beat - beatStart[i]) / len; |
| 1866 | + } |
| 1867 | + return n - 1; |
| 1868 | +} |
| 1869 | +/* Flytter begge banene til samme punkt i sangen */ |
| 1870 | +function setLanesFromBeat(beat, transition) { |
| 1871 | + sLaneStyle(transition, sOffset(beat)); |
| 1872 | + if (curDir() === 'v') vLaneStyle(transition, vOffset(indexAtBeat(beat))); |
| 1873 | +} |
1850 | 1874 | function paintBand() { |
1851 | 1875 | sSegs.forEach((el, j) => { |
1852 | 1876 | el.classList.toggle('on', j === state.cur); |
@@ -1916,7 +1940,8 @@ <h2 class="ptitle" id="ptitle">♪</h2> |
1916 | 1940 | function freezeLanes() { |
1917 | 1941 | const v = freezeOne('vlane'); |
1918 | 1942 | if (v) vY = parseFloat(v[5]); |
1919 | | - freezeOne('slane'); |
| 1943 | + const b = freezeOne('slane'); |
| 1944 | + if (b) sX = parseFloat(b[4]); |
1920 | 1945 | } |
1921 | 1946 | /* Etter dra eller hjul: hopp til tonen banen står på */ |
1922 | 1947 | function snapStripLane() { |
@@ -1966,6 +1991,75 @@ <h2 class="ptitle" id="ptitle">♪</h2> |
1966 | 1991 | $('hint').textContent = T('ui.hint'); |
1967 | 1992 | } |
1968 | 1993 |
|
| 1994 | +/* Notebåndet kan dras sidelengs — det er den naturlige bevegelsen på en |
| 1995 | + vannrett tidslinje, og den korteste veien tilbake til begynnelsen. |
| 1996 | + Kortene følger med, og ved slipp hopper begge til nærmeste tone. */ |
| 1997 | +function setupBandDrag(el) { |
| 1998 | + let down = false, moved = false, pid = null, startX = 0, base = 0; |
| 1999 | + let justDragged = false, lastIdx = -1, snapTimer = null; |
| 2000 | + const beatAt = x => clamp(beatAtBandOffset(x), 0, beatTotal); |
| 2001 | + const follow = beat => { |
| 2002 | + setLanesFromBeat(beat, 'none'); |
| 2003 | + if (curDir() !== 'v') { // kortstripen hopper til nærmeste tone |
| 2004 | + const i = clamp(Math.round(indexAtBeat(beat)), 0, state.events.length - 1); |
| 2005 | + if (i !== lastIdx) { lastIdx = i; centerCard(cards[i], true); } |
| 2006 | + } |
| 2007 | + }; |
| 2008 | + const snap = () => { |
| 2009 | + if (!state.events.length) return; |
| 2010 | + go(clamp(Math.round(indexAtBeat(beatAt(sX))), 0, state.events.length - 1)); |
| 2011 | + }; |
| 2012 | + el.addEventListener('pointerdown', e => { |
| 2013 | + if (e.pointerType === 'mouse' && e.button !== 0) return; |
| 2014 | + down = true; moved = false; pid = e.pointerId; |
| 2015 | + startX = e.clientX; base = sX; lastIdx = -1; |
| 2016 | + e.stopPropagation(); |
| 2017 | + }); |
| 2018 | + el.addEventListener('pointermove', e => { |
| 2019 | + if (!down || e.pointerId !== pid) return; |
| 2020 | + e.stopPropagation(); |
| 2021 | + const dx = e.clientX - startX; |
| 2022 | + if (!moved) { |
| 2023 | + if (Math.abs(dx) < 5) return; |
| 2024 | + moved = true; |
| 2025 | + el.classList.add('grabbing'); |
| 2026 | + stopIfPlaying(); |
| 2027 | + try { el.setPointerCapture(pid); } catch (err) { /* ignorer */ } |
| 2028 | + } |
| 2029 | + follow(beatAt(base + dx)); |
| 2030 | + e.preventDefault(); |
| 2031 | + }); |
| 2032 | + const end = e => { |
| 2033 | + if (!down) return; |
| 2034 | + down = false; |
| 2035 | + el.classList.remove('grabbing'); |
| 2036 | + try { el.releasePointerCapture(pid); } catch (err) { /* ignorer */ } |
| 2037 | + if (e) e.stopPropagation(); |
| 2038 | + justDragged = moved; |
| 2039 | + if (moved) snap(); |
| 2040 | + moved = false; |
| 2041 | + }; |
| 2042 | + el.addEventListener('pointerup', end); |
| 2043 | + el.addEventListener('pointercancel', end); |
| 2044 | + el.addEventListener('lostpointercapture', end); |
| 2045 | + el.addEventListener('click', e => { |
| 2046 | + if (!justDragged) return; |
| 2047 | + justDragged = false; |
| 2048 | + e.stopPropagation(); |
| 2049 | + e.preventDefault(); |
| 2050 | + }, true); |
| 2051 | + el.addEventListener('wheel', e => { |
| 2052 | + const d = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY; |
| 2053 | + stopIfPlaying(); |
| 2054 | + follow(beatAt(sX - d)); |
| 2055 | + clearTimeout(snapTimer); |
| 2056 | + snapTimer = setTimeout(snap, 140); |
| 2057 | + e.preventDefault(); |
| 2058 | + e.stopPropagation(); |
| 2059 | + }, { passive: false }); |
| 2060 | + el.addEventListener('dragstart', e => e.preventDefault()); |
| 2061 | +} |
| 2062 | + |
1969 | 2063 | function setupStripDrag(el) { |
1970 | 2064 | let down = false, moved = false, pid = null, start = 0, base = 0; |
1971 | 2065 | let justDragged = false, vertical = false, snapTimer = null; |
@@ -2843,6 +2937,7 @@ <h2 class="ptitle" id="ptitle">♪</h2> |
2843 | 2937 | positionBand(true); |
2844 | 2938 | }); |
2845 | 2939 | setupStripDrag($('strip')); |
| 2940 | + setupBandDrag($('sband')); |
2846 | 2941 |
|
2847 | 2942 | rebuildAll(); |
2848 | 2943 | updateTempoBar(); |
|
0 commit comments