├── .gitignore ├── README.md ├── bin └── server.js ├── config ├── index.html.ejs ├── webpack.client.config.js ├── webpack.server.config.js └── webpack.webworker.config.js ├── package.json └── src ├── client ├── assets │ ├── effects │ │ ├── base.effect.js │ │ ├── base.fragment.shader │ │ ├── base.vertex.shader │ │ ├── baseWithTexture.effect.js │ │ ├── baseWithTexture.fragment.shader │ │ └── baseWithTexture.vertex.shader │ └── images │ │ └── chicken.png ├── entry.js ├── libs │ ├── bridge │ │ ├── Bridge.js │ │ ├── Module.js │ │ └── NativeModules.js │ ├── glsurface │ │ ├── common │ │ │ └── matrix.js │ │ └── native │ │ │ ├── AssetsManager.js │ │ │ ├── BatchDraw2D.js │ │ │ ├── Effect.js │ │ │ ├── GLSurface.js │ │ │ ├── Texture.js │ │ │ └── index.js │ ├── nativeModules.js │ ├── react │ │ ├── Children.js │ │ ├── Component.js │ │ ├── createClass.js │ │ ├── createJSX.js │ │ ├── index.js │ │ └── render.js │ ├── stage2d │ │ ├── BasicAnimation.js │ │ ├── geometry.js │ │ └── index.js │ └── uimanager │ │ └── index.js └── main.js ├── index.server.js ├── index.web.js ├── server └── serveStatic.js └── webworker.web.js /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .idea 3 | node_modules 4 | yarn.lock 5 | npm-debug* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React-Game-Engine实验项目 2 | 3 | 这个项目的目标大概是这样 4 | 5 | 1. 基于react思想和编码习惯,来做基于WebGL或Canvas的游戏编程。 6 | 2. 考虑游戏场景里的对象,对象加入场景这件事情手动来控制,不去做大量对象的vdom diff。没有必要。 7 | 3. 对象内部的结构,包括什么文字啊,血条啊,播放的动画啊特效啊等等均通过react的方式控制。 8 | 4. 游戏的UI依然直接用HTML DOM撸(当然也是React),并无必要全体GL化。 9 | 5. 和mobx结合使用。 10 | 11 | 这个项目干了这么几件事情 12 | 13 | 1. 实现了一个自己的平台无关的react。 14 | 2. 实现了一个类似RN的bridge。在WebWorker里去跑React和vdom和mobx,在主脚本去跑dom操作和gl操作。 15 | 3. 不支持WebWorker的环境,退化到单线程的执行,但依然可以兼容,时序上可能有少许区别。 16 | 3. 结合上述两者,使得山寨的react可以操作普通dom,也可以操作自定义的对象。 17 | 4. 实现WebGL渲染的自定义对象。 18 | 19 | 接下来主要会干这么几件事情 20 | 1. bridge需要做batch优化。 21 | 2. 实现2d变换和几何渲染用于测试。 22 | 3. 实现图片对象和精灵动画渲染。 23 | 4. 实现文字渲染。 24 | 5. 实现文字渲染的优化(合并texture并自动分配空间)。 25 | 6. 抽离成单独组件做为依赖出现,并提供starter-kit。 26 | 7. 实现资源主动/异步加载。 27 | 8. 最佳实践和性能优化的探索。 28 | 9. 不支持WebGL的环境退化到Canvas 2D渲染。 29 | 30 | 目前有这么几件事情暂时不打算干: 31 | 32 | 1. state的支持。暂时可以用mobx或forceUpdate()来取代。 33 | 2. context的支持。所以暂时也用不了redux。 34 | 3. PropTypes检查。这个等功能方面稳定了再考虑补回来。 35 | 4. 3D的支持。理论上可行然而这个会带来巨大的工作量,晚一点再考虑。 36 | 5. react event的一部分特性,尤其是preventDefault()。这里在需要的时候打算通过自定义组件在主脚本线程去处理这些事。 37 | 6. 彻底封装封闭主线程。这个组件的设计目标是你很可能需要写一部分代码在主线程运行,包括手势识别、复杂的对象、物理计算等等。仅仅是业务驱动在Worker中编写。 38 | 39 | 预期的目标代码大概是这么玩 40 | 41 | ```javascript 42 | 43 | class Player { 44 | @observable 45 | name = '艾尔'; 46 | 47 | @observable 48 | sprite = 'eyer'; 49 | 50 | @observable 51 | hp = 100; 52 | 53 | @observable 54 | maxHp = 100; 55 | 56 | @observable 57 | x = 0; 58 | 59 | @observable 60 | y = 0; 61 | } 62 | 63 | @observer 64 | class Progress extends Component { 65 | render() { 66 | const { target, field, maxField } = this.props; 67 | const value = target[field]; 68 | const maxValue = target[maxField]; 69 | return ( 70 | 71 | 72 | 74 | ); 75 | } 76 | } 77 | 78 | @observer 79 | class PlayerComp extends Component { 80 | player = new Player(); 81 | 82 | render() { 83 | return ( 84 | 85 | 86 | 87 | 88 | 89 | 90 | {this.player.name} 91 | 92 | 93 | ); 94 | } 95 | } 96 | 97 | @observer 98 | class Game extends Component { 99 | componentDidMount() { 100 | // Scene的子节点通过方法动态加入,避免过于繁重的比对。 101 | this.refs.scene.addNode(); 102 | } 103 | 104 | render() { 105 | return ( 106 | 107 | ); 108 | } 109 | } 110 | ``` 111 | 112 | ### 资源版权说明 113 | 114 | 本工程图片和音频素材资源均为来自[66rpg](http://rm.66rpg.com/)的免费资源,若不慎侵犯版权, 115 | 请致信[dy@tdzl.cn](mailto:dy@tdzl.cn)或在Github提起issue说明,我将及时处理。 116 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | 2 | const env = process.env['SERVER_ENV']; 3 | const __DEV__ = env === 'development'; 4 | 5 | if (__DEV__) { 6 | setTimeout(()=>{ 7 | require('../build/debug/server/index.bundle'); 8 | }, 1000); 9 | } else { 10 | require('../build/release/server/index.bundle'); 11 | } 12 | -------------------------------------------------------------------------------- /config/index.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /config/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/14. 3 | */ 4 | 5 | const webpack = require('webpack'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | 8 | const ENV = process.env['WEBPACK_ENV']; 9 | const __DEV__ = ENV === 'development'; 10 | 11 | const path = require('path'); 12 | 13 | module.exports = { 14 | entry: { 15 | index: './src/index', 16 | }, 17 | output: { 18 | path: path.resolve(__dirname, `../build/${__DEV__ ? 'debug' : 'release'}/web`), // string 19 | filename: __DEV__ ? '[name].bundle.js' : '[name].[hash].js', // string 20 | publicPath: '/', // string 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.jsx?$/, 26 | include: [ 27 | path.resolve(__dirname, '../src'), 28 | ], 29 | loader: 'babel-loader', 30 | query: { 31 | presets: [ 32 | 'stage-2', 33 | ], 34 | plugins: [ 35 | 'syntax-jsx', 36 | ['transform-react-jsx', { 37 | pragma: 'createJSX', 38 | }], 39 | 'transform-decorators-legacy', 40 | ], 41 | }, 42 | }, 43 | { 44 | test: /.shader$/, 45 | loader: 'raw-loader', 46 | }, 47 | { 48 | test: /\.png|\.jpg/, 49 | loader: 'file-loader', 50 | }, 51 | ], 52 | }, 53 | resolve: { 54 | modules: [ 55 | "node_modules", 56 | path.resolve(__dirname, "../src/client/libs"), 57 | ], 58 | extensions: [".web.js", ".js", ".json", ".jsx", ".css"], 59 | alias: {}, 60 | }, 61 | plugins: [ 62 | new webpack.DefinePlugin({ 63 | __DEV__: JSON.stringify(__DEV__), 64 | __CLIENT__: "true", 65 | __SERVER__: "false", 66 | __WEBWORKER__: "false", 67 | }), 68 | new HtmlWebpackPlugin({ 69 | title: 'SuperMarket', 70 | template: 'config/index.html.ejs', 71 | }) 72 | ], 73 | performance: { 74 | hints: 'warning', // enum 75 | maxAssetSize: 200000, // int (in bytes), 76 | maxEntrypointSize: 400000, // int (in bytes) 77 | assetFilter: function(assetFilename) { 78 | // Function predicate that provides asset filenames 79 | return assetFilename.endsWith('.css') || assetFilename.endsWith('.js'); 80 | } 81 | }, 82 | devtool: 'source-map', // enum 83 | context: path.resolve(__dirname, '..'), 84 | target: 'web', 85 | stats: { 86 | assets: true, 87 | colors: true, 88 | errors: true, 89 | errorDetails: true, 90 | hash: true, 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /config/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/14. 3 | */ 4 | 5 | const webpack = require('webpack'); 6 | const ENV = process.env['WEBPACK_ENV']; 7 | const __DEV__ = ENV === 'development'; 8 | 9 | const path = require('path'); 10 | 11 | module.exports = { 12 | entry: { 13 | index: './src/index', 14 | }, 15 | output: { 16 | path: path.resolve(__dirname, `../build/${__DEV__ ? 'debug' : 'release'}/server`), // string 17 | filename: __DEV__ ? '[name].bundle.js' : '[name].[hash].js', // string 18 | publicPath: '/', // string 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.jsx?$/, 24 | include: [ 25 | path.resolve(__dirname, '../src'), 26 | ], 27 | loader: 'babel-loader', 28 | options: { 29 | presets: [], 30 | }, 31 | }, 32 | ], 33 | }, 34 | resolve: { 35 | modules: [ 36 | 'node_modules', 37 | ], 38 | extensions: ['.server.js', '.js', '.json', '.jsx', '.css'], 39 | alias: {}, 40 | }, 41 | externals: { 42 | koa: 'commonjs koa', 43 | 'koa-static': 'commonjs koa-static', 44 | 'koa-router': 'commonjs koa-router', 45 | }, 46 | plugins: [ 47 | new webpack.DefinePlugin({ 48 | __DEV__: JSON.stringify(__DEV__), 49 | __CLIENT__: "false", 50 | __SERVER__: "true", 51 | __WEBWORKER__: "false", 52 | }), 53 | ], 54 | devtool: 'source-map', // enum 55 | context: path.resolve(__dirname, '..'), 56 | target: 'node', 57 | stats: { 58 | assets: true, 59 | colors: true, 60 | errors: true, 61 | errorDetails: true, 62 | hash: true, 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /config/webpack.webworker.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/14. 3 | */ 4 | 5 | const webpack = require('webpack'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | 8 | const ENV = process.env['WEBPACK_ENV']; 9 | const __DEV__ = ENV === 'development'; 10 | 11 | const path = require('path'); 12 | 13 | module.exports = { 14 | entry: { 15 | ww: './src/webworker', 16 | }, 17 | output: { 18 | path: path.resolve(__dirname, `../build/${__DEV__ ? 'debug' : 'release'}/web`), // string 19 | filename: __DEV__ ? '[name].bundle.js' : '[name].[hash].js', // string 20 | publicPath: '/', // string 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.jsx?$/, 26 | include: [ 27 | path.resolve(__dirname, '../src'), 28 | ], 29 | loader: 'babel-loader', 30 | query: { 31 | presets: [ 32 | "stage-2", 33 | ], 34 | plugins: [ 35 | 'syntax-jsx', 36 | ['transform-react-jsx', { 37 | pragma: 'createJSX', 38 | }], 39 | 'transform-decorators-legacy', 40 | ], 41 | }, 42 | }, 43 | { 44 | test: /\.png|\.jpg/, 45 | loader: 'file-loader', 46 | }, 47 | ], 48 | }, 49 | resolve: { 50 | modules: [ 51 | 'node_modules', 52 | path.resolve(__dirname, "../src/client/libs"), 53 | ], 54 | extensions: ['.web.js', '.js', '.json', '.jsx', '.css'], 55 | alias: {}, 56 | }, 57 | plugins: [ 58 | new webpack.DefinePlugin({ 59 | __DEV__: JSON.stringify(__DEV__), 60 | __CLIENT__: 'true', 61 | __SERVER__: 'false', 62 | __WEBWORKER__: 'true', 63 | }), 64 | ], 65 | performance: { 66 | hints: 'warning', // enum 67 | maxAssetSize: 200000, // int (in bytes), 68 | maxEntrypointSize: 400000, // int (in bytes) 69 | assetFilter: function(assetFilename) { 70 | // Function predicate that provides asset filenames 71 | return assetFilename.endsWith('.css') || assetFilename.endsWith('.js'); 72 | } 73 | }, 74 | devtool: 'source-map', // enum 75 | context: path.resolve(__dirname, '..'), 76 | target: 'webworker', 77 | stats: { 78 | assets: true, 79 | colors: true, 80 | errors: true, 81 | errorDetails: true, 82 | hash: true, 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "super-market", 3 | "version": "1.0.0", 4 | "description": "Web Game", 5 | "bin": { 6 | "start": "./bin/server" 7 | }, 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "development": "npm-run-all --parallel build-client-development build-server-development build-webworker-development server-development", 11 | "server-development": "cross-env SERVER_ENV=development nodemon ./bin/server.js --watch ./build/debug/server", 12 | "build-client-development": "cross-env WEBPACK_ENV=development webpack --config config/webpack.client.config.js --watch", 13 | "build-webworker-development": "cross-env WEBPACK_ENV=development webpack --config config/webpack.webworker.config.js --watch", 14 | "build-server-development": "cross-env WEBPACK_ENV=development webpack --config config/webpack.server.config.js --watch" 15 | }, 16 | "author": "", 17 | "private": true, 18 | "dependencies": { 19 | "babel-loader": "^6.4.0", 20 | "babel-plugin-syntax-jsx": "^6.18.0", 21 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 22 | "babel-plugin-transform-react-jsx": "^6.23.0", 23 | "babel-register": "^6.24.0", 24 | "cross-env": "^3.2.4", 25 | "file-loader": "^0.10.1", 26 | "koa": "^2.2.0", 27 | "koa-router": "^5.4.0", 28 | "koa-static": "^3.0.0", 29 | "mobx": "^3.1.7", 30 | "mobx-react": "^4.1.3", 31 | "raw-loader": "^0.5.1", 32 | "urijs": "^1.18.9" 33 | }, 34 | "devDependencies": { 35 | "babel-preset-stage-2": "^6.22.0", 36 | "html-webpack-plugin": "^2.28.0", 37 | "nodemon": "^1.11.0", 38 | "npm-run-all": "^4.0.2", 39 | "webpack": "^2.2.1", 40 | "worker-loader": "^0.8.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/client/assets/effects/base.effect.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | streams: [ 3 | { 4 | name: 'vertex', 5 | size: 2 6 | }, 7 | null, 8 | { 9 | name: 'diffuse', 10 | size: 4 11 | }, 12 | ], 13 | passes: [ 14 | { 15 | vs: require('./base.vertex.shader'), 16 | fs: require('./base.fragment.shader'), 17 | }, 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /src/client/assets/effects/base.fragment.shader: -------------------------------------------------------------------------------- 1 | varying lowp vec4 vDiffuse; 2 | 3 | void main(void) { 4 | gl_FragColor = vDiffuse; 5 | } 6 | -------------------------------------------------------------------------------- /src/client/assets/effects/base.vertex.shader: -------------------------------------------------------------------------------- 1 | attribute vec2 vertex; 2 | attribute vec4 diffuse; 3 | 4 | varying lowp vec4 vDiffuse; 5 | 6 | void main(void) { 7 | vDiffuse = diffuse; 8 | gl_Position = vec4(vertex, 0.0, 1.0); 9 | } -------------------------------------------------------------------------------- /src/client/assets/effects/baseWithTexture.effect.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | streams: [ 3 | { 4 | name: 'vertex', 5 | size: 2, 6 | }, 7 | { 8 | name: 'texcoord', 9 | size: 2, 10 | }, 11 | { 12 | name: 'diffuse', 13 | size: 4, 14 | }, 15 | ], 16 | passes: [ 17 | { 18 | vs: require('./baseWithTexture.vertex.shader'), 19 | fs: require('./baseWithTexture.fragment.shader'), 20 | }, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/client/assets/effects/baseWithTexture.fragment.shader: -------------------------------------------------------------------------------- 1 | varying mediump vec2 vTexcoord; 2 | varying lowp vec4 vDiffuse; 3 | 4 | uniform sampler2D sampler; 5 | 6 | void main(void) { 7 | gl_FragColor = texture2D(sampler, vTexcoord) * vDiffuse; 8 | } 9 | -------------------------------------------------------------------------------- /src/client/assets/effects/baseWithTexture.vertex.shader: -------------------------------------------------------------------------------- 1 | attribute vec2 vertex; 2 | attribute vec2 texcoord; 3 | attribute vec4 diffuse; 4 | 5 | varying mediump vec2 vTexcoord; 6 | varying lowp vec4 vDiffuse; 7 | 8 | void main(void) { 9 | vTexcoord = texcoord; 10 | vDiffuse = diffuse; 11 | gl_Position = vec4(vertex, 0.0, 1.0); 12 | } -------------------------------------------------------------------------------- /src/client/assets/images/chicken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdzl2003/react-game-engine-experimental/cd3c9170b55977ca970bd08092b253fd433ba16d/src/client/assets/images/chicken.png -------------------------------------------------------------------------------- /src/client/entry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/15. 3 | */ 4 | 5 | const Bridge = require('bridge/Bridge').default; 6 | const bridge = global.__bridgeClient = new Bridge(); 7 | 8 | if (__WEBWORKER__) { 9 | bridge.install(global).then(postInstall); 10 | console.log('Client installed on worker.'); 11 | } else { 12 | global.__bridgeServer.install({ 13 | postMessage: data => bridge.processMessage({data}), 14 | }) 15 | 16 | bridge.install({ 17 | postMessage: data => global.__bridgeServer.processMessage({data}), 18 | }).then(postInstall); 19 | 20 | console.log('Server & Client installed on main'); 21 | } 22 | 23 | function postInstall() { 24 | require('./main'); 25 | } 26 | -------------------------------------------------------------------------------- /src/client/libs/bridge/Bridge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/15. 3 | */ 4 | import { method, asyncMethod } from './Module'; 5 | 6 | export default class Bridge{ 7 | name = 'Bridge'; 8 | 9 | // local modules 10 | modules = [this]; 11 | 12 | // remote modules, name as key, methods/constants as value 13 | remoteModules = {}; 14 | 15 | // saved callback for promise call 16 | callbacks = []; 17 | callbackId = 0; 18 | 19 | remote = null; 20 | 21 | install(worker = global) { 22 | if (this.remote) { 23 | throw new Error('Bridge cannot install multi times!'); 24 | } 25 | worker.onmessage = this.processMessage; 26 | this.remote = worker; 27 | 28 | this.modules.forEach((module, moduleId) => { 29 | this.callRemoteVoid(0, 1, moduleId, module.name, module.constants, module.__methods); 30 | }) 31 | 32 | return this.callRemotePromise(0, 2); 33 | } 34 | 35 | processMessage = e => { 36 | e.data.forEach(v => { 37 | if (__DEV__) { 38 | if (!this.modules[v[0]]) { 39 | console.error(`Cannot find module ${v[0]}`); 40 | } else if (!this.modules[v[0]].__methods[v[1]]) { 41 | console.error(`Cannot find method ${this.modules[v[0]].__methods[v[1]]} in module ${this.modules[v[0]].name}`); 42 | } 43 | } 44 | const module = this.modules[v[0]]; 45 | const [name] = module.__methods[v[1]]; 46 | 47 | if (!v[3]) { 48 | // void call 49 | module[name](...v[2]); 50 | } else { 51 | const [resolve, reject] = v[3]; 52 | // promise call 53 | Promise.resolve(module[name](...v[2])) 54 | .then(arg => { 55 | this.invokeRemoteCallback(resolve, arg); 56 | }, arg => { 57 | this.invokeRemoteCallback(reject, arg); 58 | }); 59 | } 60 | }); 61 | }; 62 | 63 | callRemoteVoid(moduleId, methodId, ...args) { 64 | this.remote.postMessage([[moduleId, methodId, args]]); 65 | } 66 | 67 | callRemotePromise(moduleId, methodId, ...args) { 68 | return new Promise((resolve, reject) => { 69 | const id = this.callbackId; 70 | this.callbacks[this.callbackId++] = resolve; 71 | this.callbacks[this.callbackId++] = reject; 72 | this.remote.postMessage([[moduleId, methodId, args, [id, id+1]]]); 73 | }) 74 | } 75 | 76 | invokeRemoteCallback(callbackId, arg) { 77 | this.callRemoteVoid(0, 0, callbackId, arg); 78 | } 79 | 80 | @method 81 | invokeCallback(callbackId, arg) { 82 | const callback = this.callbacks[callbackId]; 83 | if (__DEV__) { 84 | if (!callback) { 85 | console.error('Internal error: callback maybe called multi times.'); 86 | return; 87 | } 88 | } 89 | delete this.callbacks[callbackId]; 90 | delete this.callbacks[callbackId ^ 1]; 91 | callback(arg); 92 | } 93 | 94 | @method 95 | addRemoteModule(moduleId, name, constants, methods) { 96 | const mod = this.remoteModules[name] = {}; 97 | if (constants) { 98 | Object.assign(mod, constants); 99 | } 100 | if (methods) { 101 | methods.forEach(([name, async], methodId) => { 102 | mod[name] = (...args) => { 103 | if (async) { 104 | return this.callRemotePromise(moduleId, methodId, ...args); 105 | } else { 106 | this.callRemoteVoid(moduleId, methodId, ...args); 107 | } 108 | }; 109 | }); 110 | } 111 | } 112 | 113 | // Wait remote initial ready. 114 | @asyncMethod 115 | ping() { 116 | } 117 | 118 | addModule(module) { 119 | const moduleId = this.modules.length; 120 | this.modules.push(module); 121 | if (this.remote) { 122 | this.callRemoteVoid(0, 1, moduleId, module.name, module.constants, module.__methods); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/client/libs/bridge/Module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/15. 3 | */ 4 | 5 | export default class Module { 6 | constructor(bridge) { 7 | this.bridge = bridge; 8 | this.name = this.constructor.name; 9 | } 10 | } 11 | 12 | export function method(target, name, args) { 13 | if (target.hasOwnProperty('__methods')){ 14 | target.__methods.push([name, false]); 15 | } else { 16 | Object.defineProperty(target, '__methods', { 17 | configurable: true, 18 | enumerable: false, 19 | value: [[name, false]], 20 | }) 21 | } 22 | } 23 | 24 | export function asyncMethod(target, name, args) { 25 | if (target.hasOwnProperty('__methods')){ 26 | target.__methods.push([name, true]); 27 | } else { 28 | Object.defineProperty(target, '__methods', { 29 | configurable: true, 30 | enumerable: false, 31 | value: [[name, true]], 32 | }) 33 | } 34 | } 35 | 36 | export function serverModule(target) { 37 | const bridge = global.__bridgeServer; 38 | if (!bridge) { 39 | throw new Error('Export server module without __bridgeServer'); 40 | } 41 | target.instance = new target(bridge) 42 | bridge.addModule(target.instance); 43 | } 44 | 45 | export function clientModule(target) { 46 | const bridge = global.__bridgeClient; 47 | if (!bridge) { 48 | throw new Error('Export server module without __bridgeClient'); 49 | } 50 | target.instance = new target(bridge) 51 | bridge.addModule(target.instance); 52 | } 53 | -------------------------------------------------------------------------------- /src/client/libs/bridge/NativeModules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/16. 3 | */ 4 | 5 | module.exports = global.__bridgeClient.remoteModules; 6 | -------------------------------------------------------------------------------- /src/client/libs/glsurface/common/matrix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 3/24/17. 3 | */ 4 | 5 | export const identity = [ 6 | 1, 0, 0, 0, 7 | 0, 1, 0, 0, 8 | 0, 0, 1, 0, 9 | 0, 0, 0, 1, 10 | ]; 11 | 12 | export function ortho2D(width, height, x, y) { 13 | return [ 14 | 2 / width, 0, 0, 0, 15 | 0, -2/height, 0, 0, 16 | 0, 0, 1, 0, 17 | -x/width*2, y/height*2, 0, 1, 18 | ]; 19 | } 20 | 21 | //(x,y,z,w)*M*A == (x+dx,y+dy,z+dz, w)*A 22 | //M = 23 | // 1 0 0 0 24 | // 0 1 0 0 25 | // 0 0 1 0 26 | // dx dy dz 1 27 | 28 | export function translate2D(m, dx, dy) { 29 | for (let i = 0; i < 4; i++){ 30 | m[12+i] += m[i] * dx + m[4+i] * dy; 31 | } 32 | } 33 | 34 | //(x,y,z,w)*M*A == (x*sx,y*sy,z*dz, w)*A 35 | //M = 36 | // sx 0 0 0 37 | // 0 sy 0 0 38 | // 0 0 sz 0 39 | // 0 0 0 1 40 | //let A = M*A 41 | 42 | export function scale2D(m, sx, sy) { 43 | for (let i = 0; i < 4; i++){ 44 | m[i] *= sx; 45 | m[4+i] *= sy; 46 | } 47 | } 48 | 49 | //(x,y,z,w)*M*A == (x*cos-y*sin, y*cos+x*sin, z, w)*A 50 | //M = 51 | // cos -sin 0 0 52 | // sin cos 0 0 53 | // 0 0 1 0 54 | // 0 0 0 1 55 | 56 | export function rotate2DCalced(m, cos, sin) { 57 | for (let i = 0; i < 4; i++){ 58 | let v1 = cos*m[i] - sin*m[4+i]; 59 | let v2 = sin*m[i] + cos*m[4+i]; 60 | m[i] = v1; 61 | m[4+i] = v2; 62 | } 63 | } 64 | 65 | export function rotate2D(m, rad) { 66 | rotate2DCalced(m, Math.cos(rad), Math.sin(rad)); 67 | } 68 | 69 | export function transpose2D(m, x, y) { 70 | const x1 = x * m[0] + y * m[4] + m[12]; 71 | const y1 = x * m[1] + y * m[5] + m[13]; 72 | const w = x * m[3] + y * m[7] + m[15]; 73 | return [x1/w, y1/w]; 74 | } 75 | 76 | export default class MatrixStack { 77 | values = []; 78 | reset() { 79 | this.values.splice(0, this.values.length); 80 | } 81 | get top() { 82 | if (this.values.length === 0) { 83 | return identity; 84 | } 85 | return this.values[this.values.length - 1]; 86 | } 87 | pushOrtho2D(width, height, x, y) { 88 | this.values.push(ortho2D(width, height, x, y)); 89 | } 90 | push() { 91 | this.values.push(this.top); 92 | } 93 | pop() { 94 | return this.values.pop(); 95 | } 96 | transpose2D(x, y) { 97 | return transpose2D(this.top, x, y); 98 | } 99 | } -------------------------------------------------------------------------------- /src/client/libs/glsurface/native/AssetsManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/19. 3 | */ 4 | 5 | export class AssetType { 6 | ref = 0; 7 | 8 | // return a promise if assets needs a async loading. 9 | load() { 10 | } 11 | 12 | unload() { 13 | } 14 | 15 | addRef() { 16 | ++this.ref; 17 | } 18 | 19 | release() { 20 | --this.ref; 21 | } 22 | 23 | destroy() { 24 | } 25 | } 26 | 27 | export default class AssetManager { 28 | clazz = null; 29 | assets = {} 30 | 31 | constructor(clazz) { 32 | this.clazz = clazz; 33 | } 34 | 35 | obtain(gl, key) { 36 | let ref = this.assets[key]; 37 | if (!ref) { 38 | this.assets[key] = ref = new this.clazz(gl, key); 39 | ref.load(gl); 40 | } 41 | 42 | ref.addRef(); 43 | return ref; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/client/libs/glsurface/native/BatchDraw2D.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/19. 3 | */ 4 | 5 | const CAPACITY_NAMES = { 6 | maxElementsIndecies: 'MAX_ELEMENTS_INDICES', 7 | maxElementsVerticies: 'MAX_ELEMENTS_VERTICES', 8 | }; 9 | 10 | const DEFAULT_CAPACITY = { 11 | maxElementsIndecies: 8192, 12 | maxElementsVerticies: 4096, 13 | }; 14 | 15 | const VERTEX_ATTRIBUTE_LOCATION = { 16 | VERTEX: 0, 17 | TEXCOORD: 1, 18 | DIFFUSE: 2, 19 | }; 20 | 21 | const VERTEX_FORMAT_SIZE = [ 22 | 2, // VERTEX, 23 | 2, // TEXCOORD, 24 | 4, // DIFFUSE, 25 | ]; 26 | 27 | const MAX_VERTEX_ELEMENT_SIZE = VERTEX_FORMAT_SIZE.reduce((a, b) => a + b); 28 | 29 | const VERTEX_FORMAT_BIT = [ 30 | 0, // VERTEX, always true. 31 | 1 << 0, // TEXCOORD 32 | 1 << 1, // DIFFUSE 33 | ] 34 | 35 | // vertex buffer 36 | // diffuse buffer 37 | // texcoord buffer 38 | 39 | export default class BatchDraw2D { 40 | caps = {}; 41 | 42 | mode = 0; 43 | format = 0; 44 | effect = null; 45 | texture = null; 46 | 47 | // buffers: 48 | maxVertexBufferSize = 0; 49 | maxIndeciesBufferSize = 0; 50 | 51 | vertexBuffer = null; 52 | vertexBufferData = null; 53 | vertexBufferCount = 0; 54 | 55 | indeciesBuffer = null; 56 | indeciesBufferData = null; 57 | indeciesBufferCount = 0; 58 | 59 | // dependencies 60 | baseEffect = null; 61 | baseEffectWithTexture = null; 62 | 63 | constructor(gl) { 64 | for (const key of Object.keys(CAPACITY_NAMES)) { 65 | this.caps[key] = gl.getParameter(gl[CAPACITY_NAMES[key]]) || DEFAULT_CAPACITY[key]; 66 | } 67 | 68 | if (__DEV__) { 69 | console.log('CAPS: ', JSON.stringify(this.caps)); 70 | } 71 | 72 | this.maxVertexBufferSize = Math.min(4096, this.caps.maxElementsVerticies); 73 | this.vertexBufferData = new Float32Array(this.maxVertexBufferSize * MAX_VERTEX_ELEMENT_SIZE); 74 | this.maxIndeciesBufferSize = Math.min(8192, this.caps.maxElementsIndecies); 75 | this.indeciesBufferData = new Uint16Array(this.maxIndeciesBufferSize); 76 | 77 | this.vertexBuffer = gl.createBuffer(); 78 | this.indeciesBuffer = gl.createBuffer(); 79 | 80 | this.baseEffect = gl.effectManager.obtain(gl, 'base'); 81 | this.baseEffectWithTexture = gl.effectManager.obtain(gl, 'baseWithTexture'); 82 | } 83 | 84 | drawRect(gl, x, y, w, h, r = 0, g = 0, b = 0, a = 1) { 85 | this.prepare(gl, 4, 6, gl.TRIANGLES, 2, this.baseEffect); 86 | const baseId = this.vertexBufferCount; 87 | let base = baseId * 6; 88 | let idxBase = this.indeciesBufferCount; 89 | this.vertexBufferCount += 4; 90 | this.indeciesBufferCount += 6; 91 | 92 | const xs = [x, x + w, x, x + w]; 93 | const ys = [y, y, y + h, y + h]; 94 | 95 | for (let i = 0; i < 4; i++) { 96 | const [rx, ry] = gl.matrixStack.transpose2D(xs[i], ys[i]); 97 | this.vertexBufferData[base++] = rx; 98 | this.vertexBufferData[base++] = ry; 99 | this.vertexBufferData[base++] = r; 100 | this.vertexBufferData[base++] = g; 101 | this.vertexBufferData[base++] = b; 102 | this.vertexBufferData[base++] = a; 103 | } 104 | 105 | this.indeciesBufferData[idxBase++] = baseId; 106 | this.indeciesBufferData[idxBase++] = baseId + 1; 107 | this.indeciesBufferData[idxBase++] = baseId + 2; 108 | this.indeciesBufferData[idxBase++] = baseId + 2; 109 | this.indeciesBufferData[idxBase++] = baseId + 1; 110 | this.indeciesBufferData[idxBase++] = baseId + 3; 111 | } 112 | 113 | drawTexture( 114 | gl, texture, 115 | x, y, w, h, 116 | tx, ty, tw, th, 117 | r = 0, g = 0, b = 0, a = 1 118 | ) { 119 | this.prepare(gl, 4, 6, gl.TRIANGLES, 3, this.baseEffectWithTexture, texture); 120 | const baseId = this.vertexBufferCount; 121 | let base = baseId * 8; 122 | let idxBase = this.indeciesBufferCount; 123 | this.vertexBufferCount += 4; 124 | this.indeciesBufferCount += 6; 125 | 126 | const xs = [x, x + w, x, x + w]; 127 | const ys = [y, y, y + h, y + h]; 128 | 129 | const txs = [tx, tx + tw, tx, tx + tw]; 130 | const tys = [ty, ty, ty + th, ty + th]; 131 | 132 | for (let i = 0; i < 4; i++) { 133 | const [rx, ry] = gl.matrixStack.transpose2D(xs[i], ys[i]); 134 | this.vertexBufferData[base++] = rx; 135 | this.vertexBufferData[base++] = ry; 136 | this.vertexBufferData[base++] = txs[i]; 137 | this.vertexBufferData[base++] = tys[i]; 138 | this.vertexBufferData[base++] = r; 139 | this.vertexBufferData[base++] = g; 140 | this.vertexBufferData[base++] = b; 141 | this.vertexBufferData[base++] = a; 142 | } 143 | 144 | this.indeciesBufferData[idxBase++] = baseId; 145 | this.indeciesBufferData[idxBase++] = baseId + 1; 146 | this.indeciesBufferData[idxBase++] = baseId + 2; 147 | this.indeciesBufferData[idxBase++] = baseId + 2; 148 | this.indeciesBufferData[idxBase++] = baseId + 1; 149 | this.indeciesBufferData[idxBase++] = baseId + 3; 150 | } 151 | 152 | prepare(gl, eleCnt, idxCount, mode, format, effect, texture = null) { 153 | if (mode !== this.mode || format !== this.format || effect !== this.effect || texture !== this.texture) { 154 | this.flush(gl); 155 | this.mode = mode; 156 | this.format = format; 157 | this.effect = effect; 158 | this.texture = texture; 159 | } else if (eleCnt + this.vertexBufferCount > this.maxVertexBufferSize || idxCount + this.indeciesBufferCount > this.maxIndeciesBufferSize) { 160 | this.flush(gl); 161 | } 162 | } 163 | 164 | flush(gl) { 165 | if (this.indeciesBufferCount === 0) { 166 | return; 167 | } 168 | 169 | gl.enable(gl.BLEND); 170 | gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); 171 | // commit buffer data 172 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 173 | gl.bufferData(gl.ARRAY_BUFFER, this.vertexBufferData, gl.DYNAMIC_DRAW); 174 | 175 | if (this.texture !== null) { 176 | gl.activeTexture(gl.TEXTURE0); 177 | gl.bindTexture(gl.TEXTURE_2D, this.texture); 178 | this.effect.setParameter1i(gl, 'sampler', 0); 179 | } else { 180 | gl.activeTexture(gl.TEXTURE0); 181 | gl.bindTexture(gl.TEXTURE_2D, null); 182 | } 183 | 184 | let totalSize = 0; 185 | 186 | for (let i = 0; i < 3; i++) { 187 | if ((this.format & VERTEX_FORMAT_BIT[i]) === VERTEX_FORMAT_BIT[i]) { 188 | totalSize += VERTEX_FORMAT_SIZE[i]; 189 | } 190 | } 191 | 192 | 193 | let offset = 0; 194 | 195 | for (let i = 0; i < 3; i++) { 196 | if ((this.format & VERTEX_FORMAT_BIT[i]) === VERTEX_FORMAT_BIT[i]) { 197 | gl.enableVertexAttribArray(i); 198 | gl.vertexAttribPointer(i, VERTEX_FORMAT_SIZE[i], gl.FLOAT, false, totalSize * 4, offset * 4); 199 | offset += VERTEX_FORMAT_SIZE[i]; 200 | } else { 201 | gl.disableVertexAttribArray(i); 202 | } 203 | } 204 | 205 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indeciesBuffer); 206 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indeciesBufferData, gl.DYNAMIC_DRAW); 207 | // this.effect.drawArrays(this.mode, 0, 3); 208 | this.effect.drawElements(this.mode, this.indeciesBufferCount, gl.UNSIGNED_SHORT, 0); 209 | 210 | this.vertexBufferCount = 0; 211 | this.indeciesBufferCount = 0; 212 | 213 | this.texture = null; 214 | } 215 | } -------------------------------------------------------------------------------- /src/client/libs/glsurface/native/Effect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/19. 3 | */ 4 | import {AssetType} from "./AssetsManager"; 5 | 6 | export default class Effect extends AssetType { 7 | streams; 8 | passes; 9 | 10 | params = {}; 11 | 12 | constructor(gl, key) { 13 | super(gl); 14 | 15 | if (typeof(key) === 'string') { 16 | key = require('../../../assets/effects/' + key + '.effect.js'); 17 | } 18 | this.streams = key.streams; 19 | this.passes = key.passes.map((v, i) => this.loadPass(v, i)); 20 | } 21 | 22 | loadPass(pass, passId) { 23 | const vs = this.loadShader(gl.VERTEX_SHADER, pass.vs); 24 | const fs = this.loadShader(gl.FRAGMENT_SHADER, pass.fs); 25 | const program = gl.createProgram(); 26 | gl.attachShader(program, vs); 27 | gl.deleteShader(vs); 28 | gl.attachShader(program, fs); 29 | gl.deleteShader(fs); 30 | for (let i = 0; i < this.streams.length; i++) { 31 | const stream = this.streams[i]; 32 | if (stream) { 33 | gl.bindAttribLocation(program, i, stream.name); 34 | } 35 | } 36 | 37 | gl.linkProgram(program); 38 | 39 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 40 | console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(program)); 41 | gl.deleteProgram(program); 42 | return; 43 | } 44 | 45 | const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); 46 | 47 | for (let i = 0; i < uniformCount; i++) { 48 | const info = gl.getActiveUniform(program, i); 49 | const loc = gl.getUniformLocation(program, info.name); 50 | this.params[info.name] = this.params[info.name] || []; 51 | this.params[info.name][passId] = loc; 52 | } 53 | 54 | // bind streams. 55 | // gl.useProgram(program); 56 | return program; 57 | } 58 | 59 | loadShader(type, source) { 60 | const shader = gl.createShader(type); 61 | 62 | gl.shaderSource(shader, source); 63 | 64 | // Compile the shader program 65 | gl.compileShader(shader); 66 | 67 | // See if it compiled successfully 68 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 69 | console.warn('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader)); 70 | gl.deleteShader(shader); 71 | return null; 72 | } 73 | 74 | return shader; 75 | } 76 | 77 | drawArrays(mode, first, count) { 78 | if (gl.lastUsedEffect) { 79 | for (let i = this.streams.length; i < gl.lastUsedEffect.stream.length; i++) { 80 | gl.disableVertexAttribArray(i); 81 | } 82 | } 83 | for (const pass of this.passes) { 84 | gl.useProgram(pass); 85 | gl.drawArrays(mode, first, count); 86 | } 87 | } 88 | 89 | drawElements(mode, count, type, offset) { 90 | if (gl.lastUsedEffect && gl.lastUsedEffect !== this) { 91 | for (let i = this.streams.length; i < gl.lastUsedEffect.streams.length; i++) { 92 | gl.disableVertexAttribArray(i); 93 | } 94 | } 95 | for (const pass of this.passes) { 96 | gl.useProgram(pass); 97 | gl.drawElements(mode, count, type, offset); 98 | } 99 | gl.lastUsedEffect = this; 100 | } 101 | 102 | setParameter1i(gl, name, value) { 103 | const positions = this.params[name]; 104 | if (positions) { 105 | for (let i = 0; i < this.passes.length; i++) { 106 | if (positions[i] !== undefined) { 107 | const pass = this.passes[i]; 108 | gl.useProgram(pass); 109 | gl.uniform1i(positions[i], value); 110 | } 111 | } 112 | } 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /src/client/libs/glsurface/native/GLSurface.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/19. 3 | */ 4 | import {prop, nativeComponent, NativeComponent, NativeElementComponent} from "../../uimanager/index"; 5 | import BatchDraw2D from "./BatchDraw2D"; 6 | import AssetManager from "./AssetsManager"; 7 | import Effect from "./Effect"; 8 | import MatrixStack from '../common/matrix'; 9 | import {ImageTexture} from './Texture'; 10 | 11 | export class GLNode extends NativeComponent { 12 | prevSibling = null; 13 | nextSibling = null; 14 | 15 | parentNode = null; 16 | 17 | mount(parentNode, before) { 18 | if (__DEV__) { 19 | if (this.parentNode) { 20 | console.error('Mount before unmounted.'); 21 | } 22 | } 23 | this.parentNode = parentNode; 24 | parentNode.insertBefore(this, before); 25 | } 26 | 27 | unmount() { 28 | this.parentNode.removeChild(this); 29 | } 30 | 31 | renderGL(gl) { 32 | // Override me. 33 | } 34 | } 35 | 36 | export class GLContainer extends GLNode { 37 | firstChild = null; 38 | lastChild = null; 39 | 40 | appendChild(child) { 41 | this.insertBefore(child, null); 42 | } 43 | 44 | insertBefore(child, before) { 45 | if (__DEV__) { 46 | if (before && before.parentNode !== this) { 47 | console.error('Before is not child of this.'); 48 | } 49 | } 50 | const after = before ? before.prevSibling : this.lastChild; 51 | if (after) { 52 | after.nextSibling = child; 53 | child.prevSibling = after; 54 | } else { 55 | this.firstChild = child; 56 | } 57 | if (before) { 58 | before.prevSibling = child; 59 | child.nextSibling = before; 60 | } else { 61 | this.lastChild = child; 62 | } 63 | 64 | child.parentNode = this; 65 | } 66 | 67 | removeChild(child) { 68 | if (__DEV__) { 69 | if (child.parentNode !== this) { 70 | console.error('removeChild target is not child of this.'); 71 | } 72 | } 73 | 74 | const { nextSibling, prevSibling } = child; 75 | if (nextSibling) { 76 | nextSibling.prevSibling = child.prevSibling; 77 | child.nextSibling = null; 78 | } else { 79 | this.lastChild = prevSibling; 80 | } 81 | 82 | if (prevSibling) { 83 | prevSibling.nextSibling = child.nextSibling; 84 | child.prevSibling = null; 85 | } else { 86 | this.firstChild = nextSibling; 87 | } 88 | 89 | child.parentNode = null; 90 | } 91 | 92 | renderGL(gl) { 93 | for (let l = this.firstChild; l; l = l.nextSibling) { 94 | l.renderGL(gl); 95 | } 96 | } 97 | } 98 | 99 | @nativeComponent('gl-surface') 100 | export class GLSurface extends NativeElementComponent { 101 | gl; 102 | 103 | renderTimer = null; 104 | 105 | container = new GLContainer(); 106 | 107 | constructor(id, eventId) { 108 | super(id, eventId, 'canvas'); 109 | } 110 | 111 | mount(parentNode, before) { 112 | super.mount(parentNode, before); 113 | 114 | if (!this.gl) { 115 | this.initGL(); 116 | this.performRender(); 117 | } 118 | } 119 | 120 | unmount() { 121 | super.unmount(); 122 | this.gl.destroyed = true; 123 | this.gl = null; 124 | } 125 | 126 | performRender = () => { 127 | if (this.reactId !== null) { 128 | this.renderTimer = requestAnimationFrame(this.performRender); 129 | this.renderGL(this.gl); 130 | } 131 | }; 132 | 133 | initGL() { 134 | const canvas = this.el; 135 | const gl = canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); 136 | const ratio = window.devicePixelRatio || 1; 137 | 138 | const {offsetWidth, offsetHeight} = this.el; 139 | 140 | const width = this.el.width = (offsetWidth * ratio) | 0; 141 | const height = this.el.height = (offsetHeight * ratio) | 0; 142 | 143 | this.sendEvent('surfaceCreated', { 144 | width: offsetWidth, 145 | height: offsetHeight, 146 | ratio, 147 | }); 148 | 149 | if (__DEV__) { 150 | global.gl = gl; 151 | } 152 | 153 | // If we don't have a GL context, give up now 154 | if (!gl) { 155 | console.error('Unable to initialize WebGL. Your browser may not support it.'); 156 | } 157 | 158 | this.gl = gl; 159 | 160 | gl.effectManager = new AssetManager(Effect); 161 | gl.imageTextureManager = new AssetManager(ImageTexture); 162 | 163 | gl.matrixStack = new MatrixStack(); 164 | gl.painter2d = new BatchDraw2D(gl); 165 | } 166 | 167 | renderGL(gl) { 168 | const {offsetWidth, offsetHeight} = this.el; 169 | const ratio = window.devicePixelRatio || 1; 170 | const width = (offsetWidth * ratio) | 0; 171 | const height = (offsetHeight * ratio) | 0; 172 | if (width !== this.el.width || height !== this.el.height) { 173 | this.el.width = width; 174 | this.el.height = height; 175 | this.sendEvent('sizeChanged', { 176 | width: offsetWidth, 177 | height: offsetHeight, 178 | ratio, 179 | }); 180 | } 181 | if (__DEV__) { 182 | gl.viewport(0, 0, width, height); 183 | gl.clearColor(0.0, 0.0, 1.0, 1.0); 184 | gl.clear(gl.COLOR_BUFFER_BIT); 185 | } 186 | 187 | this.container.renderGL(gl); 188 | 189 | gl.painter2d.flush(gl); 190 | } 191 | 192 | appendChild(child) { 193 | this.container.appendChild(child); 194 | } 195 | 196 | insertBefore(el, before) { 197 | this.container.insertBefore(el, before); 198 | } 199 | 200 | removeChild(child) { 201 | this.container.removeChild(child); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/client/libs/glsurface/native/Texture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/19. 3 | */ 4 | import {AssetType} from "./AssetsManager"; 5 | 6 | function powerOfTwo(num) { 7 | if( num > 0 ) 8 | { 9 | num--; 10 | num |= (num >> 1); //Or first 2 bits 11 | num |= (num >> 2); //Or next 2 bits 12 | num |= (num >> 4); //Or next 4 bits 13 | num |= (num >> 8); //Or next 8 bits 14 | num |= (num >> 16); //Or next 16 bits 15 | num++; 16 | } 17 | 18 | return num; 19 | } 20 | 21 | export class ImageTexture extends AssetType { 22 | uri; 23 | texture; 24 | info = null; 25 | 26 | constructor(gl, uri) { 27 | super(gl); 28 | this.uri = uri; 29 | } 30 | 31 | load(gl) { 32 | this.texture = gl.createTexture(); 33 | 34 | const image = new Image(); 35 | image.src = this.uri; 36 | image.onload = () => { 37 | if (this.texture) { 38 | const { width, height } = image; 39 | const texWidth = powerOfTwo(width); 40 | const texHeight = powerOfTwo(height); 41 | 42 | gl.bindTexture(gl.TEXTURE_2D, this.texture); 43 | gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); 44 | if (texWidth === width && texHeight === height) { 45 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 46 | } else { 47 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, texWidth, texHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 48 | gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, image); 49 | } 50 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 51 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST); 52 | gl.generateMipmap(gl.TEXTURE_2D); 53 | 54 | const err = gl.getError(); 55 | if (err) { 56 | console.warn(`Load texture error: ${err}`); 57 | } 58 | 59 | if (__DEV__) { 60 | console.log(`Texture loaded: ${this.uri} ${width}x${height} (${texWidth}x${texHeight})`); 61 | } 62 | this.info = { 63 | width, 64 | height, 65 | texWidth, 66 | texHeight, 67 | }; 68 | } 69 | }; 70 | } 71 | 72 | get loaded() { 73 | return !!this.info; 74 | } 75 | 76 | unload(gl) { 77 | gl.deleteTexture(this.texture); 78 | this.texture = null; 79 | this.info = null; 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /src/client/libs/glsurface/native/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/18. 3 | */ 4 | 5 | export * from './GLSurface'; 6 | -------------------------------------------------------------------------------- /src/client/libs/nativeModules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/16. 3 | */ 4 | 5 | import 'uimanager'; 6 | import 'glsurface/native'; 7 | import 'stage2d'; 8 | -------------------------------------------------------------------------------- /src/client/libs/react/Children.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/17. 3 | */ 4 | 5 | export function map(children, func) { 6 | if (children == null || children === false) { 7 | return []; 8 | } 9 | if (Array.isArray(children)) { 10 | return children.map(func); 11 | } 12 | return [func(children, 0)]; 13 | } 14 | 15 | export function count(children) { 16 | if (children == null || children === false) { 17 | return 0; 18 | } 19 | if (Array.isArray(children)) { 20 | return children.length; 21 | } 22 | return 1; 23 | } 24 | -------------------------------------------------------------------------------- /src/client/libs/react/Component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/17. 3 | */ 4 | 5 | // No state! 6 | 7 | const scheduleUpdateComponents = []; 8 | 9 | let scheduleUpdateTimer = null; 10 | 11 | function scheduleUpdate(component) { 12 | scheduleUpdateComponents.push(component); 13 | if (!scheduleUpdateTimer) { 14 | scheduleUpdateTimer = setTimeout(() => { 15 | scheduleUpdateTimer = null; 16 | const components = scheduleUpdateComponents.splice(0); 17 | for (const comp of components) { 18 | if (comp.mount) { 19 | comp._willUpdate = false; 20 | comp.componentWillUpdate(comp.props); 21 | comp.mount.update(comp.render()); 22 | comp.componentDidUpdate(comp.props); 23 | } 24 | } 25 | }); 26 | } 27 | } 28 | 29 | export default class Component { 30 | mount = null; 31 | props = null; 32 | 33 | _willUpdate = false; 34 | 35 | constructor(props, children) { 36 | this.props = props; 37 | } 38 | 39 | // methods: 40 | forceUpdate() { 41 | if (!this._willUpdate) { 42 | this._willUpdate = true; 43 | scheduleUpdate(this); 44 | } 45 | } 46 | 47 | //life-cycle methods. 48 | componentWillMount() { 49 | 50 | } 51 | 52 | componentDidMount() { 53 | 54 | } 55 | 56 | componentWillUnmount() { 57 | 58 | } 59 | 60 | componentWillReceiveProps(newProps) { 61 | 62 | } 63 | 64 | shouldComponentUpdate(newProps) { 65 | return true; 66 | } 67 | 68 | componentWillUpdate(newProps) { 69 | 70 | } 71 | 72 | componentDidUpdate() { 73 | 74 | } 75 | 76 | //render() {} should be implemented. 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/client/libs/react/createClass.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/18. 3 | */ 4 | 5 | 6 | import Component from './Component'; 7 | 8 | // don't autobind these methods since they already have guaranteed context. 9 | const AUTOBIND_BLACKLIST = { 10 | constructor: 1, 11 | render: 1, 12 | shouldComponentUpdate: 1, 13 | componentWillReceiveProps: 1, 14 | componentWillUpdate: 1, 15 | componentDidUpdate: 1, 16 | componentWillMount: 1, 17 | componentDidMount: 1, 18 | componentWillUnmount: 1, 19 | componentDidUnmount: 1 20 | }; 21 | 22 | function extend(base, props, all) { 23 | for (let key in props) { 24 | if (all === true || props[key] == null) { 25 | base[key] = props[key]; 26 | } 27 | } 28 | return base; 29 | } 30 | 31 | function bindAll(ctx) { 32 | for (let i in ctx) { 33 | const v = ctx[i]; 34 | if (typeof v === 'function' && !v.__bound && !AUTOBIND_BLACKLIST[i]) { 35 | (ctx[i] = v.bind(ctx)).__bound = true; 36 | } 37 | } 38 | } 39 | 40 | function collateMixins(mixins, keyed) { 41 | for (let i = 0, len = mixins.length; i < len; i++) { 42 | const mixin = mixins[i]; 43 | 44 | // Surprise: Mixins can have mixins 45 | if (mixin.mixins) { 46 | // Recursively collate sub-mixins 47 | collateMixins(mixin.mixins, keyed); 48 | } 49 | 50 | for (let key in mixin) { 51 | if (mixin.hasOwnProperty(key) && typeof mixin[key] === 'function') { 52 | (keyed[key] || (keyed[key] = [])).push(mixin[key]); 53 | } 54 | } 55 | } 56 | return keyed; 57 | } 58 | 59 | function multihook(inst, hooks, mergeFn) { 60 | return function() { 61 | let ret; 62 | 63 | for (let i = 0, len = hooks.length; i < len; i ++) { 64 | const hook = hooks[i]; 65 | let r = hook.apply(this, arguments); 66 | 67 | if (mergeFn) { 68 | ret = mergeFn(ret, r); 69 | } else if (!isUndefined(r)) { 70 | ret = r; 71 | } 72 | } 73 | 74 | return ret; 75 | }; 76 | } 77 | 78 | function mergeNoDupes(previous, current) { 79 | if (!isUndefined(current)) { 80 | if (!isObject(current)) { 81 | throw new Error('Expected Mixin to return value to be an object or null.'); 82 | } 83 | 84 | if (!previous) { 85 | previous = {}; 86 | } 87 | 88 | for (let key in current) { 89 | if (current.hasOwnProperty(key)) { 90 | if (previous.hasOwnProperty(key)) { 91 | throw new Error(`Mixins return duplicate key ${key} in their return values`); 92 | } 93 | 94 | previous[key] = current[key]; 95 | } 96 | } 97 | } 98 | return previous; 99 | } 100 | 101 | function applyMixin(key, inst, mixin) { 102 | const hooks = isUndefined(inst[key]) ? mixin : mixin.concat(inst[key]); 103 | 104 | if (key === 'getDefaultProps' || key === 'getInitialState' || key === 'getChildContext') { 105 | inst[key] = multihook(inst, hooks, mergeNoDupes); 106 | } else { 107 | inst[key] = multihook(inst, hooks); 108 | } 109 | } 110 | 111 | function applyMixins(Cl, mixins) { 112 | for (let key in mixins) { 113 | if (mixins.hasOwnProperty(key)) { 114 | const mixin = mixins[key]; 115 | 116 | let inst; 117 | 118 | if (key === 'getDefaultProps') { 119 | inst = Cl; 120 | } else { 121 | inst = Cl.prototype; 122 | } 123 | 124 | if (isFunction(mixin[0])) { 125 | applyMixin(key, inst, mixin); 126 | } else { 127 | inst[key] = mixin; 128 | } 129 | } 130 | } 131 | } 132 | 133 | export default function createClass(obj) { 134 | class Cl extends Component { 135 | static defaultProps; 136 | static displayName = obj.displayName || 'Component'; 137 | static propTypes = obj.propTypes; 138 | static mixins = obj.mixins && collateMixins(obj.mixins); 139 | static getDefaultProps = obj.getDefaultProps; 140 | 141 | constructor(props, context) { 142 | super(props, context); 143 | bindAll(this); 144 | } 145 | 146 | replaceState(nextState, callback) { 147 | this.setState(nextState, callback); 148 | } 149 | 150 | isMounted() { 151 | return !this._unmounted; 152 | }; 153 | 154 | } 155 | 156 | extend(Cl.prototype, obj); 157 | 158 | if (obj.statics) { 159 | extend(Cl, obj.statics); 160 | } 161 | 162 | if (obj.mixins) { 163 | applyMixins(Cl, collateMixins(obj.mixins)); 164 | } 165 | 166 | Cl.defaultProps = Cl.getDefaultProps ? undefined : Cl.getDefaultProps(); 167 | 168 | return Cl; 169 | } -------------------------------------------------------------------------------- /src/client/libs/react/createJSX.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/16. 3 | */ 4 | 5 | export default function createJSX(type, props) { 6 | props = props || {}; 7 | if (arguments.length === 2) { 8 | // props.children = undefined; 9 | } else if (arguments.length === 3) { 10 | props.children = arguments[2]; 11 | } else { 12 | props.children = Array.prototype.slice.call(arguments, 2); 13 | } 14 | let key = props ? props.key : undefined; 15 | if (key !== undefined && typeof(key) !== 'string') { 16 | key = `${key}`; 17 | } 18 | return { 19 | type, 20 | key, 21 | ref: props.ref, 22 | props, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/client/libs/react/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/16. 3 | */ 4 | 5 | import renderRoot from './render'; 6 | import createJSX from './createJSX'; 7 | import * as Children from './Children'; 8 | import Component from './Component'; 9 | import createClass from './createClass'; 10 | 11 | export const PropTypes = { 12 | func: {}, 13 | object: {}, 14 | }; 15 | export const createElement = createJSX; 16 | export { 17 | Children, 18 | Component, 19 | 20 | renderRoot, 21 | createJSX, 22 | createClass, 23 | }; 24 | 25 | export default { 26 | Children, 27 | Component, 28 | PropTypes, 29 | 30 | renderRoot, 31 | createJSX, 32 | createElement: createJSX, 33 | createClass, 34 | }; 35 | -------------------------------------------------------------------------------- /src/client/libs/react/render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/16. 3 | */ 4 | import { UIManager } from 'bridge/NativeModules'; 5 | 6 | import { map } from './Children'; 7 | import Component from './Component'; 8 | import Module, {clientModule, method} from "../bridge/Module"; 9 | 10 | const { 11 | createView, 12 | moveView, 13 | setViewProps, 14 | destroyView, // for element & text & empty node. 15 | 16 | createTextNode, 17 | updateTextNode, 18 | 19 | createEmptyNode, 20 | } = UIManager; 21 | 22 | let nativeIdCounter = 0; 23 | const nativeIdRecycler = []; 24 | 25 | let eventIdCounter = 0; 26 | const eventIdRecycler = []; 27 | const eventIdRegistry = []; 28 | 29 | function allocEventId() { 30 | if (eventIdRecycler.length > 0) { 31 | return eventIdRecycler.pop(); 32 | } 33 | return ++eventIdCounter; 34 | } 35 | 36 | @clientModule 37 | class UIEventEmitter extends Module { 38 | 39 | @method 40 | emit(id, name, ...args) { 41 | const mount = eventIdRegistry[id]; 42 | if (!mount/* || !mount.jsx*/) { 43 | return; 44 | } 45 | const backName = name.replace(/^[a-z]/, v=>`on${v.toUpperCase()}`); 46 | const {props} = mount.jsx; 47 | const callback = props && props[backName]; 48 | if (callback && callback.call) { 49 | callback.apply(null, args); 50 | } 51 | } 52 | } 53 | 54 | function allocNativeId() { 55 | if (nativeIdCounter.length > 0) { 56 | return nativeIdCounter.pop(); 57 | } 58 | return ++nativeIdCounter; 59 | } 60 | 61 | const TextNode = {}; 62 | const EmptyNode = {}; 63 | 64 | function getType(jsx) { 65 | if (typeof jsx === 'string' || typeof jsx === 'number') { 66 | return TextNode; 67 | } 68 | if (jsx === null) { 69 | return EmptyNode; 70 | } 71 | return jsx.type; 72 | } 73 | 74 | const isEventReg = /^on([A-Z])/; 75 | 76 | function wrapEventName(key) { 77 | return key.replace(isEventReg, (m, v) => v.toLowerCase()); 78 | } 79 | 80 | function compareProps(newProps, oldProps, blacklist = EmptyNode) { 81 | const diff = { }; 82 | let haveDiff = false; 83 | 84 | const events = {}; 85 | let haveEvents = false; 86 | 87 | if (newProps) { 88 | for (const key of Object.keys(newProps)) { 89 | if (isEventReg.test(key)) { 90 | if (!oldProps || oldProps[key] == null) { 91 | // added event listener. 92 | events[wrapEventName(key)] = true; 93 | haveEvents = true; 94 | } 95 | } else if (newProps[key] !== (oldProps && oldProps[key]) && !blacklist[key]) { 96 | diff[key] = newProps[key]; 97 | haveDiff = true; 98 | } 99 | } 100 | } 101 | if (oldProps) { 102 | for (const key of Object.keys(oldProps)) { 103 | if (isEventReg.test(key)) { 104 | if (!newProps || newProps[key] == null) { 105 | // removed event listener. 106 | events[wrapEventName(key)] = false; 107 | haveEvents = true; 108 | } 109 | } else if ((!newProps || newProps[key] == null) && !blacklist[key]) { 110 | diff[key] = null; 111 | haveDiff = true; 112 | } 113 | } 114 | } 115 | if (haveEvents) { 116 | haveDiff = true; 117 | diff.events = events; 118 | } 119 | return haveDiff ? diff : null; 120 | } 121 | 122 | function blacklistProps(props, blacklist) { 123 | const ret = {}; 124 | let haveProp = false; 125 | const events = {}; 126 | let haveEvents = false; 127 | 128 | if (props) { 129 | for (const key of Object.keys(props)) { 130 | if (!blacklist[key]) { 131 | if (isEventReg.test(key)) { 132 | events[wrapEventName(key)] = true; 133 | haveEvents = true; 134 | } else { 135 | ret[key] = props[key]; 136 | haveProp = true; 137 | } 138 | } 139 | } 140 | } 141 | if (haveEvents) { 142 | haveProp = true; 143 | ret.events = events; 144 | } 145 | return haveProp ? ret : null; 146 | } 147 | 148 | function unmountAllChildren(container, children) { 149 | for (const child of children) { 150 | child.unmount(); 151 | } 152 | children.splice(0); // .clear(); 153 | } 154 | 155 | function lis_algorithm(arr) { 156 | const p = arr.slice(0); 157 | const result = [0]; 158 | let i; 159 | let j; 160 | let u; 161 | let v; 162 | let c; 163 | const len = arr.length; 164 | 165 | for (i = 0; i < len; i++) { 166 | let arrI = arr[i]; 167 | 168 | if (arrI === -1) { 169 | continue; 170 | } 171 | 172 | j = result[result.length - 1]; 173 | if (arr[j] < arrI) { 174 | p[i] = j; 175 | result.push(i); 176 | continue; 177 | } 178 | 179 | u = 0; 180 | v = result.length - 1; 181 | 182 | while (u < v) { 183 | c = ((u + v) / 2) | 0; 184 | if (arr[result[c]] < arrI) { 185 | u = c + 1; 186 | } else { 187 | v = c; 188 | } 189 | } 190 | 191 | if (arrI < arr[result[u]]) { 192 | if (u > 0) { 193 | p[i] = result[u - 1]; 194 | } 195 | result[u] = i; 196 | } 197 | } 198 | 199 | u = result.length; 200 | v = result[u - 1]; 201 | 202 | while (u-- > 0) { 203 | result[u] = v; 204 | v = p[v]; 205 | } 206 | 207 | return result; 208 | } 209 | 210 | function getChildrenMap(children, newChildren) { 211 | const targets = []; 212 | { 213 | // 计算每个组件对应的原下标。 214 | const keys = {}; 215 | const noKeys = []; 216 | 217 | for (let i = 0; i < newChildren.length; i++) { 218 | const child = newChildren[i]; 219 | const key = child.key; 220 | if (key) { 221 | keys[key] = i; 222 | } else { 223 | noKeys.push(i); 224 | } 225 | } 226 | 227 | for (let i = 0; i < children.length; i++) { 228 | const child = children[i]; 229 | const key = child.key; 230 | if (key) { 231 | targets.push(keys[key] === undefined ? -1 : keys[key]); 232 | } else { 233 | const v = noKeys.shift(); 234 | targets.push(v === undefined ? -1 : v); 235 | } 236 | } 237 | } 238 | return targets; 239 | } 240 | 241 | function updateChildrenArray(container, children, newChildrenJSX) { 242 | const oldChildren = [...children]; 243 | const targets = getChildrenMap(children, newChildrenJSX); 244 | const sources = getChildrenMap(newChildrenJSX, children); 245 | 246 | // lis同时就是保留且不移动的下标 247 | const lis = lis_algorithm(sources); 248 | 249 | // 先移除所有要移除的节点 250 | for (let i = children.length - 1; i >= 0; i--) { 251 | if (targets[i] === -1) { 252 | children[i].unmount(); 253 | } 254 | } 255 | 256 | children.splice(-1); 257 | 258 | // 从右侧开始整理所有的节点 259 | let lastMountId = null; 260 | 261 | for (let i = newChildrenJSX.length - 1; i >= 0; i--) { 262 | // 是否是直接更新的组件 263 | const jsx = newChildrenJSX[i]; 264 | let mount = null; 265 | if (lis[lis.length - 1] === i) { 266 | // 不移动 267 | mount = oldChildren[sources[i]]; 268 | lis.pop(); 269 | mount.update(jsx); 270 | } else if (sources[i] >= 0) { 271 | mount = oldChildren[sources[i]]; 272 | mount.moveTo(container, lastMountId); 273 | mount.update(jsx); 274 | } else { 275 | mount = new ReactMountRoot(); 276 | mount.mount(jsx, container, lastMountId); 277 | } 278 | 279 | lastMountId = mount.nativeId; 280 | children[i] = mount; 281 | } 282 | } 283 | 284 | function updateChildren(container, children, _newChildrenJSX) { 285 | 286 | // 目标是个数组。 287 | let newChildrenJSX = _newChildrenJSX; 288 | 289 | if (Array.isArray(newChildrenJSX)) { 290 | newChildrenJSX = newChildrenJSX.filter(v => (v!== false && v != null)); 291 | if (newChildrenJSX.length <= 1) { 292 | newChildrenJSX = newChildrenJSX[0]; 293 | } 294 | } 295 | 296 | if (newChildrenJSX === false || newChildrenJSX == null) { 297 | // no children after update. 298 | if (children.length) { 299 | unmountAllChildren(container, children); 300 | } 301 | } else if (!children.length) { 302 | // no children before update. 303 | if (!Array.isArray(newChildrenJSX)) { 304 | const ret = new ReactMountRoot(); 305 | ret.mount(newChildrenJSX, container); 306 | children.push(ret); 307 | } else { 308 | let lastMounted = null; 309 | for (const child of newChildrenJSX) { 310 | const ret = new ReactMountRoot(); 311 | ret.mount(child, container); 312 | if (lastMounted) { 313 | lastMounted.setBefore(ret.nativeId); 314 | } 315 | lastMounted = ret; 316 | children.push(ret); 317 | } 318 | } 319 | } else if (!Array.isArray(newChildrenJSX)){ 320 | let mount = null; 321 | if (newChildrenJSX.key) { 322 | // 有key,直接找到对应的节点来update。找不到就重新创建。 323 | mount = children.find(v => v.key === newChildrenJSX.key) || new ReactMountRoot(); 324 | } else { 325 | const type = getType(newChildrenJSX); 326 | if (type === TextNode) { 327 | // 文本,优先寻找完全一样的,减少更新。 328 | mount = children.find(v => v.jsx === newChildrenJSX); 329 | } 330 | // 随便找一个可用于更新的节点。 331 | mount = mount || children.find(v => !v.key); 332 | } 333 | 334 | mount = mount || new ReactMountRoot(); 335 | 336 | for (const child of children) { 337 | if (child !== mount) { 338 | child.unmount(); 339 | } 340 | } 341 | children.splice(0); // .clear(); 342 | children.push(mount); 343 | mount.update(newChildrenJSX); 344 | } else { 345 | // 目标是个数组了。 346 | updateChildrenArray(container, children, newChildrenJSX); 347 | } 348 | } 349 | 350 | export class ReactMount { 351 | // null if mount to document.body, or number if mount to other dom. 352 | // or string which presents a query selector. 353 | container = null; 354 | 355 | before = null; 356 | 357 | nativeId = null; 358 | 359 | // will be object if component, or number if dom 360 | instance = null; 361 | 362 | // last updated jsx. 363 | jsx = null; 364 | 365 | children = null; 366 | 367 | constructor(nativeId) { 368 | this.nativeId = nativeId; 369 | } 370 | 371 | get key() { 372 | if (!this.jsx) { 373 | return undefined; 374 | } 375 | return this.jsx.key; 376 | } 377 | 378 | mount(jsx, container = null, before = null) { 379 | this.container = container; 380 | this.before = before; 381 | 382 | let type = getType(jsx); 383 | 384 | while (typeof type === 'function' && !(type.prototype instanceof Component)) { 385 | // stateless functional component 386 | jsx = type(jsx.props); 387 | type = getType(jsx); 388 | } 389 | 390 | if (type === TextNode) { 391 | createTextNode(this.nativeId, container, jsx, before); 392 | } else if (type === EmptyNode) { 393 | createEmptyNode(this.nativeId, container, before); 394 | } else if (typeof(type) === 'string') { 395 | // dom 396 | const setProps = blacklistProps(jsx.props, { 397 | children: true, 398 | ref: true, 399 | key: true, 400 | }); 401 | const eventId = this.instance = allocEventId(); 402 | 403 | createView(this.nativeId, eventId, container, setProps, jsx.type, before); 404 | eventIdRegistry[eventId] = this; 405 | // if (setProps) { 406 | // setViewProps(this.nativeId, setProps); 407 | // } 408 | let last = null; 409 | this.children = map(jsx.props.children, jsx => { 410 | const ret = new ReactMountRoot(); 411 | ret.mount(jsx, this.nativeId); 412 | if (last) { 413 | last.setBefore(ret.nativeId); 414 | } 415 | last = ret; 416 | return ret; 417 | }); 418 | } else { 419 | // composite component 420 | this.instance = new jsx.type(jsx.props); 421 | this.instance.componentWillMount(); 422 | // rendered composite component use same native Id with parent component. 423 | this.instance.mount = new ReactMount(this.nativeId); 424 | this.instance.mount.mount(this.instance.render(), container, before); 425 | this.instance.componentDidMount(); 426 | } 427 | 428 | this.jsx = jsx; 429 | if (jsx.ref) { 430 | jsx.ref(this.instance); 431 | } 432 | } 433 | 434 | unmount() { 435 | const type = getType(this.jsx); 436 | if (this.jsx.ref) { 437 | this.jsx.ref(null); 438 | } 439 | if (typeof type === 'string') { 440 | unmountAllChildren(this.nativeId, this.children); 441 | } 442 | if (typeof type !== 'function') { 443 | const eventId = this.instance; 444 | this.instance = null; 445 | delete eventIdRegistry[eventId]; 446 | destroyView(this.nativeId, this.container) 447 | .then(() => { 448 | eventIdRecycler.push(eventId); 449 | }); 450 | } else { 451 | // composite components 452 | this.instance.componentWillUnmount(); 453 | this.instance.mount.unmount(); 454 | this.instance = null; 455 | } 456 | this.jsx = null; 457 | } 458 | 459 | update(newJsx) { 460 | let type = getType(newJsx); 461 | if (type !== getType(this.jsx) || newJsx.key !== this.jsx.key) { 462 | this.unmount(); 463 | this.mount(newJsx, this.container, this.before); 464 | return false; 465 | } 466 | if (newJsx.ref !== this.jsx.ref) { 467 | if (this.jsx.ref) { 468 | this.jsx.ref(null); 469 | } 470 | if (newJsx.ref) { 471 | this.jsx.ref(this.instance); 472 | } 473 | } 474 | while (typeof type === 'function' && !(type.prototype instanceof Component)) { 475 | // stateless functional component 476 | newJsx = type(newJsx.props); 477 | type = getType(newJsx); 478 | } 479 | 480 | if (type === TextNode) { 481 | updateTextNode(this.nativeId, newJsx); 482 | } else if (type === EmptyNode) { 483 | // Do nothing for empty node. 484 | } else if (typeof type === 'string') { 485 | const diffStyle = compareProps(newJsx.props && newJsx.props.style, this.jsx.props && this.jsx.props.style); 486 | let diffProps = compareProps(newJsx.props, this.jsx.props, { 487 | children: true, 488 | ref: true, 489 | key: true, 490 | style: true, 491 | }); 492 | 493 | if (diffStyle) { 494 | diffProps = diffProps || {}; 495 | diffProps.style = diffStyle; 496 | } 497 | if (diffProps) { 498 | setViewProps(this.nativeId, this.instance, diffProps); 499 | } 500 | updateChildren(this.nativeId, this.children, newJsx.props.children); 501 | } else { 502 | // composite component 503 | this.instance.componentWillReceiveProps(newJsx.props); 504 | if (this.instance.shouldComponentUpdate(newJsx.props)) { 505 | this.instance.componentWillUpdate(newJsx.props); 506 | this.instance.props = newJsx.props; 507 | } else { 508 | this.instance.props = newJsx.props; 509 | } 510 | } 511 | this.jsx = newJsx; 512 | return true; 513 | } 514 | 515 | moveTo(container, before) { 516 | if (typeof(this.instance) !== 'number') { 517 | this.instance.mount.moveTo(container, before); 518 | } else { 519 | moveView(this.nativeId, container, before); 520 | } 521 | 522 | this.container = container; 523 | this.before = before; 524 | } 525 | 526 | // 仅在尾部节点后面又插入节点的额情况下用 527 | setBefore(before) { 528 | if (typeof(this.instance) !== 'number') { 529 | this.instance.mount.setBefore(before); 530 | } 531 | this.before = before; 532 | } 533 | } 534 | 535 | export class ReactMountRoot extends ReactMount { 536 | constructor() { 537 | super(allocNativeId()); 538 | } 539 | 540 | unmount() { 541 | super.unmount(); 542 | nativeIdRecycler.push(this.nativeId); 543 | } 544 | } 545 | 546 | export default function renderRoot(jsx, container = null) { 547 | const { type, props } = jsx; 548 | 549 | const ret = new ReactMountRoot(); 550 | ret.mount(jsx, container); 551 | 552 | return ret; 553 | } 554 | -------------------------------------------------------------------------------- /src/client/libs/stage2d/BasicAnimation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 3/26/17. 3 | */ 4 | 5 | import { GLNode } from 'glsurface/native/GLSurface'; 6 | import { nativeComponent, prop } from 'uimanager'; 7 | 8 | function setAnimationInterval(onFrame) { 9 | let timer; 10 | function nextFrame() { 11 | if (timer !== null) { 12 | setNextFrame(); 13 | onFrame(); 14 | } 15 | } 16 | function setNextFrame() { 17 | timer = requestAnimationFrame(nextFrame); 18 | } 19 | setNextFrame(); 20 | return () => { 21 | cancelAnimationFrame(timer); 22 | timer = null; 23 | } 24 | } 25 | 26 | @nativeComponent('gl-2d-basic-animation') 27 | export class BasicAnimation extends GLNode { 28 | texture = null; 29 | uri = null; 30 | 31 | currentFrame = 0; 32 | disposeTimer = null; 33 | 34 | _interval = 0; 35 | _animationData = null; 36 | 37 | @prop r = 1; 38 | @prop g = 1; 39 | @prop b = 1; 40 | @prop a = 1; 41 | 42 | @prop 43 | tileW = 0; 44 | 45 | @prop 46 | tileH = 0; 47 | 48 | @prop 49 | columns = 1; // 有多少列 50 | 51 | @prop 52 | set interval(value) { 53 | this._interval = value; 54 | this.resetTimer(); 55 | } 56 | 57 | @prop 58 | set animationData(value) { 59 | this._animationData = value; 60 | this.resetTimer(); 61 | } 62 | 63 | resetTimer() { 64 | if (this.disposeTimer !== null) { 65 | this.disposeTimer(); 66 | } 67 | this.currentFrame = 0; 68 | if (this._interval > 0) { 69 | const timer = setInterval(this.onFrame, this._interval); 70 | this.disposeTimer = () => { 71 | clearInterval(timer); 72 | } 73 | } else { 74 | this.disposeTimer = setAnimationInterval(this.onFrame); 75 | } 76 | } 77 | 78 | onFrame = () => { 79 | if (++this.currentFrame >= this._animationData.length) { 80 | this.currentFrame = 0; 81 | } 82 | } 83 | 84 | @prop 85 | set src(value) { 86 | this.releaseTexture(); 87 | this.uri = value; 88 | } 89 | 90 | unmount() { 91 | this.releaseTexture(); 92 | if (this.disposeTimer) { 93 | this.disposeTimer(); 94 | this.disposeTimer = null; 95 | } 96 | } 97 | 98 | releaseTexture() { 99 | if (this.texture) { 100 | this.texture.release(); 101 | this.texture = null; 102 | } 103 | } 104 | 105 | renderGL(gl) { 106 | if (!this.texture && this.uri) { 107 | this.texture = gl.imageTextureManager.obtain(gl, this.uri); 108 | } 109 | if (!this.texture.loaded || !this._animationData || this.currentFrame >= this._animationData.length) { 110 | return; 111 | } 112 | const frameId = this._animationData[this.currentFrame]; 113 | const tx = this.tileW * (frameId % this.columns); 114 | const ty = this.tileH * ((frameId / this.columns) | 0); 115 | 116 | gl.painter2d.drawTexture( 117 | gl, 118 | this.texture.texture, 119 | -0.5, -0.5, 1, 1, 120 | tx, ty, this.tileW, this.tileH, 121 | this.r, this.g, this.b, this.a, 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/client/libs/stage2d/geometry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 3/26/17. 3 | */ 4 | 5 | import { GLNode, GLContainer } from 'glsurface/native'; 6 | import { nativeComponent, prop } from 'uimanager'; 7 | import {translate2D, scale2D, rotate2D} from 'glsurface/common/matrix'; 8 | 9 | @nativeComponent('gl-2d-layer') 10 | export class GLLayer extends GLContainer { 11 | // defaults to -1 to 1 12 | @prop x = 0; 13 | @prop y = 0; 14 | @prop width = 2; 15 | @prop height = 2; 16 | 17 | renderGL(gl) { 18 | gl.matrixStack.pushOrtho2D(this.width, this.height, this.x, this.y); 19 | super.renderGL(gl); 20 | gl.matrixStack.pop(); 21 | } 22 | } 23 | 24 | @nativeComponent('gl-2d-node') 25 | export class GLNode2D extends GLContainer { 26 | // defaults to -1 to 1 27 | @prop x = 0; 28 | @prop y = 0; 29 | @prop rotate = 0; 30 | @prop scaleX = 1; 31 | @prop scaleY = 1; 32 | 33 | renderGL(gl) { 34 | const { matrixStack } = gl; 35 | matrixStack.push(); 36 | const { top } = matrixStack; 37 | scale2D(top, this.scaleX, this.scaleY); 38 | rotate2D(top, this.rotate); 39 | translate2D(top, this.x, this.y); 40 | 41 | super.renderGL(gl); 42 | matrixStack.pop(); 43 | } 44 | } 45 | 46 | @nativeComponent('gl-2d-rect') 47 | export class GLRect extends GLNode { 48 | @prop x = 0; 49 | @prop y = 0; 50 | @prop w = 0; 51 | @prop h = 0; 52 | 53 | @prop r = 1; 54 | @prop g = 1; 55 | @prop b = 1; 56 | @prop a = 1; 57 | 58 | renderGL(gl) { 59 | gl.painter2d.drawRect( 60 | gl, 61 | this.x, this.y, this.w, this.h, 62 | this.r, this.g, this.b, this.a, 63 | ); 64 | } 65 | } 66 | 67 | @nativeComponent('gl-2d-image') 68 | export class GLImage extends GLRect { 69 | texture = null; 70 | uri = null; 71 | 72 | @prop tx = 0; 73 | @prop ty = 0; 74 | @prop tw = 1; 75 | @prop th = 1; 76 | 77 | @prop 78 | set src(value) { 79 | this.releaseTexture(); 80 | this.uri = value; 81 | } 82 | 83 | unmount() { 84 | this.releaseTexture(); 85 | } 86 | 87 | releaseTexture() { 88 | if (this.texture) { 89 | this.texture.release(); 90 | this.texture = null; 91 | } 92 | } 93 | 94 | renderGL(gl) { 95 | if (!this.texture && this.uri) { 96 | this.texture = gl.imageTextureManager.obtain(gl, this.uri); 97 | } 98 | if (this.texture.loaded) { 99 | gl.painter2d.drawTexture( 100 | gl, 101 | this.texture.texture, 102 | this.x, this.y, this.w, this.h, 103 | this.tx, this.ty, this.tw, this.th, 104 | this.r, this.g, this.b, this.a, 105 | ); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /src/client/libs/stage2d/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 3/26/17. 3 | */ 4 | 5 | export * from './geometry'; 6 | import BasicAnimation from './BasicAnimation'; 7 | 8 | export { 9 | BasicAnimation, 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /src/client/libs/uimanager/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/15. 3 | */ 4 | 5 | import Module, { serverModule, method, asyncMethod } from '../bridge/Module'; 6 | 7 | let postEvent = function (eventId, name, ...args) { 8 | // Should have remote event here. 9 | const { remoteModules: { 10 | UIEventEmitter, 11 | } } = global.__bridgeServer; 12 | 13 | postEvent = function(eventId, name, ...args) { 14 | UIEventEmitter.emit(eventId, name, ...args); 15 | }; 16 | return postEvent.call(null, eventId, name, ...args); 17 | }; 18 | 19 | export class NativeComponent { 20 | reactId = null; 21 | eventId = null; 22 | events = {}; 23 | 24 | constructor(id, eventId) { 25 | this.reactId = id; 26 | this.eventId = eventId; 27 | } 28 | 29 | sendEvent(eventName, ...args) { 30 | const v = this.events[eventName]; 31 | if (v) { 32 | postEvent(this.eventId, eventName, ...args); 33 | } 34 | } 35 | 36 | setViewProps(props) { 37 | const { propFields, defaultProps } = this.constructor; 38 | if (propFields) { 39 | for (const k in props) { 40 | if (propFields[k]) { 41 | if (props[k] === null) { 42 | this[k] = defaultProps[k]; 43 | } else { 44 | this[k] = props[k]; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | mount(parentNode, before) { 52 | } 53 | 54 | unmount() { 55 | 56 | } 57 | 58 | setAttribute(key, value) { 59 | } 60 | } 61 | 62 | export class NativeElementComponent extends NativeComponent { 63 | el = null; 64 | 65 | constructor(id, eventId, tagName) { 66 | super(id, eventId, tagName); 67 | 68 | this.el = document.createElement(tagName); 69 | if (__DEV__) { 70 | this.el.setAttribute('data-react-id', id); 71 | this.el.setAttribute('data-react-event-id', eventId); 72 | } 73 | } 74 | 75 | insertBefore(el, ref) { 76 | this.el.insertBefore(el, ref); 77 | } 78 | 79 | appendChild(el) { 80 | this.el.appendChild(el); 81 | } 82 | 83 | setViewProps(props) { 84 | for (const key of Object.keys(props)) { 85 | const value = props[key]; 86 | 87 | switch (key) { 88 | case 'style': { 89 | this.updateStyle(value); 90 | break; 91 | } 92 | case 'events': { 93 | for (const name of Object.keys(value)) { 94 | if (value[name]) { 95 | const f = this.events[name] = (ev) => { 96 | postEvent(this.eventId, name); 97 | }; 98 | this.el.addEventListener(name.toLowerCase(), f); 99 | } else { 100 | this.el.removeEventListener(name.toLowerCase(), this.events[name]); 101 | delete this.events[name]; 102 | } 103 | } 104 | break; 105 | } 106 | default:{ 107 | if (value === null) { 108 | this.removeAttribute(key); 109 | } else { 110 | this.setAttribute(key, value); 111 | } 112 | break; 113 | } 114 | } 115 | } 116 | } 117 | 118 | updateStyle(style) { 119 | for (const name of Object.keys(style)) { 120 | let styleValue = style[name]; 121 | if (typeof(styleValue) === 'number') { 122 | styleValue = `${styleValue}px`; 123 | } 124 | this.el.style[name] = styleValue; 125 | } 126 | } 127 | 128 | setAttribute(key, value) { 129 | this.el.setAttribute(key, value); 130 | } 131 | 132 | removeAttribute(key) { 133 | this.el.removeAttribute(key); 134 | } 135 | 136 | mount(parentNode, before) { 137 | if (before) { 138 | parentNode.insertBefore(this.el, before); 139 | } else { 140 | parentNode.appendChild(this.el); 141 | } 142 | } 143 | 144 | unmount() { 145 | this.el.parentNode.removeChild(this.el); 146 | } 147 | 148 | removeChild(el) { 149 | this.el.removeChild(el); 150 | } 151 | } 152 | 153 | export function nativeComponent(name) { 154 | return target => { 155 | UIManager.instance.nativeComponents[name] = target; 156 | }; 157 | } 158 | 159 | export function prop(target, name, args) { 160 | const defaultValue = args.value || (args.initializer && args.initializer()); 161 | const clazz = target.constructor; 162 | 163 | if (clazz.hasOwnProperty('propFields')){ 164 | clazz.propFields[name] = true; 165 | } else { 166 | Object.defineProperty(clazz, 'propFields', { 167 | configurable: true, 168 | enumerable: false, 169 | value: { 170 | ...clazz.__proto__.propFields, 171 | [name]: true 172 | }, 173 | }) 174 | } 175 | 176 | if (clazz.hasOwnProperty('defaultProps')){ 177 | clazz.defaultProps[name] = defaultValue; 178 | } else { 179 | Object.defineProperty(clazz, 'defaultProps', { 180 | configurable: true, 181 | enumerable: false, 182 | value: { 183 | ...clazz.__proto__.defaultProps, 184 | [name]: defaultValue 185 | }, 186 | }) 187 | } 188 | } 189 | 190 | @serverModule 191 | export default class UIManager extends Module { 192 | nativeComponents = {}; 193 | 194 | viewRegistry = []; 195 | 196 | @method 197 | createView(id, eventId, container, initialProps, tagName, before) { 198 | const Clazz = this.nativeComponents[tagName]; 199 | const el = Clazz ? new Clazz(id, eventId) : new NativeElementComponent(id, eventId, tagName) 200 | this.viewRegistry[id] = el; 201 | if (initialProps) { 202 | el.setViewProps(initialProps); 203 | } 204 | this.mountView(container, before, el); 205 | } 206 | 207 | @method 208 | moveView(id, container, before) { 209 | const el = this.viewRegistry[id]; 210 | if (el instanceof NativeComponent) { 211 | el.unmount(); 212 | } else { 213 | el.parentNode.removeChild(el); 214 | } 215 | this.mountView(container, before, el); 216 | } 217 | 218 | mountView(container, before, el) { 219 | let parent = null; 220 | switch (typeof(container)) { 221 | case 'number': 222 | parent = this.viewRegistry[container]; 223 | break; 224 | case 'object': // include null 225 | parent = document.body; 226 | break; 227 | case 'string': 228 | parent = document.querySelector(container); 229 | break; 230 | } 231 | if (el instanceof NativeComponent) { 232 | el.mount(parent, before && this.viewRegistry[before]); 233 | return; 234 | } 235 | if (before) { 236 | parent.insertBefore(el, this.viewRegistry[before]); 237 | } else { 238 | parent.appendChild(el); 239 | } 240 | } 241 | 242 | @method 243 | createTextNode(id, container, value, before) { 244 | const el = document.createTextNode(value); 245 | this.mountView(container, before, el); 246 | this.viewRegistry[id] = el; 247 | } 248 | 249 | @method 250 | updateTextNode(id, value) { 251 | const el = this.viewRegistry[id]; 252 | el.data = value; 253 | } 254 | 255 | @method 256 | createEmptyNode(id, container, before) { 257 | const el = __DEV__ ? document.createComment(`react-id=${id}`) : document.createComment(); 258 | this.mountView(container, before, el); 259 | this.viewRegistry[id] = el; 260 | } 261 | 262 | @method 263 | setViewProps(id, eventId, props) { 264 | const view = this.viewRegistry[id]; 265 | if (view instanceof NativeComponent) { 266 | view.setViewProps(props); 267 | return; 268 | } 269 | updateProps(view, id, eventId, props); 270 | } 271 | 272 | @asyncMethod 273 | destroyView(id, parent) { 274 | const view = this.viewRegistry[id]; 275 | const parentNode = typeof(parent) === 'number' ? this.viewRegistry[id] : view.parentNode; 276 | if (view instanceof NativeComponent) { 277 | view.unmount(); 278 | } else { 279 | parentNode.removeChild(view); 280 | } 281 | delete this.viewRegistry[id]; 282 | } 283 | } 284 | 285 | -------------------------------------------------------------------------------- /src/client/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/16. 3 | */ 4 | 5 | import { observable } from 'mobx'; 6 | import { observer } from 'mobx-react/custom'; 7 | import { Component, renderRoot, createJSX } from 'react'; 8 | 9 | @observer 10 | class Chicken extends Component { 11 | 12 | static animationData = [ 13 | [0,1,2,1], 14 | [3,4,5,4], 15 | [6,7,8,7], 16 | [9,10,11,10], 17 | ]; 18 | 19 | @observable 20 | direction = 0; 21 | 22 | render() { 23 | return ( 24 | 32 | ); 33 | } 34 | } 35 | 36 | @observer 37 | class App extends Component { 38 | @observable 39 | surfaceInfo = {}; 40 | 41 | onSizeChanged = info => { 42 | console.log(`GLSurface Size: ${info.width}x${info.height} Ratio: ${info.ratio}`) 43 | this.surfaceInfo = info; 44 | }; 45 | 46 | onClick = () => { 47 | this.chicken.direction = (this.chicken.direction + 1) % 4; 48 | } 49 | 50 | onChickenRef = ref => { 51 | this.chicken = ref; 52 | }; 53 | 54 | render() { 55 | const { width, height } = this.surfaceInfo; 56 | 57 | return ( 58 |
67 | 75 | 76 | {/**/} 77 | 78 | {/**/} 79 | {/**/} 80 | {/**/} 81 | {/**/} 82 | 83 | 84 |
91 | 107 |
108 |
109 | ); 110 | } 111 | } 112 | 113 | const mount = renderRoot( 114 | 115 | ); 116 | -------------------------------------------------------------------------------- /src/index.server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/14. 3 | */ 4 | 5 | import Koa from 'koa'; 6 | 7 | const app = new Koa(); 8 | 9 | if (__DEV__) { 10 | require('./server/serveStatic').install(app); 11 | } 12 | 13 | app.listen(3000, () => { 14 | console.log('Ready.'); 15 | }); 16 | -------------------------------------------------------------------------------- /src/index.web.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/14. 3 | */ 4 | 5 | import Bridge from 'bridge/Bridge'; 6 | const bridge = global.__bridgeServer = new Bridge(); 7 | 8 | require('./client/libs/nativeModules'); 9 | 10 | if (global.Worker) { 11 | const w = new Worker('/ww.bundle.js'); 12 | bridge.install(w); 13 | console.log('Server installed.'); 14 | } else { 15 | require.ensure([], () => { 16 | require('./client/entry'); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/server/serveStatic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/15. 3 | */ 4 | 5 | import KoaStatic from 'koa-static'; 6 | 7 | export function install(app) { 8 | app.use(KoaStatic(`./build/${__DEV__ ? 'debug' : 'release'}/web`, { 9 | defer: true, 10 | })); 11 | } -------------------------------------------------------------------------------- /src/webworker.web.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by tdzl2003 on 2017/3/14. 3 | */ 4 | 5 | console.log('Client running in web worker.'); 6 | 7 | require('./client/entry'); 8 | --------------------------------------------------------------------------------