[
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Jonas Treub\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# VRComponent\n\nA virtual reality component for [Framer](http://framerjs.com). The virtual enviroment is created using the [cubemap technique](https://en.wikipedia.org/wiki/Cube_mapping). The cube requires six images, one for each side. Your own layers can be projected on top of the virtual environment. Projected layers are positioned using `heading` and `elevation` values.\n\nYou can listen for orientation updates using the `onOrientationChange` event. This event contains information about heading, elevation and tilt.\n\nRead more on the associated [blogpost](https://blog.framer.com/design-for-virtual-reality-b510b4641ca9#.jua87j76h).\n\n## Examples\n- [Base setup](https://framer.cloud/pzkuO)\n- [Event data](https://framer.cloud/XaSrp)\n- [Space puzzle](https://framer.cloud/KdTnn)\n- [Interactive resume](http://share.framerjs.com/ojd9q3dg5xem/) by [Jonathan](http://jonathanravasz.com)\n\nOn mobile the orientation is synced to that of your device. On desktop you can change the direction you are facing either by dragging the environment or by using your arrow keys.\n\n## Properties\n- **`front`** (set: imagePath *\\<string>*, get: layer)\n- **`right`** (set: imagePath *\\<string>*, get: layer)\n- **`back`** (set: imagePath *\\<string>*, get: layer)\n- **`left`** (set: imagePath *\\<string>*, get: layer)\n- **`top`** (set: imagePath *\\<string>*, get: layer)\n- **`bottom`** (set: imagePath *\\<string>*, get: layer)\n- **`panning`** *\\<bool>*\n- **`mobilePanning`** *\\<bool>*\n- **`arrowKeys`** *\\<bool>*\n- **`lookAtLatestProjectedLayer`** *\\<bool>* (handy during initial setup)\n\n--\n```coffee\n# Include the VRComponent\n{VRComponent, VRLayer} = require \"VRComponent\"\n\n# Create a new VRComponent, map images\nvr = new VRComponent\n\tfront:  \"images/front.png\"\n\tleft:   \"images/left.png\"\n\tright:  \"images/right.png\"\n\tback:   \"images/back.png\"\n\ttop:    \"images/top.png\"\n\tbottom: \"images/bottom.png\"\n```\n\n## Mapping images\nTo map your environment, you can look for cubemap images on the web. Each side is often named by the positive or negative X, Y, or Z axis.\n\n- **right** - positive-x\n- **top** - positive-y\n- **front** - positive-z\n- **left** - negative-x\n- **bottom** - negative-y\n- **back** - negative-z\n\nNote: This [tool](https://www.360toolkit.co/convert-spherical-equirectangular-to-cubemap.html) can convert your spherical panoramas to a cubemap.\n\n## Projecting Layers\nCreating a new Layer on top of your virtual environment will position them in 2D space by default. This is useful when looking to overlay interface elements, like sliders or printed values of heading, elevation or tilt. However, if you'd like to position layers within the 3D space, you can use the **`projectLayer()`** method.\n\n![sherical projection](http://github.jonastreub.com/sphere.png)\n\n##### VRLayers\nAny layer can be projected within your virtual environment, but if you'd like to adjust or animate their `heading` or `elevation` values later, you'll need to use a **`VRLayer`** instead.\n\n```coffee\n# Include VRComponent and VRLayer\n{VRComponent, VRLayer} = require \"VRComponent\"\n\n# Create layer\nlayerA = new VRLayer \n\n# Set layer heading and elevation before projecting\nlayerA.heading = 230\nlayerA.elevation = 10\n\n# Project the layer\nvr.projectLayer(layerA)\n```\n\n`distance` is a third positioning value which defaults to `1200`, equal to the default perspective. When distance and perspective are equal layers are rendered at the size they had before projecting.\n\n## Animating VRLayers\n\nThe `heading` and `elevation` values of a `VRLayer` can be animated.\n\n```coffee\n# Include VRComponent and VRLayer\n{VRComponent, VRLayer} = require \"VRComponent\"\n\n# Create VRLayer\nlayerA = new VRLayer\n\n# Project the VRLayer\nvr.projectLayer(layerA)\n\n# Animate the layer\nlayerA.animate\n\tproperties:\n\t\theading: 30\n\ttime: 10\n```\n\n## Events\n- **`onOrientationChange`** (*\\<object>* {heading, elevation, tilt})\n\n```coffee\nvr.onOrientationChange (data) ->\n\theading = data.heading\n\televation = data.elevation\n\ttilt = data.tilt\n```\n\n## Devices\n\nThe module has been tested on the following devices.\n\nDevice | Performance\n------ | -----------\niPhone 7 | Great\niPhone 6 | Good\niPhone 6 Plus | Great\niPhone 5C | Poor\nNexus 5 | Poor\n"
  },
  {
    "path": "VRComponent.coffee",
    "content": "\"\"\"\n\nVRComponent class\n\nproperties\n- front (set: imagePath <string>, get: layer)\n- right\n- back\n- left\n- top\n- bottom\n- heading <number>\n- elevation <number>\n- tilt <number> readonly\n\n- panning <bool>\n- mobilePanning <bool>\n- arrowKeys <bool>\n\n- lookAtLatestProjectedLayer <bool>\n\nmethods\n- projectLayer(layer) # heading and elevation are set as properties on the layer\n- hideEnviroment()\n\nevents\n- onOrientationChange (data {heading, elevation, tilt})\n\n--------------------------------------------------------------------------------\n\nVRLayer class\n\nproperties\n- heading <number> (from 0 up to 360)\n- elevation <number> (from -90 down to 90 up)\n\n\"\"\"\n\nSIDES = [\n\t\"north\",\n\t\"front\",\n\t\"east\",\n\t\"right\",\n\t\"south\",\n\t\"back\",\n\t\"west\",\n\t\"left\",\n\t\"top\",\n\t\"bottom\",\n]\n\nKEYS = {\n\tLeftArrow: 37\n\tUpArrow: 38\n\tRightArrow: 39\n\tDownArrow: 40\n}\n\nKEYSDOWN = {\n\tleft: false\n\tup: false\n\tright: false\n\tdown: false\n}\n\nEvents.OrientationDidChange = \"orientationdidchange\"\n\nclass VRAnchorLayer extends Layer\n\n\tconstructor: (layer, cubeSide) ->\n\t\tsuper()\n\t\t@width = 2\n\t\t@height = 2\n\t\t@clip = false\n\t\t@name = \"anchor\"\n\t\t@cubeSide = cubeSide\n\t\t@backgroundColor = null\n\n\t\t@layer = layer\n\t\tlayer.parent = @\n\t\tlayer.center()\n\n\t\tlayer.on \"change:orientation\", (newValue, layer) => @updatePosition(layer)\n\t\t@updatePosition(layer)\n\n\t\tlayer._context.on \"layer:destroy\", (layer) => @destroy() if layer is @layer\n\n\tupdatePosition: (layer) ->\n\t\thalfCubeSide = @cubeSide / 2\n\t\tdpr = Framer.CurrentContext.pixelMultiplier\n\t\tx = ((@cubeSide - @width) / 2) * dpr\n\t\ty = ((@cubeSide - @height) / 2) * dpr\n\t\tz = layer.distance * dpr\n\t\t@style.WebkitTransform = \"translateX(#{x}px) translateY(#{y}px) rotateZ(#{layer.heading}deg) rotateX(#{90-layer.elevation}deg) translateZ(#{z}px) rotateX(180deg)\"\n\nclass exports.VRLayer extends Layer\n\n\tconstructor: (options = {}) ->\n\t\toptions = _.defaults options,\n\t\t\theading: 0\n\t\t\televation: 0\n\t\tsuper options\n\n\t@define \"heading\",\n\t\tget: -> @_heading\n\t\tset: (value) ->\n\t\t\tif value >= 360\n\t\t\t\tvalue = value % 360\n\t\t\telse if value < 0\n\t\t\t\trest = Math.abs(value) % 360\n\t\t\t\tvalue = 360 - rest\n\t\t\troundedValue = Math.round(value * 1000) / 1000\n\t\t\tif @_heading isnt roundedValue\n\t\t\t\t@_heading = roundedValue\n\t\t\t\t@emit(\"change:heading\", @_heading)\n\t\t\t\t@emit(\"change:orientation\", @_heading)\n\n\t@define \"elevation\",\n\t\tget: -> @_elevation\n\t\tset: (value) ->\n\t\t\tvalue = Utils.clamp(value, -90, 90)\n\t\t\troundedValue = Math.round(value * 1000) / 1000\n\t\t\tif roundedValue isnt @_elevation\n\t\t\t\t@_elevation = roundedValue\n\t\t\t\t@emit(\"change:elevation\", roundedValue)\n\t\t\t\t@emit(\"change:orientation\", roundedValue)\n\n\t@define \"distance\",\n\t\tget: -> @_distance\n\t\tset: (value) ->\n\t\t\tif value isnt @_distance\n\t\t\t\t@_distance = value\n\t\t\t\t@emit(\"change:distance\", value)\n\t\t\t\t@emit(\"change:orientation\", value)\n\nclass exports.VRComponent extends Layer\n\n\tconstructor: (options = {}) ->\n\t\toptions = _.defaults options,\n\t\t\tcubeSide: 1500\n\t\t\tperspective: 600\n\t\t\tlookAtLatestProjectedLayer: false\n\t\t\twidth: Screen.width\n\t\t\theight: Screen.height\n\t\t\tarrowKeys: true\n\t\t\tpanning: true\n\t\t\tmobilePanning: true\n\t\t\tflat: true\n\t\t\tclip: true\n\t\tsuper options\n\n\t\t# to hide the seems where the cube surfaces come together we disable the viewport perspective and set a black background\n\t\tScreen.backgroundColor = \"black\"\n\t\tScreen.perspective = 0\n\n\t\t@setupDefaultValues()\n\t\t@degToRad = Math.PI / 180\n\t\t@backgroundColor = null\n\n\t\t@createCube(options.cubeSide)\n\t\t@lookAtLatestProjectedLayer = options.lookAtLatestProjectedLayer\n\t\t@setupKeys(options.arrowKeys)\n\n\t\t@heading = options.heading if options.heading?\n\t\t@elevation = options.elevation if options.elevation?\n\n\t\t@setupPan(options.panning)\n\t\t@mobilePanning = options.mobilePanning\n\n\t\tif Utils.isMobile()\n\t\t\twindow.addEventListener \"deviceorientation\", (event) => @orientationData = event\n\n\t\tFramer.Loop.on(\"update\", @deviceOrientationUpdate)\n\n\t\t# Make sure we remove the update from the loop when we destroy the context\n\t\tFramer.CurrentContext.on \"reset\", -> Framer.Loop.off(\"update\", @deviceOrientationUpdate)\n\n\t\t@on \"change:frame\", -> @desktopPan(0,0)\n\n\tsetupDefaultValues: =>\n\n\t\t@_heading = 0\n\t\t@_elevation = 0\n\t\t@_tilt = 0\n\n\t\t@_headingOffset = 0\n\t\t@_elevationOffset = 0\n\t\t@_deviceHeading = 0\n\t\t@_deviceElevation = 0\n\n\tsetupKeys: (enabled) ->\n\n\t\t@arrowKeys = enabled\n\n\t\tdocument.addEventListener \"keydown\", (event) =>\n\t\t\tswitch event.which\n\t\t\t\twhen KEYS.UpArrow\n\t\t\t\t\tKEYSDOWN.up = true\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\twhen KEYS.DownArrow\n\t\t\t\t\tKEYSDOWN.down = true\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\twhen KEYS.LeftArrow\n\t\t\t\t\tKEYSDOWN.left = true\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\twhen KEYS.RightArrow\n\t\t\t\t\tKEYSDOWN.right = true\n\t\t\t\t\tevent.preventDefault()\n\n\t\tdocument.addEventListener \"keyup\", (event) =>\n\t\t\tswitch event.which\n\t\t\t\twhen KEYS.UpArrow\n\t\t\t\t\tKEYSDOWN.up = false\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\twhen KEYS.DownArrow\n\t\t\t\t\tKEYSDOWN.down = false\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\twhen KEYS.LeftArrow\n\t\t\t\t\tKEYSDOWN.left = false\n\t\t\t\t\tevent.preventDefault()\n\t\t\t\twhen KEYS.RightArrow\n\t\t\t\t\tKEYSDOWN.right = false\n\t\t\t\t\tevent.preventDefault()\n\n\t\twindow.onblur = ->\n\t\t\tKEYSDOWN.up = false\n\t\t\tKEYSDOWN.down = false\n\t\t\tKEYSDOWN.left = false\n\t\t\tKEYSDOWN.right = false\n\n\t@define \"heading\",\n\t\tget: ->\n\t\t\theading = @_heading + @_headingOffset\n\t\t\tif heading > 360\n\t\t\t\theading = heading % 360\n\t\t\telse if heading < 0\n\t\t\t\trest = Math.abs(heading) % 360\n\t\t\t\theading = 360 - rest\n\t\t\treturn Math.round(heading * 1000) / 1000\n\t\tset: (value) ->\n\t\t\t@lookAt(value, @_elevation)\n\n\t@define \"elevation\",\n\t\tget: -> Math.round(@_elevation * 1000) / 1000\n\t\tset: (value) ->\n\t\t\tvalue = Utils.clamp(value, -90, 90)\n\t\t\t@lookAt(@_heading, value)\n\n\t@define \"tilt\",\n\t\tget: -> @_tilt\n\t\tset: (value) -> throw \"Tilt is readonly\"\n\n\tSIDES.map (face) =>\n\t\t@define face,\n\t\t\tget: -> @layerFromFace(face) # @getImage(face)\n\t\t\tset: (value) -> @setImage(face, value)\n\n\tcreateCube: (cubeSide = @cubeSide) =>\n\t\t@cubeSide = cubeSide\n\n\t\t@world?.destroy()\n\t\t@world = new Layer\n\t\t\tname: \"world\"\n\t\t\tsuperLayer: @\n\t\t\tsize: cubeSide\n\t\t\tbackgroundColor: null\n\t\t\tclip: false\n\t\t@world.center()\n\n\t\t@sides = []\n\t\thalfCubeSide = @cubeSide / 2\n\t\tcolors = [\"#866ccc\", \"#28affa\", \"#2dd7aa\", \"#ffc22c\", \"#7ddd11\", \"#f95faa\"]\n\t\tsideNames = [\"front\", \"right\", \"back\", \"left\", \"top\", \"bottom\"]\n\n\t\tfor sideIndex in [0...6]\n\n\t\t\trotationX = 0\n\t\t\trotationX = -90 if sideIndex in [0...4]\n\t\t\trotationX = 180 if sideIndex is 4\n\n\t\t\trotationY = 0\n\t\t\trotationY = sideIndex * -90 if sideIndex in [0...4]\n\n\t\t\tside = new Layer\n\t\t\t\tsize: cubeSide\n\t\t\t\tz: -halfCubeSide\n\t\t\t\toriginZ: halfCubeSide\n\t\t\t\trotationX: rotationX\n\t\t\t\trotationY: rotationY\n\t\t\t\tparent: @world\n\t\t\t\tname: sideNames[sideIndex]\n\t\t\t\thtml: sideNames[sideIndex]\n\t\t\t\tcolor: \"white\"\n\t\t\t\tbackgroundColor: colors[sideIndex]\n\t\t\t\tstyle:\n\t\t\t\t\tlineHeight: \"#{cubeSide}px\"\n\t\t\t\t\ttextAlign: \"center\"\n\t\t\t\t\tfontSize: \"#{cubeSide / 10}px\"\n\t\t\t\t\tfontWeight: \"100\"\n\t\t\t\t\tfontFamily: \"Helvetica Neue\"\n\t\t\t@sides.push(side)\n\t\t\tside._backgroundColor = side.backgroundColor\n\n\t\tfor key of @sideImages when @sideImages?\n\t\t\t@setImage key, @sideImages[key]\n\n\thideEnviroment: ->\n\t\tfor side in @sides\n\t\t\tside.destroy()\n\n\tlayerFromFace: (face) ->\n\t\treturn unless @sides?\n\t\tmap =\n\t\t\tnorth: @sides[0]\n\t\t\tfront: @sides[0]\n\t\t\teast:  @sides[1]\n\t\t\tright: @sides[1]\n\t\t\tsouth: @sides[2]\n\t\t\tback:  @sides[2]\n\t\t\twest:  @sides[3]\n\t\t\tleft:  @sides[3]\n\t\t\ttop:   @sides[4]\n\t\t\tbottom:@sides[5]\n\t\treturn map[face]\n\n\tsetImage: (face, imagePath) ->\n\n\t\tthrow Error \"VRComponent setImage, wrong name for face: \" + face + \", valid options: front, right, back, left, top, bottom, north, east, south, west\" unless face in SIDES\n\n\t\t@sideImages = {} unless @sideImages?\n\t\t@sideImages[face] = imagePath\n\n\t\tlayer = @layerFromFace(face)\n\n\t\tif imagePath?\n\t\t\tlayer?.html = \"\"\n\t\t\tlayer?.image = imagePath\n\t\telse\n\t\t\tlayer?.html = layer?.name\n\t\t\tlayer?.backgroundColor = layer?._backgroundColor\n\n\tgetImage: (face) ->\n\n\t\tthrow Error \"VRComponent getImage, wrong name for face: \" + face + \", valid options: front, right, back, left, top, bottom, north, east, south, west\" unless face in SIDES\n\n\t\tlayer = @layerFromFace(face)\n\t\treturn layer.image if layer?\n\n\tprojectLayer: (insertLayer) ->\n\n\t\theading = insertLayer.heading\n\t\theading = 0 unless heading?\n\n\t\tif heading >= 360\n\t\t\theading = value % 360\n\t\telse if heading < 0\n\t\t\trest = Math.abs(heading) % 360\n\t\t\theading = 360 - rest\n\n\t\televation = insertLayer.elevation\n\t\televation = 0 unless elevation?\n\t\televation = Utils.clamp(elevation, -90, 90)\n\n\t\tdistance = insertLayer.distance\n\t\tdistance = 600 unless distance?\n\n\t\tinsertLayer.heading = heading\n\t\tinsertLayer.elevation = elevation\n\t\tinsertLayer.distance = distance\n\n\t\tanchor = new VRAnchorLayer(insertLayer, @cubeSide)\n\t\tanchor.superLayer = @world\n\n\t\t@lookAt(heading, elevation) if @lookAtLatestProjectedLayer\n\n\t# Mobile device orientation\n\n\tdeviceOrientationUpdate: =>\n\n\t\tif Utils.isDesktop()\n\t\t\tif @arrowKeys\n\t\t\t\tif @_lastCallHorizontal is undefined\n\t\t\t\t\t@_lastCallHorizontal = 0\n\t\t\t\t\t@_lastCallVertical = 0\n\t\t\t\t\t@_accelerationHorizontal = 1\n\t\t\t\t\t@_accelerationVertical = 1\n\t\t\t\t\t@_goingUp = false\n\t\t\t\t\t@_goingLeft = false\n\n\t\t\t\tdate = new Date()\n\t\t\t\tx = .1\n\t\t\t\tif KEYSDOWN.up or KEYSDOWN.down\n\t\t\t\t\tdiff = date - @_lastCallVertical\n\t\t\t\t\tif diff < 30\n\t\t\t\t\t\tif @_accelerationVertical < 30\n\t\t\t\t\t\t\t@_accelerationVertical += 0.18\n\t\t\t\t\tif KEYSDOWN.up\n\t\t\t\t\t\tif @_goingUp is false\n\t\t\t\t\t\t\t@_accelerationVertical = 1\n\t\t\t\t\t\t\t@_goingUp = true\n\t\t\t\t\t\t@desktopPan(0, 1 * @_accelerationVertical * x)\n\t\t\t\t\telse\n\t\t\t\t\t\tif @_goingUp is true\n\t\t\t\t\t\t\t@_accelerationVertical = 1\n\t\t\t\t\t\t\t@_goingUp = false\n\n\t\t\t\t\t\t@desktopPan(0, -1 * @_accelerationVertical * x)\n\t\t\t\t\t@_lastCallVertical = date\n\n\t\t\t\telse\n\t\t\t\t\t@_accelerationVertical = 1\n\n\t\t\t\tif KEYSDOWN.left or KEYSDOWN.right\n\t\t\t\t\tdiff = date - @_lastCallHorizontal\n\t\t\t\t\tif diff < 30\n\t\t\t\t\t\tif @_accelerationHorizontal < 25\n\t\t\t\t\t\t\t@_accelerationHorizontal += 0.18\n\t\t\t\t\tif KEYSDOWN.left\n\t\t\t\t\t\tif @_goingLeft is false\n\t\t\t\t\t\t\t@_accelerationHorizontal = 1\n\t\t\t\t\t\t\t@_goingLeft = true\n\t\t\t\t\t\t@desktopPan(1 * @_accelerationHorizontal * x, 0)\n\t\t\t\t\telse\n\t\t\t\t\t\tif @_goingLeft is true\n\t\t\t\t\t\t\t@_accelerationHorizontal = 1\n\t\t\t\t\t\t\t@_goingLeft = false\n\t\t\t\t\t\t@desktopPan(-1 * @_accelerationHorizontal * x, 0)\n\t\t\t\t\t@_lastCallHorizontal = date\n\t\t\t\telse\n\t\t\t\t\t@_accelerationHorizontal = 1\n\n\t\telse if @orientationData?\n\n\t\t\talpha = @orientationData.alpha\n\t\t\tbeta = @orientationData.beta\n\t\t\tgamma = @orientationData.gamma\n\n\t\t\t@directionParams(alpha, beta, gamma) if alpha isnt 0 and beta isnt 0 and gamma isnt 0\n\n\t\t\txAngle = beta\n\t\t\tyAngle = -gamma\n\t\t\tzAngle = alpha\n\n\t\t\thalfCubeSide = @cubeSide/2\n\t\t\torientation = \"rotate(#{window.orientation * -1}deg) \"\n\t\t\ttranslationX = \"translateX(#{((@width / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)\"\n\t\t\ttranslationY = \" translateY(#{((@height / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)\"\n\t\t\ttranslationZ = \" translateZ(#{@perspective * Framer.CurrentContext.pixelMultiplier}px)\"\n\t\t\trotation = translationZ + translationX + translationY + orientation + \" rotateY(#{yAngle}deg) rotateX(#{xAngle}deg) rotateZ(#{zAngle}deg)\" + \" rotateZ(#{-@_headingOffset}deg)\"\n\t\t\t@world.style[\"webkitTransform\"] = rotation\n\n\tdirectionParams: (alpha, beta, gamma) ->\n\n\t\talphaRad = alpha * @degToRad\n\t\tbetaRad = beta * @degToRad\n\t\tgammaRad = gamma * @degToRad\n\n\t\t# Calculate equation components\n\t\tcA = Math.cos(alphaRad)\n\t\tsA = Math.sin(alphaRad)\n\t\tcB = Math.cos(betaRad)\n\t\tsB = Math.sin(betaRad)\n\t\tcG = Math.cos(gammaRad)\n\t\tsG = Math.sin(gammaRad)\n\n\t\t# x unitvector\n\t\txrA = -sA * sB * sG + cA * cG\n\t\txrB = cA * sB * sG + sA * cG\n\t\txrC = cB * sG\n\n\t\t# y unitvector\n\t\tyrA = -sA * cB\n\t\tyrB = cA * cB\n\t\tyrC = -sB\n\n\t\t# -z unitvector\n\t\tzrA = -sA * sB * cG - cA * sG\n\t\tzrB = cA * sB * cG - sA * sG\n\t\tzrC = cB * cG\n\n\t\t# Calculate heading\n\t\theading = Math.atan(zrA / zrB)\n\n\t\t# Convert from half unit circle to whole unit circle\n\t\tif zrB < 0\n\t\t\theading += Math.PI\n\t\telse if zrA < 0\n\t\t\theading += 2 * Math.PI\n\n\t\t# # Calculate Altitude (in degrees)\n\t\televation = Math.PI / 2 - Math.acos(-zrC)\n\n\t\tcH = Math.sqrt(1 - (zrC * zrC))\n\t\ttilt = Math.acos(-xrC / cH) * Math.sign(yrC)\n\n\t\t# Convert radians to degrees\n\t\theading *= 180 / Math.PI\n\t\televation *= 180 / Math.PI\n\t\ttilt *= 180 / Math.PI\n\n\t\t@_heading = Math.round(heading * 1000) / 1000\n\t\t@_elevation = Math.round(elevation * 1000) / 1000\n\n\t\ttilt = Math.round(tilt * 1000) / 1000\n\t\torientationTiltOffset = (window.orientation * -1) + 90\n\t\ttilt += orientationTiltOffset\n\t\ttilt -= 360 if tilt > 180\n\t\t@_tilt = tilt\n\n\t\t@_deviceHeading = @_heading\n\t\t@_deviceElevation = @_elevation\n\t\t@_emitOrientationDidChangeEvent()\n\n\t# Panning\n\n\t_canvasToComponentRatio: =>\n\t\tpointA = Utils.convertPointFromContext({x:0, y:0}, @, true)\n\t\tpointB = Utils.convertPointFromContext({x:1, y:1}, @, true)\n\t\txDist = Math.abs(pointA.x - pointB.x)\n\t\tyDist = Math.abs(pointA.y - pointB.y)\n\t\treturn {x:xDist, y:yDist}\n\n\tsetupPan: (enabled) =>\n\n\t\t@panning = enabled\n\t\t@desktopPan(0, 0)\n\n\t\t@onMouseDown => @animateStop()\n\n\t\t@onPan (data) =>\n\t\t\treturn if not @panning\n\t\t\tratio = @_canvasToComponentRatio()\n\t\t\tdeltaX = data.deltaX * ratio.x\n\t\t\tdeltaY = data.deltaY * ratio.y\n\t\t\tstrength = Utils.modulate(@perspective, [1200, 900], [22, 17.5])\n\n\t\t\tif Utils.isMobile()\n\t\t\t\t@_headingOffset -= (deltaX / strength) if @mobilePanning\n\t\t\telse\n\t\t\t\t@desktopPan(deltaX / strength, deltaY / strength)\n\n\t\t\t@_prevVeloX = data.velocityX\n\t\t\t@_prevVeloU = data.velocityY\n\n\t\t@onPanEnd (data) =>\n\t\t\treturn if not @panning or Utils.isMobile()\n\t\t\tratio = @_canvasToComponentRatio()\n\t\t\tvelocityX = (data.velocityX + @_prevVeloX) * 0.5\n\t\t\tvelocityY = (data.velocityY + @_prevVeloY) * 0.5\n\t\t\tvelocityX *= velocityX\n\t\t\tvelocityY *= velocityY\n\t\t\tvelocityX *= ratio.x\n\t\t\tvelocityY *= ratio.y\n\t\t\tstrength = Utils.modulate(@perspective, [1200, 900], [22, 17.5])\n\n\t\t\t@animate\n\t\t\t\theading: @heading - (data.velocityX * ratio.x * 200) / strength\n\t\t\t\televation: @elevation + (data.velocityY * ratio.y * 200) / strength\n\t\t\t\toptions: curve: \"spring(300,100)\"\n\n\tdesktopPan: (deltaDir, deltaHeight) ->\n\t\thalfCubeSide = @cubeSide/2\n\t\ttranslationX = \"translateX(#{((@width / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)\"\n\t\ttranslationY = \" translateY(#{((@height / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)\"\n\t\ttranslationZ = \" translateZ(#{@perspective * Framer.CurrentContext.pixelMultiplier}px)\"\n\t\t@_heading -= deltaDir\n\n\t\tif @_heading > 360\n\t\t\t@_heading -= 360\n\t\telse if @_heading < 0\n\t\t\t@_heading += 360\n\n\t\t@_elevation += deltaHeight\n\t\t@_elevation = Utils.clamp(@_elevation, -90, 90)\n\n\t\trotation = translationZ + translationX + translationY + \" rotateX(#{@_elevation + 90}deg) rotateZ(#{360 - @_heading}deg)\" + \" rotateZ(#{-@_headingOffset}deg)\"\n\t\t@world.style[\"webkitTransform\"] = rotation\n\n\t\t@_emitOrientationDidChangeEvent()\n\n\tlookAt: (heading, elevation) ->\n\t\thalfCubeSide = @cubeSide/2\n\t\ttranslationX = \"translateX(#{((@width / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)\"\n\t\ttranslationY = \" translateY(#{((@height / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)\"\n\t\ttranslationZ = \" translateZ(#{@perspective * Framer.CurrentContext.pixelMultiplier}px)\"\n\t\trotation = translationZ + translationX + translationY + \" rotateZ(#{@_tilt}deg) rotateX(#{elevation + 90}deg) rotateZ(#{-heading}deg)\"\n\n\t\t@world?.style[\"webkitTransform\"] = rotation\n\t\t@_heading = heading\n\t\t@_elevation = elevation\n\t\t@_headingOffset = @_heading - @_deviceHeading if Utils.isMobile()\n\t\t@_elevationOffset = @_elevation - @_deviceElevation\n\n\t\theading = @_heading\n\t\tif heading < 0\n\t\t\theading += 360\n\t\telse if heading > 360\n\t\t\theading -= 360\n\n\t\t@_emitOrientationDidChangeEvent()\n\n\t_emitOrientationDidChangeEvent: =>\n\t\t@emit(Events.OrientationDidChange, {heading: @heading, elevation: @elevation, tilt: @tilt})\n\n\t# event shortcuts\n\n\tonOrientationChange:(cb) -> @on(Events.OrientationDidChange, cb)\n"
  },
  {
    "path": "cubemaps/Lycksele/credits.txt",
    "content": "Author\r\n======\r\n\r\nThis is the work of Emil Persson, aka Humus.\r\nhttp://www.humus.name\r\n\r\n\r\n\r\nLicense\r\n=======\r\n\r\nThis work is licensed under a Creative Commons Attribution 3.0 Unported License.\r\nhttp://creativecommons.org/licenses/by/3.0/\r\n"
  },
  {
    "path": "cubemaps/Park/credits.txt",
    "content": "Author\r\n======\r\n\r\nThis is the work of Emil Persson, aka Humus.\r\nhttp://www.humus.name\r\n\r\n\r\n\r\nLicense\r\n=======\r\n\r\nThis work is licensed under a Creative Commons Attribution 3.0 Unported License.\r\nhttp://creativecommons.org/licenses/by/3.0/\r\n"
  }
]