mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 10:15:31 +00:00
Refactor Milkdrop
This commit is contained in:
parent
f30ad0ebc9
commit
c89529f442
9 changed files with 577 additions and 275 deletions
|
|
@ -120,6 +120,7 @@ class App extends React.Component {
|
|||
analyser={media.getAnalyser()}
|
||||
width={width}
|
||||
height={height}
|
||||
playing={this.props.status === "PLAYING"}
|
||||
/>
|
||||
)}
|
||||
</GenWindow>
|
||||
|
|
@ -154,6 +155,7 @@ App.propTypes = {
|
|||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
status: state.media.status,
|
||||
focused: state.windows.focused,
|
||||
closed: state.display.closed,
|
||||
genWindowsInfo: state.windows.genWindows
|
||||
|
|
|
|||
26
js/components/MilkdropWindow/Background.js
Normal file
26
js/components/MilkdropWindow/Background.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import React from "react";
|
||||
|
||||
const Background = props => {
|
||||
const { innerRef, ...restProps } = props;
|
||||
return (
|
||||
<div
|
||||
ref={innerRef}
|
||||
className="draggable"
|
||||
style={{
|
||||
// This color will be used until Butterchurn is loaded
|
||||
backgroundColor: "#000",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
}}
|
||||
tabIndex="0"
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Background;
|
||||
202
js/components/MilkdropWindow/Milkdrop.js
Normal file
202
js/components/MilkdropWindow/Milkdrop.js
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import React from "react";
|
||||
import screenfull from "screenfull";
|
||||
import PresetOverlay from "./PresetOverlay";
|
||||
import Background from "./Background";
|
||||
|
||||
const USER_PRESET_TRANSITION_SECONDS = 5.7;
|
||||
const PRESET_TRANSITION_SECONDS = 2.7;
|
||||
const MILLISECONDS_BETWEEN_PRESET_TRANSITIONS = 15000;
|
||||
|
||||
export default class Milkdrop extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isFullscreen: false,
|
||||
presetOverlay: false
|
||||
};
|
||||
this._handleFocusedKeyboardInput = this._handleFocusedKeyboardInput.bind(
|
||||
this
|
||||
);
|
||||
this._handleFullscreenChange = this._handleFullscreenChange.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.setState({ currentPreset: this.props.presets.getCurrentIndex() });
|
||||
this.visualizer = this.props.butterchurn.createVisualizer(
|
||||
this.props.analyser.context,
|
||||
this._canvasNode,
|
||||
{
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
pixelRatio: window.devicePixelRatio || 1
|
||||
}
|
||||
);
|
||||
this._setRendererSize(this.props.width, this.props.height);
|
||||
|
||||
this.visualizer.connectAudio(this.props.analyser);
|
||||
|
||||
// Kick off the animation loop
|
||||
const loop = () => {
|
||||
if (this.props.playing) {
|
||||
this.visualizer.render();
|
||||
}
|
||||
this._animationFrameRequest = window.requestAnimationFrame(loop);
|
||||
};
|
||||
loop();
|
||||
|
||||
this.presetCycle = true;
|
||||
this._unsubscribeFocusedKeyDown = this.props.onFocusedKeyDown(
|
||||
this._handleFocusedKeyboardInput
|
||||
);
|
||||
screenfull.onchange(this._handleFullscreenChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._pauseViz();
|
||||
this._stopCycling();
|
||||
if (this._unsubscribeFocusedKeyDown) {
|
||||
this._unsubscribeFocusedKeyDown();
|
||||
}
|
||||
screenfull.off("change", this._handleFullscreenChange);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.props.width !== prevProps.width ||
|
||||
this.props.height !== prevProps.height
|
||||
) {
|
||||
this._setRendererSize(this.props.width, this.props.height);
|
||||
}
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
_setRendererSize(width, height) {
|
||||
this._canvasNode.width = width;
|
||||
this._canvasNode.height = height;
|
||||
this.visualizer.setRendererSize(width, height);
|
||||
}
|
||||
|
||||
_handleFullscreenChange() {
|
||||
if (screenfull.isFullscreen) {
|
||||
this._setRendererSize(window.innerWidth, window.innerHeight);
|
||||
} else {
|
||||
this._setRendererSize(this.props.width, this.props.height);
|
||||
}
|
||||
this.setState({ isFullscreen: screenfull.isFullscreen });
|
||||
}
|
||||
|
||||
_handleRequestFullsceen() {
|
||||
if (screenfull.enabled) {
|
||||
if (!screenfull.isFullscreen) {
|
||||
screenfull.request(this._wrapperNode);
|
||||
} else {
|
||||
screenfull.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_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 145: // scroll lock
|
||||
case 125: // F14 (scroll lock for OS X)
|
||||
this.presetCycle = !this.presetCycle;
|
||||
this._restartCycling();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
this.setState({ currentPreset: this.props.presets.getCurrentIndex() });
|
||||
}
|
||||
}
|
||||
|
||||
closePresetOverlay() {
|
||||
this.setState({ presetOverlay: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const width = this.state.isFullscreen
|
||||
? window.innerWidth
|
||||
: this.props.width;
|
||||
const height = this.state.isFullscreen
|
||||
? window.innerHeight
|
||||
: this.props.height;
|
||||
return (
|
||||
<Background
|
||||
innerRef={node => (this._wrapperNode = node)}
|
||||
onDoubleClick={() => this._handleRequestFullsceen()}
|
||||
>
|
||||
{this.state.presetOverlay && (
|
||||
<PresetOverlay
|
||||
width={width}
|
||||
height={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);
|
||||
}}
|
||||
closeOverlay={() => this.closePresetOverlay()}
|
||||
/>
|
||||
)}
|
||||
<canvas
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
}}
|
||||
ref={node => (this._canvasNode = node)}
|
||||
/>
|
||||
</Background>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,16 +8,19 @@ class PresetOverlay extends React.Component {
|
|||
this
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._unsubscribeFocusedKeyDown = this.props.onFocusedKeyDown(
|
||||
this._handleFocusedKeyboardInput
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._unsubscribeFocusedKeyDown) {
|
||||
this._unsubscribeFocusedKeyDown();
|
||||
}
|
||||
}
|
||||
|
||||
_handleFocusedKeyboardInput(e) {
|
||||
switch (e.keyCode) {
|
||||
case 38: // up arrow
|
||||
|
|
@ -43,6 +46,7 @@ class PresetOverlay extends React.Component {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.presetKeys) {
|
||||
return (
|
||||
|
|
|
|||
102
js/components/MilkdropWindow/Presets.js
Normal file
102
js/components/MilkdropWindow/Presets.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
function getRandomIndex(arr) {
|
||||
return Math.floor(Math.random() * arr.length);
|
||||
}
|
||||
function getRandomValue(arr) {
|
||||
return arr[getRandomIndex(arr)];
|
||||
}
|
||||
|
||||
function getLast(arr) {
|
||||
return arr[arr.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a collection of async loaded presets
|
||||
*
|
||||
* Presets can be changed via `next`, `previous` or `selectIndex`. In each case,
|
||||
* a promise is returned. If the promise resolves to `null` it means the
|
||||
* selection was canceled by a subsequent request before it could be fulfilled.
|
||||
* If the request is successful, the promise resolves to the selected preset.
|
||||
*
|
||||
* We assume a model where some portion of the preset are supplied at initialization
|
||||
* and the remainder can be loaded async via the function `getRest`.
|
||||
*/
|
||||
export default class Presets {
|
||||
constructor({ keys, initialPresets, getRest, randomize = true }) {
|
||||
this._keys = keys; // Alphabetical list of preset names
|
||||
this._presets = initialPresets; // Presets indexed by name
|
||||
this._getRest = getRest; // An async function to get the rest of the presets
|
||||
this._history = []; // Indexes into _keys
|
||||
|
||||
this._randomize = randomize;
|
||||
|
||||
// Initialize with a key that we already have.
|
||||
const avaliableKeys = Object.keys(initialPresets);
|
||||
const currentKey = randomize
|
||||
? getRandomValue(avaliableKeys)
|
||||
: avaliableKeys[0];
|
||||
this._currentIndex = this._keys.indexOf(currentKey);
|
||||
this._history.push(this._currentIndex);
|
||||
}
|
||||
|
||||
toggleRandomize() {
|
||||
this._randomize = !this._randomize;
|
||||
}
|
||||
|
||||
setRandomize(val) {
|
||||
this._randomize = val;
|
||||
}
|
||||
|
||||
async next() {
|
||||
let idx;
|
||||
if (this._randomize || this._history.length === 0) {
|
||||
idx = getRandomIndex(this._keys);
|
||||
} else {
|
||||
idx = (getLast(this._history) + 1) % this._keys.length;
|
||||
}
|
||||
this._history.push(idx);
|
||||
return this._selectIndex(idx);
|
||||
}
|
||||
|
||||
async previous() {
|
||||
if (this._history.length > 1) {
|
||||
this._history.pop();
|
||||
return this._selectIndex(getLast(this._history));
|
||||
}
|
||||
// We are at the very beginning. There is no "previous" preset.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async selectIndex(idx) {
|
||||
// The public version of this method must add to the history
|
||||
this._history.push(idx);
|
||||
return this._selectIndex(idx);
|
||||
}
|
||||
|
||||
async _selectIndex(idx) {
|
||||
const preset = this._presets[this._keys[idx]];
|
||||
if (!preset) {
|
||||
const rest = await this._getRest();
|
||||
this._presets = Object.assign(this._presets, rest);
|
||||
if (getLast(this._history) !== idx) {
|
||||
// This selection must be obsolete. Return null so that
|
||||
// the caller knows this request got canceled.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
this._currentIndex = idx;
|
||||
return this.getCurrent();
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
return this._keys;
|
||||
}
|
||||
|
||||
getCurrentIndex() {
|
||||
return this._currentIndex;
|
||||
}
|
||||
|
||||
getCurrent() {
|
||||
// #matryoshka
|
||||
return this._presets[this._keys[this._currentIndex]];
|
||||
}
|
||||
}
|
||||
173
js/components/MilkdropWindow/__tests__/Presets.test.js
Normal file
173
js/components/MilkdropWindow/__tests__/Presets.test.js
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { mockRandom } from "jest-mock-random";
|
||||
import Presets from "../Presets";
|
||||
|
||||
let presets;
|
||||
beforeEach(() => {
|
||||
mockRandom([0.0]);
|
||||
presets = new Presets({
|
||||
keys: ["a", "b"],
|
||||
initialPresets: { a: "Preset A", b: "Preset B" },
|
||||
randomize: true
|
||||
});
|
||||
});
|
||||
|
||||
describe("initialize", () => {
|
||||
beforeEach(() => {});
|
||||
test("picks a random value", () => {
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
});
|
||||
|
||||
test("picks another random value", () => {
|
||||
mockRandom([0.9]);
|
||||
presets = new Presets({
|
||||
keys: ["a", "b"],
|
||||
initialPresets: { a: "Preset A", b: "Preset B" },
|
||||
randomize: true
|
||||
});
|
||||
expect(presets.getCurrent()).toBe("Preset B");
|
||||
});
|
||||
|
||||
test("picks its random value from the initial presets", () => {
|
||||
presets = new Presets({
|
||||
keys: ["a", "b", "c", "d", "e", "f", "g", "h"],
|
||||
initialPresets: { a: "Preset A" },
|
||||
randomize: true
|
||||
});
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
});
|
||||
});
|
||||
|
||||
describe("next", () => {
|
||||
test("picks a 'random' preset", async () => {
|
||||
mockRandom([0.9]);
|
||||
presets.next();
|
||||
expect(presets.getCurrent()).toBe("Preset B");
|
||||
|
||||
mockRandom([0.0]);
|
||||
presets.next();
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
});
|
||||
|
||||
test("picks the next key", async () => {
|
||||
presets.setRandomize(false);
|
||||
presets.next();
|
||||
expect(presets.getCurrent()).toBe("Preset B");
|
||||
});
|
||||
|
||||
test("wraps around", async () => {
|
||||
presets.setRandomize(false);
|
||||
presets.next();
|
||||
presets.next();
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
});
|
||||
});
|
||||
|
||||
describe("previous", () => {
|
||||
test("picks the previous key", async () => {
|
||||
presets.setRandomize(false);
|
||||
presets.next();
|
||||
presets.previous();
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
});
|
||||
|
||||
test("does nothing when you are on the first item", async () => {
|
||||
presets.previous();
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
});
|
||||
|
||||
test("can do sequential previouses", async () => {
|
||||
mockRandom([0.0]);
|
||||
presets = new Presets({
|
||||
keys: ["a", "b", "c", "d"],
|
||||
initialPresets: {
|
||||
a: "Preset A",
|
||||
b: "Preset B",
|
||||
c: "Preset C",
|
||||
d: "Preset D"
|
||||
},
|
||||
randomize: false
|
||||
});
|
||||
presets.next(); // b
|
||||
presets.next(); // c
|
||||
presets.next(); // d
|
||||
presets.previous(); // c
|
||||
presets.previous(); // b
|
||||
presets.previous(); // a
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
});
|
||||
|
||||
test("will successfully resolve an unloaded preset", async () => {
|
||||
mockRandom([0.0]);
|
||||
presets = new Presets({
|
||||
keys: ["a", "b", "c", "d"],
|
||||
initialPresets: {
|
||||
a: "Preset A"
|
||||
},
|
||||
randomize: false,
|
||||
getRest: () =>
|
||||
Promise.resolve({
|
||||
b: "Preset B",
|
||||
c: "Preset C"
|
||||
})
|
||||
});
|
||||
presets.next(); // b
|
||||
presets.next(); // c
|
||||
const final = await presets.previous(); // b
|
||||
expect(final).toBe("Preset B");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRest", () => {
|
||||
beforeEach(() => {
|
||||
mockRandom([0.0]);
|
||||
presets = new Presets({
|
||||
keys: ["a", "b"],
|
||||
initialPresets: { a: "Preset A" },
|
||||
getRest: () =>
|
||||
Promise.resolve({
|
||||
b: "Preset B"
|
||||
})
|
||||
});
|
||||
});
|
||||
test("will get the rest of the presets if needed", async () => {
|
||||
mockRandom([0.9]);
|
||||
const resolved = presets.next();
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
await resolved;
|
||||
expect(presets.getCurrent()).toBe("Preset B");
|
||||
});
|
||||
|
||||
test("next (loading), previous brings us back to where we started", async () => {
|
||||
presets.setRandomize(false);
|
||||
presets.next();
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
await presets.previous();
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectIndex", () => {
|
||||
test("adds an entry to the history", async () => {
|
||||
presets.selectIndex(1);
|
||||
presets.previous();
|
||||
expect(presets.getCurrent()).toBe("Preset A");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCurrentIndex", () => {
|
||||
test("gets the active index while loading", async () => {
|
||||
presets = new Presets({
|
||||
keys: ["a", "b"],
|
||||
initialPresets: { a: "Preset A" },
|
||||
randomize: false,
|
||||
getRest: () =>
|
||||
Promise.resolve({
|
||||
b: "Preset B"
|
||||
})
|
||||
});
|
||||
const resolved = presets.next();
|
||||
expect(presets.getCurrentIndex()).toBe(0);
|
||||
await resolved;
|
||||
expect(presets.getCurrentIndex()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,26 +1,47 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import screenfull from "screenfull";
|
||||
import PresetOverlay from "./PresetOverlay";
|
||||
import Presets from "./Presets";
|
||||
import Milkdrop from "./Milkdrop";
|
||||
import Background from "./Background";
|
||||
|
||||
const USER_PRESET_TRANSITION_SECONDS = 5.7;
|
||||
const PRESET_TRANSITION_SECONDS = 2.7;
|
||||
const MILLISECONDS_BETWEEN_PRESET_TRANSITIONS = 15000;
|
||||
|
||||
class MilkdropWindow extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isFullscreen: false,
|
||||
presetOverlay: false,
|
||||
currentPreset: -1
|
||||
};
|
||||
this._handleFocusedKeyboardInput = this._handleFocusedKeyboardInput.bind(
|
||||
this
|
||||
);
|
||||
this._handleFullscreenChange = this._handleFullscreenChange.bind(this);
|
||||
// This component is just responsible for loading dependencies.
|
||||
// This simplifies the inner <Milkdrop /> component, by allowing
|
||||
// it to alwasy assume that it has it's dependencies.
|
||||
export default class PresetsLoader extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { presets: null, butterchurn: null };
|
||||
}
|
||||
componentDidMount() {
|
||||
|
||||
async componentDidMount() {
|
||||
const {
|
||||
butterchurn,
|
||||
presetKeys,
|
||||
minimalPresets
|
||||
} = await loadInitialDependencies();
|
||||
|
||||
this.setState({
|
||||
butterchurn,
|
||||
presets: new Presets({
|
||||
keys: presetKeys,
|
||||
initialPresets: minimalPresets,
|
||||
getRest: loadNonMinimalPresets
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { butterchurn, presets } = this.state;
|
||||
const loaded = butterchurn != null && presets != null;
|
||||
return loaded ? (
|
||||
<Milkdrop {...this.props} presets={presets} butterchurn={butterchurn} />
|
||||
) : (
|
||||
<Background />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadInitialDependencies() {
|
||||
return new Promise((resolve, reject) => {
|
||||
require.ensure(
|
||||
[
|
||||
"butterchurn",
|
||||
|
|
@ -28,265 +49,32 @@ class MilkdropWindow extends React.Component {
|
|||
"butterchurn-presets/lib/butterchurnPresetPackMeta.min"
|
||||
],
|
||||
require => {
|
||||
const analyserNode = this.props.analyser;
|
||||
const butterchurn = require("butterchurn");
|
||||
const butterchurnMinimalPresets = require("butterchurn-presets/lib/butterchurnPresetsMinimal.min");
|
||||
const presetPackMeta = require("butterchurn-presets/lib/butterchurnPresetPackMeta.min");
|
||||
this.presets = butterchurnMinimalPresets.getPresets();
|
||||
this.minmalPresetKeys = Object.keys(this.presets);
|
||||
this.presetKeys = presetPackMeta.getMainPresetMeta().presets;
|
||||
const presetKey = this.minmalPresetKeys[
|
||||
Math.floor(Math.random() * this.minmalPresetKeys.length)
|
||||
];
|
||||
|
||||
this.visualizer = butterchurn.createVisualizer(
|
||||
analyserNode.context,
|
||||
this._canvasNode,
|
||||
{
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
pixelRatio: window.devicePixelRatio || 1
|
||||
}
|
||||
);
|
||||
this._canvasNode.width = this.props.width;
|
||||
this._canvasNode.height = this.props.height;
|
||||
this.visualizer.connectAudio(analyserNode);
|
||||
this.visualizer.loadPreset(this.presets[presetKey], 0);
|
||||
// Kick off the animation loop
|
||||
const loop = () => {
|
||||
if (this.props.status === "PLAYING") {
|
||||
this.visualizer.render();
|
||||
}
|
||||
window.requestAnimationFrame(loop);
|
||||
};
|
||||
loop();
|
||||
|
||||
const presetIdx = this.presetKeys.indexOf(presetKey);
|
||||
this.presetHistory = [presetIdx];
|
||||
this.presetRandomize = true;
|
||||
this.presetCycle = true;
|
||||
this._restartCycling();
|
||||
this._unsubscribeFocusedKeyDown = this.props.onFocusedKeyDown(
|
||||
this._handleFocusedKeyboardInput
|
||||
);
|
||||
this.setState({ currentPreset: presetIdx });
|
||||
},
|
||||
e => {
|
||||
console.error("Error loading Butterchurn", e);
|
||||
resolve({
|
||||
butterchurn,
|
||||
minimalPresets: butterchurnMinimalPresets.getPresets(),
|
||||
presetKeys: presetPackMeta.getMainPresetMeta().presets
|
||||
});
|
||||
},
|
||||
reject,
|
||||
"butterchurn"
|
||||
);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this._pauseViz();
|
||||
this._stopCycling();
|
||||
if (this._unsubscribeFocusedKeyDown) {
|
||||
this._unsubscribeFocusedKeyDown();
|
||||
}
|
||||
screenfull.off("change", this._handleFullscreenChange);
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.props.width !== prevProps.width ||
|
||||
this.props.height !== prevProps.height
|
||||
) {
|
||||
this._setRendererSize(this.props.width, this.props.height);
|
||||
}
|
||||
}
|
||||
_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);
|
||||
}
|
||||
}
|
||||
_setRendererSize(width, height) {
|
||||
this._canvasNode.width = width;
|
||||
this._canvasNode.height = height;
|
||||
// It's possible that the visualizer has not been intialized yet.
|
||||
if (this.visualizer != null) {
|
||||
this.visualizer.setRendererSize(width, height);
|
||||
}
|
||||
}
|
||||
_handleFullscreenChange() {
|
||||
if (screenfull.isFullscreen) {
|
||||
this._setRendererSize(window.innerWidth, window.innerHeight);
|
||||
} else {
|
||||
this._setRendererSize(this.props.width, this.props.height);
|
||||
}
|
||||
this.setState({ isFullscreen: screenfull.isFullscreen });
|
||||
}
|
||||
_handleRequestFullsceen() {
|
||||
if (screenfull.enabled) {
|
||||
if (!screenfull.isFullscreen) {
|
||||
screenfull.request(this._wrapperNode);
|
||||
} else {
|
||||
screenfull.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
_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.presetRandomize = !this.presetRandomize;
|
||||
break;
|
||||
case 76: // L
|
||||
this.setState({ presetOverlay: !this.state.presetOverlay });
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case 145: // scroll lock
|
||||
case 125: // F14 (scroll lock for OS X)
|
||||
this.presetCycle = !this.presetCycle;
|
||||
this._restartCycling();
|
||||
break;
|
||||
}
|
||||
}
|
||||
async _loadMainPresetPack() {
|
||||
this.loadingPresets = true;
|
||||
return require.ensure(
|
||||
["butterchurn-presets/lib/butterchurnPresetsNonMinimal.min"],
|
||||
require => {
|
||||
const butterchurnNonMinimalPresets = require("butterchurn-presets/lib/butterchurnPresetsNonMinimal.min");
|
||||
Object.assign(this.presets, butterchurnNonMinimalPresets.getPresets());
|
||||
this.loadingPresets = false;
|
||||
},
|
||||
e => {
|
||||
console.error("Error loading Butterchurn Presets", e);
|
||||
},
|
||||
"butterchurn-presets"
|
||||
);
|
||||
}
|
||||
async _nextPreset(blendTime) {
|
||||
// The visualizer may not have initialized yet.
|
||||
if (this.visualizer != null) {
|
||||
let presetIdx;
|
||||
if (this.presetRandomize || this.presetHistory.length === 0) {
|
||||
presetIdx = Math.floor(this.presetKeys.length * Math.random());
|
||||
} else {
|
||||
const prevPresetIdx = this.presetHistory[this.presetHistory.length - 1];
|
||||
presetIdx = (prevPresetIdx + 1) % this.presetKeys.length;
|
||||
}
|
||||
this.selectPreset(presetIdx, blendTime);
|
||||
}
|
||||
}
|
||||
_prevPreset(blendTime) {
|
||||
if (this.loadingPresets && this.presetQueue.length > 1) {
|
||||
this.presetQueue.pop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.presetHistory.length > 1 && this.visualizer != null) {
|
||||
this.presetHistory.pop();
|
||||
const prevPreset = this.presetHistory[this.presetHistory.length - 1];
|
||||
this.visualizer.loadPreset(
|
||||
this.presets[this.presetKeys[prevPreset]],
|
||||
blendTime
|
||||
);
|
||||
this._restartCycling();
|
||||
this.setState({ currentPreset: prevPreset });
|
||||
}
|
||||
}
|
||||
async selectPreset(presetIdx, blendTime) {
|
||||
if (this.loadingPresets) {
|
||||
this.presetQueue.push(presetIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
let preset = this.presets[this.presetKeys[presetIdx]];
|
||||
let selectedIdx;
|
||||
if (!preset) {
|
||||
this.presetQueue = [presetIdx];
|
||||
await this._loadMainPresetPack();
|
||||
if (this.presetQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
selectedIdx = this.presetQueue[this.presetQueue.length - 1];
|
||||
preset = this.presets[this.presetKeys[selectedIdx]];
|
||||
this.presetHistory = this.presetHistory.concat(this.presetQueue);
|
||||
} else {
|
||||
selectedIdx = presetIdx;
|
||||
this.presetHistory.push(selectedIdx);
|
||||
}
|
||||
this.visualizer.loadPreset(preset, blendTime);
|
||||
this._restartCycling();
|
||||
this.setState({ currentPreset: selectedIdx });
|
||||
}
|
||||
closePresetOverlay() {
|
||||
this.setState({ presetOverlay: false });
|
||||
}
|
||||
render() {
|
||||
const width = this.state.isFullscreen
|
||||
? window.innerWidth
|
||||
: this.props.width;
|
||||
const height = this.state.isFullscreen
|
||||
? window.innerHeight
|
||||
: this.props.height;
|
||||
return (
|
||||
<div
|
||||
className="draggable"
|
||||
style={{
|
||||
// This color will be used until Butterchurn is loaded
|
||||
backgroundColor: "#000",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
}}
|
||||
tabIndex="0"
|
||||
ref={node => (this._wrapperNode = node)}
|
||||
onDoubleClick={() => this._handleRequestFullsceen()}
|
||||
>
|
||||
{this.state.presetOverlay && (
|
||||
<PresetOverlay
|
||||
width={width}
|
||||
height={height}
|
||||
presetKeys={this.presetKeys}
|
||||
currentPreset={this.state.currentPreset}
|
||||
onFocusedKeyDown={listener => this.props.onFocusedKeyDown(listener)}
|
||||
selectPreset={idx => this.selectPreset(idx, 0)}
|
||||
closeOverlay={() => this.closePresetOverlay()}
|
||||
/>
|
||||
)}
|
||||
<canvas
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
}}
|
||||
ref={node => (this._canvasNode = node)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
status: state.media.status
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(MilkdropWindow);
|
||||
async function loadNonMinimalPresets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
require.ensure(
|
||||
["butterchurn-presets/lib/butterchurnPresetsNonMinimal.min"],
|
||||
require => {
|
||||
resolve(
|
||||
require("butterchurn-presets/lib/butterchurnPresetsNonMinimal.min").getPresets()
|
||||
);
|
||||
},
|
||||
reject,
|
||||
"butterchurn-presets"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
"weight": "npm run build-library > /dev/null && gzip-size built/webamp.bundle.min.js",
|
||||
"test": "jest --projects config/jest.unit.js config/jest.eslint.js",
|
||||
"travis-tests": "npm run test && npm run build && npm run build-library",
|
||||
"tdd": "jest --watch",
|
||||
"tdd": "jest --projects config/jest.unit.js --watch",
|
||||
"format": "prettier --write experiments/**/*.js js/**/*.js css/**/*.css !css/**/*.min.css",
|
||||
"build-skin": "rm skins/base-2.91.wsz && cd skins/base-2.91 && zip -x .* -x 'Skining Updates.txt' -r ../base-2.91.wsz .",
|
||||
"build-skin-png": "rm skins/base-2.91-png.wsz && cd skins/base-2.91-png && zip -x .* -x 'Skining Updates.txt' -r ../base-2.91-png.wsz .",
|
||||
|
|
@ -74,6 +74,7 @@
|
|||
"http-server": "^0.11.1",
|
||||
"invariant": "^2.2.3",
|
||||
"jest": "^23.0.0",
|
||||
"jest-mock-random": "^1.0.2",
|
||||
"jest-runner-eslint": "^0.4.0",
|
||||
"jsmediatags": "^3.8.1",
|
||||
"jszip": "^3.1.3",
|
||||
|
|
|
|||
|
|
@ -4341,6 +4341,10 @@ jest-message-util@^23.0.0:
|
|||
slash "^1.0.0"
|
||||
stack-utils "^1.0.1"
|
||||
|
||||
jest-mock-random@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-mock-random/-/jest-mock-random-1.0.2.tgz#81a1aa641fdb3a049bf64e2a7a0411fd8fc3fb20"
|
||||
|
||||
jest-mock@^23.0.0:
|
||||
version "23.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-23.0.0.tgz#d9d897a1b74dc05c66a737213931496215897dd8"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue