hap.mjsannotatedhap.mjssource178 lines · 5.9 KB · raw
1/*
2hap.mjs - <short description TODO>
3Copyright (C) 2022 Strudel contributors - see <https://codeberg.org/uzu/strudel/src/branch/main/packages/core/hap.mjs>
4This 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/>.
5*/
6import Fraction from './fraction.mjs';
7import { stringifyValues } from './util.mjs';
8
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.
20
21      The word 'Event' is more or less a reserved word in javascript, hence this
22      class is named called 'Hap'.
23      */
24
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  }
35
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;