From bb17d4ec7cf125559c3d02af4570e76ee54691bc Mon Sep 17 00:00:00 2001 From: Manuel Maestinger Date: Thu, 20 Nov 2025 22:28:14 +0100 Subject: [PATCH 1/6] Visually distinct triplets (issue #125) --- src/ui/pattern-player/pattern-player.vue | 170 ++++++++++++++++++++--- 1 file changed, 154 insertions(+), 16 deletions(-) diff --git a/src/ui/pattern-player/pattern-player.vue b/src/ui/pattern-player/pattern-player.vue index cf17260c5..de1da018e 100644 --- a/src/ui/pattern-player/pattern-player.vue +++ b/src/ui/pattern-player/pattern-player.vue @@ -109,6 +109,41 @@ return strokeEl ? (strokeEl.offsetLeft + strokeEl.offsetWidth * (stroke - strokeIdx)) : 0; }; + const isTriplet = (ptrn, idx: number, time: number) => { + if([3, "3", 6, "6", 9, "9"].includes(time)) return true; + if(![12, "12", 24, "24"].includes(time)) return false; + + const hasNote = i => ptrn[i] !== undefined && ptrn[i] !== null && ptrn[i] !== " "; + const areEmptyBetween = (s, e) => ptrn.slice(s+1, e).every(i => i === " "); + + if(hasNote(idx)) { + for (let step of [2, 4, 8]) { + // CASE 1: idx is on the right note of a triplet + const left = idx - step; + if (left >= 0 && hasNote(left) && areEmptyBetween(left, idx)) return true; + + // CASE 2: idx is on the left note of a triplet + const right = idx + step; + if (right < ptrn.length && hasNote(right) && areEmptyBetween(idx, right)) return true; + } + return false; + } + + // CASE 3: idx is between two notes of a triplet + const next = ptrn.findIndex((v, i) => i >= idx && hasNote(i)); + const prev = ptrn.findLastIndex((v, i) => i <= idx && hasNote(i)); + const nextTriplet = (next == -1 ? false : isTriplet(ptrn, next, time)); + const prevTriplet = (prev == -1 ? false : isTriplet(ptrn, prev, time)); + if (nextTriplet && prevTriplet && (next-prev <= 8)) return true; + + // CASE 4: a triplet starts or ends in the current beat + const inNextBeat = (Math.floor(idx/time) == Math.floor(next/time)); + const inPrevBeat = (Math.floor(idx/time) == Math.floor(prev/time) && prev%time != 0); + if ((inNextBeat && nextTriplet) || (inPrevBeat && prevTriplet)) return true; + + return false; + }; + const getBeatClass = (i: number) => { let positiveI = i; while(positiveI < 0) // Support negative numbers properly @@ -141,6 +176,9 @@ if(originalPattern.value && (originalPattern.value[instrumentKey][realI] || "").trim() != (pattern.value[instrumentKey][realI] || "").trim()) ret.push("has-changes"); + if(isTriplet(pattern.value[instrumentKey], realI, pattern.value.time)) + ret.push("is-triplet"); + return ret; }; @@ -233,7 +271,7 @@
- +
@@ -298,10 +336,16 @@ border-right: 1px solid #ddd; text-align: center; position: relative; + overflow: visible; + padding: 0; &.has-changes { background-color: #fbe8d0; } + + &.is-triplet { + color: #0000ff; + } } .stroke-inner { @@ -357,34 +401,128 @@ white-space: nowrap; } - &.time-2 { - .stroke-inner { - min-width: 5.4ex; + &.time-2 { /* 64px/beat */ + .stroke { max-width: 32px; } + .stroke-inner { min-width: 32px; } + } + + &.time-3 { /* 63px/beat */ + .stroke { max-width: 21px; } + .stroke-inner { min-width: 21px; } + } + + &.time-4 { /* 64px/beat */ + .stroke { max-width: 16px; } + .stroke-inner { min-width: 16px; } + } + + &.time-5 { /* 65px/beat */ + .stroke { max-width: 13px; } + .stroke-inner { min-width: 13px; } + } + + &.time-6 { /* 66px/beat */ + .stroke { max-width: 11px; } + .stroke-inner { min-width: 11px; } + .stroke--2, .stroke--4, + .stroke-0, .stroke-2, .stroke-4 { + border-right: none; + } + } + + &.time-8 { /* 76px/beat */ + .stroke { max-width: 9.5px; } + .stroke-inner { min-width: 9.5px; } + .stroke--2, .stroke--4, .stroke--6, + .stroke-0, .stroke-2, .stroke-4, .stroke-6 { + border-right: none; + } + } + + &.time-9 { /* 76.5px/beat */ + .stroke { max-width: 8.5px; } + .stroke-inner { min-width: 8.5px; } + .stroke--2, .stroke--3, .stroke--5, .stroke--6, .stroke--8, + .stroke-0, .stroke-1, .stroke-3, .stroke-4, .stroke-6, .stroke-7 { + border-right: none; } } - &.time-12 { - .stroke-inner { - min-width: 1ex; + &.time-12 { /* 78px/beat */ + .stroke { max-width: 6.5px; } + .stroke-inner { min-width: 6.5px; } + .stroke--2, .stroke--3, .stroke--5, .stroke--6, + .stroke--8, .stroke--9, .stroke--11, + .stroke-0, .stroke-1, .stroke-3, .stroke-4, + .stroke-6, .stroke-7, .stroke-9, .stroke-10 { + border-right: none; } + .stroke.is-triplet { + &.stroke--4, &.stroke--7, &.stroke--10, + &.stroke-2, &.stroke-5, &.stroke-8 { + border-right: none; + } + &.stroke--5, &.stroke--9, + &.stroke-3, &.stroke-7 { + border-right: 1px solid #ddd; + } + } + } - .stroke-0, .stroke-1, .stroke-3, .stroke-4, .stroke-6, .stroke-7, .stroke-9, .stroke-10 { + &.time-16 { /* 80px/beat */ + .stroke { max-width: 5px; } + .stroke-inner { min-width: 5px; } + .stroke--2, .stroke--3, .stroke--4, + .stroke--6, .stroke--7, .stroke--8, + .stroke--10, .stroke--11, .stroke--12, + .stroke--14, .stroke--15, + .stroke-0, .stroke-1, .stroke-2, + .stroke-4, .stroke-5, .stroke-6, + .stroke-8, .stroke-9, .stroke-10, + .stroke-12, .stroke-13, .stroke-14 { border-right: none; } } - &.time-20 { - .stroke-inner { - min-width: 1ex; + &.time-20 { /* 80px/beat */ + .stroke { max-width: 4px; } + .stroke-inner { min-width: 4px; } + .stroke--2, .stroke--3, .stroke--4, .stroke--5, + .stroke--7, .stroke--8, .stroke--9, .stroke--10, + .stroke--12, .stroke--13, .stroke--14, .stroke--15, + .stroke--17, .stroke--18, .stroke--19, + .stroke-0, .stroke-1, .stroke-2, .stroke-3, + .stroke-5, .stroke-6, .stroke-7, .stroke-8, + .stroke-10, .stroke-11, .stroke-12, .stroke-13, + .stroke-15, .stroke-16, .stroke-17, .stroke-18 { + border-right: none; } + } - .stroke-0, .stroke-1, .stroke-2, .stroke-3, - .stroke-5, .stroke-6, .stroke-7, .stroke-8, - .stroke-10,.stroke-11,.stroke-12,.stroke-13, - .stroke-15,.stroke-16,.stroke-17,.stroke-18 { + &.time-24 { /* 84px/beat */ + .stroke { max-width: 3.5px; } + .stroke-inner { min-width: 3.5px; } + .stroke--2, .stroke--3, .stroke--4, .stroke--5, .stroke--6, + .stroke--8, .stroke--9, .stroke--10, .stroke--11, .stroke--12, + .stroke--14, .stroke--15, .stroke--16, .stroke--17, .stroke--18, + .stroke--20, .stroke--21, .stroke--22, .stroke--23, + .stroke-0, .stroke-1, .stroke-2, .stroke-3, .stroke-4, + .stroke-6, .stroke-7, .stroke-8, .stroke-9, .stroke-10, + .stroke-12, .stroke-13, .stroke-14, .stroke-15, .stroke-16, + .stroke-18, .stroke-19, .stroke-20, .stroke-21, .stroke-22 { border-right: none; } + .stroke.is-triplet { + &.stroke--7, &.stroke--13, &.stroke--19, + &.stroke-5, &.stroke-11, &.stroke-17 { + border-right: none; + } + &.stroke--9, &.stroke--17, + &.stroke-7, &.stroke-15 { + border-right: 1px solid #ddd; + } + } } } } - \ No newline at end of file + From a9d8849174cdefc182d0a1c9ec38e2c0a3f1f805 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Mon, 1 Dec 2025 13:41:52 +0100 Subject: [PATCH 2/6] Simpler way to compute triplets --- src/ui/pattern-player/pattern-player.vue | 55 +++++++++--------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/src/ui/pattern-player/pattern-player.vue b/src/ui/pattern-player/pattern-player.vue index de1da018e..b9d2107a9 100644 --- a/src/ui/pattern-player/pattern-player.vue +++ b/src/ui/pattern-player/pattern-player.vue @@ -109,40 +109,25 @@ return strokeEl ? (strokeEl.offsetLeft + strokeEl.offsetWidth * (stroke - strokeIdx)) : 0; }; - const isTriplet = (ptrn, idx: number, time: number) => { - if([3, "3", 6, "6", 9, "9"].includes(time)) return true; - if(![12, "12", 24, "24"].includes(time)) return false; - - const hasNote = i => ptrn[i] !== undefined && ptrn[i] !== null && ptrn[i] !== " "; - const areEmptyBetween = (s, e) => ptrn.slice(s+1, e).every(i => i === " "); - - if(hasNote(idx)) { - for (let step of [2, 4, 8]) { - // CASE 1: idx is on the right note of a triplet - const left = idx - step; - if (left >= 0 && hasNote(left) && areEmptyBetween(left, idx)) return true; - - // CASE 2: idx is on the left note of a triplet - const right = idx + step; - if (right < ptrn.length && hasNote(right) && areEmptyBetween(idx, right)) return true; - } - return false; - } - - // CASE 3: idx is between two notes of a triplet - const next = ptrn.findIndex((v, i) => i >= idx && hasNote(i)); - const prev = ptrn.findLastIndex((v, i) => i <= idx && hasNote(i)); - const nextTriplet = (next == -1 ? false : isTriplet(ptrn, next, time)); - const prevTriplet = (prev == -1 ? false : isTriplet(ptrn, prev, time)); - if (nextTriplet && prevTriplet && (next-prev <= 8)) return true; - - // CASE 4: a triplet starts or ends in the current beat - const inNextBeat = (Math.floor(idx/time) == Math.floor(next/time)); - const inPrevBeat = (Math.floor(idx/time) == Math.floor(prev/time) && prev%time != 0); - if ((inNextBeat && nextTriplet) || (inPrevBeat && prevTriplet)) return true; - - return false; - }; + + // Is the beat that this stroke is part of a ternary beat? + const isTernaryBeat = (instrumentKey: Instrument, strokeIndex: number) => { + if([3, "3", 6, "6", 9, "9"].includes(pattern.value.time)) return true; + // We only support 12 and 24 time signatures for now. + if(![12, "12", 24, "24"].includes(pattern.value.time)) return false; + + const patternForInstrument = pattern.value[instrumentKey]; + const hasNote = (i: number) => patternForInstrument[i] !== undefined && patternForInstrument[i] !== null && patternForInstrument[i] !== " "; + + const firstStrokeInBeat = Math.floor(strokeIndex/pattern.value.time)*pattern.value.time; // todo upbeats ? + const lastStrokeInBeat = firstStrokeInBeat + pattern.value.time - 1; + for (let strokeNum = firstStrokeInBeat; strokeNum < lastStrokeInBeat+1; strokeNum++) { + if (strokeNum%3 !==0 && hasNote(strokeNum)) { + return true; + } + } + return false; + } const getBeatClass = (i: number) => { let positiveI = i; @@ -176,7 +161,7 @@ if(originalPattern.value && (originalPattern.value[instrumentKey][realI] || "").trim() != (pattern.value[instrumentKey][realI] || "").trim()) ret.push("has-changes"); - if(isTriplet(pattern.value[instrumentKey], realI, pattern.value.time)) + if(isTernaryBeat(instrumentKey, realI)) ret.push("is-triplet"); return ret; From b5b50154e04c68da5f58ad920b03fcea1870e4de Mon Sep 17 00:00:00 2001 From: Manuel Date: Sat, 6 Dec 2025 15:46:51 +0100 Subject: [PATCH 3/6] sub-subdivisions and wider table in compose mode; uniform stroke-colors; shouting-text overlay; border-css cleanup --- src/ui/pattern-player/pattern-player.vue | 196 ++++++++++++++--------- 1 file changed, 123 insertions(+), 73 deletions(-) diff --git a/src/ui/pattern-player/pattern-player.vue b/src/ui/pattern-player/pattern-player.vue index b9d2107a9..172dcfc49 100644 --- a/src/ui/pattern-player/pattern-player.vue +++ b/src/ui/pattern-player/pattern-player.vue @@ -256,7 +256,7 @@
- +
-
@@ -274,7 +274,7 @@ - {{config.strokes[pattern[instrumentKey][i-1]] || '\xa0'}} + {{config.strokes[pattern[instrumentKey][i-1]]}} Date: Sat, 6 Dec 2025 16:01:54 +0100 Subject: [PATCH 4/6] quickfix for triplet calculation; formatting cleanup --- src/ui/pattern-player/pattern-player.vue | 174 +++++++++++------------ 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/src/ui/pattern-player/pattern-player.vue b/src/ui/pattern-player/pattern-player.vue index 172dcfc49..524729962 100644 --- a/src/ui/pattern-player/pattern-player.vue +++ b/src/ui/pattern-player/pattern-player.vue @@ -161,8 +161,8 @@ if(originalPattern.value && (originalPattern.value[instrumentKey][realI] || "").trim() != (pattern.value[instrumentKey][realI] || "").trim()) ret.push("has-changes"); - if(isTernaryBeat(instrumentKey, realI)) - ret.push("is-triplet"); + if(isTernaryBeat(instrumentKey, i)) + ret.push("is-triplet"); return ret; }; @@ -320,34 +320,34 @@ .stroke { text-align: center; position: relative; - overflow: visible; - padding: 0; + overflow: visible; + padding: 0; - .stroke-inner { - color: rgb(33, 37, 41); - } + .stroke-inner { + color: rgb(33, 37, 41); + } - &.is-triplet .stroke-inner { - color: #0000ff; - } + &.is-triplet .stroke-inner { + color: #0000ff; + } &.has-changes { background-color: #fbe8d0; } } - &.compose { - .stroke { - border-right: 1px solid #f3f3f3; - } - } + &.compose { + .stroke { + border-right: 1px solid #f3f3f3; + } + } - &.listen tr:last-child { - .stroke-inner:not(:empty) { - /* Shouting: Hide table lines behind overlapping text */ - background-color: #fff; - } - } + &.listen tr:last-child { + .stroke-inner:not(:empty) { + /* Shouting: Hide table lines behind overlapping text */ + background-color: #fff; + } + } .stroke-inner { display: inline-block; @@ -401,133 +401,133 @@ tbody th, td.instrument-operations { white-space: nowrap; } - } + } .bb-pattern-player { &.time-2 { /* 64px/beat */ - .stroke { max-width: 32px; } + .stroke { max-width: 32px; } .stroke-inner { min-width: 32px; } } &.time-3 { /* 63px/beat */ - .stroke { max-width: 21px; } + .stroke { max-width: 21px; } .stroke-inner { min-width: 21px; } } &.time-4 { /* 64px/beat */ - .stroke { max-width: 16px; } + .stroke { max-width: 16px; } .stroke-inner { min-width: 16px; } } &.time-5 { /* 65px/beat */ - .stroke { max-width: 13px; } + .stroke { max-width: 13px; } .stroke-inner { min-width: 13px; } } &.time-6 { /* 66px/beat */ - .stroke { max-width: 11px; } + .stroke { max-width: 11px; } .stroke-inner { min-width: 11px; } - } - - &.time-8 { /* 76px/beat */ - .stroke { max-width: 9.5px; } + } + + &.time-8 { /* 76px/beat */ + .stroke { max-width: 9.5px; } .stroke-inner { min-width: 9.5px; } - } + } &.time-9 { /* 76.5px/beat */ - .stroke { max-width: 8.5px; } + .stroke { max-width: 8.5px; } .stroke-inner { min-width: 8.5px; } - } + } - &.time-12.compose, - &.time-16.compose, - &.time-20.compose, - &.time-24.compose { - .stroke { max-width: 8px; } + &.time-12.compose, + &.time-16.compose, + &.time-20.compose, + &.time-24.compose { + .stroke { max-width: 8px; } .stroke-inner { min-width: 8px; } - } + } &.time-12.listen { /* 78px/beat */ .stroke { max-width: 6.5px; } .stroke-inner { min-width: 6.5px; } - } - - &.time-16.listen { /* 80px/beat */ + } + + &.time-16.listen { /* 80px/beat */ .stroke { max-width: 5px; } .stroke-inner { min-width: 5px; } - } - - &.time-20.listen { /* 80px/beat */ + } + + &.time-20.listen { /* 80px/beat */ .stroke { max-width: 4px; } .stroke-inner { min-width: 4px; } - } - - &.time-24.listen { /* 84px/beat */ + } + + &.time-24.listen { /* 84px/beat */ .stroke { max-width: 3.5px; } .stroke-inner { min-width: 3.5px; } - } + } &.time-2 { .stroke-0 { - border-right: 1px solid #ddd; + border-right: 1px solid #ddd; } - } + } &.time-3 { .stroke--2, .stroke-0, .stroke-1 { - border-right: 1px solid #ddd; + border-right: 1px solid #ddd; } - } + } &.time-4 { .stroke--2, .stroke--3, - .stroke-0, .stroke-1, .stroke-2 { - border-right: 1px solid #ddd; + .stroke-0, .stroke-1, .stroke-2 { + border-right: 1px solid #ddd; } - } + } &.time-5 { .stroke--2, .stroke--3, .stroke--4, - .stroke-0, .stroke-1, .stroke-2, .stroke-3 { - border-right: 1px solid #ddd; + .stroke-0, .stroke-1, .stroke-2, .stroke-3 { + border-right: 1px solid #ddd; } - } + } &.time-6 { .stroke--3, .stroke--5, .stroke-1, .stroke-3, { - border-right: 1px solid #ddd; + border-right: 1px solid #ddd; } } &.time-8 { .stroke--3, .stroke--5, .stroke--7, .stroke-1, .stroke-3, .stroke-5 { - border-right: 1px solid #ddd; + border-right: 1px solid #ddd; } } &.time-9 { .stroke--4, .stroke--7, .stroke-2, .stroke-5 { - border-right: 1px solid #ddd; + border-right: 1px solid #ddd; } } &.time-12 { - .stroke:not(.is-triplet) { - &.stroke--4, &.stroke--7, &.stroke--10, - &.stroke-2, &.stroke-5, &.stroke-8 { - border-right: 1px solid #ddd; - } - } - .stroke.is-triplet { - &.stroke--5, &.stroke--9, - &.stroke-3, &.stroke-7 { - border-right: 1px solid #ddd; - } - } + .stroke:not(.is-triplet) { + &.stroke--4, &.stroke--7, &.stroke--10, + &.stroke-2, &.stroke-5, &.stroke-8 { + border-right: 1px solid #ddd; + } + } + .stroke.is-triplet { + &.stroke--5, &.stroke--9, + &.stroke-3, &.stroke-7 { + border-right: 1px solid #ddd; + } + } } &.time-16 { @@ -545,18 +545,18 @@ } &.time-24 { - .stroke:not(.is-triplet) { - &.stroke--7, &.stroke--13, &.stroke--19, - &.stroke-5, &.stroke-11, &.stroke-17 { - border-right: 1px solid #ddd; - } - } - .stroke.is-triplet { - &.stroke--9, &.stroke--17, - &.stroke-7, &.stroke-15 { - border-right: 1px solid #ddd; - } - } + .stroke:not(.is-triplet) { + &.stroke--7, &.stroke--13, &.stroke--19, + &.stroke-5, &.stroke-11, &.stroke-17 { + border-right: 1px solid #ddd; + } + } + .stroke.is-triplet { + &.stroke--9, &.stroke--17, + &.stroke-7, &.stroke-15 { + border-right: 1px solid #ddd; + } + } } } } From 3edd1fbb0f20062bed15e300390f62781a7c183b Mon Sep 17 00:00:00 2001 From: Manuel Date: Thu, 11 Dec 2025 10:54:27 +0100 Subject: [PATCH 5/6] even more exact spacing --- src/ui/pattern-player/pattern-player.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ui/pattern-player/pattern-player.vue b/src/ui/pattern-player/pattern-player.vue index 524729962..7c18cb659 100644 --- a/src/ui/pattern-player/pattern-player.vue +++ b/src/ui/pattern-player/pattern-player.vue @@ -342,6 +342,12 @@ } } + &.listen { + .stroke { + border-right: 1px solid #ffffff; + } + } + &.listen tr:last-child { .stroke-inner:not(:empty) { /* Shouting: Hide table lines behind overlapping text */ From 33a4342f8dcef0afd6582c60ed0c73062a2d0db6 Mon Sep 17 00:00:00 2001 From: Estelle Comment Date: Thu, 11 Dec 2025 16:25:03 +0100 Subject: [PATCH 6/6] More efficient triplet computation, and pink color --- src/ui/pattern-player/pattern-player.vue | 56 +++++++++++++++++------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/ui/pattern-player/pattern-player.vue b/src/ui/pattern-player/pattern-player.vue index 7c18cb659..8a8dc428e 100644 --- a/src/ui/pattern-player/pattern-player.vue +++ b/src/ui/pattern-player/pattern-player.vue @@ -110,24 +110,51 @@ }; - // Is the beat that this stroke is part of a ternary beat? - const isTernaryBeat = (instrumentKey: Instrument, strokeIndex: number) => { - if([3, "3", 6, "6", 9, "9"].includes(pattern.value.time)) return true; - // We only support 12 and 24 time signatures for now. - if(![12, "12", 24, "24"].includes(pattern.value.time)) return false; - + // If there is no upbeat, beatI goes from 0 to length - 1. + // If there is an upbeat, beatI goes from floor(-upbeat/time) to length - 1. + const isTernaryBeat = (instrumentKey: Instrument, beatI: number) => { const patternForInstrument = pattern.value[instrumentKey]; - const hasNote = (i: number) => patternForInstrument[i] !== undefined && patternForInstrument[i] !== null && patternForInstrument[i] !== " "; - - const firstStrokeInBeat = Math.floor(strokeIndex/pattern.value.time)*pattern.value.time; // todo upbeats ? + const hasNote = (j: number) => { + // pattern is an array so its indexes start at 0, not -upbeat. + const realJ = j + pattern.value.upbeat; + return patternForInstrument[realJ] !== undefined && patternForInstrument[realJ] !== null && patternForInstrument[realJ] !== " "; + } + const firstStrokeInBeat = beatI * pattern.value.time; const lastStrokeInBeat = firstStrokeInBeat + pattern.value.time - 1; - for (let strokeNum = firstStrokeInBeat; strokeNum < lastStrokeInBeat+1; strokeNum++) { + for (let strokeNum = firstStrokeInBeat; strokeNum <= lastStrokeInBeat; strokeNum++) { if (strokeNum%3 !==0 && hasNote(strokeNum)) { return true; } } return false; } + const beatIFromStrokeI = (strokeI: number) => { + return Math.floor(strokeI/pattern.value.time); + } + // For each instrument, for each stroke, return the CSS class to apply to the stroke : "" or "is-triplet". + const ternaryCSSClasses = computed(() => { + const ret = {} as Record>; + for (let instrumentKey of config.instrumentKeys) { + ret[instrumentKey] = {}; + } + // We only support 12 and 24 time signatures for now. + if(![12, "12", 24, "24"].includes(pattern.value.time)) return ret; + const ternaryClassesForInstrument = (instrumentKey: Instrument) => { + const areBeatsTernary: Record = {}; + for (let beatI = beatIFromStrokeI(-pattern.value.upbeat); beatI < pattern.value.length; beatI++) { + areBeatsTernary[beatI] = isTernaryBeat(instrumentKey, beatI); + } + const ternaryCSSClassesForInstrument: Record = {}; + for (let strokeI = -pattern.value.upbeat; strokeI < pattern.value.length*pattern.value.time + pattern.value.upbeat; strokeI++) { + ternaryCSSClassesForInstrument[strokeI] = areBeatsTernary[beatIFromStrokeI(strokeI)] ? 'is-triplet' : ''; + } + return ternaryCSSClassesForInstrument; + } + for (let instrumentKey of config.instrumentKeys) { + ret[instrumentKey] = ternaryClassesForInstrument(instrumentKey); + } + return ret; + }); const getBeatClass = (i: number) => { let positiveI = i; @@ -143,6 +170,8 @@ }; const getStrokeClass = (realI: number, instrumentKey: Instrument) => { + // realI starts at 0, even if there is an upbeat. + // i goes from -upbeat to length*time - 1. let i = realI - pattern.value.upbeat; const ret = [ @@ -161,9 +190,6 @@ if(originalPattern.value && (originalPattern.value[instrumentKey][realI] || "").trim() != (pattern.value[instrumentKey][realI] || "").trim()) ret.push("has-changes"); - if(isTernaryBeat(instrumentKey, i)) - ret.push("is-triplet"); - return ret; }; @@ -273,7 +299,7 @@ + {{config.strokes[pattern[instrumentKey][i-1]]}}