mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-28 04:24:12 +00:00
[Modern] Specific code for different slider actions
This commit is contained in:
parent
497550a2fb
commit
3c34824e9e
5 changed files with 239 additions and 24 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue