├── .gitignore
├── src
├── client
│ ├── libs
│ │ ├── glsurface
│ │ │ ├── native
│ │ │ │ ├── index.js
│ │ │ │ ├── AssetsManager.js
│ │ │ │ ├── Texture.js
│ │ │ │ ├── Effect.js
│ │ │ │ ├── GLSurface.js
│ │ │ │ └── BatchDraw2D.js
│ │ │ └── common
│ │ │ │ └── matrix.js
│ │ ├── bridge
│ │ │ ├── NativeModules.js
│ │ │ ├── Module.js
│ │ │ └── Bridge.js
│ │ ├── nativeModules.js
│ │ ├── stage2d
│ │ │ ├── index.js
│ │ │ ├── geometry.js
│ │ │ └── BasicAnimation.js
│ │ ├── react
│ │ │ ├── Children.js
│ │ │ ├── createJSX.js
│ │ │ ├── index.js
│ │ │ ├── Component.js
│ │ │ ├── createClass.js
│ │ │ └── render.js
│ │ └── uimanager
│ │ │ └── index.js
│ ├── assets
│ │ ├── effects
│ │ │ ├── base.fragment.shader
│ │ │ ├── base.vertex.shader
│ │ │ ├── baseWithTexture.fragment.shader
│ │ │ ├── baseWithTexture.vertex.shader
│ │ │ ├── base.effect.js
│ │ │ └── baseWithTexture.effect.js
│ │ └── images
│ │ │ └── chicken.png
│ ├── entry.js
│ └── main.js
├── webworker.web.js
├── server
│ └── serveStatic.js
├── index.server.js
└── index.web.js
├── bin
└── server.js
├── config
├── index.html.ejs
├── webpack.server.config.js
├── webpack.webworker.config.js
└── webpack.client.config.js
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | .idea
3 | node_modules
4 | yarn.lock
5 | npm-debug*
6 |
--------------------------------------------------------------------------------
/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/assets/effects/base.fragment.shader:
--------------------------------------------------------------------------------
1 | varying lowp vec4 vDiffuse;
2 |
3 | void main(void) {
4 | gl_FragColor = vDiffuse;
5 | }
6 |
--------------------------------------------------------------------------------
/src/client/assets/images/chicken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tdzl2003/react-game-engine-experimental/HEAD/src/client/assets/images/chicken.png
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/config/index.html.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= htmlWebpackPlugin.options.title %>
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------