[Modern] Specific code for different slider actions

This commit is contained in:
Jordan Eldredge 2021-09-11 00:41:45 -07:00
parent 497550a2fb
commit 3c34824e9e
5 changed files with 239 additions and 24 deletions

View file

@ -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

View file

@ -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}`);
}

View file

@ -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);

View file

@ -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 {

View file

@ -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();
}
}