diff --git a/nix/node-packages.nix b/nix/node-packages.nix index 1407364..62795f4 100644 --- a/nix/node-packages.nix +++ b/nix/node-packages.nix @@ -1813,6 +1813,15 @@ let sha512 = "YRRv9DNZhaVTVRh9Wmmit7Y0UFhEVqXqCSw3uazRWMxa2x85hWQZ5BN24i7GXZbaclaLXEcodEeIHsjBA8eAMw=="; }; }; + "@types/react-dom-17.0.3" = { + name = "_at_types_slash_react-dom"; + packageName = "@types/react-dom"; + version = "17.0.3"; + src = fetchurl { + url = "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz"; + sha512 = "4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w=="; + }; + }; "@types/react-redux-7.1.16" = { name = "_at_types_slash_react-redux"; packageName = "@types/react-redux"; @@ -1840,6 +1849,15 @@ let sha512 = "7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg=="; }; }; + "@types/three-0.127.0" = { + name = "_at_types_slash_three"; + packageName = "@types/three"; + version = "0.127.0"; + src = fetchurl { + url = "https://registry.npmjs.org/@types/three/-/three-0.127.0.tgz"; + sha512 = "4Q33L6PzzxCXm0VdUv4x1/4VBnwWgCS7Ui6WpRh88GVIUUYsg5qU2GVzva14hDJbtfyNBxey7UcdInR4RkKPeQ=="; + }; + }; "abab-2.0.5" = { name = "abab"; packageName = "abab"; @@ -9992,9 +10010,11 @@ let sources."@types/prop-types-15.7.3" sources."@types/q-1.5.4" sources."@types/react-16.14.5" + sources."@types/react-dom-17.0.3" sources."@types/react-redux-7.1.16" sources."@types/scheduler-0.16.1" sources."@types/sizzle-2.3.2" + sources."@types/three-0.127.0" sources."abab-2.0.5" sources."abortcontroller-polyfill-1.7.1" sources."acorn-3.3.0" @@ -10398,6 +10418,7 @@ let (sources."favicons-6.2.1" // { dependencies = [ sources."semver-7.3.5" + sources."sharp-0.26.3" ]; }) sources."file-type-9.0.0" @@ -11153,11 +11174,6 @@ let }) sources."setimmediate-1.0.5" sources."sha.js-2.4.11" - (sources."sharp-0.26.3" // { - dependencies = [ - sources."semver-7.3.5" - ]; - }) sources."shebang-command-1.2.0" sources."shebang-regex-1.0.0" sources."signal-exit-3.0.3" diff --git a/package-lock.json b/package-lock.json index 31105df..47d2291 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3191,6 +3191,12 @@ "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", "dev": true }, + "@types/three": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.127.0.tgz", + "integrity": "sha512-4Q33L6PzzxCXm0VdUv4x1/4VBnwWgCS7Ui6WpRh88GVIUUYsg5qU2GVzva14hDJbtfyNBxey7UcdInR4RkKPeQ==", + "dev": true + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", diff --git a/package.json b/package.json index 1f6d796..3c5290f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@types/react": "^16.9.41", "@types/react-dom": "^17.0.3", "@types/react-redux": "^7.1.9", + "@types/three": "^0.127.0", "jstransformer-markdown-it": "^2.1.0", "parcel": "2.0.0-beta.2", "posthtml-favicons": "^1.3.0", diff --git a/src/index.ts b/src/index.ts index c35abda..de047be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import $ from "jquery"; +import jQuery from "jquery"; // Helpers @@ -6,114 +6,114 @@ import $ from "jquery"; * "Types" out a DOM element, emulating the way a human might. */ class Typer { - private element: JQuery; - private text: string; - private cursor: boolean; - private typed: number; - private min: number; - private max: number; - private blink_tick: number; - private blink_timeout: number; - private end?: number; + private element: JQuery; + private text: string; + private cursor: boolean; + private typed: number; + private min: number; + private max: number; + private blink_tick: number; + private blink_timeout: number; + private end?: number; - /** - * Create the typer. - * @param {HTMLElement} element - The element to type. - * @param {number} blink - The time between cursor blinks. - * @param {number} blink_timeout - How long the cursor should keep - * blinking for after the text - * finishes typing. - */ - constructor(element: HTMLElement, blink: number, blink_timeout: number) { - // Retrieve the current content and wipe it. We also make the - // element visible if it was hidden. - this.element = $(element); - this.text = this.element.html(); - this.element.html(""); - this.element.css("visibility", "visible"); + /** + * Create the typer. + * @param {HTMLElement} element - The element to type. + * @param {number} blink - The time between cursor blinks. + * @param {number} blink_timeout - How long the cursor should keep + * blinking for after the text + * finishes typing. + */ + constructor(element: HTMLElement, blink: number, blink_timeout: number) { + // Retrieve the current content and wipe it. We also make the + // element visible if it was hidden. + this.element = $(element); + this.text = this.element.html(); + this.element.html(""); + this.element.css("visibility", "visible"); - this.cursor = false; - this.typed = 0; + this.cursor = false; + this.typed = 0; - this.min = 20; - this.max = 70; - this.blink_tick = blink; - this.blink_timeout = blink_timeout; + this.min = 20; + this.max = 70; + this.blink_tick = blink; + this.blink_timeout = blink_timeout; - this.end = null; - } - - /** - * Start typing. - */ - type() { - this._type(); - this._blink(); - } - - /** - * Draw the current text line, i.e., anything that has been typed - * so far, and a cursor if it is currently supposed to be on. - * @private - */ - _draw() { - let text = this.text.slice(0, this.typed); - - if (this.cursor) { - text += "\u2588"; + this.end = null; } - window.requestAnimationFrame(() => this.element.html(text)); - } - - /** - * Type the next character, and prepare to draw the next one. If - * no new characters are to be drawn, set the end timestamp. - * @private - */ - _type() { - this.typed += 1; - this._draw(); - - if (this.typed != this.text.length) - setTimeout(this._type.bind(this), this._type_tick()); - else { - this.end = Date.now(); + /** + * Start typing. + */ + type() { + this._type(); + this._blink(); } - } - /** - * Make the cursor change blink status, and prepare for the next - * blink. - * @private - */ - _blink() { - this.cursor = !this.cursor; - this._draw(); + /** + * Draw the current text line, i.e., anything that has been typed + * so far, and a cursor if it is currently supposed to be on. + * @private + */ + _draw() { + let text = this.text.slice(0, this.typed); - // As long as we are typing, keep blinking - if (this.typed != this.text.length) - setTimeout(this._blink.bind(this), this.blink_tick); - // Once typing ends, keep going for a little bit - else if (Date.now() - this.end < this.blink_timeout) - setTimeout(this._blink.bind(this), this.blink_tick); - // Make sure we get rid of the cursor in the end - else { - this.cursor = true; - setTimeout(this._blink.bind(this), this.blink_tick); + if (this.cursor) { + text += "\u2588"; + } + + window.requestAnimationFrame(() => this.element.html(text)); } - } - /** - * Calculate a "human" time for the next character to type. - * @private - */ - _type_tick() { - return Math.round(Math.random() * this.max) + this.min; - } + /** + * Type the next character, and prepare to draw the next one. If + * no new characters are to be drawn, set the end timestamp. + * @private + */ + _type() { + this.typed += 1; + this._draw(); + + if (this.typed != this.text.length) + setTimeout(this._type.bind(this), this._type_tick()); + else { + this.end = Date.now(); + } + } + + /** + * Make the cursor change blink status, and prepare for the next + * blink. + * @private + */ + _blink() { + this.cursor = !this.cursor; + this._draw(); + + // As long as we are typing, keep blinking + if (this.typed != this.text.length) + setTimeout(this._blink.bind(this), this.blink_tick); + // Once typing ends, keep going for a little bit + else if (Date.now() - this.end < this.blink_timeout) + setTimeout(this._blink.bind(this), this.blink_tick); + // Make sure we get rid of the cursor in the end + else { + this.cursor = true; + setTimeout(this._blink.bind(this), this.blink_tick); + } + } + + /** + * Calculate a "human" time for the next character to type. + * @private + */ + _type_tick() { + return Math.round(Math.random() * this.max) + this.min; + } } -$(document).ready(() => { - let typer = new Typer($(".head-line .typed").get(0), 500, 3000); - typer.type(); +jQuery(($) => { + let typer = new Typer($(".head-line .typed").get(0), 500, 3000); + typer.type(); }); diff --git a/src/lib/js/main.ts b/src/lib/js/main.ts index 0481cef..ccf6bea 100644 --- a/src/lib/js/main.ts +++ b/src/lib/js/main.ts @@ -1,3 +1,5 @@ -import $ from "jquery"; +import jQuery from "jquery"; -$(document).ready(() => $("html").removeClass("no-js")); +jQuery(($) => { + $("html").removeClass("no-js") +}) diff --git a/src/music/MusicPlayer.tsx b/src/music/MusicPlayer.tsx index 22cb10d..7890461 100644 --- a/src/music/MusicPlayer.tsx +++ b/src/music/MusicPlayer.tsx @@ -6,89 +6,89 @@ import Visualizer from "./components/visualizer"; import { State } from "./store"; type AudioState = { - audioContext: AudioContext; - audioSource: HTMLAudioElement; - audioSourceNode: MediaElementAudioSourceNode; - audioVolume: GainNode; + audioContext: AudioContext; + audioSource: HTMLAudioElement; + audioSourceNode: MediaElementAudioSourceNode; + audioVolume: GainNode; }; type MusicPlayerProps = { - playing: boolean; - muted: boolean; - source?: string; + playing: boolean; + muted: boolean; + source?: string; }; class MusicPlayer extends React.Component { - private audioState: AudioState; + private audioState: AudioState; - constructor(props: MusicPlayerProps) { - super(props); + constructor(props: MusicPlayerProps) { + super(props); - let context = new AudioContext(); - let source = new Audio(); - let sourceNode = context.createMediaElementSource(source); - let volume = context.createGain(); + let context = new AudioContext(); + let source = new Audio(); + let sourceNode = context.createMediaElementSource(source); + let volume = context.createGain(); - sourceNode.connect(volume); - volume.connect(context.destination); + sourceNode.connect(volume); + volume.connect(context.destination); - this.audioState = { - audioContext: context, - audioSourceNode: sourceNode, - audioSource: source, - audioVolume: volume, - }; - } - - render() { - return ( -
- - -
- ); - } - - componentDidUpdate() { - let context = this.audioState.audioContext; - let source = this.audioState.audioSource; - let volume = this.audioState.audioVolume; - - // First, set the audio source (if it changed) - if (this.props.source && source.src != this.props.source) { - source.src = this.props.source; + this.audioState = { + audioContext: context, + audioSourceNode: sourceNode, + audioSource: source, + audioVolume: volume, + }; } - if (this.props.playing) { - source - .play() - .then(() => { - console.info("Started playing audio"); - }) - .catch((error) => { - console.error(`Could not play audio: ${error}`); - }); - } else { - source.pause(); + render() { + return ( +
+ + +
+ ); } - if (!this.props.muted) { - volume.gain.setValueAtTime(1, context.currentTime); - } else { - volume.gain.setValueAtTime(0, context.currentTime); + componentDidUpdate() { + let context = this.audioState.audioContext; + let source = this.audioState.audioSource; + let volume = this.audioState.audioVolume; + + // First, set the audio source (if it changed) + if (this.props.source && source.src != this.props.source) { + source.src = this.props.source; + } + + if (this.props.playing) { + source + .play() + .then(() => { + console.info("Started playing audio"); + }) + .catch((error) => { + console.error(`Could not play audio: ${error}`); + }); + } else { + source.pause(); + } + + if (!this.props.muted) { + volume.gain.setValueAtTime(1, context.currentTime); + } else { + volume.gain.setValueAtTime(0, context.currentTime); + } } - } } function mapStateToProps(state: State): MusicPlayerProps { - return { - playing: state.musicState.playing, - muted: state.musicState.muted, - source: state.musicState.source, - }; + return { + playing: state.musicState.playing, + muted: state.musicState.muted, + source: state.musicState.source, + }; } export default connect(mapStateToProps)(MusicPlayer); diff --git a/src/music/components/controls.tsx b/src/music/components/controls.tsx index 7aa35b6..2b4036f 100644 --- a/src/music/components/controls.tsx +++ b/src/music/components/controls.tsx @@ -1,50 +1,49 @@ import React from "react"; -import Redux from "redux"; import { connect } from "react-redux"; import { State } from "../store"; -import { Title, togglePlay } from "../store/music/types"; +import { Title } from "../store/music/types"; import Indicator from "./indicator"; type ControlProps = { - title: Title; + title: Title; }; class Controls extends React.Component { - render() { - return ( -
-
- -
- {this.props.title.name} - {this.props.title.album} -
+ render() { + return ( +
+
+ +
+ {this.props.title.name} - {this.props.title.album} +
- {this.props.title.name === "Journey" && - this.props.title.artist === "Mseq" ? ( -
- Journey + {this.props.title.name === "Journey" && + this.props.title.artist === "Mseq" ? ( +
+ Journey  by Mseq (c) copyright 2016 Licensed under a Creative Commons  - - Attribution Noncommercial (3.0) + + Attribution Noncommercial (3.0)   license. Ft: Admiral Bob,Texas Radio Fish +
+ ) : null} +
- ) : null} -
-
- ); - } + ); + } } function mapStateToProps(state: State): ControlProps { - return { - title: state.musicState.title, - }; + return { + title: state.musicState.title, + }; } export default connect(mapStateToProps)(Controls); diff --git a/src/music/components/indicator.tsx b/src/music/components/indicator.tsx index 544d221..f4c2d7e 100644 --- a/src/music/components/indicator.tsx +++ b/src/music/components/indicator.tsx @@ -2,59 +2,59 @@ import React from "react"; import { connect } from "react-redux"; import classNames from "classnames"; -import { State } from "../store"; -import { MusicState, togglePlay } from "../store/music/types"; +import { State, Dispatch } from "../store"; +import { togglePlay } from "../store/music/types"; type IndicatorProps = { - muted: boolean; - playing: boolean; + muted: boolean; + playing: boolean; }; type IndicatorDispatch = { - play: () => void; + play: () => void; }; type Props = IndicatorProps & IndicatorDispatch; class Indicator extends React.Component { - click() { - this.props.play(); - } + click() { + this.props.play(); + } - render() { - let classes = classNames({ - btn: true, - "col-auto": true, - fas: true, - "fa-muted": this.props.muted, - "fa-play": this.props.playing, - "fa-pause": !this.props.playing, - }); + render() { + let classes = classNames({ + btn: true, + "col-auto": true, + fas: true, + "fa-muted": this.props.muted, + "fa-play": this.props.playing, + "fa-pause": !this.props.playing, + }); - return ( - - ); - } + return ( + + ); + } } function mapStateToProps(state: State): IndicatorProps { - return { - muted: state.musicState.muted, - playing: state.musicState.playing, - }; + return { + muted: state.musicState.muted, + playing: state.musicState.playing, + }; } -function mapDispatchToProps(dispatch: Redux.Dispatch): IndicatorDispatch { - return { - play: () => { - dispatch(togglePlay()); - }, - }; +function mapDispatchToProps(dispatch: Dispatch): IndicatorDispatch { + return { + play: () => { + dispatch(togglePlay()); + }, + }; } export default connect(mapStateToProps, mapDispatchToProps)(Indicator); diff --git a/src/music/components/visualizer.tsx b/src/music/components/visualizer.tsx index ebb7bfe..113fff9 100644 --- a/src/music/components/visualizer.tsx +++ b/src/music/components/visualizer.tsx @@ -2,217 +2,217 @@ import React from "react"; import * as three from "three"; type VisualizerProps = { - audioContext: AudioContext; - audioSource: AudioNode; + audioContext: AudioContext; + audioSource: AudioNode; }; class CanvasDrawer { - private analyser: AnalyserNode; - private canvas: HTMLCanvasElement; + private analyser: AnalyserNode; + private canvas: HTMLCanvasElement; - private analyserData: Float32Array; + private analyserData: Float32Array; - private boxes: Array; - private camera: three.Camera; - private renderer: three.Renderer; - private scene: three.Scene; + private boxes: Array; + private camera: three.PerspectiveCamera; + private renderer: three.WebGLRenderer; + private scene: three.Scene; - private angle: number; + private angle: number; - private animationFrame: number; - private lastTime: number; + private animationFrame: number; + private lastTime: number; - constructor(analyser: AnalyserNode, canvas: HTMLCanvasElement) { - this.analyser = analyser; - this.canvas = canvas; + constructor(analyser: AnalyserNode, canvas: HTMLCanvasElement) { + this.analyser = analyser; + this.canvas = canvas; - // Set up analyser data storage - this.analyserData = new Float32Array(analyser.frequencyBinCount); + // Set up analyser data storage + this.analyserData = new Float32Array(analyser.frequencyBinCount); - // Initialize the scene - this.scene = new three.Scene(); + // Initialize the scene + this.scene = new three.Scene(); - // Make a bunch of boxes to represent the bars - this.boxes = Array(analyser.frequencyBinCount); - let width = 2 / analyser.frequencyBinCount; - for (let freq = 0; freq < analyser.frequencyBinCount; freq++) { - let geometry = new three.BoxGeometry(1, 1, 1); - let material = new three.MeshLambertMaterial({ - color: new three.Color(0x99d1ce), - }); - let cube = new three.Mesh(geometry, material); + // Make a bunch of boxes to represent the bars + this.boxes = Array(analyser.frequencyBinCount); + let width = 2 / analyser.frequencyBinCount; + for (let freq = 0; freq < analyser.frequencyBinCount; freq++) { + let geometry = new three.BoxGeometry(1, 1, 1); + let material = new three.MeshLambertMaterial({ + color: new three.Color(0x99d1ce), + }); + let cube = new three.Mesh(geometry, material); - cube.scale.set(width, 1e-6, width); - cube.position.set(-1 + freq * width, 0, 0); + cube.scale.set(width, 1e-6, width); + cube.position.set(-1 + freq * width, 0, 0); - this.scene.add(cube); - this.boxes[freq] = cube; + this.scene.add(cube); + this.boxes[freq] = cube; + } + + // Add lights for shadowing + let ambientLight = new three.AmbientLight(0xffffff, 0.4); + this.scene.add(ambientLight); + + let directionalLight = new three.DirectionalLight(0xffffff, 1); + directionalLight.position.set(-1, 0.3, -1); + directionalLight.castShadow = true; + this.scene.add(directionalLight); + + // Add a camera + this.angle = 3; + this.camera = new three.PerspectiveCamera( + 70, + canvas.width / canvas.height, + 0.01, + 10 + ); + this.camera.lookAt(0, 0, 0); + this.scene.add(this.camera); + this.rotateCamera(1); + + // Add a renderer + this.renderer = new three.WebGLRenderer({ + antialias: true, + canvas: canvas, + powerPreference: "low-power", + }); + + this.renderer.setClearColor(new three.Color(0x0f0f0f), 1.0); + this.renderer.setSize(canvas.width, canvas.height); + + // Set up canvas resizing + window.addEventListener("resize", this.resize.bind(this)); + + // Run the first, set the first animation frame time and start requesting + // animation frames + this.resize(); + this.lastTime = 0; + this.animationFrame = requestAnimationFrame(this.render.bind(this)); } - // Add lights for shadowing - let ambientLight = new three.AmbientLight(0xffffff, 0.4); - this.scene.add(ambientLight); + render(time: number) { + // Set our animation frame to 0, so that if we stop, we don't try to cancel a past animation frame + this.animationFrame = 0; + // Update elapsed time + let elapsed = time - this.lastTime; + this.lastTime = time; - let directionalLight = new three.DirectionalLight(0xffffff, 1); - directionalLight.position.set(-1, 0.3, -1); - directionalLight.castShadow = true; - this.scene.add(directionalLight); + let camera = this.camera; + let renderer = this.renderer; + let scene = this.scene; - // Add a camera - this.angle = 3; - this.camera = new three.PerspectiveCamera( - 70, - canvas.width / canvas.height, - 0.01, - 10 - ); - this.camera.lookAt(0, 0, 0); - this.scene.add(this.camera); - this.rotateCamera(1); + this.scaleBoxes(); + this.rotateCamera(elapsed); - // Add a renderer - this.renderer = new three.WebGLRenderer({ - antialias: true, - canvas: canvas, - powerPreference: "low-power", - }); - - this.renderer.setClearColor(new three.Color(0x0f0f0f)); - this.renderer.setSize(canvas.width, canvas.height); - - // Set up canvas resizing - window.addEventListener("resize", this.resize.bind(this)); - - // Run the first, set the first animation frame time and start requesting - // animation frames - this.resize(); - this.lastTime = 0; - this.animationFrame = requestAnimationFrame(this.render.bind(this)); - } - - render(time: number) { - // Set our animation frame to 0, so that if we stop, we don't try to cancel a past animation frame - this.animationFrame = 0; - // Update elapsed time - let elapsed = time - this.lastTime; - this.lastTime = time; - - let camera = this.camera; - let renderer = this.renderer; - let scene = this.scene; - - this.scaleBoxes(); - this.rotateCamera(elapsed); - - renderer.render(scene, camera); - this.animationFrame = requestAnimationFrame(this.render.bind(this)); - } - - scaleBoxes() { - let analyser = this.analyser; - - analyser.getFloatFrequencyData(this.analyserData); - - for (let freq = 0; freq < analyser.frequencyBinCount; freq++) { - let height = analyser.maxDecibels / this.analyserData[freq]; - - if (height > 0.3) { - height -= 0.3; - } else { - height = 1e-6; - } - - this.boxes[freq].scale.y = height; - } - } - - rotateCamera(elapsed: number) { - if (this.angle >= Math.PI * 2) { - this.angle = 0; - } else { - this.angle += 0.1 * (elapsed / 1000); + renderer.render(scene, camera); + this.animationFrame = requestAnimationFrame(this.render.bind(this)); } - let camera = this.camera; - let angle = this.angle; + scaleBoxes() { + let analyser = this.analyser; - camera.position.x = 1.01 * Math.sin(angle); - camera.position.z = 1.01 * Math.cos(angle); + analyser.getFloatFrequencyData(this.analyserData); - /* camera.position.y = (1 - Math.abs(angle - 0.5) / 0.5); */ - camera.lookAt(0, 0, 0); - } + for (let freq = 0; freq < analyser.frequencyBinCount; freq++) { + let height = analyser.maxDecibels / this.analyserData[freq]; - resize() { - let canvas = this.canvas; - if (canvas.parentElement === null) { - throw Error("Could not access canvas parent for size calculation"); + if (height > 0.3) { + height -= 0.3; + } else { + height = 1e-6; + } + + this.boxes[freq].scale.y = height; + } } - // Compute the height of all our siblings - let combinedHeight = 0; - for (let i = 0; i < canvas.parentElement.children.length; i++) { - const child = canvas.parentElement.children[i]; + rotateCamera(elapsed: number) { + if (this.angle >= Math.PI * 2) { + this.angle = 0; + } else { + this.angle += 0.1 * (elapsed / 1000); + } - if (child != canvas) { - combinedHeight += child.clientHeight; - } + let camera = this.camera; + let angle = this.angle; + + camera.position.x = 1.01 * Math.sin(angle); + camera.position.z = 1.01 * Math.cos(angle); + + /* camera.position.y = (1 - Math.abs(angle - 0.5) / 0.5); */ + camera.lookAt(0, 0, 0); } - // The remaining space we want to fill - let remainingHeight = canvas.parentElement.clientHeight - combinedHeight; - canvas.height = remainingHeight; - canvas.width = canvas.parentElement.clientWidth; + resize() { + let canvas = this.canvas; + if (canvas.parentElement === null) { + throw Error("Could not access canvas parent for size calculation"); + } - this.camera.aspect = canvas.width / remainingHeight; - this.camera.updateProjectionMatrix(); - this.renderer.setSize(canvas.width, remainingHeight); - } + // Compute the height of all our siblings + let combinedHeight = 0; + for (let i = 0; i < canvas.parentElement.children.length; i++) { + const child = canvas.parentElement.children[i]; - stop() { - if (this.animationFrame != 0) { - cancelAnimationFrame(this.animationFrame); + if (child != canvas) { + combinedHeight += child.clientHeight; + } + } + + // The remaining space we want to fill + let remainingHeight = canvas.parentElement.clientHeight - combinedHeight; + canvas.height = remainingHeight; + canvas.width = canvas.parentElement.clientWidth; + + this.camera.aspect = canvas.width / remainingHeight; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(canvas.width, remainingHeight); + } + + stop() { + if (this.animationFrame != 0) { + cancelAnimationFrame(this.animationFrame); + } } - } } class Visualizer extends React.Component { - private analyser: AnalyserNode; - private canvas: React.RefObject; - private drawer: CanvasDrawer; + private analyser: AnalyserNode; + private canvas: React.RefObject; + private drawer: CanvasDrawer; - constructor(props: VisualizerProps) { - super(props); - this.canvas = React.createRef(); - } - - render() { - return ( - - ); - } - - componentDidMount() { - if (this.canvas.current === null) { - throw Error("Failed to create canvas; aborting"); + constructor(props: VisualizerProps) { + super(props); + this.canvas = React.createRef(); } - this.analyser = this.props.audioContext.createAnalyser(); - this.analyser.fftSize = 2048; - this.analyser.smoothingTimeConstant = 0.8; - this.props.audioSource.connect(this.analyser); + render() { + return ( + + ); + } - this.drawer = new CanvasDrawer(this.analyser, this.canvas.current); - } + componentDidMount() { + if (this.canvas.current === null) { + throw Error("Failed to create canvas; aborting"); + } - componentWillUnmount() { - this.drawer.stop(); - this.props.audioSource.disconnect(this.analyser); - } + this.analyser = this.props.audioContext.createAnalyser(); + this.analyser.fftSize = 2048; + this.analyser.smoothingTimeConstant = 0.8; + this.props.audioSource.connect(this.analyser); + + this.drawer = new CanvasDrawer(this.analyser, this.canvas.current); + } + + componentWillUnmount() { + this.drawer.stop(); + this.props.audioSource.disconnect(this.analyser); + } } export default Visualizer; diff --git a/src/music/index.tsx b/src/music/index.tsx index a5c2917..2a053cf 100644 --- a/src/music/index.tsx +++ b/src/music/index.tsx @@ -5,23 +5,24 @@ import { Provider } from "react-redux"; import { store } from "./store"; import MusicPlayer from "./MusicPlayer"; import { setSource, setTitle } from "./store/music/types"; +// @ts-ignore Can't find module - this is an mp3 file, so... import mseq from "./Mseq_-_Journey.mp3"; const rootElement = document.getElementById("playerUI"); ReactDOM.render( - - - , - rootElement + + + , + rootElement ); store.dispatch(setSource(mseq)); store.dispatch( - setTitle({ - name: "Journey", - artist: "Mseq", - album: "Unknown album", - length: 192052244, - }) + setTitle({ + name: "Journey", + artist: "Mseq", + album: "Unknown album", + length: 192052244, + }) ); diff --git a/src/music/store/index.ts b/src/music/store/index.ts index f523735..a994939 100644 --- a/src/music/store/index.ts +++ b/src/music/store/index.ts @@ -4,14 +4,17 @@ import { MusicState } from "./music/types"; import { musicStateReducer } from "./music/reducers"; export interface State { - musicState: MusicState; + musicState: MusicState; } const rootReducer = combineReducers({ - musicState: musicStateReducer, + musicState: musicStateReducer, }); export const store = createStore( - rootReducer, - window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() + rootReducer, + // @ts-ignore Missing property - this will only exist if the devtools extension is installed, and is actually what we're checking for + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() ); + +export type Dispatch = typeof store.dispatch; diff --git a/src/music/store/music/reducers.ts b/src/music/store/music/reducers.ts index 83883d1..f351d60 100644 --- a/src/music/store/music/reducers.ts +++ b/src/music/store/music/reducers.ts @@ -2,44 +2,44 @@ import { createReducer } from "redux-act"; import update from "immutability-helper"; import { - Title, - MusicState, - setTitle, - toggleMute, - togglePlay, - setSource, + Title, + MusicState, + setTitle, + toggleMute, + togglePlay, + setSource, } from "./types"; const defaultTitle: Title = { - name: "Untitled", - artist: "Unknown Artist", - album: "Unknown Album", - length: 0, + name: "Untitled", + artist: "Unknown Artist", + album: "Unknown Album", + length: 0, }; const initialState: MusicState = { - muted: false, - playing: false, - title: defaultTitle, - playTime: 0, + muted: false, + playing: false, + title: defaultTitle, + playTime: 0, }; export const musicStateReducer = createReducer( - { - [setTitle]: (state: MusicState, title: Title): MusicState => { - return update(state, { - title: { $set: title }, - }); + { + [setTitle]: (state: MusicState, title: Title): MusicState => { + return update(state, { + title: { $set: title }, + }); + }, + [togglePlay]: (state: MusicState): MusicState => { + return update(state, { $toggle: ["playing"] }); + }, + [toggleMute]: (state: MusicState): MusicState => { + return update(state, { $toggle: ["muted"] }); + }, + [setSource]: (state: MusicState, source: string): MusicState => { + return update(state, { source: { $set: source } }); + }, }, - [togglePlay]: (state: MusicState): MusicState => { - return update(state, { $toggle: ["playing"] }); - }, - [toggleMute]: (state: MusicState): MusicState => { - return update(state, { $toggle: ["muted"] }); - }, - [setSource]: (state: MusicState, source: string): MusicState => { - return update(state, { source: { $set: source } }); - }, - }, - initialState + initialState ); diff --git a/src/music/store/music/types.ts b/src/music/store/music/types.ts index 39555fc..ca05cbd 100644 --- a/src/music/store/music/types.ts +++ b/src/music/store/music/types.ts @@ -1,29 +1,29 @@ import { Action, createAction } from "redux-act"; export interface Title { - name: string; - artist: string; - album: string; - /** - * The length of the title in nanoseconds. - */ - length: number; + name: string; + artist: string; + album: string; + /** + * The length of the title in nanoseconds. + */ + length: number; } export interface MusicState { - muted: boolean; - playing: boolean; - title: Title; - playTime: number; - source?: string; + muted: boolean; + playing: boolean; + title: Title; + playTime: number; + source?: string; } export const setTitle: (title: Title) => Action = createAction( - "set currently playing title" + "set currently playing title" ); export const setPlayTime: (time: number) => Action = createAction( - "set the play time" + "set the play time" ); export const toggleMute: () => Action = createAction("toggle mute"); @@ -31,5 +31,5 @@ export const toggleMute: () => Action = createAction("toggle mute"); export const togglePlay: () => Action = createAction("toggle play"); export const setSource: (source: string) => Action = createAction( - "set the title" + "set the title" );