Repository: jonastreub/VRComponent Branch: master Commit: ab8c3b1de577 Files: 5 Total size: 21.0 KB Directory structure: gitextract_2y6jlz4j/ ├── LICENSE ├── README.md ├── VRComponent.coffee └── cubemaps/ ├── Lycksele/ │ └── credits.txt └── Park/ └── credits.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Jonas Treub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # VRComponent A 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. You can listen for orientation updates using the `onOrientationChange` event. This event contains information about heading, elevation and tilt. Read more on the associated [blogpost](https://blog.framer.com/design-for-virtual-reality-b510b4641ca9#.jua87j76h). ## Examples - [Base setup](https://framer.cloud/pzkuO) - [Event data](https://framer.cloud/XaSrp) - [Space puzzle](https://framer.cloud/KdTnn) - [Interactive resume](http://share.framerjs.com/ojd9q3dg5xem/) by [Jonathan](http://jonathanravasz.com) On 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. ## Properties - **`front`** (set: imagePath *\*, get: layer) - **`right`** (set: imagePath *\*, get: layer) - **`back`** (set: imagePath *\*, get: layer) - **`left`** (set: imagePath *\*, get: layer) - **`top`** (set: imagePath *\*, get: layer) - **`bottom`** (set: imagePath *\*, get: layer) - **`panning`** *\* - **`mobilePanning`** *\* - **`arrowKeys`** *\* - **`lookAtLatestProjectedLayer`** *\* (handy during initial setup) -- ```coffee # Include the VRComponent {VRComponent, VRLayer} = require "VRComponent" # Create a new VRComponent, map images vr = new VRComponent front: "images/front.png" left: "images/left.png" right: "images/right.png" back: "images/back.png" top: "images/top.png" bottom: "images/bottom.png" ``` ## Mapping images To 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. - **right** - positive-x - **top** - positive-y - **front** - positive-z - **left** - negative-x - **bottom** - negative-y - **back** - negative-z Note: This [tool](https://www.360toolkit.co/convert-spherical-equirectangular-to-cubemap.html) can convert your spherical panoramas to a cubemap. ## Projecting Layers Creating 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. ![sherical projection](http://github.jonastreub.com/sphere.png) ##### VRLayers Any 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. ```coffee # Include VRComponent and VRLayer {VRComponent, VRLayer} = require "VRComponent" # Create layer layerA = new VRLayer # Set layer heading and elevation before projecting layerA.heading = 230 layerA.elevation = 10 # Project the layer vr.projectLayer(layerA) ``` `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. ## Animating VRLayers The `heading` and `elevation` values of a `VRLayer` can be animated. ```coffee # Include VRComponent and VRLayer {VRComponent, VRLayer} = require "VRComponent" # Create VRLayer layerA = new VRLayer # Project the VRLayer vr.projectLayer(layerA) # Animate the layer layerA.animate properties: heading: 30 time: 10 ``` ## Events - **`onOrientationChange`** (*\* {heading, elevation, tilt}) ```coffee vr.onOrientationChange (data) -> heading = data.heading elevation = data.elevation tilt = data.tilt ``` ## Devices The module has been tested on the following devices. Device | Performance ------ | ----------- iPhone 7 | Great iPhone 6 | Good iPhone 6 Plus | Great iPhone 5C | Poor Nexus 5 | Poor ================================================ FILE: VRComponent.coffee ================================================ """ VRComponent class properties - front (set: imagePath , get: layer) - right - back - left - top - bottom - heading - elevation - tilt readonly - panning - mobilePanning - arrowKeys - lookAtLatestProjectedLayer methods - projectLayer(layer) # heading and elevation are set as properties on the layer - hideEnviroment() events - onOrientationChange (data {heading, elevation, tilt}) -------------------------------------------------------------------------------- VRLayer class properties - heading (from 0 up to 360) - elevation (from -90 down to 90 up) """ SIDES = [ "north", "front", "east", "right", "south", "back", "west", "left", "top", "bottom", ] KEYS = { LeftArrow: 37 UpArrow: 38 RightArrow: 39 DownArrow: 40 } KEYSDOWN = { left: false up: false right: false down: false } Events.OrientationDidChange = "orientationdidchange" class VRAnchorLayer extends Layer constructor: (layer, cubeSide) -> super() @width = 2 @height = 2 @clip = false @name = "anchor" @cubeSide = cubeSide @backgroundColor = null @layer = layer layer.parent = @ layer.center() layer.on "change:orientation", (newValue, layer) => @updatePosition(layer) @updatePosition(layer) layer._context.on "layer:destroy", (layer) => @destroy() if layer is @layer updatePosition: (layer) -> halfCubeSide = @cubeSide / 2 dpr = Framer.CurrentContext.pixelMultiplier x = ((@cubeSide - @width) / 2) * dpr y = ((@cubeSide - @height) / 2) * dpr z = layer.distance * dpr @style.WebkitTransform = "translateX(#{x}px) translateY(#{y}px) rotateZ(#{layer.heading}deg) rotateX(#{90-layer.elevation}deg) translateZ(#{z}px) rotateX(180deg)" class exports.VRLayer extends Layer constructor: (options = {}) -> options = _.defaults options, heading: 0 elevation: 0 super options @define "heading", get: -> @_heading set: (value) -> if value >= 360 value = value % 360 else if value < 0 rest = Math.abs(value) % 360 value = 360 - rest roundedValue = Math.round(value * 1000) / 1000 if @_heading isnt roundedValue @_heading = roundedValue @emit("change:heading", @_heading) @emit("change:orientation", @_heading) @define "elevation", get: -> @_elevation set: (value) -> value = Utils.clamp(value, -90, 90) roundedValue = Math.round(value * 1000) / 1000 if roundedValue isnt @_elevation @_elevation = roundedValue @emit("change:elevation", roundedValue) @emit("change:orientation", roundedValue) @define "distance", get: -> @_distance set: (value) -> if value isnt @_distance @_distance = value @emit("change:distance", value) @emit("change:orientation", value) class exports.VRComponent extends Layer constructor: (options = {}) -> options = _.defaults options, cubeSide: 1500 perspective: 600 lookAtLatestProjectedLayer: false width: Screen.width height: Screen.height arrowKeys: true panning: true mobilePanning: true flat: true clip: true super options # to hide the seems where the cube surfaces come together we disable the viewport perspective and set a black background Screen.backgroundColor = "black" Screen.perspective = 0 @setupDefaultValues() @degToRad = Math.PI / 180 @backgroundColor = null @createCube(options.cubeSide) @lookAtLatestProjectedLayer = options.lookAtLatestProjectedLayer @setupKeys(options.arrowKeys) @heading = options.heading if options.heading? @elevation = options.elevation if options.elevation? @setupPan(options.panning) @mobilePanning = options.mobilePanning if Utils.isMobile() window.addEventListener "deviceorientation", (event) => @orientationData = event Framer.Loop.on("update", @deviceOrientationUpdate) # Make sure we remove the update from the loop when we destroy the context Framer.CurrentContext.on "reset", -> Framer.Loop.off("update", @deviceOrientationUpdate) @on "change:frame", -> @desktopPan(0,0) setupDefaultValues: => @_heading = 0 @_elevation = 0 @_tilt = 0 @_headingOffset = 0 @_elevationOffset = 0 @_deviceHeading = 0 @_deviceElevation = 0 setupKeys: (enabled) -> @arrowKeys = enabled document.addEventListener "keydown", (event) => switch event.which when KEYS.UpArrow KEYSDOWN.up = true event.preventDefault() when KEYS.DownArrow KEYSDOWN.down = true event.preventDefault() when KEYS.LeftArrow KEYSDOWN.left = true event.preventDefault() when KEYS.RightArrow KEYSDOWN.right = true event.preventDefault() document.addEventListener "keyup", (event) => switch event.which when KEYS.UpArrow KEYSDOWN.up = false event.preventDefault() when KEYS.DownArrow KEYSDOWN.down = false event.preventDefault() when KEYS.LeftArrow KEYSDOWN.left = false event.preventDefault() when KEYS.RightArrow KEYSDOWN.right = false event.preventDefault() window.onblur = -> KEYSDOWN.up = false KEYSDOWN.down = false KEYSDOWN.left = false KEYSDOWN.right = false @define "heading", get: -> heading = @_heading + @_headingOffset if heading > 360 heading = heading % 360 else if heading < 0 rest = Math.abs(heading) % 360 heading = 360 - rest return Math.round(heading * 1000) / 1000 set: (value) -> @lookAt(value, @_elevation) @define "elevation", get: -> Math.round(@_elevation * 1000) / 1000 set: (value) -> value = Utils.clamp(value, -90, 90) @lookAt(@_heading, value) @define "tilt", get: -> @_tilt set: (value) -> throw "Tilt is readonly" SIDES.map (face) => @define face, get: -> @layerFromFace(face) # @getImage(face) set: (value) -> @setImage(face, value) createCube: (cubeSide = @cubeSide) => @cubeSide = cubeSide @world?.destroy() @world = new Layer name: "world" superLayer: @ size: cubeSide backgroundColor: null clip: false @world.center() @sides = [] halfCubeSide = @cubeSide / 2 colors = ["#866ccc", "#28affa", "#2dd7aa", "#ffc22c", "#7ddd11", "#f95faa"] sideNames = ["front", "right", "back", "left", "top", "bottom"] for sideIndex in [0...6] rotationX = 0 rotationX = -90 if sideIndex in [0...4] rotationX = 180 if sideIndex is 4 rotationY = 0 rotationY = sideIndex * -90 if sideIndex in [0...4] side = new Layer size: cubeSide z: -halfCubeSide originZ: halfCubeSide rotationX: rotationX rotationY: rotationY parent: @world name: sideNames[sideIndex] html: sideNames[sideIndex] color: "white" backgroundColor: colors[sideIndex] style: lineHeight: "#{cubeSide}px" textAlign: "center" fontSize: "#{cubeSide / 10}px" fontWeight: "100" fontFamily: "Helvetica Neue" @sides.push(side) side._backgroundColor = side.backgroundColor for key of @sideImages when @sideImages? @setImage key, @sideImages[key] hideEnviroment: -> for side in @sides side.destroy() layerFromFace: (face) -> return unless @sides? map = north: @sides[0] front: @sides[0] east: @sides[1] right: @sides[1] south: @sides[2] back: @sides[2] west: @sides[3] left: @sides[3] top: @sides[4] bottom:@sides[5] return map[face] setImage: (face, imagePath) -> throw Error "VRComponent setImage, wrong name for face: " + face + ", valid options: front, right, back, left, top, bottom, north, east, south, west" unless face in SIDES @sideImages = {} unless @sideImages? @sideImages[face] = imagePath layer = @layerFromFace(face) if imagePath? layer?.html = "" layer?.image = imagePath else layer?.html = layer?.name layer?.backgroundColor = layer?._backgroundColor getImage: (face) -> throw Error "VRComponent getImage, wrong name for face: " + face + ", valid options: front, right, back, left, top, bottom, north, east, south, west" unless face in SIDES layer = @layerFromFace(face) return layer.image if layer? projectLayer: (insertLayer) -> heading = insertLayer.heading heading = 0 unless heading? if heading >= 360 heading = value % 360 else if heading < 0 rest = Math.abs(heading) % 360 heading = 360 - rest elevation = insertLayer.elevation elevation = 0 unless elevation? elevation = Utils.clamp(elevation, -90, 90) distance = insertLayer.distance distance = 600 unless distance? insertLayer.heading = heading insertLayer.elevation = elevation insertLayer.distance = distance anchor = new VRAnchorLayer(insertLayer, @cubeSide) anchor.superLayer = @world @lookAt(heading, elevation) if @lookAtLatestProjectedLayer # Mobile device orientation deviceOrientationUpdate: => if Utils.isDesktop() if @arrowKeys if @_lastCallHorizontal is undefined @_lastCallHorizontal = 0 @_lastCallVertical = 0 @_accelerationHorizontal = 1 @_accelerationVertical = 1 @_goingUp = false @_goingLeft = false date = new Date() x = .1 if KEYSDOWN.up or KEYSDOWN.down diff = date - @_lastCallVertical if diff < 30 if @_accelerationVertical < 30 @_accelerationVertical += 0.18 if KEYSDOWN.up if @_goingUp is false @_accelerationVertical = 1 @_goingUp = true @desktopPan(0, 1 * @_accelerationVertical * x) else if @_goingUp is true @_accelerationVertical = 1 @_goingUp = false @desktopPan(0, -1 * @_accelerationVertical * x) @_lastCallVertical = date else @_accelerationVertical = 1 if KEYSDOWN.left or KEYSDOWN.right diff = date - @_lastCallHorizontal if diff < 30 if @_accelerationHorizontal < 25 @_accelerationHorizontal += 0.18 if KEYSDOWN.left if @_goingLeft is false @_accelerationHorizontal = 1 @_goingLeft = true @desktopPan(1 * @_accelerationHorizontal * x, 0) else if @_goingLeft is true @_accelerationHorizontal = 1 @_goingLeft = false @desktopPan(-1 * @_accelerationHorizontal * x, 0) @_lastCallHorizontal = date else @_accelerationHorizontal = 1 else if @orientationData? alpha = @orientationData.alpha beta = @orientationData.beta gamma = @orientationData.gamma @directionParams(alpha, beta, gamma) if alpha isnt 0 and beta isnt 0 and gamma isnt 0 xAngle = beta yAngle = -gamma zAngle = alpha halfCubeSide = @cubeSide/2 orientation = "rotate(#{window.orientation * -1}deg) " translationX = "translateX(#{((@width / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)" translationY = " translateY(#{((@height / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)" translationZ = " translateZ(#{@perspective * Framer.CurrentContext.pixelMultiplier}px)" rotation = translationZ + translationX + translationY + orientation + " rotateY(#{yAngle}deg) rotateX(#{xAngle}deg) rotateZ(#{zAngle}deg)" + " rotateZ(#{-@_headingOffset}deg)" @world.style["webkitTransform"] = rotation directionParams: (alpha, beta, gamma) -> alphaRad = alpha * @degToRad betaRad = beta * @degToRad gammaRad = gamma * @degToRad # Calculate equation components cA = Math.cos(alphaRad) sA = Math.sin(alphaRad) cB = Math.cos(betaRad) sB = Math.sin(betaRad) cG = Math.cos(gammaRad) sG = Math.sin(gammaRad) # x unitvector xrA = -sA * sB * sG + cA * cG xrB = cA * sB * sG + sA * cG xrC = cB * sG # y unitvector yrA = -sA * cB yrB = cA * cB yrC = -sB # -z unitvector zrA = -sA * sB * cG - cA * sG zrB = cA * sB * cG - sA * sG zrC = cB * cG # Calculate heading heading = Math.atan(zrA / zrB) # Convert from half unit circle to whole unit circle if zrB < 0 heading += Math.PI else if zrA < 0 heading += 2 * Math.PI # # Calculate Altitude (in degrees) elevation = Math.PI / 2 - Math.acos(-zrC) cH = Math.sqrt(1 - (zrC * zrC)) tilt = Math.acos(-xrC / cH) * Math.sign(yrC) # Convert radians to degrees heading *= 180 / Math.PI elevation *= 180 / Math.PI tilt *= 180 / Math.PI @_heading = Math.round(heading * 1000) / 1000 @_elevation = Math.round(elevation * 1000) / 1000 tilt = Math.round(tilt * 1000) / 1000 orientationTiltOffset = (window.orientation * -1) + 90 tilt += orientationTiltOffset tilt -= 360 if tilt > 180 @_tilt = tilt @_deviceHeading = @_heading @_deviceElevation = @_elevation @_emitOrientationDidChangeEvent() # Panning _canvasToComponentRatio: => pointA = Utils.convertPointFromContext({x:0, y:0}, @, true) pointB = Utils.convertPointFromContext({x:1, y:1}, @, true) xDist = Math.abs(pointA.x - pointB.x) yDist = Math.abs(pointA.y - pointB.y) return {x:xDist, y:yDist} setupPan: (enabled) => @panning = enabled @desktopPan(0, 0) @onMouseDown => @animateStop() @onPan (data) => return if not @panning ratio = @_canvasToComponentRatio() deltaX = data.deltaX * ratio.x deltaY = data.deltaY * ratio.y strength = Utils.modulate(@perspective, [1200, 900], [22, 17.5]) if Utils.isMobile() @_headingOffset -= (deltaX / strength) if @mobilePanning else @desktopPan(deltaX / strength, deltaY / strength) @_prevVeloX = data.velocityX @_prevVeloU = data.velocityY @onPanEnd (data) => return if not @panning or Utils.isMobile() ratio = @_canvasToComponentRatio() velocityX = (data.velocityX + @_prevVeloX) * 0.5 velocityY = (data.velocityY + @_prevVeloY) * 0.5 velocityX *= velocityX velocityY *= velocityY velocityX *= ratio.x velocityY *= ratio.y strength = Utils.modulate(@perspective, [1200, 900], [22, 17.5]) @animate heading: @heading - (data.velocityX * ratio.x * 200) / strength elevation: @elevation + (data.velocityY * ratio.y * 200) / strength options: curve: "spring(300,100)" desktopPan: (deltaDir, deltaHeight) -> halfCubeSide = @cubeSide/2 translationX = "translateX(#{((@width / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)" translationY = " translateY(#{((@height / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)" translationZ = " translateZ(#{@perspective * Framer.CurrentContext.pixelMultiplier}px)" @_heading -= deltaDir if @_heading > 360 @_heading -= 360 else if @_heading < 0 @_heading += 360 @_elevation += deltaHeight @_elevation = Utils.clamp(@_elevation, -90, 90) rotation = translationZ + translationX + translationY + " rotateX(#{@_elevation + 90}deg) rotateZ(#{360 - @_heading}deg)" + " rotateZ(#{-@_headingOffset}deg)" @world.style["webkitTransform"] = rotation @_emitOrientationDidChangeEvent() lookAt: (heading, elevation) -> halfCubeSide = @cubeSide/2 translationX = "translateX(#{((@width / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)" translationY = " translateY(#{((@height / 2) - halfCubeSide) * Framer.CurrentContext.pixelMultiplier}px)" translationZ = " translateZ(#{@perspective * Framer.CurrentContext.pixelMultiplier}px)" rotation = translationZ + translationX + translationY + " rotateZ(#{@_tilt}deg) rotateX(#{elevation + 90}deg) rotateZ(#{-heading}deg)" @world?.style["webkitTransform"] = rotation @_heading = heading @_elevation = elevation @_headingOffset = @_heading - @_deviceHeading if Utils.isMobile() @_elevationOffset = @_elevation - @_deviceElevation heading = @_heading if heading < 0 heading += 360 else if heading > 360 heading -= 360 @_emitOrientationDidChangeEvent() _emitOrientationDidChangeEvent: => @emit(Events.OrientationDidChange, {heading: @heading, elevation: @elevation, tilt: @tilt}) # event shortcuts onOrientationChange:(cb) -> @on(Events.OrientationDidChange, cb) ================================================ FILE: cubemaps/Lycksele/credits.txt ================================================ Author ====== This is the work of Emil Persson, aka Humus. http://www.humus.name License ======= This work is licensed under a Creative Commons Attribution 3.0 Unported License. http://creativecommons.org/licenses/by/3.0/ ================================================ FILE: cubemaps/Park/credits.txt ================================================ Author ====== This is the work of Emil Persson, aka Humus. http://www.humus.name License ======= This work is licensed under a Creative Commons Attribution 3.0 Unported License. http://creativecommons.org/licenses/by/3.0/