├── .gitignore ├── LICENSE ├── README.md ├── assets ├── images │ ├── apainter-banner.png │ ├── brush_atlas.json │ ├── brush_atlas.png │ ├── controller-diffuse.png │ ├── controller-pressed.png │ ├── controller-specular.png │ ├── floor.jpg │ ├── logo.png │ ├── messages.png │ ├── sky.jpg │ ├── tooltip.png │ ├── ui-hover.png │ ├── ui-normal.png │ └── ui-pressed.png ├── models │ ├── controller-tip.glb │ ├── controller_vive.json │ ├── logo.mtl │ ├── logo.obj │ ├── oculus-left-controller.mtl │ ├── oculus-left-controller.obj │ ├── oculus-right-controller.mtl │ ├── oculus-right-controller.obj │ ├── teleportHitEntity.obj │ ├── ui.obj │ ├── vive-controller.glb │ ├── vive-controller.mtl │ └── vive-controller.obj └── sounds │ ├── ui_click0.ogg │ ├── ui_click1.ogg │ ├── ui_menu.ogg │ ├── ui_paint.ogg │ ├── ui_tick.ogg │ ├── ui_tick0.ogg │ └── ui_undo.ogg ├── brushes ├── line_gradient.png ├── line_grunge1.png ├── line_grunge2.png ├── line_grunge3.png ├── lines1.png ├── lines2.png ├── lines3.png ├── lines4.png ├── lines5.png ├── silky_flat.png ├── silky_textured.png ├── squared_textured.png ├── stamp_bush.png ├── stamp_column.png ├── stamp_dots.png ├── stamp_fur1.png ├── stamp_fur2.png ├── stamp_gear.png ├── stamp_grass.png ├── stamp_grunge1.png ├── stamp_grunge2.png ├── stamp_grunge3.png ├── stamp_grunge4.png ├── stamp_grunge5.png ├── stamp_leaf1.png ├── stamp_leaf2.png ├── stamp_leaf3.png ├── stamp_snow.png ├── stamp_squares.png ├── stamp_star.png ├── thumb_cubes.gif ├── thumb_flat.gif ├── thumb_line_gradient.gif ├── thumb_line_grunge1.gif ├── thumb_line_grunge2.gif ├── thumb_line_grunge3.gif ├── thumb_lines1.gif ├── thumb_lines2.gif ├── thumb_lines3.gif ├── thumb_lines4.gif ├── thumb_lines5.gif ├── thumb_rainbow.png ├── thumb_silky_flat.gif ├── thumb_silky_textured.gif ├── thumb_single_sphere.png ├── thumb_smooth.gif ├── thumb_spheres.gif ├── thumb_squared_textured.gif ├── thumb_stamp_bush.gif ├── thumb_stamp_column.gif ├── thumb_stamp_dots.gif ├── thumb_stamp_fur1.png ├── thumb_stamp_fur2.png ├── thumb_stamp_gear.gif ├── thumb_stamp_grass.png ├── thumb_stamp_grunge5.gif ├── thumb_stamp_leaf2.gif ├── thumb_stamp_leaf3.gif ├── thumb_stamp_snow.png ├── thumb_stamp_squares.gif └── thumb_stamp_star.png ├── build.js ├── css └── main.css ├── demo.apa ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── img ├── icon-128.png ├── icon-256.png ├── icon-32.png ├── icon-512.png ├── icon-64.png ├── icon-original.png ├── icon.png └── icon.psd ├── index.html ├── manifest.webmanifest ├── package-lock.json ├── package.json ├── paintings ├── dancer.apa └── dancer.json ├── sounds ├── ui_click0.ogg ├── ui_click1.ogg ├── ui_menu.ogg ├── ui_paint.ogg ├── ui_tick.ogg ├── ui_tick0.ogg └── ui_undo.ogg ├── src ├── atlas.js ├── binarymanager.js ├── brushes │ ├── cubes.js │ ├── line.js │ ├── rainbow.js │ ├── single-sphere.js │ ├── spheres.js │ └── stamp.js ├── components │ ├── brush-tip.js │ ├── brush.js │ ├── json-model.js │ ├── logo-model.js │ ├── paint-controls.js │ ├── ui-raycaster.js │ ├── ui.js │ └── vr-analytics.js ├── dragndrop.js ├── index.js ├── onloaded.js ├── sharedbuffergeometry.js ├── sharedbuffergeometrymanager.js ├── systems │ ├── brush.js │ ├── painter.js │ └── ui.js ├── ui2d.js └── utils.js ├── vendor ├── aframe-blink-controls.js ├── aframe-input-mapping-component.js ├── aframe-input-mapping-component.min.js ├── aframe-master.min.js ├── aframe-master.min.js.map ├── aframe-tooltip-component.js ├── ga.js └── saveas.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | gh-pages/ 4 | node_modules 5 | npm-debug.log* 6 | *.wav 7 | *.bat 8 | .vs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2015-2016 A-Frame authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A-Painter 2 | 3 | Paint in VR in your browser. [Read more!](https://blog.mozvr.com/a-painter/) 4 | 5 | [![A-Painter logo](https://blog.mozvr.com/content/images/2016/09/logo_a-painter_high-nobrands.jpg)](https://blog.mozvr.com/a-painter/) 6 | 7 | [VIEW GALLERY](https://github.com/aframevr/a-painter/issues/99) 8 | 9 | ## Usage 10 | 11 | - Grab a [WebXR-enabled browser](https://immersiveweb.dev/). 12 | - Head to [https://aframe.io/a-painter/](https://aframe.io/a-painter/) and start painting. See the [blog post](https://blog.mozvr.com/a-painter/) for some instructions. 13 | - Painted something beautiful? Share it on [this GitHub issue](https://github.com/aframevr/a-painter/issues/99)! 14 | 15 | ## Local Development 16 | 17 | ```bash 18 | git clone git@github.com:aframevr/a-painter && cd a-painter 19 | npm install 20 | npm start 21 | ``` 22 | 23 | Then, load [`http://localhost:8080`](http://localhost:8080) in your browser. 24 | 25 | ## URL parameters 26 | 27 | - **url** (url) Loads a painting in apa format 28 | - **urljson** (url) Loads a painting in json format 29 | - **sky** (image url) Changes the sky texture (empty to remove sky) 30 | - **floor** (image url) Changes the floor texture (empty to remove) 31 | - **bgcolor** (Hex color without the #) Background color 32 | 33 | Example: [https://aframe.io/a-painter/?sky=&floor=http://i.imgur.com/w9BylL0.jpg&bgcolor=24caff&url=https://ucarecdn.com/0b45b93b-e651-42d8-ba49-b2df907575f3/](https://aframe.io/a-painter/?sky=&floor=http://i.imgur.com/w9BylL0.jpg&bgcolor=24caff&url=https://ucarecdn.com/0b45b93b-e651-42d8-ba49-b2df907575f3/) 34 | 35 | ## Brush API 36 | 37 | ### Brush Interface 38 | 39 | To create a new brush, implement the following interface: 40 | 41 | ```javascript 42 | BrushInterface.prototype = { 43 | init: function () {}, 44 | addPoint: function (position, orientation, pointerPosition, pressure, timestamp) {}, 45 | tick: function (timeOffset, delta) {} 46 | }; 47 | ``` 48 | 49 | - **init** (): Use this for initializing variables, materials, etc. for your brush. 50 | 51 | - **addPoint** (*Mandatory*): It will be called every time the brush should add a new point to the stroke. You should return `true` if you've added something to the scene and `false` otherwise. To add some mesh to the scene, every brush has an injected `object3D` attribute that can be used to add children to the scene. 52 | - **position** (*vector3*): Controller position. 53 | - **orientation** (*quaternion*): Controller orientation. 54 | - **pointerPosition** (*vector3*): Position of the pointer where the brush should start painting. 55 | - **pressure** (*float[0..1]*): Trigger pressure. 56 | - **timestamp** (*int*): Elapsed milliseconds since the starting of A-Painter. 57 | 58 | - **tick** (*Optional*): Is called on every frame. 59 | - **timeOffset** (*int*): Elapsed milliseconds since the starting of A-Painter. 60 | - **delta** (*int*): Delta time in milliseconds since the last frame. 61 | 62 | *Development Tip*: set your brush as the default brush at the top of 63 | `src/components/brush.js` (`brush: {default: 'yourbrush'}`) while developing so 64 | you don't have to re-select it every time you reload. 65 | 66 | ### Common Data 67 | 68 | Every brush will have some common data injected with the following default values: 69 | 70 | ```javascript 71 | this.data = { 72 | points: [], 73 | size: brushSize, 74 | prevPosition: null, 75 | prevPointerPosition: null, 76 | numPoints: 0, 77 | maxPoints: 1000, 78 | color: color.clone() 79 | }; 80 | ``` 81 | 82 | - **points** (*Array of vector3*): List of control points already painted in the current stroke with this brush. (It's updated on every call to `addPoint`.) 83 | - **size** (*float*): Brush size. (It's defined when the stroke is created.) 84 | - **prevPosition** (*vector3*): The latest controller position (from the last `addPoint` call). 85 | - **prevPointerPosition** (*vector3*): The latest pointer position (from the last `addPoint` call). 86 | - **numPoints** (*int*): Length of `points` array. 87 | - **color** (*color*): Base color to be used on the brush. (It's defined when the stroke is created.) 88 | 89 | ### Registering a New Brush 90 | 91 | To register a new brush we should call `AFRAME.registerBrush`: 92 | 93 | ```javascript 94 | AFRAME.registerBrush(brushName, brushDefinition, options); 95 | ``` 96 | 97 | Register brush needs three parameters: 98 | 99 | - **brushName** (*string*): The unique brush name. 100 | - **brushDefinition** (*object*): The custom implementation of the previously defined `brushDefinition`. 101 | - **options** (*object* [Optional]): 102 | - **thumbnail** (*string*): Path to the thumbnail image file. 103 | - **spacing** (*float*): Minimum distance, in meters, from the previous point needed to call `addPoint`. 104 | - **maxPoints** (*integer*): If defined, `addPoint` won't be called after reaching that number of points. 105 | 106 | ## File Format 107 | 108 | A-Painter uses the following custom binary file format to store the drawings and its strokes. 109 | 110 | ```text 111 | string magic ('apainter') 112 | uint16 version (currently 1) 113 | uint8 num_brushes_used 114 | [num_brushed_used] x { 115 | string brush_name 116 | } 117 | uint32 num_strokes 118 | [num_strokes] x { 119 | uint8 brush_index (Based on the previous definition order) 120 | float32x3 color (rgb) 121 | float32 size 122 | uint32 num_points 123 | [num_points] x { 124 | float32x3 position (vector3) 125 | float32x4 orientation (quaternion) 126 | float32 intensity 127 | uint32 timestamp 128 | } 129 | } 130 | 131 | string = uint8 (size) + size * uint8 132 | ``` 133 | -------------------------------------------------------------------------------- /assets/images/apainter-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/apainter-banner.png -------------------------------------------------------------------------------- /assets/images/brush_atlas.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "image": "brush_atlas.png", 4 | "size": {"w":3584,"h":2944}, 5 | "scale": "1" 6 | }, 7 | "frames": { 8 | "stamp_grass.png": 9 | { 10 | "frame": {"x":0,"y":128,"w":1536,"h":512}, 11 | "rotated": false, 12 | "trimmed": false, 13 | "spriteSourceSize": {"x":0,"y":0,"w":1536,"h":512}, 14 | "sourceSize": {"w":1536,"h":512} 15 | }, 16 | "lines4.png": 17 | { 18 | "frame": {"x":0,"y":0,"w":2048,"h":128}, 19 | "rotated": false, 20 | "trimmed": false, 21 | "spriteSourceSize": {"x":0,"y":0,"w":2048,"h":128}, 22 | "sourceSize": {"w":2048,"h":128} 23 | }, 24 | "stamp_fur2.png": 25 | { 26 | "frame": {"x":0,"y":640,"w":1536,"h":512}, 27 | "rotated": false, 28 | "trimmed": false, 29 | "spriteSourceSize": {"x":0,"y":0,"w":1536,"h":512}, 30 | "sourceSize": {"w":1536,"h":512} 31 | }, 32 | "stamp_bush.png": 33 | { 34 | "frame": {"x":0,"y":1152,"w":1024,"h":512}, 35 | "rotated": false, 36 | "trimmed": false, 37 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":512}, 38 | "sourceSize": {"w":1024,"h":512} 39 | }, 40 | "stamp_gear.png": 41 | { 42 | "frame": {"x":1024,"y":1152,"w":1024,"h":512}, 43 | "rotated": false, 44 | "trimmed": false, 45 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":512}, 46 | "sourceSize": {"w":1024,"h":512} 47 | }, 48 | "stamp_fur1.png": 49 | { 50 | "frame": {"x":2048,"y":0,"w":1024,"h":512}, 51 | "rotated": false, 52 | "trimmed": false, 53 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":512}, 54 | "sourceSize": {"w":1024,"h":512} 55 | }, 56 | "lines3.png": 57 | { 58 | "frame": {"x":2048,"y":512,"w":1024,"h":256}, 59 | "rotated": false, 60 | "trimmed": false, 61 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 62 | "sourceSize": {"w":1024,"h":256} 63 | }, 64 | "line_gradient.png": 65 | { 66 | "frame": {"x":2048,"y":768,"w":1024,"h":256}, 67 | "rotated": false, 68 | "trimmed": false, 69 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 70 | "sourceSize": {"w":1024,"h":256} 71 | }, 72 | "lines5.png": 73 | { 74 | "frame": {"x":2048,"y":1024,"w":1024,"h":256}, 75 | "rotated": false, 76 | "trimmed": false, 77 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 78 | "sourceSize": {"w":1024,"h":256} 79 | }, 80 | "silky_flat.png": 81 | { 82 | "frame": {"x":2048,"y":1280,"w":1024,"h":256}, 83 | "rotated": false, 84 | "trimmed": false, 85 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 86 | "sourceSize": {"w":1024,"h":256} 87 | }, 88 | "silky_textured.png": 89 | { 90 | "frame": {"x":0,"y":1664,"w":1024,"h":256}, 91 | "rotated": false, 92 | "trimmed": false, 93 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 94 | "sourceSize": {"w":1024,"h":256} 95 | }, 96 | "squared_textured.png": 97 | { 98 | "frame": {"x":1024,"y":1664,"w":1024,"h":256}, 99 | "rotated": false, 100 | "trimmed": false, 101 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 102 | "sourceSize": {"w":1024,"h":256} 103 | }, 104 | "line_grunge2.png": 105 | { 106 | "frame": {"x":2048,"y":1664,"w":1024,"h":256}, 107 | "rotated": false, 108 | "trimmed": false, 109 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 110 | "sourceSize": {"w":1024,"h":256} 111 | }, 112 | "line_grunge3.png": 113 | { 114 | "frame": {"x":0,"y":1920,"w":1024,"h":256}, 115 | "rotated": false, 116 | "trimmed": false, 117 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 118 | "sourceSize": {"w":1024,"h":256} 119 | }, 120 | "line_grunge1.png": 121 | { 122 | "frame": {"x":1024,"y":1920,"w":1024,"h":256}, 123 | "rotated": false, 124 | "trimmed": false, 125 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 126 | "sourceSize": {"w":1024,"h":256} 127 | }, 128 | "lines2.png": 129 | { 130 | "frame": {"x":2048,"y":1920,"w":1024,"h":256}, 131 | "rotated": false, 132 | "trimmed": false, 133 | "spriteSourceSize": {"x":0,"y":0,"w":1024,"h":256}, 134 | "sourceSize": {"w":1024,"h":256} 135 | }, 136 | "stamp_leaf3.png": 137 | { 138 | "frame": {"x":1536,"y":128,"w":512,"h":512}, 139 | "rotated": false, 140 | "trimmed": false, 141 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 142 | "sourceSize": {"w":512,"h":512} 143 | }, 144 | "stamp_dots.png": 145 | { 146 | "frame": {"x":1536,"y":640,"w":512,"h":512}, 147 | "rotated": false, 148 | "trimmed": false, 149 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 150 | "sourceSize": {"w":512,"h":512} 151 | }, 152 | "thumb_stamp_star.png": 153 | { 154 | "frame": {"x":2304,"y":1536,"w":128,"h":128}, 155 | "rotated": false, 156 | "trimmed": false, 157 | "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, 158 | "sourceSize": {"w":128,"h":128} 159 | }, 160 | "stamp_grunge1.png": 161 | { 162 | "frame": {"x":512,"y":2176,"w":512,"h":512}, 163 | "rotated": false, 164 | "trimmed": false, 165 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 166 | "sourceSize": {"w":512,"h":512} 167 | }, 168 | "stamp_grunge2.png": 169 | { 170 | "frame": {"x":1024,"y":2176,"w":512,"h":512}, 171 | "rotated": false, 172 | "trimmed": false, 173 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 174 | "sourceSize": {"w":512,"h":512} 175 | }, 176 | "stamp_grunge3.png": 177 | { 178 | "frame": {"x":1536,"y":2176,"w":512,"h":512}, 179 | "rotated": false, 180 | "trimmed": false, 181 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 182 | "sourceSize": {"w":512,"h":512} 183 | }, 184 | "stamp_grunge4.png": 185 | { 186 | "frame": {"x":2048,"y":2176,"w":512,"h":512}, 187 | "rotated": false, 188 | "trimmed": false, 189 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 190 | "sourceSize": {"w":512,"h":512} 191 | }, 192 | "stamp_grunge5.png": 193 | { 194 | "frame": {"x":2560,"y":2176,"w":512,"h":512}, 195 | "rotated": false, 196 | "trimmed": false, 197 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 198 | "sourceSize": {"w":512,"h":512} 199 | }, 200 | "stamp_leaf1.png": 201 | { 202 | "frame": {"x":3072,"y":0,"w":512,"h":512}, 203 | "rotated": false, 204 | "trimmed": false, 205 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 206 | "sourceSize": {"w":512,"h":512} 207 | }, 208 | "stamp_leaf2.png": 209 | { 210 | "frame": {"x":3072,"y":512,"w":512,"h":512}, 211 | "rotated": false, 212 | "trimmed": false, 213 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 214 | "sourceSize": {"w":512,"h":512} 215 | }, 216 | "stamp_column.png": 217 | { 218 | "frame": {"x":3072,"y":1024,"w":512,"h":512}, 219 | "rotated": false, 220 | "trimmed": false, 221 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 222 | "sourceSize": {"w":512,"h":512} 223 | }, 224 | "stamp_snow.png": 225 | { 226 | "frame": {"x":3072,"y":1536,"w":512,"h":512}, 227 | "rotated": false, 228 | "trimmed": false, 229 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 230 | "sourceSize": {"w":512,"h":512} 231 | }, 232 | "stamp_squares.png": 233 | { 234 | "frame": {"x":3072,"y":2048,"w":512,"h":512}, 235 | "rotated": false, 236 | "trimmed": false, 237 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 238 | "sourceSize": {"w":512,"h":512} 239 | }, 240 | "lines1.png": 241 | { 242 | "frame": {"x":0,"y":2688,"w":256,"h":256}, 243 | "rotated": false, 244 | "trimmed": false, 245 | "spriteSourceSize": {"x":0,"y":0,"w":256,"h":256}, 246 | "sourceSize": {"w":256,"h":256} 247 | }, 248 | "thumb_rainbow.png": 249 | { 250 | "frame": {"x":3072,"y":2560,"w":128,"h":128}, 251 | "rotated": false, 252 | "trimmed": false, 253 | "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, 254 | "sourceSize": {"w":128,"h":128} 255 | }, 256 | "thumb_single_sphere.png": 257 | { 258 | "frame": {"x":3200,"y":2560,"w":128,"h":128}, 259 | "rotated": false, 260 | "trimmed": false, 261 | "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, 262 | "sourceSize": {"w":128,"h":128} 263 | }, 264 | "thumb_stamp_fur1.png": 265 | { 266 | "frame": {"x":3328,"y":2560,"w":128,"h":128}, 267 | "rotated": false, 268 | "trimmed": false, 269 | "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, 270 | "sourceSize": {"w":128,"h":128} 271 | }, 272 | "thumb_stamp_fur2.png": 273 | { 274 | "frame": {"x":3456,"y":2560,"w":128,"h":128}, 275 | "rotated": false, 276 | "trimmed": false, 277 | "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, 278 | "sourceSize": {"w":128,"h":128} 279 | }, 280 | "thumb_stamp_grass.png": 281 | { 282 | "frame": {"x":2048,"y":1536,"w":128,"h":128}, 283 | "rotated": false, 284 | "trimmed": false, 285 | "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, 286 | "sourceSize": {"w":128,"h":128} 287 | }, 288 | "thumb_stamp_snow.png": 289 | { 290 | "frame": {"x":2176,"y":1536,"w":128,"h":128}, 291 | "rotated": false, 292 | "trimmed": false, 293 | "spriteSourceSize": {"x":0,"y":0,"w":128,"h":128}, 294 | "sourceSize": {"w":128,"h":128} 295 | }, 296 | "stamp_star.png": 297 | { 298 | "frame": {"x":0,"y":2176,"w":512,"h":512}, 299 | "rotated": false, 300 | "trimmed": false, 301 | "spriteSourceSize": {"x":0,"y":0,"w":512,"h":512}, 302 | "sourceSize": {"w":512,"h":512} 303 | } 304 | } 305 | } -------------------------------------------------------------------------------- /assets/images/brush_atlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/brush_atlas.png -------------------------------------------------------------------------------- /assets/images/controller-diffuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/controller-diffuse.png -------------------------------------------------------------------------------- /assets/images/controller-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/controller-pressed.png -------------------------------------------------------------------------------- /assets/images/controller-specular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/controller-specular.png -------------------------------------------------------------------------------- /assets/images/floor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/floor.jpg -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/messages.png -------------------------------------------------------------------------------- /assets/images/sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/sky.jpg -------------------------------------------------------------------------------- /assets/images/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/tooltip.png -------------------------------------------------------------------------------- /assets/images/ui-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/ui-hover.png -------------------------------------------------------------------------------- /assets/images/ui-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/ui-normal.png -------------------------------------------------------------------------------- /assets/images/ui-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/images/ui-pressed.png -------------------------------------------------------------------------------- /assets/models/controller-tip.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/models/controller-tip.glb -------------------------------------------------------------------------------- /assets/models/logo.mtl: -------------------------------------------------------------------------------- 1 | newmtl Material.001 2 | 3 | Ns 10.0000 4 | Ni 1.5000 5 | d 0.99 6 | Tr 0.0000 7 | Tf 1.0000 1.0000 1.0000 8 | illum 2 9 | Ka 1.0 1.0 1.0 10 | Kd 1.0 1.0 1.0 11 | Ks 0.0000 0.0000 0.0000 12 | Ke 0.0000 0.0000 0.0000 13 | map_Ka ../images/logo.png 14 | map_Kd ../images/logo.png 15 | -------------------------------------------------------------------------------- /assets/models/logo.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.77 (sub 0) OBJ File: 'logo.blend' 2 | # www.blender.org 3 | mtllib logo.mtl 4 | o Grid.009 5 | v -0.471213 1.444180 -1.645922 6 | v -0.466566 1.671926 -1.645922 7 | v -0.712217 1.444180 -1.645922 8 | v -0.757909 1.671926 -1.645922 9 | vt 0.3524 0.6508 10 | vt 0.2705 0.4510 11 | vt 0.3509 0.4510 12 | vt 0.2553 0.6508 13 | vn 0.0000 -0.0000 1.0000 14 | usemtl Material.001 15 | s off 16 | f 2/1/1 3/2/1 1/3/1 17 | f 2/1/1 4/4/1 3/2/1 18 | o Grid.008 19 | v 1.497139 1.755647 -1.434515 20 | v 1.167906 1.756516 -1.467615 21 | v 1.174374 1.319379 -1.466965 22 | v 1.497139 1.320668 -1.434515 23 | vt 0.8919 0.3416 24 | vt 1.0000 0.7243 25 | vt 0.8897 0.7250 26 | vt 1.0000 0.3427 27 | vn -0.1000 0.0000 0.9950 28 | usemtl Material.001 29 | s off 30 | f 7/5/2 5/6/2 6/7/2 31 | f 7/5/2 8/8/2 5/6/2 32 | o Grid.007 33 | v 0.917805 1.790618 -1.535342 34 | v 1.171649 1.789994 -1.509821 35 | v 0.920131 1.357118 -1.535108 36 | v 1.178077 1.355527 -1.509175 37 | vt 0.8053 0.3747 38 | vt 0.8895 0.7544 39 | vt 0.8045 0.7549 40 | vt 0.8917 0.3733 41 | vn -0.1000 0.0000 0.9950 42 | usemtl Material.001 43 | s off 44 | f 11/9/3 10/10/3 9/11/3 45 | f 11/9/3 12/12/3 10/10/3 46 | o Grid.006 47 | v 0.911832 1.757965 -1.486516 48 | v 0.606281 1.758775 -1.501881 49 | v 0.914283 1.302936 -1.486392 50 | v 0.611682 1.304929 -1.501609 51 | vt 0.7044 0.3289 52 | vt 0.8046 0.7263 53 | vt 0.7026 0.7270 54 | vt 0.8054 0.3271 55 | vn -0.0502 0.0000 0.9987 56 | usemtl Material.001 57 | s off 58 | f 16/13/4 13/14/4 14/15/4 59 | f 16/13/4 15/16/4 13/14/4 60 | o Grid.005 61 | v 0.606978 1.821490 -1.579312 62 | v 0.279151 1.822160 -1.579312 63 | v 0.612668 1.343967 -1.579312 64 | v 0.297016 1.345926 -1.579311 65 | vt 0.5990 0.3648 66 | vt 0.7023 0.7820 67 | vt 0.5931 0.7826 68 | vt 0.7042 0.3631 69 | vn -0.0000 0.0000 1.0000 70 | usemtl Material.001 71 | s off 72 | f 20/17/5 17/18/5 18/19/5 73 | f 20/17/5 19/20/5 17/18/5 74 | o Grid.004 75 | v 0.279157 1.821983 -1.515850 76 | v 0.107117 1.822317 -1.515850 77 | v 0.297471 1.333814 -1.515850 78 | v 0.139652 1.334805 -1.515850 79 | vt 0.5466 0.3551 80 | vt 0.5931 0.7824 81 | vt 0.5357 0.7827 82 | vt 0.5992 0.3542 83 | vn -0.0000 0.0000 1.0000 84 | usemtl Material.001 85 | s off 86 | f 24/21/6 21/22/6 22/23/6 87 | f 24/21/6 23/24/6 21/22/6 88 | o Grid.003 89 | v 0.111772 1.752563 -1.601345 90 | v -0.097571 1.753527 -1.601345 91 | v 0.143929 1.270704 -1.601345 92 | v -0.207435 1.273131 -1.601345 93 | vt 0.5480 0.2989 94 | vt 0.4675 0.7224 95 | vt 0.4309 0.3010 96 | vt 0.5373 0.7215 97 | vn -0.0000 0.0000 1.0000 98 | usemtl Material.001 99 | s off 100 | f 27/25/7 26/26/7 28/27/7 101 | f 27/25/7 25/28/7 26/26/7 102 | o Grid.002 103 | v -0.213497 1.246625 -1.527408 104 | v -0.080655 1.827492 -1.527408 105 | v -0.451344 1.249474 -1.527408 106 | v -0.439698 1.820292 -1.527408 107 | v -0.207435 1.273131 -1.527408 108 | vt 0.4309 0.3010 109 | vt 0.3534 0.7810 110 | vt 0.3496 0.2802 111 | vt 0.4288 0.2777 112 | vt 0.4731 0.7873 113 | vn 0.0000 0.0000 1.0000 114 | usemtl Material.001 115 | s off 116 | f 33/29/8 32/30/8 31/31/8 117 | f 31/31/8 29/32/8 33/29/8 118 | f 33/29/8 30/33/8 32/30/8 119 | o Grid.001 120 | v 0.354175 0.918392 -1.679939 121 | v 1.734390 0.916231 -1.679939 122 | v 1.731629 1.163418 -1.679939 123 | v 0.351575 1.172653 -1.679939 124 | vt 0.9191 0.2769 125 | vt 0.4576 0.0619 126 | vt 0.9201 0.0600 127 | vt 0.4567 0.2850 128 | vn 0.0000 0.0000 1.0000 129 | usemtl Material.001 130 | s off 131 | f 36/34/9 34/35/9 35/36/9 132 | f 36/34/9 37/37/9 34/35/9 133 | o Grid 134 | v -0.576517 0.930000 -1.529653 135 | v -0.801831 2.070000 -1.490361 136 | v -1.477699 0.930000 -1.372497 137 | v -1.477699 2.070000 -1.372497 138 | v -0.689174 1.500000 -1.510007 139 | v -1.027108 0.930000 -1.451075 140 | v -1.139765 2.070000 -1.431429 141 | v -1.477699 1.500000 -1.372497 142 | v -1.083436 1.500000 -1.441252 143 | vt 0.1334 0.5000 144 | vt 0.0000 0.0000 145 | vt 0.1525 0.0000 146 | vt 0.2668 0.5000 147 | vt 0.3049 0.0000 148 | vt 0.2287 1.0000 149 | vt 0.1143 1.0000 150 | vt 0.0000 0.5000 151 | vt 0.0000 1.0000 152 | vn 0.1718 0.0000 0.9851 153 | usemtl Material.001 154 | s off 155 | f 46/38/10 40/39/10 43/40/10 156 | f 42/41/10 43/40/10 38/42/10 157 | f 39/43/10 46/38/10 42/41/10 158 | f 44/44/10 45/45/10 46/38/10 159 | f 46/38/10 45/45/10 40/39/10 160 | f 42/41/10 46/38/10 43/40/10 161 | f 39/43/10 44/44/10 46/38/10 162 | f 44/44/10 41/46/10 45/45/10 163 | -------------------------------------------------------------------------------- /assets/models/oculus-left-controller.mtl: -------------------------------------------------------------------------------- 1 | # 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware 2 | # File Created: 21.04.2017 21:11:12 3 | 4 | newmtl wire_085028177 5 | Ns 32 6 | d 1 7 | Tr 0 8 | Tf 1 1 1 9 | illum 2 10 | Ka 0.3333 0.1098 0.6941 11 | Kd 0.3333 0.1098 0.6941 12 | Ks 0.3500 0.3500 0.3500 13 | 14 | newmtl stick 15 | Ns 10.0000 16 | Ni 1.5000 17 | d 1.0000 18 | Tr 0.0000 19 | Tf 1.0000 1.0000 1.0000 20 | illum 2 21 | Ka 0.5880 0.5880 0.5880 22 | Kd 0.5880 0.5880 0.5880 23 | Ks 0.0000 0.0000 0.0000 24 | Ke 0.0000 0.0000 0.0000 25 | 26 | newmtl body 27 | Ns 10.0000 28 | Ni 1.5000 29 | d 1.0000 30 | Tr 0.0000 31 | Tf 1.0000 1.0000 1.0000 32 | illum 2 33 | Ka 0.5880 0.5880 0.5880 34 | Kd 0.5880 0.5880 0.5880 35 | Ks 0.0000 0.0000 0.0000 36 | Ke 0.0000 0.0000 0.0000 37 | 38 | newmtl buttonTrigger 39 | Ns 10.0000 40 | Ni 1.5000 41 | d 1.0000 42 | Tr 0.0000 43 | Tf 1.0000 1.0000 1.0000 44 | illum 2 45 | Ka 0.5880 0.5880 0.5880 46 | Kd 0.5880 0.5880 0.5880 47 | Ks 0.0000 0.0000 0.0000 48 | Ke 0.0000 0.0000 0.0000 49 | 50 | newmtl buttonHand 51 | Ns 10.0000 52 | Ni 1.5000 53 | d 1.0000 54 | Tr 0.0000 55 | Tf 1.0000 1.0000 1.0000 56 | illum 2 57 | Ka 0.5880 0.5880 0.5880 58 | Kd 0.5880 0.5880 0.5880 59 | Ks 0.0000 0.0000 0.0000 60 | Ke 0.0000 0.0000 0.0000 61 | 62 | newmtl buttonMenu 63 | Ns 10.0000 64 | Ni 1.5000 65 | d 1.0000 66 | Tr 0.0000 67 | Tf 1.0000 1.0000 1.0000 68 | illum 2 69 | Ka 0.5880 0.5880 0.5880 70 | Kd 0.5880 0.5880 0.5880 71 | Ks 0.0000 0.0000 0.0000 72 | Ke 0.0000 0.0000 0.0000 73 | 74 | newmtl buttonX 75 | Ns 10.0000 76 | Ni 1.5000 77 | d 1.0000 78 | Tr 0.0000 79 | Tf 1.0000 1.0000 1.0000 80 | illum 2 81 | Ka 0.5880 0.5880 0.5880 82 | Kd 0.5880 0.5880 0.5880 83 | Ks 0.0000 0.0000 0.0000 84 | Ke 0.0000 0.0000 0.0000 85 | 86 | newmtl buttonY 87 | Ns 10.0000 88 | Ni 1.5000 89 | d 1.0000 90 | Tr 0.0000 91 | Tf 1.0000 1.0000 1.0000 92 | illum 2 93 | Ka 0.5880 0.5880 0.5880 94 | Kd 0.5880 0.5880 0.5880 95 | Ks 0.0000 0.0000 0.0000 96 | Ke 0.0000 0.0000 0.0000 97 | 98 | newmtl mat_tip 99 | Ns 32.6421 100 | Ni 1.5000 101 | d 1.0000 102 | Tr 0.0000 103 | Tf 1.0000 1.0000 1.0000 104 | illum 2 105 | Ka 0.0000 0.0000 0.0000 106 | Kd 0.8000 0.0000 0.0026 107 | Ks 0.0000 0.0000 0.0000 108 | Ke 0.0000 0.0000 0.0000 109 | -------------------------------------------------------------------------------- /assets/models/oculus-right-controller.mtl: -------------------------------------------------------------------------------- 1 | # 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware 2 | # File Created: 21.04.2017 21:11:20 3 | 4 | newmtl buttonTrigger 5 | Ns 10.0000 6 | Ni 1.5000 7 | d 1.0000 8 | Tr 0.0000 9 | Tf 1.0000 1.0000 1.0000 10 | illum 2 11 | Ka 0.5880 0.5880 0.5880 12 | Kd 0.5880 0.5880 0.5880 13 | Ks 0.0000 0.0000 0.0000 14 | Ke 0.0000 0.0000 0.0000 15 | 16 | newmtl buttonHand 17 | Ns 10.0000 18 | Ni 1.5000 19 | d 1.0000 20 | Tr 0.0000 21 | Tf 1.0000 1.0000 1.0000 22 | illum 2 23 | Ka 0.5880 0.5880 0.5880 24 | Kd 0.5880 0.5880 0.5880 25 | Ks 0.0000 0.0000 0.0000 26 | Ke 0.0000 0.0000 0.0000 27 | 28 | newmtl stick 29 | Ns 10.0000 30 | Ni 1.5000 31 | d 1.0000 32 | Tr 0.0000 33 | Tf 1.0000 1.0000 1.0000 34 | illum 2 35 | Ka 0.5880 0.5880 0.5880 36 | Kd 0.5880 0.5880 0.5880 37 | Ks 0.0000 0.0000 0.0000 38 | Ke 0.0000 0.0000 0.0000 39 | 40 | newmtl buttonB 41 | Ns 10.0000 42 | Ni 1.5000 43 | d 1.0000 44 | Tr 0.0000 45 | Tf 1.0000 1.0000 1.0000 46 | illum 2 47 | Ka 0.5880 0.5880 0.5880 48 | Kd 0.5880 0.5880 0.5880 49 | Ks 0.0000 0.0000 0.0000 50 | Ke 0.0000 0.0000 0.0000 51 | 52 | newmtl buttonA 53 | Ns 10.0000 54 | Ni 1.5000 55 | d 1.0000 56 | Tr 0.0000 57 | Tf 1.0000 1.0000 1.0000 58 | illum 2 59 | Ka 0.5880 0.5880 0.5880 60 | Kd 0.5880 0.5880 0.5880 61 | Ks 0.0000 0.0000 0.0000 62 | Ke 0.0000 0.0000 0.0000 63 | 64 | newmtl buttonHome 65 | Ns 10.0000 66 | Ni 1.5000 67 | d 1.0000 68 | Tr 0.0000 69 | Tf 1.0000 1.0000 1.0000 70 | illum 2 71 | Ka 0.5880 0.5880 0.5880 72 | Kd 0.5880 0.5880 0.5880 73 | Ks 0.0000 0.0000 0.0000 74 | Ke 0.0000 0.0000 0.0000 75 | 76 | newmtl body 77 | Ns 10.0000 78 | Ni 1.5000 79 | d 1.0000 80 | Tr 0.0000 81 | Tf 1.0000 1.0000 1.0000 82 | illum 2 83 | Ka 0.5880 0.5880 0.5880 84 | Kd 0.5880 0.5880 0.5880 85 | Ks 0.0000 0.0000 0.0000 86 | Ke 0.0000 0.0000 0.0000 87 | 88 | newmtl wire_085028177 89 | Ns 32 90 | d 1 91 | Tr 0 92 | Tf 1 1 1 93 | illum 2 94 | Ka 0.3333 0.1098 0.6941 95 | Kd 0.3333 0.1098 0.6941 96 | Ks 0.3500 0.3500 0.3500 97 | 98 | newmtl mat_tip 99 | Ns 32.6421 100 | Ni 1.5000 101 | d 1.0000 102 | Tr 0.0000 103 | Tf 1.0000 1.0000 1.0000 104 | illum 2 105 | Ka 0.0000 0.0000 0.0000 106 | Kd 0.8000 0.0000 0.0026 107 | Ks 0.0000 0.0000 0.0000 108 | Ke 0.0000 0.0000 0.0000 109 | -------------------------------------------------------------------------------- /assets/models/teleportHitEntity.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.77 (sub 0) OBJ File: '' 2 | # www.blender.org 3 | o Circle 4 | v 0.088694 0.009423 -0.088695 5 | v 0.083151 0.027662 -0.124444 6 | v 0.069687 0.009423 -0.104294 7 | v -0.029198 0.027662 0.146791 8 | v -0.000000 0.009423 0.125433 9 | v -0.024471 0.009423 0.123023 10 | v -0.048001 0.009423 -0.115885 11 | v -0.083151 0.027662 -0.124444 12 | v -0.069687 0.009423 -0.104294 13 | v 0.057275 0.027662 -0.138275 14 | v 0.048001 0.009423 -0.115885 15 | v 0.000000 0.027662 0.149667 16 | v 0.024471 0.009423 0.123023 17 | v -0.088695 0.009423 -0.088695 18 | v 0.024471 0.009423 -0.123023 19 | v 0.029199 0.027662 0.146791 20 | v 0.048001 0.009423 0.115885 21 | v -0.105831 0.027662 -0.105831 22 | v -0.104294 0.009423 -0.069687 23 | v 0.029198 0.027662 -0.146791 24 | v -0.000000 0.009423 -0.125433 25 | v 0.083151 0.027662 0.124444 26 | v 0.069687 0.009423 0.104294 27 | v -0.138274 0.027662 -0.057275 28 | v -0.115885 0.009423 -0.048001 29 | v 0.105831 0.027662 0.105830 30 | v 0.088695 0.009423 0.088694 31 | v -0.123023 0.009423 -0.024471 32 | v 0.104294 0.009423 0.069687 33 | v -0.146791 0.027662 -0.029198 34 | v -0.125433 0.009423 -0.000000 35 | v 0.138274 0.027662 0.057275 36 | v 0.115885 0.009423 0.048001 37 | v -0.149667 0.027662 0.000000 38 | v -0.123023 0.009423 0.024471 39 | v 0.123023 0.009423 0.024471 40 | v -0.146791 0.027662 0.029198 41 | v -0.115885 0.009423 0.048001 42 | v 0.146791 0.027662 0.029198 43 | v 0.125433 0.009423 -0.000000 44 | v -0.138274 0.027662 0.057275 45 | v -0.104294 0.009423 0.069687 46 | v 0.146791 0.027662 -0.029199 47 | v 0.123023 0.009423 -0.024471 48 | v -0.124444 0.027662 0.083151 49 | v -0.088695 0.009423 0.088695 50 | v 0.115885 0.009423 -0.048001 51 | v -0.105831 0.027662 0.105831 52 | v -0.069687 0.009423 0.104294 53 | v 0.138274 0.027662 -0.057275 54 | v 0.104294 0.009423 -0.069687 55 | v -0.057275 0.027662 0.138274 56 | v -0.048001 0.009423 0.115885 57 | v -0.029198 0.027662 -0.146791 58 | v -0.024471 0.009423 -0.123023 59 | v 0.124444 0.027662 -0.083151 60 | v -0.057275 0.027662 -0.138274 61 | v 0.138581 0.009559 -0.138581 62 | v 0.103588 0.009559 -0.155031 63 | v 0.131842 0.009559 -0.131843 64 | v -0.038234 0.009559 0.192217 65 | v 0.000000 0.009559 0.186454 66 | v -0.036375 0.009559 0.182871 67 | v -0.074999 0.009559 -0.181064 68 | v -0.103588 0.009559 -0.155030 69 | v -0.071353 0.009559 -0.172261 70 | v 0.074999 0.009559 -0.181064 71 | v 0.071352 0.009559 -0.172261 72 | v 0.000000 0.009559 0.195982 73 | v 0.036375 0.009559 0.182871 74 | v -0.138581 0.009559 -0.138581 75 | v -0.131842 0.009559 -0.131843 76 | v 0.036375 0.009559 -0.182871 77 | v 0.038235 0.009559 0.192217 78 | v 0.071353 0.009559 0.172261 79 | v -0.155030 0.009559 -0.103588 80 | v 0.038234 0.009559 -0.192217 81 | v 0.000000 0.009559 -0.186454 82 | v 0.074999 0.009559 0.181064 83 | v 0.103588 0.009559 0.155030 84 | v -0.162953 0.009559 -0.108882 85 | v -0.172261 0.009559 -0.071353 86 | v 0.108882 0.009559 0.162953 87 | v 0.131842 0.009559 0.131842 88 | v -0.181064 0.009559 -0.074999 89 | v -0.182871 0.009559 -0.036375 90 | v 0.162953 0.009559 0.108882 91 | v 0.155030 0.009559 0.103588 92 | v -0.192217 0.009559 -0.038234 93 | v -0.186454 0.009559 0.000000 94 | v 0.181064 0.009559 0.074999 95 | v 0.172261 0.009559 0.071353 96 | v -0.195982 0.009559 0.000000 97 | v -0.182871 0.009559 0.036375 98 | v 0.182871 0.009559 0.036375 99 | v -0.192217 0.009559 0.038234 100 | v -0.172261 0.009559 0.071353 101 | v 0.192217 0.009559 0.038234 102 | v 0.186454 0.009559 0.000000 103 | v -0.162953 0.009559 0.108882 104 | v -0.155030 0.009559 0.103588 105 | v 0.192217 0.009559 -0.038235 106 | v 0.182871 0.009559 -0.036375 107 | v -0.138581 0.009559 0.138581 108 | v -0.131842 0.009559 0.131842 109 | v 0.181064 0.009559 -0.074999 110 | v 0.172261 0.009559 -0.071353 111 | v -0.108882 0.009559 0.162953 112 | v -0.103588 0.009559 0.155030 113 | v 0.162953 0.009559 -0.108882 114 | v 0.155030 0.009559 -0.103588 115 | v -0.074999 0.009559 0.181064 116 | v -0.071353 0.009559 0.172261 117 | v -0.038234 0.009559 -0.192217 118 | v -0.036375 0.009559 -0.182871 119 | v 0.105830 0.027662 -0.105831 120 | v 0.057275 0.027662 0.138274 121 | v -0.124444 0.027662 -0.083151 122 | v 0.000000 0.027662 -0.149667 123 | v 0.124444 0.027662 0.083151 124 | v 0.149667 0.027662 0.000000 125 | v -0.083151 0.027662 0.124444 126 | v 0.108882 0.009559 -0.162953 127 | v -0.108882 0.009559 -0.162953 128 | v 0.000000 0.009559 -0.195982 129 | v 0.138581 0.009559 0.138581 130 | v -0.181064 0.009559 0.074999 131 | v 0.195982 0.009559 0.000000 132 | s off 133 | f 1 2 3 134 | f 4 5 6 135 | f 7 8 9 136 | f 3 10 11 137 | f 12 13 5 138 | f 8 14 9 139 | f 10 15 11 140 | f 16 17 13 141 | f 18 19 14 142 | f 20 21 15 143 | f 17 22 23 144 | f 19 24 25 145 | f 23 26 27 146 | f 24 28 25 147 | f 26 29 27 148 | f 30 31 28 149 | f 29 32 33 150 | f 34 35 31 151 | f 32 36 33 152 | f 37 38 35 153 | f 39 40 36 154 | f 41 42 38 155 | f 40 43 44 156 | f 45 46 42 157 | f 43 47 44 158 | f 48 49 46 159 | f 50 51 47 160 | f 49 52 53 161 | f 21 54 55 162 | f 56 1 51 163 | f 52 6 53 164 | f 55 57 7 165 | f 58 59 60 166 | f 61 62 63 167 | f 64 65 66 168 | f 59 67 68 169 | f 69 70 62 170 | f 65 71 72 171 | f 67 73 68 172 | f 74 75 70 173 | f 71 76 72 174 | f 77 78 73 175 | f 79 80 75 176 | f 81 82 76 177 | f 83 84 80 178 | f 85 86 82 179 | f 84 87 88 180 | f 89 90 86 181 | f 88 91 92 182 | f 93 94 90 183 | f 91 95 92 184 | f 96 97 94 185 | f 98 99 95 186 | f 97 100 101 187 | f 99 102 103 188 | f 101 104 105 189 | f 103 106 107 190 | f 105 108 109 191 | f 107 110 111 192 | f 109 112 113 193 | f 78 114 115 194 | f 110 60 111 195 | f 112 63 113 196 | f 115 64 66 197 | f 1 116 2 198 | f 4 12 5 199 | f 7 57 8 200 | f 3 2 10 201 | f 12 16 13 202 | f 8 18 14 203 | f 10 20 15 204 | f 16 117 17 205 | f 18 118 19 206 | f 20 119 21 207 | f 17 117 22 208 | f 19 118 24 209 | f 23 22 26 210 | f 24 30 28 211 | f 26 120 29 212 | f 30 34 31 213 | f 29 120 32 214 | f 34 37 35 215 | f 32 39 36 216 | f 37 41 38 217 | f 39 121 40 218 | f 41 45 42 219 | f 40 121 43 220 | f 45 48 46 221 | f 43 50 47 222 | f 48 122 49 223 | f 50 56 51 224 | f 49 122 52 225 | f 21 119 54 226 | f 56 116 1 227 | f 52 4 6 228 | f 55 54 57 229 | f 58 123 59 230 | f 61 69 62 231 | f 64 124 65 232 | f 59 123 67 233 | f 69 74 70 234 | f 65 124 71 235 | f 67 77 73 236 | f 74 79 75 237 | f 71 81 76 238 | f 77 125 78 239 | f 79 83 80 240 | f 81 85 82 241 | f 83 126 84 242 | f 85 89 86 243 | f 84 126 87 244 | f 89 93 90 245 | f 88 87 91 246 | f 93 96 94 247 | f 91 98 95 248 | f 96 127 97 249 | f 98 128 99 250 | f 97 127 100 251 | f 99 128 102 252 | f 101 100 104 253 | f 103 102 106 254 | f 105 104 108 255 | f 107 106 110 256 | f 109 108 112 257 | f 78 125 114 258 | f 110 58 60 259 | f 112 61 63 260 | f 115 114 64 261 | -------------------------------------------------------------------------------- /assets/models/vive-controller.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/models/vive-controller.glb -------------------------------------------------------------------------------- /assets/models/vive-controller.mtl: -------------------------------------------------------------------------------- 1 | # 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware 2 | # File Created: 21.04.2017 19:32:09 3 | 4 | newmtl mat_sizehint 5 | Ns 32.6421 6 | Ni 1.5000 7 | d 1.0000 8 | Tr 0.0000 9 | Tf 1.0000 1.0000 1.0000 10 | illum 2 11 | Ka 0.0000 0.0000 0.0000 12 | Kd 0.8000 0.0013 0.0000 13 | Ks 0.2500 0.2500 0.2500 14 | Ke 0.0000 0.0000 0.0000 15 | 16 | newmtl mat_body 17 | Ns 32.6421 18 | Ni 1.5000 19 | d 1.0000 20 | Tr 0.0000 21 | Tf 1.0000 1.0000 1.0000 22 | illum 2 23 | Ka 0.0000 0.0000 0.0000 24 | Kd 0.3757 0.3757 0.3757 25 | Ks 0.0885 0.0885 0.0885 26 | Ke 0.0000 0.0000 0.0000 27 | 28 | newmtl mat_tip 29 | Ns 32.6421 30 | Ni 1.5000 31 | d 1.0000 32 | Tr 0.0000 33 | Tf 1.0000 1.0000 1.0000 34 | illum 2 35 | Ka 0.0000 0.0000 0.0000 36 | Kd 0.8000 0.0000 0.0026 37 | Ks 0.0000 0.0000 0.0000 38 | Ke 0.0000 0.0000 0.0000 39 | -------------------------------------------------------------------------------- /assets/sounds/ui_click0.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/sounds/ui_click0.ogg -------------------------------------------------------------------------------- /assets/sounds/ui_click1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/sounds/ui_click1.ogg -------------------------------------------------------------------------------- /assets/sounds/ui_menu.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/sounds/ui_menu.ogg -------------------------------------------------------------------------------- /assets/sounds/ui_paint.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/sounds/ui_paint.ogg -------------------------------------------------------------------------------- /assets/sounds/ui_tick.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/sounds/ui_tick.ogg -------------------------------------------------------------------------------- /assets/sounds/ui_tick0.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/sounds/ui_tick0.ogg -------------------------------------------------------------------------------- /assets/sounds/ui_undo.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/assets/sounds/ui_undo.ogg -------------------------------------------------------------------------------- /brushes/line_gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/line_gradient.png -------------------------------------------------------------------------------- /brushes/line_grunge1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/line_grunge1.png -------------------------------------------------------------------------------- /brushes/line_grunge2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/line_grunge2.png -------------------------------------------------------------------------------- /brushes/line_grunge3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/line_grunge3.png -------------------------------------------------------------------------------- /brushes/lines1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/lines1.png -------------------------------------------------------------------------------- /brushes/lines2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/lines2.png -------------------------------------------------------------------------------- /brushes/lines3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/lines3.png -------------------------------------------------------------------------------- /brushes/lines4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/lines4.png -------------------------------------------------------------------------------- /brushes/lines5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/lines5.png -------------------------------------------------------------------------------- /brushes/silky_flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/silky_flat.png -------------------------------------------------------------------------------- /brushes/silky_textured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/silky_textured.png -------------------------------------------------------------------------------- /brushes/squared_textured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/squared_textured.png -------------------------------------------------------------------------------- /brushes/stamp_bush.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_bush.png -------------------------------------------------------------------------------- /brushes/stamp_column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_column.png -------------------------------------------------------------------------------- /brushes/stamp_dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_dots.png -------------------------------------------------------------------------------- /brushes/stamp_fur1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_fur1.png -------------------------------------------------------------------------------- /brushes/stamp_fur2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_fur2.png -------------------------------------------------------------------------------- /brushes/stamp_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_gear.png -------------------------------------------------------------------------------- /brushes/stamp_grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_grass.png -------------------------------------------------------------------------------- /brushes/stamp_grunge1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_grunge1.png -------------------------------------------------------------------------------- /brushes/stamp_grunge2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_grunge2.png -------------------------------------------------------------------------------- /brushes/stamp_grunge3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_grunge3.png -------------------------------------------------------------------------------- /brushes/stamp_grunge4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_grunge4.png -------------------------------------------------------------------------------- /brushes/stamp_grunge5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_grunge5.png -------------------------------------------------------------------------------- /brushes/stamp_leaf1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_leaf1.png -------------------------------------------------------------------------------- /brushes/stamp_leaf2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_leaf2.png -------------------------------------------------------------------------------- /brushes/stamp_leaf3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_leaf3.png -------------------------------------------------------------------------------- /brushes/stamp_snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_snow.png -------------------------------------------------------------------------------- /brushes/stamp_squares.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_squares.png -------------------------------------------------------------------------------- /brushes/stamp_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/stamp_star.png -------------------------------------------------------------------------------- /brushes/thumb_cubes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_cubes.gif -------------------------------------------------------------------------------- /brushes/thumb_flat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_flat.gif -------------------------------------------------------------------------------- /brushes/thumb_line_gradient.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_line_gradient.gif -------------------------------------------------------------------------------- /brushes/thumb_line_grunge1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_line_grunge1.gif -------------------------------------------------------------------------------- /brushes/thumb_line_grunge2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_line_grunge2.gif -------------------------------------------------------------------------------- /brushes/thumb_line_grunge3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_line_grunge3.gif -------------------------------------------------------------------------------- /brushes/thumb_lines1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_lines1.gif -------------------------------------------------------------------------------- /brushes/thumb_lines2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_lines2.gif -------------------------------------------------------------------------------- /brushes/thumb_lines3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_lines3.gif -------------------------------------------------------------------------------- /brushes/thumb_lines4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_lines4.gif -------------------------------------------------------------------------------- /brushes/thumb_lines5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_lines5.gif -------------------------------------------------------------------------------- /brushes/thumb_rainbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_rainbow.png -------------------------------------------------------------------------------- /brushes/thumb_silky_flat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_silky_flat.gif -------------------------------------------------------------------------------- /brushes/thumb_silky_textured.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_silky_textured.gif -------------------------------------------------------------------------------- /brushes/thumb_single_sphere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_single_sphere.png -------------------------------------------------------------------------------- /brushes/thumb_smooth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_smooth.gif -------------------------------------------------------------------------------- /brushes/thumb_spheres.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_spheres.gif -------------------------------------------------------------------------------- /brushes/thumb_squared_textured.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_squared_textured.gif -------------------------------------------------------------------------------- /brushes/thumb_stamp_bush.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_bush.gif -------------------------------------------------------------------------------- /brushes/thumb_stamp_column.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_column.gif -------------------------------------------------------------------------------- /brushes/thumb_stamp_dots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_dots.gif -------------------------------------------------------------------------------- /brushes/thumb_stamp_fur1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_fur1.png -------------------------------------------------------------------------------- /brushes/thumb_stamp_fur2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_fur2.png -------------------------------------------------------------------------------- /brushes/thumb_stamp_gear.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_gear.gif -------------------------------------------------------------------------------- /brushes/thumb_stamp_grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_grass.png -------------------------------------------------------------------------------- /brushes/thumb_stamp_grunge5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_grunge5.gif -------------------------------------------------------------------------------- /brushes/thumb_stamp_leaf2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_leaf2.gif -------------------------------------------------------------------------------- /brushes/thumb_stamp_leaf3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_leaf3.gif -------------------------------------------------------------------------------- /brushes/thumb_stamp_snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_snow.png -------------------------------------------------------------------------------- /brushes/thumb_stamp_squares.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_squares.gif -------------------------------------------------------------------------------- /brushes/thumb_stamp_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/brushes/thumb_stamp_star.png -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color:#030404; 3 | } 4 | 5 | #apainter-ui{ 6 | font-family: Helvetica, Arial, sans-serif; 7 | position: absolute; 8 | left: 20px; 9 | bottom: 20px; 10 | line-height: 1.2em; 11 | background-color: rgba(10,10,10,0.8); 12 | border-radius: 6px; 13 | padding: 1.7em 2em; 14 | display: none; 15 | } 16 | 17 | #apainter-ui .help{ 18 | width: 600px; 19 | color: #ccc; 20 | font-size: 14px; 21 | text-shadow: 0px 2px 1px #000; 22 | } 23 | 24 | #apainter-ui .help h1{ 25 | font-size: 22px; 26 | font-weight: 100; 27 | color: #e42b5a; 28 | margin-top: 0; 29 | } 30 | 31 | #apainter-ui .button { 32 | background-color: #ef2d5e; 33 | color: #fff; 34 | cursor: pointer; 35 | font-size: 12px; 36 | width: 130px; 37 | text-align: center; 38 | max-width: 115px; 39 | padding: 6px; 40 | } 41 | 42 | #apainter-ui .button:hover { 43 | background-color: #f43b6a; 44 | } 45 | 46 | #apainter-ui .form { 47 | display: flex; 48 | align-content: center; 49 | } 50 | 51 | #apainter-ui .share input { 52 | width: 600px; 53 | color: #333; 54 | text-align: center; 55 | } 56 | 57 | #apainter-ui .hide { 58 | display: none !important; 59 | } 60 | 61 | #apainter-ui .progress{ 62 | font-size: 22px; 63 | font-weight: 100; 64 | color: #e42b5a; 65 | border-bottom: 1px solid #e42b5a; 66 | text-shadow: 0px 2px 1px #000; 67 | } 68 | 69 | #apainter-ui .bar{ 70 | width: 0%; 71 | height: 3px; 72 | margin-top: 13px; 73 | background-color: #e42b5a; 74 | } 75 | 76 | #apainter-logo { 77 | position: fixed; 78 | bottom: 15px; 79 | left: 15px; 80 | height: 54px; 81 | width: 226px; /* 252px; for logo with author */ 82 | background-image: url(../assets/images/apainter-banner.png); 83 | } 84 | 85 | #apainter-logo.hidden, #apainter-author.hidden{ 86 | display: none; 87 | } 88 | 89 | #apainter-author { 90 | position: fixed; 91 | bottom: 28px; 92 | left: 268px; 93 | display: inline-block; 94 | font: 18px arial, sans-serif; 95 | color: #e42b5a; 96 | } 97 | -------------------------------------------------------------------------------- /demo.apa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/demo.apa -------------------------------------------------------------------------------- /favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/favicon-16x16.png -------------------------------------------------------------------------------- /favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/favicon-32x32.png -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/favicon.ico -------------------------------------------------------------------------------- /img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/img/icon-128.png -------------------------------------------------------------------------------- /img/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/img/icon-256.png -------------------------------------------------------------------------------- /img/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/img/icon-32.png -------------------------------------------------------------------------------- /img/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/img/icon-512.png -------------------------------------------------------------------------------- /img/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/img/icon-64.png -------------------------------------------------------------------------------- /img/icon-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/img/icon-original.png -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/img/icon.png -------------------------------------------------------------------------------- /img/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/img/icon.psd -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Painter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | 42 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 61 | 62 | 65 | 66 | 69 | 70 | 73 | 74 | 77 | 78 | 79 | 80 | 82 | 83 | 85 | 86 | 88 | 89 | 91 | 92 | 94 | 95 | 96 | 97 | 100 | 101 | 104 | 105 | 108 | 109 | 112 | 113 | 114 | 115 | 118 | 119 | 122 | 123 | 126 | 127 | 130 | 131 | 132 | 133 | 138 | 139 | 142 | 143 | 146 | 147 | 150 | 151 | 154 | 155 | 156 | 157 | 159 | 160 | 162 | 163 | 165 | 166 | 168 | 169 | 171 | 172 | 173 | 174 | 177 | 178 | 181 | 182 | 185 | 186 | 189 | 190 | 191 | 192 | 195 | 196 | 199 | 200 | 203 | 204 | 207 | 208 | 209 | 210 | 211 | 212 | 213 |
214 |
215 | Saving painting... 216 |
217 |
218 |
219 |
220 |

Painting saved!

221 |

Your painting was saved in the following URL.
You can use it to continue your work later or to share your masterpiece in social media.

222 |
223 |
224 | 225 |
COPY URL
226 |
227 |
228 |
229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "en", 3 | "dir": "ltr", 4 | "name": "A-Painter", 5 | "short_name": "A-Painter", 6 | "description": "Paint in VR in your browser. Powered by A-Frame.", 7 | "start_url": "./?src=manifest", 8 | "scope": "./", 9 | "display": "fullscreen", 10 | "theme_color": "#0a0a0a", 11 | "background_color": "#030404", 12 | "icons": [ 13 | { 14 | "src": "img/icon-512.png", 15 | "sizes": "512x512", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "img/icon-256.png", 20 | "sizes": "256x256", 21 | "type": "image/png" 22 | }, 23 | { 24 | "src": "img/icon-128.png", 25 | "sizes": "128x128", 26 | "type": "image/png" 27 | }, 28 | { 29 | "src": "img/icon-64.png", 30 | "sizes": "64x64", 31 | "type": "image/png" 32 | }, 33 | { 34 | "src": "img/icon-32.png", 35 | "sizes": "32x32", 36 | "type": "image/png" 37 | }, 38 | { 39 | "src": "favicon.ico", 40 | "sizes": "16x16 32x32 48x48 64x64 256x256 512x512", 41 | "type": "image/x-icon" 42 | } 43 | ], 44 | "about_url": "https://blog.mozvr.com/a-painter/", 45 | "vr_display_required": false, 46 | "vr_default_display": "HTC Vive", 47 | "vr_available_displays": [ 48 | "HTC Vive", 49 | "Oculus Rift", 50 | "Google Daydream", 51 | "Samsung Gear VR", 52 | "Google Cardboard" 53 | ], 54 | "vr_gamepad_required": true, 55 | "vr_default_gamepad": "HTC Vive", 56 | "vr_supported_gamepads": [ 57 | "HTC Vive", 58 | "Oculus Touch" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a-painter", 3 | "version": "0.1.1", 4 | "description": "A-Painter", 5 | "scripts": { 6 | "atlas": "spritesheet-js --name brush_atlas --path assets/images brushes/*.png", 7 | "build": "webpack --mode=production", 8 | "build-dev": "webpack --mode=development", 9 | "start": "webpack-dev-server --host 0.0.0.0 --mode=development", 10 | "lint": "semistandard -v | snazzy" 11 | }, 12 | "repository": "aframevr/a-painter", 13 | "license": "MIT", 14 | "semistandard": { 15 | "ignore": [ 16 | "**/vendor/**", 17 | "build.js" 18 | ] 19 | }, 20 | "devDependencies": { 21 | "semistandard": "^16.0.1", 22 | "snazzy": "^9.0.0", 23 | "spritesheet-js": "^1.2.6", 24 | "terser-webpack-plugin": "^5.3.1", 25 | "webpack": "^5.69.1", 26 | "webpack-cli": "^4.9.2", 27 | "webpack-dev-server": "^4.7.4" 28 | }, 29 | "dependencies": { 30 | "aframe-tooltip-component": "^0.1.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /paintings/dancer.apa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/paintings/dancer.apa -------------------------------------------------------------------------------- /sounds/ui_click0.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/sounds/ui_click0.ogg -------------------------------------------------------------------------------- /sounds/ui_click1.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/sounds/ui_click1.ogg -------------------------------------------------------------------------------- /sounds/ui_menu.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/sounds/ui_menu.ogg -------------------------------------------------------------------------------- /sounds/ui_paint.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/sounds/ui_paint.ogg -------------------------------------------------------------------------------- /sounds/ui_tick.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/sounds/ui_tick.ogg -------------------------------------------------------------------------------- /sounds/ui_tick0.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/sounds/ui_tick0.ogg -------------------------------------------------------------------------------- /sounds/ui_undo.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aframevr/a-painter/0deba1f3314ee4cd053bf2a2ae2854994014dd65/sounds/ui_undo.ogg -------------------------------------------------------------------------------- /src/atlas.js: -------------------------------------------------------------------------------- 1 | /* global THREE */ 2 | const AtlasJSON = require('../assets/images/brush_atlas.json'); 3 | 4 | function Atlas () { 5 | this.map = new THREE.TextureLoader().load('assets/images/' + AtlasJSON.meta.image); 6 | } 7 | 8 | Atlas.prototype = { 9 | getUVConverters: function (filename) { 10 | if (filename) { 11 | filename = filename.replace('brushes/', ''); 12 | return { 13 | convertU: function (u) { 14 | var totalSize = AtlasJSON.meta.size; 15 | var data = AtlasJSON.frames[filename]; 16 | if (u > 1 || u < 0) { 17 | u = 0; 18 | } 19 | return data.frame.x / totalSize.w + u * data.frame.w / totalSize.w; 20 | }, 21 | 22 | convertV: function (v) { 23 | var totalSize = AtlasJSON.meta.size; 24 | var data = AtlasJSON.frames[filename]; 25 | if (v > 1 || v < 0) { 26 | v = 0; 27 | } 28 | 29 | return 1 - (data.frame.y / totalSize.h + v * data.frame.h / totalSize.h); 30 | } 31 | }; 32 | } else { 33 | return { 34 | convertU: function (u) { return u; }, 35 | convertV: function (v) { return v; } 36 | }; 37 | } 38 | } 39 | }; 40 | 41 | window.atlas = new Atlas(); 42 | -------------------------------------------------------------------------------- /src/binarymanager.js: -------------------------------------------------------------------------------- 1 | /* globals THREE */ 2 | window.BinaryManager = function (buffer) { 3 | this.dataview = new DataView(buffer); 4 | this.offset = 0; 5 | this.isLittleEndian = true; 6 | }; 7 | 8 | window.BinaryManager.prototype = { 9 | // READER 10 | readQuaternion: function () { 11 | return new THREE.Quaternion( 12 | this.readFloat(), 13 | this.readFloat(), 14 | this.readFloat(), 15 | this.readFloat() 16 | ); 17 | }, 18 | readVector3: function () { 19 | return new THREE.Vector3( 20 | this.readFloat(), 21 | this.readFloat(), 22 | this.readFloat() 23 | ); 24 | }, 25 | readString: function () { 26 | var length = this.dataview.getUint8(this.offset++, true); 27 | var output = ''; 28 | for (var i = 0; i < length; i++) { 29 | output += String.fromCharCode(this.dataview.getUint8(this.offset++, true)); 30 | } 31 | return output; 32 | }, 33 | readColor: function () { 34 | return new THREE.Color( 35 | this.readFloat(), 36 | this.readFloat(), 37 | this.readFloat() 38 | ); 39 | }, 40 | readFloat: function () { 41 | var output = this.dataview.getFloat32(this.offset, true); 42 | this.offset += 4; 43 | return output; 44 | }, 45 | readUint32: function () { 46 | var output = this.dataview.getUint32(this.offset, true); 47 | this.offset += 4; 48 | return output; 49 | }, 50 | readUint16: function () { 51 | var output = this.dataview.getUint16(this.offset, true); 52 | this.offset += 2; 53 | return output; 54 | }, 55 | readUint8: function () { 56 | var output = this.dataview.getUint8(this.offset, true); 57 | this.offset++; 58 | return output; 59 | }, 60 | // WRITER 61 | writeVector: function (value) { 62 | this.writeFloat32Array(value.toArray()); 63 | }, 64 | writeColor: function (value) { 65 | this.writeFloat32Array(value.toArray()); 66 | }, 67 | writeString: function (value) { 68 | this.writeUint8(value.length); 69 | for (var i = 0; i < value.length; i++) { 70 | this.writeUint8(value.charCodeAt(i)); 71 | } 72 | }, 73 | writeUint8: function (value) { 74 | this.dataview.setUint8(this.offset, value, this.isLittleEndian); 75 | this.offset ++; 76 | }, 77 | writeUint16: function (value) { 78 | this.dataview.setUint16(this.offset, value, this.isLittleEndian); 79 | this.offset += 2; 80 | }, 81 | writeUint32: function (value) { 82 | this.dataview.setUint32(this.offset, value, this.isLittleEndian); 83 | this.offset += 4; 84 | }, 85 | writeFloat32: function (value) { 86 | this.dataview.setFloat32(this.offset, value, this.isLittleEndian); 87 | this.offset += 4; 88 | }, 89 | writeFloat32Array: function (value) { 90 | for (var i = 0; i < value.length; i++) { 91 | this.writeFloat32(value[i]); 92 | } 93 | }, 94 | getDataView: function () { 95 | return this.dataview; 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /src/brushes/cubes.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | AFRAME.registerBrush('cubes', 3 | { 4 | init: function (color, width) { 5 | this.material = new THREE.MeshStandardMaterial({ 6 | color: this.data.color, 7 | roughness: 0.5, 8 | metalness: 0.5, 9 | side: THREE.DoubleSide, 10 | flatShading: true 11 | }); 12 | this.geometry = new THREE.BoxGeometry(1, 1, 1); 13 | this.drawingEl = document.querySelector('.a-drawing'); 14 | this.drawingEl.object3D.add(this.object3D); 15 | }, 16 | addPoint: function (position, orientation, pointerPosition, pressure, timestamp) { 17 | var box = new THREE.Mesh(this.geometry, this.material); 18 | 19 | var sca = pressure * this.data.size * Math.random(); 20 | box.scale.set(sca, sca, sca); 21 | box.position.copy(pointerPosition); 22 | box.quaternion.copy(orientation); 23 | 24 | this.object3D.add(box); 25 | 26 | return true; 27 | }, 28 | undo: function () { 29 | this.drawingEl.object3D.children.pop(); 30 | } 31 | }, 32 | {thumbnail: 'brushes/thumb_cubes.gif', spacing: 0.01} 33 | ); 34 | -------------------------------------------------------------------------------- /src/brushes/line.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | var sharedBufferGeometryManager = require('../sharedbuffergeometrymanager.js'); 3 | var onLoaded = require('../onloaded.js'); 4 | 5 | (function () { 6 | 7 | var geometryManager = null; 8 | 9 | onLoaded(function () { 10 | var optionsBasic = { 11 | vertexColors: THREE.VertexColors, 12 | side: THREE.DoubleSide 13 | }; 14 | 15 | var optionsStandard = { 16 | roughness: 0.75, 17 | metalness: 0.25, 18 | vertexColors: THREE.VertexColors, 19 | map: window.atlas.map, 20 | side: THREE.DoubleSide 21 | }; 22 | 23 | var optionTextured = { 24 | roughness: 0.75, 25 | metalness: 0.25, 26 | vertexColors: THREE.VertexColors, 27 | side: THREE.DoubleSide, 28 | map: window.atlas.map, 29 | transparent: true, 30 | alphaTest: 0.5 31 | }; 32 | 33 | sharedBufferGeometryManager.addSharedBuffer('strip-flat', new THREE.MeshBasicMaterial(optionsBasic)); 34 | sharedBufferGeometryManager.addSharedBuffer('strip-shaded', new THREE.MeshStandardMaterial(optionsStandard)); 35 | sharedBufferGeometryManager.addSharedBuffer('strip-textured', new THREE.MeshStandardMaterial(optionTextured)); 36 | }); 37 | 38 | var line = { 39 | 40 | init: function (color, brushSize) { 41 | this.sharedBuffer = sharedBufferGeometryManager.getSharedBuffer('strip-' + this.materialOptions.type); 42 | this.sharedBuffer.restartPrimitive(); 43 | this.sharedBuffer.strip = true; 44 | 45 | this.prevIdx = Object.assign({}, this.sharedBuffer.idx); 46 | this.idx = Object.assign({}, this.sharedBuffer.idx); 47 | 48 | this.first = true; 49 | }, 50 | remove: function () { 51 | this.sharedBuffer.remove(this.prevIdx, this.idx); 52 | }, 53 | undo: function () { 54 | this.sharedBuffer.undo(this.prevIdx); 55 | }, 56 | addPoint: (function () { 57 | var direction = new THREE.Vector3(); 58 | 59 | return function (position, orientation, pointerPosition, pressure, timestamp) { 60 | var converter = this.materialOptions.converter; 61 | 62 | direction.set(1, 0, 0); 63 | direction.applyQuaternion(orientation); 64 | direction.normalize(); 65 | 66 | var posA = pointerPosition.clone(); 67 | var posB = pointerPosition.clone(); 68 | var brushSize = this.data.size * pressure; 69 | posA.add(direction.clone().multiplyScalar(brushSize / 2)); 70 | posB.add(direction.clone().multiplyScalar(-brushSize / 2)); 71 | 72 | if (this.first && this.prevIdx.position > 0) { 73 | // Degenerated triangle 74 | this.first = false; 75 | this.sharedBuffer.addVertex(posA.x, posA.y, posA.z); 76 | this.sharedBuffer.idx.normal++; 77 | this.sharedBuffer.idx.color++; 78 | this.sharedBuffer.idx.uv++; 79 | 80 | this.idx = Object.assign({}, this.sharedBuffer.idx); 81 | } 82 | 83 | /* 84 | 2---3 85 | | \ | 86 | 0---1 87 | */ 88 | this.sharedBuffer.addVertex(posA.x, posA.y, posA.z); 89 | this.sharedBuffer.addVertex(posB.x, posB.y, posB.z); 90 | this.sharedBuffer.idx.normal += 2; 91 | 92 | this.sharedBuffer.addColor(this.data.color.r, this.data.color.g, this.data.color.b); 93 | this.sharedBuffer.addColor(this.data.color.r, this.data.color.g, this.data.color.b); 94 | 95 | if (this.materialOptions.type === 'textured') { 96 | this.sharedBuffer.idx.uv += 2; 97 | var uvs = this.sharedBuffer.current.attributes.uv.array; 98 | var u, offset; 99 | for (var i = 0; i < this.data.numPoints + 1; i++) { 100 | u = i / this.data.numPoints; 101 | offset = 4 * i; 102 | if (this.prevIdx.uv !== 0) { 103 | offset += (this.prevIdx.uv + 1) * 2; 104 | } 105 | 106 | uvs[offset] = converter.convertU(u); 107 | uvs[offset + 1] = converter.convertV(0); 108 | 109 | uvs[offset + 2] = converter.convertU(u); 110 | uvs[offset + 3] = converter.convertV(1); 111 | } 112 | } 113 | 114 | this.idx = Object.assign({}, this.sharedBuffer.idx); 115 | 116 | this.sharedBuffer.update(); 117 | this.computeVertexNormals(); 118 | return true; 119 | }; 120 | })(), 121 | 122 | computeVertexNormals: (function () { 123 | var pA = new THREE.Vector3(); 124 | var pB = new THREE.Vector3(); 125 | var pC = new THREE.Vector3(); 126 | var cb = new THREE.Vector3(); 127 | var ab = new THREE.Vector3(); 128 | 129 | return function () { 130 | var start = this.prevIdx.position === 0 ? 0 : (this.prevIdx.position + 1) * 3; 131 | var end = (this.idx.position) * 3; 132 | var vertices = this.sharedBuffer.current.attributes.position.array; 133 | var normals = this.sharedBuffer.current.attributes.normal.array; 134 | 135 | for (var i = start; i <= end; i++) { 136 | normals[i] = 0; 137 | } 138 | 139 | var pair = true; 140 | for (i = start; i < end - 6; i += 3) { 141 | if (pair) { 142 | pA.fromArray(vertices, i); 143 | pB.fromArray(vertices, i + 3); 144 | pC.fromArray(vertices, i + 6); 145 | } else { 146 | pB.fromArray(vertices, i); 147 | pC.fromArray(vertices, i + 6); 148 | pA.fromArray(vertices, i + 3); 149 | } 150 | pair = !pair; 151 | 152 | cb.subVectors(pC, pB); 153 | ab.subVectors(pA, pB); 154 | cb.cross(ab); 155 | cb.normalize(); 156 | 157 | normals[i] += cb.x; 158 | normals[i + 1] += cb.y; 159 | normals[i + 2] += cb.z; 160 | 161 | normals[i + 3] += cb.x; 162 | normals[i + 4] += cb.y; 163 | normals[i + 5] += cb.z; 164 | 165 | normals[i + 6] += cb.x; 166 | normals[i + 7] += cb.y; 167 | normals[i + 8] += cb.z; 168 | } 169 | 170 | /* 171 | first and last vertices (0 and 8) belongs just to one triangle 172 | second and penultimate (1 and 7) belongs to two triangles 173 | the rest of the vertices belongs to three triangles 174 | 175 | 1_____3_____5_____7 176 | /\ /\ /\ /\ 177 | / \ / \ / \ / \ 178 | /____\/____\/____\/____\ 179 | 0 2 4 6 8 180 | */ 181 | 182 | // Vertices that are shared across three triangles 183 | for (i = start + 2 * 3; i < end - 2 * 3; i++) { 184 | normals[i] = normals[i] / 3; 185 | } 186 | 187 | // Second and penultimate triangle, that shares just two triangles 188 | normals[start + 3] = normals[start + 3] / 2; 189 | normals[start + 3 + 1] = normals[start + 3 + 1] / 2; 190 | normals[start + 3 + 2] = normals[start + 3 * 1 + 2] / 2; 191 | 192 | normals[end - 2 * 3] = normals[end - 2 * 3] / 2; 193 | normals[end - 2 * 3 + 1] = normals[end - 2 * 3 + 1] / 2; 194 | normals[end - 2 * 3 + 2] = normals[end - 2 * 3 + 2] / 2; 195 | }; 196 | })() 197 | }; 198 | 199 | var lines = [ 200 | { 201 | name: 'flat', 202 | materialOptions: { 203 | type: 'flat' 204 | }, 205 | thumbnail: 'brushes/thumb_flat.gif' 206 | }, 207 | { 208 | name: 'smooth', 209 | materialOptions: { 210 | type: 'shaded' 211 | }, 212 | thumbnail: 'brushes/thumb_smooth.gif' 213 | }, 214 | { 215 | name: 'squared-textured', 216 | materialOptions: { 217 | type: 'textured', 218 | textureSrc: 'brushes/squared_textured.png' 219 | }, 220 | thumbnail: 'brushes/thumb_squared_textured.gif' 221 | }, 222 | { 223 | name: 'line-gradient', 224 | materialOptions: { 225 | type: 'textured', 226 | textureSrc: 'brushes/line_gradient.png' 227 | }, 228 | thumbnail: 'brushes/thumb_line_gradient.gif' 229 | }, 230 | { 231 | name: 'silky-flat', 232 | materialOptions: { 233 | type: 'textured', 234 | textureSrc: 'brushes/silky_flat.png' 235 | }, 236 | thumbnail: 'brushes/thumb_silky_flat.gif' 237 | }, 238 | { 239 | name: 'silky-textured', 240 | materialOptions: { 241 | type: 'textured', 242 | textureSrc: 'brushes/silky_textured.png' 243 | }, 244 | thumbnail: 'brushes/thumb_silky_textured.gif' 245 | }, 246 | { 247 | name: 'lines1', 248 | materialOptions: { 249 | type: 'textured', 250 | textureSrc: 'brushes/lines1.png' 251 | }, 252 | thumbnail: 'brushes/thumb_lines1.gif' 253 | }, 254 | { 255 | name: 'lines2', 256 | materialOptions: { 257 | type: 'textured', 258 | textureSrc: 'brushes/lines2.png' 259 | }, 260 | thumbnail: 'brushes/thumb_lines2.gif' 261 | }, 262 | { 263 | name: 'lines3', 264 | materialOptions: { 265 | type: 'textured', 266 | textureSrc: 'brushes/lines3.png' 267 | }, 268 | thumbnail: 'brushes/thumb_lines3.gif' 269 | }, 270 | { 271 | name: 'lines4', 272 | materialOptions: { 273 | type: 'textured', 274 | textureSrc: 'brushes/lines4.png' 275 | }, 276 | thumbnail: 'brushes/thumb_lines4.gif' 277 | }, 278 | { 279 | name: 'lines5', 280 | materialOptions: { 281 | type: 'textured', 282 | textureSrc: 'brushes/lines5.png' 283 | }, 284 | thumbnail: 'brushes/thumb_lines5.gif' 285 | }, 286 | { 287 | name: 'line-grunge1', 288 | materialOptions: { 289 | type: 'textured', 290 | textureSrc: 'brushes/line_grunge1.png' 291 | }, 292 | thumbnail: 'brushes/thumb_line_grunge1.gif' 293 | }, 294 | { 295 | name: 'line-grunge2', 296 | materialOptions: { 297 | type: 'textured', 298 | textureSrc: 'brushes/line_grunge2.png' 299 | }, 300 | thumbnail: 'brushes/thumb_line_grunge2.gif' 301 | }, 302 | { 303 | name: 'line-grunge3', 304 | materialOptions: { 305 | type: 'textured', 306 | textureSrc: 'brushes/line_grunge3.png' 307 | }, 308 | thumbnail: 'brushes/thumb_line_grunge3.gif' 309 | } 310 | ]; 311 | 312 | for (var i = 0; i < lines.length; i++) { 313 | var definition = lines[i]; 314 | if (definition.materialOptions.textureSrc) { 315 | definition.materialOptions.converter = window.atlas.getUVConverters(definition.materialOptions.textureSrc); 316 | } else { 317 | definition.materialOptions.converter = window.atlas.getUVConverters(null); 318 | } 319 | 320 | AFRAME.registerBrush(definition.name, Object.assign({}, line, {materialOptions: definition.materialOptions}), {thumbnail: definition.thumbnail, maxPoints: 3000}); 321 | } 322 | })(); 323 | -------------------------------------------------------------------------------- /src/brushes/rainbow.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | (function(){ 3 | var vertexShader = "varying vec2 vUv; \ 4 | void main() { \ 5 | vUv = uv; \ 6 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); \ 7 | }"; 8 | 9 | var fragmentShader = "uniform sampler2D tDiffuse; \ 10 | uniform float amount; \ 11 | uniform float time; \ 12 | varying vec2 vUv; \ 13 | \ 14 | vec3 hsv2rgb(vec3 c) { \ 15 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); \ 16 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); \ 17 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); \ 18 | } \ 19 | \ 20 | void main() { \ 21 | float h = mod(vUv.x - time / 3000.0, 1.0); \ 22 | vec4 color = vec4(hsv2rgb(vec3(h, 1.0, 0.5)), 1.0); \ 23 | gl_FragColor = color; \ 24 | }"; 25 | 26 | var material = new THREE.ShaderMaterial({ 27 | vertexShader: vertexShader, 28 | fragmentShader: fragmentShader, 29 | side: THREE.DoubleSide, 30 | uniforms: { 31 | time: {type: 'f', value: 0} 32 | } 33 | }); 34 | 35 | AFRAME.registerBrush('line-rainbow', 36 | { 37 | init: function (color, brushSize) { 38 | this.idx = 0; 39 | this.geometry = new THREE.BufferGeometry(); 40 | this.vertices = new Float32Array(this.options.maxPoints * 3 * 3); 41 | this.indices = new Uint32Array(this.options.maxPoints * 4.5 * 4.5) 42 | this.uvs = new Float32Array(this.options.maxPoints * 2 * 2); 43 | this.linepositions = new Float32Array(this.options.maxPoints); 44 | 45 | this.geometry.setDrawRange(0, 0); 46 | this.geometry.setAttribute('position', new THREE.BufferAttribute(this.vertices, 3).setUsage(THREE.DynamicDrawUsage)); 47 | this.geometry.setIndex(new THREE.BufferAttribute(this.indices, 3).setUsage(THREE.DynamicDrawUsage)); 48 | this.geometry.setAttribute('uv', new THREE.BufferAttribute(this.uvs, 2).setUsage(THREE.DynamicDrawUsage)); 49 | this.geometry.setAttribute('lineposition', new THREE.BufferAttribute(this.linepositions, 1).setUsage(THREE.DynamicDrawUsage)); 50 | 51 | this.material = material; 52 | var mesh = new THREE.Mesh(this.geometry, this.material); 53 | 54 | mesh.frustumCulled = false; 55 | mesh.vertices = this.vertices; 56 | 57 | this.object3D.add(mesh); 58 | this.drawing = document.querySelector('.a-drawing'); 59 | this.drawing.object3D.add(this.object3D); 60 | }, 61 | addPoint: (function () { 62 | var direction = new THREE.Vector3(); 63 | var posA = new THREE.Vector3(); 64 | var posB = new THREE.Vector3(); 65 | var auxDir = new THREE.Vector3(); 66 | 67 | return function (position, orientation, pointerPosition, pressure, timestamp) { 68 | var uv = 0; 69 | for (i = 0; i < this.data.numPoints; i++) { 70 | this.uvs[uv++] = i / (this.data.numPoints - 1); 71 | this.uvs[uv++] = 0; 72 | 73 | this.uvs[uv++] = i / (this.data.numPoints - 1); 74 | this.uvs[uv++] = 1; 75 | } 76 | 77 | direction.set(1, 0, 0); 78 | direction.applyQuaternion(orientation); 79 | direction.normalize(); 80 | 81 | posA.copy(pointerPosition); 82 | posB.copy(pointerPosition); 83 | 84 | var brushSize = this.data.size * pressure; 85 | posA.add(auxDir.copy(direction).multiplyScalar(brushSize / 2)); 86 | posB.add(auxDir.copy(direction).multiplyScalar(-brushSize / 2)); 87 | 88 | this.vertices[this.idx++] = posA.x; 89 | this.vertices[this.idx++] = posA.y; 90 | this.vertices[this.idx++] = posA.z; 91 | 92 | this.vertices[this.idx++] = posB.x; 93 | this.vertices[this.idx++] = posB.y; 94 | this.vertices[this.idx++] = posB.z; 95 | 96 | // Same principle as line brush strips 97 | if (this.idx > 6) { 98 | this.geometry.index.setXYZ(this.idx / 3 - 4, this.idx / 3 - 4, this.idx / 3 - 3, this.idx / 3 - 2); 99 | this.geometry.index.setXYZ(this.idx / 3 - 3, this.idx / 3 - 2, this.idx / 3 - 3, this.idx / 3 - 1); 100 | } 101 | 102 | this.geometry.attributes.position.needsUpdate = true; 103 | this.geometry.index.needsUpdate = true; 104 | this.geometry.attributes.uv.needsUpdate = true; 105 | 106 | this.geometry.setDrawRange(0, this.data.numPoints * 2 * 6); 107 | 108 | return true; 109 | } 110 | })(), 111 | tick: function(timeOffset, delta) { 112 | this.material.uniforms.time.value = timeOffset; 113 | }, 114 | undo: function () { 115 | this.drawing.object3D.children.pop(); 116 | } 117 | }, 118 | {thumbnail:'brushes/thumb_rainbow.png', maxPoints: 3000} 119 | ); 120 | })(); 121 | -------------------------------------------------------------------------------- /src/brushes/single-sphere.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | AFRAME.registerBrush('single-sphere', 3 | { 4 | init: function (color, width) { 5 | this.material = new THREE.MeshStandardMaterial({ 6 | color: this.data.color, 7 | roughness: 0.6, 8 | metalness: 0.2, 9 | side: THREE.FrontSide, 10 | flatShading: true 11 | }); 12 | this.geometry = new THREE.IcosahedronGeometry(1, 2); 13 | this.mesh = new THREE.Mesh(this.geometry, this.material); 14 | this.object3D.add(this.mesh); 15 | this.mesh.visible = false; 16 | this.drawingEl = document.querySelector('.a-drawing'); 17 | this.drawingEl.object3D.add(this.object3D); 18 | }, 19 | addPoint: function (position, orientation, pointerPosition, pressure, timestamp) { 20 | if (!this.firstPoint) { 21 | this.firstPoint = pointerPosition.clone(); 22 | this.mesh.position.set(this.firstPoint.x, this.firstPoint.y, this.firstPoint.z) 23 | } 24 | this.mesh.visible = true 25 | var distance = this.firstPoint.distanceTo(pointerPosition); 26 | this.mesh.scale.set(distance, distance, distance); 27 | return true; 28 | }, 29 | undo: function () { 30 | this.drawingEl.object3D.children.pop(); 31 | } 32 | }, 33 | {thumbnail: 'brushes/thumb_single_sphere.png', spacing: 0.0} 34 | ); 35 | -------------------------------------------------------------------------------- /src/brushes/spheres.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | AFRAME.registerBrush('spheres', 3 | { 4 | init: function (color, width) { 5 | // Initialize the material based on the stroke color 6 | this.material = new THREE.MeshStandardMaterial({ 7 | color: this.data.color, 8 | roughness: 0.5, 9 | metalness: 0.5, 10 | side: THREE.DoubleSide, 11 | flatShading: true 12 | }); 13 | this.geometry = new THREE.IcosahedronGeometry(1, 0); 14 | this.drawingEl = document.querySelector('.a-drawing'); 15 | this.drawingEl.object3D.add(this.object3D); 16 | }, 17 | // This function is called every time we need to add a point to our stroke 18 | // It should returns true if the point is added correctly, false otherwise. 19 | addPoint: function (position, orientation, pointerPosition, pressure, timestamp) { 20 | // Create a new sphere mesh to insert at the given position 21 | var sphere = new THREE.Mesh(this.geometry, this.material); 22 | 23 | // The scale is determined by the trigger preassure 24 | var sca = this.data.size / 2 * pressure; 25 | sphere.scale.set(sca, sca, sca); 26 | sphere.initialScale = sphere.scale.clone(); 27 | 28 | // Generate a random phase to be used in the tick animation 29 | sphere.phase = Math.random() * Math.PI * 2; 30 | 31 | // Set the position of the sphere to match the controller positoin 32 | sphere.position.copy(pointerPosition); 33 | sphere.quaternion.copy(orientation); 34 | 35 | // Add the sphere to the object3D 36 | this.object3D.add(sphere); 37 | 38 | // Return true as we've added correctly a new point (or sphere) 39 | return true; 40 | }, 41 | // This function is called on every frame 42 | tick: function (time, delta) { 43 | for (var i = 0; i < this.object3D.children.length; i++) { 44 | var sphere = this.object3D.children[i]; 45 | // Calculate the sine value based on the time and the phase for this sphere 46 | // and use it to scale the geometry 47 | var sin = (Math.sin(sphere.phase + time / 500.0) + 1) / 2 + 0.1; 48 | sphere.scale.copy(sphere.initialScale).multiplyScalar(sin); 49 | } 50 | }, 51 | undo: function () { 52 | this.drawingEl.object3D.children.pop(); 53 | } 54 | }, 55 | // Define extra options for this brush 56 | {thumbnail: 'brushes/thumb_spheres.gif', spacing: 0.01} 57 | ); 58 | -------------------------------------------------------------------------------- /src/brushes/stamp.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME THREE */ 2 | var sharedBufferGeometryManager = require('../sharedbuffergeometrymanager.js'); 3 | var onLoaded = require('../onloaded.js'); 4 | 5 | (function () { 6 | 7 | onLoaded(function () { 8 | var shaded = new THREE.MeshStandardMaterial({ 9 | side: THREE.DoubleSide, 10 | map: window.atlas.map, 11 | vertexColors: THREE.VertexColors, 12 | transparent: true, 13 | alphaTest: 0.5, 14 | roughness: 0.75, 15 | metalness: 0.25 16 | }); 17 | var flat = new THREE.MeshBasicMaterial({ 18 | side: THREE.DoubleSide, 19 | map: window.atlas.map, 20 | vertexColors: THREE.VertexColors, 21 | transparent: true, 22 | alphaTest: 0.5 23 | }); 24 | 25 | sharedBufferGeometryManager.addSharedBuffer('tris-flat', flat); 26 | sharedBufferGeometryManager.addSharedBuffer('tris-shaded', shaded); 27 | }); 28 | 29 | var stamp = { 30 | 31 | init: function (color, brushSize) { 32 | this.sharedBuffer = sharedBufferGeometryManager.getSharedBuffer('tris-' + this.materialOptions.type); 33 | this.prevIdx = Object.assign({}, this.sharedBuffer.idx); 34 | this.idx = Object.assign({}, this.sharedBuffer.idx); 35 | this.sharedBuffer.strip = false; 36 | 37 | this.currAngle = 0; 38 | this.subTextures = 1; 39 | this.angleJitter = 0; 40 | this.autoRotate = false; 41 | 42 | if (this.materialOptions['subTextures'] !== undefined) { 43 | this.subTextures = this.materialOptions['subTextures']; 44 | } 45 | if (this.materialOptions['autoRotate'] === true) { 46 | this.autoRotate = true; 47 | } 48 | if (this.materialOptions['angleJitter'] !== undefined) { 49 | this.angleJitter = this.materialOptions['angleJitter']; 50 | this.angleJitter = this.angleJitter * 2 - this.angleJitter; 51 | } 52 | }, 53 | 54 | remove: function () { 55 | this.sharedBuffer.remove(this.prevIdx, this.idx); 56 | }, 57 | 58 | undo: function () { 59 | this.sharedBuffer.undo(this.prevIdx); 60 | }, 61 | 62 | addPoint: (function () { 63 | var axis = new THREE.Vector3(); 64 | var dir = new THREE.Vector3(); 65 | var a = new THREE.Vector3(); 66 | var b = new THREE.Vector3(); 67 | var c = new THREE.Vector3(); 68 | var d = new THREE.Vector3(); 69 | var auxDir = new THREE.Vector3(); 70 | var pi2 = Math.PI / 2; 71 | 72 | return function (position, rotation, pointerPosition, pressure, timestamp) { 73 | // brush side 74 | dir.set(1, 0, 0); 75 | dir.applyQuaternion(rotation); 76 | dir.normalize(); 77 | 78 | // brush normal 79 | axis.set(0, 1, 0); 80 | axis.applyQuaternion(rotation); 81 | axis.normalize(); 82 | 83 | var brushSize = this.data.size * pressure / 2; 84 | var brushAngle = Math.PI / 4 + Math.random() * this.angleJitter; 85 | 86 | if (this.autoRotate) { 87 | this.currAngle += 0.1; 88 | brushAngle += this.currAngle; 89 | } 90 | 91 | a.copy(pointerPosition).add(auxDir.copy(dir.applyAxisAngle(axis, brushAngle)).multiplyScalar(brushSize)); 92 | b.copy(pointerPosition).add(auxDir.copy(dir.applyAxisAngle(axis, pi2)).multiplyScalar(brushSize)); 93 | c.copy(pointerPosition).add(auxDir.copy(dir.applyAxisAngle(axis, pi2)).multiplyScalar(brushSize)); 94 | d.copy(pointerPosition).add(dir.applyAxisAngle(axis, pi2).multiplyScalar(brushSize)); 95 | 96 | var nidx = this.idx.position; 97 | var cidx = this.idx.position; 98 | 99 | // triangle 1 100 | this.sharedBuffer.addVertex(a.x, a.y, a.z); 101 | this.sharedBuffer.addVertex(b.x, b.y, b.z); 102 | this.sharedBuffer.addVertex(c.x, c.y, c.z); 103 | 104 | // triangle 2 105 | this.sharedBuffer.addVertex(c.x, c.y, c.z); 106 | this.sharedBuffer.addVertex(d.x, d.y, d.z); 107 | this.sharedBuffer.addVertex(a.x, a.y, a.z); 108 | 109 | // normals & color 110 | for (var i = 0; i < 6; i++) { 111 | this.sharedBuffer.addNormal(axis.x, axis.y, axis.z); 112 | this.sharedBuffer.addColor(this.data.color.r, this.data.color.g, this.data.color.b); 113 | } 114 | 115 | // UVs 116 | var uv = this.data.numPoints * 6 * 2; 117 | 118 | // subTextures? 119 | var Umin = 0; 120 | var Umax = 1; 121 | if (this.subTextures > 1) { 122 | var subt = Math.floor(Math.random() * this.subTextures); 123 | Umin = 1.0 / this.subTextures * subt; 124 | Umax = Umin + 1.0 / this.subTextures; 125 | } 126 | 127 | var converter = this.materialOptions.converter; 128 | 129 | // triangle 1 uv 130 | this.sharedBuffer.addUV(converter.convertU(Umin), converter.convertV(1)); 131 | this.sharedBuffer.addUV(converter.convertU(Umin), converter.convertV(0)); 132 | this.sharedBuffer.addUV(converter.convertU(Umax), converter.convertV(0)); 133 | 134 | // triangle2 uv 135 | this.sharedBuffer.addUV(converter.convertU(Umax), converter.convertV(0)); 136 | this.sharedBuffer.addUV(converter.convertU(Umax), converter.convertV(1)); 137 | this.sharedBuffer.addUV(converter.convertU(Umin), converter.convertV(1)); 138 | 139 | this.idx = Object.assign({}, this.sharedBuffer.idx); 140 | 141 | this.sharedBuffer.update(); 142 | 143 | return true; 144 | } 145 | })() 146 | 147 | }; 148 | 149 | var stamps = [ 150 | { 151 | name: 'dots', 152 | materialOptions: { 153 | type: 'shaded', 154 | textureSrc: 'brushes/stamp_dots.png' 155 | }, 156 | thumbnail: 'brushes/thumb_stamp_dots.gif', 157 | spacing: 0.01 158 | }, 159 | { 160 | name: 'squares', 161 | materialOptions: { 162 | type: 'shaded', 163 | textureSrc: 'brushes/stamp_squares.png' 164 | }, 165 | thumbnail: 'brushes/thumb_stamp_squares.gif', 166 | spacing: 0.01 167 | }, 168 | { 169 | name: 'column', 170 | materialOptions: { 171 | type: 'shaded', 172 | autoRotate: true, 173 | textureSrc: 'brushes/stamp_column.png' 174 | }, 175 | thumbnail: 'brushes/thumb_stamp_column.gif', 176 | spacing: 0.01 177 | }, 178 | { 179 | name: 'gear1', 180 | materialOptions: { 181 | type: 'shaded', 182 | angleJitter: Math.PI * 2, 183 | subTextures: 2, 184 | textureSrc: 'brushes/stamp_gear.png' 185 | }, 186 | thumbnail: 'brushes/thumb_stamp_gear.gif', 187 | spacing: 0.05 188 | }, 189 | { 190 | name: 'grunge1', 191 | materialOptions: { 192 | type: 'shaded', 193 | angleJitter: Math.PI * 2, 194 | textureSrc: 'brushes/stamp_grunge1.png' 195 | }, 196 | thumbnail: 'brushes/stamp_grunge1.png', 197 | spacing: 0.02 198 | }, 199 | { 200 | name: 'grunge2', 201 | materialOptions: { 202 | type: 'shaded', 203 | angleJitter: Math.PI * 2, 204 | textureSrc: 'brushes/stamp_grunge2.png' 205 | }, 206 | thumbnail: 'brushes/stamp_grunge2.png', 207 | spacing: 0.02 208 | }, 209 | { 210 | name: 'grunge3', 211 | materialOptions: { 212 | type: 'shaded', 213 | angleJitter: Math.PI * 2, 214 | textureSrc: 'brushes/stamp_grunge3.png' 215 | }, 216 | thumbnail: 'brushes/stamp_grunge3.png', 217 | spacing: 0.02 218 | }, 219 | { 220 | name: 'grunge4', 221 | materialOptions: { 222 | type: 'shaded', 223 | angleJitter: Math.PI * 2, 224 | textureSrc: 'brushes/stamp_grunge4.png' 225 | }, 226 | thumbnail: 'brushes/stamp_grunge4.png', 227 | spacing: 0.02 228 | }, 229 | { 230 | name: 'grunge5', 231 | materialOptions: { 232 | type: 'shaded', 233 | angleJitter: Math.PI * 2, 234 | textureSrc: 'brushes/stamp_grunge5.png' 235 | }, 236 | thumbnail: 'brushes/thumb_stamp_grunge5.gif', 237 | spacing: 0.02 238 | }, 239 | { 240 | name: 'leaf1', 241 | materialOptions: { 242 | type: 'shaded', 243 | angleJitter: Math.PI, 244 | textureSrc: 'brushes/stamp_leaf1.png' 245 | }, 246 | thumbnail: 'brushes/stamp_leaf1.png', 247 | spacing: 0.03 248 | }, 249 | { 250 | name: 'leaf2', 251 | materialOptions: { 252 | type: 'shaded', 253 | angleJitter: 60 * Math.PI / 180.0, 254 | textureSrc: 'brushes/stamp_leaf2.png' 255 | }, 256 | thumbnail: 'brushes/thumb_stamp_leaf2.gif', 257 | spacing: 0.03 258 | }, 259 | { 260 | name: 'leaf3', 261 | materialOptions: { 262 | type: 'shaded', 263 | angleJitter: 60 * Math.PI / 180.0, 264 | textureSrc: 'brushes/stamp_leaf3.png' 265 | }, 266 | thumbnail: 'brushes/thumb_stamp_leaf3.gif', 267 | spacing: 0.03 268 | }, 269 | { 270 | name: 'fur1', 271 | materialOptions: { 272 | type: 'shaded', 273 | angleJitter: 40 * Math.PI / 180.0, 274 | subTextures: 2, 275 | textureSrc: 'brushes/stamp_fur1.png' 276 | }, 277 | thumbnail: 'brushes/stamp_fur1.png', 278 | spacing: 0.01 279 | }, 280 | { 281 | name: 'fur2', 282 | materialOptions: { 283 | type: 'shaded', 284 | angleJitter: 10 * Math.PI / 180.0, 285 | subTextures: 3, 286 | textureSrc: 'brushes/stamp_fur2.png' 287 | }, 288 | thumbnail: 'brushes/stamp_fur2.png', 289 | spacing: 0.01 290 | }, 291 | { 292 | name: 'grass', 293 | materialOptions: { 294 | type: 'shaded', 295 | angleJitter: 10 * Math.PI / 180.0, 296 | subTextures: 3, 297 | textureSrc: 'brushes/stamp_grass.png' 298 | }, 299 | thumbnail: 'brushes/thumb_stamp_grass.png', 300 | spacing: 0.03 301 | }, 302 | { 303 | name: 'bush', 304 | materialOptions: { 305 | type: 'shaded', 306 | subTextures: 2, 307 | textureSrc: 'brushes/stamp_bush.png' 308 | }, 309 | thumbnail: 'brushes/thumb_stamp_bush.gif', 310 | spacing: 0.04 311 | }, 312 | { 313 | name: 'star', 314 | materialOptions: { 315 | type: 'shaded', 316 | textureSrc: 'brushes/stamp_star.png' 317 | }, 318 | thumbnail: 'brushes/thumb_stamp_star.png', 319 | spacing: 0.06 320 | }, 321 | { 322 | name: 'snow', 323 | materialOptions: { 324 | type: 'shaded', 325 | angleJitter: Math.PI * 2, 326 | textureSrc: 'brushes/stamp_snow.png' 327 | }, 328 | thumbnail: 'brushes/thumb_stamp_snow.png', 329 | spacing: 0.06 330 | } 331 | ]; 332 | 333 | // var textureLoader = new THREE.TextureLoader(); 334 | for (var i = 0; i < stamps.length; i++) { 335 | var definition = stamps[i]; 336 | if (definition.materialOptions.textureSrc) { 337 | definition.materialOptions.map = window.atlas.map; //textureLoader.load(definition.materialOptions.textureSrc); 338 | definition.materialOptions.converter = window.atlas.getUVConverters(definition.materialOptions.textureSrc); 339 | delete definition.materialOptions.textureSrc; 340 | } 341 | AFRAME.registerBrush(definition.name, Object.assign({}, stamp, {materialOptions: definition.materialOptions}), {thumbnail: definition.thumbnail, spacing: definition.spacing, maxPoints: 3000}); 342 | } 343 | 344 | /* 345 | - type: <'flat'|'shaded'> 346 | Flat: constant, just color. Shaded: phong shading with subtle speculars 347 | - autoRotate: 348 | The brush rotates incrementally at 0.1rad per point 349 | - angleJitter: 350 | The brush rotates randomly from -r to r 351 | - subTextures: 352 | textureSrc is divided in n horizontal pieces, and the brush picks one randomly on each point 353 | */ 354 | })(); 355 | -------------------------------------------------------------------------------- /src/components/brush-tip.js: -------------------------------------------------------------------------------- 1 | AFRAME.registerComponent('brush-tip', { 2 | schema: { 3 | controller: { type: 'string' }, 4 | hand: { 5 | type: 'string', 6 | oneOf: ['left', 'right'] 7 | } 8 | }, 9 | 10 | init: function () { 11 | var toRad = degrees => THREE.MathUtils.degToRad(degrees); 12 | 13 | this.controllers = { 14 | 'oculus-touch-controller-v3': { 15 | left: { 16 | positionOffset: { x: 0, y: -0.025, z: -0.042 }, 17 | rotationOffset: { x: toRad(-45), y: toRad(7), z: toRad(-7) } 18 | }, 19 | right: { 20 | positionOffset: { x: 0, y: -0.025, z: -0.042 }, 21 | rotationOffset: { x: toRad(-45), y: toRad(-7), z: toRad(7) } 22 | } 23 | } 24 | }; 25 | 26 | if (this.data.controller) { 27 | this.setController(this.data.controller, this.data.hand); 28 | } 29 | }, 30 | 31 | setController: function (controller, hand) { 32 | if (controller in this.controllers) { 33 | this.el.object3D.position.set( 34 | this.controllers[controller][hand].positionOffset.x, 35 | this.controllers[controller][hand].positionOffset.y, 36 | this.controllers[controller][hand].positionOffset.z 37 | ); 38 | this.el.object3D.rotation.set( 39 | this.controllers[controller][hand].rotationOffset.x, 40 | this.controllers[controller][hand].rotationOffset.y, 41 | this.controllers[controller][hand].rotationOffset.z 42 | ) 43 | } else { 44 | console.error(`${controller} is not present in the controllers list!`); 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/components/brush.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | AFRAME.registerComponent('brush', { 3 | schema: { 4 | color: {type: 'color', default: '#ef2d5e'}, 5 | size: {default: 0.01, min: 0.001, max: 0.3}, 6 | brush: {default: 'smooth'}, 7 | enabled: { default: true } 8 | }, 9 | init: function () { 10 | var data = this.data; 11 | this.color = new THREE.Color(data.color); 12 | 13 | this.el.emit('brushcolor-changed', {color: this.color}); 14 | this.el.emit('brushsize-changed', {brushSize: data.size}); 15 | 16 | this.active = false; 17 | this.obj = this.el.object3D; 18 | 19 | this.currentStroke = null; 20 | this.strokeEntities = []; 21 | 22 | this.sizeModifier = 0.0; 23 | this.textures = {}; 24 | this.currentMap = 0; 25 | 26 | this.model = this.el.getObject3D('mesh'); 27 | this.drawing = false; 28 | 29 | var self = this; 30 | 31 | this.el.addEventListener('undo', function(evt) { 32 | if (!self.data.enabled) { return; } 33 | self.system.undo(); 34 | document.getElementById('ui_undo').play(); 35 | }); 36 | 37 | this.el.addEventListener('paint', function (evt) { 38 | if (!self.data.enabled) { return; } 39 | // Trigger 40 | var value = evt.detail.value; 41 | self.sizeModifier = value; 42 | if (value > 0.1) { 43 | if (!self.active) { 44 | self.startNewStroke(); 45 | self.active = true; 46 | } 47 | } else { 48 | if (self.active) { 49 | self.previousEntity = self.currentEntity; 50 | self.currentStroke = null; 51 | } 52 | self.active = false; 53 | } 54 | }); 55 | 56 | this.hand = this.el.id === 'right-hand' ? 'right' : 'left'; 57 | }, 58 | update: function (oldData) { 59 | var data = this.data; 60 | if (oldData.color !== data.color) { 61 | this.color.set(data.color); 62 | this.el.emit('brushcolor-changed', {color: this.color}); 63 | } 64 | if (oldData.size !== data.size) { 65 | this.el.emit('brushsize-changed', {size: data.size}); 66 | } 67 | }, 68 | tick: (function () { 69 | var position = new THREE.Vector3(); 70 | var rotation = new THREE.Quaternion(); 71 | var scale = new THREE.Vector3(); 72 | 73 | return function tick (time, delta) { 74 | if (this.currentStroke && this.active) { 75 | this.obj.matrixWorld.decompose(position, rotation, scale); 76 | var pointerPosition = this.system.getPointerPosition(position, rotation, this.hand); 77 | this.currentStroke.addPoint(position, rotation, pointerPosition, this.sizeModifier, time); 78 | } 79 | }; 80 | })(), 81 | startNewStroke: function () { 82 | document.getElementById('ui_paint').play(); 83 | this.currentStroke = this.system.addNewStroke(this.data.brush, this.color, this.data.size); 84 | this.el.emit('stroke-started', {entity: this.el, stroke: this.currentStroke}); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /src/components/json-model.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | 3 | AFRAME.registerComponent('json-model', { 4 | schema: { 5 | src: {type: 'asset'} 6 | }, 7 | 8 | init: function () { 9 | this.objectLoader = new THREE.ObjectLoader(); 10 | this.objectLoader.setCrossOrigin(''); 11 | }, 12 | 13 | update: function (oldData) { 14 | var self = this; 15 | var src = this.data.src; 16 | if (!src || src === oldData.src) { return; } 17 | this.objectLoader.load(this.data.src, function (group) { 18 | var Rotation = new THREE.Matrix4().makeRotationX(-Math.PI / 2); 19 | group.traverse(function (child) { 20 | if (!(child instanceof THREE.Mesh)) { return; } 21 | child.position.applyMatrix4(Rotation); 22 | }); 23 | self.el.setObject3D('mesh', group); 24 | self.el.emit('model-loaded', {format: 'json', model: group, src: src}); 25 | }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/logo-model.js: -------------------------------------------------------------------------------- 1 | AFRAME.registerComponent('logo-model', { 2 | schema: { 3 | opacity: {default: 1.0} 4 | }, 5 | init: function () { 6 | this.model = null; 7 | 8 | this.el.setAttribute('obj-model', 'obj: #logoobj; mtl: #logomtl'); 9 | this.el.addEventListener('model-loaded', this.setModel.bind(this)); 10 | }, 11 | setModel: function (evt) { 12 | this.model = evt.detail.model; 13 | }, 14 | update: function () { 15 | if (this.model != null) { 16 | this.model.children[0].material.opacity = this.data.opacity; 17 | } 18 | } 19 | }); -------------------------------------------------------------------------------- /src/components/paint-controls.js: -------------------------------------------------------------------------------- 1 | AFRAME.registerSystem('paint-controls', { 2 | numberStrokes: 0 3 | }); 4 | 5 | /* globals AFRAME THREE */ 6 | AFRAME.registerComponent('paint-controls', { 7 | dependencies: ['brush'], 8 | 9 | schema: { 10 | hand: {default: 'left'} 11 | }, 12 | 13 | init: function () { 14 | var el = this.el; 15 | var self = this; 16 | var highLightTextureUrl = 'assets/images/controller-pressed.png'; 17 | var tooltipGroups = null; 18 | this.controller = null; 19 | this.modelLoaded = false; 20 | 21 | this.onEnterVR = this.onEnterVR.bind(this); 22 | el.sceneEl.addEventListener('enter-vr', this.onEnterVR); 23 | el.object3D.visible = false; 24 | 25 | this.onModelLoaded = this.onModelLoaded.bind(this); 26 | el.addEventListener('model-loaded', this.onModelLoaded); 27 | 28 | el.addEventListener('changeBrushSizeAbs', function (evt) { 29 | if (evt.detail.axis[1] === 0 && evt.detail.axis[3] === 0) { return; } 30 | 31 | var magnitude = evt.detail.axis[1] || evt.detail.axis[3]; 32 | var delta = magnitude / 300; 33 | var size = el.components.brush.schema.size; 34 | var value = THREE.Math.clamp(self.el.getAttribute('brush').size - delta, size.min, size.max); 35 | 36 | self.el.setAttribute('brush', 'size', value); 37 | }); 38 | 39 | el.addEventListener('changeBrushSizeInc', function (evt) { 40 | if (evt.detail.axis[1] === 0 && evt.detail.axis[3] === 0) { return; } 41 | 42 | var magnitude = evt.detail.axis[1] || evt.detail.axis[3]; 43 | 44 | if (self.touchStarted) { 45 | self.touchStarted = false; 46 | self.startAxis = (magnitude + 1) / 2; 47 | } 48 | 49 | var currentAxis = (magnitude + 1) / 2; 50 | var delta = (self.startAxis - currentAxis) / 2; 51 | 52 | self.startAxis = currentAxis; 53 | 54 | var startValue = self.el.getAttribute('brush').size; 55 | var size = el.components.brush.schema.size; 56 | var value = THREE.Math.clamp(startValue - delta, size.min, size.max); 57 | 58 | self.el.setAttribute('brush', 'size', value); 59 | }); 60 | 61 | self.touchStarted = false; 62 | 63 | el.addEventListener('startChangeBrushSize', function () { 64 | self.touchStarted = true; 65 | }); 66 | 67 | el.addEventListener('controllerconnected', function (evt) { 68 | var controllerName = evt.detail.name; 69 | var hand = evt.detail.component.data.hand; 70 | 71 | const createBrushTip = (controllerVersion, hand) => { 72 | // Create brush tip and position it dynamically based on our current controller 73 | this.brushTip = document.createElement('a-entity'); 74 | this.brushTip.id = `${hand}-tip`; 75 | this.brushTip.setAttribute('gltf-model', '#tipObj'); 76 | this.brushTip.setAttribute('brush-tip', `controller: ${controllerVersion}; hand: ${hand}`); 77 | this.brushTip.addEventListener('model-loaded', self.onModelLoaded); 78 | el.appendChild(this.brushTip); 79 | } 80 | 81 | if (controllerName === 'windows-motion-controls') 82 | { 83 | var gltfName = evt.detail.component.el.components['gltf-model'].data; 84 | const SAMSUNG_DEVICE = '045E-065D'; 85 | if (!!gltfName) 86 | { 87 | if (gltfName.indexOf(SAMSUNG_DEVICE) >= 0) 88 | { 89 | controllerName = "windows-motion-samsung-controls"; 90 | } 91 | } 92 | } 93 | 94 | tooltipGroups = Utils.getTooltips(controllerName); 95 | if (controllerName.indexOf('windows-motion') >= 0) { 96 | // el.setAttribute('teleport-controls', {button: 'trackpad'}); 97 | } else if (controllerName === 'oculus-touch-controls') { 98 | const controllerModelURL = el.components[controllerName].displayModel[hand].modelUrl; 99 | const versionMatchPattern = /[^\/]*(?=-(?:left|right)\.)/; // Matches the "oculus-touch-controller-[version]" part of URL 100 | const controllerVersion = versionMatchPattern.exec(controllerModelURL)[0]; 101 | createBrushTip(controllerVersion, hand); 102 | } else if (controllerName === 'vive-controls') { 103 | el.setAttribute('gltf-model', 'url(assets/models/vive-controller.glb)'); 104 | } else { return; } 105 | 106 | if (!!tooltipGroups) { 107 | tooltipGroups.forEach(function (tooltipGroup) { 108 | tooltipGroup.setAttribute('visible', true); 109 | const tooltips = Array.prototype.slice.call(tooltipGroup.querySelectorAll('[tooltip]')); 110 | tooltips.forEach(function (tooltip) { 111 | tooltip.setAttribute('animation', { dur: 1000, delay: 2000, property: 'tooltip.opacity', from: 1.0, to: 0.0, startEvents: 'tooltip-fade' }); 112 | }); 113 | }); 114 | } 115 | 116 | this.controller = controllerName; 117 | }); 118 | 119 | el.addEventListener('brushsize-changed', function (evt) { self.changeBrushSize(evt.detail.size); }); 120 | el.addEventListener('brushcolor-changed', function (evt) { self.changeBrushColor(evt.detail.color); }); 121 | 122 | function createTexture (texture) { 123 | var material = self.highLightMaterial = new THREE.MeshBasicMaterial(); 124 | material.map = texture; 125 | material.needsUpdate = true; 126 | } 127 | el.sceneEl.systems.material.loadTexture(highLightTextureUrl, {src: highLightTextureUrl}, createTexture); 128 | 129 | this.startAxis = 0; 130 | 131 | this.numberStrokes = 0; 132 | 133 | document.addEventListener('stroke-started', function (event) { 134 | if (event.detail.entity.components['paint-controls'] !== self) { return; } 135 | 136 | self.numberStrokes++; 137 | self.system.numberStrokes++; 138 | 139 | // 3 Strokes to hide 140 | if (self.system.numberStrokes === 3) { 141 | const tooltips = Array.prototype.slice.call(document.querySelectorAll('[tooltip]')); 142 | tooltips.forEach(function (tooltip) { 143 | tooltip.emit('tooltip-fade'); 144 | }); 145 | } 146 | }); 147 | }, 148 | 149 | changeBrushColor: function (color) { 150 | if (this.modelLoaded && !!this.buttonMeshes.sizeHint) { 151 | this.buttonMeshes.colorTip.material.color.copy(color); 152 | this.buttonMeshes.sizeHint.material.color.copy(color); 153 | } 154 | }, 155 | 156 | changeBrushSize: function (size) { 157 | var scale = size / 2 * 10; 158 | if (this.modelLoaded && !!this.buttonMeshes.sizeHint) { 159 | this.buttonMeshes.sizeHint.scale.set(scale, 1, scale); 160 | } 161 | }, 162 | 163 | // buttonId 164 | // 0 - trackpad 165 | // 1 - trigger ( intensity value from 0.5 to 1 ) 166 | // 2 - grip 167 | // 3 - menu ( dispatch but better for menu options ) 168 | // 4 - system ( never dispatched on this layer ) 169 | mapping: { 170 | axis0: 'trackpad', 171 | axis1: 'trackpad', 172 | button0: 'trackpad', 173 | button1: 'trigger', 174 | button2: 'grip', 175 | button3: 'menu', 176 | button4: 'system' 177 | }, 178 | 179 | update: function () { 180 | var data = this.data; 181 | var el = this.el; 182 | el.setAttribute('vive-controls', {hand: data.hand, model: false}); 183 | el.setAttribute('oculus-touch-controls', {hand: data.hand, model: true}); 184 | el.setAttribute('windows-motion-controls', {hand: data.hand}); 185 | }, 186 | 187 | play: function () { 188 | }, 189 | 190 | pause: function () { 191 | }, 192 | 193 | onEnterVR: function () { 194 | this.el.object3D.visible = true; 195 | }, 196 | 197 | onModelLoaded: function (evt) { 198 | // Only act on lone brush tip or custom model to set the button meshes, ignore anything else. 199 | if ((evt.target !== this.el && !evt.target.id.includes('-tip')) || this.buttonMeshes) { return; } 200 | 201 | var controllerObject3D = evt.detail.model; 202 | var buttonMeshes; 203 | 204 | buttonMeshes = this.buttonMeshes = {}; 205 | 206 | buttonMeshes.sizeHint = controllerObject3D.getObjectByName('sizehint'); 207 | buttonMeshes.colorTip = controllerObject3D.getObjectByName('tip'); 208 | 209 | this.modelLoaded = true; 210 | 211 | this.changeBrushSize(this.el.components.brush.data.size); 212 | this.changeBrushColor(this.el.components.brush.color); 213 | }, 214 | 215 | onButtonEvent: function (id, evtName) { 216 | var buttonName = this.mapping['button' + id]; 217 | this.el.emit(buttonName + evtName); 218 | this.updateModel(buttonName, evtName); 219 | }, 220 | 221 | updateModel: function (buttonName, state) { 222 | var material = state === 'up' ? this.material : this.highLightMaterial; 223 | var buttonMeshes = this.buttonMeshes; 224 | var button = buttonMeshes && buttonMeshes[buttonName]; 225 | if (state === 'down' && button && !this.material) { 226 | material = this.material = button.material; 227 | } 228 | if (!material) { return; } 229 | if (buttonName === 'grip') { 230 | buttonMeshes.grip.left.material = material; 231 | buttonMeshes.grip.right.material = material; 232 | return; 233 | } 234 | if (!button) { return; } 235 | button.material = material; 236 | } 237 | }); 238 | -------------------------------------------------------------------------------- /src/components/ui-raycaster.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | /** 3 | * Raycaster component. 4 | * 5 | * Pass options to three.js Raycaster including which objects to test. 6 | * Poll for intersections. 7 | * Emit event on origin entity and on target entity on intersect. 8 | * 9 | * @member {array} intersectedEls - List of currently intersected entities. 10 | * @member {array} objects - Cached list of meshes to intersect. 11 | * @member {number} prevCheckTime - Previous time intersection was checked. To help interval. 12 | * @member {object} raycaster - three.js Raycaster. 13 | */ 14 | AFRAME.registerComponent('ui-raycaster', { 15 | schema: { 16 | far: {default: Infinity}, // Infinity. 17 | interval: {default: 100}, 18 | near: {default: 0}, 19 | objects: {default: ''}, 20 | recursive: {default: true}, 21 | rotation: {default: 0} 22 | }, 23 | 24 | init: function () { 25 | this.direction = new THREE.Vector3(); 26 | this.intersectedEls = []; 27 | this.objects = null; 28 | this.prevCheckTime = undefined; 29 | this.raycaster = new THREE.Raycaster(); 30 | this.updateOriginDirection(); 31 | this.refreshObjects = this.refreshObjects.bind(this); 32 | }, 33 | 34 | play: function () { 35 | this.el.sceneEl.addEventListener('child-attached', this.refreshObjects); 36 | this.el.sceneEl.addEventListener('child-detached', this.refreshObjects); 37 | }, 38 | 39 | pause: function () { 40 | this.el.sceneEl.removeEventListener('child-attached', this.refreshObjects); 41 | this.el.sceneEl.removeEventListener('child-detached', this.refreshObjects); 42 | }, 43 | 44 | /** 45 | * Create or update raycaster object. 46 | */ 47 | update: function () { 48 | var data = this.data; 49 | var raycaster = this.raycaster; 50 | 51 | // Set raycaster properties. 52 | raycaster.far = data.far; 53 | raycaster.near = data.near; 54 | 55 | this.refreshObjects(); 56 | }, 57 | 58 | /** 59 | * Update list of objects to test for intersection. 60 | */ 61 | refreshObjects: function () { 62 | var data = this.data; 63 | var i; 64 | var objectEls; 65 | 66 | // Push meshes onto list of objects to intersect. 67 | if (data.objects) { 68 | objectEls = this.el.sceneEl.querySelectorAll(data.objects); 69 | this.objects = []; 70 | for (i = 0; i < objectEls.length; i++) { 71 | this.objects.push(objectEls[i].object3D); 72 | } 73 | return; 74 | } 75 | 76 | // If objects not defined, intersect with everything. 77 | this.objects = this.el.sceneEl.object3D.children; 78 | }, 79 | 80 | /** 81 | * Check for intersections and cleared intersections on an interval. 82 | */ 83 | tick: function (time) { 84 | var el = this.el; 85 | var data = this.data; 86 | var intersectedEls; 87 | var intersections; 88 | var prevCheckTime = this.prevCheckTime; 89 | var prevIntersectedEls; 90 | 91 | // Only check for intersection if interval time has passed. 92 | if (prevCheckTime && (time - prevCheckTime < data.interval)) { return; } 93 | 94 | // Store old previously intersected entities. 95 | prevIntersectedEls = this.intersectedEls.slice(); 96 | 97 | // Raycast. 98 | this.updateOriginDirection(); 99 | intersections = this.raycaster.intersectObjects(this.objects, data.recursive); 100 | 101 | // Only keep intersections against objects that have a reference to an entity. 102 | intersections = intersections.filter(function hasEl (intersection) { 103 | return !!intersection.object.el; 104 | }); 105 | 106 | // Update intersectedEls. 107 | intersectedEls = this.intersectedEls = intersections.map(function getEl (intersection) { 108 | return intersection.object.el; 109 | }); 110 | 111 | // Emit intersected on intersected entity per intersected entity. 112 | intersections.forEach(function emitEvents (intersection) { 113 | var intersectedEl = intersection.object.el; 114 | intersectedEl.emit('raycaster-intersected', {el: el, intersection: intersection}); 115 | }); 116 | 117 | // Emit all intersections at once on raycasting entity. 118 | if (intersections.length) { 119 | el.emit('raycaster-intersection', { 120 | els: intersectedEls, 121 | intersections: intersections 122 | }); 123 | } 124 | 125 | // Emit intersection cleared on both entities per formerly intersected entity. 126 | prevIntersectedEls.forEach(function checkStillIntersected (intersectedEl) { 127 | if (intersectedEls.indexOf(intersectedEl) !== -1) { return; } 128 | el.emit('raycaster-intersection-cleared', {el: intersectedEl}); 129 | intersectedEl.emit('raycaster-intersected-cleared', {el: el}); 130 | }); 131 | }, 132 | 133 | /** 134 | * Set origin and direction of raycaster using entity position and rotation. 135 | */ 136 | updateOriginDirection: (function () { 137 | var directionHelper = new THREE.Quaternion(); 138 | var originVec3 = new THREE.Vector3(); 139 | var scaleDummy = new THREE.Vector3(); 140 | 141 | // Closure to make quaternion/vector3 objects private. 142 | return function updateOriginDirection () { 143 | var el = this.el; 144 | var direction = this.direction; 145 | var object3D = el.object3D; 146 | 147 | // Update matrix world. 148 | object3D.updateMatrixWorld(); 149 | // Grab the position and rotation. 150 | object3D.matrixWorld.decompose(originVec3, directionHelper, scaleDummy); 151 | // Apply rotation to a 0, 0, -1 vector. 152 | direction.set(0, 0, -1); 153 | direction.applyAxisAngle(new THREE.Vector3(1, 0, 0), (this.data.rotation / 360) * 2 * Math.PI); 154 | direction.applyQuaternion(directionHelper); 155 | 156 | this.raycaster.set(originVec3, direction); 157 | }; 158 | })() 159 | }); 160 | -------------------------------------------------------------------------------- /src/components/vr-analytics.js: -------------------------------------------------------------------------------- 1 | AFRAME.registerComponent('vr-analytics', { 2 | init: function () { 3 | var el = this.el; 4 | var emitted = false; 5 | 6 | el.addEventListener('enter-vr', function () { 7 | if (emitted || !AFRAME.utils.device.checkHeadsetConnected() || 8 | AFRAME.utils.device.isMobile()) { return; } 9 | ga('send', 'event', 'General', 'entervr'); 10 | emitted = true; 11 | }); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/dragndrop.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME Image FileReader */ 2 | window.addEventListener('load', function (event) { 3 | var dropArea = document.body; 4 | 5 | dropArea.addEventListener('dragover', function (event) { 6 | event.stopPropagation(); 7 | event.preventDefault(); 8 | event.dataTransfer.dropEffect = 'copy'; 9 | }, false); 10 | 11 | dropArea.addEventListener('drop', function (event) { 12 | event.stopPropagation(); 13 | event.preventDefault(); 14 | 15 | // for each dropped file 16 | var files = event.dataTransfer.files; 17 | for (var i = 0; i < files.length; i++) { 18 | var file = files[i]; 19 | 20 | if (file.name.substring(file.name.length - 4).toLowerCase() === '.apa') { 21 | // a-painter binary 22 | var reader = new FileReader(); 23 | 24 | // file read, parse obj and add to the scene 25 | reader.onload = function (event) { 26 | document.querySelector('a-scene').systems.brush.loadBinary(event.target.result); 27 | }; 28 | reader.readAsArrayBuffer(file); 29 | } 30 | else if (file.name.substring(file.name.length - 5).toLowerCase() === '.json') { 31 | // a-painter json 32 | var reader = new FileReader(); 33 | 34 | // file read, parse obj and add to the scene 35 | reader.onload = function (event) { 36 | document.querySelector('a-scene').systems.brush.loadJSON(JSON.parse(event.target.result)); 37 | }; 38 | reader.readAsText(file); 39 | } 40 | else if (file.name.substring(file.name.length - 4).toLowerCase() === '.obj') { 41 | // OBJs 42 | reader = new FileReader(); 43 | 44 | // file read, parse obj and add to the scene 45 | reader.onload = function (event) { 46 | var objloader = new AFRAME.THREE.OBJLoader(); 47 | var mesh = objloader.parse(event.target.result); 48 | 49 | var entity = document.createElement('a-entity'); 50 | // set all mesh objects to dark gray 51 | for (var o = 0; o < mesh.children.length; o++) { 52 | var child = mesh.children[o]; 53 | if (child.material.constructor === Array) { 54 | child.material.forEach(mat => { 55 | mat.color.set('#333'); 56 | }) 57 | } else { 58 | child.material.color.set('#333'); 59 | } 60 | } 61 | // add mesh to entity 62 | entity.setObject3D('mesh', mesh); 63 | entity.className = 'templateitem'; 64 | document.querySelector('a-scene').appendChild(entity); 65 | }; 66 | reader.readAsText(file); 67 | } else if (file.type.match(/image.*/)) { 68 | // dropping images 69 | reader = new FileReader(); 70 | reader.onload = function (event) { 71 | // create img to get its size 72 | var img = new Image(); 73 | img.src = event.target.result; 74 | 75 | img.onload = () => { 76 | // find good image size 77 | var width, height; 78 | if (img.width > img.height) { 79 | width = 1.0; 80 | height = img.height / img.width; 81 | } else { 82 | height = 1.0; 83 | width = img.width / img.height; 84 | } 85 | 86 | // find a random position in a side of the room 87 | var pos = [Math.random() * 3 - 1.5, 1 + Math.random() - 0.5, -1.4 + Math.random() * 0.2]; 88 | 89 | // create a-image entity and set attributes 90 | var entity = document.createElement('a-image'); 91 | entity.setAttribute('src', event.target.result); 92 | entity.setAttribute('position', pos.join(' ')); 93 | entity.setAttribute('width', width); 94 | entity.setAttribute('height', height); 95 | entity.className = 'templateitem'; 96 | document.querySelector('a-scene').appendChild(entity); 97 | } 98 | }; 99 | reader.readAsDataURL(file); 100 | } 101 | } 102 | }, false); 103 | }); 104 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | window.saveAs = require('../vendor/saveas.js').saveAs; 2 | 3 | require('./atlas.js'); 4 | require('./dragndrop.js'); 5 | require('./binarymanager.js'); 6 | require('./sharedbuffergeometrymanager.js'); 7 | require('./sharedbuffergeometry.js'); 8 | 9 | require('./utils.js'); 10 | require('./ui2d.js'); 11 | 12 | require('./systems/brush.js'); 13 | require('./systems/ui.js'); 14 | require('./systems/painter.js'); 15 | 16 | require('./components/brush.js'); 17 | require('./components/brush-tip.js'); 18 | require('./components/json-model.js'); 19 | require('./components/paint-controls.js'); 20 | require('./components/ui.js'); 21 | require('./components/ui-raycaster.js'); 22 | require('./components/logo-model.js'); 23 | 24 | require('./brushes/line.js'); 25 | require('./brushes/stamp.js'); 26 | require('./brushes/spheres.js'); 27 | require('./brushes/cubes.js'); 28 | require('./brushes/rainbow.js'); 29 | require('./brushes/single-sphere.js'); 30 | -------------------------------------------------------------------------------- /src/onloaded.js: -------------------------------------------------------------------------------- 1 | module.exports = function (callback) { 2 | if (document.readyState === 'complete' || document.readyState === 'loaded') { 3 | onDomLoaded(); 4 | } else { 5 | document.addEventListener('DOMContentLoaded', onDomLoaded); 6 | } 7 | 8 | function onDomLoaded() { 9 | var sceneEl = document.querySelector('a-scene'); 10 | if (sceneEl.hasLoaded) { 11 | callback(); 12 | } else { 13 | sceneEl.addEventListener('loaded', callback()); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/sharedbuffergeometry.js: -------------------------------------------------------------------------------- 1 | function SharedBufferGeometry (material) { 2 | this.material = material; 3 | 4 | this.maxBufferSize = 1000000; 5 | this.geometries = []; 6 | this.current = null; 7 | this.strip = true; 8 | this.addBuffer(false); 9 | } 10 | 11 | SharedBufferGeometry.prototype = { 12 | restartPrimitive: function () { 13 | if (this.idx.position >= this.current.attributes.position.count) { 14 | this.addBuffer(false); 15 | } else if (this.idx.position !== 0) { 16 | var prev = (this.idx.position - 1) * 3; 17 | var position = this.current.attributes.position.array; 18 | this.addVertex(position[prev++], position[prev++], position[prev++]); 19 | 20 | this.idx.color++; 21 | this.idx.normal++; 22 | this.idx.uv++; 23 | } 24 | }, 25 | 26 | remove: function (prevIdx, idx) { 27 | var pos = this.current.attributes.position.array; 28 | 29 | // Loop through all the attributes: position, color, uv, normal,... 30 | if (this.idx.position > idx.position) { 31 | for (key in this.idx) { 32 | var componentSize = key === 'uv' ? 2 : 3; 33 | var pos = (prevIdx[key]) * componentSize; 34 | var start = (idx[key] + 1) * componentSize; 35 | var end = this.idx[key] * componentSize; 36 | for (var i = start; i < end; i++) { 37 | this.current.attributes[key].array[pos++] = this.current.attributes[key].array[i]; 38 | } 39 | } 40 | } 41 | 42 | for (key in this.idx) { 43 | var diff = (idx[key] - prevIdx[key]); 44 | this.idx[key] -= diff; 45 | } 46 | 47 | this.update(); 48 | }, 49 | 50 | undo: function (prevIdx) { 51 | for (let i = prevIdx.position; i < this.idx.position; i++) { 52 | this.current.attributes.position.setXYZ(i, 0, 0, 0); 53 | this.current.index.setXYZ(i, 0, 0, 0); 54 | } 55 | this.idx = prevIdx; 56 | this.update(); 57 | }, 58 | 59 | addBuffer: function (copyLast) { 60 | var geometry = new THREE.BufferGeometry(); 61 | 62 | var vertices = new Float32Array(this.maxBufferSize * 3); 63 | var indices = new Uint32Array(this.maxBufferSize * 4.5); 64 | var normals = new Float32Array(this.maxBufferSize * 3); 65 | var uvs = new Float32Array(this.maxBufferSize * 2); 66 | var colors = new Float32Array(this.maxBufferSize * 3); 67 | 68 | var mesh = new THREE.Mesh(geometry, this.material); 69 | 70 | mesh.frustumCulled = false; 71 | mesh.vertices = vertices; 72 | 73 | this.object3D = new THREE.Object3D(); 74 | var drawing = document.querySelector('.a-drawing'); 75 | if (!drawing) { 76 | drawing = document.createElement('a-entity'); 77 | drawing.className = "a-drawing"; 78 | document.querySelector('a-scene').appendChild(drawing); 79 | } 80 | drawing.object3D.add(this.object3D); 81 | 82 | this.object3D.add(mesh); 83 | 84 | geometry.setDrawRange(0, 0); 85 | geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3).setUsage(THREE.DynamicDrawUsage)); 86 | geometry.attributes.position.updateRange.count = 0; 87 | geometry.setIndex(new THREE.BufferAttribute(indices, 3).setUsage(THREE.DynamicDrawUsage)); 88 | geometry.index.updateRange.count = 0; 89 | geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2).setUsage(THREE.DynamicDrawUsage)); 90 | geometry.attributes.uv.updateRange.count = 0; 91 | geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3).setUsage(THREE.DynamicDrawUsage)); 92 | geometry.attributes.normal.updateRange.count = 0; 93 | geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3).setUsage(THREE.DynamicDrawUsage)); 94 | geometry.attributes.color.updateRange.count = 0; 95 | 96 | 97 | this.previous = null; 98 | if (this.geometries.length > 0) { 99 | this.previous = this.current; 100 | } 101 | 102 | this.idx = { 103 | position: 0, 104 | uv: 0, 105 | normal: 0, 106 | color: 0 107 | }; 108 | 109 | this.geometries.push(geometry); 110 | this.current = geometry; 111 | 112 | if (this.previous && copyLast) { 113 | var prev = (this.maxBufferSize - 2) * 3; 114 | var col = (this.maxBufferSize - 2) * 3; 115 | var uv = (this.maxBufferSize - 2) * 2; 116 | var norm = (this.maxBufferSize - 2) * 3; 117 | 118 | var position = this.previous.attributes.position.array; 119 | this.addVertex(position[prev++], position[prev++], position[prev++]); 120 | this.addVertex(position[prev++], position[prev++], position[prev++]); 121 | 122 | var normal = this.previous.attributes.normal.array; 123 | this.addNormal(normal[norm++], normal[norm++], normal[norm++]); 124 | this.addNormal(normal[norm++], normal[norm++], normal[norm++]); 125 | 126 | var color = this.previous.attributes.color.array; 127 | this.addColor(color[col++], color[col++], color[col++]); 128 | this.addColor(color[col++], color[col++], color[col++]); 129 | 130 | var uvs = this.previous.attributes.uv.array; 131 | 132 | } 133 | }, 134 | 135 | addColor: function (r, g, b) { 136 | this.current.attributes.color.setXYZ(this.idx.color++, r, g, b); 137 | }, 138 | 139 | addNormal: function (x, y, z) { 140 | this.current.attributes.normal.setXYZ(this.idx.normal++, x, y, z); 141 | }, 142 | 143 | addVertex: function (x, y, z) { 144 | var buffer = this.current.attributes.position; 145 | if (this.idx.position === buffer.count) { 146 | this.addBuffer(true); 147 | buffer = this.current.attributes.position; 148 | } 149 | buffer.setXYZ(this.idx.position++, x, y, z); 150 | if (this.strip) { 151 | if ((this.idx.position + 1) % 2 == 0 && this.idx.position > 1) { 152 | /* Line brushes 153 | 2---3 154 | | \ | 155 | 0---1 156 | {0, 1, 2}, {2, 1, 3} 157 | */ 158 | this.current.index.setXYZ(this.idx.position - 3, this.idx.position - 3, this.idx.position - 2, this.idx.position - 1); 159 | this.current.index.setXYZ(this.idx.position - 2, this.idx.position - 1, this.idx.position - 2, this.idx.position); 160 | } 161 | } 162 | else { 163 | if ((this.idx.position + 1) % 3 == 0) { 164 | /* Stamp brushes 165 | 0---1 0 166 | \ | | \ 167 | 2 3---2 168 | {0, 1, 2}, {2, 3, 0} 169 | */ 170 | this.current.index.setXYZ(this.idx.position, this.idx.position - 2, this.idx.position - 1, this.idx.position); 171 | } 172 | } 173 | }, 174 | 175 | addUV: function (u, v) { 176 | this.current.attributes.uv.setXY(this.idx.uv++, u, v); 177 | }, 178 | 179 | update: function () { 180 | // Draw one less triangle to prevent indexing into blank positions 181 | // on an even-number-positioned undo 182 | this.current.setDrawRange(0, (this.idx.position * 3) - 4); 183 | 184 | this.current.attributes.color.updateRange.count = this.idx.position * 3; 185 | this.current.attributes.color.needsUpdate = true; 186 | this.current.attributes.normal.updateRange.count = this.idx.position * 3; 187 | this.current.attributes.normal.needsUpdate = true; 188 | this.current.attributes.position.updateRange.count = this.idx.position * 3; 189 | this.current.attributes.position.needsUpdate = true; 190 | this.current.attributes.uv.updateRange.count = this.idx.position * 2; 191 | this.current.attributes.uv.needsUpdate = true; 192 | this.current.index.updateRange.count = this.idx.position * 3; 193 | this.current.index.needsUpdate = true; 194 | } 195 | }; 196 | 197 | module.exports = SharedBufferGeometry; 198 | -------------------------------------------------------------------------------- /src/sharedbuffergeometrymanager.js: -------------------------------------------------------------------------------- 1 | var SharedBufferGeometry = require('./sharedbuffergeometry.js'); 2 | 3 | function SharedBufferGeometryManager () { 4 | this.sharedBuffers = {}; 5 | } 6 | 7 | SharedBufferGeometryManager.prototype = { 8 | addSharedBuffer: function (name, material) { 9 | var bufferGeometry = new SharedBufferGeometry(material); 10 | this.sharedBuffers[name] = bufferGeometry; 11 | }, 12 | 13 | getSharedBuffer: function (name) { 14 | return this.sharedBuffers[name]; 15 | } 16 | }; 17 | 18 | module.exports = new SharedBufferGeometryManager(); 19 | -------------------------------------------------------------------------------- /src/systems/brush.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE BinaryManager */ 2 | var VERSION = 1; 3 | 4 | AFRAME.BRUSHES = {}; 5 | 6 | APAINTER_STATS = { 7 | brushes: {} 8 | }; 9 | 10 | AFRAME.registerBrush = function (name, definition, options) { 11 | var proto = {}; 12 | 13 | // Format definition object to prototype object. 14 | Object.keys(definition).forEach(function (key) { 15 | proto[key] = { 16 | value: definition[key], 17 | writable: true 18 | }; 19 | }); 20 | 21 | if (AFRAME.BRUSHES[name]) { 22 | throw new Error('The brush `' + name + '` has been already registered. ' + 23 | 'Check that you are not loading two versions of the same brush ' + 24 | 'or two different brushes of the same name.'); 25 | } 26 | 27 | var BrushInterface = function () {}; 28 | 29 | var defaultOptions = { 30 | spacing: 0, 31 | maxPoints: 0 32 | }; 33 | 34 | BrushInterface.prototype = { 35 | options: Object.assign(defaultOptions, options), 36 | reset: function () {}, 37 | tick: function (timeoffset, delta) {}, 38 | undo: function () {}, 39 | remove: function () {}, 40 | addPoint: function (position, orientation, pointerPosition, pressure, timestamp) {}, 41 | getJSON: function (system) { 42 | var points = []; 43 | for (var i = 0; i < this.data.points.length; i++) { 44 | var point = this.data.points[i]; 45 | points.push({ 46 | 'orientation': Utils.arrayNumbersToFixed(point.orientation.toArray()), 47 | 'position': Utils.arrayNumbersToFixed(point.position.toArray()), 48 | 'pressure': Utils.numberToFixed(point.pressure), 49 | 'timestamp': point.timestamp 50 | }); 51 | } 52 | 53 | return { 54 | brush: { 55 | index: system.getUsedBrushes().indexOf(this.brushName), 56 | color: Utils.arrayNumbersToFixed(this.data.color.toArray()), 57 | size: Utils.numberToFixed(this.data.size), 58 | }, 59 | points: points 60 | }; 61 | }, 62 | getBinary: function (system) { 63 | // Color = 3*4 = 12 64 | // NumPoints = 4 65 | // Brush index = 1 66 | // ----------- = 21 67 | // [Point] = vector3 + quat + pressure + timestamp = (3+4+1+1)*4 = 36 68 | 69 | var bufferSize = 21 + (36 * this.data.points.length); 70 | var binaryManager = new BinaryManager(new ArrayBuffer(bufferSize)); 71 | binaryManager.writeUint8(system.getUsedBrushes().indexOf(this.brushName)); // brush index 72 | binaryManager.writeColor(this.data.color); // color 73 | binaryManager.writeFloat32(this.data.size); // brush size 74 | 75 | // Number of points 76 | binaryManager.writeUint32(this.data.points.length); 77 | 78 | // Points 79 | for (var i = 0; i < this.data.points.length; i++) { 80 | var point = this.data.points[i]; 81 | binaryManager.writeFloat32Array(point.position.toArray()); 82 | binaryManager.writeFloat32Array(point.orientation.toArray()); 83 | binaryManager.writeFloat32(point.pressure); 84 | binaryManager.writeUint32(point.timestamp); 85 | } 86 | return binaryManager.getDataView(); 87 | } 88 | }; 89 | 90 | function wrapInit (initMethod) { 91 | return function init (color, brushSize, owner, timestamp) { 92 | this.object3D = new THREE.Object3D(); 93 | this.data = { 94 | points: [], 95 | size: brushSize, 96 | prevPosition: null, 97 | prevPointerPosition: null, 98 | numPoints: 0, 99 | color: color.clone(), 100 | timestamp: timestamp, 101 | owner: owner 102 | }; 103 | initMethod.call(this, color, brushSize); 104 | }; 105 | } 106 | 107 | function wrapAddPoint (addPointMethod) { 108 | return function addPoint (position, orientation, pointerPosition, pressure, timestamp) { 109 | if ((this.data.prevPosition && this.data.prevPosition.distanceTo(position) <= this.options.spacing) || 110 | this.options.maxPoints !== 0 && this.data.numPoints >= this.options.maxPoints) { 111 | return; 112 | } 113 | if (addPointMethod.call(this, position, orientation, pointerPosition, pressure, timestamp)) { 114 | this.data.numPoints++; 115 | this.data.points.push({ 116 | 'position': position.clone(), 117 | 'orientation': orientation.clone(), 118 | 'pressure': pressure, 119 | 'timestamp': timestamp 120 | }); 121 | 122 | this.data.prevPosition = position.clone(); 123 | this.data.prevPointerPosition = pointerPosition.clone(); 124 | } 125 | }; 126 | } 127 | 128 | var NewBrush = function () {}; 129 | NewBrush.prototype = Object.create(BrushInterface.prototype, proto); 130 | NewBrush.prototype.brushName = name; 131 | NewBrush.prototype.constructor = NewBrush; 132 | NewBrush.prototype.init = wrapInit(NewBrush.prototype.init); 133 | NewBrush.prototype.addPoint = wrapAddPoint(NewBrush.prototype.addPoint); 134 | AFRAME.BRUSHES[name] = NewBrush; 135 | 136 | // console.log('New brush registered `' + name + '`'); 137 | NewBrush.used = false; // Used to know which brushes have been used on the drawing 138 | return NewBrush; 139 | }; 140 | 141 | AFRAME.registerSystem('brush', { 142 | schema: {}, 143 | brushes: {}, 144 | strokes: [], 145 | getUsedBrushes: function () { 146 | return Object.keys(AFRAME.BRUSHES) 147 | .filter(function (name) { return AFRAME.BRUSHES[name].used; }); 148 | }, 149 | getBrushByName: function (name) { 150 | return AFRAME.BRUSHES[name]; 151 | }, 152 | undo: function () { 153 | var stroke; 154 | for (var i = this.strokes.length - 1; i >= 0; i--) { 155 | if (this.strokes[i].data.owner !== 'local') continue; 156 | stroke = this.strokes.splice(i, 1)[0]; 157 | break; 158 | } 159 | if (stroke) { 160 | stroke.undo(); 161 | var drawing = document.querySelector('.a-drawing'); 162 | drawing.emit('stroke-removed', {stroke: stroke}); 163 | } 164 | }, 165 | removeById: function (order) { 166 | order = 1; 167 | var targetStroke = this.strokes[order]; 168 | console.log(targetStroke, this.strokes); 169 | if (targetStroke) { 170 | for (var i = this.strokes.length - 1; i > order; i--) { 171 | stroke = this.strokes[i]; 172 | if (targetStroke.sharedBuffer === stroke.sharedBuffer) { 173 | // Update idx and prevIdx 174 | console.log('>>>', stroke.prevIdx, '->', stroke.idx, 'target', targetStroke.prevIdx, '->', targetStroke.idx); 175 | for (key in targetStroke.idx) { 176 | var diff = (targetStroke.idx[key] - targetStroke.prevIdx[key]); 177 | stroke.idx[key] -= diff; 178 | stroke.prevIdx[key] -= diff; 179 | } 180 | console.log('<<<', stroke.idx, stroke.prevIdx); 181 | } 182 | } 183 | this.strokes.splice(order, 1)[0].remove(); 184 | } 185 | }, 186 | clear: function () { 187 | // Remove all the stroke entities 188 | for (var i = this.strokes.length - 1; i >= 0; i--) { 189 | if(this.strokes[i].data.owner !== 'local') continue; 190 | var stroke = this.strokes[i]; 191 | stroke.undo(); 192 | var drawing = document.querySelector('.a-drawing'); 193 | drawing.emit('stroke-removed', { stroke: stroke }); 194 | } 195 | 196 | // Reset the used brushes 197 | Object.keys(AFRAME.BRUSHES).forEach(function (name) { 198 | AFRAME.BRUSHES[name].used = false; 199 | }); 200 | 201 | this.strokes = []; 202 | }, 203 | init: function () { 204 | this.version = VERSION; 205 | this.clear(); 206 | this.controllerName = null; 207 | 208 | var self = this; 209 | this.sceneEl.addEventListener('controllerconnected', function (evt) { 210 | self.controllerName = evt.detail.name; 211 | }); 212 | }, 213 | tick: function (time, delta) { 214 | if (!this.strokes.length) { return; } 215 | for (var i = 0; i < this.strokes.length; i++) { 216 | this.strokes[i].tick(time, delta); 217 | } 218 | }, 219 | generateTestLines: function () { 220 | function randNeg() { return 2 * Math.random() - 1; } 221 | var z = -2; 222 | var size = 0.5; 223 | var width = 3; 224 | var pressure = 1; 225 | var numPoints = 4; 226 | 227 | var steps = width / numPoints; 228 | var numStrokes = 1; 229 | var brushesNames = Object.keys(AFRAME.BRUSHES); 230 | 231 | brushesNames2 = [ 232 | 'leaf1', 233 | 'fur2', 234 | 'star', 235 | 'squared-textured', 236 | 'flat', 237 | 'squared-textured', 238 | 'lines5' 239 | ]; 240 | 241 | var x = -(size + 0.1) * brushesNames.length / 2; 242 | x= 0; 243 | var y = 0; 244 | brushesNames.forEach(function (brushName) { 245 | var color = new THREE.Color(Math.random(), Math.random(), Math.random()); 246 | 247 | var stroke = this.addNewStroke(brushName, color, size); 248 | var entity = document.querySelector('#left-hand'); 249 | entity.emit('stroke-started', { entity: entity, stroke: stroke }); 250 | 251 | var position = new THREE.Vector3(x, y, z); 252 | var aux = new THREE.Vector3(); 253 | 254 | for (var i = 0; i < numPoints; i++) { 255 | var orientation = new THREE.Quaternion(); 256 | aux.set(0, steps, 0.1); 257 | var euler = new THREE.Euler(0, Math.PI, 0); 258 | orientation.setFromEuler(euler); 259 | position = position.add(aux); 260 | var timestamp = 0; 261 | 262 | var pointerPosition = this.getPointerPosition(position, orientation); 263 | stroke.addPoint(position, orientation, pointerPosition, pressure, timestamp); 264 | } 265 | 266 | x+= size + 0.1; 267 | }); 268 | }, 269 | generateRandomStrokes: function (numStrokes) { 270 | function randNeg () { return 2 * Math.random() - 1; } 271 | 272 | var entity = document.querySelector('#left-hand'); 273 | 274 | var brushesNames = Object.keys(AFRAME.BRUSHES); 275 | 276 | for (var l = 0; l < numStrokes; l++) { 277 | //var brushName = brushesNames[parseInt(Math.random() * 30)]; 278 | var brushName = brushesNames[parseInt(Math.random() * 13)]; 279 | var color = new THREE.Color(Math.random(), Math.random(), Math.random()); 280 | var size = Math.random() * 0.3; 281 | var numPoints = parseInt(Math.random() * 500); 282 | 283 | var stroke = this.addNewStroke(brushName, color, size); 284 | entity.emit('stroke-started', {entity: entity, stroke: stroke}); 285 | 286 | var position = new THREE.Vector3(randNeg(), randNeg(), randNeg()); 287 | var aux = new THREE.Vector3(); 288 | var orientation = new THREE.Quaternion(); 289 | 290 | var pressure = 0.2; 291 | for (var i = 0; i < numPoints; i++) { 292 | aux.set(randNeg(), randNeg(), randNeg()); 293 | aux.multiplyScalar(randNeg() / 20); 294 | orientation.setFromUnitVectors(position.clone().normalize(), aux.clone().normalize()); 295 | position = position.add(aux); 296 | if (position.y < 0) { 297 | position.y = -position.y; 298 | } 299 | var timestamp = 0; 300 | pressure += 1 - 2 * Math.random(); 301 | if (pressure < 0) pressure = 0.2; 302 | if (pressure > 1) pressure = 1; 303 | 304 | var pointerPosition = this.getPointerPosition(position, orientation); 305 | stroke.addPoint(position, orientation, pointerPosition, pressure, timestamp); 306 | } 307 | } 308 | }, 309 | addNewStroke: function (brushName, color, size, owner, timestamp) { 310 | if (!APAINTER_STATS.brushes[brushName]) { 311 | APAINTER_STATS.brushes[brushName] = 0; 312 | } 313 | APAINTER_STATS.brushes[brushName]++; 314 | 315 | owner = owner || 'local'; 316 | timestamp = timestamp || Date.now(); 317 | var Brush = this.getBrushByName(brushName); 318 | if (!Brush) { 319 | var newBrushName = Object.keys(AFRAME.BRUSHES)[0]; 320 | Brush = AFRAME.BRUSHES[newBrushName]; 321 | console.warn('Invalid brush name: `' + brushName + '` using `' + newBrushName + '`'); 322 | } 323 | 324 | Brush.used = true; 325 | var stroke = new Brush(); 326 | stroke.brush = Brush; 327 | stroke.init(color, size, owner, timestamp); 328 | this.strokes.push(stroke); 329 | 330 | var drawing = document.querySelector('.a-drawing'); 331 | if (!drawing) { 332 | drawing = document.createElement('a-entity'); 333 | drawing.className = "a-drawing"; 334 | document.querySelector('a-scene').appendChild(drawing); 335 | } 336 | 337 | //var entity = document.createElement('a-entity'); 338 | //entity.className = "a-stroke"; 339 | //drawing.appendChild(entity); 340 | // drawing.object3D.add(stroke.object3D); 341 | //entity.setObject3D('mesh', stroke.object3D); 342 | //stroke.entity = entity; 343 | 344 | return stroke; 345 | }, 346 | getJSON: function () { 347 | // Strokes 348 | var json = { 349 | version: VERSION, 350 | strokes: [], 351 | author: '', 352 | brushes: this.getUsedBrushes() 353 | }; 354 | 355 | for (i = 0; i < this.strokes.length; i++) { 356 | json.strokes.push(this.strokes[i].getJSON(this)); 357 | } 358 | 359 | return json; 360 | }, 361 | getBinary: function () { 362 | var dataViews = []; 363 | var MAGIC = 'apainter'; 364 | 365 | // Used brushes 366 | var usedBrushes = this.getUsedBrushes(); 367 | 368 | // MAGIC(8) + version (2) + usedBrushesNum(2) + usedBrushesStrings(*) 369 | var bufferSize = MAGIC.length + usedBrushes.join(' ').length + 9; 370 | var binaryManager = new BinaryManager(new ArrayBuffer(bufferSize)); 371 | 372 | // Header magic and version 373 | binaryManager.writeString(MAGIC); 374 | binaryManager.writeUint16(VERSION); 375 | 376 | binaryManager.writeUint8(usedBrushes.length); 377 | for (var i = 0; i < usedBrushes.length; i++) { 378 | binaryManager.writeString(usedBrushes[i]); 379 | } 380 | 381 | // Number of strokes 382 | binaryManager.writeUint32(this.strokes.length); 383 | dataViews.push(binaryManager.getDataView()); 384 | 385 | // Strokes 386 | for (i = 0; i < this.strokes.length; i++) { 387 | dataViews.push(this.strokes[i].getBinary(this)); 388 | } 389 | return dataViews; 390 | }, 391 | getPointerPosition: (function () { 392 | var pointerPosition = new THREE.Vector3(); 393 | var controllerOffset = { 394 | 'vive-controls': { 395 | vec: { 396 | left: new THREE.Vector3(0, 0.7, 1), 397 | right: new THREE.Vector3(0, 0.7, 1), 398 | }, 399 | mult: -0.03 400 | }, 401 | 'oculus-touch-controls': { 402 | vec: { 403 | left: new THREE.Vector3(-2, 0, 2.8), 404 | right: new THREE.Vector3(2, 0, 2.8) 405 | }, 406 | mult: -0.025 407 | }, 408 | 'windows-motion-controls': { 409 | vec: { 410 | left: new THREE.Vector3(0, 0, 1), 411 | right: new THREE.Vector3(0, 0, 1), 412 | }, 413 | mult: -.12 414 | } 415 | }; 416 | 417 | return function getPointerPosition (position, orientation, hand) { 418 | if (!this.controllerName) { 419 | return position; 420 | } 421 | 422 | var offsets = controllerOffset[this.controllerName]; 423 | var pointer = offsets.vec[hand] 424 | .clone() 425 | .applyQuaternion(orientation) 426 | .normalize() 427 | .multiplyScalar(offsets.mult); 428 | pointerPosition.copy(position).add(pointer); 429 | return pointerPosition; 430 | }; 431 | })(), 432 | loadJSON: function (data) { 433 | if (data.version !== VERSION) { 434 | console.error('Invalid version: ', data.version, '(Expected: ' + VERSION + ')'); 435 | } 436 | 437 | console.time('JSON Loading'); 438 | 439 | var usedBrushes = []; 440 | 441 | for (var i = 0; i < data.strokes.length; i++) { 442 | var strokeData = data.strokes[i]; 443 | var brush = strokeData.brush; 444 | 445 | var stroke = this.addNewStroke( 446 | data.brushes[brush.index], 447 | new THREE.Color().fromArray(brush.color), 448 | brush.size 449 | ); 450 | 451 | for (var j = 0; j < strokeData.points.length; j++) { 452 | var point = strokeData.points[j]; 453 | 454 | var position = new THREE.Vector3().fromArray(point.position); 455 | var orientation = new THREE.Quaternion().fromArray(point.orientation); 456 | var pressure = point.pressure; 457 | var timestamp = point.timestamp; 458 | 459 | var pointerPosition = this.getPointerPosition(position, orientation); 460 | stroke.addPoint(position, orientation, pointerPosition, pressure, timestamp); 461 | } 462 | } 463 | 464 | console.timeEnd('JSON Loading'); 465 | }, 466 | loadBinary: function (buffer) { 467 | var binaryManager = new BinaryManager(buffer); 468 | var magic = binaryManager.readString(); 469 | if (magic !== 'apainter') { 470 | console.error('Invalid `magic` header'); 471 | return; 472 | } 473 | 474 | var version = binaryManager.readUint16(); 475 | if (version !== VERSION) { 476 | console.error('Invalid version: ', version, '(Expected: ' + VERSION + ')'); 477 | } 478 | 479 | console.time('Binary Loading'); 480 | 481 | var numUsedBrushes = binaryManager.readUint8(); 482 | var usedBrushes = []; 483 | for (var b = 0; b < numUsedBrushes; b++) { 484 | usedBrushes.push(binaryManager.readString()); 485 | } 486 | 487 | var numStrokes = binaryManager.readUint32(); 488 | 489 | for (var l = 0; l < numStrokes; l++) { 490 | var brushIndex = binaryManager.readUint8(); 491 | var color = binaryManager.readColor(); 492 | var size = binaryManager.readFloat(); 493 | var numPoints = binaryManager.readUint32(); 494 | 495 | var stroke = this.addNewStroke(usedBrushes[brushIndex], color, size); 496 | 497 | for (var i = 0; i < numPoints; i++) { 498 | var position = binaryManager.readVector3(); 499 | var orientation = binaryManager.readQuaternion(); 500 | var pressure = binaryManager.readFloat(); 501 | var timestamp = binaryManager.readUint32(); 502 | 503 | var pointerPosition = this.getPointerPosition(position, orientation); 504 | stroke.addPoint(position, orientation, pointerPosition, pressure, timestamp); 505 | } 506 | } 507 | console.timeEnd('Binary Loading'); 508 | }, 509 | loadFromUrl: function (url, binary) { 510 | var loader = new THREE.XHRLoader(this.manager); 511 | loader.crossOrigin = 'anonymous'; 512 | if (binary === true) { 513 | loader.setResponseType('arraybuffer'); 514 | } 515 | 516 | var self = this; 517 | 518 | loader.load(url, function (buffer) { 519 | if (binary === true) { 520 | self.loadBinary(buffer); 521 | } else { 522 | self.loadJSON(JSON.parse(buffer)); 523 | } 524 | }); 525 | } 526 | }); 527 | -------------------------------------------------------------------------------- /src/systems/painter.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME Blob uploadcare */ 2 | 3 | var saveAs = require('../../vendor/saveas.js').saveAs; 4 | 5 | AFRAME.registerSystem('painter', { 6 | init: function () { 7 | var mappings = { 8 | behaviours: {}, 9 | mappings: { 10 | painting: { 11 | common: { 12 | 'grip.down': 'undo', 13 | 'trigger.changed': 'paint' 14 | }, 15 | 16 | 'vive-controls': { 17 | 'axismove': 'changeBrushSizeInc', 18 | 'trackpad.touchstart': 'startChangeBrushSize', 19 | 'menu.down': 'toggleMenu', 20 | 21 | // Teleport 22 | 'trackpad.down': 'aim', 23 | 'trackpad.up': 'teleport' 24 | }, 25 | 26 | 'oculus-touch-controls': { 27 | 'axismove': 'changeBrushSizeAbs', 28 | 'abutton.down': 'toggleMenu', 29 | 'xbutton.down': 'toggleMenu', 30 | 31 | // Teleport 32 | 'ybutton.down': 'aim', 33 | 'ybutton.up': 'teleport', 34 | 35 | 'bbutton.down': 'aim', 36 | 'bbutton.up': 'teleport' 37 | }, 38 | 39 | 'windows-motion-controls': { 40 | 'axismove': 'changeBrushSizeAbs', 41 | 'menu.down': 'toggleMenu', 42 | 43 | // Teleport 44 | 'trackpad.down': 'aim', 45 | 'trackpad.up': 'teleport' 46 | }, 47 | } 48 | } 49 | }; 50 | 51 | this.sceneEl.addEventListener('loaded', function() { 52 | AFRAME.registerInputMappings(mappings); 53 | AFRAME.currentInputMapping = 'painting'; 54 | }); 55 | 56 | this.version = '1.2'; 57 | this.brushSystem = this.sceneEl.systems.brush; 58 | this.showTemplateItems = true; 59 | 60 | function getUrlParams () { 61 | var match; 62 | var pl = /\+/g; // Regex for replacing addition symbol with a space 63 | var search = /([^&=]+)=?([^&]*)/g; 64 | var decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')); }; 65 | var query = window.location.search.substring(1); 66 | var urlParams = {}; 67 | 68 | match = search.exec(query); 69 | while (match) { 70 | urlParams[decode(match[1])] = decode(match[2]); 71 | match = search.exec(query); 72 | } 73 | return urlParams; 74 | } 75 | var urlParams = getUrlParams(); 76 | if (urlParams.url || urlParams.urljson) { 77 | var isBinary = urlParams.urljson === undefined; 78 | this.brushSystem.loadFromUrl(urlParams.url || urlParams.urljson, isBinary); 79 | document.getElementById('logo').setAttribute('visible', false); 80 | document.getElementById('acamera').setAttribute('orbit-controls', 'initialPosition: 0 1.6 3'); 81 | document.getElementById('apainter-logo').classList.remove('hidden'); 82 | //document.getElementById('apainter-author').classList.remove('hidden'); // not used yet 83 | } else { // No painting to load, move camera in front of logo 84 | const cameraEl = document.getElementById('acamera') 85 | cameraEl.setAttribute('position', '0 1.6 0'); 86 | } 87 | 88 | if (urlParams.bgcolor !== undefined) { 89 | document.body.style.backgroundColor = '#' + urlParams.bgcolor; 90 | } 91 | if (urlParams.sky !== undefined) { 92 | this.sceneEl.addEventListener('loaded', function (evt) { 93 | if (urlParams.sky === '') { 94 | document.getElementById('sky').setAttribute('visible', false); 95 | } else { 96 | document.getElementById('sky').setAttribute('material', 'src', urlParams.sky); 97 | } 98 | }); 99 | } 100 | if (urlParams.floor !== undefined) { 101 | this.sceneEl.addEventListener('loaded', function (evt) { 102 | if (urlParams.floor === '') { 103 | document.getElementById('ground').setAttribute('visible', false); 104 | } else { 105 | document.getElementById('ground').setAttribute('material', 'src', urlParams.floor); 106 | } 107 | }); 108 | } 109 | 110 | this.paintingStarted = false; 111 | var self = this; 112 | document.addEventListener('stroke-started', function (event) { 113 | if (!self.paintingStarted) { 114 | const logoEl = document.getElementById('logo'); 115 | logoEl.emit('painting-started'); 116 | self.paintingStarted = true; 117 | } 118 | }); 119 | 120 | // @fixme This is just for debug until we'll get some UI 121 | document.addEventListener('keyup', function (event) { 122 | if(event.shiftKey || event.ctrlKey) return; 123 | if (event.keyCode === 8) { 124 | // Undo (Backspace) 125 | self.brushSystem.undo(); 126 | } 127 | if (event.keyCode === 67) { 128 | // Clear (c) 129 | self.brushSystem.clear(); 130 | } 131 | if (event.keyCode === 71) { 132 | // Export to GTF (g) 133 | var drawing = document.querySelector('.a-drawing'); 134 | self.sceneEl.systems['gltf-exporter'].export(drawing); 135 | } 136 | if (event.keyCode === 78) { 137 | // Next brush (n) 138 | var hands = document.querySelectorAll('[paint-controls]'); 139 | var brushesNames = Object.keys(AFRAME.BRUSHES); 140 | var index = brushesNames.indexOf(hands[0].components.brush.data.brush); 141 | index = (index + 1) % brushesNames.length; 142 | [].forEach.call(hands, function (hand) { 143 | hand.setAttribute('brush', 'brush', brushesNames[index]); 144 | }); 145 | } 146 | 147 | if (event.keyCode === 84) { 148 | // Random stroke (t) 149 | self.brushSystem.generateTestLines(); 150 | } 151 | 152 | if (event.keyCode === 82) { 153 | // Random stroke (r) 154 | self.brushSystem.generateRandomStrokes(1); 155 | } 156 | if (event.keyCode === 76) { 157 | // load binary from file (l) 158 | self.brushSystem.loadFromUrl('demo.apa', true); 159 | } 160 | if (event.keyCode === 85) { // u - upload 161 | self.upload(); 162 | } 163 | if (event.keyCode === 86) { // v - save 164 | self.save(); 165 | } 166 | if (event.keyCode === 74) { // j - save json 167 | self.saveJSON(); 168 | } 169 | if (event.keyCode === 79) { // o - toggle template objects+images visibility 170 | self.showTemplateItems = !self.showTemplateItems; 171 | var templateItems = document.querySelectorAll('.templateitem'); 172 | for (var i = 0; i < templateItems.length; i++) { 173 | templateItems[i].setAttribute('visible', self.showTemplateItems); 174 | } 175 | } 176 | if (event.keyCode === 88) { // x remove 2nd 177 | self.brushSystem.removeById(2); 178 | } 179 | }); 180 | 181 | console.info('A-PAINTER Version: ' + this.version); 182 | }, 183 | saveJSON: function () { 184 | var json = this.brushSystem.getJSON(); 185 | var blob = new Blob([JSON.stringify(json)], {type: 'application/json'}); 186 | saveAs(blob, 'demo.json'); 187 | }, 188 | save: function () { 189 | var dataviews = this.brushSystem.getBinary(); 190 | var blob = new Blob(dataviews, {type: 'application/octet-binary'}); 191 | saveAs(blob, 'demo.apa'); 192 | }, 193 | upload: function () { 194 | this.sceneEl.emit('drawing-upload-started'); 195 | var self = this; 196 | 197 | var baseUrl = 'https://aframe.io/a-painter/?url='; 198 | 199 | var dataviews = this.brushSystem.getBinary(); 200 | var blob = new Blob(dataviews, {type: 'application/octet-binary'}); 201 | 202 | const cloudName = 'a-painter'; 203 | const unsignedUploadPreset = 'upload_painting'; 204 | const url = `https://api.cloudinary.com/v1_1/${cloudName}/upload`; 205 | const fd = new FormData(); 206 | 207 | fd.append('upload_preset', unsignedUploadPreset); 208 | fd.append('file', blob); 209 | fetch(url, { method: 'POST', body: fd }).then(res => { 210 | if (!res.ok) { 211 | throw new Error(`Network request failed with status ${res.status}: ${res.statusText}`); 212 | } 213 | return res.json(); 214 | }).then(json => { 215 | console.log('Uploaded link: ', baseUrl + json.secure_url); 216 | self.sceneEl.emit('drawing-upload-completed', { url: baseUrl + json.secure_url }); 217 | }).catch(err => { 218 | console.error(err); 219 | self.sceneEl.emit('drawing-upload-error', { errorInfo: err }); 220 | }) 221 | } 222 | }); 223 | -------------------------------------------------------------------------------- /src/systems/ui.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME */ 2 | AFRAME.registerSystem('ui', { 3 | init: function () { 4 | this.initTextures(); 5 | }, 6 | 7 | initTextures: function () { 8 | var self = this; 9 | var hoverTextureUrl = 'assets/images/ui-hover.png'; 10 | var pressedTextureUrl = 'assets/images/ui-pressed.png'; 11 | this.sceneEl.systems.material.loadTexture(hoverTextureUrl, {src: hoverTextureUrl}, onLoadedHoverTexture); 12 | this.sceneEl.systems.material.loadTexture(pressedTextureUrl, {src: pressedTextureUrl}, onLoadedPressedTexture); 13 | function onLoadedHoverTexture (texture) { 14 | self.hoverTexture = texture; 15 | } 16 | function onLoadedPressedTexture (texture) { 17 | self.pressedTexture = texture; 18 | } 19 | }, 20 | 21 | closeAll: function () { 22 | var els = document.querySelectorAll('[ui]'); 23 | var i; 24 | for (i = 0; i < els.length; i++) { 25 | els[i].components.ui.close(); 26 | } 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/ui2d.js: -------------------------------------------------------------------------------- 1 | /* global Clipboard */ 2 | window.addEventListener('load', function (event) { 3 | var apainterUI = document.getElementById('apainter-ui'); 4 | var shareDiv = document.querySelector('#apainter-ui .share'); 5 | var shareUrl = document.getElementById('apainter-share-url'); 6 | var progressDiv = document.querySelector('#apainter-ui .progress'); 7 | var progressBar = document.querySelector('#apainter-ui .bar'); 8 | document.addEventListener('drawing-upload-completed', function (event) { 9 | shareDiv.classList.remove('hide'); 10 | progressDiv.classList.add('hide'); 11 | shareUrl.value = event.detail.url; 12 | }); 13 | 14 | document.addEventListener('drawing-upload-started', function (event) { 15 | apainterUI.style.display = 'block'; 16 | shareDiv.classList.add('hide'); 17 | progressDiv.classList.remove('hide'); 18 | }); 19 | 20 | document.addEventListener('drawing-upload-progress', function (event) { 21 | progressBar.style.width = Math.floor(event.detail.progress * 100) + '%'; 22 | }); 23 | 24 | var clipboard = new Clipboard('.button.copy'); 25 | clipboard.on('error', function (e) { 26 | console.error('Error copying to clipboard:', e.action, e.trigger); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | window.Utils = (function() { 2 | const DIGITS = 6; 3 | function numberToFixed (number) { 4 | return parseFloat(number.toFixed(DIGITS)); 5 | } 6 | 7 | function arrayNumbersToFixed (array) { 8 | for (var i = 0; i < array.length; i++) { 9 | array[i] = numberToFixed(array[i]); 10 | } 11 | return array; 12 | } 13 | 14 | function getTooltips (controllerName) { 15 | var tooltips; 16 | var tooltipName; 17 | switch (controllerName) { 18 | case 'windows-motion-samsung-controls': { 19 | tooltipName = '.windows-motion-samsung-tooltips'; 20 | break; 21 | } 22 | case 'windows-motion-controls': { 23 | tooltipName = '.windows-motion-tooltips'; 24 | break; 25 | } 26 | case 'oculus-touch-controls': { 27 | tooltipName = '.oculus-tooltips'; 28 | break; 29 | } 30 | case 'vive-controls': { 31 | tooltipName = '.vive-tooltips'; 32 | break; 33 | } 34 | default: { 35 | break; 36 | } 37 | } 38 | 39 | tooltips = Array.prototype.slice.call(document.querySelectorAll(tooltipName)); 40 | return tooltips; 41 | } 42 | 43 | return { 44 | numberToFixed: numberToFixed, 45 | arrayNumbersToFixed: arrayNumbersToFixed, 46 | getTooltips: getTooltips 47 | } 48 | }()); 49 | -------------------------------------------------------------------------------- /vendor/aframe-input-mapping-component.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // getDefaultExport function for compatibility with non-harmony modules 48 | /******/ __webpack_require__.n = function(module) { 49 | /******/ var getter = module && module.__esModule ? 50 | /******/ function getDefault() { return module['default']; } : 51 | /******/ function getModuleExports() { return module; }; 52 | /******/ __webpack_require__.d(getter, 'a', getter); 53 | /******/ return getter; 54 | /******/ }; 55 | /******/ 56 | /******/ // Object.prototype.hasOwnProperty.call 57 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 58 | /******/ 59 | /******/ // __webpack_public_path__ 60 | /******/ __webpack_require__.p = ""; 61 | /******/ 62 | /******/ // Load entry module and return exports 63 | /******/ return __webpack_require__(__webpack_require__.s = 0); 64 | /******/ }) 65 | /************************************************************************/ 66 | /******/ ([ 67 | /* 0 */ 68 | /***/ (function(module, exports, __webpack_require__) { 69 | 70 | "use strict"; 71 | 72 | 73 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 74 | 75 | /* global AFRAME */ 76 | 77 | if (typeof AFRAME === 'undefined') { 78 | throw new Error('Component attempted to register before AFRAME was available.'); 79 | } 80 | 81 | __webpack_require__(1); 82 | __webpack_require__(6); 83 | 84 | AFRAME.currentInputMapping = null; 85 | AFRAME.inputMappings = {}; 86 | AFRAME.inputActions = {}; 87 | 88 | var behaviour = { 89 | trackpad: 'dpad' 90 | }; 91 | 92 | AFRAME.registerSystem('input-mapping', { 93 | mappings: {}, 94 | mappingsPerControllers: {}, 95 | loadedControllers: [], 96 | 97 | init: function init() { 98 | var self = this; 99 | 100 | this.keyboardHandler = this.keyboardHandler.bind(this); 101 | 102 | this.sceneEl.addEventListener('inputmappingregistered', function () { 103 | self.removeControllersListeners(); 104 | for (var i = 0; i < self.loadedControllers.length; i++) { 105 | var controllerObj = self.loadedControllers[i]; 106 | self.updateControllersListeners(controllerObj); 107 | } 108 | }); 109 | 110 | // Controllers 111 | this.sceneEl.addEventListener('controllerconnected', function (event) { 112 | var matchedController = self.findMatchingController(event.target); 113 | 114 | if (matchedController) { 115 | self.updateControllersListeners(matchedController); 116 | return; 117 | } 118 | 119 | var controllerObj = { 120 | name: event.detail.name, 121 | hand: event.detail.component.data.hand, 122 | element: event.target, 123 | handlers: {}, 124 | activators: {} 125 | }; 126 | self.loadedControllers.push(controllerObj); 127 | 128 | self.updateControllersListeners(controllerObj); 129 | }); 130 | 131 | this.sceneEl.addEventListener('controllerdisconnected', function (event) { 132 | var controller = self.findMatchingController(event.target); 133 | if (controller) { 134 | self.removeControllerListeners(controller); 135 | } 136 | }); 137 | 138 | // Keyboard 139 | this.addKeyboardListeners(); 140 | }, 141 | 142 | findMatchingController: function findMatchingController(matchElement) { 143 | var controller; 144 | var i; 145 | for (i = 0; i < this.loadedControllers.length; i++) { 146 | controller = this.loadedControllers[i]; 147 | if (controller.element === matchElement) { 148 | return controller; 149 | } 150 | } 151 | return undefined; 152 | }, 153 | 154 | addKeyboardListeners: function addKeyboardListeners() { 155 | document.addEventListener('keyup', this.keyboardHandler); 156 | document.addEventListener('keydown', this.keyboardHandler); 157 | document.addEventListener('keypress', this.keyboardHandler); 158 | }, 159 | 160 | removeKeyboardListeners: function removeKeyboardListeners() { 161 | document.removeEventListener('keyup', this.keyboardHandler); 162 | document.removeEventListener('keydown', this.keyboardHandler); 163 | document.removeEventListener('keypress', this.keyboardHandler); 164 | }, 165 | 166 | removeControllerListeners: function removeControllerListeners(controller) { 167 | // Remove events handlers 168 | for (var eventName in controller.handlers) { 169 | var handler = controller.handlers[eventName]; 170 | controller.element.removeEventListener(eventName, handler); 171 | } 172 | controller.handlers = {}; 173 | 174 | // Remove activators 175 | for (var activatorName in controller.activators) { 176 | var activator = controller.activators[activatorName]; 177 | activator.removeListeners(); 178 | } 179 | 180 | controller.activators = {}; 181 | }, 182 | 183 | updateBehaviours: function updateBehaviours(controllerObj) { 184 | var controllerBehaviour = AFRAME.inputBehaviours[controllerObj.name]; 185 | var behavioursPerController = this.mappingsPerControllers[controllerObj.name].behaviours; 186 | if (!behavioursPerController) { 187 | return; 188 | } 189 | for (var button in behavioursPerController) { 190 | var behaviourName = behavioursPerController[button]; 191 | var behaviourDefinition = AFRAME.inputBehaviours[behaviourName]; 192 | if (behaviourDefinition) { 193 | var behaviour = new behaviourDefinition(controllerObj.element, button); 194 | } 195 | } 196 | }, 197 | 198 | updateControllersListeners: function updateControllersListeners(controllerObj) { 199 | this.removeControllerListeners(controllerObj); 200 | 201 | if (!AFRAME.inputMappings) { 202 | console.warn('input-mapping: No mappings defined'); 203 | return; 204 | } 205 | 206 | var mappingsPerController = this.mappingsPerControllers[controllerObj.name] = { 207 | mappings: {}, 208 | behaviours: {} 209 | }; 210 | 211 | // Create the listener for each event 212 | for (var mappingName in AFRAME.inputMappings.mappings) { 213 | var mapping = AFRAME.inputMappings.mappings[mappingName]; 214 | 215 | var commonMappings = mapping.common; 216 | if (commonMappings) { 217 | this.updateMappingsPerController(commonMappings, mappingsPerController, mappingName); 218 | } 219 | 220 | var controllerMappings = mapping[controllerObj.name]; 221 | if (controllerMappings) { 222 | this.updateMappingsPerController(controllerMappings, mappingsPerController, mappingName); 223 | } else { 224 | console.warn('input-mapping: No mappings defined for controller type: ', controllerObj.name); 225 | } 226 | } 227 | 228 | // Mapping the behaviours 229 | for (var mappingName in AFRAME.inputMappings.behaviours) { 230 | var behaviour = AFRAME.inputMappings.behaviours[mappingName]; 231 | 232 | var controllerBehaviours = behaviour[controllerObj.name]; 233 | if (controllerBehaviours) { 234 | this.updateBehavioursPerController(controllerBehaviours, mappingsPerController, mappingName); 235 | } 236 | } 237 | 238 | var self = this; 239 | 240 | var OnActivate = function OnActivate(eventName) { 241 | return function (event) { 242 | var mapping = mappingsPerController.mappings[eventName]; 243 | var mappedEvent = mapping[AFRAME.currentInputMapping]; 244 | if ((typeof mappedEvent === 'undefined' ? 'undefined' : _typeof(mappedEvent)) === 'object') { 245 | // Handedness 246 | var controller = self.findMatchingController(event.target); 247 | mappedEvent = mappedEvent[controller.hand]; 248 | if (!mappedEvent) { 249 | return; 250 | } 251 | } 252 | event.target.emit(mappedEvent, event.detail); 253 | }; 254 | }; 255 | 256 | for (var eventName in mappingsPerController.mappings) { 257 | // Check for activators 258 | if (eventName.indexOf('.') !== -1) { 259 | var aux = eventName.split('.'); 260 | var button = aux[0]; // eg: trackpad 261 | var activatorName = aux[1]; // eg: doublepress 262 | var onActivate = OnActivate(eventName); 263 | var Activator = AFRAME.inputActivators[activatorName]; 264 | controllerObj.activators[eventName] = new Activator(controllerObj.element, button, onActivate); 265 | } 266 | 267 | var onActivate = OnActivate(eventName); 268 | controllerObj.element.addEventListener(eventName, onActivate); 269 | controllerObj.handlers[eventName] = onActivate; 270 | } 271 | 272 | this.updateBehaviours(controllerObj); 273 | }, 274 | 275 | checkValidInputMapping: function checkValidInputMapping() { 276 | if (AFRAME.currentInputMapping === null) { 277 | console.warn('input-mapping: No current input mapping defined.'); 278 | } 279 | }, 280 | 281 | keyboardHandler: function keyboardHandler(event) { 282 | this.checkValidInputMapping(); 283 | if (AFRAME.inputMappings && AFRAME.inputMappings.mappings[AFRAME.currentInputMapping] && AFRAME.inputMappings.mappings[AFRAME.currentInputMapping].keyboard) { 284 | var currentKeyboardMapping = AFRAME.inputMappings.mappings[AFRAME.currentInputMapping].keyboard; 285 | var key = event.keyCode === 32 ? "Space" : event.key; 286 | var keyEvent = (key + "_" + event.type.substr(3)).toLowerCase(); 287 | var mapEvent = currentKeyboardMapping[keyEvent]; 288 | if (mapEvent) { 289 | this.sceneEl.emit(mapEvent); 290 | } 291 | } 292 | }, 293 | 294 | updateBehavioursPerController: function updateBehavioursPerController(behaviours, mappingsPerController, mappingName) { 295 | for (var button in behaviours) { 296 | var behaviour = behaviours[button]; 297 | 298 | if (!mappingsPerController.behaviours[button]) { 299 | mappingsPerController.behaviours[button] = behaviour; 300 | } 301 | } 302 | }, 303 | 304 | updateMappingsPerController: function updateMappingsPerController(mappings, mappingsPerController, mappingName) { 305 | // Generate a mapping for each controller: (Eg: vive-controls.triggerdown.default.paint) 306 | for (var eventName in mappings) { 307 | var mapping = mappings[eventName]; 308 | if (!mappingsPerController.mappings[eventName]) { 309 | mappingsPerController.mappings[eventName] = {}; 310 | } 311 | mappingsPerController.mappings[eventName][mappingName] = mapping; 312 | } 313 | }, 314 | 315 | removeControllersListeners: function removeControllersListeners() { 316 | for (var i = 0; i < this.loadedControllers.length; i++) { 317 | var controller = this.loadedControllers[i]; 318 | this.removeControllerListeners(controller); 319 | } 320 | this.mappingsPerControllers = { 321 | mappings: {}, 322 | behaviours: {} 323 | }; 324 | } 325 | }); 326 | 327 | AFRAME.registerInputActions = function (inputActions, defaultActionSet) { 328 | AFRAME.inputActions = inputActions; 329 | if (typeof defaultActionSet !== 'undefined') { 330 | if (AFRAME.inputActions[defaultActionSet]) { 331 | AFRAME.currentInputMapping = defaultActionSet; 332 | } else { 333 | console.error('input-mapping: trying to set a non registered action set \'' + defaultActionSet + '\''); 334 | } 335 | } 336 | }; 337 | 338 | AFRAME.registerInputMappings = function (data, override) { 339 | if (override || Object.keys(AFRAME.inputMappings).length === 0) { 340 | AFRAME.inputMappings = data; 341 | } else { 342 | // @todo Merge behaviours too 343 | AFRAME.inputMappings.behaviours = data.behaviours; 344 | 345 | // Merge mappings 346 | for (var mappingName in data.mappings) { 347 | var mapping = data.mappings[mappingName]; 348 | if (!AFRAME.inputMappings[mappingName]) { 349 | AFRAME.inputMappings[mappingName] = mapping; 350 | continue; 351 | } 352 | 353 | for (var controllerName in mapping) { 354 | var controllerMapping = mapping[controllerName]; 355 | if (!AFRAME.inputMappings[mappingName][controllerName]) { 356 | AFRAME.inputMappings[mappingName][controllerName] = controllerMapping; 357 | continue; 358 | } 359 | 360 | for (var eventName in controllerMapping) { 361 | AFRAME.inputMappings[mappingName][controllerName][eventName] = controllerMapping[eventName]; 362 | } 363 | } 364 | } 365 | } 366 | 367 | if (!AFRAME.scenes) { 368 | return; 369 | } 370 | 371 | for (var i = 0; i < AFRAME.scenes.length; i++) { 372 | AFRAME.scenes[i].emit('inputmappingregistered'); 373 | } 374 | }; 375 | 376 | /***/ }), 377 | /* 1 */ 378 | /***/ (function(module, exports, __webpack_require__) { 379 | 380 | "use strict"; 381 | 382 | 383 | AFRAME.inputActivators = {}; 384 | 385 | AFRAME.registerInputActivator = function (name, definition) { 386 | AFRAME.inputActivators[name] = definition; 387 | }; 388 | 389 | __webpack_require__(2); 390 | __webpack_require__(3); 391 | __webpack_require__(4); 392 | __webpack_require__(5); 393 | 394 | /***/ }), 395 | /* 2 */ 396 | /***/ (function(module, exports, __webpack_require__) { 397 | 398 | "use strict"; 399 | 400 | 401 | function LongPress(el, button, onActivate) { 402 | this.lastTime = 0; 403 | this.timeOut = 250; 404 | this.eventNameDown = button + 'down'; 405 | this.eventNameUp = button + 'up'; 406 | 407 | this.el = el; 408 | this.onActivate = onActivate; 409 | 410 | this.onButtonDown = this.onButtonDown.bind(this); 411 | this.onButtonUp = this.onButtonUp.bind(this); 412 | 413 | el.addEventListener(this.eventNameDown, this.onButtonDown); 414 | el.addEventListener(this.eventNameUp, this.onButtonUp); 415 | } 416 | 417 | LongPress.prototype = { 418 | onButtonDown: function onButtonDown(event) { 419 | var self = this; 420 | this.pressTimer = window.setTimeout(function () { 421 | self.onActivate(event); 422 | }, 1000); 423 | }, 424 | onButtonUp: function onButtonUp(event) { 425 | clearTimeout(this.pressTimer); 426 | }, 427 | removeListeners: function removeListeners() { 428 | this.el.removeEventListener(this.eventNameDown, this.onButtonDown); 429 | this.el.removeEventListener(this.eventNameUp, this.onButtonUp); 430 | } 431 | }; 432 | 433 | AFRAME.registerInputActivator('longpress', LongPress); 434 | 435 | /***/ }), 436 | /* 3 */ 437 | /***/ (function(module, exports, __webpack_require__) { 438 | 439 | "use strict"; 440 | 441 | 442 | function DoubleTouch(el, button, onActivate) { 443 | this.lastTime = 0; 444 | this.timeOut = 250; 445 | this.eventName = button + 'touchstart'; 446 | this.el = el; 447 | this.onActivate = onActivate; 448 | 449 | this.onButtonDown = this.onButtonDown.bind(this); 450 | 451 | el.addEventListener(this.eventName, this.onButtonDown); 452 | } 453 | 454 | DoubleTouch.prototype = { 455 | onButtonDown: function onButtonDown(event) { 456 | var time = performance.now(); 457 | if (time - this.lastTime < this.timeOut) { 458 | this.onActivate(event); 459 | } else { 460 | this.lastTime = time; 461 | } 462 | }, 463 | removeListeners: function removeListeners() { 464 | this.el.removeEventListener(this.eventName, this.onButtonDown); 465 | } 466 | }; 467 | 468 | AFRAME.registerInputActivator('doubletouch', DoubleTouch); 469 | 470 | /***/ }), 471 | /* 4 */ 472 | /***/ (function(module, exports, __webpack_require__) { 473 | 474 | "use strict"; 475 | 476 | 477 | function DoublePress(el, button, onActivate) { 478 | this.lastTime = 0; 479 | this.timeOut = 250; 480 | this.eventName = button + 'down'; 481 | this.el = el; 482 | this.onActivate = onActivate; 483 | 484 | this.onButtonDown = this.onButtonDown.bind(this); 485 | 486 | el.addEventListener(this.eventName, this.onButtonDown); 487 | } 488 | 489 | DoublePress.prototype = { 490 | onButtonDown: function onButtonDown(event) { 491 | var time = performance.now(); 492 | if (time - this.lastTime < this.timeOut) { 493 | this.onActivate(event); 494 | } else { 495 | this.lastTime = time; 496 | } 497 | }, 498 | removeListeners: function removeListeners() { 499 | this.el.removeEventListener(this.eventName, this.onButtonDown); 500 | } 501 | }; 502 | 503 | AFRAME.registerInputActivator('doublepress', DoublePress); 504 | 505 | /***/ }), 506 | /* 5 */ 507 | /***/ (function(module, exports, __webpack_require__) { 508 | 509 | "use strict"; 510 | 511 | 512 | function createSimpleActivator(suffix) { 513 | return function (el, button, onActivate) { 514 | var eventName = button + suffix; 515 | 516 | el.addEventListener(eventName, onActivate); 517 | this.removeListeners = function () { 518 | el.removeEventListener(eventName, onActivate); 519 | }; 520 | }; 521 | } 522 | 523 | var activators = ['down', 'up', 'touchstart', 'touchend', 'changed', 'move']; 524 | 525 | activators.forEach(function (activatorName) { 526 | AFRAME.registerInputActivator(activatorName, createSimpleActivator(activatorName)); 527 | }); 528 | 529 | /***/ }), 530 | /* 6 */ 531 | /***/ (function(module, exports, __webpack_require__) { 532 | 533 | "use strict"; 534 | 535 | 536 | AFRAME.inputBehaviours = {}; 537 | 538 | AFRAME.registerInputBehaviour = function (name, definition) { 539 | AFRAME.inputBehaviours[name] = definition; 540 | }; 541 | 542 | __webpack_require__(7); 543 | 544 | /***/ }), 545 | /* 7 */ 546 | /***/ (function(module, exports, __webpack_require__) { 547 | 548 | "use strict"; 549 | 550 | 551 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 552 | 553 | function DPad(el, buttonName) { 554 | this.buttonName = buttonName; 555 | this.onButtonPresed = this.onButtonPresed.bind(this); 556 | this.onAxisMove = this.onAxisMove.bind(this); 557 | el.addEventListener('trackpaddown', this.onButtonPresed); 558 | el.addEventListener('trackpadup', this.onButtonPresed); 559 | el.addEventListener('axismove', this.onAxisMove); 560 | this.lastPos = [0, 0]; 561 | this.el = el; 562 | }; 563 | 564 | DPad.prototype = { 565 | onAxisMove: function onAxisMove(event) { 566 | this.lastPos = event.detail.axis; 567 | }, 568 | 569 | onButtonPresed: function onButtonPresed(event) { 570 | var _lastPos = _slicedToArray(this.lastPos, 2), 571 | x = _lastPos[0], 572 | y = _lastPos[1]; 573 | 574 | var state = 'trackpadup'.includes(event.type) ? "up" : "down"; 575 | var centerZone = 0.5; 576 | var direction = state === "up" && this.lastDirection // Always trigger the up event for the last down event 577 | ? this.lastDirection : x * x + y * y < centerZone * centerZone // If within center zone angle does not matter 578 | ? "center" : angleToDirection(Math.atan2(x, y)); 579 | 580 | this.el.emit(this.buttonName + 'dpad' + direction + state); 581 | 582 | if (state === "down") { 583 | this.lastDirection = direction; 584 | } else { 585 | delete this.lastDirection; 586 | } 587 | }, 588 | 589 | removeListeners: function removeListeners() { 590 | el.removeEventListener('trackpaddown', this.onButtonPresed); 591 | el.removeEventListener('trackpadup', this.onButtonPresed); 592 | el.removeEventListener('axismove', this.onAxisMove); 593 | } 594 | }; 595 | 596 | var angleToDirection = function angleToDirection(angle) { 597 | angle = (angle * THREE.Math.RAD2DEG + 180 + 45) % 360; 598 | if (angle > 0 && angle < 90) { 599 | return "down"; 600 | } else if (angle >= 90 && angle < 180) { 601 | return "left"; 602 | } else if (angle >= 180 && angle < 270) { 603 | return "up"; 604 | } else { 605 | return "right"; 606 | } 607 | }; 608 | 609 | AFRAME.registerInputBehaviour('dpad', DPad); 610 | 611 | /***/ }) 612 | /******/ ]); -------------------------------------------------------------------------------- /vendor/aframe-tooltip-component.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = ""; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ (function(module, exports, __webpack_require__) { 46 | 47 | /* global AFRAME */ 48 | __webpack_require__(1); 49 | 50 | if (typeof AFRAME === 'undefined') { 51 | throw new Error('Component attempted to register before AFRAME was available.'); 52 | } 53 | 54 | /** 55 | * Tooltip component for A-Frame. 56 | */ 57 | AFRAME.registerComponent('tooltip', { 58 | schema: { 59 | text: {default: ''}, 60 | end: {type: 'vec3'}, 61 | src: {default: ''}, 62 | rotation: {type: 'vec3'}, 63 | width: {default: 1, min: 0}, 64 | height: {default: 1, min: 0}, 65 | lineColor: {default: '#fff', type: 'color'}, 66 | lineHorizontalAlign: {default: 'left', oneOf: ['left', 'right', 'center']}, 67 | lineVerticalAlign: {default: 'center', oneOf: ['top', 'bottom', 'center']}, 68 | opacity: {default: 1, min: 0, max: 1}, 69 | /* 70 | targetType: {default: 'element', oneOf: ['element', 'position']}, 71 | targetElement: {type: 'selector', if: {targetType: ['element']}}, 72 | */ 73 | targetPosition: {type: 'vec3', if: {targetType: ['position']}} 74 | }, 75 | 76 | /** 77 | * Called once when component is attached. Generally for initial setup. 78 | */ 79 | init: function () { 80 | var el = this.el; 81 | var data = this.data; 82 | 83 | var quad = this.quad = document.createElement('a-entity'); 84 | var self = this; 85 | 86 | quad.addEventListener('loaded', function () { 87 | self.updateTooltip(); 88 | }); 89 | 90 | quad.setAttribute('slice9', {width: data.width, height: data.height, left: 20, right: 43, top: 20, bottom: 43, padding: 0.005, src: data.src}); 91 | quad.setAttribute('rotation', data.rotation); 92 | quad.setAttribute('text', {width: 0.25, color: '#fff', value: data.text, align: 'center'}); 93 | el.appendChild(quad); 94 | 95 | // Line 96 | var material = this.material = new THREE.LineBasicMaterial({color: data.lineColor, opacity: data.opacity, transparent: data.opacity < 1}); 97 | var geometry = this.geometry = new THREE.BufferGeometry(); 98 | geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(2 * 3), 3)); 99 | 100 | this.line = new THREE.Line(geometry, material); 101 | this.el.setObject3D('line', this.line); 102 | 103 | this.el.addEventListener('componentchanged', function (evt) { 104 | self.updateTooltip(); 105 | }); 106 | 107 | this.updateTooltip(); 108 | }, 109 | 110 | /** 111 | * Called when component is attached and when component data changes. 112 | * Generally modifies the entity based on the data. 113 | */ 114 | 115 | updateTooltip: (function () { 116 | var parentPosition = new THREE.Vector3(); 117 | var targetPosition = new THREE.Vector3(); 118 | var startPosition = new THREE.Vector3(); 119 | 120 | return function () { 121 | var data = this.data; 122 | parentPosition.copy(this.el.getAttribute('position')); 123 | targetPosition.copy(data.targetPosition); 124 | 125 | var endPosition = targetPosition.sub(parentPosition); 126 | 127 | var data = this.data; 128 | var valign = {top: data.height / 2, bottom: -data.height / 2, center: 0}; 129 | var halign = {left: -data.width / 2, right: data.width / 2, center: 0}; 130 | 131 | var y = valign[data.lineVerticalAlign]; 132 | var x = halign[data.lineHorizontalAlign]; 133 | 134 | this.quad.setAttribute('slice9', {opacity: data.opacity}); 135 | this.quad.setAttribute('text', {opacity: data.opacity, value: data.text}); 136 | 137 | // Update geometry 138 | this.quad.object3D.updateMatrix(); 139 | startPosition.set(x, y, 0); 140 | startPosition.applyMatrix4(this.quad.object3D.matrix); 141 | 142 | var pos = this.geometry.attributes.position.array; 143 | pos[0] = startPosition.x; 144 | pos[1] = startPosition.y; 145 | pos[2] = startPosition.z; 146 | pos[3] = endPosition.x; 147 | pos[4] = endPosition.y; 148 | pos[5] = endPosition.z; 149 | this.geometry.attributes.position.needsUpdate = true; 150 | 151 | this.material.opacity = data.opacity; 152 | this.material.transparent = data.opacity < 1; 153 | this.material.color.setStyle(data.color); 154 | } 155 | })(), 156 | 157 | update: function () { 158 | this.updateTooltip(); 159 | } 160 | }); 161 | 162 | 163 | /***/ }), 164 | /* 1 */ 165 | /***/ (function(module, exports) { 166 | 167 | /* global AFRAME */ 168 | 169 | if (typeof AFRAME === 'undefined') { 170 | throw new Error('Component attempted to register before AFRAME was available.'); 171 | } 172 | 173 | /** 174 | * Slice9 component for A-Frame. 175 | */ 176 | AFRAME.registerComponent('slice9', { 177 | schema: { 178 | width: {default: 1, min: 0}, 179 | height: {default: 1, min: 0}, 180 | left: {default: 0, min: 0}, 181 | right: {default: 0, min: 0}, 182 | bottom: {default: 0, min: 0}, 183 | top: {default: 0, min: 0}, 184 | side: {default: 'front', oneOf: ['front', 'back', 'double']}, 185 | padding: {default: 0.1, min: 0.01}, 186 | color: {type: 'color', default: '#fff'}, 187 | opacity: {default: 1.0, min: 0, max: 1}, 188 | transparent: {default: true}, 189 | debug: {default: false}, 190 | src: {type: 'map'} 191 | }, 192 | 193 | /** 194 | * Set if component needs multiple instancing. 195 | */ 196 | multiple: false, 197 | 198 | /** 199 | * Called once when component is attached. Generally for initial setup. 200 | */ 201 | init: function () { 202 | var data = this.data; 203 | var material = this.material = new THREE.MeshBasicMaterial({color: data.color, opacity: data.opacity, transparent: data.transparent, wireframe: data.debug}); 204 | var geometry = this.geometry = new THREE.PlaneBufferGeometry(data.width, data.height, 3, 3); 205 | 206 | var textureLoader = new THREE.TextureLoader(); 207 | this.plane = new THREE.Mesh(geometry, material); 208 | this.el.setObject3D('mesh', this.plane); 209 | this.textureSrc = null; 210 | }, 211 | 212 | updateMap: function () { 213 | var src = this.data.src; 214 | 215 | if (src) { 216 | if (src === this.textureSrc) { return; } 217 | // Texture added or changed. 218 | this.textureSrc = src; 219 | this.el.sceneEl.systems.material.loadTexture(src, {src: src}, setMap.bind(this)); 220 | return; 221 | } 222 | 223 | // Texture removed. 224 | if (!this.material.map) { return; } 225 | setMap(null); 226 | 227 | 228 | function setMap (texture) { 229 | this.material.map = texture; 230 | this.material.needsUpdate = true; 231 | this.regenerateMesh(); 232 | } 233 | }, 234 | 235 | regenerateMesh: function () { 236 | var data = this.data; 237 | var pos = this.geometry.attributes.position.array; 238 | var uvs = this.geometry.attributes.uv.array; 239 | var image = this.material.map.image; 240 | 241 | if (!image) {return;} 242 | 243 | /* 244 | 0--1------------------------------2--3 245 | | | | | 246 | 4--5------------------------------6--7 247 | | | | | 248 | | | | | 249 | | | | | 250 | 8--9-----------------------------10--11 251 | | | | | 252 | 12-13----------------------------14--15 253 | */ 254 | function setPos(id, x, y) { 255 | pos[3 * id] = x; 256 | pos[3 * id + 1] = y; 257 | } 258 | 259 | function setUV(id, u, v) { 260 | uvs[2 * id] = u; 261 | uvs[2 * id + 1] = v; 262 | } 263 | 264 | // Update UVS 265 | var uv = { 266 | left: data.left / image.width, 267 | right: data.right / image.width, 268 | top: data.top / image.height, 269 | bottom: data.bottom / image.height 270 | }; 271 | 272 | setUV(1, uv.left, 1); 273 | setUV(2, uv.right, 1); 274 | 275 | setUV(4, 0, uv.bottom); 276 | setUV(5, uv.left, uv.bottom); 277 | setUV(6, uv.right, uv.bottom); 278 | setUV(7, 1, uv.bottom); 279 | 280 | setUV(8, 0, uv.top); 281 | setUV(9, uv.left, uv.top); 282 | setUV(10, uv.right, uv.top); 283 | setUV(11, 1, uv.top); 284 | 285 | setUV(13, uv.left, 0); 286 | setUV(14, uv.right, 0); 287 | 288 | // Update vertex positions 289 | var w2 = data.width / 2; 290 | var h2 = data.height / 2; 291 | var left = -w2 + data.padding; 292 | var right = w2 - data.padding; 293 | var top = h2 - data.padding; 294 | var bottom = -h2 + data.padding; 295 | 296 | setPos(0, -w2, h2); 297 | setPos(1, left, h2); 298 | setPos(2, right, h2); 299 | setPos(3, w2, h2); 300 | 301 | setPos(4, -w2, top); 302 | setPos(5, left, top); 303 | setPos(6, right, top); 304 | setPos(7, w2, top); 305 | 306 | setPos(8, -w2, bottom); 307 | setPos(9, left, bottom); 308 | setPos(10, right, bottom); 309 | setPos(11, w2, bottom); 310 | 311 | setPos(13, left, -h2); 312 | setPos(14, right, -h2); 313 | setPos(12, -w2, -h2); 314 | setPos(15, w2, -h2); 315 | 316 | this.geometry.attributes.position.needsUpdate = true; 317 | this.geometry.attributes.uv.needsUpdate = true; 318 | }, 319 | 320 | /** 321 | * Called when component is attached and when component data changes. 322 | * Generally modifies the entity based on the data. 323 | */ 324 | update: function (oldData) { 325 | var data = this.data; 326 | 327 | this.material.color.setStyle(data.color); 328 | this.material.opacity = data.opacity; 329 | this.material.transparent = data.transparent; 330 | this.material.wireframe = data.debug; 331 | this.material.side = parseSide(data.side); 332 | 333 | var diff = AFRAME.utils.diff(data, oldData); 334 | if ('src' in diff) { 335 | this.updateMap(); 336 | } 337 | else if ('width' in diff || 'height' in diff || 'padding' in diff || 'left' in diff || 'top' in diff || 'bottom' in diff || 'right' in diff) { 338 | this.regenerateMesh(); 339 | } 340 | }, 341 | 342 | /** 343 | * Called when a component is removed (e.g., via removeAttribute). 344 | * Generally undoes all modifications to the entity. 345 | */ 346 | remove: function () { }, 347 | 348 | /** 349 | * Called on each scene tick. 350 | */ 351 | // tick: function (t) { }, 352 | 353 | /** 354 | * Called when entity pauses. 355 | * Use to stop or remove any dynamic or background behavior such as events. 356 | */ 357 | pause: function () { }, 358 | 359 | /** 360 | * Called when entity resumes. 361 | * Use to continue or add any dynamic or background behavior such as events. 362 | */ 363 | play: function () { } 364 | }); 365 | 366 | function parseSide (side) { 367 | switch (side) { 368 | case 'back': { 369 | return THREE.BackSide; 370 | } 371 | case 'double': { 372 | return THREE.DoubleSide; 373 | } 374 | default: { 375 | // Including case `front`. 376 | return THREE.FrontSide; 377 | } 378 | } 379 | } 380 | 381 | 382 | /***/ }) 383 | /******/ ]); -------------------------------------------------------------------------------- /vendor/ga.js: -------------------------------------------------------------------------------- 1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 4 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 5 | ga('create', 'UA-80530812-2', 'auto'); 6 | ga('send', 'pageview'); 7 | -------------------------------------------------------------------------------- /vendor/saveas.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 2 | var saveAs=saveAs||function(e){"use strict";if(typeof e==="undefined"||typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var t=e.document,n=function(){return e.URL||e.webkitURL||e},r=t.createElementNS("http://www.w3.org/1999/xhtml","a"),o="download"in r,i=function(e){var t=new MouseEvent("click");e.dispatchEvent(t)},a=/constructor/i.test(e.HTMLElement),f=/CriOS\/[\d]+/.test(navigator.userAgent),u=function(t){(e.setImmediate||e.setTimeout)(function(){throw t},0)},d="application/octet-stream",s=1e3*40,c=function(e){var t=function(){if(typeof e==="string"){n().revokeObjectURL(e)}else{e.remove()}};setTimeout(t,s)},l=function(e,t,n){t=[].concat(t);var r=t.length;while(r--){var o=e["on"+t[r]];if(typeof o==="function"){try{o.call(e,n||e)}catch(i){u(i)}}}},p=function(e){if(/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)){return new Blob([String.fromCharCode(65279),e],{type:e.type})}return e},v=function(t,u,s){if(!s){t=p(t)}var v=this,w=t.type,m=w===d,y,h=function(){l(v,"writestart progress write writeend".split(" "))},S=function(){if((f||m&&a)&&e.FileReader){var r=new FileReader;r.onloadend=function(){var t=f?r.result:r.result.replace(/^data:[^;]*;/,"data:attachment/file;");var n=e.open(t,"_blank");if(!n)e.location.href=t;t=undefined;v.readyState=v.DONE;h()};r.readAsDataURL(t);v.readyState=v.INIT;return}if(!y){y=n().createObjectURL(t)}if(m){e.location.href=y}else{var o=e.open(y,"_blank");if(!o){e.location.href=y}}v.readyState=v.DONE;h();c(y)};v.readyState=v.INIT;if(o){y=n().createObjectURL(t);setTimeout(function(){r.href=y;r.download=u;i(r);h();c(y);v.readyState=v.DONE});return}S()},w=v.prototype,m=function(e,t,n){return new v(e,t||e.name||"download",n)};if(typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob){return function(e,t,n){t=t||e.name||"download";if(!n){e=p(e)}return navigator.msSaveOrOpenBlob(e,t)}}w.abort=function(){};w.readyState=w.INIT=0;w.WRITING=1;w.DONE=2;w.error=w.onwritestart=w.onprogress=w.onwrite=w.onabort=w.onerror=w.onwriteend=null;return m}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined"&&module.exports){module.exports.saveAs=saveAs}else if(typeof define!=="undefined"&&define!==null&&define.amd!==null){define([],function(){return saveAs})} 3 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const TerserPlugin = require('terser-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: __dirname, 7 | filename: 'build.js' 8 | }, 9 | module: {}, 10 | optimization: { 11 | minimizer: [new TerserPlugin({ 12 | extractComments: false 13 | })] 14 | }, 15 | devServer: { 16 | historyApiFallback: true, 17 | allowedHosts: "all", 18 | static: { 19 | directory: __dirname, 20 | publicPath: '/' 21 | } 22 | } 23 | }; 24 | --------------------------------------------------------------------------------