Zpět

React & PlayCanvas

Ukázka #1 - Data-binding do 3D scény

  • // Hlavní komponent aplikace
    class ExampleApp1 extends React.Component {
    
        // konstruktor komponentu
    	constructor(props) {
    		super(props);
            // stav komponentu
    		this.state = {
    			text: 'Hello world!',
    			diffuseColor: '#dd3300',
                outlineColor: '#ee9966'
    		};
            // objekt pro zprostředkování změn stavu
            this.binder = OP({});
    	}
    
        // metoda volaná při změně stavu nebo props
        componentDidUpdate(prevProps, prevState, snapshot) {
            // synchronizace se zprostředkujícím objektem
            this.binder.set(this.state);
        }
    
        // metoda pro získání odkazu na funkci pro změnu stavu
        getStateUpdater() {
            var that = this;
            return (updater) => {
                return (value) => {
                    that.setState((state) => {
                        updater(value, state);
                        return state;
                    });
                }
            }
        }
    
        // metoda volaná při připojení komponentu
        componentDidMount() {
        
            // vytvoření scény
            var scene = setupScene(this.canvas);
    
            // načtení fontu
            var font = new pc.Asset('Arial.json', "font", { url: "../../../libs/playcanvas/Arial.json" });
            var that = this;
            scene.assets.on('load', function () {
                // entita pro text    
                var text = new pc.Entity("text", scene);
                text.addComponent("element", {
                    type: "text",
                    anchor: [0, 0, 0, 0],
                    pivot: [0.5, 0.5],
                    fontSize: 16,
                    fontAsset: font,
                    opacity: 1,
                    color: [0, 0, 1],
                    shadowColor: [0, 0, 1],
                    shadowOffset: [0.2, -0.2],
                    text: that.state.text
                });
                text.setLocalScale(1 / 32, 1 / 32, 1 / 32);
                scene.root.addChild(text);
        
                // propojení scény se stavem React komponentu pomocí mapy
                var state = that.getStateUpdater();
                that.binder.map(MAP.TEXT.to3DModel, text).set(that.state);
                OP(text).map({
                    text: state((v, s) => s.text = v),
                    color: state((v, s) => s.diffuseColor = v),
                    shadowColor: state((v, s) => s.outlineColor = v)
                }, {});
            });
    
            scene.assets.add(font);
            scene.assets.load(font);
        }
    
    
        // metoda pro vykreslení plátna
    	renderScene() {
            return (
                <div className="fixed-ratio">
                    <canvas ref={ref => (this.canvas = ref)}></canvas>
                </div>
            );
    	}
    
        // metoda pro vykreslení ovládacích prvků
    	renderControls() {
    		return (
    			<form>
    				<div>
    					<label>Text</label>
    					<!-- data-binding textu do vstupu, listener pro změnu -->
    					<input type="text"
                            value={this.state.text}
                            onChange={(e) => this.setState({ text: e.target.value })}
                        />
    				</div>
    				<div className="grid">
    				    <div>
    					    <label>Základní barva</label>
    					    <div>
    						    <input 
                                    type="color"
                                    value={this.state.diffuseColor}
                                    onChange={(e) => this.setState({ diffuseColor: e.target.value })} 
                                />
    						    <span style={{ color: this.state.diffuseColor }}>{this.state.diffuseColor}</span>
    					    </div>
    				    </div>
    				    <div>
    					    <label>Barva okraje</label>
    					    <div>
    						    <input 
                                    type="color"
                                    value={this.state.outlineColor}
                                    onChange={(e) => this.setState({ outlineColor: e.target.value })} 
                                />
    						    <span style={{ color: this.state.outlineColor }}>{this.state.outlineColor}</span>
    					    </div>
    				    </div>
    				</div>
    			</form>
    		);
    	}
    
        // hlavní vykreslovací metoda
    	render() {
    		return (
    			<div>
    				{this.renderScene()}
    				{this.renderControls()}
    			</div>
    		);
    	}
    }
    
    // vykreslení aplikace do HTML elementu
    ReactDOM.render(<ExampleApp1 />, document.getElementById('app1'));
    
  • // React JSX zkompilovaný pomocí Babel.js
    function () {
        var ExampleApp1 = function (_React$Component) {
            _inherits(ExampleApp1, _React$Component);
    
            function ExampleApp1(props) {
                _classCallCheck(this, ExampleApp1);
    
                var _this = _possibleConstructorReturn(this, (ExampleApp1.__proto__ || Object.getPrototypeOf(ExampleApp1)).call(this, props));
    
                _this.state = {
                    text: 'Hello world!',
                    diffuseColor: '#dd3300',
                    outlineColor: '#ee9966'
                };
                _this.binder = OP({});
                return _this;
            }
    
            _createClass(ExampleApp1, [{
                key: 'componentDidUpdate',
                value: function componentDidUpdate(prevProps, prevState, snapshot) {
                    this.binder.set(this.state);
                }
            }, {
                key: 'getStateUpdater',
                value: function getStateUpdater() {
                    var that = this;
                    return function (updater) {
                        return function (value) {
                            that.setState(function (state) {
                                updater(value, state);
                                return state;
                            });
                        };
                    };
                }
            }, {
                key: 'componentDidMount',
                value: function componentDidMount() {
                    var scene = setupScene(this.canvas);
    
                    var font = new pc.Asset('Arial.json', "font", { url: "../../../libs/playcanvas/Arial.json" });
    
                    var that = this;
                    scene.assets.on('load', function () {
    
                        var text = new pc.Entity("text", scene);
                        text.addComponent("element", {
                            type: "text",
                            anchor: [0, 0, 0, 0],
                            pivot: [0.5, 0.5],
                            fontSize: 16,
                            fontAsset: font,
                            opacity: 1,
                            color: [0, 0, 1],
                            shadowColor: [0, 0, 1],
                            shadowOffset: [0.2, -0.2],
                            text: that.state.text
                        });
                        text.setLocalScale(1 / 32, 1 / 32, 1 / 32);
                        scene.root.addChild(text);
                        window.text = text;
    
                        var state = that.getStateUpdater();
                        that.binder.map(MAP.TEXT.to3DModel, text).set(that.state);
                        OP(text).map({
                            text: state(function (v, s) {
                                return s.text = v;
                            }),
                            color: state(function (v, s) {
                                return s.diffuseColor = v;
                            }),
                            shadowColor: state(function (v, s) {
                                return s.outlineColor = v;
                            })
                        }, {});
                    });
    
                    scene.assets.add(font);
                    scene.assets.load(font);
                }
            }, {
                key: 'renderScene',
                value: function renderScene() {
                    var _this2 = this;
    
                    return React.createElement(
                        'div',
                        { className: 'fixed-ratio' },
                        React.createElement('canvas', { ref: function ref(_ref) {
                                return _this2.canvas = _ref;
                            } })
                    );
                }
            }, {
                key: 'renderControls',
                value: function renderControls() {
                    var _this3 = this;
    
                    return React.createElement(
                        'form',
                        { className: 'uk-form-stacked' },
                        React.createElement(
                            'div',
                            { className: 'uk-margin-small-top' },
                            React.createElement(
                                'label',
                                { className: 'uk-form-label' },
                                'Text'
                            ),
                            React.createElement(
                                'div',
                                { className: 'uk-form-controls' },
                                React.createElement('input', { type: 'text', value: this.state.text, className: 'uk-input', onChange: function onChange(e) {
                                        return _this3.setState({ text: e.target.value });
                                    } })
                            )
                        ),
                        React.createElement(
                            'div',
                            { className: 'uk-margin-small-top' },
                            React.createElement(
                                'div',
                                { className: 'uk-child-width-1-2 uk-grid-small', 'uk-grid': '' },
                                React.createElement(
                                    'div',
                                    null,
                                    React.createElement(
                                        'label',
                                        { className: 'uk-form-label' },
                                        'Z\xE1kladn\xED barva'
                                    ),
                                    React.createElement(
                                        'div',
                                        { className: 'uk-form-controls' },
                                        React.createElement('input', { type: 'color', value: this.state.diffuseColor, onChange: function onChange(e) {
                                                return _this3.setState({ diffuseColor: e.target.value });
                                            } }),
                                        React.createElement(
                                            'span',
                                            { style: { color: this.state.diffuseColor } },
                                            this.state.diffuseColor
                                        )
                                    )
                                ),
                                React.createElement(
                                    'div',
                                    null,
                                    React.createElement(
                                        'label',
                                        { className: 'uk-form-label' },
                                        'Barva okraje'
                                    ),
                                    React.createElement(
                                        'div',
                                        { className: 'uk-form-controls' },
                                        React.createElement('input', { type: 'color', value: this.state.outlineColor, onChange: function onChange(e) {
                                                return _this3.setState({ outlineColor: e.target.value });
                                            } }),
                                        React.createElement(
                                            'span',
                                            { style: { color: this.state.outlineColor } },
                                            this.state.outlineColor
                                        )
                                    )
                                )
                            )
                        )
                    );
                }
            }, {
                key: 'render',
                value: function render() {
                    return React.createElement(
                        'div',
                        null,
                        this.renderScene(),
                        this.renderControls()
                    );
                }
            }]);
    
            return ExampleApp1;
        }(React.Component);
    
        ReactDOM.render(React.createElement(ExampleApp1, null), document.getElementById('app1'));
    }
#dd3300
#ee9966

Ukázka #2 - Práce s polem

  • // Hlavní komponent
    class ExampleApp2 extends React.Component {
    
    	// stav komponentu
    	constructor(props) {
    		super(props);
    		this.state = { boxes: [] };
    	}
    
    	// metoda pro smazání kostky
    	remove(index) {
    		var that = this;
    		return () => {
                that.group.setLocalPosition((-(that.state.boxes.length - 2) * 0.2), 0, 0);
    			return that.setState((state) => {
    				state.boxes = state.boxes.filter((v, i) => i != index);
    				return state;
    			});
    		}
    	}
    
    	// metoda pro přidání kostky
    	add() {
    		this.setState((state) => {
    			state.boxes.push({
    				position: { x: 0, y: 0, z: 0 },
    				size: { x: 1, y: 1, z: 1 },
    				color: getRandomColor(),
    				key: getUniqueHash()
    			});
    			return state;
    		});
    	};
    	
    	// metoda volaná při připojení komponentu
    	componentDidMount() {
    	    // inicializace scény
            this.scene = setupScene(this.canvas);
            this.group = new pc.Entity("group", this.scene);
            this.group.setLocalScale(0.2, 0.2, 0.2);
            this.scene.root.addChild(this.group);
            this.add();
            this.add();
    	}
    
    	// metoda pro získání funkce pro změnu stavu kostky
    	getBoxStateUpdater() {
    		var that = this;
    		return (indexReader) => {
    			return (updater) => {
    				return (value) => {
    					value = value.target && value.target.value ? value.target.value : value;
    					that.setState((state) => {
    						updater(value, state.boxes[indexReader()]);
    						return state;
    					});
    				}
    			}
    		}
    	}
    
    	// metoda pro vykreslování
    	render() {
    		return (
    			<div>
    				<div className="fixed-ratio">
    					<canvas ref={ref => (this.canvas = ref)}></canvas>
    				</div>
    				<form>
    					<ul>
    						<!-- cyklus pro ovládání jednotlivých kostek -->
    						{this.state.boxes.map((box, index) => (
    							<BoxControl instance={box} index={index} key={box.key} group={this.group} scene={this.scene} updater={this.getBoxStateUpdater()} remove={this.remove(index)} />
    						))}
    					</ul>
    					<a onClick={(e) => this.add()}> Přidat nový objekt</a>
    				</form>
    			</div>
    		);
    	}
    }
    
    // vykreslení aplikace do HTML elementu
    ReactDOM.render(<ExampleApp2 />, document.getElementById('app2'));
    
  • // Komponent pro ovládání kostky
    class BoxControl extends React.Component {
    
    	// konstruktor komponentu
    	constructor(props) {
    		super(props);
    		this.cube;
    		// objekt pro zprostředkování změn stavu
    		this.binder = OP({});
    	}
    
    	// metoda volaná při změně stavu nebo props
    	componentDidUpdate(prevProps, prevState, snapshot) {
            const { index, instance } = this.props;
            if (prevProps.index > index) {
    			// přepočet pozice a indexu
                this.binder.set({ index });
                var original = this.cube.getLocalPosition();
                original.x = original.x - 2;
                this.cube.setLocalPosition(original);
            } else {
                this.binder.set(instance);
            }
        }
    
    	// pomocná metoda pro získání indexu
    	getIndexReader() {
    		return () => this.props.index;
    	}
    
    	// metoda volaná při připojení komponentu
        componentDidMount() {
            const { index, instance, group, scene, updater } = this.props;
            const { color } = instance;
            var state = updater(this.getIndexReader());
    	
    		// vytvoření kostky
            var cube = new pc.Entity("box" + index, scene);
            cube.addComponent("model", { type: "box" });
            var material = new pc.StandardMaterial();
            material.update();
            cube.model.material = material;
            group.addChild(cube);
    
    		// propojení 3D kostky s React komponentem pomocí mapy
            this.binder.map(MAP.CUBE.to3DModel, cube).set({ index }).set(instance);
    
    		// relativní pozice kostky
            group.setLocalPosition(-index * 0.2, 0, 0);
            cube.setLocalPosition(index * 2, 0, 0);
            this.cube = cube;
        }
        
    	// metoda volaná při odpojení komponentu
        componentWillUnmount() {
    		// odstranění 3D kostky
            this.props.group.removeChild(this.cube);
            this.cube.destroy();
        }
    
    	// metoda pro vykreslování
    	render() {
    		const { index, instance, updater, remove } = this.props;
    		const { position, size, color } = instance;
    		const onChange = (handler) => updater(this.getIndexReader())((value, box) => { handler(box, value); });
    		return (
    			<li>
    				<div>
    					<div className="grid">
    						<div>
    							<label>Objekt {index + 1}</label>
    							<div>
    								<input type="color" value={color} onChange={onChange((box, value) => box.color = value)} />
    								<span style={{ color: color }}> { color } </span>
    							</div>
    						</div>
    						<div>
    							<div className="grid">
    								<div>
    									<label>↔ W { Math.floor(size.x * 100) }%</label>
    									<input type="range" value={size.x} min={0.1} max={2} step={0.1} onChange={onChange((box, value) => box.size.x = value)} />
    								</div>
    								<div>
    									<label>↕ H { Math.floor(size.y * 100) }%</label>
    									<input type="range" value={size.y} min={0.1} max={2} step={0.1} onChange={onChange((box, value) => box.size.y = value)} />
    								</div>
    								<div>
    									<label>⤢ L { Math.floor(size.z * 100) }%</label>
    									<input type="range" value={size.z} min={0.1} max={2} step={0.1} onChange={onChange((box, value) => box.size.z = value)} />
    								</div>
    								<div>
    									<label>→ X { position.x }</label>
    									<input type="range" value={position.x} min={-1} max={1} step={0.1} onChange={onChange((box, value) => box.position.x = value)} />
    								</div>
    								<div>
    									<label">↑ Y { position.y }</label>
    									<input type="range" value={position.y} min={-1} max={1} step={0.1} onChange={onChange((box, value) => box.position.y = value)} />
    								</div>
    								<div>
    									<label">↗ Z { position.z }</label>
    									<input type="range" value={position.z} min={-1} max={1} step={0.1} onChange={onChange((box, value) => box.position.z = value)} />
    								</div>
    							</div>
    						</div>
    						<div>
    							<a onClick={() => remove()}>Smazat</a>
    						</div>
    					</div>
    				</div>
    			</li>
    		);
    	}
    }
    
  • #b5582d
  • #762be2
Přidat nový objekt

Ukázka #3 - Pokročilejší scéna

  • // Předpoklad: Vytvořená základní scéna s kamerou a světelným zdrojem 
    function (scene) {
    
        var rotationNode = new pc.Entity("rotated", scene);
        var positionNode = new pc.Entity("positioned", scene);
    
        // příprava materiálů
        var seatFreeMaterial = new pc.StandardMaterial({});
        seatFreeMaterial.diffuse = new pc.Color(0, 0.6, 1);
        seatFreeMaterial.emissive = new pc.Color(0, 0.33, 0.9);
        seatFreeMaterial.update();
        var seatTakenMaterial = new pc.StandardMaterial({});
        seatTakenMaterial.diffuse = new pc.Color(1, 0.6, 0);
        seatTakenMaterial.emissive = new pc.Color(0.9, 0.33, 0);
        seatTakenMaterial.update();
        var backFreeMaterial = new pc.StandardMaterial({});
        backFreeMaterial.diffuse = new pc.Color(0, 0.53, 1);
        backFreeMaterial.emissive = new pc.Color(0, 0.33, 0.9);
        backFreeMaterial.update();
        var backTakenMaterial = new pc.StandardMaterial({});
        backTakenMaterial.diffuse = new pc.Color(1, 0.53, 0);
        backTakenMaterial.emissive = new pc.Color(0.9, 0.33, 0);
        backTakenMaterial.update();
        var holderFreeMaterial = new pc.StandardMaterial({});
        holderFreeMaterial.diffuse = new pc.Color(0, 0.33, 0.9);
        holderFreeMaterial.emissive = new pc.Color(0, 0.33, 0.9);
        holderFreeMaterial.update();
        var holderTakenMaterial = new pc.StandardMaterial({});
        holderTakenMaterial.diffuse = new pc.Color(0.9, 0.33, 0);
        holderTakenMaterial.emissive = new pc.Color(0.9, 0.33, 0);
        holderTakenMaterial.update();
    
        // Sedadlo
        function Seat(row, column) {
            var seat = new pc.Entity("seat" + row + "x" + column, scene);
            seat.setLocalPosition(column * 1.4, row * -0.5, row * 3);
            var bottom = new pc.Entity("bottom" + row + "x" + column, scene);
            bottom.addComponent("model", { type: "box" });
            bottom.setLocalScale(0.6, 0.5, 0.6);
            var right = new pc.Entity("right" + row + "x" + column, scene);
            right.addComponent("model", { type: "box" });
            right.setLocalScale(0.2, 0.8, 0.6);
            right.setLocalPosition(0.4, 0.15, 0);
            var left = new pc.Entity("left" + row + "x" + column, scene);
            left.addComponent("model", { type: "box" });
            left.setLocalScale(0.2, 0.8, 0.6);
            left.setLocalPosition(-0.4, 0.15, 0);
            var back = new pc.Entity("back" + row + "x" + column, scene);
            back.addComponent("model", { type: "box" });
            back.setLocalScale(0.8, 1.2, 0.1);
            back.setLocalPosition(0, 0.35, -0.25);
    
            // předání akce pro výběr sedadla do Redux úložiště
            function click() {
                store.dispatch(pickSeatAction(row, column));
            }
    
            // připojení listeneru na kliknutí
            bottom.onclick = click;
            right.onclick = click;
            left.onclick = click;
            back.onclick = click;
    
            // metoda pro přepnutí
            this.switch = function (state) {
                if (state) {
                    bottom.model.material = seatFreeMaterial;
                    right.model.material = holderFreeMaterial;
                    left.model.material = holderFreeMaterial;
                    back.model.material = backFreeMaterial;
                } else {
                    bottom.model.material = seatTakenMaterial;
                    right.model.material = holderTakenMaterial;
                    left.model.material = holderTakenMaterial;
                    back.model.material = backTakenMaterial;
                }
            };
    
            seat.addChild(bottom);
            seat.addChild(right);
            seat.addChild(left);
            seat.addChild(back);
            positionNode.addChild(seat);
        }
    
        // vytvoření sedadel na scéně
        var state = store.getState();
        var seats = [];
        for (var r = 0; r < state.seats.length; r++) {
            seats.push([]);
            for (var c = 0; c < state.seats[r].length; c++) {
                var seat = new Seat(r, c);
                seat.switch(state.seats[r][c]);
                seats[r].push(seat);
            }
        }
        positionNode.setLocalPosition(-state.seats[0].length / 2 - 0.5, 0, -state.seats.length * 1.5);
        rotationNode.addChild(positionNode);
        rotationNode.setLocalScale(0.2, 0.2, 0.2);
        rotationNode.rotateLocal(30, 0, 0);
        scene.root.addChild(rotationNode);
    
        // připojení na Redux úložiště
        store.subscribe(function () {
            state = store.getState();
            for (var r = 0; r < state.seats.length; r++) {
                for (var c = 0; c < state.seats[r].length; c++) {
                    seats[r][c].switch(state.seats[r][c]);
                }
            }
        });
    }
  • 
    // mapování obdrženého stavu na props 
    const mapStateToProps = (state) => ({ seats: state.seats, free: state.free, taken: state.taken });
    
    // mapování dispatch funkce na props
    const mapDispatchToProps = (dispatch, ownProps) => ({
    	pickSeat: (row, column) => dispatch(pickSeatAction(row, column))
    });
    
    // Bezstavový komponent tlačítka pro výběr místa
    const SeatButton = ({ row, column, enabled, picker }) => {
    	const click = () => picker(row, column);
    	return (
    		<input type='button' className={enabled ? 'seat-free' : ' seat-taken'} onClick={click} value={'S ' + (column + 1)} />
    	);
    }
    
    // Bezstavový hlavní komponent aplikace
    const SeatApp = ({ seats, free, taken, pickSeat }) => {
    	return (
    		<form>
    			<h3>Vyberte sedadlo</h3>
    			<table>
    				<caption>Obsazeno je {taken}, zbývá {free} volných míst</caption>
    				<tbody>
    					{seats.map((row, rowIndex) => (
    						<tr key={rowIndex}>
    							<td>Řada {rowIndex + 1}</td>
    							{row.map((enabled, colIndex) => (
    								<td key={rowIndex + 'x' + colIndex}>
    									<SeatButton row={rowIndex} column={colIndex} enabled={enabled} picker={pickSeat} />
    								</td>
    							))}
    						</tr>
    					))}
    				</tbody>
    			</table>
    		</form>
    	);
    }
    
    // Hlavní komponent napojený na Redux úložiště
    const ConnectedSeatApp = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(SeatApp);
    
    // vykreslení aplikace do HTML elementu s obalením do poskytovatele Redux úložiště
    ReactDOM.render(<ReactRedux.Provider store={store}><ConnectedSeatApp /></ReactRedux.Provider>, document.getElementById('app3'));
    
  • 
    // Akce pro výběr sedadla
    function pickSeatAction(row, column) { return { type: 'PICK_SEAT', row, column }; }
    
    // Reducer úložiště
    function SeatReducer(state, action) {
    	// výchozí stav
        if (typeof state === 'undefined') {
            return {
                seats: [
                    [true, true, true, true, true, true, true],
                    [true, true, true, true, true, true, true],
                    [true, true, true, true, true, true, true],
                    [true, true, true, true, true, true, true]
                ],
                free: 28,
                taken: 0
            }
        }
    	// zpracování akcí
        switch (action.type) {
            case 'PICK_SEAT':
                const newState = Object.assign({}, state);
                newState.seats[action.row][action.column] = !state.seats[action.row][action.column];
                newState.seats[action.row][action.column] ? (newState.free++ , newState.taken--) : (newState.taken++ , newState.free--);
                return newState;
            default:
                return state;
        }
    }
    
    // Úložiště
    var store = Redux.createStore(SeatReducer);
    

Vyberte sedadlo

Obsazeno je 0, zbývá 28 volných míst
Řada 1
Řada 2
Řada 3
Řada 4