diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..3ccf435 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], + plugins: ['svelte3', '@typescript-eslint'], + ignorePatterns: ['*.cjs'], + overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], + settings: { + 'svelte3/typescript': () => require('typescript') + }, + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020 + }, + env: { + browser: true, + es2017: true, + node: true + } +}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 2e6e6b4..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true - }, - "extends": ["plugin:react/recommended", "airbnb"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["prettier", "react", "@typescript-eslint"], - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"] - } - } - }, - "rules": { - "prettier/prettier": "error", - "import/extensions": [ - "error", - "ignorePackages", - { - "js": "never", - "jsx": "never", - "ts": "never", - "tsx": "never" - } - ], - "react/jsx-filename-extension": [1, { "extensions": [".tsx", ".jsx"] }], - "func-names": 0, - "quotes": 0, - "comma-dangle": 0, - "react/jsx-one-expression-per-line": 0 - } -} diff --git a/.gitignore b/.gitignore index c5cea0d..a3c7586 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,13 @@ -# build output -dist - -# dependencies -node_modules/ -.snowpack/ - -# logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# environment variables -.env -.env.production - -# macOS-specific files .DS_Store - -# project files -public/playlist.json +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +.vercel +.output # Local Netlify folder .netlify diff --git a/.npmrc b/.npmrc index 0cc653b..b6f27f1 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1 @@ -## force pnpm to hoist -shamefully-hoist = true \ No newline at end of file +engine-strict=true diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ff2677e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100 +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 0b2a640..cac0e10 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,21 +1,3 @@ { - "editor.formatOnSave": true, - "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "eslint.format.enable": true, - "[javascript]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" - }, - "[typescript]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" - }, - "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[astro]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "cSpell.words": ["astrojs", "dragmove", "hackerfm", "knadh", "meyda"] -} + "editor.formatOnSave": true +} \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs deleted file mode 100644 index 15513e8..0000000 --- a/astro.config.mjs +++ /dev/null @@ -1,21 +0,0 @@ -export default { - // projectRoot: '.', // Where to resolve all URLs relative to. Useful if you have a monorepo project. - // pages: './src/pages', // Path to Astro components, pages, and data - // dist: './dist', // When running `astro build`, path to final static output - // public: './public', // A folder of static files Astro will copy to the root. Useful for favicons, images, and other files that don’t need processing. - buildOptions: { - // site: 'http://example.com', // Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. - sitemap: true, // Generate sitemap (set to "false" to disable) - }, - devOptions: { - // hostname: 'localhost', // The hostname to run the dev server on. - // port: 3000, // The port to run the dev server on. - // tailwindConfig: '', // Path to tailwind.config.js if used, e.g. './tailwind.config.js' - }, - renderers: [ - "@astrojs/renderer-preact", - "@astrojs/renderer-react", - "@astrojs/renderer-svelte", - "@astrojs/renderer-vue" - ], -}; diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..572afda --- /dev/null +++ b/netlify.toml @@ -0,0 +1,3 @@ +[build] + command = "npm run build" + publish = "build" \ No newline at end of file diff --git a/package.json b/package.json index 7972c9c..2892e06 100644 --- a/package.json +++ b/package.json @@ -1,37 +1,47 @@ { - "name": "hackerfm", - "version": "0.0.1", - "private": true, - "scripts": { - "dev": "yarn generatePlaylist && astro dev", - "start": "astro dev", - "build": "yarn generatePlaylist && astro build", - "preview": "astro preview", - "generatePlaylist": "tsm scripts/generatePlaylist.ts", - "lint": "eslint src/**/*.{js,ts,tsx} --fix" - }, - "devDependencies": { - "@astrojs/renderer-react": "^0.3.0", - "@typescript-eslint/eslint-plugin": "^5.5.0", - "@typescript-eslint/parser": "^5.5.0", - "astro": "^0.21.6", - "eslint": "^8.3.0", - "eslint-config-airbnb": "^19.0.1", - "eslint-plugin-import": "^2.25.3", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.27.1", - "eslint-plugin-react-hooks": "^4.3.0", - "prettier": "2.5.0", - "prettier-plugin-astro": "^0.0.11", - "tsm": "^2.1.4" - }, - "dependencies": { - "@knadh/dragmove": "^0.1.3", - "@types/meyda": "^4.3.2", - "axios": "^0.24.0", - "meyda": "^5.2.2", - "react": "^17.0.2", - "three": "^0.130.0" - } + "name": "my-app", + "version": "0.0.1", + "scripts": { + "dev": "svelte-kit dev --host", + "build": "svelte-kit build", + "package": "svelte-kit package", + "preview": "svelte-kit preview", + "check": "svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", + "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." + }, + "devDependencies": { + "@poppanator/sveltekit-svg": "^0.2.2", + "@sveltejs/adapter-auto": "next", + "@sveltejs/adapter-netlify": "^1.0.0-next.37", + "@sveltejs/adapter-static": "^1.0.0-next.24", + "@sveltejs/kit": "next", + "@types/cookie": "^0.4.1", + "@typescript-eslint/eslint-plugin": "^4.31.1", + "@typescript-eslint/parser": "^4.31.1", + "autoprefixer": "^10.4.0", + "carbon-icons-svelte": "^10.44.3", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-svelte3": "^3.2.1", + "prettier": "^2.4.1", + "prettier-plugin-svelte": "^2.4.0", + "svelte": "^3.44.0", + "svelte-check": "^2.2.6", + "svelte-preprocess": "^4.10.1", + "svelte-typewriter": "^3.0.0-alpha.5", + "tslib": "^2.3.1", + "typescript": "^4.4.3" + }, + "type": "module", + "dependencies": { + "@fontsource/fira-mono": "^4.5.0", + "@knadh/dragmove": "^0.1.3", + "@lukeed/uuid": "^2.0.0", + "@types/meyda": "^4.3.2", + "cookie": "^0.4.1", + "meyda": "^5.3.0", + "three": "^0.130.0" + } } diff --git a/postcss.config.cjs b/postcss.config.cjs new file mode 100644 index 0000000..e9895c3 --- /dev/null +++ b/postcss.config.cjs @@ -0,0 +1,7 @@ +const autoprefixer = require('autoprefixer'); + +const config = { + plugins: [autoprefixer] +}; + +module.exports = config; diff --git a/public/assets/logo.svg b/public/assets/logo.svg deleted file mode 100644 index d751556..0000000 --- a/public/assets/logo.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 1f53798..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: / diff --git a/public/style/background.css b/public/style/background.css deleted file mode 100644 index ade46dd..0000000 --- a/public/style/background.css +++ /dev/null @@ -1,150 +0,0 @@ -/* -Adapted from the amazing work of: https://gist.github.com/codingdudecom - -Codepen - https://codepen.io/inegoita/pen/BgdXMw -Github Gist - https://gist.github.com/codingdudecom/1f9c416339fb7dcb7cef12170d411be6 -*/ - -:root { - --grid-color: rgba(115, 59, 139, 0.7); - --grid-size: 20px; - --grid-blur: 0px; -} - -.grid { - position: fixed; - width: 90%; - height: 30%; - background-image: repeating-linear-gradient( - 90deg, - transparent calc(1px + var(--grid-blur)), - transparent var(--grid-size), - var(--grid-color, black) - calc(var(--grid-size) + 1px + var(--grid-blur, 0px)) - ), - repeating-linear-gradient( - 180deg, - var(--grid-color, black) 6%, - transparent calc(1px + var(--grid-blur, 0px)), - transparent var(--grid-size), - var(--grid-color, black) - calc(var(--grid-size) + 1px + var(--grid-blur, 0px)) - ); - transform: perspective(10vh) rotateX(47deg) translateZ(41px); - bottom: 12vh; -} - -.background-80s { - background: linear-gradient( - to bottom, - #010310 0, - #0c1142 24vh, - #45125e 45vh, - #d53567 60vh, - #f0c3d9 65vh, - #0c1142 65vh - ) - fixed; - background-size: 100% var(--background-height, 100vh); - overflow: hidden; - position: absolute; - left: 0; - top: 0; - width: 100vw; - height: 100vh; - z-index: -999; -} - -/*stars*/ -.stars:after { - transform: translateY(-40%); - content: " "; - border-radius: 100%; - width: 3px; - height: 4px; - position: absolute; - left: 0; - top: 0; - z-index: -1; - box-shadow: 5vw 15vh 2px white, 1vw 33vh 0px white, 2vw 25vh 2px white, - 10vw 10vh 2px white, 12vw 20vh 0px white, 30vw 15vh 2px white, - 16vw 5vh 2px white, 24vw 10vh 0px white, 32vw 40vh 0px white, - 33vw 35vh 2px white, 12vw 38vh 2px white, 24vw 10vh 0px white, - 33vw 5vh 2px white, 20vw 10vh 0px white, 80vw 10vh 2px white, - 62vw 20vh 0px white, 60vw 15vh 2px white, 70vw 7vh 0px white, - 62vw 50vh 0px white, 65vw 35vh 2px white, 64vw 10vh 0px white, - 85vw 2vh 0px white, 92vw 40vh 0px white, 75vw 35vh 2px white, - 90vw 10vh 0px white; - opacity: 0.3; - animation: glitter 2s infinite; -} - -@keyframes glitter { - 0% { - opacity: 0.5; - } - 50% { - opacity: 0.9; - } - - 100% { - opacity: 0.5; - } -} - -.crt-overlay-effect { - width: 100%; - height: 100vh; - position: fixed; - left: 0; - top: 0; - background-image: repeating-linear-gradient( - rgba(0, 0, 0, 0.3) 0, - transparent 1px, - transparent 2px, - rgba(0, 0, 0, 0.3) 3px - ); - pointer-events: none; -} - -/* turn off the grid on narrow screens */ -@media screen and (max-width: 750px) and (max-height: 600px) { - .grid { - width: 0%; - height: 0%; - background-image: none; - } - - .background-80s { - background: linear-gradient( - to bottom, - #010310 0, - #0c1142 24%, - #45125e 75%, - #d53567 90%, - #f0c3d9 100% - ) - fixed; - } -} - -/* turn off the grid on narrow screens */ -@media screen and (max-width: 500px) { - .grid { - width: 0%; - height: 0%; - background-image: none; - } - - .background-80s { - background: linear-gradient( - to bottom, - #010310 0, - #0c1142 24%, - #45125e 75%, - #d53567 90%, - #f0c3d9 100% - ) - fixed; - } -} diff --git a/public/style/global.css b/public/style/global.css deleted file mode 100644 index db29e5f..0000000 --- a/public/style/global.css +++ /dev/null @@ -1,101 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); - -/* Modern CSS Reset Start */ - -/* Box sizing rules */ -*, -*::before, -*::after { - box-sizing: border-box; -} - -/* Remove default padding */ -ul[class], -ol[class] { - padding: 0; -} - -/* Remove default margin */ -body, -h1, -h2, -h3, -h4, -p, -ul[class], -ol[class], -li, -figure, -figcaption, -blockquote, -dl, -dd { - margin: 0; -} - -/* Set core body defaults */ -body { - min-height: 100vh; - scroll-behavior: smooth; - text-rendering: optimizeSpeed; - line-height: 1.5; -} - -/* Remove list styles on ul, ol elements with a class attribute */ -ul[class], -ol[class] { - list-style: none; -} - -/* A elements that don't have a class get default styles */ -a:not([class]) { - text-decoration-skip-ink: auto; -} - -/* Make images easier to work with */ -img { - max-width: 100%; - display: block; -} - -/* Natural flow and rhythm in articles by default */ -article > * + * { - margin-top: 1em; -} - -/* Inherit fonts for inputs and buttons */ -input, -button, -textarea, -select { - font: inherit; -} - -/* Modern CSS Reset End */ - -/*Strip button styling */ -button, input[type="submit"], input[type="reset"] { - background: none; - color: inherit; - border: none; - padding: 0; - font: inherit; - cursor: pointer; - outline: inherit; -} -/**/ - -/* Remove all animations and transitions for people that prefer not to see them */ -@media (prefers-reduced-motion: reduce) { - * { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } -} - -:root { - font-family: "PT Mono", monospace; - font-size: 1rem; -} diff --git a/public/style/home.css b/public/style/home.css deleted file mode 100644 index 08b0ad6..0000000 --- a/public/style/home.css +++ /dev/null @@ -1,32 +0,0 @@ -@import url("background.css"); - -.loading-background { - font-family: monospace; - width: 100%; - height: 100vh; - background-color: #a4508b; - background-image: linear-gradient(326deg, #a4508b 0%, #5f0a87 74%); - color: white; - white-space: pre-wrap; - text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #0073e6, 0 0 20px #0073e6, - 0 0 25px #0073e6, 0 0 30px #0073e6, 0 0 35px #0073e6; - padding: 10px; - text-transform: uppercase; - overflow: hidden; -} - -.desktop-screen { - height: 100vh; - width: 100vw; - background-image: url("/assets/background-1.jpg"); - background-size: cover; - background-repeat: no-repeat; -} - -.hide { - display: none; -} - -.terminal-text{ - font-size: clamp(.4rem, 1vw, .8rem); -} \ No newline at end of file diff --git a/sandbox.config.json b/sandbox.config.json deleted file mode 100644 index 9178af7..0000000 --- a/sandbox.config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "infiniteLoopProtection": true, - "hardReloadOnChange": false, - "view": "browser", - "template": "node", - "container": { - "port": 3000, - "startScript": "start", - "node": "14" - } -} diff --git a/scripts/generatePlaylist.ts b/scripts/generatePlaylist.ts deleted file mode 100644 index 5a82823..0000000 --- a/scripts/generatePlaylist.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Fetch playlist from audius and create a local copy - */ - -import * as fs from "fs"; -import axios from "axios"; -import { Playlist } from "../types/playlist.interface"; - -const AUDIUS_ENDPOINT = "https://api.audius.co"; -const APP_NAME = "HACKERFM"; -const hackerPlaylist = "nVmr6"; -const lofiNightsPlaylist = "nqZmb"; - -async function getAvailableHostUrl() { - const resp = await axios.get(AUDIUS_ENDPOINT, { - headers: { - Accept: "application/json", - }, - }); - - return resp.data.data; -} - -async function generatePlaylist( - hostURL: string, - playlist: string, - appName: string -) { - console.log(`Fetching playlist ${playlist} from ${hostURL}`); - - const resp = await axios.get( - `${hostURL}/v1/playlists/${playlist}/tracks?app_name=${appName}`, - { - headers: { - Accept: "application/json", - }, - } - ); - - const data = JSON.stringify(resp.data.data) as unknown as Playlist; - - fs.writeFile( - "./public/playlist.json", - JSON.stringify(data), - "utf8", - (err) => { - if (err) { - console.error( - "An error occurred while writing JSON Object to File.", - err - ); - return err; - } - - console.log("Playlist has been generated"); - } - ); -} - -getAvailableHostUrl().then((resp) => - generatePlaylist(resp[0], lofiNightsPlaylist, APP_NAME) -); diff --git a/src/actions/draggable.ts b/src/actions/draggable.ts new file mode 100644 index 0000000..728e4fc --- /dev/null +++ b/src/actions/draggable.ts @@ -0,0 +1,35 @@ +import { dragmove } from '@knadh/dragmove'; + +const snapThreshold = 1; + +export function makeDraggable(node: HTMLElement): void { + function onStart(el) { + // On drag start, remove the fixed bottom style to prevent the bottom + // from sticking on the screen. + + el.style.top = `${el.offsetTop}px`; + el.style.bottom = 'auto'; + } + function onEnd(el) { + console.log('end', el); + + // Automatically snap to corners. + if (window.innerHeight - (el.offsetTop + el.offsetHeight) < snapThreshold) { + el.style.top = 'auto'; + el.style.bottom = '0px'; + } + if (window.innerWidth - (el.offsetLeft + el.offsetWidth) < snapThreshold) { + el.style.left = 'auto'; + el.style.right = '0px'; + } + if (el.offsetTop < snapThreshold) { + el.style.top = '0px'; + } + if (el.offsetLeft < snapThreshold) { + el.style.left = '0px'; + } + } + dragmove(node, node.firstChild, onStart, onEnd); + + return; +} diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..18599a3 --- /dev/null +++ b/src/app.html @@ -0,0 +1,17 @@ + + + + + Hacker.FM + + + + + %svelte.head% + + + +
%svelte.body%
+ + + \ No newline at end of file diff --git a/src/components/AudioPlayer/AudioPlayer.astro b/src/components/AudioPlayer/AudioPlayer.astro deleted file mode 100644 index c98abab..0000000 --- a/src/components/AudioPlayer/AudioPlayer.astro +++ /dev/null @@ -1,86 +0,0 @@ ---- -import AudioPlayer from "../AudioPlayer/AudioPlayer.tsx"; - -export interface Props { - appName: string; -} -const { appName } = Astro.props; ---- - -
-
-
-
- {appName} - - - -
- -
- - - - diff --git a/src/components/AudioPlayer/AudioPlayer.svelte b/src/components/AudioPlayer/AudioPlayer.svelte new file mode 100644 index 0000000..58f8f58 --- /dev/null +++ b/src/components/AudioPlayer/AudioPlayer.svelte @@ -0,0 +1,112 @@ + + +
+ + + +
+ + diff --git a/src/components/AudioPlayer/AudioPlayer.tsx b/src/components/AudioPlayer/AudioPlayer.tsx deleted file mode 100644 index ec049f9..0000000 --- a/src/components/AudioPlayer/AudioPlayer.tsx +++ /dev/null @@ -1,165 +0,0 @@ -/* eslint-disable jsx-a11y/media-has-caption */ -import React, { useState, useEffect, useRef } from "react"; - -import AudioPlayerManager, { Song } from "./AudioPlayerManager"; -import playlist from "../../../public/playlist.json"; -import { Playlist } from "../../../types/playlist.interface"; -import isSafariBrowser from "../../helpers/detectSafari"; -import "./styles.css"; - -const parsedData = JSON.parse(playlist) as unknown as Playlist; - -const PLAYLIST = parsedData.map((track, index) => ({ - index, - name: track.title, - artist: track.user.name, - // TODO: need to check for available host up front. - url: `https://discoveryprovider.audius4.prod-us-west-2.staked.cloud/v1/tracks/${track.id}/stream?app_name=HACKERFM`, - permaLink: track.permalink, -})); - -const AudioPlayer = function () { - const canvasContainer = useRef(null!); - const audioElement = useRef(null!); - const AudioPlayerRef = useRef(null!); - - const [selectedTrack, setSelectedTrack] = useState(PLAYLIST[0]); - const [isPlaying, setIsPlaying] = useState(false); - - useEffect(() => { - AudioPlayerRef.current = new AudioPlayerManager( - canvasContainer.current, - audioElement.current, - PLAYLIST - ); - - if (window.HAS_INTERACTED && !isSafariBrowser()) { - AudioPlayerRef.current.playTrack(); - setIsPlaying(true); - } - }, []); - - const playSongHandler = () => { - AudioPlayerRef.current.playTrack(); - if (isPlaying) { - setIsPlaying(false); - } else { - setIsPlaying(true); - } - }; - - const playNextTrackHandler = () => { - if (selectedTrack.index === PLAYLIST.length - 1) { - AudioPlayerRef.current.changeTrack(PLAYLIST[0]); - setSelectedTrack(PLAYLIST[0]); - } else { - // decrement track - const incrementTrack = PLAYLIST[selectedTrack.index + 1]; - AudioPlayerRef.current.changeTrack(incrementTrack); - setSelectedTrack(incrementTrack); - } - }; - - const playPreviousTrackHandler = () => { - if (selectedTrack.index === 0) { - // loop to back of the list - const decrementTrack = PLAYLIST[PLAYLIST.length - 1]; - AudioPlayerRef.current.changeTrack(decrementTrack); - setSelectedTrack(decrementTrack); - } else { - // decrement track - const decrementTrack = PLAYLIST[selectedTrack.index - 1]; - AudioPlayerRef.current.changeTrack(decrementTrack); - setSelectedTrack(decrementTrack); - } - }; - - const openSongPermalinkHandler = () => { - window.open( - `https://audius.co${selectedTrack.permaLink}`, - "newwindow", - "width=800, height=800" - ); - }; - - return ( -
-
- ); -}; - -export default AudioPlayer; diff --git a/src/components/AudioPlayer/AudioPlayerManager.ts b/src/components/AudioPlayer/AudioPlayerManager.ts deleted file mode 100644 index 1682097..0000000 --- a/src/components/AudioPlayer/AudioPlayerManager.ts +++ /dev/null @@ -1,185 +0,0 @@ -import * as THREE from 'three'; -import * as Meyda from 'meyda'; - -import { fragmentShader, vertexShader } from './orbShader'; -import isSafariBrowser from '../../helpers/detectSafari'; - -export interface Song { - index: number; - url: string; - name: string; -} - -const uniforms: Record = { - uFrequency: { value: 1 }, - uAmplitude: { value: 4 }, - uDensity: { value: 1 }, - uStrength: { value: 0.8 }, - uDeepPurple: { value: 0.74 }, - uOpacity: { value: 0.73 }, - uBrightness: { - value: { x: 0.1, y: 0.15000000000000002, z: -0.44000000000000006 }, - }, - uContrast: { value: { x: 0.3, y: 0.3, z: 0.3 } }, - uOscilation: { value: { x: 0.45, y: 0.5, z: 0.9 } }, - uPhase: { value: { x: 0.31000000000000005, y: -0.68, z: 0.8 } }, -}; - -export default class AudioPlayerManager { - playlist: Song[]; - - audioElem: HTMLAudioElement; - - audioContext: AudioContext | undefined; - - canvasElem: HTMLElement; - - currentSongId: number; - - constructor( - canvasElem: HTMLElement, - audioElem: HTMLAudioElement, - playlist: Song[] = [], - ) { - this.playlist = playlist; - this.audioContext = undefined; - this.canvasElem = canvasElem; - this.audioElem = audioElem; - if (window.HAS_INTERACTED && !isSafariBrowser()) { - this.createVisualizer(); - } - this.createScene(); - } - - playTrack() { - this.handleFirstPlayWhenNoInteractionYet(); - if (this.audioElem.paused) { this.audioElem.play(); } else { this.audioElem.pause(); } - } - - changeTrack(track: Song) { - this.audioElem.src = track.url; - this.audioElem.play(); - } - - createScene() { - // Scene - const scene = new THREE.Scene(); - - /** - * Orb - */ - const orbGeometry = new THREE.IcosahedronGeometry(1, 10); - - const orbMaterial = new THREE.ShaderMaterial({ - wireframe: true, - // blending: THREE.AdditiveBlending, - transparent: true, - vertexShader, - fragmentShader, - uniforms, - }); - - const orbMesh = new THREE.Mesh(orbGeometry, orbMaterial); - - scene.add(orbMesh); - - /** - * Sizes - */ - const sizes = { - width: 300, - height: 270, - }; - - /** - * Camera - */ - // Base camera - const camera = new THREE.PerspectiveCamera( - 75, - sizes.width / sizes.height, - 0.1, - 100, - ); - camera.position.z = 2; - scene.add(camera); - - /** - * Renderer - */ - const renderer = new THREE.WebGLRenderer({ - canvas: this.canvasElem, - alpha: true, - }); - renderer.setSize(sizes.width, sizes.height); - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); - - /** - * Animate - */ - const tick = () => { - orbGeometry.rotateY(0.005); - // Render - renderer.render(scene, camera); - requestAnimationFrame(tick); - }; - tick(); - } - - createVisualizer() { - if (!this.audioElem) { - return; - } - const AudioContext = window.AudioContext || (window as any).webkitAudioContext; - this.audioContext = new AudioContext(); - - this.audioElem.crossOrigin = 'anonymous'; - - const src = this.audioContext.createMediaElementSource(this.audioElem); - const analyser = this.audioContext.createAnalyser(); - - src.connect(analyser); - analyser.connect(this.audioContext.destination); - analyser.fftSize = 256; - - function bound(_number: number, _min: number, _max: number) { - return Math.max(Math.min(_number, _max), _min); - } - - // Create the Meyda Analyzer - const analyzer = Meyda.createMeydaAnalyzer({ - // Pass in the AudioContext so that Meyda knows which AudioContext Box to work with - audioContext: this.audioContext, - // Source is the audio node that is playing your audio. It could be any node, - // but in this case, it's the MediaElementSourceNode corresponding to your - // HTML 5 Audio Element with your audio in it. - source: src, - // Buffer Size tells Meyda how often to check the audio feature, and is - // measured in Audio Samples. Usually there are 44100 Audio Samples in 1 - // second, which means in this case Meyda will calculate the level about 86 - // (44100/512) times per second. - bufferSize: 512, - // Here we're telling Meyda which audio features to calculate. While Meyda can - // calculate a variety of audio features, in this case we only want to know - // the "rms" (root mean square) of the audio signal, which corresponds to its - // level - featureExtractors: ['rms', 'energy'], - // Finally, we provide a function which Meyda will call every time it - // calculates a new level. This function will be called around 86 times per - // second. - callback: (features) => { - uniforms.uStrength.value = bound(features.rms as number, 0, 10) * 2; - uniforms.uDensity.value = bound(features.energy as number, 0, 2) * 2; - }, - }); - - analyzer.start(); - } - - handleFirstPlayWhenNoInteractionYet() { - if (!window.HAS_INTERACTED) { - this.createVisualizer(); - window.HAS_INTERACTED = true; - } - } -} diff --git a/src/components/AudioPlayer/TopBar.svelte b/src/components/AudioPlayer/TopBar.svelte new file mode 100644 index 0000000..7d5107b --- /dev/null +++ b/src/components/AudioPlayer/TopBar.svelte @@ -0,0 +1,60 @@ + + +
+
HACKER FM RADIO
+
+ + +
+
+ + diff --git a/src/components/AudioPlayer/TrackSeeker.svelte b/src/components/AudioPlayer/TrackSeeker.svelte new file mode 100644 index 0000000..7190701 --- /dev/null +++ b/src/components/AudioPlayer/TrackSeeker.svelte @@ -0,0 +1,84 @@ + + +
+ + +
{secondsToMinsAndSecs($currentTime)}/{secondsToMinsAndSecs($duration)}
+
+ + diff --git a/src/components/AudioPlayer/Visualiser.svelte b/src/components/AudioPlayer/Visualiser.svelte new file mode 100644 index 0000000..1cba545 --- /dev/null +++ b/src/components/AudioPlayer/Visualiser.svelte @@ -0,0 +1,162 @@ + + +