tsg-ut/mnemo

View on GitHub
lib/board-component.jsx

Summary

Maintainability
F
4 days
Test Coverage
const React = require('react');
const PropTypes = require('prop-types');
const {default: Hammer} = require('react-hammerjs');
const {INPUT_MOVE, INPUT_END} = (typeof window === 'undefined') ? {} : require('hammerjs');
const {default: Measure} = require('react-measure');
const Path = require('svg-path-generator');
const assert = require('assert');
const Board = require('./board');
const BlockComponent = require('./block-component.jsx');
const IOComponent = require('./io-component.jsx');
const {id, sum, isBetween} = require('./util');
const {BLOCK_SIZE} = require('./constants');

const inputColors = [
    '#de3131', // red
    '#4527a8', // blue
    '#1c6925', // green
    '#db40cd', // pink
];

class BoardComponent extends React.Component {
    static propTypes = {
        status: PropTypes.string.isRequired,
        width: PropTypes.number.isRequired,
        height: PropTypes.number.isRequired,
        clockLimit: PropTypes.number.isRequired,
        inputX: PropTypes.arrayOf(PropTypes.number).isRequired,
        outputX: PropTypes.number.isRequired,
        input: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number))).isRequired,
        currentInputIndex: PropTypes.number,
        output: PropTypes.arrayOf(PropTypes.number).isRequired,
        userOutput: PropTypes.arrayOf(PropTypes.number).isRequired,
        userOutputCorrectness: PropTypes.arrayOf(PropTypes.bool).isRequired,
        onClickBlock: PropTypes.func.isRequired,
        onOutput: PropTypes.func.isRequired,
        onHalt: PropTypes.func.isRequired,
        onPaused: PropTypes.func.isRequired,
        onDataLimitExceeded: PropTypes.func.isRequired,
        onClockLimitExceeded: PropTypes.func.isRequired,
        isRapid: PropTypes.bool.isRequired,
        isForced: PropTypes.bool.isRequired,
        isMovingMode: PropTypes.bool.isRequired,
        onFinishMove: PropTypes.func.isRequired,
    }

    static defaultProps = {
        currentInputIndex: null,
    }

    constructor(props, state) {
        super(props, state);

        // Currently, initial status should be always 'stop'
        assert(props.status === 'stop');

        this.board = new Board({
            height: this.props.height,
            width: this.props.width,
            clockLimit: this.props.clockLimit,
            inputX: this.props.inputX,
            outputX: this.props.outputX,
        }, this.blockSize);

        this.board.on('output', this.handleBoardOutput);
        this.board.on('halt', this.handleBoardHalt);
        this.board.on('paused', this.handleBoardPaused);

        this.passAnimationResolvers = new WeakMap();

        this.inputBlockY = 0;
        this.outputBlockX = this.props.outputX;
        this.outputBlockY = this.props.height - 1;

        this.animations = [];

        this.backgroundDimensions = null;

        this.state = {
            blocks: this.board.getBlocks(),
            clocks: 0,
            isPanning: false,
            isPinch: false,
            panDistance: 0,
            panAngle: 0,
            pinchCenterX: 0,
            pinchCenterY: 0,
            pinchScale: 1,
            offsetX: 0,
            offsetY: 0,
            scale: 1,
            viewBoxScale: null,
            selectStart: null,
            selectEnd: null,
            moveStart: null,
            moveEnd: null,
            moveStatus: 'select',
        };
    }

    componentDidUpdate(prevProps) {
        if (prevProps.isMovingMode && !this.props.isMovingMode) {
            this.resetMoveState();
        }

        if (prevProps.status === 'stop') {
            assert(this.props.status !== 'pause');

            if (this.props.status === 'executing') {
                this.execute(this.props.input[this.props.currentInputIndex]);
            }
        }

        if (prevProps.status === 'executing') {
            if (this.props.status === 'stop') {
                this.halt({force: this.props.isForced});
            }
        }
    }

    get _borderSize() {
        return 25;
    }

    get _boardWidth() {
        return this.props.width * BLOCK_SIZE;
    }

    get _boardHeight() {
        return this.props.height * BLOCK_SIZE;
    }

    get _inputHeight() {
        return 120;
    }

    get _outputHeight() {
        return 170;
    }

    get _boardBorderWidth() {
        return this._borderSize * 2 + this._boardWidth;
    }

    get _boardBorderHeight() {
        return this._borderSize * 2 + this._boardHeight;
    }

    get _boardOuterWidth() {
        return Math.max(this._borderSize * 2 + this._boardWidth, this._inputAreaWidth);
    }

    get _boardOuterHeight() {
        return this._borderSize * 2 + this._boardHeight + this._inputHeight + this._outputHeight;
    }

    get _inputAreaWidth() {
        const inputComponentSize = sum(this.props.input.map((inputList) => inputList.length));
        return inputComponentSize * 200 - 50;
    }

    get _outputAreaWidth() {
        return this.props.output.length * 200 - 50;
    }

    // TODO: publicなメソッドを殺したい

    getBlock(x, y) {
        return this.board.getBlock(x, y);
    }

    getBoardData() {
        return this.board.boardData;
    }

    getClock() {
        return this.board.clock;
    }

    getWeighedBlockCount() {
        return this.board.weighedBlockCount;
    }

    getSeededRandom() {
        return this.board.getSeededRandom();
    }

    placeBlock({x, y, type, rotate}) {
        this.board.placeBlock({x, y, type, rotate});
        this.setState({
            blocks: this.board.getBlocks(),
        });
    }

    resetMoveState() {
        this.setState({
            selectStart: null,
            selectEnd: null,
            moveStart: null,
            moveEnd: null,
            moveStatus: 'select',
        });
    }

    execute = (value) => {
        // Reset scale on start executing
        const {offsetX, offsetY, scale} = this.normalizeScaleAndOffset({
            offsetX: this.state.offsetX,
            offsetY: this.state.offsetY,
            scale: 1,
        });
        this.setState({offsetX, offsetY, scale});

        this.board.input(value);
        this.clockUp();
    }

    halt({force}) {
        if (force) {
            this.board.halt();
        }
    }

    clockUp = async () => {
        const passAnimations = [];

        this.board.step({
            onPass: (passEvent) => {
                passAnimations.push(new Promise((resolve) => {
                    this.passAnimationResolvers.set(passEvent, resolve);
                }));
            },
        });

        this.setState({
            clocks: this.board.clock,
        });

        if (this.board.status !== 'executing') {
            return;
        }

        await Promise.all(passAnimations);

        this.board.hand();

        if (this.board.status !== 'executing') {
            return;
        }

        if (this.board.clock >= this.board.clockLimit) {
            this.board.halt();
            this.props.onClockLimitExceeded(this.board.clockLimit);

            return;
        }

        if (this.board.dataCount > 100) {
            this.board.halt();
            this.props.onDataLimitExceeded();

            return;
        }

        this.clockUp();
    }

    normalizeScaleAndOffset = ({offsetX, offsetY, scale}) => {
        const maxScale = Math.max(this._boardOuterWidth, this._boardOuterHeight) / (BLOCK_SIZE * 2);
        const normalizedScale = Math.max(1, Math.min(scale, maxScale));

        const maxOffsetX = (this._boardOuterWidth - this._boardOuterWidth / normalizedScale) / 2;
        const normalizedOffsetX = Math.max(-maxOffsetX, Math.min(offsetX, maxOffsetX));

        const maxOffsetY = (this._boardOuterHeight - this._boardOuterHeight / normalizedScale) / 2;
        const normalizedOffsetY = Math.max(-maxOffsetY, Math.min(offsetY, maxOffsetY));

        return {
            scale: normalizedScale,
            offsetX: normalizedOffsetX,
            offsetY: normalizedOffsetY,
        };
    }

    handleBoardOutput = (value) => {
        this.props.onOutput(value);
    }

    handleBoardHalt = () => {
        this.props.onHalt();
    }

    handleBoardPaused = () => {
        this.props.onPaused();
    }

    handleClickBlock = (event, x, y) => {
        event.preventDefault();
        if (!this.props.isMovingMode) {
            return this.props.onClickBlock({x, y, type: event.type});
        }
        return false;
    }

    isSelectedBlock = (x, y) => {
        if (this.props.isMovingMode && this.state.selectStart !== null && this.state.selectEnd !== null) {
            return isBetween({
                number: x,
                left: this.state.selectStart.x,
                right: this.state.selectEnd.x,
            }) && isBetween({
                number: y,
                left: this.state.selectStart.y,
                right: this.state.selectEnd.y,
            });
        }
        return false;
    }

    handlePassAnimationComplete = (passEvent) => {
        if (this.passAnimationResolvers.has(passEvent)) {
            this.passAnimationResolvers.get(passEvent)();
        }
    }

    handlePan = (event) => {
        event.preventDefault();
        if (this.props.isMovingMode) {
            if (this.measureComponent) {
                this.measureComponent.measure();
            }

            const realBlockSize = BLOCK_SIZE * (this.state.viewBoxScale || 1);
            const onBoardX = event.center.x - this.backgroundDimensions.left;
            const onBoardY = event.center.y - this.backgroundDimensions.top;
            const onBoardStartX = event.center.x - event.deltaX - this.backgroundDimensions.left;
            const onBoardStartY = event.center.y - event.deltaY - this.backgroundDimensions.top;

            const blockX = Math.floor(onBoardX / realBlockSize);
            const blockY = Math.floor(onBoardY / realBlockSize);
            const startBlockX = Math.floor(onBoardStartX / realBlockSize);
            const startBlockY = Math.floor(onBoardStartY / realBlockSize);

            if (this.state.moveStatus === 'select') {
                if (event.type === 'panstart') {
                    // start selecting
                    if (
                        isBetween({
                            number: startBlockX,
                            left: 0,
                            right: this.props.width - 1,
                        }) && isBetween({
                            number: startBlockY,
                            left: 0,
                            right: this.props.height - 1,
                        })
                    ) {
                        // valid block
                        this.setState({
                            selectStart: {x: startBlockX, y: startBlockY},
                            selectEnd: {x: startBlockX, y: startBlockY},
                        });
                    }
                } else if (event.type === 'pan') {
                    // update selecting blocks
                    this.setState({
                        selectEnd: {
                            // limit 0 <= num < this.props.width
                            x: Math.min(Math.max(0, blockX), this.props.width - 1),
                            y: Math.min(Math.max(0, blockY), this.props.height - 1),
                        },
                    });
                } else if (event.type === 'panend') {
                    // end selecting
                    if (this.state.selectStart !== null && this.state.selectEnd !== null) {
                        this.setState({
                            moveStatus: 'move',
                        });
                    }
                }
            } else if (this.state.moveStatus === 'move') {
                if (event.type === 'panstart') {
                    // panstart should be fired on board
                    if (this.isSelectedBlock(startBlockX, startBlockY)) {
                        this.setState({
                            moveStart: {x: startBlockX, y: startBlockY},
                            moveEnd: {x: startBlockX, y: startBlockY},
                        });
                    }
                } else if (event.type === 'pan') {
                    if (this.state.moveStart !== null) {
                        this.setState({
                            moveEnd: {x: blockX, y: blockY},
                        });
                    }
                } else if (event.type === 'panend') {
                    if (this.state.moveStart !== null && this.state.moveEnd !== null) {
                        this.props.onFinishMove({
                            selectStart: this.state.selectStart,
                            selectEnd: this.state.selectEnd,
                            deltaX: this.state.moveEnd.x - this.state.moveStart.x,
                            deltaY: this.state.moveEnd.y - this.state.moveStart.y,
                        });
                    }
                }
            }
            return;
        }
        // when this.props.isMovingMode === false
        if (event.type === 'pan') {
            const distance = this.state.viewBoxScale === null
                ? event.distance
                : event.distance / this.state.viewBoxScale;
            const angle = event.angle / 180 * Math.PI;

            if (event.eventType === INPUT_MOVE) {
                this.setState({
                    isPanning: true,
                    panDistance: distance,
                    panAngle: angle,
                });
            } else if (event.eventType === INPUT_END) {
                const {offsetX, offsetY, scale} = this.normalizeScaleAndOffset({
                    offsetX: this.state.offsetX - distance * Math.cos(angle),
                    offsetY: this.state.offsetY - distance * Math.sin(angle),
                    scale: this.state.scale,
                });

                this.setState({
                    isPanning: false,
                    offsetX,
                    offsetY,
                    scale,
                });
            }
        }
    }

    handlePinch = (event) => {
        event.preventDefault();

        if (event.eventType === INPUT_MOVE) {
            this.setState({
                isPinching: true,
                pinchScale: event.scale,
            });
        } else if (event.eventType === INPUT_END) {
            const {offsetX, offsetY, scale} = this.normalizeScaleAndOffset({
                offsetX: this.state.offsetX,
                offsetY: this.state.offsetY,
                scale: this.state.scale * this.state.pinchScale,
            });

            this.setState({
                isPinching: false,
                offsetX,
                offsetY,
                scale,
            });

            if (this.measureComponent) {
                this.measureComponent.measure();
            }
        }
    }

    handleWheel = (event) => {
        const direction = (event.deltaY > 0) ? -1 : 1;

        const {offsetX, offsetY, scale} = this.normalizeScaleAndOffset({
            offsetX: this.state.offsetX,
            offsetY: this.state.offsetY,
            scale: this.state.scale * (1.0 + 0.1 * direction),
        });

        this.setState({
            offsetX,
            offsetY,
            scale,
        });

        if (this.measureComponent) {
            this.measureComponent.measure();
        }
    }

    handleDragStart = (event) => {
        event.preventDefault();
    }

    handleMeasureBackground = (dimensions) => {
        this.backgroundDimensions = dimensions.bounds;
        this.setState({
            viewBoxScale: this.backgroundDimensions.width / this._boardWidth,
        });
    }

    handleMeasureInput = (dimensions) => {
        this.handleMeasureIO({type: 'input', dimensions});
    }

    handleMeasureOutput = (dimensions) => {
        this.handleMeasureIO({type: 'output', dimensions});
    }

    handleMeasureUserOutput = (dimensions) => {
        this.handleMeasureIO({type: 'user_output', dimensions});
    }

    getViewBox = () => {
        const {offsetX, offsetY, scale} = this.normalizeScaleAndOffset({
            offsetX: this.state.isPanning
                ? this.state.offsetX - this.state.panDistance * Math.cos(this.state.panAngle)
                : this.state.offsetX,
            offsetY: this.state.isPanning
                ? this.state.offsetY - this.state.panDistance * Math.sin(this.state.panAngle)
                : this.state.offsetY,
            scale: this.state.isPinching
                ? this.state.scale * this.state.pinchScale
                : this.state.scale,
        });

        const normalOffsetY = (this._outputHeight - this._inputHeight) / 2;

        const viewBoxWidth = this._boardOuterWidth / scale;
        const viewBoxHeight = this._boardOuterHeight / scale;

        return [
            -viewBoxWidth / 2 + offsetX,
            -viewBoxHeight / 2 + offsetY + normalOffsetY,
            viewBoxWidth,
            viewBoxHeight,
        ];
    }

    getIOWirePathData = ({startX, endX, head, tail}) => {
        const pathLength = 30;
        const curveLength = pathLength * 0.9;

        return Path()
            .moveTo(startX, 0)
            .relative().lineTo(0, head)
            .relative().curveTo(
                0, curveLength,
                endX - startX, pathLength - curveLength,
                endX - startX, pathLength)
            .relative().lineTo(0, tail)
            .end();
    }

    getInputColor = (index) => {
        if (this.props.currentInputIndex !== null && this.props.currentInputIndex !== index) {
            return 'gray';
        }

        return inputColors[index % inputColors.length];
    }

    getBlockTransform = (x, y) => {
        if (this.state.moveStatus === 'move' && this.isSelectedBlock(x, y)) {
            if (this.state.moveStart !== null && this.state.moveEnd !== null) {
                const deltaX = this.state.moveEnd.x - this.state.moveStart.x;
                const deltaY = this.state.moveEnd.y - this.state.moveStart.y;
                return `translate(${-10 + deltaX * BLOCK_SIZE}, ${-10 + deltaY * BLOCK_SIZE})`;
            }
            return 'translate(-10, -10)';
        }
        return 'translate(0, 0)';
    }

    getBlockFill = (x, y) => {
        if (this.isSelectedBlock(x, y)) {
            return '#ffc37a';
        }
        if (this.props.isMovingMode) {
            return '#967c52';
        }
        return 'transparent';
    }

    permutation = (array) => {
        const front = [];
        const back = [];
        for (let row = 0; row < array.length; row++) {
            for (let line = 0; line < array[row].length; line++) {
                if (this.isSelectedBlock(line, row)) {
                    front.push(array[row][line]);
                } else {
                    back.push(array[row][line]);
                }
            }
        }
        return back.concat(front);
    }

    render() {
        return (
            <Hammer
                onPan={this.handlePan}
                onPanStart={this.handlePan}
                onPanEnd={this.handlePan}
                onPinch={this.handlePinch}
                options={{
                    recognizers: {
                        pinch: {enable: true},
                        pan: {threshold: 10},
                    },
                    preventDefault: true,
                }}
            >
                <svg
                    className="board-svg"
                    viewBox={this.getViewBox()}
                    onWheel={this.handleWheel}
                    onDragStart={this.handleDragStart}
                >
                    {/* inputs */}
                    <g transform={`translate(0, ${-this._boardBorderHeight / 2 - 100})`}>
                        {this.renderInputs()}
                    </g>
                    {/* board + board-border */}
                    <g transform={`translate(${-this._boardBorderWidth / 2}, ${-this._boardBorderHeight / 2})`}>
                        {/* board-border */}
                        <g>
                            {/* top-left */}
                            <image
                                x="0"
                                y="0"
                                width={this._borderSize}
                                height={this._borderSize}
                                xlinkHref="image/frame-topleft.png"
                            />
                            {/* top */}
                            <image
                                x={this._borderSize}
                                y="0"
                                width={this._boardWidth}
                                height={this._borderSize}
                                preserveAspectRatio="none"
                                xlinkHref="image/frame-top.png"
                            />
                            {/* top-right */}
                            <image
                                x={this._borderSize + this._boardWidth}
                                y="0"
                                width={this._borderSize}
                                height={this._borderSize}
                                xlinkHref="image/frame-topright.png"
                            />
                            {/* left */}
                            <image
                                x="0"
                                y={this._borderSize}
                                width={this._borderSize}
                                height={this._boardHeight}
                                preserveAspectRatio="none"
                                xlinkHref="image/frame-left.png"
                            />
                            {/* right */}
                            <image
                                x={this._borderSize + this._boardWidth}
                                y={this._borderSize}
                                width={this._borderSize}
                                height={this._boardHeight}
                                preserveAspectRatio="none"
                                xlinkHref="image/frame-right.png"
                            />
                            {/* bottom-left */}
                            <image
                                x="0"
                                y={this._borderSize + this._boardHeight}
                                width={this._borderSize}
                                height={this._borderSize}
                                xlinkHref="image/frame-bottomleft.png"
                            />
                            {/* bottom */}
                            <image
                                x={this._borderSize}
                                y={this._borderSize + this._boardHeight}
                                width={this._boardWidth}
                                height={this._borderSize}
                                preserveAspectRatio="none"
                                xlinkHref="image/frame-bottom.png"
                            />
                            {/* bottom-right */}
                            <image
                                x={this._borderSize + this._boardWidth}
                                y={this._borderSize + this._boardHeight}
                                width={this._borderSize}
                                height={this._borderSize}
                                xlinkHref="image/frame-bottomright.png"
                            />
                        </g>
                        {/* clocks */}
                        <foreignObject
                            transform={`translate(0, ${this._boardBorderHeight})`}
                            height="100%"
                            width="100%"
                            fontSize="0"
                        >
                            <div className="clock-area">
                                {this.state.clocks}/{this.props.clockLimit}
                            </div>
                        </foreignObject>
                        {/* board */}
                        <g transform={`translate(${this._borderSize}, ${this._borderSize})`}>
                            <Measure
                                ref={(ref) => {
                                    this.measureComponent = ref;
                                }}
                                onResize={this.handleMeasureBackground}
                                bounds
                            >
                                {({measureRef}) => (
                                    <rect
                                        ref={measureRef}
                                        className="board-background"
                                        width={this.props.width * BLOCK_SIZE}
                                        height={this.props.height * BLOCK_SIZE}
                                    />
                                )}
                            </Measure>
                            <g>
                                {this.renderBlocks()}
                            </g>
                            <g>
                                {
                                    this.state.blocks.map((row, y) => (
                                        row.map((block, x) => (
                                            <g
                                                key={id(block)}
                                                transform={`translate(${x * BLOCK_SIZE}, ${y * BLOCK_SIZE})`}
                                            >
                                                <BlockComponent
                                                    block={block}
                                                    x={x}
                                                    y={y}
                                                    boardEnds={{
                                                        left: x === 0,
                                                        right: x === this.props.width - 1,
                                                        top: y === 0,
                                                        bottom: y === this.props.height - 1,
                                                    }}
                                                    status={this.props.status}
                                                    onClick={this.handleClickBlock}
                                                    onPassAnimationComplete={this.handlePassAnimationComplete}
                                                    isRapid={this.props.isRapid}
                                                    viewBoxScale={this.state.viewBoxScale}
                                                />
                                            </g>
                                        ))
                                    ))
                                }
                            </g>
                        </g>
                    </g>
                    {/* outputs */}
                    <g transform={`translate(0, ${this._boardBorderHeight / 2})`}>
                        {this.renderOutputs()}
                    </g>
                </svg>
            </Hammer>
        );
    }

    renderInputs = () => (
        this.props.input.map((inputsList, index) => (
            <g key={index}>
                {
                    inputsList.map((inputs, inputsIndex) => {
                        const x = -this._inputAreaWidth / 2 + (index * inputsList.length + inputsIndex) * 200;

                        return (
                            <g key={inputsIndex}>
                                <IOComponent
                                    x={x}
                                    y={0}
                                    value={inputs}
                                    correctness={null}
                                    color={this.getInputColor(index)}
                                    filled
                                    nullable={false}
                                />
                                <path
                                    d={this.getIOWirePathData({
                                        startX: x + 75,
                                        endX: (this.props.inputX[inputsIndex] - this.props.width / 2 + 0.5) * BLOCK_SIZE + (index - (this.props.input.length - 1) / 2) * 10,
                                        head: 0,
                                        tail: 20,
                                    })}
                                    transform={'translate(0, 50)'}
                                    fill="none"
                                    strokeWidth="5"
                                    stroke={this.getInputColor(index)}
                                    style={{
                                        transition: 'stroke 0.3s ease',
                                    }}
                                />
                            </g>
                        );
                    })
                }
            </g>
        ))
    )

    /*
        Because of the limitation of React (cannot render sibling
        elements) and SVG (first element always rendered first),
        blocks renderings are located here.
        They must be inside BlockComponent, though...
    */
    renderBlocks = () => (
        this.permutation(
            this.state.blocks.map((row, y) => (
                row.map((block, x) => (
                    <g
                        key={id(block)}
                        transform={this.getBlockTransform(x, y)}
                    >
                        <rect
                            className="block-border"
                            width={BLOCK_SIZE}
                            height={BLOCK_SIZE}
                            x={x * BLOCK_SIZE}
                            y={y * BLOCK_SIZE}
                            fill={this.getBlockFill(x, y)}
                        />
                        {block.config.onRotatableWire && (
                            <image
                                className="block"
                                width={BLOCK_SIZE}
                                height={BLOCK_SIZE}
                                x={x * BLOCK_SIZE}
                                y={y * BLOCK_SIZE}
                                xlinkHref="image/wireI.png"
                                style={{
                                    transform: `rotate(${block.rotate * 90}deg)`,
                                    transformOrigin: 'center',
                                    // Enabled from FF55
                                    transformBox: 'fill-box',
                                    pointerEvents: 'none',
                                }}
                            />
                        )}
                        {(block.name !== 'empty') && (
                            <image
                                className="block"
                                width={BLOCK_SIZE}
                                height={BLOCK_SIZE}
                                x={x * BLOCK_SIZE}
                                y={y * BLOCK_SIZE}
                                xlinkHref={`image/${block.name}.png`}
                                style={{
                                    transform: block.config.onRotatableWire ? 'none' : `rotate(${block.rotate * 90}deg)`,
                                    transformOrigin: 'center',
                                    // Enabled from FF55
                                    transformBox: 'fill-box',
                                    pointerEvents: 'none',
                                }}
                            />
                        )}
                    </g>
                ))
            ))
        )
    )

    renderOutputs = () => (
        this.props.output.map((output, index) => (
            <g key={index}>
                <path
                    d={this.getIOWirePathData({
                        startX: (index - (this.props.output.length - 1) / 2) * 10,
                        endX: -this._outputAreaWidth / 2 + index * 200 + 75,
                        head: 20,
                        tail: 0,
                    })}
                    fill="none"
                    strokeWidth="5"
                    stroke={this.getInputColor(index)}
                    style={{
                        transition: 'stroke 0.3s ease',
                    }}
                />
                <IOComponent
                    x={-this._outputAreaWidth / 2 + index * 200}
                    y={110}
                    value={output}
                    correctness={null}
                    color={this.getInputColor(index)}
                    filled
                    nullable={false}
                />
                <IOComponent
                    x={-this._outputAreaWidth / 2 + index * 200}
                    y={50}
                    value={this.props.userOutput[index]}
                    correctness={this.props.userOutputCorrectness[index]}
                    color={this.getInputColor(index)}
                    filled={false}
                    nullable
                />
            </g>
        ))
    )
}

module.exports = BoardComponent;