diff --git a/examples/toybox/musicPlayer/createNewMusicPlayerOnClick.js b/examples/toybox/musicPlayer/createNewMusicPlayerOnClick.js new file mode 100644 index 0000000000..68da1d2628 --- /dev/null +++ b/examples/toybox/musicPlayer/createNewMusicPlayerOnClick.js @@ -0,0 +1,100 @@ +// +// createNewMusicPlayerOnClick.js +// +// Created by Brad Hefta-Gaub on 3/3/16. +// Copyright 2016 High Fidelity, Inc. +// +// Entity Script that you can attach to any entity to have it spawn new "music players" +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +(function(){ + + var musicPlayerScript = Script.resolvePath("./musicPlayer.js"); + var imageShader = Script.resolvePath("./imageShader.fs"); + var defaultImage = Script.resolvePath("./defaultImage.jpg"); + + function getPositionToCreateEntity() { + var distance = 0.5; + var direction = Quat.getFront(Camera.orientation); + var offset = Vec3.multiply(distance, direction); + var placementPosition = Vec3.sum(Camera.position, offset); + + var cameraPosition = Camera.position; + + var HALF_TREE_SCALE = 16384; + + var cameraOutOfBounds = Math.abs(cameraPosition.x) > HALF_TREE_SCALE || Math.abs(cameraPosition.y) > HALF_TREE_SCALE || Math.abs(cameraPosition.z) > HALF_TREE_SCALE; + var placementOutOfBounds = Math.abs(placementPosition.x) > HALF_TREE_SCALE || Math.abs(placementPosition.y) > HALF_TREE_SCALE || Math.abs(placementPosition.z) > HALF_TREE_SCALE; + + if (cameraOutOfBounds && placementOutOfBounds) { + return null; + } + + placementPosition.x = Math.min(HALF_TREE_SCALE, Math.max(-HALF_TREE_SCALE, placementPosition.x)); + placementPosition.y = Math.min(HALF_TREE_SCALE, Math.max(-HALF_TREE_SCALE, placementPosition.y)); + placementPosition.z = Math.min(HALF_TREE_SCALE, Math.max(-HALF_TREE_SCALE, placementPosition.z)); + + return placementPosition; + } + + function createNewIPOD() { + var iPodPosition = { x: 0, y: .5, z: 0}; + var iPodDimensions = { x: 0.15, y: 0.3, z: 0.03 }; + var overlayDimensions = { x: 0.13, y: 0.13, z: 0.001 }; + var boxOverlayDimensions = { x: 0.13, y: 0.13, z: 0.0001 }; + + var iPodID = Entities.addEntity({ + type: "Box", + name: "music player", + position: iPodPosition, + dimensions: iPodDimensions, + color: { red: 150, green: 150, blue: 150}, + script: musicPlayerScript, + dynamic: true + }); + print(iPodID); + + var textID = Entities.addEntity({ + type: "Text", + name: "now playing", + position: { x: 0, y: (0.5+0.07), z: 0.0222}, + dimensions: overlayDimensions, + color: { red: 150, green: 150, blue: 150}, + parentID: iPodID, + lineHeight: 0.01, + text: "Pause" + }); + + + var newAlbumArt = JSON.stringify( + { + "ProceduralEntity": { + "version":2, + "shaderUrl":imageShader, + "uniforms":{"iSpeed":0,"iShell":1}, + "channels":[defaultImage] + } + }); + + + var albumArtID = Entities.addEntity({ + type: "Box", + name: "album art", + position: { x: 0, y: (0.5-0.07), z: 0.01506}, + dimensions: boxOverlayDimensions, + color: { red: 255, green: 255, blue: 255}, + userData: newAlbumArt, + parentID: iPodID + }); + Entities.editEntity(iPodID, { position: getPositionToCreateEntity() }); + } + + + this.clickDownOnEntity = function(myID, mouseEvent) { + createNewIPOD(); + }; +}) + + diff --git a/examples/toybox/musicPlayer/defaultImage.jpg b/examples/toybox/musicPlayer/defaultImage.jpg new file mode 100644 index 0000000000..54de95bf42 Binary files /dev/null and b/examples/toybox/musicPlayer/defaultImage.jpg differ diff --git a/examples/toybox/musicPlayer/imageShader.fs b/examples/toybox/musicPlayer/imageShader.fs new file mode 100644 index 0000000000..7c1aba248d --- /dev/null +++ b/examples/toybox/musicPlayer/imageShader.fs @@ -0,0 +1,44 @@ +float aspect(vec2 v) { + return v.x / v.y; +} + +vec3 indexedTexture() { + vec2 uv = _position.xy; + uv += 0.5; + uv.y = 1.0 - uv.y; + + float targetAspect = iWorldScale.x / iWorldScale.y; + float sourceAspect = aspect(iChannelResolution[0].xy); + float aspectCorrection = sourceAspect / targetAspect; + if (aspectCorrection > 1.0) { + float offset = aspectCorrection - 1.0; + float halfOffset = offset / 2.0; + uv.y -= halfOffset; + uv.y *= aspectCorrection; + } else { + float offset = 1.0 - aspectCorrection; + float halfOffset = offset / 2.0; + uv.x -= halfOffset; + uv.x /= aspectCorrection; + } + + if (any(lessThan(uv, vec2(0.0)))) { + return vec3(0.0); + } + + if (any(greaterThan(uv, vec2(1.0)))) { + return vec3(0.0); + } + + vec4 color = texture(iChannel0, uv); + return color.rgb * max(0.5, sourceAspect) * max(0.9, fract(iWorldPosition.x)); +} + +float getProceduralColors(inout vec3 diffuse, inout vec3 specular, inout float shininess) { + if (_position.z > -0.49) { + discard; + } + + specular = indexedTexture(); + return 1.0; +} \ No newline at end of file diff --git a/examples/toybox/musicPlayer/musicPlayer.js b/examples/toybox/musicPlayer/musicPlayer.js new file mode 100644 index 0000000000..4a7655efcc --- /dev/null +++ b/examples/toybox/musicPlayer/musicPlayer.js @@ -0,0 +1,330 @@ +// +// musicPlayer.js +// +// Created by Brad Hefta-Gaub on 3/3/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, randFloat, randInt */ + +(function() { + + var imageShader = Script.resolvePath("./imageShader.fs"); + var defaultImage = Script.resolvePath("./defaultImage.jpg"); + + var MAPPING_NAME = "com.highfidelity.musicPlayerEntity"; + + var PLAYLIST_URL = "https://spreadsheets.google.com/feeds/cells/1x-ceGPGHldkHadARABFWfujLPTOWzXJPhrf2bTwg2cQ/od6/public/basic?alt=json"; + var SONG_VOLUME = 0.1; + var HEADPHONES_ATTACHMENT = { + modelURL: "https://s3.amazonaws.com/hifi-public/brad/musicplayer/headphones2-v2.fbx", + jointName: "Head", + translation: {"x": 0, "y": 0.19, "z": 0.06}, + rotation: {"x":0,"y":0.7071067690849304,"z":0.7071067690849304,"w":0}, + scale: 0.435, + isSoft: false + }; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Various helper functions... + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // Helper function for returning either a value, or the default value if the value is undefined. This is + // is handing in parsing JSON where you don't know if the values have been set or not. + function valueOrDefault(value, defaultValue) { + if (value !== undefined) { + return value; + } + return defaultValue; + } + + // return a random float between high and low + function randFloat(low, high) { + return low + Math.random() * (high - low); + } + + // wears an attachment on MyAvatar + function wearAttachment(attachment) { + MyAvatar.attach(attachment.modelURL, + attachment.jointName, + attachment.translation, + attachment.rotation, + attachment.scale, + attachment.isSoft); + } + + // un-wears an attachment from MyAvatar + function removeAttachment(attachment) { + var attachments = MyAvatar.attachmentData; + var i, l = attachments.length; + for (i = 0; i < l; i++) { + if (attachments[i].modelURL === attachment.modelURL) { + attachments.splice(i, 1); + MyAvatar.attachmentData = attachments; + break; + } + } + } + + var _this; + MusicPlayer = function() { + _this = this; + this.equipped = false; + }; + + MusicPlayer.prototype = { + preload: function(entityID) { + print("preload"); + //print("rotation:" + JSON.stringify(Quat.fromPitchYawRollDegrees(-90,180,0))); + this.entityID = entityID; + + // Get the entities userData property, to see if someone has overridden any of our default settings + var userDataText = Entities.getEntityProperties(entityID, ["userData"]).userData; + //print(userDataText); + var userData = {}; + if (userDataText !== "") { + //print("calling JSON.parse"); + userData = JSON.parse(userDataText); + //print("userData:" + JSON.stringify(userData)); + } + var musicPlayerUserData = valueOrDefault(userData.musicPlayer, {}); + this.headphonesAttachment = valueOrDefault(musicPlayerUserData.headphonesAttachment, HEADPHONES_ATTACHMENT); + + this.track = 0; // start at the first track + this.playlistURL = valueOrDefault(musicPlayerUserData.playlistURL, PLAYLIST_URL); + this.songVolume = valueOrDefault(musicPlayerUserData.songVolume, SONG_VOLUME); + this.songPlaying = false; + + this.loadPlayList(); + + // Find my screen and any controlls + var children = Entities.getChildrenIDsOfJoint(entityID, 65535); + for (var child in children) { + var childID = children[child]; + var childProperties = Entities.getEntityProperties(childID,["type", "name"]); + if (childProperties.type == "Text" && childProperties.name == "now playing") { + this.nowPlayingID = childID; + } + if (childProperties.type == "Box" && childProperties.name == "album art") { + this.albumArt = childID; + } + } + }, + + unload: function() { + print("unload"); + if (_this.songInjector !== undefined) { + _this.songInjector.stop(); + } + }, + + loadPlayList: function() { + print("loadPlayList"); + var req = new XMLHttpRequest(); + req.open("GET", _this.playlistURL, false); + req.send(); + + var entries = JSON.parse(req.responseText).feed.entry; + + for (entry in entries) { + var cellDetails = entries[entry]; + var cellName = cellDetails.title.$t; + var column = Number(cellName.charCodeAt(0)) - Number("A".charCodeAt(0)); + var row = Number(cellName.slice(1)) - 1; + var cellContent = cellDetails.content.$t; + //print(JSON.stringify(cellDetails)); + //print("["+column +"/"+ row +":"+cellContent+"]"); + if (_this.playList === undefined) { + _this.playList = new Array(); + } + if (_this.playList[row] === undefined) { + _this.playList[row] = { }; + } + switch (column) { + case 0: + _this.playList[row].title = cellContent; + break; + case 1: + _this.playList[row].artist = cellContent; + break; + case 2: + _this.playList[row].album = cellContent; + break; + case 3: + _this.playList[row].url = cellContent; + _this.playList[row].sound = SoundCache.getSound(cellContent); + break; + case 4: + _this.playList[row].albumArtURL = cellContent; + break; + } + } + //print(req.responseText); + print(JSON.stringify(_this.playList)); + }, + + startEquip: function(id, params) { + var whichHand = params[0]; // "left" or "right" + print("I am equipped in the " + whichHand + " hand...."); + this.equipped = true; + this.hand = whichHand; + + this.loadPlayList(); // reload the playlist in case... + + this.mapHandButtons(whichHand); + wearAttachment(HEADPHONES_ATTACHMENT); + }, + + continueEquip: function(id, params) { + if (!this.equipped) { + return; + } + }, + releaseEquip: function(id, params) { + print("I am NO LONGER equipped...."); + this.hand = null; + this.equipped = false; + Controller.disableMapping(MAPPING_NAME); + removeAttachment(HEADPHONES_ATTACHMENT); + this.pause(); + }, + + mapHandButtons: function(hand) { + var mapping = Controller.newMapping(MAPPING_NAME); + if (hand === "left") { + mapping.from(Controller.Standard.LS).peek().to(this.playOrPause); + mapping.from(Controller.Standard.LX).peek().to(this.seek); + mapping.from(Controller.Standard.LY).peek().to(this.volume); + } else { + mapping.from(Controller.Standard.RS).peek().to(this.playOrPause); + mapping.from(Controller.Standard.RX).peek().to(this.seek); + mapping.from(Controller.Standard.RY).peek().to(this.volume); + } + Controller.enableMapping(MAPPING_NAME); + }, + + playOrPause: function(value) { + print("[playOrPause: "+value+"]"); + if (value === 1) { + if (!_this.songPlaying) { + _this.play(); + } else { + _this.pause(); + } + } + }, + + play: function() { + print("play current track:" + _this.track); + if (!_this.playList[_this.track].sound.downloaded) { + print("still waiting on track to download...."); + return; // not yet ready + } + + var statusText = "Song:" + _this.playList[_this.track].title + "\n" + + "Artist:" + _this.playList[_this.track].artist + "\n" + + "Album:" + _this.playList[_this.track].album; + + Entities.editEntity(_this.nowPlayingID, { text: statusText }); + + var newAlbumArt = JSON.stringify( + { + "ProceduralEntity": { + "version":2, + "shaderUrl":imageShader, + "uniforms":{"iSpeed":0,"iShell":1}, + "channels":[_this.playList[_this.track].albumArtURL] + } + }); + + Entities.editEntity(_this.albumArt, { userData: newAlbumArt }); + + + _this.songInjector = Audio.playSound(_this.playList[_this.track].sound, { + position: MyAvatar.position, + volume: _this.songVolume, + loop: false, + localOnly: true + }); + _this.songPlaying = true; + }, + + pause: function() { + print("pause"); + Entities.editEntity(_this.nowPlayingID, { text: "Paused" }); + if (_this.songInjector !== undefined) { + _this.songInjector.stop(); + } + var newAlbumArt = JSON.stringify( + { + "ProceduralEntity": { + "version":2, + "shaderUrl":imageShader, + "uniforms":{"iSpeed":0,"iShell":1}, + "channels":[defaultImage] + } + }); + + Entities.editEntity(_this.albumArt, { userData: newAlbumArt }); + + + _this.songPlaying = false; + }, + + seek: function(value) { + print("[seek: " + value + "]"); + if (value > 0.9) { + _this.next(); + } else if (value < -0.9) { + _this.previous(); + } + }, + + volume: function(value) { + print("adjusting volume disabled because of a bug in audio injectors...."); + /* + var scaledValue = value / 20; + _this.songVolume += scaledValue; + print("[volume: " + value + "] new volume:" + _this.songVolume); + if (_this.songInjector !== undefined) { + print("[volume: attempting to set options...."); + var newOptions = { + position: MyAvatar.position, + volume: _this.songVolume, + loop: false, + localOnly: true + }; + + _this.songInjector.options = newOptions; + print("[volume: attempting to set options.... RESULT:" + JSON.stringify(_this.songInjector.options)); + } + */ + }, + + previous: function() { + print("[previous]"); + _this.pause(); + _this.track--; + if (_this.track < 0) { + _this.track = (_this.playList.length - 1); + } + _this.play(); + }, + + next: function() { + print("[next]"); + _this.pause(); + _this.track++; + if (_this.track >= _this.playList.length) { + _this.track = 0; + } + _this.play(); + }, + + }; + + // entity scripts always need to return a newly constructed object of our type + return new MusicPlayer(); +});