hap.mjs - <short description TODO> Copyright (C) 2022 Strudel contributors - see https://codeberg.org/uzu/strudel/src/branch/main/packages/core/hap.mjs This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
9export class Hap { 10 /* 11 Event class, representing a value active during the timespan 12 'part'. This might be a fragment of an event, in which case the 13 timespan will be smaller than the 'whole' timespan, otherwise the 14 two timespans will be the same. The 'part' must never extend outside of the 15 'whole'. If the event represents a continuously changing value 16 then the whole will be returned as None, in which case the given 17 value will have been sampled from the point halfway between the 18 start and end of the 'part' timespan. 19 The context is to store a list of source code locations causing the event.
The word 'Event' is more or less a reserved word in javascript, hence this class is named called 'Hap'.
25 constructor(whole, part, value, context = {}, stateful = false) { 26 this.whole = whole; 27 this.part = part; 28 this.value = value; 29 this.context = context; 30 this.stateful = stateful; 31 if (stateful) { 32 console.assert(typeof this.value === 'function', 'Stateful values must be functions'); 33 } 34 }
36 get duration() { 37 let duration; 38 if (typeof this.value?.duration === 'number') { 39 duration = Fraction(this.value.duration); 40 } else { 41 duration = this.whole.end.sub(this.whole.begin); 42 } 43 if (typeof this.value?.clip === 'number') { 44 return duration.mul(this.value.clip); 45 } 46 return duration; 47 } 48 49 get endClipped() { 50 return this.whole.begin.add(this.duration); 51 } 52 53 isActive(currentTime) { 54 return this.whole.begin <= currentTime && this.endClipped >= currentTime; 55 } 56 57 isInPast(currentTime) { 58 return currentTime > this.endClipped; 59 } 60 isInNearPast(margin, currentTime) { 61 return currentTime - margin <= this.endClipped; 62 } 63 64 isInFuture(currentTime) { 65 return currentTime < this.whole.begin; 66 } 67 isInNearFuture(margin, currentTime) { 68 return currentTime < this.whole.begin && currentTime > this.whole.begin - margin; 69 } 70 isWithinTime(min, max) { 71 return this.whole.begin <= max && this.endClipped >= min; 72 } 73 74 wholeOrPart() { 75 return this.whole ? this.whole : this.part; 76 } 77 78 withSpan(func) { 79 // Returns a new hap with the function f applies to the hap timespan. 80 const whole = this.whole ? func(this.whole) : undefined; 81 return new Hap(whole, func(this.part), this.value, this.context); 82 } 83 84 withValue(func) { 85 // Returns a new hap with the function f applies to the hap value. 86 return new Hap(this.whole, this.part, func(this.value), this.context); 87 } 88 89 hasOnset() { 90 // Test whether the hap contains the onset, i.e that 91 // the beginning of the part is the same as that of the whole timespan.""" 92 return this.whole != undefined && this.whole.begin.equals(this.part.begin); 93 } 94 95 hasTag(tag) { 96 return this.context.tags?.includes(tag); 97 } 98 99 resolveState(state) { 100 if (this.stateful && this.hasOnset()) { 101 console.log('stateful'); 102 const func = this.value; 103 const [newState, newValue] = func(state); 104 return [newState, new Hap(this.whole, this.part, newValue, this.context, false)]; 105 } 106 return [state, this]; 107 } 108 109 spanEquals(other) { 110 return (this.whole == undefined && other.whole == undefined) || this.whole.equals(other.whole); 111 } 112 113 equals(other) { 114 return ( 115 this.spanEquals(other) && 116 this.part.equals(other.part) && 117 // TODO would == be better ?? 118 this.value === other.value 119 ); 120 } 121 122 show(compact = false) { 123 const value = 124 typeof this.value === 'object' 125 ? compact 126 ? JSON.stringify(this.value).slice(1, -1).replaceAll('"', '').replaceAll(',', ' ') 127 : JSON.stringify(this.value) 128 : this.value; 129 var spans = ''; 130 if (this.whole == undefined) { 131 spans = '~' + this.part.show; 132 } else { 133 var is_whole = this.whole.begin.equals(this.part.begin) && this.whole.end.equals(this.part.end); 134 if (!this.whole.begin.equals(this.part.begin)) { 135 spans = this.whole.begin.show() + ' ⇜ '; 136 } 137 if (!is_whole) { 138 spans += '('; 139 } 140 spans += this.part.show(); 141 if (!is_whole) { 142 spans += ')'; 143 } 144 if (!this.whole.end.equals(this.part.end)) { 145 spans += ' ⇝ ' + this.whole.end.show(); 146 } 147 } 148 return '[ ' + spans + ' | ' + value + ' ]'; 149 } 150 151 showWhole(compact = false) { 152 return `${this.whole == undefined ? '~' : this.whole.show()}: ${stringifyValues(this.value, compact)}`; 153 } 154 155 combineContext(b) { 156 const a = this; 157 return { ...a.context, ...b.context, locations: (a.context.locations || []).concat(b.context.locations || []) }; 158 } 159 160 setContext(context) { 161 return new Hap(this.whole, this.part, this.value, context); 162 } 163 164 ensureObjectValue() { 165 /* if (isNote(hap.value)) { 166 // supports primitive hap values that look like notes 167 hap.value = { note: hap.value }; 168 } */ 169 if (typeof this.value !== 'object') { 170 throw new Error( 171 `expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`, 172 'error', 173 ); 174 } 175 } 176} 177 178export default Hap;