@dawcore/components 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -174,7 +174,7 @@ var DawTrackElement = class extends LitElement2 {
174
174
  this.pan = 0;
175
175
  this.muted = false;
176
176
  this.soloed = false;
177
- this.renderMode = "waveform";
177
+ this._renderMode = "waveform";
178
178
  this.spectrogramConfig = null;
179
179
  this.trackId = crypto.randomUUID();
180
180
  // Track removal is detected by the editor's MutationObserver,
@@ -182,6 +182,21 @@ var DawTrackElement = class extends LitElement2 {
182
182
  // cannot bubble events to ancestors).
183
183
  this._hasRendered = false;
184
184
  }
185
+ get renderMode() {
186
+ return this._renderMode;
187
+ }
188
+ set renderMode(value) {
189
+ const old = this._renderMode;
190
+ let next = value;
191
+ if (next === "both") {
192
+ console.warn(
193
+ `[dawcore] <daw-track render-mode="both"> is not yet supported; falling back to 'spectrogram'`
194
+ );
195
+ next = "spectrogram";
196
+ }
197
+ this._renderMode = next;
198
+ this.requestUpdate("renderMode", old);
199
+ }
185
200
  // Light DOM so <daw-clip> children are queryable.
186
201
  createRenderRoot() {
187
202
  return this;
@@ -244,8 +259,8 @@ __decorateClass([
244
259
  property2({ type: Boolean })
245
260
  ], DawTrackElement.prototype, "soloed", 2);
246
261
  __decorateClass([
247
- property2({ attribute: "render-mode" })
248
- ], DawTrackElement.prototype, "renderMode", 2);
262
+ property2({ attribute: "render-mode", noAccessor: true })
263
+ ], DawTrackElement.prototype, "renderMode", 1);
249
264
  __decorateClass([
250
265
  property2({ attribute: false })
251
266
  ], DawTrackElement.prototype, "spectrogramConfig", 2);
@@ -1181,8 +1196,8 @@ DawStopButtonElement = __decorateClass([
1181
1196
  ], DawStopButtonElement);
1182
1197
 
1183
1198
  // src/elements/daw-editor.ts
1184
- import { LitElement as LitElement10, html as html9, css as css9 } from "lit";
1185
- import { customElement as customElement12, property as property8, state as state3 } from "lit/decorators.js";
1199
+ import { LitElement as LitElement11, html as html10, css as css10 } from "lit";
1200
+ import { customElement as customElement13, property as property9, state as state3 } from "lit/decorators.js";
1186
1201
 
1187
1202
  // src/types.ts
1188
1203
  function isDomClip(desc) {
@@ -1666,10 +1681,268 @@ var PeakPipeline = class {
1666
1681
  }
1667
1682
  };
1668
1683
 
1669
- // src/elements/daw-track-controls.ts
1684
+ // src/elements/daw-ruler.ts
1670
1685
  import { LitElement as LitElement8, html as html7, css as css6 } from "lit";
1671
1686
  import { customElement as customElement10, property as property6 } from "lit/decorators.js";
1672
- var DawTrackControlsElement = class extends LitElement8 {
1687
+
1688
+ // src/utils/time-format.ts
1689
+ function formatTime(milliseconds) {
1690
+ const seconds = Math.floor(milliseconds / 1e3);
1691
+ const s = seconds % 60;
1692
+ const m = (seconds - s) / 60;
1693
+ return `${m}:${String(s).padStart(2, "0")}`;
1694
+ }
1695
+
1696
+ // src/utils/smart-scale.ts
1697
+ var timeinfo = /* @__PURE__ */ new Map([
1698
+ [700, { marker: 1e3, bigStep: 500, smallStep: 100 }],
1699
+ [1500, { marker: 2e3, bigStep: 1e3, smallStep: 200 }],
1700
+ [2500, { marker: 2e3, bigStep: 1e3, smallStep: 500 }],
1701
+ [5e3, { marker: 5e3, bigStep: 1e3, smallStep: 500 }],
1702
+ [1e4, { marker: 1e4, bigStep: 5e3, smallStep: 1e3 }],
1703
+ [12e3, { marker: 15e3, bigStep: 5e3, smallStep: 1e3 }],
1704
+ [Infinity, { marker: 3e4, bigStep: 1e4, smallStep: 5e3 }]
1705
+ ]);
1706
+ function getScaleInfo(samplesPerPixel) {
1707
+ for (const [resolution, config] of timeinfo) {
1708
+ if (samplesPerPixel < resolution) {
1709
+ return config;
1710
+ }
1711
+ }
1712
+ return { marker: 3e4, bigStep: 1e4, smallStep: 5e3 };
1713
+ }
1714
+ function computeTemporalTicks(samplesPerPixel, sampleRate, duration, rulerHeight) {
1715
+ const widthX = Math.ceil(duration * sampleRate / samplesPerPixel);
1716
+ const config = getScaleInfo(samplesPerPixel);
1717
+ const { marker, bigStep, smallStep } = config;
1718
+ const canvasInfo = /* @__PURE__ */ new Map();
1719
+ const labels = [];
1720
+ const pixPerSec = sampleRate / samplesPerPixel;
1721
+ for (let counter = 0; ; counter += smallStep) {
1722
+ const pix = Math.floor(counter / 1e3 * pixPerSec);
1723
+ if (pix >= widthX) break;
1724
+ if (counter % marker === 0) {
1725
+ canvasInfo.set(pix, rulerHeight);
1726
+ labels.push({ pix, text: formatTime(counter) });
1727
+ } else if (counter % bigStep === 0) {
1728
+ canvasInfo.set(pix, Math.floor(rulerHeight / 2));
1729
+ } else if (counter % smallStep === 0) {
1730
+ canvasInfo.set(pix, Math.floor(rulerHeight / 5));
1731
+ }
1732
+ }
1733
+ return { widthX, canvasInfo, labels };
1734
+ }
1735
+
1736
+ // src/utils/musical-tick-cache.ts
1737
+ import { computeMusicalTicks } from "@waveform-playlist/core";
1738
+ var cachedParams = null;
1739
+ var cachedResult = null;
1740
+ function meterEntriesMatch(a, b) {
1741
+ if (a.length !== b.length) return false;
1742
+ for (let i = 0; i < a.length; i++) {
1743
+ if (a[i].tick !== b[i].tick || a[i].numerator !== b[i].numerator || a[i].denominator !== b[i].denominator)
1744
+ return false;
1745
+ }
1746
+ return true;
1747
+ }
1748
+ function paramsMatch(a, b) {
1749
+ return a.ticksPerPixel === b.ticksPerPixel && a.startPixel === b.startPixel && a.endPixel === b.endPixel && meterEntriesMatch(a.meterEntries, b.meterEntries) && (a.ppqn ?? 960) === (b.ppqn ?? 960);
1750
+ }
1751
+ function getCachedMusicalTicks(params) {
1752
+ if (cachedParams && cachedResult && paramsMatch(cachedParams, params)) {
1753
+ return cachedResult;
1754
+ }
1755
+ cachedResult = computeMusicalTicks(params);
1756
+ cachedParams = {
1757
+ ...params,
1758
+ meterEntries: params.meterEntries.map((e) => ({ ...e }))
1759
+ };
1760
+ return cachedResult;
1761
+ }
1762
+
1763
+ // src/elements/daw-ruler.ts
1764
+ var MAX_CANVAS_WIDTH3 = 1e3;
1765
+ var DawRulerElement = class extends LitElement8 {
1766
+ constructor() {
1767
+ super(...arguments);
1768
+ this.samplesPerPixel = 1024;
1769
+ this.sampleRate = 48e3;
1770
+ this.duration = 0;
1771
+ this.rulerHeight = 30;
1772
+ this.scaleMode = "temporal";
1773
+ this.ticksPerPixel = 4;
1774
+ this.meterEntries = [
1775
+ { tick: 0, numerator: 4, denominator: 4 }
1776
+ ];
1777
+ this.ppqn = 960;
1778
+ this.totalWidth = 0;
1779
+ this._tickData = null;
1780
+ this._musicalTickData = null;
1781
+ }
1782
+ willUpdate() {
1783
+ if (this.scaleMode === "beats" && this.totalWidth > 0) {
1784
+ this._musicalTickData = getCachedMusicalTicks({
1785
+ meterEntries: this.meterEntries,
1786
+ ticksPerPixel: this.ticksPerPixel,
1787
+ startPixel: 0,
1788
+ endPixel: this.totalWidth,
1789
+ ppqn: this.ppqn
1790
+ });
1791
+ this._tickData = null;
1792
+ } else if (this.duration > 0 || this.totalWidth > 0) {
1793
+ const widthDerivedDuration = this.totalWidth * this.samplesPerPixel / this.sampleRate;
1794
+ const effectiveDuration = Math.max(this.duration, widthDerivedDuration);
1795
+ this._musicalTickData = null;
1796
+ this._tickData = computeTemporalTicks(
1797
+ this.samplesPerPixel,
1798
+ this.sampleRate,
1799
+ effectiveDuration,
1800
+ this.rulerHeight
1801
+ );
1802
+ } else {
1803
+ this._musicalTickData = null;
1804
+ this._tickData = null;
1805
+ }
1806
+ }
1807
+ render() {
1808
+ const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
1809
+ if (widthX <= 0) return html7``;
1810
+ const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH3);
1811
+ const indices = Array.from({ length: totalChunks }, (_, i) => i);
1812
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1813
+ const beatsLabels = this.scaleMode === "beats" ? this._musicalTickData?.ticks.filter((t) => t.label) ?? [] : [];
1814
+ const temporalLabels = this.scaleMode !== "beats" ? this._tickData?.labels ?? [] : [];
1815
+ return html7`
1816
+ <div class="container" style="width: ${widthX}px; height: ${this.rulerHeight}px;">
1817
+ ${indices.map((i) => {
1818
+ const width = Math.min(MAX_CANVAS_WIDTH3, widthX - i * MAX_CANVAS_WIDTH3);
1819
+ return html7`
1820
+ <canvas
1821
+ data-index=${i}
1822
+ width=${width * dpr}
1823
+ height=${this.rulerHeight * dpr}
1824
+ style="left: ${i * MAX_CANVAS_WIDTH3}px; width: ${width}px; height: ${this.rulerHeight}px;"
1825
+ ></canvas>
1826
+ `;
1827
+ })}
1828
+ ${this.scaleMode === "beats" ? beatsLabels.map(
1829
+ (t) => html7`<span
1830
+ class="label ${t.pixel > 0 ? "centered" : ""}"
1831
+ style="left: ${t.pixel > 0 ? t.pixel : t.pixel + 4}px;"
1832
+ >${t.label}</span
1833
+ >`
1834
+ ) : temporalLabels.map(
1835
+ ({ pix, text }) => html7`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
1836
+ )}
1837
+ </div>
1838
+ `;
1839
+ }
1840
+ updated() {
1841
+ this._drawTicks();
1842
+ }
1843
+ _drawTicks() {
1844
+ const canvases = this.shadowRoot?.querySelectorAll("canvas");
1845
+ if (!canvases) return;
1846
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1847
+ const rulerColor = getComputedStyle(this).getPropertyValue("--daw-ruler-color").trim() || "#c49a6c";
1848
+ const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
1849
+ for (const canvas of canvases) {
1850
+ const idx = Number(canvas.dataset.index);
1851
+ const ctx = canvas.getContext("2d");
1852
+ if (!ctx) continue;
1853
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH3, widthX - idx * MAX_CANVAS_WIDTH3);
1854
+ const globalOffset = idx * MAX_CANVAS_WIDTH3;
1855
+ ctx.resetTransform();
1856
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1857
+ ctx.scale(dpr, dpr);
1858
+ ctx.strokeStyle = rulerColor;
1859
+ ctx.lineWidth = 1;
1860
+ if (this.scaleMode === "beats" && this._musicalTickData) {
1861
+ const h = this.rulerHeight;
1862
+ for (const tick of this._musicalTickData.ticks) {
1863
+ const localX = tick.pixel - globalOffset;
1864
+ if (localX < 0 || localX >= canvasWidth) continue;
1865
+ const tickH = tick.type === "major" ? h * 0.6 : tick.type === "minor" ? h * 0.35 : h * 0.15;
1866
+ ctx.globalAlpha = tick.type === "major" ? 1 : 0.5;
1867
+ ctx.beginPath();
1868
+ ctx.moveTo(localX + 0.5, h);
1869
+ ctx.lineTo(localX + 0.5, h - tickH);
1870
+ ctx.stroke();
1871
+ }
1872
+ ctx.globalAlpha = 1;
1873
+ } else if (this._tickData) {
1874
+ for (const [pix, height] of this._tickData.canvasInfo) {
1875
+ const localX = pix - globalOffset;
1876
+ if (localX < 0 || localX >= canvasWidth) continue;
1877
+ ctx.beginPath();
1878
+ ctx.moveTo(localX + 0.5, this.rulerHeight);
1879
+ ctx.lineTo(localX + 0.5, this.rulerHeight - height);
1880
+ ctx.stroke();
1881
+ }
1882
+ }
1883
+ }
1884
+ }
1885
+ };
1886
+ DawRulerElement.styles = css6`
1887
+ :host {
1888
+ display: block;
1889
+ position: relative;
1890
+ background: var(--daw-ruler-background, #0f0f1a);
1891
+ }
1892
+ .container {
1893
+ position: relative;
1894
+ }
1895
+ canvas {
1896
+ position: absolute;
1897
+ top: 0;
1898
+ }
1899
+ .label {
1900
+ position: absolute;
1901
+ font-size: 0.7rem;
1902
+ line-height: 1;
1903
+ white-space: nowrap;
1904
+ color: var(--daw-ruler-color, #c49a6c);
1905
+ top: 1px;
1906
+ }
1907
+ .label.centered {
1908
+ transform: translateX(-50%);
1909
+ }
1910
+ `;
1911
+ __decorateClass([
1912
+ property6({ type: Number, attribute: false })
1913
+ ], DawRulerElement.prototype, "samplesPerPixel", 2);
1914
+ __decorateClass([
1915
+ property6({ type: Number, attribute: false })
1916
+ ], DawRulerElement.prototype, "sampleRate", 2);
1917
+ __decorateClass([
1918
+ property6({ type: Number, attribute: false })
1919
+ ], DawRulerElement.prototype, "duration", 2);
1920
+ __decorateClass([
1921
+ property6({ type: Number, attribute: false })
1922
+ ], DawRulerElement.prototype, "rulerHeight", 2);
1923
+ __decorateClass([
1924
+ property6({ type: String, attribute: false })
1925
+ ], DawRulerElement.prototype, "scaleMode", 2);
1926
+ __decorateClass([
1927
+ property6({ type: Number, attribute: false })
1928
+ ], DawRulerElement.prototype, "ticksPerPixel", 2);
1929
+ __decorateClass([
1930
+ property6({ attribute: false })
1931
+ ], DawRulerElement.prototype, "meterEntries", 2);
1932
+ __decorateClass([
1933
+ property6({ type: Number, attribute: false })
1934
+ ], DawRulerElement.prototype, "ppqn", 2);
1935
+ __decorateClass([
1936
+ property6({ type: Number, attribute: false })
1937
+ ], DawRulerElement.prototype, "totalWidth", 2);
1938
+ DawRulerElement = __decorateClass([
1939
+ customElement10("daw-ruler")
1940
+ ], DawRulerElement);
1941
+
1942
+ // src/elements/daw-track-controls.ts
1943
+ import { LitElement as LitElement9, html as html8, css as css7 } from "lit";
1944
+ import { customElement as customElement11, property as property7 } from "lit/decorators.js";
1945
+ var DawTrackControlsElement = class extends LitElement9 {
1673
1946
  constructor() {
1674
1947
  super(...arguments);
1675
1948
  this.trackId = null;
@@ -1703,9 +1976,20 @@ var DawTrackControlsElement = class extends LitElement8 {
1703
1976
  );
1704
1977
  };
1705
1978
  }
1706
- _dispatchControl(prop, value) {
1707
- if (!this.trackId) return;
1708
- this.dispatchEvent(
1979
+ firstUpdated() {
1980
+ requestAnimationFrame(() => {
1981
+ if (!this.isConnected) return;
1982
+ const rect = this.getBoundingClientRect();
1983
+ if (rect.width > 0 && rect.height === 0) {
1984
+ console.warn(
1985
+ "[dawcore] <daw-track-controls> has zero height: container-type: size requires an explicit height on the element (the editor sets one automatically; standalone usage must too). The controls are currently invisible."
1986
+ );
1987
+ }
1988
+ });
1989
+ }
1990
+ _dispatchControl(prop, value) {
1991
+ if (!this.trackId) return;
1992
+ this.dispatchEvent(
1709
1993
  new CustomEvent("daw-track-control", {
1710
1994
  bubbles: true,
1711
1995
  composed: true,
@@ -1717,7 +2001,7 @@ var DawTrackControlsElement = class extends LitElement8 {
1717
2001
  const volPercent = Math.round(this.volume * 100);
1718
2002
  const panPercent = Math.round(Math.abs(this.pan) * 100);
1719
2003
  const panDisplay = this.pan === 0 ? "C" : (this.pan > 0 ? "R" : "L") + panPercent;
1720
- return html7`
2004
+ return html8`
1721
2005
  <div class="header">
1722
2006
  <span class="name" title=${this.trackName}>${this.trackName || "Untitled"}</span>
1723
2007
  <button class="remove-btn" @click=${this._onRemoveClick} title="Remove track">
@@ -1736,7 +2020,7 @@ var DawTrackControlsElement = class extends LitElement8 {
1736
2020
  S
1737
2021
  </button>
1738
2022
  </div>
1739
- <div class="slider-row">
2023
+ <div class="slider-row vol-row">
1740
2024
  <span class="slider-label">
1741
2025
  <span class="slider-label-name">Vol</span>
1742
2026
  <span class="slider-label-value">${volPercent}%</span>
@@ -1750,7 +2034,7 @@ var DawTrackControlsElement = class extends LitElement8 {
1750
2034
  @input=${this._onVolumeInput}
1751
2035
  />
1752
2036
  </div>
1753
- <div class="slider-row">
2037
+ <div class="slider-row pan-row">
1754
2038
  <span class="slider-label">
1755
2039
  <span class="slider-label-name">Pan</span>
1756
2040
  <span class="slider-label-value">${panDisplay}</span>
@@ -1767,7 +2051,7 @@ var DawTrackControlsElement = class extends LitElement8 {
1767
2051
  `;
1768
2052
  }
1769
2053
  };
1770
- DawTrackControlsElement.styles = css6`
2054
+ DawTrackControlsElement.styles = css7`
1771
2055
  :host {
1772
2056
  display: flex;
1773
2057
  flex-direction: column;
@@ -1780,6 +2064,7 @@ DawTrackControlsElement.styles = css6`
1780
2064
  font-family: system-ui, sans-serif;
1781
2065
  font-size: 11px;
1782
2066
  overflow: hidden;
2067
+ container-type: size;
1783
2068
  }
1784
2069
  .header {
1785
2070
  display: flex;
@@ -1899,64 +2184,52 @@ DawTrackControlsElement.styles = css6`
1899
2184
  border: none;
1900
2185
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
1901
2186
  }
2187
+ /* Compact modes: drop sliders when the row is too short for the full
2188
+ stack. Thresholds are CONTENT-BOX heights — the host is border-box
2189
+ with 12px vertical padding + 1px border, so an editor-given height H
2190
+ enters compact mode at H <= 89px (Pan hidden) and H <= 73px (Vol also
2191
+ hidden). NOTE: container-type: size requires an explicit height on
2192
+ the host — the editor always provides one; standalone consumers must
2193
+ too (see the firstUpdated guard). */
2194
+ @container (max-height: 76px) {
2195
+ .pan-row {
2196
+ display: none;
2197
+ }
2198
+ }
2199
+ @container (max-height: 60px) {
2200
+ .vol-row {
2201
+ display: none;
2202
+ }
2203
+ }
1902
2204
  `;
1903
2205
  __decorateClass([
1904
- property6({ attribute: false })
2206
+ property7({ attribute: false })
1905
2207
  ], DawTrackControlsElement.prototype, "trackId", 2);
1906
2208
  __decorateClass([
1907
- property6({ attribute: false })
2209
+ property7({ attribute: false })
1908
2210
  ], DawTrackControlsElement.prototype, "trackName", 2);
1909
2211
  __decorateClass([
1910
- property6({ type: Number, attribute: false })
2212
+ property7({ type: Number, attribute: false })
1911
2213
  ], DawTrackControlsElement.prototype, "volume", 2);
1912
2214
  __decorateClass([
1913
- property6({ type: Number, attribute: false })
2215
+ property7({ type: Number, attribute: false })
1914
2216
  ], DawTrackControlsElement.prototype, "pan", 2);
1915
2217
  __decorateClass([
1916
- property6({ type: Boolean, attribute: false })
2218
+ property7({ type: Boolean, attribute: false })
1917
2219
  ], DawTrackControlsElement.prototype, "muted", 2);
1918
2220
  __decorateClass([
1919
- property6({ type: Boolean, attribute: false })
2221
+ property7({ type: Boolean, attribute: false })
1920
2222
  ], DawTrackControlsElement.prototype, "soloed", 2);
1921
2223
  DawTrackControlsElement = __decorateClass([
1922
- customElement10("daw-track-controls")
2224
+ customElement11("daw-track-controls")
1923
2225
  ], DawTrackControlsElement);
1924
2226
 
1925
2227
  // src/elements/daw-grid.ts
1926
- import { LitElement as LitElement9, html as html8, css as css7 } from "lit";
1927
- import { customElement as customElement11, property as property7 } from "lit/decorators.js";
2228
+ import { LitElement as LitElement10, html as html9, css as css8 } from "lit";
2229
+ import { customElement as customElement12, property as property8 } from "lit/decorators.js";
1928
2230
  import { MIN_PIXELS_PER_UNIT } from "@waveform-playlist/core";
1929
-
1930
- // src/utils/musical-tick-cache.ts
1931
- import { computeMusicalTicks } from "@waveform-playlist/core";
1932
- var cachedParams = null;
1933
- var cachedResult = null;
1934
- function meterEntriesMatch(a, b) {
1935
- if (a.length !== b.length) return false;
1936
- for (let i = 0; i < a.length; i++) {
1937
- if (a[i].tick !== b[i].tick || a[i].numerator !== b[i].numerator || a[i].denominator !== b[i].denominator)
1938
- return false;
1939
- }
1940
- return true;
1941
- }
1942
- function paramsMatch(a, b) {
1943
- return a.ticksPerPixel === b.ticksPerPixel && a.startPixel === b.startPixel && a.endPixel === b.endPixel && meterEntriesMatch(a.meterEntries, b.meterEntries) && (a.ppqn ?? 960) === (b.ppqn ?? 960);
1944
- }
1945
- function getCachedMusicalTicks(params) {
1946
- if (cachedParams && cachedResult && paramsMatch(cachedParams, params)) {
1947
- return cachedResult;
1948
- }
1949
- cachedResult = computeMusicalTicks(params);
1950
- cachedParams = {
1951
- ...params,
1952
- meterEntries: params.meterEntries.map((e) => ({ ...e }))
1953
- };
1954
- return cachedResult;
1955
- }
1956
-
1957
- // src/elements/daw-grid.ts
1958
- var MAX_CANVAS_WIDTH3 = 1e3;
1959
- var DawGridElement = class extends LitElement9 {
2231
+ var MAX_CANVAS_WIDTH4 = 1e3;
2232
+ var DawGridElement = class extends LitElement10 {
1960
2233
  constructor() {
1961
2234
  super(...arguments);
1962
2235
  this.ticksPerPixel = 24;
@@ -1984,25 +2257,25 @@ var DawGridElement = class extends LitElement9 {
1984
2257
  }
1985
2258
  }
1986
2259
  render() {
1987
- if (!this._tickData) return html8``;
2260
+ if (!this._tickData) return html9``;
1988
2261
  const totalWidth = this.length;
1989
2262
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1990
2263
  const indices = getVisibleChunkIndices(
1991
2264
  totalWidth,
1992
- MAX_CANVAS_WIDTH3,
2265
+ MAX_CANVAS_WIDTH4,
1993
2266
  this.visibleStart,
1994
2267
  this.visibleEnd
1995
2268
  );
1996
- return html8`
2269
+ return html9`
1997
2270
  <div class="container" style="width: ${totalWidth}px; height: ${this.height}px;">
1998
2271
  ${indices.map((i) => {
1999
- const width = Math.min(MAX_CANVAS_WIDTH3, totalWidth - i * MAX_CANVAS_WIDTH3);
2000
- return html8`
2272
+ const width = Math.min(MAX_CANVAS_WIDTH4, totalWidth - i * MAX_CANVAS_WIDTH4);
2273
+ return html9`
2001
2274
  <canvas
2002
2275
  data-index=${i}
2003
2276
  width=${width * dpr}
2004
2277
  height=${this.height * dpr}
2005
- style="left: ${i * MAX_CANVAS_WIDTH3}px; width: ${width}px; height: ${this.height}px;"
2278
+ style="left: ${i * MAX_CANVAS_WIDTH4}px; width: ${width}px; height: ${this.height}px;"
2006
2279
  ></canvas>
2007
2280
  `;
2008
2281
  })}
@@ -2026,8 +2299,8 @@ var DawGridElement = class extends LitElement9 {
2026
2299
  const idx = Number(canvas.dataset.index);
2027
2300
  const ctx = canvas.getContext("2d");
2028
2301
  if (!ctx) continue;
2029
- const chunkLeft = idx * MAX_CANVAS_WIDTH3;
2030
- const canvasWidth = Math.min(MAX_CANVAS_WIDTH3, this.length - chunkLeft);
2302
+ const chunkLeft = idx * MAX_CANVAS_WIDTH4;
2303
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH4, this.length - chunkLeft);
2031
2304
  ctx.resetTransform();
2032
2305
  ctx.clearRect(0, 0, canvas.width, canvas.height);
2033
2306
  ctx.scale(dpr, dpr);
@@ -2058,7 +2331,7 @@ var DawGridElement = class extends LitElement9 {
2058
2331
  }
2059
2332
  }
2060
2333
  };
2061
- DawGridElement.styles = css7`
2334
+ DawGridElement.styles = css8`
2062
2335
  :host {
2063
2336
  display: block;
2064
2337
  position: absolute;
@@ -2076,33 +2349,33 @@ DawGridElement.styles = css7`
2076
2349
  }
2077
2350
  `;
2078
2351
  __decorateClass([
2079
- property7({ type: Number, attribute: false })
2352
+ property8({ type: Number, attribute: false })
2080
2353
  ], DawGridElement.prototype, "ticksPerPixel", 2);
2081
2354
  __decorateClass([
2082
- property7({ attribute: false })
2355
+ property8({ attribute: false })
2083
2356
  ], DawGridElement.prototype, "meterEntries", 2);
2084
2357
  __decorateClass([
2085
- property7({ type: Number, attribute: false })
2358
+ property8({ type: Number, attribute: false })
2086
2359
  ], DawGridElement.prototype, "ppqn", 2);
2087
2360
  __decorateClass([
2088
- property7({ type: Number, attribute: false })
2361
+ property8({ type: Number, attribute: false })
2089
2362
  ], DawGridElement.prototype, "visibleStart", 2);
2090
2363
  __decorateClass([
2091
- property7({ type: Number, attribute: false })
2364
+ property8({ type: Number, attribute: false })
2092
2365
  ], DawGridElement.prototype, "visibleEnd", 2);
2093
2366
  __decorateClass([
2094
- property7({ type: Number, attribute: false })
2367
+ property8({ type: Number, attribute: false })
2095
2368
  ], DawGridElement.prototype, "length", 2);
2096
2369
  __decorateClass([
2097
- property7({ type: Number, attribute: false })
2370
+ property8({ type: Number, attribute: false })
2098
2371
  ], DawGridElement.prototype, "height", 2);
2099
2372
  DawGridElement = __decorateClass([
2100
- customElement11("daw-grid")
2373
+ customElement12("daw-grid")
2101
2374
  ], DawGridElement);
2102
2375
 
2103
2376
  // src/styles/theme.ts
2104
- import { css as css8 } from "lit";
2105
- var hostStyles = css8`
2377
+ import { css as css9 } from "lit";
2378
+ var hostStyles = css9`
2106
2379
  :host {
2107
2380
  --daw-wave-color: #c49a6c;
2108
2381
  --daw-progress-color: #63c75f;
@@ -2118,7 +2391,7 @@ var hostStyles = css8`
2118
2391
  --daw-clip-header-text: #e0d4c8;
2119
2392
  }
2120
2393
  `;
2121
- var clipStyles = css8`
2394
+ var clipStyles = css9`
2122
2395
  .clip-container {
2123
2396
  position: absolute;
2124
2397
  overflow: hidden;
@@ -2725,15 +2998,10 @@ var RecordingController = class {
2725
2998
  import {
2726
2999
  SpectrogramOrchestrator
2727
3000
  } from "@dawcore/spectrogram";
2728
- var LIBRARY_DEFAULTS = {
2729
- fftSize: 2048,
2730
- windowFunction: "hann",
2731
- frequencyScale: "mel",
2732
- minFrequency: 0,
2733
- gainDb: 20,
2734
- rangeDb: 80
2735
- };
2736
- var LIBRARY_DEFAULT_COLOR_MAP = "viridis";
3001
+ import {
3002
+ SPECTROGRAM_DEFAULTS as LIBRARY_DEFAULTS,
3003
+ DEFAULT_SPECTROGRAM_COLOR_MAP as LIBRARY_DEFAULT_COLOR_MAP
3004
+ } from "@waveform-playlist/core";
2737
3005
  var SpectrogramController = class {
2738
3006
  constructor(host, workerFactory) {
2739
3007
  this.orchestrator = null;
@@ -2807,7 +3075,21 @@ var SpectrogramController = class {
2807
3075
  const detail = e.detail;
2808
3076
  this.host.dispatchEvent(
2809
3077
  new CustomEvent("daw-spectrogram-ready", {
2810
- detail,
3078
+ detail: { trackId: detail.trackId, generation: detail.generation },
3079
+ bubbles: true,
3080
+ composed: true
3081
+ })
3082
+ );
3083
+ });
3084
+ this.orchestrator.addEventListener("viewport-error", (e) => {
3085
+ const detail = e.detail;
3086
+ this.host.dispatchEvent(
3087
+ new CustomEvent("daw-spectrogram-error", {
3088
+ detail: {
3089
+ trackId: detail.trackId,
3090
+ generation: detail.generation,
3091
+ error: detail.error
3092
+ },
2811
3093
  bubbles: true,
2812
3094
  composed: true
2813
3095
  })
@@ -2861,7 +3143,11 @@ var PointerHandler = class {
2861
3143
  e.preventDefault();
2862
3144
  this._timeline = this._host.shadowRoot?.querySelector(".timeline");
2863
3145
  if (this._timeline) {
2864
- this._timeline.setPointerCapture(e.pointerId);
3146
+ try {
3147
+ this._timeline.setPointerCapture(e.pointerId);
3148
+ } catch (err) {
3149
+ console.warn("[dawcore] setPointerCapture failed: " + String(err));
3150
+ }
2865
3151
  const onMove = (me) => clipHandler.onPointerMove(me);
2866
3152
  const onUp = (ue) => {
2867
3153
  clipHandler.onPointerUp(ue);
@@ -2883,11 +3169,20 @@ var PointerHandler = class {
2883
3169
  }
2884
3170
  }
2885
3171
  this._timeline = this._host.shadowRoot?.querySelector(".timeline");
2886
- if (!this._timeline) return;
3172
+ if (!this._timeline) {
3173
+ console.warn(
3174
+ "[dawcore] PointerHandler: .timeline not found in shadow root \u2014 seek/selection ignored"
3175
+ );
3176
+ return;
3177
+ }
2887
3178
  this._timelineRect = this._timeline.getBoundingClientRect();
2888
3179
  this._dragStartPx = this._pxFromPointer(e);
2889
3180
  this._isDragging = false;
2890
- this._timeline.setPointerCapture(e.pointerId);
3181
+ try {
3182
+ this._timeline.setPointerCapture(e.pointerId);
3183
+ } catch (err) {
3184
+ console.warn("[dawcore] setPointerCapture failed: " + String(err));
3185
+ }
2891
3186
  this._timeline.addEventListener("pointermove", this._onPointerMove);
2892
3187
  this._timeline.addEventListener("pointerup", this._onPointerUp);
2893
3188
  };
@@ -3819,9 +4114,143 @@ async function loadWaveformDataFromUrl(src) {
3819
4114
  }
3820
4115
  }
3821
4116
 
4117
+ // src/controllers/scroll-sync-controller.ts
4118
+ var LINE_HEIGHT_PX = 16;
4119
+ var ScrollSyncController = class {
4120
+ constructor(host) {
4121
+ this._scrollContainer = null;
4122
+ this._wheelTargets = /* @__PURE__ */ new Set();
4123
+ this._warnedX = false;
4124
+ this._warnedY = false;
4125
+ /** Selector (in host shadow DOM) for the scroll container. */
4126
+ this.scrollSelector = "";
4127
+ /** Selector for the element receiving translate3d(-scrollLeft, 0, 0). */
4128
+ this.xTargetSelector = "";
4129
+ /** Selector for the element receiving translate3d(0, -scrollTop, 0). */
4130
+ this.yTargetSelector = "";
4131
+ /**
4132
+ * Selector (or comma-separated selectors) for elements whose wheel events
4133
+ * forward to the scroll container. All matching elements receive listeners.
4134
+ */
4135
+ this.wheelForwardSelector = "";
4136
+ this._onScroll = () => {
4137
+ this._apply();
4138
+ };
4139
+ this._onWheel = (e) => {
4140
+ const sc = this._scrollContainer;
4141
+ if (!sc) return;
4142
+ const scale = e.deltaMode === WheelEvent.DOM_DELTA_LINE ? LINE_HEIGHT_PX : e.deltaMode === WheelEvent.DOM_DELTA_PAGE ? sc.clientHeight : 1;
4143
+ const scaleX = e.deltaMode === WheelEvent.DOM_DELTA_PAGE ? sc.clientWidth : scale;
4144
+ const beforeLeft = sc.scrollLeft;
4145
+ const beforeTop = sc.scrollTop;
4146
+ sc.scrollLeft += e.deltaX * scaleX;
4147
+ sc.scrollTop += e.deltaY * scale;
4148
+ if (sc.scrollLeft !== beforeLeft || sc.scrollTop !== beforeTop) {
4149
+ e.preventDefault();
4150
+ }
4151
+ };
4152
+ this._host = host;
4153
+ host.addController(this);
4154
+ }
4155
+ hostConnected() {
4156
+ requestAnimationFrame(() => {
4157
+ if (!this._host.isConnected) return;
4158
+ this._attach();
4159
+ if (!this._scrollContainer && this.scrollSelector) {
4160
+ console.warn(
4161
+ '[dawcore] ScrollSyncController: scroll container not found for "' + this.scrollSelector + '"'
4162
+ );
4163
+ }
4164
+ });
4165
+ }
4166
+ hostDisconnected() {
4167
+ this._scrollContainer?.removeEventListener("scroll", this._onScroll);
4168
+ this._scrollContainer = null;
4169
+ for (const target of this._wheelTargets) {
4170
+ target.removeEventListener("wheel", this._onWheel);
4171
+ }
4172
+ this._wheelTargets.clear();
4173
+ }
4174
+ /**
4175
+ * Re-attach and re-apply transforms from the current scroll position.
4176
+ * Called from the host's updated() so elements created by a re-render
4177
+ * (e.g. the ruler appearing when the first track loads) pick up the
4178
+ * current offset and listeners.
4179
+ */
4180
+ sync() {
4181
+ this._attach();
4182
+ }
4183
+ _query(selector) {
4184
+ return selector ? this._host.shadowRoot?.querySelector(selector) : null;
4185
+ }
4186
+ _queryAll(selector) {
4187
+ if (!selector) return [];
4188
+ return Array.from(this._host.shadowRoot?.querySelectorAll(selector) ?? []);
4189
+ }
4190
+ _attach() {
4191
+ const container = this._query(this.scrollSelector);
4192
+ if (!container) {
4193
+ if (this._scrollContainer && !this._scrollContainer.isConnected) {
4194
+ console.warn(
4195
+ '[dawcore] ScrollSyncController: scroll container "' + this.scrollSelector + '" was removed from the DOM \u2014 detaching listeners until it reappears.'
4196
+ );
4197
+ this._scrollContainer.removeEventListener("scroll", this._onScroll);
4198
+ this._scrollContainer = null;
4199
+ for (const t of this._wheelTargets) t.removeEventListener("wheel", this._onWheel);
4200
+ this._wheelTargets.clear();
4201
+ }
4202
+ return;
4203
+ }
4204
+ if (container !== this._scrollContainer) {
4205
+ this._scrollContainer?.removeEventListener("scroll", this._onScroll);
4206
+ this._scrollContainer = container;
4207
+ container.addEventListener("scroll", this._onScroll, { passive: true });
4208
+ }
4209
+ const nextTargets = new Set(this._queryAll(this.wheelForwardSelector));
4210
+ for (const old of this._wheelTargets) {
4211
+ if (!nextTargets.has(old)) {
4212
+ old.removeEventListener("wheel", this._onWheel);
4213
+ this._wheelTargets.delete(old);
4214
+ }
4215
+ }
4216
+ for (const next of nextTargets) {
4217
+ if (!this._wheelTargets.has(next)) {
4218
+ next.addEventListener("wheel", this._onWheel, { passive: false });
4219
+ this._wheelTargets.add(next);
4220
+ }
4221
+ }
4222
+ this._apply();
4223
+ }
4224
+ _apply() {
4225
+ const sc = this._scrollContainer;
4226
+ if (!sc) return;
4227
+ const xTarget = this._query(this.xTargetSelector);
4228
+ if (xTarget) {
4229
+ xTarget.style.transform = `translate3d(${-sc.scrollLeft}px, 0, 0)`;
4230
+ this._warnedX = false;
4231
+ } else if (this.xTargetSelector && sc.scrollLeft !== 0 && !this._warnedX) {
4232
+ this._warnedX = true;
4233
+ console.warn(
4234
+ '[dawcore] ScrollSyncController: x target "' + this.xTargetSelector + '" not found while scrolled \u2014 the synced pane will appear frozen. Check the selector, or clear it if the target is intentionally not rendered.'
4235
+ );
4236
+ }
4237
+ const yTarget = this._query(this.yTargetSelector);
4238
+ if (yTarget) {
4239
+ yTarget.style.transform = `translate3d(0, ${-sc.scrollTop}px, 0)`;
4240
+ this._warnedY = false;
4241
+ } else if (this.yTargetSelector && sc.scrollTop !== 0 && !this._warnedY) {
4242
+ this._warnedY = true;
4243
+ console.warn(
4244
+ '[dawcore] ScrollSyncController: y target "' + this.yTargetSelector + '" not found while scrolled \u2014 the synced pane will appear frozen. Check the selector, or clear it if the target is intentionally not rendered.'
4245
+ );
4246
+ }
4247
+ }
4248
+ };
4249
+
3822
4250
  // src/elements/daw-editor.ts
4251
+ var RULER_HEIGHT = 30;
3823
4252
  var NO_ADAPTER_ERROR = "No PlayoutAdapter set on <daw-editor>. Set editor.adapter before use.\n\n // Option 1: Native Web Audio (no Tone.js)\n npm install @dawcore/transport\n import { NativePlayoutAdapter } from '@dawcore/transport';\n editor.adapter = new NativePlayoutAdapter(new AudioContext());\n\n // Option 2: Tone.js (effects, MIDI synths)\n npm install @waveform-playlist/playout\n import { createToneAdapter } from '@waveform-playlist/playout';\n editor.adapter = createToneAdapter();";
3824
- var DawEditorElement = class extends LitElement10 {
4253
+ var DawEditorElement = class extends LitElement11 {
3825
4254
  constructor() {
3826
4255
  super(...arguments);
3827
4256
  this._samplesPerPixel = 1024;
@@ -3880,13 +4309,21 @@ var DawEditorElement = class extends LitElement10 {
3880
4309
  v.scrollSelector = ".scroll-area";
3881
4310
  return v;
3882
4311
  })();
4312
+ this._scrollSync = (() => {
4313
+ const s = new ScrollSyncController(this);
4314
+ s.scrollSelector = ".scroll-area";
4315
+ return s;
4316
+ })();
3883
4317
  /**
3884
4318
  * Cache of the last ViewportState forwarded to the spectrogram controller.
3885
4319
  * Lit's `updated()` fires on every reactive state change (`_isPlaying`,
3886
4320
  * `_selectedTrackId`, etc.) — most of which don't affect the spectrogram
3887
4321
  * viewport. Skip the cross-controller call when nothing changed.
3888
4322
  *
3889
- * The orchestrator dedupes too, but this avoids the call entirely.
4323
+ * The orchestrator dedupes identical viewports too, so removing this cache
4324
+ * wouldn't change observable behavior — but it would push a fresh
4325
+ * `setViewport` call (with object allocation) into every Lit reactive
4326
+ * update for properties unrelated to the viewport.
3890
4327
  */
3891
4328
  this._lastSpectrogramViewport = null;
3892
4329
  // --- Track Events ---
@@ -4129,9 +4566,10 @@ var DawEditorElement = class extends LitElement10 {
4129
4566
  this._spectrogramController?.unregisterCanvas(canvasId);
4130
4567
  }
4131
4568
  /**
4132
- * Push a clip's decoded audio into the spectrogram controller. No-op
4133
- * unless the track is in spectrogram render-mode and the controller
4134
- * already exists (it bootstraps from canvas registration).
4569
+ * Forward a clip's AudioBuffer to the spectrogram controller if the parent
4570
+ * track is in spectrogram render-mode. Eagerly creates the controller via
4571
+ * `_ensureSpectrogramController` so the audio data is queued for the first
4572
+ * render — even if no canvases have been registered yet.
4135
4573
  */
4136
4574
  _maybeRegisterSpectrogramClipAudio(trackId, clip) {
4137
4575
  const descriptor = this._tracks.get(trackId);
@@ -4422,6 +4860,13 @@ var DawEditorElement = class extends LitElement10 {
4422
4860
  }
4423
4861
  }
4424
4862
  updated(_changed) {
4863
+ this._scrollSync.xTargetSelector = this._showRuler ? ".ruler-content" : "";
4864
+ this._scrollSync.yTargetSelector = this._showControls ? ".controls-column" : "";
4865
+ this._scrollSync.wheelForwardSelector = [
4866
+ this._showControls ? ".controls-viewport" : "",
4867
+ this._showRuler ? ".ruler-viewport" : ""
4868
+ ].filter(Boolean).join(", ");
4869
+ this._scrollSync.sync();
4425
4870
  if (this._spectrogramController) {
4426
4871
  const vs = this._viewport.visibleStart;
4427
4872
  const ve = this._viewport.visibleEnd;
@@ -5248,6 +5693,13 @@ var DawEditorElement = class extends LitElement10 {
5248
5693
  }
5249
5694
  const oldDesc = this._tracks.get(trackId);
5250
5695
  if (!oldDesc) return;
5696
+ let normalizedRenderMode = partial.renderMode;
5697
+ if (normalizedRenderMode === "both") {
5698
+ console.warn(
5699
+ `[dawcore] render-mode="both" is not yet supported; falling back to 'spectrogram'`
5700
+ );
5701
+ normalizedRenderMode = "spectrogram";
5702
+ }
5251
5703
  const newDesc = {
5252
5704
  ...oldDesc,
5253
5705
  ...partial.name !== void 0 && { name: partial.name },
@@ -5255,7 +5707,7 @@ var DawEditorElement = class extends LitElement10 {
5255
5707
  ...partial.pan !== void 0 && { pan: partial.pan },
5256
5708
  ...partial.muted !== void 0 && { muted: partial.muted },
5257
5709
  ...partial.soloed !== void 0 && { soloed: partial.soloed },
5258
- ...partial.renderMode !== void 0 && { renderMode: partial.renderMode }
5710
+ ...normalizedRenderMode !== void 0 && { renderMode: normalizedRenderMode }
5259
5711
  };
5260
5712
  this._tracks = new Map(this._tracks).set(trackId, newDesc);
5261
5713
  if (this._engine) {
@@ -5576,7 +6028,7 @@ var DawEditorElement = class extends LitElement10 {
5576
6028
  const w = Math.floor(audibleSamples / renderSpp);
5577
6029
  return rs.peaks.map((chPeaks, ch) => {
5578
6030
  const slicedPeaks = latencyPixels > 0 ? chPeaks.slice(latencyPixels * 2) : chPeaks;
5579
- return html9`
6031
+ return html10`
5580
6032
  <daw-waveform
5581
6033
  data-recording-track=${trackId}
5582
6034
  data-recording-channel=${ch}
@@ -5632,6 +6084,14 @@ var DawEditorElement = class extends LitElement10 {
5632
6084
  _getPlayhead() {
5633
6085
  return this.shadowRoot?.querySelector("daw-playhead");
5634
6086
  }
6087
+ /** True when the controls column should be rendered (and its selector is valid). */
6088
+ get _showControls() {
6089
+ return this._getOrderedTracks().length > 0 || this.indefinitePlayback;
6090
+ }
6091
+ /** True when the ruler header band should be rendered (and its selector is valid). */
6092
+ get _showRuler() {
6093
+ return (this._getOrderedTracks().length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale;
6094
+ }
5635
6095
  _getOrderedTracks() {
5636
6096
  const domOrder = [...this.querySelectorAll("daw-track")].map(
5637
6097
  (el) => el.trackId
@@ -5673,64 +6133,79 @@ var DawEditorElement = class extends LitElement10 {
5673
6133
  trackHeight: this.waveHeight * numChannels + (this.clipHeaders ? this.clipHeaderHeight : 0)
5674
6134
  };
5675
6135
  });
5676
- return html9`
5677
- ${orderedTracks.length > 0 || this.indefinitePlayback ? html9`<div class="controls-column">
5678
- ${this.timescale ? html9`<div style="height: 30px;"></div>` : ""}
5679
- ${orderedTracks.map(
5680
- (t) => html9`
5681
- <daw-track-controls
5682
- style="height: ${t.trackHeight}px;"
5683
- .trackId=${t.trackId}
5684
- .trackName=${t.descriptor?.name ?? "Untitled"}
5685
- .volume=${t.descriptor?.volume ?? 1}
5686
- .pan=${t.descriptor?.pan ?? 0}
5687
- .muted=${t.descriptor?.muted ?? false}
5688
- .soloed=${t.descriptor?.soloed ?? false}
5689
- ></daw-track-controls>
5690
- `
5691
- )}
5692
- </div>` : ""}
5693
- <div class="scroll-area">
5694
- <div
5695
- class="timeline ${this._dragOver ? "drag-over" : ""}"
5696
- style="width: ${this._totalWidth > 0 ? this._totalWidth + "px" : "100%"};"
5697
- data-playing=${this._isPlaying}
5698
- @pointerdown=${this._pointer.onPointerDown}
5699
- @dragover=${this._onDragOver}
5700
- @dragleave=${this._onDragLeave}
5701
- @drop=${this._onDrop}
5702
- >
5703
- ${(orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale ? html9`<daw-ruler
5704
- .samplesPerPixel=${spp}
5705
- .sampleRate=${this.effectiveSampleRate}
5706
- .duration=${this._duration}
5707
- .scaleMode=${this.scaleMode}
5708
- .ticksPerPixel=${this.ticksPerPixel}
5709
- .meterEntries=${this._meterEntries}
5710
- .ppqn=${this.ppqn}
5711
- .totalWidth=${this._totalWidth}
5712
- ></daw-ruler>` : ""}
5713
- ${this.scaleMode === "beats" ? html9`<daw-grid
5714
- style="top: ${this.timescale ? 30 : 0}px;"
5715
- .ticksPerPixel=${this.ticksPerPixel}
5716
- .meterEntries=${this._meterEntries}
5717
- .ppqn=${this.ppqn}
5718
- .visibleStart=${this._viewport.visibleStart}
5719
- .visibleEnd=${this._viewport.visibleEnd}
5720
- .length=${this._totalWidth}
5721
- .height=${orderedTracks.length > 0 ? orderedTracks.reduce((sum, t) => sum + t.trackHeight + 1, 0) : this._emptyGridHeight}
5722
- ></daw-grid>` : ""}
5723
- ${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? html9`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
5724
- <daw-playhead></daw-playhead>` : ""}
5725
- ${orderedTracks.map((t) => {
5726
- const channelHeight = this.waveHeight;
5727
- return html9`
6136
+ const showControls = this._showControls;
6137
+ const showRuler = this._showRuler;
6138
+ return html10`
6139
+ ${showRuler ? html10`<div class="header-row" style="height: ${RULER_HEIGHT}px;">
6140
+ ${showControls ? html10`<div class="ruler-gap"></div>` : ""}
6141
+ <div class="ruler-viewport" @pointerdown=${this._pointer.onPointerDown}>
5728
6142
  <div
5729
- class="track-row ${t.trackId === this._selectedTrackId ? "selected" : ""}"
5730
- style="height: ${t.trackHeight}px;"
5731
- data-track-id=${t.trackId}
6143
+ class="ruler-content"
6144
+ style="width: ${this._totalWidth > 0 ? this._totalWidth + "px" : "100%"};"
5732
6145
  >
5733
- ${t.track.clips.map((clip) => {
6146
+ <daw-ruler
6147
+ .samplesPerPixel=${spp}
6148
+ .sampleRate=${this.effectiveSampleRate}
6149
+ .duration=${this._duration}
6150
+ .scaleMode=${this.scaleMode}
6151
+ .ticksPerPixel=${this.ticksPerPixel}
6152
+ .meterEntries=${this._meterEntries}
6153
+ .ppqn=${this.ppqn}
6154
+ .totalWidth=${this._totalWidth}
6155
+ .rulerHeight=${RULER_HEIGHT}
6156
+ ></daw-ruler>
6157
+ </div>
6158
+ </div>
6159
+ </div>` : ""}
6160
+ <div class="body">
6161
+ ${showControls ? html10`<div class="controls-viewport">
6162
+ <div class="controls-column">
6163
+ ${orderedTracks.map(
6164
+ (t) => html10`
6165
+ <daw-track-controls
6166
+ style="height: ${t.trackHeight}px;"
6167
+ .trackId=${t.trackId}
6168
+ .trackName=${t.descriptor?.name ?? "Untitled"}
6169
+ .volume=${t.descriptor?.volume ?? 1}
6170
+ .pan=${t.descriptor?.pan ?? 0}
6171
+ .muted=${t.descriptor?.muted ?? false}
6172
+ .soloed=${t.descriptor?.soloed ?? false}
6173
+ ></daw-track-controls>
6174
+ `
6175
+ )}
6176
+ </div>
6177
+ </div>` : ""}
6178
+ <div class="scroll-area">
6179
+ <div
6180
+ class="timeline ${this._dragOver ? "drag-over" : ""}"
6181
+ style="width: ${this._totalWidth > 0 ? this._totalWidth + "px" : "100%"};"
6182
+ data-playing=${this._isPlaying}
6183
+ @pointerdown=${this._pointer.onPointerDown}
6184
+ @dragover=${this._onDragOver}
6185
+ @dragleave=${this._onDragLeave}
6186
+ @drop=${this._onDrop}
6187
+ >
6188
+ ${this.scaleMode === "beats" ? html10`<daw-grid
6189
+ style="top: 0px;"
6190
+ .ticksPerPixel=${this.ticksPerPixel}
6191
+ .meterEntries=${this._meterEntries}
6192
+ .ppqn=${this.ppqn}
6193
+ .visibleStart=${this._viewport.visibleStart}
6194
+ .visibleEnd=${this._viewport.visibleEnd}
6195
+ .length=${this._totalWidth}
6196
+ .height=${orderedTracks.length > 0 ? orderedTracks.reduce((sum, t) => sum + t.trackHeight, 0) : this._emptyGridHeight}
6197
+ ></daw-grid>` : ""}
6198
+ ${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? html10`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
6199
+ <daw-playhead></daw-playhead>` : ""}
6200
+ ${orderedTracks.map((t) => {
6201
+ const channelHeight = this.waveHeight;
6202
+ return html10`
6203
+ <div
6204
+ class="track-row ${t.trackId === this._selectedTrackId ? "selected" : ""}"
6205
+ style="height: ${t.trackHeight}px;"
6206
+ data-track-id=${t.trackId}
6207
+ >
6208
+ ${t.track.clips.map((clip) => {
5734
6209
  const peakData = this._peaksData.get(clip.id);
5735
6210
  let clipLeft;
5736
6211
  let width;
@@ -5773,7 +6248,10 @@ var DawEditorElement = class extends LitElement10 {
5773
6248
  const segEndSample = Math.round(segEndAudioSec * sr);
5774
6249
  const totalPeaks = clip.durationSamples / baseScale;
5775
6250
  clipSegments.push({
5776
- peakStart: Math.max(0, (segStartSample - clip.offsetSamples) / baseScale),
6251
+ peakStart: Math.max(
6252
+ 0,
6253
+ (segStartSample - clip.offsetSamples) / baseScale
6254
+ ),
5777
6255
  peakEnd: Math.min(
5778
6256
  totalPeaks,
5779
6257
  (segEndSample - clip.offsetSamples) / baseScale
@@ -5787,78 +6265,79 @@ var DawEditorElement = class extends LitElement10 {
5787
6265
  const channels = segmentChannels ?? peakData?.data ?? [new Int16Array(0)];
5788
6266
  const hdrH = this.clipHeaders ? this.clipHeaderHeight : 0;
5789
6267
  const chH = this.waveHeight;
5790
- return html9` <div
5791
- class="clip-container"
5792
- style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
5793
- data-clip-id=${clip.id}
5794
- >
5795
- ${hdrH > 0 ? html9`<div
5796
- class="clip-header"
5797
- data-clip-id=${clip.id}
5798
- data-track-id=${t.trackId}
5799
- ?data-interactive=${this.interactiveClips}
5800
- >
5801
- <span>${clip.name || t.descriptor?.name || ""}</span>
5802
- </div>` : ""}
5803
- ${t.descriptor?.renderMode === "piano-roll" ? html9`<daw-piano-roll
5804
- style="position:absolute;left:0;top:${hdrH}px;"
5805
- .midiNotes=${clip.midiNotes ?? []}
5806
- .length=${peakData?.length ?? width}
5807
- .waveHeight=${chH * channels.length}
5808
- .samplesPerPixel=${this._renderSpp}
5809
- .sampleRate=${this.effectiveSampleRate}
5810
- .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
5811
- .visibleStart=${this._viewport.visibleStart}
5812
- .visibleEnd=${this._viewport.visibleEnd}
5813
- .originX=${clipLeft}
5814
- ?selected=${t.trackId === this._selectedTrackId}
5815
- ></daw-piano-roll>` : t.descriptor?.renderMode === "spectrogram" ? channels.map(
5816
- (_chPeaks, chIdx) => html9`<daw-spectrogram
5817
- style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;height:${chH}px;width:${peakData?.length ?? width}px;"
5818
- .clipId=${clip.id}
5819
- .trackId=${t.trackId}
5820
- .channelIndex=${chIdx}
5821
- .length=${peakData?.length ?? width}
5822
- .waveHeight=${chH}
5823
- .samplesPerPixel=${this._renderSpp}
5824
- .sampleRate=${this.effectiveSampleRate}
5825
- .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
5826
- .visibleStart=${this._viewport.visibleStart}
5827
- .visibleEnd=${this._viewport.visibleEnd}
5828
- .originX=${clipLeft}
5829
- ></daw-spectrogram>`
5830
- ) : channels.map(
5831
- (chPeaks, chIdx) => html9` <daw-waveform
5832
- style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
5833
- .peaks=${chPeaks}
5834
- .length=${peakData?.length ?? width}
5835
- .waveHeight=${chH}
5836
- .barWidth=${this.barWidth}
5837
- .barGap=${this.barGap}
5838
- .visibleStart=${this._viewport.visibleStart}
5839
- .visibleEnd=${this._viewport.visibleEnd}
5840
- .originX=${clipLeft}
5841
- .segments=${clipSegments}
5842
- ></daw-waveform>`
5843
- )}
5844
- ${this.interactiveClips ? html9` <div
5845
- class="clip-boundary"
5846
- data-boundary-edge="left"
5847
- data-clip-id=${clip.id}
5848
- data-track-id=${t.trackId}
5849
- ></div>
5850
- <div
5851
- class="clip-boundary"
5852
- data-boundary-edge="right"
6268
+ return html10` <div
6269
+ class="clip-container"
6270
+ style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
6271
+ data-clip-id=${clip.id}
6272
+ >
6273
+ ${hdrH > 0 ? html10`<div
6274
+ class="clip-header"
5853
6275
  data-clip-id=${clip.id}
5854
6276
  data-track-id=${t.trackId}
5855
- ></div>` : ""}
5856
- </div>`;
6277
+ ?data-interactive=${this.interactiveClips}
6278
+ >
6279
+ <span>${clip.name || t.descriptor?.name || ""}</span>
6280
+ </div>` : ""}
6281
+ ${t.descriptor?.renderMode === "piano-roll" ? html10`<daw-piano-roll
6282
+ style="position:absolute;left:0;top:${hdrH}px;"
6283
+ .midiNotes=${clip.midiNotes ?? []}
6284
+ .length=${peakData?.length ?? width}
6285
+ .waveHeight=${chH * channels.length}
6286
+ .samplesPerPixel=${this._renderSpp}
6287
+ .sampleRate=${this.effectiveSampleRate}
6288
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
6289
+ .visibleStart=${this._viewport.visibleStart}
6290
+ .visibleEnd=${this._viewport.visibleEnd}
6291
+ .originX=${clipLeft}
6292
+ ?selected=${t.trackId === this._selectedTrackId}
6293
+ ></daw-piano-roll>` : t.descriptor?.renderMode === "spectrogram" ? channels.map(
6294
+ (_chPeaks, chIdx) => html10`<daw-spectrogram
6295
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;height:${chH}px;width:${peakData?.length ?? width}px;"
6296
+ .clipId=${clip.id}
6297
+ .trackId=${t.trackId}
6298
+ .channelIndex=${chIdx}
6299
+ .length=${peakData?.length ?? width}
6300
+ .waveHeight=${chH}
6301
+ .samplesPerPixel=${this._renderSpp}
6302
+ .sampleRate=${this.effectiveSampleRate}
6303
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
6304
+ .visibleStart=${this._viewport.visibleStart}
6305
+ .visibleEnd=${this._viewport.visibleEnd}
6306
+ .originX=${clipLeft}
6307
+ ></daw-spectrogram>`
6308
+ ) : channels.map(
6309
+ (chPeaks, chIdx) => html10` <daw-waveform
6310
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
6311
+ .peaks=${chPeaks}
6312
+ .length=${peakData?.length ?? width}
6313
+ .waveHeight=${chH}
6314
+ .barWidth=${this.barWidth}
6315
+ .barGap=${this.barGap}
6316
+ .visibleStart=${this._viewport.visibleStart}
6317
+ .visibleEnd=${this._viewport.visibleEnd}
6318
+ .originX=${clipLeft}
6319
+ .segments=${clipSegments}
6320
+ ></daw-waveform>`
6321
+ )}
6322
+ ${this.interactiveClips ? html10` <div
6323
+ class="clip-boundary"
6324
+ data-boundary-edge="left"
6325
+ data-clip-id=${clip.id}
6326
+ data-track-id=${t.trackId}
6327
+ ></div>
6328
+ <div
6329
+ class="clip-boundary"
6330
+ data-boundary-edge="right"
6331
+ data-clip-id=${clip.id}
6332
+ data-track-id=${t.trackId}
6333
+ ></div>` : ""}
6334
+ </div>`;
5857
6335
  })}
5858
- ${this._renderRecordingPreview(t.trackId, channelHeight)}
5859
- </div>
5860
- `;
6336
+ ${this._renderRecordingPreview(t.trackId, channelHeight)}
6337
+ </div>
6338
+ `;
5861
6339
  })}
6340
+ </div>
5862
6341
  </div>
5863
6342
  </div>
5864
6343
  <slot></slot>
@@ -5867,21 +6346,48 @@ var DawEditorElement = class extends LitElement10 {
5867
6346
  };
5868
6347
  DawEditorElement.styles = [
5869
6348
  hostStyles,
5870
- css9`
6349
+ css10`
5871
6350
  :host {
5872
6351
  display: flex;
6352
+ flex-direction: column;
5873
6353
  position: relative;
5874
6354
  background: var(--daw-background, #1a1a2e);
5875
6355
  overflow: hidden;
5876
6356
  }
5877
- .controls-column {
6357
+ .header-row {
6358
+ display: flex;
6359
+ flex-shrink: 0;
6360
+ }
6361
+ .ruler-gap {
5878
6362
  flex-shrink: 0;
5879
6363
  width: var(--daw-controls-width, 180px);
5880
6364
  }
6365
+ .ruler-viewport {
6366
+ flex: 1;
6367
+ position: relative;
6368
+ overflow: hidden;
6369
+ cursor: text;
6370
+ }
6371
+ .ruler-content {
6372
+ will-change: transform;
6373
+ }
6374
+ .body {
6375
+ flex: 1;
6376
+ min-height: 0;
6377
+ display: flex;
6378
+ }
6379
+ .controls-viewport {
6380
+ flex-shrink: 0;
6381
+ width: var(--daw-controls-width, 180px);
6382
+ overflow: hidden;
6383
+ }
6384
+ .controls-column {
6385
+ will-change: transform;
6386
+ }
5881
6387
  .scroll-area {
5882
6388
  flex: 1;
5883
- overflow-x: auto;
5884
- overflow-y: hidden;
6389
+ overflow: auto;
6390
+ overflow-anchor: none;
5885
6391
  min-height: var(--daw-min-height, 200px);
5886
6392
  }
5887
6393
  .timeline {
@@ -5891,6 +6397,7 @@ DawEditorElement.styles = [
5891
6397
  }
5892
6398
  .track-row {
5893
6399
  position: relative;
6400
+ box-sizing: border-box;
5894
6401
  background: var(--daw-track-background, #16213e);
5895
6402
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
5896
6403
  }
@@ -5915,70 +6422,70 @@ DawEditorElement.styles = [
5915
6422
  ];
5916
6423
  DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
5917
6424
  __decorateClass([
5918
- property8({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
6425
+ property9({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
5919
6426
  ], DawEditorElement.prototype, "samplesPerPixel", 1);
5920
6427
  __decorateClass([
5921
- property8({ type: Number, attribute: "wave-height" })
6428
+ property9({ type: Number, attribute: "wave-height" })
5922
6429
  ], DawEditorElement.prototype, "waveHeight", 2);
5923
6430
  __decorateClass([
5924
- property8({ type: Boolean })
6431
+ property9({ type: Boolean })
5925
6432
  ], DawEditorElement.prototype, "timescale", 2);
5926
6433
  __decorateClass([
5927
- property8({ type: Boolean })
6434
+ property9({ type: Boolean })
5928
6435
  ], DawEditorElement.prototype, "mono", 2);
5929
6436
  __decorateClass([
5930
- property8({ type: Number, attribute: "bar-width" })
6437
+ property9({ type: Number, attribute: "bar-width" })
5931
6438
  ], DawEditorElement.prototype, "barWidth", 2);
5932
6439
  __decorateClass([
5933
- property8({ type: Number, attribute: "bar-gap" })
6440
+ property9({ type: Number, attribute: "bar-gap" })
5934
6441
  ], DawEditorElement.prototype, "barGap", 2);
5935
6442
  __decorateClass([
5936
- property8({ type: Boolean, attribute: "file-drop" })
6443
+ property9({ type: Boolean, attribute: "file-drop" })
5937
6444
  ], DawEditorElement.prototype, "fileDrop", 2);
5938
6445
  __decorateClass([
5939
- property8({ type: Boolean, attribute: "clip-headers" })
6446
+ property9({ type: Boolean, attribute: "clip-headers" })
5940
6447
  ], DawEditorElement.prototype, "clipHeaders", 2);
5941
6448
  __decorateClass([
5942
- property8({ type: Number, attribute: "clip-header-height" })
6449
+ property9({ type: Number, attribute: "clip-header-height" })
5943
6450
  ], DawEditorElement.prototype, "clipHeaderHeight", 2);
5944
6451
  __decorateClass([
5945
- property8({ type: Boolean, attribute: "interactive-clips" })
6452
+ property9({ type: Boolean, attribute: "interactive-clips" })
5946
6453
  ], DawEditorElement.prototype, "interactiveClips", 2);
5947
6454
  __decorateClass([
5948
- property8({ type: Boolean, attribute: "indefinite-playback" })
6455
+ property9({ type: Boolean, attribute: "indefinite-playback" })
5949
6456
  ], DawEditorElement.prototype, "indefinitePlayback", 2);
5950
6457
  __decorateClass([
5951
- property8({ attribute: false, noAccessor: true })
6458
+ property9({ attribute: false, noAccessor: true })
5952
6459
  ], DawEditorElement.prototype, "spectrogramConfig", 1);
5953
6460
  __decorateClass([
5954
- property8({ attribute: false, noAccessor: true })
6461
+ property9({ attribute: false, noAccessor: true })
5955
6462
  ], DawEditorElement.prototype, "spectrogramColorMap", 1);
5956
6463
  __decorateClass([
5957
- property8({ type: String, attribute: "scale-mode" })
6464
+ property9({ type: String, attribute: "scale-mode" })
5958
6465
  ], DawEditorElement.prototype, "scaleMode", 2);
5959
6466
  __decorateClass([
5960
- property8({ type: Number, attribute: "ticks-per-pixel", noAccessor: true })
6467
+ property9({ type: Number, attribute: "ticks-per-pixel", noAccessor: true })
5961
6468
  ], DawEditorElement.prototype, "ticksPerPixel", 1);
5962
6469
  __decorateClass([
5963
- property8({ type: Number, noAccessor: true })
6470
+ property9({ type: Number, noAccessor: true })
5964
6471
  ], DawEditorElement.prototype, "bpm", 1);
5965
6472
  __decorateClass([
5966
- property8({ attribute: false })
6473
+ property9({ attribute: false })
5967
6474
  ], DawEditorElement.prototype, "timeSignature", 2);
5968
6475
  __decorateClass([
5969
- property8({ attribute: false })
6476
+ property9({ attribute: false })
5970
6477
  ], DawEditorElement.prototype, "meterEntries", 2);
5971
6478
  __decorateClass([
5972
- property8({ type: Number, noAccessor: true })
6479
+ property9({ type: Number, noAccessor: true })
5973
6480
  ], DawEditorElement.prototype, "ppqn", 1);
5974
6481
  __decorateClass([
5975
- property8({ type: String, attribute: "snap-to" })
6482
+ property9({ type: String, attribute: "snap-to" })
5976
6483
  ], DawEditorElement.prototype, "snapTo", 2);
5977
6484
  __decorateClass([
5978
- property8({ attribute: false })
6485
+ property9({ attribute: false })
5979
6486
  ], DawEditorElement.prototype, "secondsToTicks", 2);
5980
6487
  __decorateClass([
5981
- property8({ attribute: false })
6488
+ property9({ attribute: false })
5982
6489
  ], DawEditorElement.prototype, "ticksToSeconds", 2);
5983
6490
  __decorateClass([
5984
6491
  state3()
@@ -6002,246 +6509,15 @@ __decorateClass([
6002
6509
  state3()
6003
6510
  ], DawEditorElement.prototype, "_dragOver", 2);
6004
6511
  __decorateClass([
6005
- property8({ attribute: false })
6512
+ property9({ attribute: false })
6006
6513
  ], DawEditorElement.prototype, "adapter", 1);
6007
6514
  __decorateClass([
6008
- property8({ attribute: "eager-resume" })
6515
+ property9({ attribute: "eager-resume" })
6009
6516
  ], DawEditorElement.prototype, "eagerResume", 2);
6010
6517
  DawEditorElement = __decorateClass([
6011
- customElement12("daw-editor")
6518
+ customElement13("daw-editor")
6012
6519
  ], DawEditorElement);
6013
6520
 
6014
- // src/elements/daw-ruler.ts
6015
- import { LitElement as LitElement11, html as html10, css as css10 } from "lit";
6016
- import { customElement as customElement13, property as property9 } from "lit/decorators.js";
6017
-
6018
- // src/utils/time-format.ts
6019
- function formatTime(milliseconds) {
6020
- const seconds = Math.floor(milliseconds / 1e3);
6021
- const s = seconds % 60;
6022
- const m = (seconds - s) / 60;
6023
- return `${m}:${String(s).padStart(2, "0")}`;
6024
- }
6025
-
6026
- // src/utils/smart-scale.ts
6027
- var timeinfo = /* @__PURE__ */ new Map([
6028
- [700, { marker: 1e3, bigStep: 500, smallStep: 100 }],
6029
- [1500, { marker: 2e3, bigStep: 1e3, smallStep: 200 }],
6030
- [2500, { marker: 2e3, bigStep: 1e3, smallStep: 500 }],
6031
- [5e3, { marker: 5e3, bigStep: 1e3, smallStep: 500 }],
6032
- [1e4, { marker: 1e4, bigStep: 5e3, smallStep: 1e3 }],
6033
- [12e3, { marker: 15e3, bigStep: 5e3, smallStep: 1e3 }],
6034
- [Infinity, { marker: 3e4, bigStep: 1e4, smallStep: 5e3 }]
6035
- ]);
6036
- function getScaleInfo(samplesPerPixel) {
6037
- for (const [resolution, config] of timeinfo) {
6038
- if (samplesPerPixel < resolution) {
6039
- return config;
6040
- }
6041
- }
6042
- return { marker: 3e4, bigStep: 1e4, smallStep: 5e3 };
6043
- }
6044
- function computeTemporalTicks(samplesPerPixel, sampleRate, duration, rulerHeight) {
6045
- const widthX = Math.ceil(duration * sampleRate / samplesPerPixel);
6046
- const config = getScaleInfo(samplesPerPixel);
6047
- const { marker, bigStep, smallStep } = config;
6048
- const canvasInfo = /* @__PURE__ */ new Map();
6049
- const labels = [];
6050
- const pixPerSec = sampleRate / samplesPerPixel;
6051
- for (let counter = 0; ; counter += smallStep) {
6052
- const pix = Math.floor(counter / 1e3 * pixPerSec);
6053
- if (pix >= widthX) break;
6054
- if (counter % marker === 0) {
6055
- canvasInfo.set(pix, rulerHeight);
6056
- labels.push({ pix, text: formatTime(counter) });
6057
- } else if (counter % bigStep === 0) {
6058
- canvasInfo.set(pix, Math.floor(rulerHeight / 2));
6059
- } else if (counter % smallStep === 0) {
6060
- canvasInfo.set(pix, Math.floor(rulerHeight / 5));
6061
- }
6062
- }
6063
- return { widthX, canvasInfo, labels };
6064
- }
6065
-
6066
- // src/elements/daw-ruler.ts
6067
- var MAX_CANVAS_WIDTH4 = 1e3;
6068
- var DawRulerElement = class extends LitElement11 {
6069
- constructor() {
6070
- super(...arguments);
6071
- this.samplesPerPixel = 1024;
6072
- this.sampleRate = 48e3;
6073
- this.duration = 0;
6074
- this.rulerHeight = 30;
6075
- this.scaleMode = "temporal";
6076
- this.ticksPerPixel = 4;
6077
- this.meterEntries = [
6078
- { tick: 0, numerator: 4, denominator: 4 }
6079
- ];
6080
- this.ppqn = 960;
6081
- this.totalWidth = 0;
6082
- this._tickData = null;
6083
- this._musicalTickData = null;
6084
- }
6085
- willUpdate() {
6086
- if (this.scaleMode === "beats" && this.totalWidth > 0) {
6087
- this._musicalTickData = getCachedMusicalTicks({
6088
- meterEntries: this.meterEntries,
6089
- ticksPerPixel: this.ticksPerPixel,
6090
- startPixel: 0,
6091
- endPixel: this.totalWidth,
6092
- ppqn: this.ppqn
6093
- });
6094
- this._tickData = null;
6095
- } else if (this.duration > 0 || this.totalWidth > 0) {
6096
- const widthDerivedDuration = this.totalWidth * this.samplesPerPixel / this.sampleRate;
6097
- const effectiveDuration = Math.max(this.duration, widthDerivedDuration);
6098
- this._musicalTickData = null;
6099
- this._tickData = computeTemporalTicks(
6100
- this.samplesPerPixel,
6101
- this.sampleRate,
6102
- effectiveDuration,
6103
- this.rulerHeight
6104
- );
6105
- } else {
6106
- this._musicalTickData = null;
6107
- this._tickData = null;
6108
- }
6109
- }
6110
- render() {
6111
- const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
6112
- if (widthX <= 0) return html10``;
6113
- const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH4);
6114
- const indices = Array.from({ length: totalChunks }, (_, i) => i);
6115
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
6116
- const beatsLabels = this.scaleMode === "beats" ? this._musicalTickData?.ticks.filter((t) => t.label) ?? [] : [];
6117
- const temporalLabels = this.scaleMode !== "beats" ? this._tickData?.labels ?? [] : [];
6118
- return html10`
6119
- <div class="container" style="width: ${widthX}px; height: ${this.rulerHeight}px;">
6120
- ${indices.map((i) => {
6121
- const width = Math.min(MAX_CANVAS_WIDTH4, widthX - i * MAX_CANVAS_WIDTH4);
6122
- return html10`
6123
- <canvas
6124
- data-index=${i}
6125
- width=${width * dpr}
6126
- height=${this.rulerHeight * dpr}
6127
- style="left: ${i * MAX_CANVAS_WIDTH4}px; width: ${width}px; height: ${this.rulerHeight}px;"
6128
- ></canvas>
6129
- `;
6130
- })}
6131
- ${this.scaleMode === "beats" ? beatsLabels.map(
6132
- (t) => html10`<span
6133
- class="label ${t.pixel > 0 ? "centered" : ""}"
6134
- style="left: ${t.pixel > 0 ? t.pixel : t.pixel + 4}px;"
6135
- >${t.label}</span
6136
- >`
6137
- ) : temporalLabels.map(
6138
- ({ pix, text }) => html10`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
6139
- )}
6140
- </div>
6141
- `;
6142
- }
6143
- updated() {
6144
- this._drawTicks();
6145
- }
6146
- _drawTicks() {
6147
- const canvases = this.shadowRoot?.querySelectorAll("canvas");
6148
- if (!canvases) return;
6149
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
6150
- const rulerColor = getComputedStyle(this).getPropertyValue("--daw-ruler-color").trim() || "#c49a6c";
6151
- const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
6152
- for (const canvas of canvases) {
6153
- const idx = Number(canvas.dataset.index);
6154
- const ctx = canvas.getContext("2d");
6155
- if (!ctx) continue;
6156
- const canvasWidth = Math.min(MAX_CANVAS_WIDTH4, widthX - idx * MAX_CANVAS_WIDTH4);
6157
- const globalOffset = idx * MAX_CANVAS_WIDTH4;
6158
- ctx.resetTransform();
6159
- ctx.clearRect(0, 0, canvas.width, canvas.height);
6160
- ctx.scale(dpr, dpr);
6161
- ctx.strokeStyle = rulerColor;
6162
- ctx.lineWidth = 1;
6163
- if (this.scaleMode === "beats" && this._musicalTickData) {
6164
- const h = this.rulerHeight;
6165
- for (const tick of this._musicalTickData.ticks) {
6166
- const localX = tick.pixel - globalOffset;
6167
- if (localX < 0 || localX >= canvasWidth) continue;
6168
- const tickH = tick.type === "major" ? h * 0.6 : tick.type === "minor" ? h * 0.35 : h * 0.15;
6169
- ctx.globalAlpha = tick.type === "major" ? 1 : 0.5;
6170
- ctx.beginPath();
6171
- ctx.moveTo(localX + 0.5, h);
6172
- ctx.lineTo(localX + 0.5, h - tickH);
6173
- ctx.stroke();
6174
- }
6175
- ctx.globalAlpha = 1;
6176
- } else if (this._tickData) {
6177
- for (const [pix, height] of this._tickData.canvasInfo) {
6178
- const localX = pix - globalOffset;
6179
- if (localX < 0 || localX >= canvasWidth) continue;
6180
- ctx.beginPath();
6181
- ctx.moveTo(localX + 0.5, this.rulerHeight);
6182
- ctx.lineTo(localX + 0.5, this.rulerHeight - height);
6183
- ctx.stroke();
6184
- }
6185
- }
6186
- }
6187
- }
6188
- };
6189
- DawRulerElement.styles = css10`
6190
- :host {
6191
- display: block;
6192
- position: relative;
6193
- background: var(--daw-ruler-background, #0f0f1a);
6194
- }
6195
- .container {
6196
- position: relative;
6197
- }
6198
- canvas {
6199
- position: absolute;
6200
- top: 0;
6201
- }
6202
- .label {
6203
- position: absolute;
6204
- font-size: 0.7rem;
6205
- line-height: 1;
6206
- white-space: nowrap;
6207
- color: var(--daw-ruler-color, #c49a6c);
6208
- top: 1px;
6209
- }
6210
- .label.centered {
6211
- transform: translateX(-50%);
6212
- }
6213
- `;
6214
- __decorateClass([
6215
- property9({ type: Number, attribute: false })
6216
- ], DawRulerElement.prototype, "samplesPerPixel", 2);
6217
- __decorateClass([
6218
- property9({ type: Number, attribute: false })
6219
- ], DawRulerElement.prototype, "sampleRate", 2);
6220
- __decorateClass([
6221
- property9({ type: Number, attribute: false })
6222
- ], DawRulerElement.prototype, "duration", 2);
6223
- __decorateClass([
6224
- property9({ type: Number, attribute: false })
6225
- ], DawRulerElement.prototype, "rulerHeight", 2);
6226
- __decorateClass([
6227
- property9({ type: String, attribute: false })
6228
- ], DawRulerElement.prototype, "scaleMode", 2);
6229
- __decorateClass([
6230
- property9({ type: Number, attribute: false })
6231
- ], DawRulerElement.prototype, "ticksPerPixel", 2);
6232
- __decorateClass([
6233
- property9({ attribute: false })
6234
- ], DawRulerElement.prototype, "meterEntries", 2);
6235
- __decorateClass([
6236
- property9({ type: Number, attribute: false })
6237
- ], DawRulerElement.prototype, "ppqn", 2);
6238
- __decorateClass([
6239
- property9({ type: Number, attribute: false })
6240
- ], DawRulerElement.prototype, "totalWidth", 2);
6241
- DawRulerElement = __decorateClass([
6242
- customElement13("daw-ruler")
6243
- ], DawRulerElement);
6244
-
6245
6521
  // src/elements/daw-selection.ts
6246
6522
  import { LitElement as LitElement12, html as html11, css as css11 } from "lit";
6247
6523
  import { customElement as customElement14, property as property10 } from "lit/decorators.js";
@@ -6552,6 +6828,7 @@ var DawSpectrogramElement = class extends LitElement14 {
6552
6828
  this.originX = 0;
6553
6829
  this._canvases = [];
6554
6830
  this._registeredCanvasIds = [];
6831
+ this._warnedNoHost = false;
6555
6832
  }
6556
6833
  get samplesPerPixel() {
6557
6834
  return this._samplesPerPixel;
@@ -6618,7 +6895,15 @@ var DawSpectrogramElement = class extends LitElement14 {
6618
6895
  }
6619
6896
  _registerCanvases() {
6620
6897
  const editor = this._findHostEditor();
6621
- if (!editor || typeof editor._spectrogramRegisterCanvas !== "function") return;
6898
+ if (!editor || typeof editor._spectrogramRegisterCanvas !== "function") {
6899
+ if (!this._warnedNoHost) {
6900
+ this._warnedNoHost = true;
6901
+ console.warn(
6902
+ "[dawcore] <daw-spectrogram> (clip " + this.clipId + ") could not find host <daw-editor>. Canvases will not render. Ensure the element is mounted inside a <daw-editor>."
6903
+ );
6904
+ }
6905
+ return;
6906
+ }
6622
6907
  for (let i = 0; i < this._canvases.length; i++) {
6623
6908
  const canvas = this._canvases[i];
6624
6909
  const canvasId = this.clipId + "-ch" + this.channelIndex + "-chunk" + i;