From 3c34824e9ea95b49d5a8b170d22e18e80aa02c9b Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sat, 11 Sep 2021 00:41:45 -0700 Subject: [PATCH] [Modern] Specific code for different slider actions --- packages/webamp-modern-2/README.md | 1 + packages/webamp-modern-2/src/UIRoot.ts | 3 - .../webamp-modern-2/src/skin/AudioPlayer.ts | 88 +++++++++++ .../src/skin/makiClasses/GuiObj.ts | 24 +++ .../src/skin/makiClasses/Slider.ts | 147 +++++++++++++++--- 5 files changed, 239 insertions(+), 24 deletions(-) diff --git a/packages/webamp-modern-2/README.md b/packages/webamp-modern-2/README.md index 0c1ca494..8272b7f0 100644 --- a/packages/webamp-modern-2/README.md +++ b/packages/webamp-modern-2/README.md @@ -35,6 +35,7 @@ yarn start - [ ] Fix all `// FIXME` - [ ] SystemObject.getruntimeversion - [ ] SystemObject.getskinname +- [ ] Handle clicking through transparent: https://stackoverflow.com/questions/38487569/click-through-png-image-only-if-clicked-coordinate-is-transparent # TODO Some day diff --git a/packages/webamp-modern-2/src/UIRoot.ts b/packages/webamp-modern-2/src/UIRoot.ts index ad6e4df0..bf7f99c0 100644 --- a/packages/webamp-modern-2/src/UIRoot.ts +++ b/packages/webamp-modern-2/src/UIRoot.ts @@ -212,9 +212,6 @@ export class UIRoot { case "eject": this.audio.eject(); break; - case "seek": - this.audio.seekToPercent((param as number) / 255); - break; default: assume(false, `Unknown global action: ${action}`); } diff --git a/packages/webamp-modern-2/src/skin/AudioPlayer.ts b/packages/webamp-modern-2/src/skin/AudioPlayer.ts index 9bc22d48..33d449d9 100644 --- a/packages/webamp-modern-2/src/skin/AudioPlayer.ts +++ b/packages/webamp-modern-2/src/skin/AudioPlayer.ts @@ -1,6 +1,32 @@ +import { clamp } from "../utils"; + +class Emitter { + _cbs: { [event: string]: Array<() => void> } = {}; + on(event: string, cb: () => void) { + if (this._cbs[event] == null) { + this._cbs[event] = []; + } + this._cbs[event].push(cb); + return () => { + this._cbs[event] = this._cbs[event].filter((c) => c !== cb); + }; + } + trigger(event: string) { + const subscriptions = this._cbs[event]; + if (subscriptions == null) { + return; + } + for (const cb of subscriptions) { + cb(); + } + } +} + export class AudioPlayer { _input: HTMLInputElement = document.createElement("input"); _audio: HTMLAudioElement = document.createElement("audio"); + _eqValues: { [kind: string]: number } = {}; + _eqEmitter: Emitter = new Emitter(); constructor() { this._audio.src = "https://raw.githubusercontent.com/captbaritone/webamp-music/4b556fbf/Auto-Pilot_-_03_-_Seventeen.mp3"; @@ -61,6 +87,68 @@ export class AudioPlayer { return this._audio.currentTime / this._audio.duration; } + getEq(kind: string): number { + switch (kind) { + case "preamp": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + case "10": + if (this._eqValues[kind] == null) { + this._eqValues[kind] = 0.5; + } + return this._eqValues[kind]; + default: + console.warn(`Tried to get unknown EQ kind: ${kind}`); + } + } + + setEq(kind: string, value: number) { + switch (kind) { + case "preamp": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + case "10": + this._eqValues[kind] = clamp(value, 0, 1); + this._eqEmitter.trigger(kind); + break; + default: + console.warn(`Tried to set unknown EQ kind: ${kind}`); + } + } + + onEqChange(kind: string, cb: () => void): () => void { + switch (kind) { + case "preamp": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + case "10": + return this._eqEmitter.on(kind, cb); + default: + console.warn(`Tried to bind to an unknown EQ kind: ${kind}`); + } + } + onCurrentTimeChange(cb: () => void): () => void { const handler = () => cb(); this._audio.addEventListener("timeupdate", handler); diff --git a/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts b/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts index dd12a81c..93f6da8f 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/GuiObj.ts @@ -25,6 +25,7 @@ export default class GuiObj extends XmlObj { _targetAlpha: number | null = null; _targetSpeed: number | null = null; _div: HTMLDivElement = document.createElement("div"); + _backgroundBitmap: Bitmap | null = null; constructor() { super(); @@ -33,6 +34,28 @@ export default class GuiObj extends XmlObj { }); this._div.addEventListener("mousedown", (e) => { + if (this._backgroundBitmap != null) { + const { clientX, clientY } = e; + const { x, y } = this._div.getBoundingClientRect(); + const canvasX = clientX - x; + const canvasY = clientY - y; + const canvas = this._backgroundBitmap.getCanvas(); + const ctx = canvas.getContext("2d"); + + const opacity = ctx.getImageData(canvasX, canvasY, 1, 1).data[3]; + if (opacity === 0) { + this._div.style.pointerEvents = "none"; + const newTarget = document.elementFromPoint(clientX, clientY); + this._div.style.pointerEvents = "auto"; + var newEvent = new MouseEvent("click", { + clientX, + clientY, + bubbles: true, + }); + newTarget.dispatchEvent(newEvent); + } + } + // this.onLeftButtonDown(e.clientX, e.clientY); }); this._div.addEventListener("mouseenter", (e) => { @@ -511,6 +534,7 @@ export default class GuiObj extends XmlObj { } setBackgroundImage(bitmap: Bitmap | null) { + this._backgroundBitmap = bitmap; if (bitmap != null) { bitmap.setAsBackground(this._div); } else { diff --git a/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts b/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts index f8ec78ab..7c23afba 100644 --- a/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts +++ b/packages/webamp-modern-2/src/skin/makiClasses/Slider.ts @@ -2,6 +2,13 @@ import UI_ROOT from "../../UIRoot"; import { assume, clamp, num, px } from "../../utils"; import GuiObj from "./GuiObj"; +interface ActionHandler { + // 0-255 + onsetposition(position: number): void; + dispose(): void; +} +const MAX = 255; + // http://wiki.winamp.com/wiki/XML_GUI_Objects#.3Cslider.2F.3E_.26_.3CWasabi:HSlider.2F.3E_.26_.3CWasabi:VSlider.2F.3E export default class Slider extends GuiObj { static GUID = "62b65e3f408d375e8176ea8d771bb94a"; @@ -16,13 +23,13 @@ export default class Slider extends GuiObj { _low: number; _high: number; _position: number = 0; + _param: string | null = null; _thumbDiv: HTMLDivElement = document.createElement("div"); - _actionSubscription: null | (() => void); + _actionHandler: null | ActionHandler; constructor() { super(); this._thumbDiv.addEventListener("mousedown", (downEvent: MouseEvent) => { - const rect = this._div.getBoundingClientRect(); const bitmap = UI_ROOT.getBitmap(this._thumb); const startX = downEvent.clientX; const startY = downEvent.clientY; @@ -102,51 +109,67 @@ export default class Slider extends GuiObj { case "action": this._setAction(value); break; + case "param": + // Undocumented? In MMD3 for EQ (eq_band) action, this seems to indicate _which_ band. + this._param = value; + break; default: return false; } return true; } - _setAction(value: string) { - if (this._actionSubscription != null) { - this._actionSubscription(); - this._actionSubscription = null; - } - this._action = value.toLowerCase(); + init() { + this._initializeActionHandler(); + } + + _initializeActionHandler() { switch (this._action) { - case "seek": { - const update = () => { - this._position = UI_ROOT.audio.getCurrentTimePercent(); - // TODO: We could throttle this, or only render if the change is "significant"? - this._renderThumbPosition(); - }; - update(); - this._actionSubscription = UI_ROOT.audio.onCurrentTimeChange(update); + case "seek": + this._actionHandler = new SeekActionHandler(this); break; - } case "eq_band": + this._actionHandler = new EqActionHandler(this, this._param); break; case "pan": + this._actionHandler = new PanActionHandler(this); break; case "volume": + this._actionHandler = new VolumeActionHandler(this); + break; + case null: break; default: assume(false, `Unhandled slider action: ${this._action}`); } } + _setAction(value: string) { + if (this._actionHandler != null) { + this._actionHandler.dispose(); + this._actionHandler = null; + } + this._action = value.toLowerCase(); + + // If we've already initialized we might have an action handler already. In + // that case, we want to reinitialize. + if (this._actionHandler != null) { + this._actionHandler.dispose(); + this._initializeActionHandler(); + } + } + // extern Int Slider.getPosition(); getposition(): number { - return this._position * 255; + return this._position * MAX; } onsetposition(newPos: number) { UI_ROOT.vm.dispatch(this, "onsetposition", [ { type: "INT", value: newPos }, ]); - if (this._action) { - UI_ROOT.dispatch(this._action, this.getposition(), null); + if (this._actionHandler != null) { + this._actionHandler.onsetposition(newPos); } } @@ -198,8 +221,10 @@ export default class Slider extends GuiObj { } dispose() { + if (this._actionHandler) { + this._actionHandler.dispose(); + } super.dispose(); - this._actionSubscription(); } /* @@ -210,3 +235,83 @@ export default class Slider extends GuiObj { extern Slider.unlock(); // unloads the */ } + +/***** + * Here we have the action handlers for the different action types: + * Each one takes a reference to the slider and adds some extra behavior. + * It's a bit odd that they access pirvate fields/methods, but since they live + * in the same file I've allowed myself the sin of doing that. + **/ + +// eslint-disable-next-line rulesdir/proper-maki-types +class SeekActionHandler implements ActionHandler { + _subscription: () => void; + constructor(slider: Slider) { + const update = () => { + slider._position = UI_ROOT.audio.getCurrentTimePercent(); + // TODO: We could throttle this, or only render if the change is "significant"? + slider._renderThumbPosition(); + }; + update(); + this._subscription = UI_ROOT.audio.onCurrentTimeChange(update); + } + + onsetposition(position: number): void { + UI_ROOT.audio.seekToPercent(position / MAX); + } + dispose(): void { + this._subscription(); + } +} + +// eslint-disable-next-line rulesdir/proper-maki-types +class EqActionHandler implements ActionHandler { + _subscription: () => void; + _kind: string; + constructor(slider: Slider, kind: string) { + this._kind = kind; + const update = () => { + slider._position = UI_ROOT.audio.getEq(kind); + slider._renderThumbPosition(); + }; + update(); + this._subscription = UI_ROOT.audio.onEqChange(kind, update); + } + + onsetposition(position: number): void { + UI_ROOT.audio.setEq(this._kind, position / MAX); + } + dispose(): void { + this._subscription(); + } +} + +// eslint-disable-next-line rulesdir/proper-maki-types +class PanActionHandler implements ActionHandler { + _subscription: () => void; + constructor(slider: Slider) { + // TODO + } + + onsetposition(position: number): void { + // TODO + } + dispose(): void { + this._subscription(); + } +} + +// eslint-disable-next-line rulesdir/proper-maki-types +class VolumeActionHandler implements ActionHandler { + _subscription: () => void; + constructor(slider: Slider) { + // TODO + } + + onsetposition(position: number): void { + // TODO + } + dispose(): void { + this._subscription(); + } +}