├── .gitignore
├── public
├── img
│ ├── bg.jpg
│ └── card.png
└── index.html
├── README.md
├── src
├── components
│ ├── typography.js
│ ├── ui
│ │ ├── Text.js
│ │ ├── View.js
│ │ ├── Filter.js
│ │ ├── Sprite.js
│ │ ├── Button.js
│ │ ├── Touchable.js
│ │ └── TextInput.js
│ └── pixi
│ │ ├── PIXIViewport.js
│ │ ├── PIXIView.js
│ │ └── PIXITouchable.js
├── lib
│ ├── InteractionManager.js
│ └── subform.js
├── index.js
└── screens
│ └── multiplayer.js
├── webpack.config.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/public/img/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roman01la/subform-webgl/master/public/img/bg.jpg
--------------------------------------------------------------------------------
/public/img/card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/roman01la/subform-webgl/master/public/img/card.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [Custom React renderer](https://github.com/michalochman/react-pixi-fiber) on top of [PixiJS](http://www.pixijs.com/) with [Subform’s layout engine](https://github.com/lynaghk/subform-layout) targeting WebGL
2 |
3 | [DEMO](https://roman01la.github.io/subform-webgl/)
4 |
--------------------------------------------------------------------------------
/src/components/typography.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Text from "./ui/Text";
3 |
4 | export const H1 = ({ children }) => (
5 | {children}
6 | );
7 | export const H2 = ({ children }) => (
8 | {children}
9 | );
10 | export const H3 = ({ children }) => (
11 | {children}
12 | );
13 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/components/ui/Text.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Text as PIXIText } from "react-pixi-fiber";
3 | import PIXIView from "../pixi/PIXIView";
4 |
5 | class Text extends Component {
6 | render() {
7 | const { children, style } = this.props;
8 |
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | export default Text;
18 |
--------------------------------------------------------------------------------
/src/components/ui/View.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PIXIView from "../pixi/PIXIView";
3 |
4 | class View extends Component {
5 | render() {
6 | const { children, ...props } = this.props;
7 | return (
8 | {
11 | this.inst = inst;
12 | }}
13 | >
14 | {typeof children === "function" ? children(this.inst) : children}
15 |
16 | );
17 | }
18 | }
19 |
20 | export default View;
21 |
--------------------------------------------------------------------------------
/src/lib/InteractionManager.js:
--------------------------------------------------------------------------------
1 | class InteractionManager {
2 | viewInFocus = undefined;
3 | isViewInFocus(view) {
4 | return this.viewInFocus === view;
5 | }
6 | setViewInFocus(view) {
7 | if (this.viewInFocus !== undefined) {
8 | const { onFocusOut } = this.viewInFocus;
9 | if (typeof onFocusOut === "function") {
10 | onFocusOut();
11 | delete this.viewInFocus.onFocusOut;
12 | }
13 | }
14 |
15 | this.viewInFocus = view;
16 | }
17 | }
18 |
19 | export const im = new InteractionManager();
20 |
21 | export default InteractionManager;
22 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | entry: "./src/index.js",
5 | output: {
6 | path: path.resolve(__dirname, "public/js"),
7 | filename: "bundle.js"
8 | },
9 | module: {
10 | rules: [
11 | {
12 | test: /\.js$/,
13 | exclude: /node_modules/,
14 | use: {
15 | loader: "babel-loader",
16 | options: {
17 | presets: [
18 | "babel-preset-env",
19 | "babel-preset-react",
20 | "babel-preset-stage-2"
21 | ]
22 | }
23 | }
24 | }
25 | ]
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wui",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack --mode=development --watch",
8 | "build": "webpack --mode=production"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "babel-core": "^6.26.0",
15 | "babel-loader": "^7.1.4",
16 | "babel-preset-env": "^1.6.1",
17 | "babel-preset-react": "^6.24.1",
18 | "babel-preset-stage-2": "^6.24.1",
19 | "pixi-filters": "^2.6.0",
20 | "pixi-viewport": "^1.5.0",
21 | "pixi.js": "^4.7.1",
22 | "react": "^16.3.0",
23 | "react-dom": "^16.3.0",
24 | "react-pixi-fiber": "^0.4.3",
25 | "webpack": "^4.4.1",
26 | "webpack-cli": "^2.0.13",
27 | "yy-fps": "^0.6.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/pixi/PIXIViewport.js:
--------------------------------------------------------------------------------
1 | import { CustomPIXIComponent } from "react-pixi-fiber";
2 | import Viewport from "pixi-viewport";
3 |
4 | const PIXIViewport = CustomPIXIComponent(
5 | {
6 | customDisplayObject: ({
7 | screenWidth,
8 | screenHeight,
9 | worldWidth,
10 | worldHeight
11 | }) =>
12 | new Viewport({
13 | screenWidth,
14 | screenHeight,
15 | worldWidth,
16 | worldHeight
17 | }),
18 | customApplyProps: function(instance, oldProps, newProps) {
19 | const { x, y } = newProps;
20 |
21 | instance.left = -x;
22 | instance.top = -y;
23 |
24 | instance.wheel();
25 |
26 | if (newProps.follow !== undefined) {
27 | instance.follow(newProps.follow);
28 | }
29 | }
30 | },
31 | "PIXIViewport"
32 | );
33 |
34 | export default PIXIViewport;
35 |
--------------------------------------------------------------------------------
/src/components/ui/Filter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import { filters as pixiFilters } from "pixi.js";
3 |
4 | const filters = {
5 | blur: {
6 | fclass: pixiFilters.BlurFilter,
7 | props: ["blur", "quality", "resolution", "padding"]
8 | }
9 | };
10 |
11 | class Filter extends Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | const FilterClass = filters[props.type].fclass;
16 | const filterProps = filters[props.type].props;
17 | const filter = new FilterClass();
18 |
19 | filterProps.forEach(prop => {
20 | const value = props[prop];
21 | if (value !== undefined) {
22 | filter[prop] = props[prop];
23 | }
24 | });
25 |
26 | this.state = {
27 | filter
28 | };
29 | }
30 | render() {
31 | const { children, type, ...props } = this.props;
32 | const { filter } = this.state;
33 |
34 | return children(filter);
35 | }
36 | }
37 |
38 | export default Filter;
39 |
--------------------------------------------------------------------------------
/src/components/pixi/PIXIView.js:
--------------------------------------------------------------------------------
1 | import { CustomPIXIComponent } from "react-pixi-fiber";
2 | import { Graphics } from "pixi.js";
3 | import { applyPropsToInstance } from "../../lib/subform";
4 |
5 | const PIXIView = CustomPIXIComponent(
6 | {
7 | customDisplayObject: props => new Graphics(),
8 | customApplyProps: function(instance, oldProps, newProps) {
9 | const { fill, x, y, width, height, alpha = 1 } = newProps;
10 |
11 | instance.updateLayout = (x, y, width, height) => {
12 | instance.clear();
13 |
14 | if (fill !== undefined) {
15 | instance.beginFill(fill, alpha);
16 | instance.drawRect(0, 0, width, height);
17 | instance.endFill();
18 | }
19 |
20 | instance.x = x;
21 | instance.y = y;
22 | };
23 |
24 | instance.updateLayout(x, y, width, height);
25 |
26 | applyPropsToInstance(instance, newProps);
27 | }
28 | },
29 | "PIXIView"
30 | );
31 |
32 | export default PIXIView;
33 |
--------------------------------------------------------------------------------
/src/components/ui/Sprite.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import { Texture, loader } from "pixi.js";
3 | import { Sprite as PIXISprite } from "react-pixi-fiber";
4 |
5 | class Sprite extends Component {
6 | state = {
7 | texture: undefined,
8 | ar: undefined
9 | };
10 | componentDidMount() {
11 | loader.add("bg", this.props.src).load((loader, resources) => {
12 | const texture = Texture.from(resources.bg.data);
13 | const ar = texture.width / texture.height;
14 |
15 | this.setState(s => ({ texture, ar }));
16 | });
17 | }
18 | render() {
19 | const { texture, ar } = this.state;
20 | const { src, scaleWith, ...props } = this.props;
21 |
22 | const height = ar ? scaleWith / ar : undefined;
23 |
24 | return texture ? (
25 |
31 | ) : null;
32 | }
33 | }
34 |
35 | export default Sprite;
36 |
--------------------------------------------------------------------------------
/src/components/ui/Button.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Container } from "react-pixi-fiber";
3 | import Touchable from "./Touchable";
4 | import Text from "./Text";
5 | import PIXIView from "../pixi/PIXIView";
6 |
7 | class Button extends Component {
8 | inFocusStyle = {
9 | fill: 0x000000
10 | };
11 | idleStyle = {
12 | fill: 0xffffff
13 | };
14 | render() {
15 | const { inFocusStyle, idleStyle } = this;
16 | const { onPress, children, buttonStyle, textStyle } = this.props;
17 | return (
18 |
30 | {style => (
31 |
40 | {children}
41 |
42 | )}
43 |
44 | );
45 | }
46 | }
47 |
48 | export default Button;
49 |
--------------------------------------------------------------------------------
/src/components/pixi/PIXITouchable.js:
--------------------------------------------------------------------------------
1 | import { CustomPIXIComponent } from "react-pixi-fiber";
2 | import { Graphics } from "pixi.js";
3 | import { applyPropsToInstance } from "../../lib/subform";
4 | import { im } from "../../lib/InteractionManager";
5 |
6 | const PIXITouchable = CustomPIXIComponent(
7 | {
8 | customDisplayObject: props => new Graphics(),
9 | customApplyProps: function(instance, oldProps, newProps) {
10 | const { fill, alpha, onFocusIn, onFocusOut, onPress } = newProps;
11 |
12 | instance.interactive = true;
13 |
14 | instance.on("mousedown", event => {
15 | if (im.isViewInFocus(instance) === false) {
16 | im.setViewInFocus(instance);
17 | if (typeof onFocusIn === "function") {
18 | onFocusIn(event);
19 | }
20 | instance.onFocusOut = onFocusOut;
21 | }
22 | });
23 |
24 | instance.on("click", event => {
25 | if (typeof onPress === "function") {
26 | onPress(event);
27 | }
28 | });
29 |
30 | instance.updateLayout = (x, y, width, height) => {
31 | instance.clear();
32 |
33 | instance.beginFill(fill, alpha);
34 | instance.drawRect(0, 0, width, height);
35 | instance.endFill();
36 |
37 | instance.x = x;
38 | instance.y = y;
39 | };
40 |
41 | applyPropsToInstance(instance, newProps);
42 | }
43 | },
44 | "PIXITouchable"
45 | );
46 |
47 | export default PIXITouchable;
48 |
--------------------------------------------------------------------------------
/src/components/ui/Touchable.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PIXITouchable from "../pixi/PIXITouchable";
3 | import PIXIView from "../pixi/PIXIView";
4 |
5 | class Touchable extends Component {
6 | state = {
7 | isInFocus: false
8 | };
9 | focusIn = () => {
10 | this.setState(
11 | s => ({ isInFocus: true }),
12 | () => {
13 | if (typeof this.props.onFocusIn === "function") {
14 | this.props.onFocusIn();
15 | }
16 | }
17 | );
18 | };
19 | focusOut = () => {
20 | this.setState(
21 | s => ({ isInFocus: false }),
22 | () => {
23 | if (typeof this.props.onFocusOut === "function") {
24 | this.props.onFocusOut();
25 | }
26 | }
27 | );
28 | };
29 | render() {
30 | const { isInFocus } = this.state;
31 | const {
32 | inFocusStyle,
33 | idleStyle,
34 | onPress,
35 | children,
36 | onFocusIn,
37 | onFocusOut,
38 | ...props
39 | } = this.props;
40 |
41 | const style = isInFocus ? inFocusStyle : idleStyle;
42 |
43 | return (
44 |
53 | {typeof children === "function" ? children(style) : children}
54 |
55 | );
56 | }
57 | }
58 |
59 | export default Touchable;
60 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import { Application, ticker } from "pixi.js";
3 | import { render, Stage } from "react-pixi-fiber";
4 | import FPS from "yy-fps";
5 |
6 | import * as Subform from "./lib/subform";
7 | import MultiplayerScreen from "./screens/multiplayer";
8 |
9 | const ratio = devicePixelRatio;
10 |
11 | const WIDTH = window.innerWidth;
12 | const HEIGHT = window.innerHeight;
13 |
14 | const view = document.getElementById("canvas");
15 |
16 | view.style.transformOrigin = "0 0";
17 | view.style.transform = `scale(${1 / ratio})`;
18 | view.style.position = "absolute";
19 |
20 | const app = new Application(WIDTH, HEIGHT, {
21 | backgroundColor: 0x10bb99,
22 | resolution: ratio,
23 | antialias: true,
24 | view
25 | });
26 |
27 | class App extends Component {
28 | state = {
29 | width: WIDTH,
30 | height: HEIGHT
31 | };
32 | handleResize = () => {
33 | this.setState(
34 | s => ({
35 | width: window.innerWidth,
36 | height: window.innerHeight
37 | }),
38 | () => {
39 | this.props.renderer.resize(this.state.width, this.state.height);
40 | }
41 | );
42 | };
43 | componentDidMount() {
44 | window.addEventListener("resize", this.handleResize);
45 | }
46 | componentWillUnmount() {
47 | window.removeEventListener("resize", this.handleResize);
48 | }
49 | render() {
50 | const { width, height } = this.state;
51 |
52 | return ;
53 | }
54 | }
55 |
56 | render(, app.stage);
57 |
58 | const fps = new FPS({ side: "bottom-left" });
59 |
60 | ticker.shared.add(() => fps.frame());
61 |
62 | Subform.init(layoutFn => {
63 | ticker.shared.add(() => {
64 | Subform.layout(layoutFn, app.stage);
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/src/components/ui/TextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import { Container } from "react-pixi-fiber";
3 | import Touchable from "./Touchable";
4 | import Text from "./Text";
5 | import PIXIView from "../pixi/PIXIView";
6 |
7 | class TextInput extends Component {
8 | static defaultProps = {
9 | x: 0,
10 | y: 0
11 | };
12 | state = {
13 | value: ""
14 | };
15 | inFocusStyle = {
16 | fill: 0xffff00
17 | };
18 | idleStyle = {
19 | fill: 0xffffff
20 | };
21 | handleChange = event => {
22 | if (event.key === "Backspace") {
23 | return this.setState(s => ({ value: s.value.slice(0, -1) }));
24 | }
25 |
26 | if (
27 | (event.keyCode >= 48 && event.keyCode <= 57) ||
28 | (event.keyCode >= 65 && event.keyCode <= 90)
29 | ) {
30 | return this.setState(s => ({ value: s.value + event.key }));
31 | }
32 | };
33 | addChangeListener = () => {
34 | window.addEventListener("keydown", this.handleChange);
35 | };
36 | removeChangeListener = () => {
37 | window.removeEventListener("keydown", this.handleChange);
38 | };
39 | render() {
40 | const { inFocusStyle, idleStyle } = this;
41 | const { width, height } = this.props;
42 | const { value } = this.state;
43 |
44 | return (
45 |
58 | {style => {
59 | return (
60 |
71 | {this.state.value}
72 |
73 | );
74 | }}
75 |
76 | );
77 | }
78 | }
79 |
80 | export default TextInput;
81 |
--------------------------------------------------------------------------------
/src/screens/multiplayer.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 |
3 | import TextInput from "../components/ui/TextInput";
4 | import Button from "../components/ui/Button";
5 | import Sprite from "../components/ui/Sprite";
6 | import Filter from "../components/ui/Filter";
7 | import Text from "../components/ui/Text";
8 |
9 | import PIXIView from "../components/pixi/PIXIView";
10 |
11 | class MultiplayerScreen extends Component {
12 | render() {
13 | const { width, height } = this.props;
14 |
15 | return (
16 |
35 | {/* */}
41 |
42 |
53 |
64 |
73 |
74 |
75 |
76 |
84 |
85 |
86 |
87 |
88 |
89 |
97 |
98 | );
99 | }
100 | }
101 |
102 | export default MultiplayerScreen;
103 |
--------------------------------------------------------------------------------
/src/lib/subform.js:
--------------------------------------------------------------------------------
1 | import { Container, Graphics } from "pixi.js";
2 |
3 | const treeToArray = tree => {
4 | const nodes = [];
5 |
6 | const processTreeNode = node => {
7 | const childrenIds = [];
8 | const cnodes = node.children || [];
9 | const ln = cnodes.length;
10 |
11 | for (let idx = 0; idx < ln; idx++) {
12 | const node = cnodes[idx];
13 | if (node.hasOwnProperty("subform/layout")) {
14 | const id = processTreeNode(node);
15 | if (typeof id === "number") {
16 | childrenIds.push(id);
17 | }
18 | }
19 | }
20 |
21 | if (node.hasOwnProperty("subform/layout")) {
22 | node["subform/layout"].childrenIds = childrenIds;
23 | nodes.push(node);
24 | return nodes.length - 1;
25 | }
26 | };
27 |
28 | processTreeNode(tree);
29 |
30 | return nodes;
31 | };
32 |
33 | export const init = callback => {
34 | subform_init_layout(layoutFn => {
35 | callback(layoutFn);
36 | });
37 | };
38 |
39 | const render = function _render(pnodes, snodes, id, px, py) {
40 | const pnode = pnodes[id];
41 | const snode = snodes[id];
42 | const gl = snode.groundLayout;
43 |
44 | const x = px + gl.horizontal.before;
45 | const y = py + gl.vertical.before;
46 | const width = gl.horizontal.size;
47 | const height = gl.vertical.size;
48 |
49 | const ln = snode.childrenIds.length;
50 |
51 | for (let idx = 0; idx < ln; idx++) {
52 | const cid = snode.childrenIds[idx];
53 | _render(pnodes, snodes, cid, px, py);
54 | }
55 |
56 | pnode.updateLayout(x, y, width, height);
57 | };
58 |
59 | export const layout = (layoutFn, stage) => {
60 | const pixiNodes = treeToArray(stage);
61 | const layoutNodes = pixiNodes.map(node => node["subform/layout"]);
62 | const solvedNodes = layoutFn(layoutNodes);
63 |
64 | // console.log(solvedNodes);
65 |
66 | render(pixiNodes, solvedNodes, pixiNodes.length - 1, 0, 0);
67 | };
68 |
69 | export const applyPropsToInstance = (instance, props) => {
70 | const {
71 | mode,
72 | childrenLayout,
73 | width,
74 | height,
75 | row,
76 | col,
77 | rowSpan,
78 | colSpan
79 | } = props;
80 |
81 | const config = {};
82 |
83 | const layout = {};
84 |
85 | if (typeof mode === "string") {
86 | layout.mode = mode;
87 | }
88 | if (typeof width === "number" || width === "hug") {
89 | layout.horizontal = { size: width };
90 | }
91 | if (typeof height === "number" || height === "hug") {
92 | layout.vertical = { size: height };
93 | }
94 | if (typeof row === "number") {
95 | layout.rowIdx = row;
96 | }
97 | if (typeof col === "number") {
98 | layout.colIdx = col;
99 | }
100 | if (typeof rowSpan === "number") {
101 | layout.rowSpan = rowSpan;
102 | }
103 | if (typeof colSpan === "number") {
104 | layout.colSpan = colSpan;
105 | }
106 |
107 | if (childrenLayout !== undefined) {
108 | config.childrenLayout = childrenLayout;
109 | }
110 |
111 | config.layout = props.layout || layout;
112 |
113 | instance["subform/layout"] = config;
114 | };
115 |
--------------------------------------------------------------------------------