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 *\<string>*, get: layer)
- **`right`** (set: imagePath *\<string>*, get: layer)
- **`back`** (set: imagePath *\<string>*, get: layer)
- **`left`** (set: imagePath *\<string>*, get: layer)
- **`top`** (set: imagePath *\<string>*, get: layer)
- **`bottom`** (set: imagePath *\<string>*, get: layer)
- **`panning`** *\<bool>*
- **`mobilePanning`** *\<bool>*
- **`arrowKeys`** *\<bool>*
- **`lookAtLatestProjectedLayer`** *\<bool>* (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.

##### 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`** (*\<object>* {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 <string>, get: layer)
- right
- back
- left
- top
- bottom
- heading <number>
- elevation <number>
- tilt <number> readonly
- panning <bool>
- mobilePanning <bool>
- arrowKeys <bool>
- lookAtLatestProjectedLayer <bool>
methods
- projectLayer(layer) # heading and elevation are set as properties on the layer
- hideEnviroment()
events
- onOrientationChange (data {heading, elevation, tilt})
--------------------------------------------------------------------------------
VRLayer class
properties
- heading <number> (from 0 up to 360)
- elevation <number> (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/
gitextract_2y6jlz4j/
├── LICENSE
├── README.md
├── VRComponent.coffee
└── cubemaps/
├── Lycksele/
│ └── credits.txt
└── Park/
└── credits.txt
Condensed preview — 5 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (24K chars).
[
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2017 Jonas Treub\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 4177,
"preview": "# VRComponent\n\nA virtual reality component for [Framer](http://framerjs.com). The virtual enviroment is created using th"
},
{
"path": "VRComponent.coffee",
"chars": 15784,
"preview": "\"\"\"\n\nVRComponent class\n\nproperties\n- front (set: imagePath <string>, get: layer)\n- right\n- back\n- left\n- top\n- bottom\n- "
},
{
"path": "cubemaps/Lycksele/credits.txt",
"chars": 240,
"preview": "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 wo"
},
{
"path": "cubemaps/Park/credits.txt",
"chars": 240,
"preview": "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 wo"
}
]
About this extraction
This page contains the full source code of the jonastreub/VRComponent GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 5 files (21.0 KB), approximately 6.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.