// Check if client is using a mobile phone
const getMobileOS = ( ) => {
const ua = navigator . userAgent
if ( /android/i . test ( ua ) ) {
return "Android"
}
else if ( /iPad|iPhone|iPod/ . test ( ua ) || ( navigator . platform === 'MacIntel' && navigator . maxTouchPoints > 1 ) ) {
return "iOS"
}
return "Other"
}
const isRunningOn = getMobileOS ( ) ;
if ( isRunningOn != 'Other' ) {
alert ( 'Whoop, it seems that you attempt to access this website on a ' + isRunningOn + ' mobile/tablet, which might not work properly. You should better use a computer instead.' ) ;
}
// Loads the Youtube IFrame Player API code asynchronously.
let tag = document . createElement ( 'script' ) ;
tag . src = 'https://www.youtube.com/iframe_api' ;
let firstScriptTag = document . getElementsByTagName ( 'script' ) [ 0 ] ;
firstScriptTag . parentNode . insertBefore ( tag , firstScriptTag ) ;
//THE DJ CROSSFADER!!!
let crossfader = document . querySelector ( '#crossfader' ) ;
class player {
trackID ;
player ;
crossfaderCoef ;
constructor ( id ) {
this . id = id ;
this . iframe = document . querySelector ( '#' + this . id ) ;
this . node = this . iframe . parentNode ;
this . volume = this . node . querySelector ( '.volume-slider' ) . value ;
this . volSliderVal = this . node . querySelector ( '.volume-slider-value' ) ;
this . volVal = this . node . querySelector ( '.volume-value' )
this . volSliderVal . innerHTML = this . volume + '%' ;
this . loadButton = this . node . querySelector ( '.load-button' ) ;
this . speedSlider = this . node . querySelector ( '.speed-slider' ) ;
this . speedSlider . nextElementSibling . querySelector ( '.speed-value' ) . innerHTML = this . speedSlider . value ;
}
init ( trackID ) {
this . trackID = trackID ;
let src = ` https://www.youtube-nocookie.com/embed/ ${ trackID } ?disablekb=1&enablejsapi=1&playsinline=1&autoplay=0&rel=0&modestbranding=1&iv_load_policy=3&version=3 ` ;
this . iframe . setAttribute ( 'src' , src ) ;
this . player = new YT . Player ( this . id , {
events : {
'onReady' : this . onPlayerReady . bind ( this ) ,
'onStateChange' : this . onPlayerStateChange . bind ( this ) ,
'onError' : this . onPlayerError . bind ( this )
}
} ) ;
}
onPlayerReady ( e ) {
this . player . setVolume ( this . volume ) ;
this . node . querySelector ( '.volume-slider' ) . addEventListener ( 'input' , ( e ) => {
this . volume = e . target . value ;
this . volSliderVal . innerHTML = this . volume + '%' ;
this . volumeUpdate ( ) ;
} ) ;
this . loadButton . addEventListener ( 'click' , ( ) => {
let url = prompt ( 'Enter a YouTube URL (track or playlist):' , 'https://www.youtube.com/watch?v=' + this . trackID ) ;
if ( url ) {
let resp = check _url ( url ) ;
if ( resp < 0 ) {
alert ( 'Wrong URL...' ) ;
console . log ( 'Nothing to load!' ) ;
} else if ( resp == 0 ) {
let newTrackID = get _video _id ( url ) ;
let newPlaylistID = get _playlist _id ( url ) ;
if ( newPlaylistID [ 0 ] != 0 ) {
console . log ( 'playlistid to load: ' + newPlaylistID ) ;
this . loadPlaylist ( newPlaylistID ) ;
} else if ( newTrackID != 0 ) {
console . log ( 'track to load: ' + newTrackID ) ;
this . load ( newTrackID ) ;
} else {
alert ( 'Wrong URL...' ) ;
console . log ( 'Nothing to load!' ) ;
}
} else if ( resp == 1 ) {
// fetch_bandcamp_url(url);
alert ( 'Wrong URL...' ) ;
console . log ( 'Nothing to load!' ) ;
}
}
} ) ;
this . speedSlider . addEventListener ( 'input' , ( e ) => {
this . speedSlider . nextElementSibling . querySelector ( '.speed-value' ) . innerHTML = e . target . value ;
this . player . setPlaybackRate ( Number ( e . target . value ) ) ;
} ) ;
this . volumeUpdate ( ) ;
}
onPlayerStateChange ( e ) {
console . log ( this . id + ' state change: ' + e . data ) ;
}
onPlayerError ( e ) {
console . log ( this . id + ' - Error: ' + e . data ) ;
}
load ( trackID ) {
this . player . loadVideoById ( trackID ) ;
}
cue ( trackID ) {
this . player . cueVideoById ( trackID ) ;
}
loadPlaylist ( playlistID ) {
//First element should be the playlist ID, second element the index of the video to play
let list = playlistID [ 0 ] ;
let index = playlistID [ 1 ] ;
this . player . loadPlaylist ( {
list : list ,
listType : 'playlist' ,
index : index
} ) ;
}
volumeUpdate ( ) {
let outputVolume = Math . round ( this . volume * this . crossfaderCoef ) ;
this . player . setVolume ( outputVolume ) ;
this . volVal . innerHTML = outputVolume + '%' ;
let boxShadowStyle = '0 0 3em #ddbddd'
this . iframe . style . boxShadow = boxShadowStyle + componentToHex ( Math . round ( this . crossfaderCoef * 255 ) ) ;
}
}
let playerA = new player ( 'playerA' ) ;
let playerB = new player ( 'playerB' ) ;
function onYouTubeIframeAPIReady ( ) {
playerA . init ( 'anWaxGNDRJ4' )
playerB . init ( 'PhEuAuhH418' ) ;
}
calcCrossfaderCoefs ( crossfader . value ) ;
crossfader . addEventListener ( 'input' , ( e ) => {
//Makes the slider stick on the middle
if ( Math . abs ( crossfader . value - 500 ) < 20 ) {
crossfader . value = 500 ;
}
calcCrossfaderCoefs ( crossfader . value ) ;
playerA . volumeUpdate ( ) ;
playerB . volumeUpdate ( ) ;
// updateCrossfader(crossfader.value);
} ) ;
function calcCrossfaderCoefs ( input ) {
let normVal = input / 1000 ;
//Classic 'transition' curve
let coefB = Math . min ( normVal * 2 , 1 ) ;
let coefA = Math . min ( ( 1 - normVal ) * 2 , 1 ) ;
//Cheap smooth curve
// let coefB = normVal ** 2;
// let coefA = (1 - normVal) ** 2;
//Amplitude-preserving curve (but not power)
// let coefB = (normVal ** 2) * (3 - 2 * normVal);
// let coefA = 1 - coefB;
playerA . crossfaderCoef = coefA ;
playerB . crossfaderCoef = coefB ;
}
function check _url ( url ) {
let test = url . indexOf ( 'youtu' ) ;
if ( test > - 1 && test < 13 ) {
return 0 ; //It's most likely a youtube link
} else if ( url . indexOf ( '.bandcamp' ) > - 1 || url . indexOf ( '/album/' ) > - 1 || url . indexOf ( '/track/' ) > - 1 ) {
return 1 ; //It's most likely a bandcamp link
} else {
return - 1 ; //Wrong link
}
}
// From https://stackoverflow.com/a/55616161/14182148
function get _playlist _id ( url ) {
let VID _REGEX = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
let regPlaylist = /[?&]list=([^#\&\?]+)/ ;
let regIndex = /[?&]index=([^#]+)/ ;
let match = url . match ( regPlaylist ) ;
// console.log('match ' + match);
let matchIndex = url . match ( regIndex ) ;
// console.log('matchindex ' + matchIndex);
// console.log('regexp ' + match.match(/=LL([^#\&\?]*)/));
let index = 0 ;
if ( matchIndex != null ) {
index = matchIndex [ 1 ] - 1 ;
}
if ( match != null && match [ 1 ] . lenght > 2 ) { // lenght > 2 prevents from trying to load a "list=LL" (liked videos of signed-in user)
return [ match [ 1 ] , index ] ;
} else {
return [ 0 , 0 ] ;
}
}
function get _video _id ( url ) { //originally 'video_id_from_playlist'
let VID _REGEX = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
let video _id = url . match ( VID _REGEX ) ;
if ( video _id != null ) {
return video _id [ 2 ] ;
} else {
return 0 ;
}
}
function fetch _bandcamp _url ( url ) {
let iframe = document . createElement ( 'iframe' ) ;
iframe . setAttribute ( 'class' , 'hidden' ) ;
iframe . setAttribute ( 'src' , url ) ;
playerB . node . parentNode . appendChild ( iframe ) ;
console . log ( iframe ) ;
let scrap = iframe . contentWindow . document . querySelector ( 'script[data-tralbum]' ) ;
console . log ( 'scrap: ' + scrap ) ;
}
// Thumbnails: https://i1.ytimg.com/vi/{trackID}/mqdefault.jpg
// Converts values from 0-255 to 00-ff
function componentToHex ( c ) {
var hex = c . toString ( 16 ) ;
return hex . length == 1 ? "0" + hex : hex ;
}
// -----------------------------------
// BANDCAMP Tests
// -----------------------------------
// let surl = 'https://bandcamp.com/api/bcsearch_public_api/1/autocomplete_elastic';
// let req = {"search_text": "techno",
// "search_filter": "",
// "full_page": false,
// "fan_id": null
// };
// let bcURL = 'https://clett.bandcamp.com/track/placenta';
// let request = new Request(bcURL);
// let headers = new Headers({
// 'X-Requested-With': 'com.bandcamp.android',
// 'Content-Type': 'application/json',
// 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9; Unknown Device)',
// 'Host': 'bandcamp.com',
// });
//
// let data;
// fetch(bcURL, {
// method: 'GET',
// headers: headers
// })
// .then(response => response.json())
// .then(data => console.log('data: ' + data));
// console.log('response: ' + data);
// console.log('data: ' + data);
// let html = async await fetch(bcURL).text(); // html as text
// let doc = new DOMParser().parseFromString(html, 'text/html');
// doc.title; doc.body;
// let Http = new XMLHttpRequest();
// Http.open("GET", bcURL);
// Http.send();
//
// Http.onreadystatechange = (e) => {
// console.log(Http.responseText)
// }
// -----------------------------------
// BACKGROUND
// -----------------------------------
let areStarsShooting = true ;
let shootingStarWrapper = document . getElementsByClassName ( 'shooting-stars-wrapper' ) [ 0 ] ;
let shootingStar ;
function newShootingStar ( ) {
shootingStar = document . createElement ( 'div' ) ;
let topStart = Math . floor ( 0.6 * window . innerHeight * Math . random ( ) ) ;
let leftStart = Math . floor ( window . innerWidth * ( 0.1 + 0.8 * Math . random ( ) ) ) ;
let duration = 1000 + Math . floor ( Math . random ( ) * 5000 ) ;
let distance = 200 + Math . floor ( Math . random ( ) * 500 ) ;
let angle = Math . floor ( Math . random ( ) * 30 ) - 15 ;
let direction = Math . random ( ) < 0.5 ? - 1 : 1 ;
shootingStar . style . top = topStart + 'px' ;
shootingStar . style . left = leftStart + 'px' ;
// root.style.setProperty('--shooting-star-duration', duration + "px");
// shootingStarCSS.insertRule(':root{--shooting-star-duration: '+ duration + 'ms}');
document . documentElement . style . setProperty ( '--shooting-star-duration' , duration + "ms" ) ;
document . documentElement . style . setProperty ( '--shooting-star-translate' , distance + "px" ) ;
document . documentElement . style . setProperty ( '--shooting-star-angle' , angle + "deg" ) ;
document . documentElement . style . setProperty ( '--shooting-star-direction' , direction ) ;
shootingStarWrapper . appendChild ( shootingStar ) ;
shootingStar . classList . add ( 'shooting-star' ) ;
setTimeout ( ( ) => {
shootingStar . remove ( ) ;
// let nextIn = 10;
let nextIn = 7000 + Math . floor ( Math . random ( ) * 5000 ) ;
if ( areStarsShooting ) {
setTimeout ( ( ) => {
newShootingStar ( ) ;
} , nextIn ) ;
}
} , duration ) ;
// setTimeout('newShootingStar()', duration + nextIn);
}
newShootingStar ( ) ;
document . getElementById ( 'shooting-stars-enable' ) . innerHTML = '⭐️ Disable shooting stars⭐️ ' ;
document . getElementById ( 'shooting-stars-enable' ) . addEventListener ( 'click' , ( ) => {
if ( areStarsShooting ) {
document . getElementById ( 'shooting-stars-enable' ) . innerHTML = '⭐️ Enable shooting stars⭐️ ' ;
areStarsShooting = false ;
} else {
document . getElementById ( 'shooting-stars-enable' ) . innerHTML = '⭐️ Disable shooting stars⭐️ ' ;
areStarsShooting = true ;
newShootingStar ( ) ;
}
} ) ;
// Enable/disable shooting stars (with P5JS background)
// if (typeof processingSketch !== 'undefined') {
// let separatorSpan = document.createElement('span');
// separatorSpan.innerHTML = ' - ';
// document.getElementById('shooting-stars-enable').after(separatorSpan);
// document.getElementById('shooting-stars-enable').innerHTML = 'Disable shooting stars⭐️ ';
// }
// document.getElementById('shooting-stars-enable').addEventListener('click', () => {
// if (processingIsOn) {
// processingSketch.frameRate(0);
// document.getElementById('shooting-stars-enable').innerHTML = 'Enable shooting stars⭐️ ';
// processingIsOn = false;
// } else {
// processingSketch.frameRate(24);
// document.getElementById('shooting-stars-enable').innerHTML = 'Disable shooting stars⭐️ ';
// processingIsOn = true;
// }
// });
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- EMAIL OBFUSCATION - found here http : //www.grall.name/posts/1/antiSpam-emailAddressObfuscation.html
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
document . getElementById ( 'contact-me' ) . addEventListener ( 'click' , ( e ) => {
var y = decode ( "pbagnpg@gsypy.klm" ) ; //https://rot13.com/
e . target . setAttribute ( "href" , "mailto:" + y ) ;
} )
function decode ( a ) {
// ROT13 : a Caesar cipher
// letter -> letter' such that code(letter') = (code(letter) + 13) modulo 26
return a . replace ( /[a-zA-Z]/g , function ( c ) {
return String . fromCharCode ( ( c <= "Z" ? 90 : 122 ) >= ( c = c . charCodeAt ( 0 ) + 13 ) ? c : c - 26 ) ;
} )
} ;
/ * *
SEARCH WITH YOUTUBE API
* /
let lastSearchQuery ;
let results ;
gapi . load ( "client" , loadClient ) ;
// https://developers.google.com/youtube/v3/docs/search/list
function loadClient ( ) {
gapi . client . setApiKey ( "AIzaSyCuz5tGc9oDxw6RZnpibakYFHzMvsd8bUs" ) ;
return gapi . client . load ( "https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest" )
. then ( function ( ) { console . log ( "GAPI client loaded for API" ) ; } ,
function ( err ) { console . error ( "Error loading GAPI client for API" , err ) ; } ) ;
}
function execute ( query ) {
// Make sure the client is loaded before calling this method.
return gapi . client . youtube . search . list ( {
"part" : [
"snippet"
] ,
"maxResults" : 15 ,
"q" : query ,
"type" : [
"video"
] ,
"fields" : "items(id/videoId,snippet(title,thumbnails/medium))"
} )
. then ( function ( response ) {
if ( response . status == 200 ) {
results = response . result ;
localStorage . setItem ( 'sres' , JSON . stringify ( results ) ) ;
displaySearchResults ( results ) ;
}
// Handle the results here (response.result has the parsed body).
console . log ( "Response" , response ) ;
} ,
function ( err ) {
console . error ( "Execute error" , err ) ;
alert ( "Whoops, something went wrong. \n Maybe all of my daily Youtube Data API requests have been used. Try again later! \n Maybe it's something else. Try again later?" ) ;
} ) ;
}
const searchForm = document . getElementById ( 'search-bar' ) ;
const searchQueryObj = document . getElementById ( 'search-query' ) ;
const searchButtonObj = document . getElementById ( 'search-button' ) ;
const searchResultsObj = document . getElementById ( 'search-results' ) ;
//When the page has loaded, store the hidden search-item class div as a template for future searchs
const searchResultItemObj = searchResultsObj . querySelector ( '.search-item' ) . cloneNode ( true ) ;
searchResultsObj . querySelector ( '.search-item' ) . remove ( ) ;
searchForm . addEventListener ( 'submit' , ( e ) => {
e . preventDefault ( ) ; // We handle the form submit by ourselve
if ( searchQueryObj . value != lastSearchQuery ) { // prevent useless requests
console . log ( 'Search for: ' + searchQueryObj . value ) ;
execute ( searchQueryObj . value ) ;
lastSearchQuery = searchQueryObj . value ;
}
return false ;
} ) ;
function displaySearchResults ( res ) {
// First we clean previous search results
while ( searchResultsObj . firstChild ) {
searchResultsObj . removeChild ( searchResultsObj . firstChild ) ;
}
res . items . forEach ( ( item ) => {
let itemElmt = searchResultItemObj . cloneNode ( true ) ;
itemElmt . querySelector ( '.search-item-info img' ) . setAttribute ( 'src' , item . snippet . thumbnails . medium . url ) ;
itemElmt . querySelector ( '.search-item-info span' ) . innerHTML = item . snippet . title ;
itemElmt . querySelector ( '.search-item-info a' ) . setAttribute ( 'href' , 'https://www.youtube.com/watch?v=' + item . id . videoId )
itemElmt . dataset . id = item . id . videoId ;
itemElmt . querySelector ( '.load-to-A' ) . addEventListener ( 'click' , ( ) => {
playerA . cue ( item . id . videoId ) ;
} ) ;
itemElmt . querySelector ( '.load-to-B' ) . addEventListener ( 'click' , ( ) => {
playerB . cue ( item . id . videoId ) ;
} ) ;
console . log ( itemElmt ) ;
searchResultsObj . appendChild ( itemElmt ) ;
} ) ;
}
// For testing purpose
// results = JSON.parse(localStorage.getItem('sres'));
// if (results) {
// displaySearchResults(results);
// }