changeset 2128:e2ed5ce36253

double spectrum views have a connected cursor
author drewp@bigasterisk.com
date Fri, 03 Jun 2022 02:19:47 -0700
parents 1dc96b97a544
children 7cfca7d35cdb
files light9/ascoltami/Light9AscoltamiUi.ts light9/web/Light9CursorCanvas.ts light9/web/drawing.ts light9/web/light9-timeline-audio.ts light9/web/timeline/adjusters.ts light9/web/timeline/cursor_canvas.coffee light9/web/timeline/drawing.ts
diffstat 7 files changed, 255 insertions(+), 148 deletions(-) [+]
line wrap: on
line diff
--- a/light9/ascoltami/Light9AscoltamiUi.ts	Fri Jun 03 00:41:13 2022 -0700
+++ b/light9/ascoltami/Light9AscoltamiUi.ts	Fri Jun 03 02:19:47 2022 -0700
@@ -1,14 +1,19 @@
 import debug from "debug";
 import { css, html, LitElement } from "lit";
 import { customElement, property } from "lit/decorators.js";
+import { classMap } from "lit/directives/class-map.js";
 import { NamedNode } from "n3";
+import Sylvester from "sylvester";
+import { Zoom } from "../web/light9-timeline-audio";
+import { PlainViewState } from "../web/Light9CursorCanvas";
 import { getTopGraph } from "../web/RdfdbSyncedGraph";
 import { SyncedGraph } from "../web/SyncedGraph";
-export { RdfdbSyncedGraph } from "../web/RdfdbSyncedGraph";
+import { TimingUpdate } from "./main";
 export { Light9TimelineAudio } from "../web/light9-timeline-audio";
-import { classMap } from "lit/directives/class-map.js";
-import { TimingUpdate } from "./main";
-import { Zoom } from "../web/light9-timeline-audio";
+export { Light9CursorCanvas } from "../web/Light9CursorCanvas";
+export { RdfdbSyncedGraph } from "../web/RdfdbSyncedGraph";
+
+const $V = Sylvester.Vector.create;
 
 debug.enable("*");
 const log = debug("asco");
@@ -31,18 +36,30 @@
   @property() isPlaying: boolean = false;
   @property() show: NamedNode | null = null;
   @property() song: NamedNode | null = null;
-  @property() t: number = 0;
   @property() currentDuration: number = 0;
   @property() zoom: Zoom;
   @property() overviewZoom: Zoom;
+  @property() viewState: PlainViewState | null = null;
   static styles = [
     css`
       .timeRow {
         margin: 14px;
+        position: relative;
       }
-      light9-timeline-audio {
+      #overview {
+        height: 60px;
+      }
+      #zoomed {
+        margin-top: 40px;
         height: 80px;
       }
+      #cursor {
+        position: absolute;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+      }
     `,
   ];
   render() {
@@ -54,8 +71,9 @@
 
       <div class="timeRow">
         <div id="timeSlider"></div>
-        <light9-timeline-audio .show=${this.show} .song=${this.song} .zoom=${this.overviewZoom}></light9-timeline-audio>
-        <light9-timeline-audio .show=${this.show} .song=${this.song} .zoom=${this.zoom}></light9-timeline-audio>
+        <light9-timeline-audio id="overview" .show=${this.show} .song=${this.song} .zoom=${this.overviewZoom}></light9-timeline-audio>
+        <light9-timeline-audio id="zoomed" .show=${this.show} .song=${this.song} .zoom=${this.zoom}></light9-timeline-audio>
+        <light9-cursor-canvas id="cursor" .viewState=${this.viewState}></light9-cursor-canvas>
       </div>
 
       <div class="commands">
@@ -149,7 +167,22 @@
       this.currentDuration = data.duration;
       this.song = new NamedNode(data.song);
       this.overviewZoom = { duration: data.duration, t1: 0, t2: data.duration };
-      this.zoom = { duration: data.duration, t1: data.t - 2, t2: data.t + 20 };
+      const t1 = data.t - 2,
+        t2 = data.t + 20;
+      this.zoom = { duration: data.duration, t1, t2 };
+      const timeRow = this.shadowRoot!.querySelector(".timeRow") as HTMLDivElement;
+      const w = timeRow.offsetWidth;
+      this.viewState = {
+        zoomSpec: { t1: () => t1, t2: () => t2 },
+        cursor: { t: () => data.t },
+        audioY: () => 0,
+        audioH: () => 60,
+        zoomedTimeY: () => 60,
+        zoomedTimeH: () => 40,
+        fullZoomX: (sec: number) => (sec / data.duration) * w,
+        zoomInX: (sec: number) => ((sec - t1) / (t2 - t1)) * w,
+        mouse: { pos: () => $V([0, 0]) },
+      };
     });
   }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/web/Light9CursorCanvas.ts	Fri Jun 03 02:19:47 2022 -0700
@@ -0,0 +1,146 @@
+import debug from "debug";
+import { css, html, LitElement, PropertyValues } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import Sylvester from "sylvester";
+import { line } from "./drawing";
+
+const $V = Sylvester.Vector.create;
+
+const log = debug("cursor");
+
+export interface PlainViewState {
+  zoomSpec: { t1: () => number; t2: () => number };
+  fullZoomX: (t: number) => number;
+  zoomInX: (t: number) => number;
+  cursor: { t: () => number };
+  audioY: () => number;
+  audioH: () => number;
+  zoomedTimeY: () => number; // not what you think- it's the zone in between
+  zoomedTimeH: () => number;
+  mouse: { pos: () => Vector };
+}
+
+// For cases where you have a zoomed-out view on top of a zoomed-in view,
+// overlay this element and it'll draw a time cursor on both views.
+@customElement("light9-cursor-canvas")
+export class Light9CursorCanvas extends LitElement {
+  cursorPath: null | {
+    top0: Vector;
+    top1: Vector;
+    mid0: Vector;
+    mid1: Vector;
+    mid2: Vector;
+    mid3: Vector;
+    bot0: Vector;
+    bot1: Vector;
+  } = null;
+  canvasEl!: HTMLCanvasElement;
+  ctx!: CanvasRenderingContext2D;
+  offsetWidth: any;
+  offsetHeight: any;
+  @property() viewState: PlainViewState | null = null;
+  static styles = [
+    css`
+      :host {
+        display: inline-block;
+      }
+    `,
+  ];
+  render() {
+    return html`<canvas></canvas>`;
+  }
+
+  updated(changedProperties: PropertyValues) {
+    if (changedProperties.has("viewState")) {
+      this.redrawCursor();
+    }
+  }
+  connectedCallback() {
+    super.connectedCallback();
+    window.addEventListener("resize", this.onResize);
+    this.onResize();
+  }
+
+  firstUpdated() {
+    this.canvasEl = this.shadowRoot!.firstElementChild as HTMLCanvasElement;
+    this.onResize();
+    this.ctx = this.canvasEl.getContext("2d")!;
+  }
+
+  disconnectedCallback() {
+    window.removeEventListener("resize", this.onResize);
+    super.disconnectedCallback();
+  }
+
+  // onViewState() {
+  //   ko.computed(this.redrawCursor.bind(this));
+  // }
+
+  onResize() {
+    if (!this.canvasEl) {
+      return;
+    }
+    this.canvasEl.width = this.offsetWidth;
+    this.canvasEl.height = this.offsetHeight;
+    this.redrawCursor();
+  }
+
+  redrawCursor() {
+    const vs = this.viewState;
+    if (!vs) {
+      return;
+    }
+    const dependOn = [vs.zoomSpec.t1(), vs.zoomSpec.t2()];
+    const xZoomedOut = vs.fullZoomX(vs.cursor.t());
+    const xZoomedIn = vs.zoomInX(vs.cursor.t());
+
+    this.cursorPath = {
+      top0: $V([xZoomedOut, vs.audioY()]),
+      top1: $V([xZoomedOut, vs.audioY() + vs.audioH()]),
+      mid0: $V([xZoomedIn + 2, vs.zoomedTimeY() + vs.zoomedTimeH()]),
+      mid1: $V([xZoomedIn - 2, vs.zoomedTimeY() + vs.zoomedTimeH()]),
+      mid2: $V([xZoomedOut - 1, vs.audioY() + vs.audioH()]),
+      mid3: $V([xZoomedOut + 1, vs.audioY() + vs.audioH()]),
+      bot0: $V([xZoomedIn, vs.zoomedTimeY() + vs.zoomedTimeH()]),
+      bot1: $V([xZoomedIn, this.offsetHeight]),
+    };
+    this.redraw();
+  }
+
+  redraw() {
+    if (!this.ctx || !this.viewState) {
+      return;
+    }
+    this.ctx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
+
+    this.ctx.strokeStyle = "#fff";
+    this.ctx.lineWidth = 0.5;
+    this.ctx.beginPath();
+    const mouse = this.viewState.mouse.pos();
+    line(this.ctx, $V([0, mouse.e(2)]), $V([this.canvasEl.width, mouse.e(2)]));
+    line(this.ctx, $V([mouse.e(1), 0]), $V([mouse.e(1), this.canvasEl.height]));
+    this.ctx.stroke();
+
+    if (this.cursorPath) {
+      this.ctx.strokeStyle = "#ff0303";
+      this.ctx.lineWidth = 1.5;
+      this.ctx.beginPath();
+      line(this.ctx, this.cursorPath.top0, this.cursorPath.top1);
+      this.ctx.stroke();
+
+      this.ctx.fillStyle = "#9c0303";
+      this.ctx.beginPath();
+      this.ctx.moveTo(this.cursorPath.mid0.e(1), this.cursorPath.mid0.e(2));
+      for (let p of [this.cursorPath.mid1, this.cursorPath.mid2, this.cursorPath.mid3]) {
+        this.ctx.lineTo(p.e(1), p.e(2));
+      }
+      this.ctx.fill();
+
+      this.ctx.strokeStyle = "#ff0303";
+      this.ctx.lineWidth = 3;
+      this.ctx.beginPath();
+      line(this.ctx, this.cursorPath.bot0, this.cursorPath.bot1, "#ff0303", "3px");
+      this.ctx.stroke();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/web/drawing.ts	Fri Jun 03 02:19:47 2022 -0700
@@ -0,0 +1,64 @@
+
+export function svgPathFromPoints(pts: { forEach: (arg0: (p: any) => void) => void }) {
+  let out = "";
+  pts.forEach(function (p: Number[] | { elements: Number[] }) {
+    let x, y;
+    if ((p as any).elements) {
+      // for vec2
+      [x, y] = (p as any).elements;
+    } else {
+      [x, y] = p as Number[];
+    }
+    if (out.length === 0) {
+      out = "M ";
+    } else {
+      out += "L ";
+    }
+    out += "" + x + "," + y + " ";
+  });
+  return out;
+};
+
+export function line(
+  ctx: { moveTo: (arg0: any, arg1: any) => void; lineTo: (arg0: any, arg1: any) => any },
+  p1: { e: (arg0: number) => any },
+  p2: { e: (arg0: number) => any }
+) {
+  ctx.moveTo(p1.e(1), p1.e(2));
+  return ctx.lineTo(p2.e(1), p2.e(2));
+};
+
+// http://stackoverflow.com/a/4959890
+export function roundRect(
+  ctx: {
+    beginPath: () => void;
+    moveTo: (arg0: any, arg1: any) => void;
+    lineTo: (arg0: number, arg1: number) => void;
+    arc: (arg0: number, arg1: number, arg2: any, arg3: number, arg4: number, arg5: boolean) => void;
+    closePath: () => any;
+  },
+  sx: number,
+  sy: number,
+  ex: number,
+  ey: number,
+  r: number
+) {
+  const d2r = Math.PI / 180;
+  if (ex - sx - 2 * r < 0) {
+    r = (ex - sx) / 2;
+  } // ensure that the radius isn't too large for x
+  if (ey - sy - 2 * r < 0) {
+    r = (ey - sy) / 2;
+  } // ensure that the radius isn't too large for y
+  ctx.beginPath();
+  ctx.moveTo(sx + r, sy);
+  ctx.lineTo(ex - r, sy);
+  ctx.arc(ex - r, sy + r, r, d2r * 270, d2r * 360, false);
+  ctx.lineTo(ex, ey - r);
+  ctx.arc(ex - r, ey - r, r, d2r * 0, d2r * 90, false);
+  ctx.lineTo(sx + r, ey);
+  ctx.arc(sx + r, ey - r, r, d2r * 90, d2r * 180, false);
+  ctx.lineTo(sx, sy + r);
+  ctx.arc(sx + r, sy + r, r, d2r * 180, d2r * 270, false);
+  return ctx.closePath();
+};
--- a/light9/web/light9-timeline-audio.ts	Fri Jun 03 00:41:13 2022 -0700
+++ b/light9/web/light9-timeline-audio.ts	Fri Jun 03 02:19:47 2022 -0700
@@ -1,9 +1,7 @@
 import { debug } from "debug";
-
-import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
+import { html, LitElement, PropertyValues } from "lit";
 import { customElement, property, state } from "lit/decorators.js";
 import { NamedNode } from "n3";
-import { loadConfigFromFile } from "vite";
 import { getTopGraph } from "./RdfdbSyncedGraph";
 import { SyncedGraph } from "./SyncedGraph";
 
@@ -46,8 +44,7 @@
         img {
           height: 100%;
           position: relative;
-          transition: left .1s linear;
-
+          transition: left 0.1s linear;
         }
       </style>
       <div>
--- a/light9/web/timeline/adjusters.ts	Fri Jun 03 00:41:13 2022 -0700
+++ b/light9/web/timeline/adjusters.ts	Fri Jun 03 02:19:47 2022 -0700
@@ -4,7 +4,7 @@
 import { throttle } from "underscore";
 import * as d3 from "d3";
 import { Adjustable } from "./adjustable";
-import * as Drawing from "./drawing";
+import * as Drawing from "../drawing";
 // https://www.npmjs.com/package/@types/sylvester Global values: $L, $M, $P, $V, Line, Matrix, Plane, Sylvester, Vector
 const log = debug("adjusters");
 
--- a/light9/web/timeline/cursor_canvas.coffee	Fri Jun 03 00:41:13 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-coffeeElementSetup(class CursorCanvas extends Polymer.mixinBehaviors([Polymer.IronResizableBehavior], Polymer.Element)
-  @is: 'light9-cursor-canvas'
-  @getter_properties:
-    viewState: { type: Object, notify: true, observer: "onViewState" }
-  ready: ->
-    super.ready()
-    @cursorPath = null
-    @ctx = @$.canvas.getContext('2d')
-    @onResize()
-    @addEventListener('iron-resize', @onResize.bind(@))
-
-  onViewState: ->
-    ko.computed(@redrawCursor.bind(@))
-
-  onResize: (ev) ->
-    @$.canvas.width = @offsetWidth
-    @$.canvas.height = @offsetHeight
-    @redrawCursor()
-
-  redrawCursor: ->
-    vs = @viewState
-    dependOn = [vs.zoomSpec.t1(), vs.zoomSpec.t2()]
-    xZoomedOut = vs.fullZoomX(vs.cursor.t())
-    xZoomedIn = vs.zoomInX(vs.cursor.t())
-
-    @cursorPath = {
-      top0: $V([xZoomedOut, vs.audioY()])
-      top1: $V([xZoomedOut, vs.audioY() + vs.audioH()])
-      mid0: $V([xZoomedIn + 2, vs.zoomedTimeY() + vs.zoomedTimeH()])
-      mid1: $V([xZoomedIn - 2, vs.zoomedTimeY() + vs.zoomedTimeH()])
-      mid2: $V([xZoomedOut - 1, vs.audioY() + vs.audioH()])
-      mid3: $V([xZoomedOut + 1, vs.audioY() + vs.audioH()])
-      bot0: $V([xZoomedIn, vs.zoomedTimeY() + vs.zoomedTimeH()])
-      bot1: $V([xZoomedIn, @offsetHeight])
-    }
-    @redraw()
-
-  redraw: ->
-    return unless @ctx
-    @ctx.clearRect(0, 0, @$.canvas.width, @$.canvas.height)
-
-    @ctx.strokeStyle = '#fff'
-    @ctx.lineWidth = 0.5
-    @ctx.beginPath()
-    mouse = @viewState.mouse.pos()
-    Drawing.line(@ctx, $V([0, mouse.e(2)]), $V([@$.canvas.width, mouse.e(2)]))
-    Drawing.line(@ctx, $V([mouse.e(1), 0]), $V([mouse.e(1), @$.canvas.height]))
-    @ctx.stroke()
-
-    if @cursorPath
-      @ctx.strokeStyle = '#ff0303'
-      @ctx.lineWidth = 1.5
-      @ctx.beginPath()
-      Drawing.line(@ctx, @cursorPath.top0, @cursorPath.top1)
-      @ctx.stroke()
-
-      @ctx.fillStyle = '#9c0303'
-      @ctx.beginPath()
-      @ctx.moveTo(@cursorPath.mid0.e(1), @cursorPath.mid0.e(2))
-      @ctx.lineTo(p.e(1), p.e(2)) for p in [
-        @cursorPath.mid1, @cursorPath.mid2, @cursorPath.mid3]
-      @ctx.fill()
-      
-      @ctx.strokeStyle = '#ff0303'
-      @ctx.lineWidth = 3
-      @ctx.beginPath()
-      Drawing.line(@ctx, @cursorPath.bot0, @cursorPath.bot1, '#ff0303', '3px')
-      @ctx.stroke()
-)
\ No newline at end of file
--- a/light9/web/timeline/drawing.ts	Fri Jun 03 00:41:13 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-
-export function svgPathFromPoints(pts: { forEach: (arg0: (p: any) => void) => void }) {
-  let out = "";
-  pts.forEach(function (p: Number[] | { elements: Number[] }) {
-    let x, y;
-    if ((p as any).elements) {
-      // for vec2
-      [x, y] = (p as any).elements;
-    } else {
-      [x, y] = p as Number[];
-    }
-    if (out.length === 0) {
-      out = "M ";
-    } else {
-      out += "L ";
-    }
-    out += "" + x + "," + y + " ";
-  });
-  return out;
-};
-
-export function line(
-  ctx: { moveTo: (arg0: any, arg1: any) => void; lineTo: (arg0: any, arg1: any) => any },
-  p1: { e: (arg0: number) => any },
-  p2: { e: (arg0: number) => any }
-) {
-  ctx.moveTo(p1.e(1), p1.e(2));
-  return ctx.lineTo(p2.e(1), p2.e(2));
-};
-
-// http://stackoverflow.com/a/4959890
-export function roundRect(
-  ctx: {
-    beginPath: () => void;
-    moveTo: (arg0: any, arg1: any) => void;
-    lineTo: (arg0: number, arg1: number) => void;
-    arc: (arg0: number, arg1: number, arg2: any, arg3: number, arg4: number, arg5: boolean) => void;
-    closePath: () => any;
-  },
-  sx: number,
-  sy: number,
-  ex: number,
-  ey: number,
-  r: number
-) {
-  const d2r = Math.PI / 180;
-  if (ex - sx - 2 * r < 0) {
-    r = (ex - sx) / 2;
-  } // ensure that the radius isn't too large for x
-  if (ey - sy - 2 * r < 0) {
-    r = (ey - sy) / 2;
-  } // ensure that the radius isn't too large for y
-  ctx.beginPath();
-  ctx.moveTo(sx + r, sy);
-  ctx.lineTo(ex - r, sy);
-  ctx.arc(ex - r, sy + r, r, d2r * 270, d2r * 360, false);
-  ctx.lineTo(ex, ey - r);
-  ctx.arc(ex - r, ey - r, r, d2r * 0, d2r * 90, false);
-  ctx.lineTo(sx + r, ey);
-  ctx.arc(sx + r, ey - r, r, d2r * 90, d2r * 180, false);
-  ctx.lineTo(sx, sy + r);
-  ctx.arc(sx + r, sy + r, r, d2r * 180, d2r * 270, false);
-  return ctx.closePath();
-};