위젯을 빌드하세요 Mendix React Part 5 — WebAssembly 실행 Mendix | Mendix

메인 컨텐츠로 가기

위젯을 빌드하세요 Mendix React Part 5 — WebAssembly 실행 Mendix

Mendix 사용자 정의 React 위젯을 만들고 앱에서 사용할 수 있으므로 원하는 대로 애플리케이션의 프런트 엔드를 확장할 수 있습니다. 하지만 C, C++, Rust로 프런트 엔드를 확장할 수도 있다는 사실을 알고 계셨나요?

이것은 여러 부분으로 구성된 시리즈의 5번째 블로그입니다. 이전 블로그는 여기에서 찾을 수 있습니다:

우리가 무엇을 만들고 있는가

인생의 게임!아니, 그게 아니야 클래식 보드 게임…하지만 셀룰러 자동화 1인용 게임입니다. 몇 가지 간단한 규칙만 있으면 살아있는 세포와 죽어가는 세포로 이루어진 우주를 만들어라 멋진 반복 패턴을 만듭니다.

시작하기 전에 — Wasm이 뭐야‽

그래서 우리는 Rust로 작성되고 WebAssembly로 컴파일된 코드를 활용하는 게임을 만들 것입니다.

위젯 구축 Mendix React Part 5_Web Assembly를 사용하여

하지만 WebAssembly는 무엇일까요? 글쎄요, 그것은 작고 빠르고 효율적인 스택 기반 가상 머신으로, C, C++, Rust, Python 또는 다른 언어로 작성된 코드에서 컴파일된 바이트코드를 실행할 수 있게 해줍니다.

하지만 이것은 실제로 무엇을 의미할까?

글쎄요, WebAssembly는 기본적으로 바이너리 형식으로 컴파일되어 로드하기 쉽고 실행 속도가 번개처럼 빠릅니다. 또한 인기 있는 프로그래밍 언어의 라이브러리를 활용하고 기존 코드를 브라우저에서 작동하도록 변환할 수도 있습니다. 둠3처럼.

위젯 구축 Mendix React Part 5_Doom 3와 함께
운명 3

이건 게임에는 괜찮은데, 실제로 이걸 사용하는 사람이 있을까?

짧은 대답은 예입니다. 다음과 같은 대규모 웹 앱 피그 마 그리고 Zoom은 대부분 WebAssembly를 사용하여 작성됩니다. 하지만 이 외에도 많은 유용한 npm 패키지가 .wasm(컴파일된 WebAssembly) 파일을 활용합니다. 최근 블로그에서 사용한 ArcGis 라이브러리를 포함합니다..

WebAssembly 시작하기

코드를 컴파일하는 것을 포함하여 WebAssembly 파일을 만드는 다양한 방법이 있습니다. 엠스크립텐이 예제에서는 Rust로 작성한 코드를 사용하겠습니다.

WebAssembly에서 Game Of Life를 작성하고 위젯에서 쉽게 사용할 수 있는 노드 패키지로 게시하려면 나는 이 튜토리얼을 따랐다. 이 튜토리얼은 WebAssembly를 시작하는 데 매우 유용한 자료이며, Rust와 WebAssembly에 익숙해지고 싶다면 꼭 추천하고 싶습니다.

이 튜토리얼의 출력은 컴파일된 Rust 코드를 포함하는 Wasm 파일, Wasm 파일과의 인터페이스를 정의하는 자바스크립트 파일, Typescript와 함께 코드를 사용하기 위한 유형 파일로 구성된 노드 라이브러리입니다.

npm 라이브러리가 있으면 다음에서 코드를 실행할 수 있습니다. Mendix 플러그형 위젯을 사용합니다.

위젯 시간

우리는 위젯을 스캐폴딩하는 것으로 시작합니다. yo @mendix/widget gameOfLife 그리고 컴포넌트의 이름을 바꿉니다. 그런 다음 WebAssembly npm 라이브러리를 가져옵니다. npm i wasm-game-of-life-joerob319 (또는 본인이 직접 만든 라이브러리).

자식 구성 요소에서 Wasm 파일을 사용하기 시작할 수 있습니다. 이를 위해 파일을 가져와서 npm 라이브러리에서 제공하는 유형을 사용해야 합니다.

import * as wasm from "wasm-game-of-life-joerob319";
import { Universe, Cell, wasm_memory } from "wasm-game-of-life-joerob319";

다음으로, 파일을 로드하고 컴포넌트가 useEffect에서 처음 렌더링될 때 셀의 Universe를 생성해 보겠습니다.

const initiateWasm = async () => {
  wasm.default().then(() => {
    setUniverse(wasm.Universe.new());
  });
};
useEffect(() => {
  initiateWasm();
}, []);

표현

이제 WebAssembly 파일이 로드되었습니다. 페이지에 Universe를 어떻게 표시할지 생각해야 합니다.

가장 먼저 해야 할 일은 픽셀 단위로 우주의 크기를 계산하는 것입니다. 다행히도 WebAssembly에서 셀 수로 우주의 너비와 높이를 액세스한 다음 다음과 같이 상태에 저장할 수 있습니다.

const [width, setWidth] = useState<number>();
const [height, setHeight] = useState<number>();
useEffect(() => {
    if (universe) {
        setWidth(universe!.width());
        setHeight(universe!.height());
    }
}, [universe]);</number></number>

우리는 우리의 우주를 화면에 표시하고 싶습니다. 이를 위해 우리는 다음을 활용할 것입니다. 캔버스 – Javascript를 사용하여 그림을 그릴 수 있는 강력한 도구입니다.

return (
    <div>
        <canvas ref="{canvasRef}"></canvas>
    </div>
);

그런 다음 useEffect를 업데이트하여 캔버스의 높이와 너비를 설정합니다. 이를 위해 캔버스 참조를 사용하고 CellSize를 정의해야 합니다.

const canvasRef = useRef(null)
const CellSize = 5;
    useEffect(() => {
        if (universe) {
            setWidth(universe.width());
            setHeight(universe.height());
            const canvas = canvasRef.current!;
            if (universe.height()) {
                canvas.height = universe.height() * (CellSize + 1);
                setHeight(universe.height());
            }
            if (universe.width()) {
                canvas.width = universe.width() * (CellSize + 1);
                setWidth(universe.width());
            }
        }
    }, [universe]);

예쁜 그림을 그릴 시간입니다! 그리드부터 시작해 봅시다:

const GridColour = ‘#CCCCCC’
    const drawGrid = () => {
        const canvas = canvasRef.current!;
        const ctx = canvas.getContext("2d")!;
        ctx.beginPath();
        ctx.strokeStyle = GridColour;
        // Vertical lines.
        for (let i = 0; i <= width!; i++) {
            ctx.moveTo(i * (CellSize + 1) + 1, 0);
            ctx.lineTo(i * (CellSize + 1) + 1, (CellSize + 1) * height! + 1);
        }
        // Horizontal lines.
        for (let j = 0; j <= height!; j++) {
            ctx.moveTo(0, j * (CellSize + 1) + 1);
            ctx.lineTo((CellSize + 1) * width! + 1, j * (CellSize + 1) + 1);
        }
        ctx.stroke();
    };

다음 단계는 셀을 그리는 것입니다. WebAssembly 파일 내에서 셀은 살아있거나 죽은 것으로 저장됩니다. 위젯에 표시하려면 Wasm 메모리에 액세스해야 합니다.

Wasm 메모리에 대해 이해하는 데 중요한 한 가지는 그것이 선형적이라는 것입니다. 즉, JavaScript에서 본질적으로 바이트 배열(부호 없는 8비트 정수)로 액세스할 수 있다는 것을 의미합니다.

그래서 우리는 Wasm이 현재 셀의 위치를 ​​알려주는 지점에서 시작하여 메모리 버퍼에서 배열을 생성하고, 우주에 있는 모든 셀의 길이를 구합니다. 이 길이는 높이가 있는 셀의 수와 너비가 있는 셀의 수를 곱한 값과 같습니다.

        const cellsPtr = universe!.cells();
        const memory = wasm_memory();
        const cells = new Uint8Array(memory.buffer, cellsPtr, width! * height!);

이를 캔버스에 다음과 같이 번역할 수 있습니다.

    const getIndex = (row: number, column: number) => {
        return row * width! + column;
    };
    
    const DeadColour = ‘#FFFFF’
    const AliveColour - ‘#3a34eb’
    
    const drawCells = () => {
        const cellsPtr = universe!.cells();
        const memory = wasm_memory();
        const cells = new Uint8Array(memory.buffer, cellsPtr, width! * height!);
        console.log (cellsPtr)
        console.log (cells);
        const canvas = canvasRef.current!;
        const ctx = canvas.getContext("2d")!;
        ctx.beginPath();
        for (let row = 0; row < height!; row++) {
            for (let col = 0; col < width!; col++) {
                const idx = getIndex(row, col);
                ctx.fillStyle = cells[idx] === Cell.Dead ? DeadColour : AliveColour;
                ctx.fillRect(col * (CellSize + 1) + 1, row * (CellSize + 1) + 1, CellSize, CellSize);
            }
        }
        ctx.stroke();
    };

우리는 getIndex를 사용하여 우리의 행과 열을 Wasm 메모리에서 생성된 1D 배열의 위치로 변환합니다. 그런 다음 각 사각형을 살아있거나 죽었는지에 따라 적절한 색상으로 채웁니다.

애니메이션

이제 위젯 애니메이션을 시작할 수 있습니다. 사용자가 애니메이션을 멈추고 시작하도록 하고 싶습니다.

const [isPaused, setIsPaused] = useState(true);
    const btnClick = () => {
        setIsPaused(prevState => !prevState);
    };
    return (
        <div>
            <canvas ref={canvasRef} />
            <div className="btnContainer">
                <button onClick={btnClick} className="btn mx-button">
                    {isPaused ? "▶" : "⏸"}
                </button>
            </div>
        </div>
    );

애니메이션을 만들려면 다음을 사용하는 렌더 루프를 생성하려고 합니다. 요청애니메이션프레임() 그리드를 애니메이션화하기 위해 루프를 생성합니다. 우리는 다음을 사용합니다. 우주.틱() 모든 셀을 다시 계산한 다음 캔버스를 그립니다.

let animationId: number | null = null;
    const renderLoop = () => {
        universe!.tick();
        drawGrid();
        drawCells();
        animationId = requestAnimationFrame(renderLoop);
    };

그런 다음 이것을 사용하여 호출합니다. 레이아웃 효과 사용 그래서 업데이트되도록 브라우저가 화면을 그리기 전에.

    useLayoutEffect(() => {
        if (!isPaused) {
            renderLoop();
            return () => cancelAnimationFrame(animationId!);
        } else {
            if (animationId) {
                cancelAnimationFrame(animationId!);
            }
        }
    }, [isPaused]);

그런 다음 우리는 다음을 포함합니다. 애니메이션 프레임 취소 부작용을 피하기 위해 정리하는 과정에서. 우리는 애플리케이션을 실행하고 버튼을 클릭합니다…오류!

롤업 라운드업

이는 실제로 Wasm 파일을 브라우저에 로드하여 실행하지 않았기 때문입니다. 이를 위해, 우리에게는 오랜 친구 롤업이 필요해.

우리는 실행 npm i rollup-plugin-copy-save 루트 디렉토리에 롤업 파일을 생성하려면:

import copy from "rollup-plugin-copy";
 
export default args => {
    const result = args.configDefaultConfig;
    return result.map((config, index) => {
        if (index === 0) {
            const plugins = config.plugins || []
            config.plugins = [
                ...plugins,
                copy({
                    targets: [{ src: "node_modules/wasm-game-of-life-joerob319/*.wasm", dest: "dist/tmp/widgets/mendix/gameofLife/" }]
                })            ]  
        }
        return config;
    });
};

복사 플러그인은 Wasm 파일을 이동하여 위젯 패키지의 일부로 만들고 브라우저에 제공되도록 합니다.

위젯을 실행하면 인생 게임을 즐길 수 있습니다.

이제 해야 할 작은 일이 두 가지만 남았습니다…

세포를 죽이거나 되살리세요

그래서 우리의 Life 게임은 지금은 멋지지만, 사용자가 우주의 상태를 설정할 수 있는 기능이 없습니다. 사용자가 클릭하여 셀을 살아있거나 죽었는지 토글할 수 있도록 만들어 보겠습니다.

이를 위해 우리는 다음과 같은 함수를 생성합니다. addPointClick, 캔버스의 왼쪽 상단 위치와 클릭 위치를 찾아 클릭한 셀을 계산하고 캔버스에 적용된 모든 배율을 곱합니다. 행과 열이 있으면 다음을 사용할 수 있습니다. 토글_셀 웹어셈블리 파일 내에서 함수를 사용하여 우주를 업데이트한 다음 캔버스를 다시 그립니다.

    const addPointClick = (event: React.MouseEvent): void => {
        event.preventDefault();
        if (canvasRef.current!) {
            const node = canvasRef.current;
            const boundingRect = node.getBoundingClientRect();
            const scaleX = node.width! / boundingRect.width;
            const scaleY = node.height! / boundingRect.height!;

            const canvasLeft = (event.clientX - boundingRect.left) * scaleX;
            const canvasTop = (event.clientY - boundingRect.top) * scaleY;

            const row = Math.min(Math.floor(canvasTop / (CellSize + 1)), height! - 1);
            const col = Math.min(Math.floor(canvasLeft / (CellSize + 1)), width! - 1);

            universe!.toggle_cell(row, col);

            drawGrid();
            drawCells();
        }
    };

캔버스에도 꼭 추가하세요.

<canvas ref={canvasRef} onClick={addPointClick}/>

그리고 보일라...살아있네요!

위젯 구축 Mendix React Part 5_It's Alive Meme과 함께

위젯 구축 Mendix React Part 5_Cellular Automation Last와 함께

정리하는

파일 CellSize, GridColour, DeadColour, AliveColour에서 변수를 제거하여 마무리하고 위젯 사용자가 이를 설정할 수 있도록 합시다. xml을 다음과 같이 업데이트합니다.

<propertyGroup caption="General">
            <property key="CellSize" type="integer" required="true" defaultValue="5">
                <caption>Cell Size</caption>
                <description>Size of each square in px</description>
            </property>
            <property key="GridColour" type="string" required="true" defaultValue="#CCCCCC">
                <caption>Grid Colour</caption>
                <description>Colour of Grid</description>
            </property>
            <property key="DeadColour" type="string" required="true" defaultValue="#FFFFFF">
                <caption>Dead Colour</caption>
                <description>Colour of Dead Cells</description>
            </property>
            <property key="AliveColour" type="string" required="true" defaultValue="#000000">
                <caption>Alive Colour</caption>
                <description>Colour of Alive Cells</description>
            </property>
 </propertyGroup>

최종 부모는 다음과 같아야 합니다.

export function GameOfLife({ CellSize, GridColour, DeadColour, AliveColour }: GameOfLifeContainerProps): ReactElement {
    return (
        <GameOfLifeComponent
            CellSize={CellSize}
            GridColour={GridColour}
            DeadColour={DeadColour}
            AliveColour={AliveColour}
        />
    );
}

그리고 자식 변수를 업데이트하여 매개변수로 허용하도록 합니다(코드에서 기존 변수를 제거하는 것을 잊지 마세요!).

export interface GameOfLifeComponentProps {
    CellSize: number;
    GridColour: string;
    DeadColour: string;
    AliveColour: string;
}

export function GameOfLifeComponent({
    CellSize,
    GridColour,
    DeadColour,
    AliveColour
}: GameOfLifeComponentProps): ReactElement {

…..
}

이제 끝났습니다! C, C++, Rust 또는 Python으로 코드를 작성하고 이를 실행하는 방법을 살펴보았습니다. Mendix app. 언제나 그렇듯이, Github에서 위젯의 전체 코드를 찾을 수 있습니다. GitHub – joe-robertson-mx/gameOfLife.

끝은…사실 그렇지 않아요

Pluggable Widgets에 대한 이 시리즈는 여기서 끝입니다! 간단한 카운터 위젯을 만드는 것부터 WebAssembly를 실행하는 것까지 진행했습니다. Mendix 응용 프로그램.

이것이 끝이라는 것은 아닙니다... 앞으로 몇 주, 몇 달 동안 더 많은 훌륭한 위젯 콘텐츠가 나올 것입니다. 이 시리즈에 대해 보고 싶은 것이나 피드백이 있으면 이 스토리에 댓글을 남겨주세요.

언어를 선택하세요