Electron であそぼう
〜 Canvas で玉遊び 〜
2021-09-09 作成 福島
TOP > toys > ele-ball

前置き

ゲームエンジン (フレームワーク) を使用せず、Electoron だけを使用して直接 Canvas オブジェクトにグラフィックを描画します。

要するに「Node.js + Chromium = Electron」なので、Chromium で動作する JavaScript なら Electron でも動作します。


1. Electron のインストール
1-1. npm と Node.js のバージョンを確認する。

入っていなかったら、Node.js をインストールしておいてください。

通常ユーザで PowerShell を実行 (≠管理者)
スタート (右クリック) から「Windows PowerShell(I)」を選択

 >_ Windows PowerShell 
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6

PS C:\Users\who> npm -v 
6.14.12
PS C:\Users\who> node -v 
v14.16.1
PS C:\Users\who> exit 
1-2. ワーキングフォルダを作成し、npm で初期化する。
通常ユーザで PowerShell を実行 (≠管理者)
スタート (右クリック) から「Windows PowerShell(I)」を選択

 >_ Windows PowerShell 
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6

PS C:\Users\who> mkdir example 
PS C:\Users\who> cd .\example\ 
PS C:\Users\who\example> npm init -y 
Wrote to C:\Users\who\example\package.json:

{
  "name": "example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

PS C:\Users\who\example>

2-2. Electron をインストールする。
 >_ Windows PowerShell 

PS C:\Users\who\example> npm install electron 

> core-js@3.17.2 postinstall C:\Users\who\example\node_modules\core-js
> node -e "try{require('./postinstall')}catch(e){}"

Thank you for using core-js ( https://github.com/zloirock/core-js ) for polyfilling JavaScript standard library!

The project needs your help! Please consider supporting of core-js:
> https://opencollective.com/core-js
> https://www.patreon.com/zloirock
> https://paypal.me/zloirock
> bitcoin: bc1qlea7544qtsmj2rayg0lthvza9fau63ux0fstcz

Also, the author of core-js ( https://github.com/zloirock ) is looking for a good job -)


> electron@14.0.0 postinstall C:\Users\who\example\node_modules\electron
> node install.js

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN example@1.0.0 No description
npm WARN example@1.0.0 No repository field.

+ electron@14.0.0
added 87 packages from 95 contributors and audited 87 packages in 32.454s

6 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

PS C:\Users\who\example>
2-3. Electron のバージョンを確認する。
 >_ Windows PowerShell 

PS C:\Users\who\example> .\node_modules\.bin\electron.cmd -v 
v14.0.0
PS C:\Users\who\example>


3. プログラムを作成
3-1. index.js を作成する。
C:\Users\who\example\index.js (Canvas を定義して preload.js をロードする)
// メインプロセス (index.js)
"use strict" ;     // プログラムのミスを見つけやすくするための宣言。

const path   = require("path") ;

// Electron モジュール (クラス) の用意。
const ele = require("electron") ;

// イベントハンドラ 'ready' の定義。(Electron の初期化が完了したら呼ばれる)
ele.app.on('ready', () => {

    // HTML を用意する。
    let dynContent = "data:text/html; charset=utf-8,"
        + [
            "<html>",
            "<body id=body>",
                "<div align=center>",
                "<canvas id=screen width=320 height=200 style='border: solid black 1px;'/>",
                "</div>",
            "</body>",
            "</html>",
        ].join("") ;

    // ウィンドウを用意する。
    let win = new ele.BrowserWindow({
        width: 320 + 17*2, height: 200 + 96,
        webPreferences: { preload: path.join(__dirname, "preload.js") },
    }) ;

    win.setMenu(null) ;  // メニューバーを非表示にする。

    // ウィンドウに HTML を表示する。
    win.loadURL(dynContent) ;

    // ウィンドウが閉じたらインスタンスを解放する。
    win.on('closed', () => { win = null ; }) ;
}) ;

// イベントハンドラ 'window-all-closed' の定義。(ウィンドウがすべて閉じたら呼ばれる)
ele.app.on('window-all-closed', () => {

    if (process.platform == 'darwin')
        return ;        // Apple は quit() の必要がない。

    ele.app.quit() ;    // アプリケーションを終了する。
}) ;
3-2. preload.js を作成する。
C:\Users\who\example\preload.js (ロジック本体)
// レンダラプロセス (preload.js)
"use strict" ;

const ball = { color: "silver", r: 3, x: 0, y: 0, ix: +1, iy: +1, dx: 0, dy: 0 } ;
const racket = { color: "silver", w: 16, h: 3 } ;
const cursor = { x: 0, y: 0 } ;

let ctx = null ;
let screen = { color: "black", w: null, h:null } ;

let before = { x: -1, y: -1 } ;
let stroke = [] ;

// 玉の行動を決める。
function ball_action() {

    // 画面の壁に当たったら、方向を反転する。
    if (ball.x < 0) {
        ball.x = 0 ;
        ball.dx *= -1 ;
    }
    if (ball.x >= screen.w) {
        ball.x = screen.w ;
        ball.dx *= -1 ;
    }
    if (ball.y < 0) {
        ball.y = 0 ;
        ball.dy *= -1 ;
    }
    if (ball.y >= screen.h) {
        ball.y = screen.h ;
        ball.dy *= -1 ;
    }

    // ラケットに当たったかどうかの判定。
    if (cursor.y == ball.y) {

        if (cursor.x <= ball.x && ball.x <= cursor.x + racket.w) {
            // ラケットに当たった

            if (ball.dx > 0) {

                if (ball.x <= cursor.x + racket.w / 3) {
                    // 左から入って左端に当たった
                    ball.dx *= -1 ; // 反対側へ
                } else if (cursor.x + (racket.w / 3) * 2 <= ball.x) {
                    // 左から入って右端に当たった
                    ball.dx = Math.sign(ball.dx) * 2 ;  // チョップ
                } else {
                    // 玉の移動量を戻す。(X ベクトルの正負は変更しない)
                    ball.dx = Math.min(Math.abs(ball.ix),Math.abs(ball.dx)) * Math.sign(ball.dx) ;
                }

            } else {
                if (ball.x <= cursor.x + racket.w / 3) {
                    // 右から入って左端に当たった
                    ball.dx = Math.sign(ball.dx) * 2 ; // チョップ
                } else if (cursor.x + (racket.w / 3) * 2 <= ball.x) {
                    // 右から入って右端に当たった
                    ball.dx *= -1 ; // 反対側へ
                } else {
                    // 玉の移動量を戻す。(X ベクトルの正負は変更しない)
                    ball.dx = Math.min(Math.abs(ball.ix),Math.abs(ball.dx)) * Math.sign(ball.dx) ;
                }
            }

            ball.dy *= -1 ; // ラケットのどこかに当たったので、dy の符号を反転する。
        }
    }

    // 玉を移動する。
    ball.x += ball.dx ;
    ball.y += ball.dy ;
}

// 玉を描画する。
function ball_draw(ctx) {

    ctx.fillStyle = ball.color ;
    ctx.beginPath() ;
    ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI*2) ;
    ctx.fill() ;
}

// ラケットの行動を決める。
function racket_action() {

    // ラケットの移動を加速させる。
    let delta = 0 ;
    let z = 1 ;
    while (stroke.length > 0) {
        delta += stroke.shift().dx * z ;
        z *= 4 ;
    }

    // ラケットを移動する。
    cursor.x += delta ;

    // ラケットが画面内に収まるように表示座標を補正する。
    if (cursor.x <= 0) {
        cursor.x = 0 ;
    }
    if (cursor.x >= screen.w - racket.w) {
        cursor.x = screen.w - racket.w ;
    }
}

// ラケットを描画する。
function racket_draw(ctx) {

    ctx.fillStyle = racket.color ;
    ctx.fillRect(cursor.x, cursor.y, racket.w, racket.h) ;
}

// 画面全体を描画する。
function screen_draw() {

    racket_action() ;
    ball_action() ;

    ctx.fillStyle = screen.color ;
    ctx.fillRect(0,0, screen.w, screen.h) ;

    ball_draw(ctx) ;
    racket_draw(ctx) ;
}

// DOM (Document Object Model) のロード完了のイベントリスナを定義。
// DOM は preload の中で使う必要がある。
//
document.addEventListener('DOMContentLoaded', evt => {

    // HTML で ID を screen として定義した Canvas オブジェクトを取得する。
    const cvs = document.getElementById('screen') ;

    // 2D のコンテキストを取得する。
    ctx = cvs.getContext("2d") ;

    // Canvas からサイズを取得する。
    [ screen.w, screen.h ] = [ ctx.canvas.width, ctx.canvas.height ] ;

    // ラケットの高さ位置を計算する。
    cursor.y = screen.h - racket.h * 2 ;

    // 玉の移動量を初期化する。
    [ ball.dx, ball.dy ] = [ ball.ix, ball.iy ] ;

    screen_draw() ; // 初期画面を描画する。

    let timerId = setInterval(screen_draw, 20) ;    // 20ms (1/50 秒) ごとに画面を描画する。
});

// マウスカーソル移動のイベントリスナを定義。
//
document.addEventListener('mousemove', evt => {

    let [ dx, dy ] = [ 0, 0 ] ;

    if (before.x != -1 && before.y != -1) {
        dx = evt.offsetX - before.x ;
        dy = evt.offsetY - before.y ;
    }

    stroke.push({ dx: dx, dy: dy }) ;

    before = { x: evt.offsetX, y: evt.offsetY } ;
}) ;

描画タイミングについて
TV アニメの表示速度は 30 フレーム/秒。
一般的な PC のディスプレイやビデオゲームは 60 フレーム/秒。
なので、本稿では間をとって 50 フレーム/秒 とした。(PC のディスプレイ速度より速くても意味がない)
本来は V-SYNC (ディスプレイの垂直同期信号) に合わせるべき (window.requestAnimationFrame) だが、ここではそれを利用していない。


4. 実行
Electron で実行するとマウスの左右動作にラケットが連動するので、玉を打ち返してみてください。
 >_ Windows PowerShell 

PS C:\Users\who\example> .\node_modules\.bin\electron.cmd . 
PS C:\Users\who\example>
 

このページで動作中のデモ画面 (demo.js) は、上記プログラムとは構造が多少異なります。