mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-24 02:36:00 +00:00
277 lines
7.9 KiB
JavaScript
277 lines
7.9 KiB
JavaScript
import React from "react";
|
|
import { connect } from "react-redux";
|
|
import { getCurrentTrackDisplayName } from "../../selectors";
|
|
import DropTarget from "../DropTarget";
|
|
import PresetOverlay from "./PresetOverlay";
|
|
|
|
const USER_PRESET_TRANSITION_SECONDS = 5.7;
|
|
const PRESET_TRANSITION_SECONDS = 2.7;
|
|
const MILLISECONDS_BETWEEN_PRESET_TRANSITIONS = 15000;
|
|
|
|
class Milkdrop extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.__debugState = "CONSTRUCTED";
|
|
this.__updates = 0;
|
|
this.state = {
|
|
isFullscreen: false,
|
|
presetOverlay: false
|
|
};
|
|
}
|
|
|
|
async componentDidMount() {
|
|
this.__debugState = "MOUNT_STARTED";
|
|
this.visualizer = this.props.butterchurn.createVisualizer(
|
|
this.props.analyser.context,
|
|
this._canvasNode,
|
|
{
|
|
width: this.props.width,
|
|
height: this.props.height,
|
|
meshWidth: 32,
|
|
meshHeight: 24,
|
|
pixelRatio: window.devicePixelRatio || 1
|
|
}
|
|
);
|
|
if (this.visualizer == null) {
|
|
// https://github.com/captbaritone/webamp/issues/731
|
|
throw new Error("Visualizer not initialized. WAT.");
|
|
}
|
|
this.__debugState = "VISUALIZER_CREATED";
|
|
this.visualizer.connectAudio(this.props.analyser);
|
|
this.presetCycle = !this.props.initialPreset;
|
|
if (this.props.initialPreset) {
|
|
const presetIndices = this.props.presets.addPresets(
|
|
this.props.initialPreset
|
|
);
|
|
this.selectPreset(
|
|
await this.props.presets.selectIndex(presetIndices[0]),
|
|
0
|
|
);
|
|
} else {
|
|
this.selectPreset(this.props.presets.getCurrent(), 0);
|
|
}
|
|
|
|
// Kick off the animation loop
|
|
const loop = () => {
|
|
if (this.props.playing && this.props.isEnabledVisualizer) {
|
|
this.visualizer.render();
|
|
}
|
|
this._animationFrameRequest = window.requestAnimationFrame(loop);
|
|
};
|
|
loop();
|
|
|
|
this._unsubscribeFocusedKeyDown = this.props.onFocusedKeyDown(
|
|
this._handleFocusedKeyboardInput
|
|
);
|
|
if (this.visualizer == null) {
|
|
// https://github.com/captbaritone/webamp/issues/731
|
|
throw new Error("Visualizer not initialized after await. WAT.");
|
|
}
|
|
this.__debugState = "MOUNT_ENDED";
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.__debugState = "WILL_UNMOUNT";
|
|
this._pauseViz();
|
|
this._stopCycling();
|
|
if (this._unsubscribeFocusedKeyDown) {
|
|
this._unsubscribeFocusedKeyDown();
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
this.__updates++;
|
|
if (this.visualizer == null) {
|
|
// https://github.com/captbaritone/webamp/issues/731
|
|
throw new Error(
|
|
`Weird bug: State=${this.__debugState} updates=${this.__updates}`
|
|
);
|
|
}
|
|
if (
|
|
this.props.width !== prevProps.width ||
|
|
this.props.height !== prevProps.height
|
|
) {
|
|
this.visualizer.setRendererSize(this.props.width, this.props.height);
|
|
}
|
|
|
|
if (this.props.trackTitle !== prevProps.trackTitle) {
|
|
this.visualizer.launchSongTitleAnim(this.props.trackTitle);
|
|
}
|
|
}
|
|
|
|
_pauseViz() {
|
|
if (this._animationFrameRequest) {
|
|
window.cancelAnimationFrame(this._animationFrameRequest);
|
|
this._animationFrameRequest = null;
|
|
}
|
|
}
|
|
|
|
_stopCycling() {
|
|
if (this.cycleInterval) {
|
|
clearInterval(this.cycleInterval);
|
|
this.cycleInterval = null;
|
|
}
|
|
}
|
|
|
|
_restartCycling() {
|
|
this._stopCycling();
|
|
|
|
if (this.presetCycle) {
|
|
this.cycleInterval = setInterval(() => {
|
|
this._nextPreset(PRESET_TRANSITION_SECONDS);
|
|
}, MILLISECONDS_BETWEEN_PRESET_TRANSITIONS);
|
|
}
|
|
}
|
|
|
|
_handleFocusedKeyboardInput = e => {
|
|
switch (e.keyCode) {
|
|
case 32: // spacebar
|
|
this._nextPreset(USER_PRESET_TRANSITION_SECONDS);
|
|
break;
|
|
case 8: // backspace
|
|
this._prevPreset(0);
|
|
break;
|
|
case 72: // H
|
|
this._nextPreset(0);
|
|
break;
|
|
case 82: // R
|
|
this.props.presets.toggleRandomize();
|
|
break;
|
|
case 76: // L
|
|
this.setState({ presetOverlay: !this.state.presetOverlay });
|
|
e.stopPropagation();
|
|
break;
|
|
case 84: // T
|
|
this.visualizer.launchSongTitleAnim(this.props.trackTitle);
|
|
e.stopPropagation();
|
|
break;
|
|
case 145: // scroll lock
|
|
case 125: // F14 (scroll lock for OS X)
|
|
this.presetCycle = !this.presetCycle;
|
|
this._restartCycling();
|
|
break;
|
|
}
|
|
};
|
|
|
|
_addNewPresets(files) {
|
|
const presetKeys = this.props.presets.getKeys();
|
|
const presets = {};
|
|
const presetIndices = [];
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
const fileName = file.name;
|
|
const presetName = fileName.substring(fileName, fileName.length - 5); // remove .milk
|
|
const presetIdx = presetKeys.indexOf(presetName);
|
|
if (presetIdx >= 0) {
|
|
presetIndices.push(presetIdx);
|
|
} else {
|
|
presets[presetName] = { file };
|
|
}
|
|
}
|
|
|
|
if (Object.keys(presets).length > 0) {
|
|
const filePresetIndices = this.props.presets.addPresets(presets);
|
|
for (let j = filePresetIndices[0]; j < filePresetIndices[1]; j++) {
|
|
presetIndices.push(j);
|
|
}
|
|
}
|
|
|
|
return presetIndices;
|
|
}
|
|
|
|
async _handleDrop(e) {
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
const milkFiles = Array.from(files).filter(file =>
|
|
file.name.endsWith(".milk")
|
|
);
|
|
if (milkFiles.length === 0) {
|
|
alert("Visualizer only supports .milk files");
|
|
return;
|
|
}
|
|
|
|
const presetIndices = this._addNewPresets(milkFiles);
|
|
this.selectPreset(
|
|
await this.props.presets.selectIndex(presetIndices[0]),
|
|
PRESET_TRANSITION_SECONDS
|
|
);
|
|
}
|
|
}
|
|
|
|
async _nextPreset(blendTime) {
|
|
this.selectPreset(await this.props.presets.next(), blendTime);
|
|
}
|
|
|
|
async _prevPreset(blendTime) {
|
|
this.selectPreset(await this.props.presets.previous(), blendTime);
|
|
}
|
|
|
|
selectPreset(preset, blendTime = 0) {
|
|
if (preset != null) {
|
|
this.visualizer.loadPreset(preset, blendTime);
|
|
this._restartCycling();
|
|
// TODO: Kinda weird that we use the passed preset for the visualizer,
|
|
// but set state to the current. Maybe we should just always use the curent..
|
|
this.setState({ currentPreset: this.props.presets.getCurrentIndex() });
|
|
}
|
|
}
|
|
|
|
async loadPresets(presetFiles) {
|
|
const presets = {};
|
|
const milkFiles = Array.from(presetFiles).filter(file =>
|
|
file.name.endsWith(".milk")
|
|
);
|
|
for (let i = 0; i < milkFiles.length; i++) {
|
|
const file = milkFiles[i];
|
|
presets[file.name] = { file };
|
|
}
|
|
const numPresets = this.props.presets.loadPresets(presets);
|
|
this.selectPreset(
|
|
await this.props.presets.selectIndex(
|
|
Math.floor(Math.random() * numPresets)
|
|
),
|
|
PRESET_TRANSITION_SECONDS
|
|
);
|
|
}
|
|
|
|
closePresetOverlay() {
|
|
this.setState({ presetOverlay: false });
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<DropTarget id="milkdrop-window" handleDrop={e => this._handleDrop(e)}>
|
|
{this.state.presetOverlay && (
|
|
<PresetOverlay
|
|
width={this.props.width}
|
|
height={this.props.height}
|
|
presetKeys={this.props.presets.getKeys()}
|
|
currentPreset={this.state.currentPreset}
|
|
onFocusedKeyDown={listener => this.props.onFocusedKeyDown(listener)}
|
|
selectPreset={async idx => {
|
|
this.selectPreset(await this.props.presets.selectIndex(idx), 0);
|
|
}}
|
|
loadPresets={async presetFiles => this.loadPresets(presetFiles)}
|
|
closeOverlay={() => this.closePresetOverlay()}
|
|
/>
|
|
)}
|
|
<canvas
|
|
height={this.props.height}
|
|
width={this.props.width}
|
|
style={{
|
|
height: "100%",
|
|
width: "100%",
|
|
display: this.props.isEnabledVisualizer ? "block" : "none"
|
|
}}
|
|
ref={node => (this._canvasNode = node)}
|
|
/>
|
|
</DropTarget>
|
|
);
|
|
}
|
|
}
|
|
|
|
const mapStateToProps = state => ({
|
|
trackTitle: getCurrentTrackDisplayName(state)
|
|
});
|
|
|
|
export default connect(mapStateToProps)(Milkdrop);
|