├── .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 | --------------------------------------------------------------------------------