├── .gitignore
├── README.md
├── browser.js
├── demo.png
├── dist
├── aframe-draw-component.js
└── aframe-draw-component.min.js
├── examples
├── basic
│ └── index.html
├── index.html
└── main.js
├── index.js
├── package.json
└── tests
├── __init.test.js
├── helpers.js
├── index.test.js
└── karma.conf.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .sw[ponm]
2 | examples/build.js
3 | examples/node_modules/
4 | gh-pages
5 | node_modules/
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AFrame Draw Component
2 |
3 | HTML5 canvas as a material for an [A-Frame](https://aframe.io) VR entity.
4 |
5 | 
6 |
7 | Fully extendable with components which utilize the canvas api.
8 |
9 | **Note: if you can't get the component to work with the latest A-Frame version, check out [Mayo](https://github.com/mayognaise/)'s shader variations.**
10 |
11 |
12 | ## Installation
13 |
14 | `npm i aframe-draw-component --save`
15 |
16 | ##Usage
17 |
18 | Register the component with AFrame:
19 |
20 | ```js
21 | var AFRAME = require("aframe-core");
22 | var draw = require("aframe-draw-component").component;
23 | AFRAME.registerComponent("draw", draw);
24 | ```
25 |
26 | Then, you can implement it with third party components which utilize the draw component. Just put it after the `draw` prop, like this example using a `square` component:
27 |
28 | ```html
29 |
30 |
31 | ```
32 |
33 | Or, if you want direct access to the Canvas API, write a quick component yourself:
34 |
35 | ```js
36 | AFRAME.registerComponent("square", {
37 | dependencies: ["draw"],
38 |
39 | update: function() {
40 | var draw = this.el.components.draw; //get access to the draw component
41 | var ctx = draw.ctx;
42 | var canvas = draw.canvas;
43 | ctx.fillStyle = "#AFC5FF";
44 | ctx.fillRect(0, 0, canvas.width, canvas.height);
45 | ctx.fillStyle = "blue";
46 | ctx.fillRect(68, 68, 120, 120);
47 | ctx.fillStyle = "white";
48 | ctx.font = "36px Georgia";
49 | ctx.fillText(this.data.text, 80, 140);
50 | draw.render(); //tell it to update the texture
51 | }
52 | });
53 | ```
54 |
55 | **Note**: the example above is not ideal for third-party components, or when you're using multiple components in your project which use `draw`. See below:
56 |
57 | ##Advanced Usage & Render Binding
58 |
59 | When writing a generic third party component, you will more than likely want to re-render the canvas. This may also require a clearing of the canvas itself. Because of this, it's imperative that every third party component encapsulate their use of the canvas into their own local render functions, and then register these functions into `draw`.
60 |
61 | `draw` can then put every component into a call stack, and then re-render each component in the exact order that they were placed in. This also potentially allows for layering, although a-frame can handle this on its own using component updates pretty well.
62 |
63 | An example of this uses `Object.bind`:
64 |
65 | ```js
66 | AFRAME.registerComponent("square", {
67 | dependencies: ["draw"],
68 | init: function() {
69 | this.draw = this.el.components.draw;
70 | this.draw.register(this.render.bind(this));
71 | },
72 | update: function () {
73 | this.draw.render();
74 | },
75 | render: function () {
76 | var ctx = this.draw.ctx;
77 | var canvas = this.draw.canvas;
78 | ctx.fillStyle = "#AFC5FF";
79 | ctx.fillRect(0, 0, canvas.width, canvas.height);
80 | ctx.fillStyle = this.data.color;
81 | ctx.fillRect(68, 68, 120, 120);
82 | }
83 | });
84 |
85 | AFRAME.registerComponent("greeting", {
86 | dependencies: ["draw"],
87 | init: function() {
88 | this.draw = this.el.components.draw;
89 | this.draw.register(this.render.bind(this));
90 | },
91 | update: function () {
92 | this.draw.render();
93 | },
94 | render: function () {
95 | var ctx = this.draw.ctx;
96 | ctx.fillStyle = "white";
97 | ctx.font = "36px Georgia";
98 | ctx.fillText(this.data.text, 80, 140);
99 | }
100 | });
101 | ```
102 |
103 | After each component is initialized, it registers its own `render` function with `draw`. If its own data is changed (within the `update` function), it will tell `draw` to re-render the entire canvas, and call both the `square` and `greeting`'s `render` functions in order.
104 |
105 | ##Methods & Component Properties
106 |
107 | * These are only needed for writing your own components which utilize `draw`.
108 |
109 | |Property|Description|
110 | |------|-------|
111 | |`.canvas`|hidden canvas to perform methods on|
112 | |`.ctx`|hidden ctx to perform methods on|
113 | |`.register([func] render)`|add your component's own `render` function to the registry so that it will re-render on any render call.|
114 | |`.render()`|update the material with the new canvas|
115 |
116 | ##Properties
117 |
118 | |Property|Description|
119 | |------|-------|
120 | |`width`|width of canvas (should match ratio of a face of the entity)|
121 | |`height`|height of canvas (should match ratio of a face of the entity)|
122 | |`background`|background color of canvas|
123 |
124 | ##Featured Components
125 | * [Text Wrap](https://www.npmjs.com/package/aframe-textwrap-component) - full text wrapping
126 | * [HTML Texture](https://github.com/scenevr/htmltexture-component) - render html as a texture (!!!)
127 |
128 | ##Additional Info
129 |
130 | * Thanks to [ngokevin](https://github.com/ngokevin) and [RangerMauve](https://github.com/RangerMauve) for their help.
131 | * As this is meant to be an extendable component, PR's to the readme are welcome.
132 |
--------------------------------------------------------------------------------
/browser.js:
--------------------------------------------------------------------------------
1 | // Browser distribution of the A-Frame component.
2 | (function () {
3 | if (typeof AFRAME === 'undefined') {
4 | console.error('Component attempted to register before AFRAME was available.');
5 | return;
6 | }
7 |
8 | // Register all components here.
9 | var components = {
10 | draw: require('./index').component
11 | };
12 |
13 | Object.keys(components).forEach(function (name) {
14 | if (AFRAME.aframeCore) {
15 | AFRAME.aframeCore.registerComponent(name, components[name]);
16 | } else {
17 | AFRAME.registerComponent(name, components[name]);
18 | }
19 | });
20 | })();
21 |
--------------------------------------------------------------------------------
/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxkrieger/aframe-draw-component/0bb671cc44b007bbd15a832abd7aff708e01aaff/demo.png
--------------------------------------------------------------------------------
/dist/aframe-draw-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 | // Browser distribution of the A-Frame component.
48 | (function () {
49 | if (typeof AFRAME === 'undefined') {
50 | console.error('Component attempted to register before AFRAME was available.');
51 | return;
52 | }
53 |
54 | // Register all components here.
55 | var components = {
56 | draw: __webpack_require__(1).component
57 | };
58 |
59 | Object.keys(components).forEach(function (name) {
60 | if (AFRAME.aframeCore) {
61 | AFRAME.aframeCore.registerComponent(name, components[name]);
62 | } else {
63 | AFRAME.registerComponent(name, components[name]);
64 | }
65 | });
66 | })();
67 |
68 |
69 | /***/ },
70 | /* 1 */
71 | /***/ function(module, exports) {
72 |
73 | module.exports.component = {
74 | schema: {
75 | width: {
76 | default: 256
77 | },
78 | height: {
79 | default: 256
80 | },
81 | background: {
82 | default: "#FFFFFF"
83 | }
84 | },
85 |
86 | init: function () {
87 | this.registers = []; //order of eventing after render
88 | this.update();
89 | },
90 |
91 | register: function(render) {
92 | this.registers.push(render);
93 | },
94 |
95 | update: function (oldData) {
96 | if (!oldData) this.createCanvas(this.data.width, this.data.height);
97 | },
98 |
99 | createCanvas: function (w, h) {
100 | var _ = this;
101 | var canvas = document.createElement("canvas");
102 | canvas.width = w;
103 | canvas.height = h;
104 | canvas.style = "display: none";
105 | _.canvas = canvas;
106 | _.ctx = canvas.getContext("2d");
107 |
108 | this.texture = new THREE.Texture(_.canvas); //renders straight from a canvas
109 | if(this.el.object3D.children.length > 0) { //backwards compatibility
110 | this.el.object3D.children[0].material = new THREE.MeshBasicMaterial();
111 | this.el.object3D.children[0].material.map = this.texture;
112 | }
113 | else { //backwards compatibility
114 | this.el.object3D.material = new THREE.MeshBasicMaterial();
115 | this.el.object3D.material.map = this.texture;
116 | }
117 | if(!this.el.hasLoaded) this.el.addEventListener("loaded", function() {
118 | _.render();
119 | });
120 | else _.render();
121 | },
122 |
123 | render: function() {
124 | if(this.registers.length > 0) { //backwards compatibility
125 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
126 | this.ctx.fillStyle = this.data.background;
127 | this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
128 | this.registers.forEach(function(item) {
129 | item();
130 | });
131 | }
132 | this.texture.needsUpdate = true;
133 | },
134 |
135 | //not the most removable component out there, so will leave blank for now
136 | remove: function () {}
137 | };
138 |
139 |
140 | /***/ }
141 | /******/ ]);
--------------------------------------------------------------------------------
/dist/aframe-draw-component.min.js:
--------------------------------------------------------------------------------
1 | !function(e){function t(a){if(i[a])return i[a].exports;var n=i[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var i={};return t.m=e,t.c=i,t.p="",t(0)}([function(e,t,i){!function(){if("undefined"==typeof AFRAME)return void console.error("Component attempted to register before AFRAME was available.");var e={draw:i(1).component};Object.keys(e).forEach(function(t){AFRAME.aframeCore?AFRAME.aframeCore.registerComponent(t,e[t]):AFRAME.registerComponent(t,e[t])})}()},function(e,t){e.exports.component={schema:{width:{"default":256},height:{"default":256},background:{"default":"#FFFFFF"}},init:function(){this.registers=[],this.update()},register:function(e){this.registers.push(e)},update:function(e){e||this.createCanvas(this.data.width,this.data.height)},createCanvas:function(e,t){var i=this,a=document.createElement("canvas");a.width=e,a.height=t,a.style="display: none",i.canvas=a,i.ctx=a.getContext("2d"),this.texture=new THREE.Texture(i.canvas),this.el.object3D.children.length>0?(this.el.object3D.children[0].material=new THREE.MeshBasicMaterial,this.el.object3D.children[0].material.map=this.texture):(this.el.object3D.material=new THREE.MeshBasicMaterial,this.el.object3D.material.map=this.texture),this.el.hasLoaded?i.render():this.el.addEventListener("loaded",function(){i.render()})},render:function(){this.registers.length>0&&(this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.fillStyle=this.data.background,this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height),this.registers.forEach(function(e){e()})),this.texture.needsUpdate=!0},remove:function(){}}}]);
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | A-Frame Draw Component
4 |
5 |
22 |
23 |
24 | A-Frame Draw Component
25 | Square
26 |
27 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/examples/main.js:
--------------------------------------------------------------------------------
1 | var AFRAME = require("aframe");
2 | var drawComponent = require("../index.js").component;
3 | AFRAME.registerComponent("draw", drawComponent);
4 | AFRAME.registerComponent("square", {
5 | dependencies: ["draw"],
6 | init: function() {
7 | this.draw = this.el.components.draw;
8 | this.draw.register(this.render.bind(this));
9 | },
10 | update: function () {
11 | this.draw.render();
12 | },
13 | render: function () {
14 | var ctx = this.draw.ctx;
15 | ctx.fillStyle = this.data.color;
16 | ctx.fillRect(68, 68, 120, 120);
17 | }
18 | });
19 |
20 | AFRAME.registerComponent("greeting", {
21 | dependencies: ["draw"],
22 | init: function() {
23 | this.draw = this.el.components.draw;
24 | this.draw.register(this.render.bind(this));
25 | },
26 | update: function () {
27 | this.draw.render();
28 | },
29 | render: function () {
30 | var ctx = this.draw.ctx;
31 | ctx.fillStyle = "white";
32 | ctx.font = "36px Georgia";
33 | ctx.fillText(this.data.text, 80, 140);
34 | }
35 | });
36 |
37 | window.setTimeout(function () { //demonstrates the update closure for the square component
38 | document.querySelector("#sq").setAttribute("greeting", "text", "Hola");
39 | document.querySelector("#sq").setAttribute("square", "color", "red");
40 | }, 2000);
41 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports.component = {
2 | schema: {
3 | width: {
4 | default: 256
5 | },
6 | height: {
7 | default: 256
8 | },
9 | background: {
10 | default: "#FFFFFF"
11 | }
12 | },
13 |
14 | init: function () {
15 | this.registers = []; //order of eventing after render
16 | this.update();
17 | },
18 |
19 | register: function(render) {
20 | this.registers.push(render);
21 | },
22 |
23 | update: function (oldData) {
24 | if (!oldData) this.createCanvas(this.data.width, this.data.height);
25 | },
26 |
27 | createCanvas: function (w, h) {
28 | var _ = this;
29 | var canvas = document.createElement("canvas");
30 | canvas.width = w;
31 | canvas.height = h;
32 | canvas.style = "display: none";
33 | _.canvas = canvas;
34 | _.ctx = canvas.getContext("2d");
35 |
36 | this.texture = new THREE.Texture(_.canvas); //renders straight from a canvas
37 | if(this.el.object3D.children.length > 0) { //backwards compatibility
38 | this.el.object3D.children[0].material = new THREE.MeshBasicMaterial();
39 | this.el.object3D.children[0].material.map = this.texture;
40 | }
41 | else { //backwards compatibility
42 | this.el.object3D.material = new THREE.MeshBasicMaterial();
43 | this.el.object3D.material.map = this.texture;
44 | }
45 | if(!this.el.hasLoaded) this.el.addEventListener("loaded", function() {
46 | _.render();
47 | });
48 | else _.render();
49 | },
50 |
51 | render: function() {
52 | if(this.registers.length > 0) { //backwards compatibility
53 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
54 | this.ctx.fillStyle = this.data.background;
55 | this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
56 | this.registers.forEach(function(item) {
57 | item();
58 | });
59 | }
60 | this.texture.needsUpdate = true;
61 | },
62 |
63 | //not the most removable component out there, so will leave blank for now
64 | remove: function () {}
65 | };
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aframe-draw-component",
3 | "version": "1.5.0",
4 | "description": "HTML5 canvas component for A-Frame VR.",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "browserify examples/main.js -o examples/build.js",
8 | "dev": "budo examples/main.js:build.js --dir examples --port 8000 --live --open",
9 | "dist": "webpack browser.js dist/aframe-draw-component.js && webpack -p browser.js dist/aframe-draw-component.min.js",
10 | "postpublish": "npm run dist",
11 | "preghpages": "npm run build && rm -rf gh-pages && cp -r examples gh-pages",
12 | "ghpages": "npm run preghpages && ghpages -p gh-pages",
13 | "test": "karma start ./tests/karma.conf.js"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/maxkrieger/aframe-draw-component.git"
18 | },
19 | "keywords": [
20 | "aframe",
21 | "aframe-component",
22 | "canvas",
23 | "html5",
24 | "aframe-vr",
25 | "vr",
26 | "mozvr",
27 | "webvr"
28 | ],
29 | "author": "Max Krieger",
30 | "bugs": {
31 | "url": "https://github.com/ngokevin/aframe-draw-component/issues"
32 | },
33 | "homepage": "https://github.com/maxkrieger/aframe-draw-component#readme",
34 | "devDependencies": {
35 | "aframe": "^0.2.0",
36 | "babel-eslint": "^4.1.8",
37 | "browserify": "^12.0.1",
38 | "browserify-css": "^0.8.3",
39 | "budo": "^7.1.0",
40 | "chai": "^3.4.1",
41 | "chai-shallow-deep-equal": "^1.3.0",
42 | "ghpages": "0.0.3",
43 | "karma": "^0.13.15",
44 | "karma-browserify": "^4.4.2",
45 | "karma-chai-shallow-deep-equal": "0.0.4",
46 | "karma-firefox-launcher": "^0.1.7",
47 | "karma-mocha": "^0.2.1",
48 | "karma-mocha-reporter": "^1.1.3",
49 | "karma-sinon-chai": "^1.1.0",
50 | "mocha": "^2.3.4",
51 | "webpack": "^1.12.9"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/__init.test.js:
--------------------------------------------------------------------------------
1 | /* global sinon, setup, teardown */
2 |
3 | /**
4 | * __init.test.js is run before every test case.
5 | */
6 | window.debug = true;
7 |
8 | var AScene = require('aframe-core').AScene;
9 |
10 | beforeEach(function () {
11 | this.sinon = sinon.sandbox.create();
12 | // Stub to not create a WebGL context since Travis CI runs headless.
13 | this.sinon.stub(AScene.prototype, 'attachedCallback');
14 | });
15 |
16 | afterEach(function () {
17 | // Clean up any attached elements.
18 | ['canvas', 'a-assets', 'a-scene'].forEach(function (tagName) {
19 | var els = document.querySelectorAll(tagName);
20 | for (var i = 0; i < els.length; i++) {
21 | els[i].parentNode.removeChild(els[i]);
22 | }
23 | });
24 | AScene.scene = null;
25 |
26 | this.sinon.restore();
27 | });
28 |
--------------------------------------------------------------------------------
/tests/helpers.js:
--------------------------------------------------------------------------------
1 | /* global suite */
2 |
3 | /**
4 | * Helper method to create a scene, create an entity, add entity to scene,
5 | * add scene to document.
6 | *
7 | * @returns {object} An `` element.
8 | */
9 | module.exports.entityFactory = function () {
10 | var scene = document.createElement('a-scene');
11 | var entity = document.createElement('a-entity');
12 | scene.appendChild(entity);
13 | document.body.appendChild(scene);
14 | return entity;
15 | };
16 |
17 | /**
18 | * Creates and attaches a mixin element (and an `` element if necessary).
19 | *
20 | * @param {string} id - ID of mixin.
21 | * @param {object} obj - Map of component names to attribute values.
22 | * @returns {object} An attached `` element.
23 | */
24 | module.exports.mixinFactory = function (id, obj) {
25 | var mixinEl = document.createElement('a-mixin');
26 | mixinEl.setAttribute('id', id);
27 | Object.keys(obj).forEach(function (componentName) {
28 | mixinEl.setAttribute(componentName, obj[componentName]);
29 | });
30 |
31 | var assetsEl = document.querySelector('a-assets');
32 | if (!assetsEl) {
33 | assetsEl = document.createElement('a-assets');
34 | document.body.appendChild(assetsEl);
35 | }
36 | assetsEl.appendChild(mixinEl);
37 |
38 | return mixinEl;
39 | };
40 |
41 | /**
42 | * Test that is only run locally and is skipped on CI.
43 | */
44 | module.exports.getSkipCISuite = function () {
45 | if (window.__env__.TEST_ENV === 'ci') {
46 | return suite.skip;
47 | } else {
48 | return suite;
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/tests/index.test.js:
--------------------------------------------------------------------------------
1 | var Aframe = require('aframe-core');
2 | var example = require('../index.js').component;
3 | var entityFactory = require('./helpers').entityFactory;
4 |
5 | Aframe.registerComponent('example', example);
6 |
7 | describe('example', function () {
8 | beforeEach(function (done) {
9 | this.el = entityFactory();
10 | this.el.addEventListener('loaded', function () {
11 | done();
12 | });
13 | });
14 |
15 | describe('example property', function () {
16 | it('is good', function () {
17 | assert.equal(1, 1);
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/tests/karma.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function (config) {
3 | config.set({
4 | basePath: '../',
5 | browserify: {
6 | paths: ['./']
7 | },
8 | browsers: ['firefox_latest'],
9 | customLaunchers: {
10 | firefox_latest: {
11 | base: 'FirefoxNightly',
12 | prefs: { /* empty */ }
13 | }
14 | },
15 | client: {
16 | captureConsole: true,
17 | mocha: {ui: 'bdd'}
18 | },
19 | envPreprocessor: [
20 | 'TEST_ENV'
21 | ],
22 | files: [
23 | 'tests/**/*.test.js',
24 | ],
25 | frameworks: ['mocha', 'sinon-chai', 'chai-shallow-deep-equal', 'browserify'],
26 | preprocessors: {
27 | 'tests/**/*.js': ['browserify']
28 | },
29 | reporters: ['mocha']
30 | });
31 | };
32 |
--------------------------------------------------------------------------------