Zpět

React & Babylon.js

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);
            var planeText = new BABYLON.Mesh.CreatePlane("planeForText", 20, scene);
            var advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateForMesh(planeText, 2048, 2048);
            var text = new BABYLON.GUI.TextBlock();
            text.fontSize = 200;
            text.outlineWidth = 5;
    
            // propojení scény se stavem React komponentu pomocí mapy
            var state = this.getStateUpdater();
            this.binder.map(MAP.TEXT.to3DModel, text).set(this.state);
            OP(text).map({
                _text: state((v, s) => s.text = v),
                _color: state((v, s) => s.diffuseColor = v),
                _outlineColor: state((v, s) => s.outlineColor = v)
            }, {});
    
            advancedTexture.addControl(text);
        }
    
    
        // 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'));
    
  • 
    

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.position.x = - (that.state.boxes.length - 2);
    			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: "#ff9900", // nebo jiná náhodná barva
    				key: getUniqueHash()
    			});
    			return state;
    		});
    	};
    	
    	// metoda volaná při připojení komponentu
    	componentDidMount() {
    		// inicializace scény
    		this.scene = setupScene(this.canvas);
    		this.group = new BABYLON.AbstractMesh("group", this.scene);
    		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 });
    			this.cube.position.x = this.cube.position.x - 2;
    		} 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 material = new BABYLON.StandardMaterial("material" + index, scene);
    		var cube = new BABYLON.Mesh.CreateBox("box" + index, 1, scene);
    		cube.material = material;
    		group.addChild(cube);
    
    		// propojení 3D kostky s React komponentem pomocí mapy
    		this.binder.map(MAP.CUBE.to3DModel, cube).set({ index }).set(instance);
    		OP(cube).map({
    			scaling: {
    				inner: {
    					x: state((v, s) => s.size.x = v),
    					y: state((v, s) => s.size.y = v),
    					z: state((v, s) => s.size.z = v)
    				}
    			},
    			position: {
    				inner: {
    					x: state((v, s) => s.position.x = (v * 1) - (this.getIndexReader()() * 2)),
    					y: state((v, s) => s.position.y = v),
    					z: state((v, s) => s.position.z = v)
    				}
    			},
    			material: {
    				diffuseColor: state((v, s) => s.color = v.toHexString())
    			}
    		}, {});
    
    		// relativní pozice kostky
    		group.position.x = - index;
    		cube.position.x = (index * 2);
    		this.cube = cube;
    	}
    
    	// metoda volaná při odpojení komponentu
    	componentWillUnmount() {
    		// odstranění 3D kostky
    		this.props.scene.removeMesh(this.cube);
    	}
    
    	// 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>
    		);
    	}
    }
    

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

  • 
    // 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);