20200521のJavaScriptに関する記事は28件です。

JavaとJavaScriptでwebブラウザとのソケット通信②

背景

過去記事:https://qiita.com/take4eng/items/d0b009c48ee8c3fe420a

上記の過去記事に記述しているように、Javaでサーバープログラムを作成しソケット通信を実施。
⇒ HTTP通信をゴリ押しで解析しているため、無駄に複雑なコードになっている。

Java EEにはsocket通信に関するAPIが多数存在し、非常に簡単に実装することが可能。
既に多くの人がまとめてくれてはいるが、実装した内容をまとめておく。

便利なAPIが用意されているのにゴリ押しで解析なんて誰もやらないよね
そりゃググってもなかなか出てこないわ…

実践内容

  1. WebSocket API の使用方法
  2. APIを使用したサーバープログラムを作成
  3. 過去記事内のクライアントプログラムコードを編集しチャットアプリ作成

WebSocket API の使用方法

基本的なWebSocket APIについて説明する。
ここで紹介するもの以外にも多数のAPIが存在するが、必要ならググると良い。

Endpointクラスの作成

import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/コンテキストパス")
public class SanpleEndpoint {
}
  1. ServerEndpointクラスをインポート
  2. クラスにアノテーション@ServerEndpointを付与
  3. ファイルの場所を示すコンテキストパスを記述

処理メソッドの作成

/*
各クラスをインポートしておく
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
*/

//クライアントと接続したときの処理
@OnOpen
public void onOpen(Session session) {
}

//クライアントからメッセージを受け取ったときの処理
@OnMessage
public void onMessage(String message) {
}

//エラーが発生したときの処理
@OnError
public void onError(Throwable error) {
}

//クライアントと接続が切れたときの処理
@OnClose
public void onClose(Session session) {
}
  1. 必要なクラスをインポート
  2. メソッドに対応した各アノテーションを付与
  3. 引数は必要に応じて変更 , 追加可能

APIを使用したサーバープログラムを作成

サーバープログラム
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

// Webソケットのサーバ側クラスであること表すアノテーション。
// 引数(wSck)はクライアントから接続時、使われるURIを表す。
@ServerEndpoint(value = "/wSck")
public class SocketFree2 {

    //クライアントのセッションスレッドを作成(クライアント毎にそれぞれのセッションを保存)
    //Set:重複要素のないコレクション
    //CopyOnWriteArrayList:java.util.Setをスレッドセーフにしたもの
    private static Set<Session> user = new CopyOnWriteArraySet<>();

    @OnOpen//クライアントと接続したとき
    public void onOpen(Session mySession) {
        System.out.println("connect ID:"+mySession.getId());//session.getId():セッションIDを取得
        user.add(mySession);//クライアント毎のセッションをリストに追加
    }

    @OnMessage//クライアントからデータが送信されたとき
    public void onMessage(String text , Session mySession) {//引数は送信されたテキストと送信元のセッション
        System.out.println(text);
        //getAsyncRemote():RemoteEndpointのインスタンスを取得
        //sendText(String):クライアントにテキストを送信
        for (Session user : user) {
            user.getAsyncRemote().sendText(text);
            System.out.println(user.getId()+"番目に"+mySession.getId()+"番目のメッセージを送りました!");
        }
        if(text.equals("bye")) onClose(mySession);//textが「bye」なら切断する
    }

    @OnClose//クライアントが切断したとき
    public void onClose(Session mySession) {
        System.out.println("disconnect ID:"+mySession.getId());
        user.remove(mySession);//切断したクライアントのセッションをリストから削除
        try {
            mySession.close();//closeメソッドで切断
        } catch (IOException e) {
            System.err.println("エラーが発生しました: " + e);
        }
    }

}

コードの解説

1.クラスにEndpointアノテーション@ServerEndpointを付与し、コンテキストパスを記述
@ServerEndpoint(value = "/wSck")

2.各クライアントを識別するリストを作成
private static Set<Session> user = new CopyOnWriteArraySet<>();

3.onOpenメソッド:クライアントのセッションをリストに追加
user.add(mySession);

4.onMessageメソッド:受信したテキストをそのままクライアントへ送信
⇒ for文で接続しているクライアント全員に送信する
user.getAsyncRemote().sendText(text);

5.onCloseメソッド:接続が切れたクライアントを削除
5-1.userリストから削除:user.remove(mySession);
5-2.セッションを削除し接続を切る:mySession.close();

実行結果

クライアントプログラムのアドレスを変更し、webブラウザで実行。
※コード内容は過去記事を参照
var wSck= new WebSocket("ws://localhost:8080/プロジェクト名/コンテキストパス");
(今回なら"ws://localhost:8080/freeWeb2/wSck")

image.png
送信した内容が表示されるチャットアプリが完成。
複数のブラウザからのアクセスにも対応している。

感想

APIを使用することで非常に簡単にソケット通信を行うことができた。
過去記事で記述しのものと比べるとコードの記述量は約1/4。すごい。超簡単。

他にもエンコード、デコード処理やjsonデータの扱いについても簡単にできるらしい。
今回は使用していないが、本格的な開発をするなら必要になるだろう。
詳細は参考ページを参照。

参考ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで描くトランプの図柄

トランプの図柄

前回の記事では Canvas API の基本的な扱いや注意点を述べました。今回はそのことを踏まえ、トランプの画像を Canvas API で描いていこうと思います。

それでは改めてトランプの図柄を見ていきましょう。縦横ともに 48px の正方形でカード部分は白で塗りつぶし、黒で囲います。その上に、上部にスート(記号)、下部にランク(数値)を配置する。これが基本形となります。
spade01.png

今回も雛形となる html ファイルを用意しましょう。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8"/>
  </head>
  <body>
    <canvas id="cvs">
    <script>
    </script>
  </body>
</html>

それではコードを書いていきましょう。

ベースの描画

今回は canvas 要素取得した段階で、縦横のサイズを設定しましょう。

const canvas = document.getElementById('cvs');
canvas.width = 48;
canvas.height = 48;
const ctx = canvas.getContext('2d');

canvas の基本サイズは縦 150px の横 300px です。きちんとサイズを変更してやらないと余計な空白ができてしまいます。あと CSS で指定するサイズとは別物ですので注意して下さい。

それではこの canvas に、単純な四角形を描いてみましょう。

ctx.beginPath();
ctx.moveTo( 0, 0);
ctx.lineTo(47, 0);
ctx.lineTo(47, 47);
ctx.lineTo(0, 47);
ctx.closePath();
ctx.stroke();

上と左の線が細いですね。これは指定した座標を中心として線が描画されるため生じる現象です。つまり左は -0.5 から 0.5 の 1px 分、右は 46.5 から 47.5 の 1px 分の太さで線を描画している、結果、左側は 0.5px 分の細い線になってしまうというわけです。なので 0 となっているところはすべて 1 に変更しましょう。

それではこの四角形の角を落としてみましょう。

ctx.beginPath();
ctx.moveTo( 6, 1);
ctx.lineTo(42, 1);
ctx.lineTo(47, 6);
ctx.lineTo(47, 42);
ctx.lineTo(42, 47);
ctx.lineTo(6, 47);
ctx.lineTo(1, 42);
ctx.lineTo(1, 6);
ctx.closePath();
ctx.stroke();

落とされた角は、まだ直線で繋げられています。この角を、弧を使ってつなげてみましょう。

まず右下の角に注目しましょう。この角は三時から六時の 90 度で弧を描く必要があります。前回も述べた通り arc の描画開始角度は三時を 0 度とします。なので以下のように書けば求める描画ができます。

ctx.beginPath();
ctx.moveTo( 6, 1);
ctx.lineTo(42, 1);
ctx.lineTo(47, 6);
ctx.lineTo(47, 42);
// ctx.lineTo(42, 47);
ctx.arc(42, 42, 5, 0, Math.PI / 2);
ctx.lineTo(6, 47);
ctx.lineTo(1, 42);
ctx.lineTo(1, 6);
ctx.closePath();
ctx.stroke();

それでは左下はどうコードを書けばいいでしょうか? 六時を開始角度として 90 度の弧を描けばいいのだから、以下のようなコードを思い浮かべると思います。

ctx.arc(6, 42, 5, Math.PI / 2, Math.PI / 2);

しかしこのコードでは思い通りの描画がされません。arc の終点角度は開始角度から何度というものではなく、円全体のどの角度で終わるかを指定する必要があります。終点角度は九時の角度になりますから丁度 180 度、ラジアンだと Math.PI となります。よってコードは以下の通りとなります。

ctx.beginPath();
ctx.moveTo( 6, 1);
ctx.lineTo(42, 1);
ctx.lineTo(47, 6);
ctx.lineTo(47, 42);
// ctx.lineTo(42, 47);
ctx.arc(42, 42, 5, 0, Math.PI / 2);
ctx.lineTo(6, 47);
// ctx.lineTo(1, 42);
ctx.arc(6, 42, 5, Math.PI / 2, Math.PI);
ctx.lineTo(1, 6);
ctx.closePath();
ctx.stroke();

それでは上のふたつの角も丸めてみましょう。

ctx.beginPath();
ctx.moveTo( 6, 1);
ctx.lineTo(42, 1);
// ctx.lineTo(47, 6);
ctx.arc(42, 6, 5, -1 * Math.PI / 2, 0);
ctx.lineTo(47, 42);
// ctx.lineTo(42, 47);
ctx.arc(42, 42, 5, 0, Math.PI / 2);
ctx.lineTo(6, 47);
// ctx.lineTo(1, 42);
ctx.arc(6, 42, 5, Math.PI / 2, Math.PI);
ctx.lineTo(1, 6);
ctx.arc(6, 6, 5, Math.PI, -1 * Math.PI / 2);
ctx.closePath();
ctx.stroke();

角は中心座標と半径を調節して、好みの弧を描いてみて下さい。

さて、枠が描けたところで body 要素に背景色を設定してみてくださ。

<body style="background-color:forestgreen">

枠の中も緑色になってしまいますね。そう、塗りつぶしをまだ行っていませんでした。

fill も stroke も、デフォルトの色は黒になります。塗りつぶしは白で行うので、まずは色を変更しましょう。

ctx.fillStyle = "white";

そして塗りつぶしですが、線描画を行う前に塗りつぶしを行って下さい。順番を逆にしてしまうと線も塗りつぶされてしまいます。

ctx.fill();
ctx.stroke();

すこし長くなってしまったので、今回は最後にここまでのコードをアップして次回につなげようと思います。

それでは。

const canvas = document.getElementById('cvs');
canvas.width = 48;
canvas.height = 48;
const ctx = canvas.getContext('2d');

ctx.fillStyle = "white";
ctx.strokeStyle = "black";

ctx.beginPath();
ctx.moveTo( 6, 1);
ctx.lineTo(42, 1);
ctx.arc(42, 6, 5, -1 * Math.PI / 2, 0);
ctx.lineTo(47, 42);
ctx.arc(42, 42, 5, 0, Math.PI / 2);
ctx.lineTo(6, 47);
ctx.arc(6, 42, 5, Math.PI / 2, Math.PI);
ctx.lineTo(1, 6);
ctx.arc(6, 6, 5, Math.PI, -1 * Math.PI / 2);
ctx.closePath();
ctx.fill();
ctx.stroke();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EJSで簡易CMSのようなもの

EJSとは

Node.jsでコンパイルできるHTMLテンプレートエンジン。HTMLの共通部分をJavaScriptのような書き方で効率化することができる便利なもの。

EJSで簡易CMSのようなものを作る

今回やりたいことはタイトルの通り。EJSを使って簡易CMSのようなものを作りたい。EJSではJavaScriptのfor文のように繰り返し処理を扱うことができるから、この仕組を用いて簡易CMSのようなものを作ります。

作ろうと思ったきっかけ

今の会社でHTMLメールの制作をしているのですが、これが毎回ほぼ使いまわしで、中身の変更をコピペで何回もしないといけなかった。また、コピペでやってるからたまに変更忘れとかが起こっていた。
これらをどうにか時間短縮とミスを減らすことができないかと考えた結果、EJSを使えば簡易的なCMSのようなものが作れそうだとなり実際にやってみた。

まずはEJSが動く環境を作る

EJSはそのままでは機能しません。タスクランナーを使ってHTMLに変換してあげる必要があります。
今回はgulpで環境構築をしました。

以下、環境構築の説明ですがgulpを動かすにはNode.jsをインストールしてnpmコマンドを動くようにしておく必要があります。
Node.jsのインストールからnpmコマンドを使えるようにするまではics.mediaさんのこちらの記事などを参考に済ませておいてください。
https://ics.media/entry/3290/

記事の「3. package.jsonファイルの作成」まで進めたら以下の必要なパッケージをインストールしてgulpファイルを用意できたら環境構築が完成です。ここからはコピペしてもらったら大丈夫です!

ディレクトリ構成図

package.json
├── fuga.py
├── fuga_dir
   ├── fuga
   └── fugafuga
├── fugafuga.py
├── fugahoge.py
├── hoge.py
└── hoge_dir
    ├── hoge
    └── hogehoge

必要なパッケージ

  • gulp(gulpを動かすのに必要な本体)
  • gulp-ejs(EJSをコンパイルするもの)
  • gulp-rename(EJSをコンパイルして.htmlの形式にリネームするときに使用)

この3つが最低限必要なパッケージです。
今回はこれに加えてブラウザで表示確認もおこないたかったので以下の3つのパッケージもインストールします。

  • browser-sync(ローカルサーバーの立ち上げ)
  • gulp-plumber(エラー時の強制終了を防止)
  • gulp-notify(エラー発生時にデスクトップ通知する)

これらをそれぞれnpm installしていきます。
コマンドプロンプト上からcdコマンドで作業フォルダにて下記、コマンドでインストール。そのままコピペでOK!
npm i -D gulp
npm i -D gulp-ejs
npm i -D gulp-rename
npm i -D browser-sync
npm i -D gulp-plumber
npm i -D gulp-notify

package.jsonの中身が以下のような感じになっていたらOK!
つぎはgulpファイルを準備します。

package.json
"devDependencies": {
    "browser-sync": "^2.26.7",
    "gulp": "^4.0.2",
    "gulp-ejs": "^5.1.0",
    "gulp-notify": "^3.2.0",
    "gulp-plumber": "^1.2.1",
    "gulp-rename": "^2.0.0"
  }

gulpfile.jsを用意する

gulpfile.jsを用意します。
gulpfile.jsのなかにどのような処理を実行するかということを書き込むことで動かすことができます。

gulpfile.js
const { src, dest, watch, series } = require("gulp");
const browserSync = require("browser-sync").create();
const plumber = require("gulp-plumber");
const notify = require("gulp-notify");
const ejs = require("gulp-ejs");
const rename = require("gulp-rename");

function buildServer(done) {
  browserSync.init({
    server: {
      baseDir: "./",
      port: 3000,
    },
    online: true,
  });
  done();
  console.log("server launched!");
}
function serverReload(done) {
  browserSync.reload();
  done();
  console.log("reload completed!");
}
function Ejs() {
  return src(["./ejs/**/*.ejs", "!" + "./ejs/**/_*.ejs"])
    .pipe(plumber({ errorHandler: notify.onError("Error: <%= error.message %>") }))
    .pipe(ejs({}, {}, { ext: ".html" }))
    .pipe(rename({ extname: ".html" }))
    .pipe(dest("./"));
}
function WatchFiles() {
  console.log("start watch!");
  watch("./ejs/**/*.ejs", series(Ejs, serverReload));
  watch("./**/*.html", series(serverReload));
}
exports.default = series(buildServer, Ejs, WatchFiles);
exports.Ejs = Ejs;

参考にさせていただいた記事

https://ics.media/entry/3290/
https://www.i-ryo.com/entry/2020/05/17/094446
https://qiita.com/y_hokkey/items/31f1daa6cecb5f4ea4c9

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのアロー関数が好きではない

JavaScriptのアロー関数が好きではない。

const f = () => { a++; };

let a = 1;
f();
console.log(a); // 2 ←

こんな仕様当然みんなからフルボッコのWTF案件だろうと思いきや、批判以前にそもそもこの仕様について言及している記事自体がほとんど見当たらない。
MDNにすらはっきりとは書かれていない。
仕様書まで追えば当然載ってはいるんだけど、こんなところまでいちいち読んでられませんよね。

まあスコープチェーンというJavaScriptデフォルトの仕様のせいであるわけですが、しかしわざわざ最近追加された仕組みなのだからこんなクソ仕様は断ち切ってほしかったところですね。
アロー関数の仕様を決めるときに誰も何も言わなかったのか?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTMLで表のデータがクリックされた時にJavaScript関数を実行する

HTMLで表のデータがクリックされた時にJavaScript関数を実行したい。
このような状況があり苦戦したので残しておく。

イベントハンドラをタグの属性部分に指定すれば、そこからJavascriptを実行できるようになる。
タグなどによって使用できるハンドラが異なっているようなので注意が必要。

<!DOCTYPE html>
<html>
    <head>
        <meta charaset="UTF-8">
        <title>表のデータがクリックされた時JavaScriptを実行したい</title>
        <script type="text/javascript">
            //表のデータをクリックしたときの処理
            function PushData(){
                alert("表のデータがクリックされました。");
            }
        </script>
    </head>
    <body>
        <div align="center">
            <h2>テーブル</h2>
        </div>
        <div align="center">
            <table border="1">
                <tr>
                    <th onclick="PushData()">見出し1</th>
                    <th onclick="PushData()">見出し2</th>
                </tr>
                <tr>
                    <th onClick="PushData()">データ1</th>
                    <th onclick="PushData()">データ2</th>
                </tr>
            </table>
        </div>
    </body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サクッとaタグでPOST送信(自分メモ)

はじめに

今回、aタグでpost送信したい時(ログアウトのリンクをクリック)がありましたので、その記述をメモとして残します。簡易的な自分用のメモですので、その点はご了承くださいませ。

記述方法

<form
  method="POST"
  name="logout_form"
  action="/logout">
  <!--CSRF-->
  <a href="javascript:logout_form.submit()">ログアウト</a>
</form>

aタグのhref属性にjavascript:(実行するJavaScriptコード) の形式で記述します。
上記で行っていることは、formのname属性を指定してあげて、submit()でフォームの内容を送信しています。
はい、以上になります。

おわりに

簡易なメモではありますが、もし間違い等ありましたらご指摘をお願いいたします。

参考資料

aタグをPOSTにする
HTMLのAタグでPOSTする方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Yellowfinのv9ダッシュボードでレポートの値を取得する方法からPOSTまで

概要

Yellowfinのversion9ではダッシュボードの自由度が上がり、Javascriptを使って外部APIや自社サイトへ値をPOSTすることができるようになりました。そこで、今回はフォームを用意してレポートの値をフォームに写し、外部サイトにPOSTするというところを中心にやっていきます。(コード中のコメントでだいぶ補足してます・・・)
kintone登録.gif

表示するレポートを選ぶ

image.png
レポートを検索し、ドロップすることで対象のレポートを表示します。
また、フォームはHTMLウィジェットをドラッグアンドドロップで配置して、コードモードで編集します。

コードモードのHTMLタブ

コードモードではこのようになります。
レポートと、フォーム、ボタン、テキストのウィジェットを配置したあとにその中のname要素を変更する感じです。

dashboard.html
<canvas-area xmlns="http://www.w3.org/1999/xhtml" canvas-uuid="76a8ae2b-4f35-491a-9e3a-b8e66117ac97">
<report-output widget-uuid="dfdc880e-0216-4fdb-8d43-2caa21874aae" report-uuid="1bc0874b-2874-4dd5-87b5-4e1af27d9f7b" height="233" top="103" left="14" name="Sample" width="496" display-type="TABLE" style="z-index: 2"></report-output>
    <text-title-big widget-uuid="61cbf9ad-ffe2-41b3-98a4-c24ffc1b2526" width="547" height="69" left="14" top="18" line-spacing="40" character-spacing="0" rotation="0" name="Main Page Title" style="z-index: 3"><font xmlns="http:\/\/www.w3.org\/1999\/xhtml" color="#666666">Website Visits</font> 
<br xmlns="http:\/\/www.w3.org\/1999\/xhtml" /></text-title-big>
    <text-title widget-uuid="c749399d-0a8d-4273-bab9-f3290f454a89" width="425" height="61" left="14" top="56" line-spacing="30" character-spacing="0" rotation="0" name="Title (4)" style="z-index: 10"><font xmlns="http:\/\/www.w3.org\/1999\/xhtml" style="font-size: 24px;" face="Libre Franklin" color="#666666">Region Status and Uptime</font> 
<br xmlns="http:\/\/www.w3.org\/1999\/xhtml" /></text-title>
    <canvas-button widget-uuid="5acd6d3e-74b0-41d5-9744-fe2bb23cadb0" width="100" height="32" top="336" left="869" plugin-name="com.hof.mi.widgetcanvas.widgettemplate.CodeButtonCirclePurpleTemplate" text-color="#9C6ADE" name="Submit" style="background-color: #FFFFFF; border-radius: 6px; border: 2px solid rgb(102, 102, 102); z-index: 11; opacity: 1.0"></canvas-button>
    <custom-html class="requestForm" name="Form Container" widget-uuid="2e9bc2ad-54da-4a58-a374-5258e155372a" width="381" height="257" top="63" left="588" rotation="0" style="z-index: 7">
        <div class="form-title">kintoneに登録</div>
        <div class="form-row">
            <div class="form-label">Region: </div>
            <input type="text" name="pageTitle" />
        </div>
        <div class="form-row">
            <div class="form-label">Invoice: </div>
            <input type="text" name="exitRate" />
        </div>
        <div class="form-row">
            <div class="form-row-label">Enter Comments:</div>
        </div>
        <div>
            <textarea rows="5" name="comments"></textarea>
        </div>
    </custom-html>
    <text-paragraph widget-uuid="cedea1aa-cc5f-4209-996d-256ed18ed5af" width="493" height="33" left="588" top="389" line-spacing="17.5" character-spacing="0" rotation="0" name="Response" style="z-index: 9"></text-paragraph>
</canvas-area>

JSタブ

この中で、マウスクリックの列を取得し、中身をフォームにコピーします。
ついでにボタンにイベントを仕込み、指定したURLにデータをPOSTしてレスポンスを表示しています。

jstab.js
let report = this.apis.canvas.select('Sample');//HTMLタブ中のreport-outputタグのnameの値をセットする
let button = this.apis.canvas.select('Submit');//canvas-buttonタグのnameの値をセットする
let responseContainer = this.apis.canvas.select('Response');//必要ではないが、responseを取得した際に表示させるtext要素
/* Add an event listener to the report table so when you click it prefills the form*/
report.addEventListener('click', (event) => {
    let pageTitle = this.apis.canvas.select('pageTitle');//form上の要素のnameを指定する
    let comments = this.apis.canvas.select('comments');
    let exitRate = this.apis.canvas.select("exitRate");
    let $row = $(event.target).closest('tr');//クリックした部分に一番近い行のデータを取得
    pageTitle.value = $row.find('td').get(0).innerText;//左から0番目の値を取得し、formに代入
    exitRate.value = $row.find('td').get(2).innerText;//左から2番目の値を取得し、formに代入
    comments.innerText = $row.find('td').get(2).innerText;//適当
})
/* Setup a click listener on the Ok button so it sends an API request to anywhere */
    button.addEventListener('click', () => {//クリック時のイベントを実装
        let pageTitle = this.apis.canvas.select('pageTitle');
        let comments = this.apis.canvas.select('comments');
        let exitRate = this.apis.canvas.select("exitRate");
var sendurl = "http://localhost/test.php";//POSTしたいURLを指定、ここではphpの中でkintoneに登録し、returnで登録したidを取得しています。
var param = "data="+pageTitle.value+"_"+comments.value;
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function()
{
    var READYSTATE_COMPLETED = 4;
    var HTTP_STATUS_OK = 200;
    if( this.readyState == READYSTATE_COMPLETED
     && this.status == HTTP_STATUS_OK )
    {
        // レスポンスの表示URLなどは例です
        responseContainer.innerHTML = "確認はこちら: <a href=https://kintone.cybozu.com/k/135/show#record=" + this.responseText + " target='_blank'>https://kintone.cybozu.com/k/135/show#record=" + this.responseText + "</a>";
    }
}
xhr.open('POST', sendurl);
xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
xhr.send( param );
});

CSSタブ

ついでにスタイルシートをいじって少し修飾します。

csstab.css
custom-html.requestForm {
    font-family: Libre Franklin;
}

custom-html.requestForm div.form-row {
    margin-top: 15px;
    display: flex;
}

custom-html.requestForm div.form-title {
    font-size: 24px;
    color: #666666;
}

custom-html.requestForm div.form-label {
    text-align: left;
    font-size: 18px;
    display: inline-block;
    width: 30%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    height: 25px;
    line-height: 25px;
    padding-right: 5px;
    color: #666666;
}

custom-html.requestForm div.form-row-label {
    font-size: 18px;
    color: #666666;
}

custom-html.requestForm input {
    width: calc(70%);
    padding: 0;
    font-size: 14px;
    border: 1px solid #666666;
    border-radius: 0;
    height: 23px;
}

custom-html.requestForm textarea {
    width: calc(100% - 6px);
    resize: none;
    font-size: 14px;
    border: 1px solid #666666;
    border-radius: 0;
}

こんな感じで

APIがあるサービスならそこにJSONでもbodyのテキストをつっこんで投げればよし、自社DBに値を突っ込みたいなら自社内のwebserverからそのDBに登録するスクリプトを介して登録すればよしと、間にひとつかます必要がありますが、ダッシュボード中のレポートの結果を見ながら、新しい値を登録したりといったこともできるようになりました。

アイデア次第ではかなりいろんなことができそうな気がしてきますね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js & Contentful & Netlifyを使いポートフォリオサイトを作成しました

はじめに

簡単に自己紹介をしますと私は現在Vue.js中心に学習中のエンジニア未経験、フロントエンドエンジニア志望の者です。

ただ淡々と学習するのではなくアウトプットを通じて学習したことを深く理解すること、記事を残すことでこれを見た人へ何かしらのヒントになる情報を与えられることができればと思い作成しました。

目標物

TechpitNuxt.js & Contentfulでハイスペックなポートフォリオサイトを超簡単に公開しよう!【JAMstack】という教材からポートフォリオサイトを作成しました。

Nuxt.jsとは?

公式サイト
Vueから設計されたフレームワークです。
SSR(サーバサイドレンダリング)アプリケーションを簡単に作ることができます。

SSR(サーバサイドレンダリング)
本来クライアントサイドで実行して、レンダリングされるjavascriptの処理を、サーバサイドで実行して、レンダリングする仕組み。

Contentfulとは?

公式サイト
Wordpress等と同じCMSの一種です。

CMS
=> Contents Management System(コンテンツ・マネジメント・システム)

webサイトを簡単に構築・管理・更新できるシステムです。

JAM stackとは?

公式サイト
サーバーやデータベースに依存せずにサイトやアプリを作成します。
記事データはAPIで用意し、Nuxt.jsで各ページをマークアップすることで動的なコンテンツで静的なウェブサイトを構築する技術です。

主なメリット

  • 表示が高速である
  • バックエンドをいじらないためセキュリティを気にしないで済む

環境

エディタ
Visual Studio Code

ライブラリ等
node 12.16.3
npm 6.14.4
nuxt 2.12.2
+ nuxtjs/markdownit 1.2.9
+ nuxt-fontawesome 0.4.0
+ fortawesome/free-solid-svg-icons 5.13.0
+ fortawesome/free-brands-svg-icons 5.13.0
+ contentful .14.4

完成した物

こちらのリンクから実際のサイトをご覧できます。

画像

トップページ
スクリーンショット 2020-05-21 18.40.47.png
詳細ページ
スクリーンショット 2020-05-21 18.41.02.png

設計

ポートフォリオサイトページ構成

ページ 表示
トップページ 作品一覧
作品個別ページ 各作品に関する詳細情報
カテゴリページ 特定のカテゴリに属する作品の一覧
タグページ 特定のタグを持つ作品の一覧
検索結果ページ 特定のキーワードを含む作品の一覧

作品データ(Contentful)

フィールド名 説明
Tittle 記事のタイトル
Slug 記事のスラッグ
Subtitle 記事のサブタイトル
Date 作成日時
Category カテゴリ
Tags タグ(複数登録可)
Content 記事本文
Image 記事のサムネイル画像

Slug(スラッグ)について
SlugとはContentfulで記事を作成した時に設定する、サイト内の特定のページを識別するためのURLの一部です。
「vue.js-nuxt.js-portfolio」のように、キーワードをつなげて作ります。

作成中に起きたトラブル

問題1.

contentfulの記事データが読み込めない

解決方法
APIkeyを使う
「Space ID」と「Content Delivery API - access token」

// .contentful.json ファイル内
{
  "CTF_SPACE_ID": "Space ID",
  "CTF_CDA_ACCESS_TOKEN": "Content Delivery API - access token"
}

問題2.

ブラウザに【未定義のプロパティ「field」を読み取れません。】と出てサイトが表示しなくなる。

解決方法
原因:Contentfulで新規記事作成の時Imageを追加していなかった
解決:サムネイル画像を設定

問題3.

npmインストールしたらディレクトリ構成が変わってサイトが表示しなくなってしまった

解決方法
作業内容を戻す

$ git reset --hard HEAD

問題4.

NetlifyでGitHubと連携したデプロイができない。

Error: Cannot find module ‘Contentful’

解決方法?

Git連携を諦めて普通にアップロードする

$ npm run generate

でdistファルダ作成
Netfilyでdistフォルダをドラッグ&ドロップ

まとめ

最後に連携で詰まってしまい、5時間くらいかかってできなかったので一旦普通にデプロイしました。
いずれ再挑戦します。

フロントエンドエンジニア志望としては
Contentfulというサービスの使い方は学んでおいて損はなさそうです。
簡単にコンテンツが追加できるのは楽しいですね。

Nuxt.jsは初めてでしたがVue.jsが基本なので苦手な感じはしませんでした。
色々とエラーで苦しみながら成長できた気がします。
いい経験になりました!

これから自分なりに機能を追加したりスタイルを変えたりしてみようと思います。

ここまで読んでいただきありがとうございました。

Link

ソースコード

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

作って理解JavaScript:JOKE開発記 その2

はじめに

ノリで作り始めたJavaScript処理系JOKEのステップ2開発記です。
JOKEとはなんなのか(なんではないのか)等については開発記その1もご参照ください。

今回のスコープ

変数を宣言し利用(参照)できるようにします。ただし、

  • constとletのみ
  • var(とそこから発生する巻き上げ動作)はサポートしない
  • 宣言しないで変数を使うことはできない
  • 分割代入は今後の課題

具体的には以下のプログラムが「正しく」動作することを目指します。1

step/step0002_01.js
// 定数を宣言し参照する
const s = "foo";
console.log(s);
step/step0002_02.js
// 変数を宣言し、値を代入する
let s;
console.log(s);
s = "foo";
console.log(s);
step/step0002_03.js
// ブロックで囲めば同じ名前の変数を宣言可
// シャドーイングされていないブロック外の変数を参照できるかも確認
const s1 = "foo";
const s2 = "bar";

{
    const s1 = "FOO";
    console.log(s1, s2);
}

変数機能の仕様と実装

ソースコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0002

Validator導入

constやletでの変数定義に関する仕様は以下に書かれています。
13.3.1 Let and Const Declarations

BNFは以下のようになっています。

LexicalDeclaration :
    LetOrConst BindingList ;

BindingList :
    LexicalBinding
    BindingList , LexicalBinding

LexicalBinding :
    BindingIdentifier Initializer
    ※Initializerはopt(なくてもよい)

Initializer(宣言時の初期化)はoptなので以下のプログラムは構文解析的には通ってしまいそうですがもちろんこのプログラムは駄目でブラウザでもNode.jsでもSyntaxErrorになります。

const s;

これをSyntaxErrorにすべきことは13.3.1.1に書かれています。

It is a Syntax Error if Initializer is not present and IsConstantDeclaration of the LexicalDeclaration containing this production is true.

IsConstantDeclarationの定義は13.3.1.3で書かれています。要するにconstで宣言されているという当たり前のことが(仕様として厳密に)書かれています。
ともかく、これについては構文解析時にInitializerとして解析したノードがあるかを調べれば実装できます。

厄介なのは次のケースです。
(同じスコープ内に)同じ名前で変数を宣言しているためにこれはNGです(仕様でも明記されています)

const s = "foo";
const s = "bar";

ただ、この「すでに同じ名前の変数があるか」チェックは純粋な構文解析の段階でやろうとすると複雑化が予想されます2。そこで、とりあえず構文解析は通した(ノードを作った)後に意味解析としてvalidationを行うようにしました。つまり、ParserとAssemblerの間に工程が増えたことになります。

  1. Scanner
  2. Parser
  3. Validator ← New!
  4. Assembler
  5. Vm

letはキーワードではない

上記のようにInitializerのチェック、Validatorを導入しての変数名重複のチェックによりconstはできたので次にletに取り掛かりました。
実際にはconstをキーワードとして扱うようにScannerを改造する際に仕様の対応部分を参照したのですが

Keyword :: one of
    break       do          in          typeof
    case        else        instanceof  var
    catch       export      new         void
    class       extends     return      while
    const       finally     super       with
    continue    for         switch      yield
    debugger    function    this
    default     if          throw
    delete      import      try

(。´・ω・)? あれ?letなくない?

そう!
letはキーワードではないのである!

つまり以下のように書くのは問題ありません。

function let() {}

let();

ただし、

11.6.2.1 Keywords

let and static are treated as reserved keywords through static semantic restrictions (see 12.1.1, 13.3.1.1, 13.7.5.1, and 14.5.1) rather than the lexical grammar.

13.3.1.1 Static Semantics: Early Errors

It is a Syntax Error if the BoundNames of BindingList contains "let".

つまり、constやletで宣言する変数名としてletを使うことはできません。
頑張れば「仕様通りに」実装できますが、頑張る意味はあまりないのでJOKEではletは単純にキーワードにしました。

代入に関する仕様

代入の「左辺」に相当するLeftHandSideExpressionですが、BNFを単純に解釈するといろいろな「式」を含むことになってしまいます。

NewExpression :
    MemberExpression
    ※Memberとあるがこれ以下にただの識別子参照もある

CallExpression :
    MemberExpression Arguments

LeftHandSideExpression :
    NewExpression
    CallExpression

このBNFだけ見ると「え?これもOK?」となりますがもちろん駄目です。3

foo() = 123;

代入演算子には単純な「BNF的にはOK」に加えて以下の仕様が記載されています。

12.14.1 Static Semantics: Early Errors

It is an early Reference Error if LeftHandSideExpression is neither an ObjectLiteral nor an ArrayLiteral and IsValidSimpleAssignmentTarget of LeftHandSideExpression is false.

IsValidSimpleAssignmentTargetってなんじゃいということについてはこの単語で検索かければわかりますが普通に考えればわかるように「ただの変数参照」の場合はtrueで他はfalseです。これについては純粋な構文解析の時点で「ただの変数参照か」はわかるのでその時点でエラーにしました。

なお上記のように仕様では「Reference Error」となっていますがブラウザやNode.jsではSyntaxErrorになるし4、どちらかというと「文法的におかしい」面の方が強いのでJOKEでもSyntaxErrorにしました。

言語仕様以外の改造

以下では言語仕様以外の改造、すなわち開発していくうえでやりやすいようにしたという機能について説明します。

テスト方法の整備

ステップ1の段階で回帰テスト的な仕組みは作ったのですが、「例外起こらなければOK(言語的に変な改造を入れてしまったら例外が起こるだろう)」というものでした。
もちろん例外が起こらなければよいというだけでは駄目で、「想定する出力なのか(まだ演算はできませんが1+1がちゃんと2か等)」を調べる必要があります。これを実装しました。仕組みは以下のようになります。5

  1. ファイルへの出力を行うWriteStreamを作る
  2. 1.で作ったWriteStreamを使ってConsoleオブジェクトを作る
  3. 作ったConsoleオブジェクトでconsoleを差し替える(ここがミソ)
  4. JokeEngineに入力プログラムを与える(プログラムを実行する)。するとプログラムの出力(console.logしたもの)はファイルに出力される
  5. WriteStreamを閉じる
  6. あらかじめ用意されている「想定する出力」と同じかチェックする

具体的にはtest.jsのrunAndVerify関数参照なのですが、「WriteStreamが閉じたことはコールバック(やPromise)ではなくイベントで伝えられる」という点が厄介でした。最終的に「Promiseを素で使う」ことで実現しました。ここasync関数では実現できません。「Promise≠async関数」というのは最近どこかで見た気がするのですが、単純に置き換えはできないという事例に遭遇できて有益でした(やってることがややこしいということもありますが)

なお、「こういう場合はこういうエラーになるべき」というテストは(条件分岐等が)複雑になるので今のところ作っていません。手動でステップのプログラムを書き換えて「うん、OK(思ってる通りのエラーになった)」と確認はしていますが、エラーケースを考え出すときりがない(変数だけでもたくさん思いつくがそれ全部テストに書く?書くべきなんだろうけど)ということもありためらっています。

メインプログラム

開発記その1でも「字句解析段階でダンプするとこうですよー」というのをお見せしていますが、JOKEプログラムを書き換えて出力コピペしたら元に戻す、ではなくちゃんとオプションで指定できるようにしました。

PS C:\work\joke> node --experimental-modules .\joke.js -d .\step\step0001.js   
(node:16800) ExperimentalWarning: The ESM module loader is experimental.
(node:16800) ExperimentalWarning: Package name self resolution is an experimental feature. This feature could change at any time
Scanner result:
  [
    { type: 'IDENTIFIER', identifier: 'console', lineno: 5 },
    (中略)
    { type: 'END', lineno: 6 }
  ]

Parser result:
  {
    type: 'STATEMENTS',
    statements: [
      {
        type: 'EXPRESSION_STATEMENT',
        expression: {
          type: 'CALL',
          target: {
            type: 'MEMBER',
            object: { type: 'IDENTIFIER_REFERENCE', identifier: 'console' },
            property: 'log'
          },
          arguments: [ { type: 'STRING', string: 'Hello' } ]
        }
      }
    ]
  }

Assembler result:
  [
    { command: 'PUSH', operand: 'console' },
    (中略)
    { command: 'POP' }
  ]

Hello

この改造を入れる際に問題だったのが字句解析です。
ステップ1段階では「Parserからnext呼ばれたときに都度」解析していましたがそうするとダンプコードが散らばってしまうので「初めにトークン列にしてしまい、オプションが指定されてたらトークン列をダンプ」するようにしました。nextが呼ばれた場合は単純にトークン列中の「現在のトークン」を指す添え字を変えるだけです。

現時点の残課題

VMスタック

今はVM(スタックマシン)のスタックとして「JavaScriptの配列」を素で使っており、「pushしすぎ(全命令コードを実行した後に残り物がないか)」はチェックしていますが、「popしすぎ」はチェックできていません(空配列をpopしても例外等は起きません。何かバグってたらpopしすぎも起こるはずです)
popしてるところ全部に「popしすぎ」チェックを入れるのは現実的ではないのでVmStackみたいな薄いラッパーオブジェクトを作りpopメソッド呼び出し時にチェックしようかなと考えています。

例外表示

SyntaxError等のエラーは今は素の(JOKEを実行している)JavaScript(処理系)の例外を投げています。この際問題になるのは

  • JOKE実装のエラーなのか(JOKEを動かすJavaScript処理系が投げた例外なのか)
  • 「JOKEが動かしているJavaScriptプログラム」のエラーなのか

が区別できていないことです。joke.jsではどちらもcatchしてしまっているので「あっ例外起きた。想定するプログラムのエラーかな…、これはJOKEのバグやん」ということが多々ありました。これも直したいと思います(JOKE自身が例外処理を実装するのはまだ先なので、安直な解決策としては例外メッセージに[JOKE]と入れるぐらいしか思いつかないのですが)

今後の予定

以上、JOKEステップ2として変数機能と言語仕様以外の改造について説明してきました。
次回実装するもの、ベタに考えると演算ですが変数を実装した理由としては関数の引数の仕組みを作りたかったということがあります。
というわけで一足飛びに「関数作るぜ!」と思ったのですが、「でも関数呼び出してconsole.logするだけじゃ意味ないな」とも思ったのでやはり演算を実装し、満を持して関数呼び出しを作ることになるかなと思います。条件分岐やループはその後の予定です。


  1. リテラルはまだ文字列しかサポートしてません。 

  2. 今の構文解析はBNFに対応する「関数」を定義しScannerだけを引数で渡していますが(戻り値は解析結果のノード)、「今定義されている変数」などの「解析状況」を渡す必要が出てくる、あるいは状態を保持しないといけなくなったから関数を諦めてメソッドに書き直すといった複雑化が予想されます。 

  3. 参照を返せるC++ならいけそう。 

  4. Chrome使ってるのでよく思うと両方V8? 

  5. テストシステムについてはセルフホスティングの対象外にしているのでNode.jsの機能をばんばん使っています。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シンメトリーなfacesの THREE.KusudamaGeometry 作ってみた

THREE.SphereGeometryのFacesが非対称で気になった

ふと球体の頂点を引っ張りあげてトゲトゲにしてみたいと思い、標準の球体のジオメトリーであるSphereGeometryをワイヤー表示させてみた。
SphereGeometry.png
解像度をあげて使う分にはこれで問題ないのだろうが、荒くすると各面(Face)の分割が左右非対称なのが気になって仕方がない。

そこで球体のGeometryを自作してみることに。

頂点座標の作成

まずはTHREE.SphereGeometryの引数と互換性があるように、垂直分割、水平分割で球体を分割した頂点を作るテストプログラムを作成しました。

let radius = 20 //半径
let rows = 10 //水平分割
let columns = 10 //垂直分割

let vertices = [] //頂点座標のArray

// 上の頂点
let top = new THREE.Vector3(0, radius, 0)

// 下の頂点
let bottom = new THREE.Vector3(0, -radius, 0)

vertices.push(top)

for (let j = 1; j < rows; j++) {

    // 高さ
    let y = Math.cos((j / rows) * Math.PI) * radius

    for (let i = 0; i <= columns; i++) {

        // 横軸
        let x = Math.cos((i / columns) * Math.PI * 2) * radius

        // 奥行き
        let z = Math.sin((i / columns) * Math.PI * 2) * radius

        // Y軸で回転させる
        x *= Math.sin((j / rows) * Math.PI)
        z *= Math.sin((j / rows) * Math.PI)

        // できた頂点座標
        let cord = new THREE.Vector3(x, y, z)

        vertices.push(cord)

    }

}

vertices.push(bottom)

数学はあまり得意ではないので計算した座標とTHREE.SphereGeometryを比べてみて「なんでそうなるー!?」と頭を抱えているところ。

試行錯誤の末やっとなんとかうまく合致させることに成功。
Vertices.png

クラスの作成

できたプログラムを元にTHREE.Geometryクラスを親に持つクラスを作成します。

まずは頂点座標を入れるところ。

THREE.TestGeometry.prototype = Object.create(THREE.Geometry.prototype)

THREE.TestGeometry = function (radius, columns, rows) {
    THREE.Geometry.call(this);

    this.vertices = [];
    let indexes = [];

    // create vertex

    let top = new THREE.Vector3(0, radius, 0)
    let bottom = new THREE.Vector3(0, -radius, 0)

    indexes[0] = [this.vertices.push(top) - 1]

    for (let j = 1; j < rows; j++) {

        indexes[j] = []

        let d = Math.sin((j / rows) * Math.PI)

        let y = Math.cos((j / rows) * Math.PI) * radius

        for (let i = 0; i <= columns; i++) {

            let x = Math.cos((i / columns) * Math.PI * 2) * radius * d

            let z = Math.sin((i / columns) * Math.PI * 2) * radius * d

            let cord = new THREE.Vector3(x, y, z)

            indexes[j].push(this.vertices.push(cord) - 1)

        }

    }

    indexes[rows] = [this.vertices.push(bottom) - 1]

    this.center();
    this.mergeVertices();
}
THREE.TestGeometry.prototype.constructor = THREE.TestGeometry

このままでは面情報(faces)が無いのでエラーとなりますので、ここに今回の目的である交互分割の面情報を入れていきます。

THREE.TestGeometry = function (radius, columns, rows) {
    THREE.Geometry.call(this);

    this.vertices = [];
    let indexes = [];

    // create vertices

    //~~~~~~~中略~~~~~~~

    this.faces = []
    let white = new THREE.Color(1, 1, 1)

    // 範囲チェック付きで配列の中身を取得する関数
    let get = (o, a, b) => {
        return b < o[a].length ? o[a][b] : o[a][o[a].length - 1]
    }

    // 重複チェック付きで push する関数
    let push = (o, a) => {
        return o.indexOf(a) >= 0 ? o.indexOf(a) + 1 : o.push(a)
    }

    // 面を作成
    for (let j = 0; j < rows; j++) {

        for (let i = 0; i < columns; i++) {

            //4個または3個の頂点の配列インデックスを取得
            let v = []
            push(v, get(indexes, j + 0, i + 0))
            push(v, get(indexes, j + 0, i + 1))
            push(v, get(indexes, j + 1, i + 1))
            push(v, get(indexes, j + 1, i + 0))

            //頂点が4個
            if (v.length == 4) {

                let f = Math.floor((j + i) % 2) //垂直+水平の剰余で分割を交互させる
                // 0_____1 0_____1
                // |\    | |    /|
                // |  \  | |  /  |
                // |____\| |/____|
                // 3     2 3     2

                this.faces.push(
                    new THREE.Face3(v[0], v[1], v[2 + f], THREE.vertexNormals, white, 0),
                    new THREE.Face3(v[0 + f], v[2], v[3], THREE.vertexNormals, white, 0)
                )

            //球体の上下の頂点ならば3個
            } else {

                this.faces.push(
                    new THREE.Face3(v[2], v[0], v[1], THREE.vertexNormals, white, 0)
                )

            }

        }

    }

    this.center();
    this.mergeVertices();
    this.computeFaceNormals()
}

表示結果:
KusudamaGeometry.png

やったぞ!これぞ求めていた分割!!!

UV座標の追加

これでも使えることは使えますが、ちゃんと行儀よくUV座標の情報も入れておきます。

THREE.TestGeometry = function (radius, columns, rows) {
    THREE.Geometry.call(this);

    this.vertices = [];
    let indexes = [];

    // create vertices

    //~~~~~~~中略~~~~~~~

    // UV座標の計算(X軸を反転させているのは横の面(face)設定が時計回りの為、面が右から左へ向かって追加されている為)
    let uvs = []
    for (let j = 0; j < rows; j++) {

        uvs[j] = []

        for (let i = 0; i < columns; i++) {

            if (j == 0) {
                //上面のテクスチャ用
                uvs[j][i] = [{
                    x: 1 - (i + 0.5) / columns,
                    y: 1 - j / rows
                },
                {
                    x: 1 - i / columns,
                    y: 1 - (j + 1) / rows
                },
                {
                    x: 1 - (i + 1) / columns,
                    y: 1 - (j + 1) / rows
                },
                ]

            } else if (j == rows - 1) {
                //下面のテクスチャ用
                uvs[j][i] = [{
                    x: 1 - i / columns,
                    y: 1 - j / rows
                },
                {
                    x: 1 - (i + 1) / columns,
                    y: 1 - j / rows
                },
                {
                    x: 1 - (i + 0.5) / columns,
                    y: 1 - (j + 1) / rows
                },
                ]

            } else {
                //側面のテクスチャ用
                uvs[j][i] = [{
                    x: 1 - i / columns,
                    y: 1 - j / rows
                },
                {
                    x: 1 - (i + 1) / columns,
                    y: 1 - j / rows
                },
                {
                    x: 1 - (i + 1) / columns,
                    y: 1 - (j + 1) / rows
                },
                {
                    x: 1 - i / columns,
                    y: 1 - (j + 1) / rows
                },
                ]

            }
        }
    }

    // create face

    this.faces = []
    let white = new THREE.Color(1, 1, 1)

    let get = (o, a, b) => {
        return b < o[a].length ? o[a][b] : o[a][o[a].length - 1]
    }

    let push = (o, a) => {
        return o.indexOf(a) >= 0 ? o.indexOf(a) + 1 : o.push(a)
    }

    for (let j = 0; j < rows; j++) {

        for (let i = 0; i < columns; i++) {

            let v = []
            push(v, get(indexes, j + 0, i + 0))
            push(v, get(indexes, j + 0, i + 1))
            push(v, get(indexes, j + 1, i + 1))
            push(v, get(indexes, j + 1, i + 0))

            let p = uvs[j][i]

            if (v.length > 3) {

                let f = Math.floor((j + i) % 2)

                this.faces.push(
                    new THREE.Face3(v[0], v[1], v[2 + f], THREE.vertexNormals, white, 0),
                    new THREE.Face3(v[0 + f], v[2], v[3], THREE.vertexNormals, white, 0)
                )

                // UV座標の追加
                this.faceVertexUvs[0].push(
                    [new THREE.Vector2(p[0].x, p[0].y), new THREE.Vector2(p[1].x, p[1].y), new THREE.Vector2(p[2 + f].x, p[2 + f].y)],
                    [new THREE.Vector2(p[f].x, p[f].y), new THREE.Vector2(p[2].x, p[2].y), new THREE.Vector2(p[3].x, p[3].y), ],
                )

            } else {

                this.faces.push(
                    new THREE.Face3(v[2], v[0], v[1], THREE.vertexNormals, white, 0)
                )


                // UV座標の追加(上下でちゃんと向きが変わるようにする)
                let f = j == 0 ? 0: 1

                this.faceVertexUvs[0].push(
                    [new THREE.Vector2(p[1 + f].x, p[1 + f].y), new THREE.Vector2(p[0].x, p[0].y), new THREE.Vector2(p[2 - f].x, p[2 - f].y)]
                )

            }

        }

    }

    this.center()
    this.mergeVertices()
    this.computeFaceNormals()

}

テクスチャの貼り付けテスト。これで完成です!

TextureTest.png

実際のクラスと動作はCodePenにアップロードしました。
CodePen - THREE.KusudamaGeometry


See the Pen
THREE.KusudamaGeometry
by Urushibara (@pneuma01)
on CodePen.


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

moment.jsで2つの日時の差分を計算する。(kintone編)

2つの日時の差分を計算する

最近kintoneのカスタマイズしかしてない

最近、kintoneのカスタマイズをjsでゴリゴリ書く仕事しかしてない感じがするのですが、日時差分を取って、所定の時間を超えていたら入力欄の背景を赤くしろ、とか、黄色くしろ、とかいう話が複数あって、その度に考えていたので備忘録としてメモ。

前提条件

kintoneでアプリをカスタマイズする際にはmoment.jsを読み込んでおくのを忘れないように。

例えば

kintoneでタイムカードアプリを作ったとしてですね…

タイムカード.png

開始、終了、にそれぞれユーザ打刻してもらって、両方に値が入った状態で「保存」すると、「計算」欄に両者の差分時間が入る、なんてことをやりたい場合です。

ソース例

sample.js
    // レコード保存時休憩時間計算
    kintone.events.on(["app.record.edit.submit"], function(event){
        var record = event.record;

        // 休憩開始と終了が入っていれば計算する
        if(record['休憩開始']['value'] !== undefined && record['休憩終了']['value'] !== undefined){
            const timeFrom = moment(record['休憩開始']['value']);
            const timeTo = moment(record['休憩終了']['value']);
            let result = timeTo.diff(timeFrom);
            record['休憩時間計算']['value'] = moment.utc(result).format('HH:mm');
        }
        return event;
    });

保存時アクション

最初の

sample.js
    // レコード保存時休憩時間計算
    kintone.events.on(["app.record.edit.submit"], function(event){
    });

というのは、編集から、保存実行時の場合、というkintoneのお作法的コードですが、editなので、新規作成時はこのプログラムは動作しません。新規作成、編集時、問わず、動作させたい場合はこんな感じ。

sample.js
    // レコード保存時休憩時間計算
    kintone.events.on(["app.record.create.submit","app.record.edit.submit"], function(event){
    });

計算

sample.js
        // 休憩開始と終了が入っていれば計算する
        if(record['休憩開始']['value'] !== undefined && record['休憩終了']['value'] !== undefined){
            const timeFrom = moment(record['休憩開始']['value']);
            const timeTo = moment(record['休憩終了']['value']);
            let result = timeTo.diff(timeFrom);
            record['休憩時間計算']['value'] = moment.utc(result).format('HH:mm');
        }

「休憩開始」と「休憩終了」に値があることを確認した上で、それぞれconst定義に詰め込んで、diffを取っているだけですね。この場合は、休憩時間の計算なので、1日跨ぎとかは仕様としてあり得ませんので、フォーマットは「HH:mm」にしてあります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

electron-vue でNeDBを使用する

のじみんです。

今回はelectron-vueでNeDBを使用する方法について
インストールからDB設定するところまでを説明します

※ 公式ドキュメントと同じ内容なのでそちらをまず参考にしてください。↓
electron-vue - ローカルファイルの読み書き

実行環境とバージョン一覧

  • macOS Mojave v10.14.6
  • npm v6.9.0
  • node v10.16.3
  • electron v1.4.13

Installing

まずはNeDBをインストールしましょう

npmの場合
$ npm install --save nedb

もしくは

yarnの場合
$ yarn add nedb

Settings

rendererディレクトリの配下にdatastore.jsというファイルを作成する

$ cd <YOUR_PROJECRT>
$ touch ./src/renderer/datastore.js

datastore.jsの中身は以下のようにする

./src/renderer/datastore.js
import path from 'path';
import { remote } from 'electron';
import Datastore from 'nedb';

const dbPath = path.join(remote.app.getPath('userData'), '/data.db');

// DB初期化
export default new Datastore({
  autoload: true,
  filename: dbPath,
});

上記ではelectronのアプリケーションディレクトリのdata.dbファイルを読み込む処理をしています。
data.dbがない場合は上の処理でファイルが作成されるのでエラーがでることはありません。

main.jsに以下の処理を追記する

./src/renderer/main.js
...

import db from './datastore';
Vue.prototype.$db = db;

...

以上でどのコンポーネントからもDBを呼び出すことができるようになりました。やったね!

usage

このままだとdata.dbの中身は空なのでデータを引っ張ってくることができません。
なのであらかじめ適当なデータを突っ込んでから
electron-vueでdbからデータを取得していきましょう

データを挿入する

まず適当にdata.dbにデータを突っ込んでみましょう。

main.jsに以下の文を追記します。

./src/renderer/main.js
...

import db from './datastore';

Vue.prototype.$db = db;
// 今回追加する処理
db.find({}, (err, doc) => {
  const data = [
    { name: '山田太郎', age: 20 },
    { name: 'のじ先生' age: 10 },
  ];

  // 初回のみ:dbのデータが空だった場合、テーブルの構築をする
  if (doc.length === 0) {
    db.insert(docs);
  }
});

...

一度アプリをビルドする

$ yarn run dev

ここまで終われば、無事data.dbにデータを入れることができました。
今度はelectorn-vueでそのデータを取得してみましょう。

electron-vueでdbからデータを取得する

適当なコンポーネントのscript部分に以下を記述する
※ 以下ではApp.vueで記述していると想定して書いています。

./src/renderer/App.vue
<script>
this.$db.find({}, (err, doc) => {
  console.log(doc);
});
</script>

はい。以上でおしまいです。
あとはビルドしてみてコンソール出力に格納されたデータが表示されている確認してみてください。

$ yarn run dev
アプリケーションのコンソールから出力されているか確認

ほんでは

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでドット絵作成ツールを作ってみた

JavaScriptでドット絵作成ツールを作ってみた

JavaScriptでドット絵作成ツールを作ってみました。作ってみた感想としては電卓の方が難しかった...
意外と少ないステップで完成します。

動画の埋め込み方がわからなかったため、ツイートを埋め込みます。
今回は主に本参考のため制作物はリンクしていません。下記動画のようになりました。
ツイートのため少々お見苦しいかもしれませんが...ご興味がございましたらぜひ。

作成手順

作成手順としては下記のようになります。

  • 本の通りに作る
  • マスを自由に作れるように改造する
  • カラーピッカーを使用する

マスを自由に作る

マスはテーブルで作りました。
HTMLにテキストを入力できるinputタグを使用し、そこからマス数を取得することで任意のマス数のテーブルを作成できるようにしました。
今思えば、縦と横は普通にheightとwidthで良かったなとこの記事を書いていて気付きました。

function dotTable() {
  let dotRow = Number(document.getElementById("vertical").value);
  let dotCell = Number(document.getElementById("side").value);
  for (let i = 0; i <= dotRow - 1; i++) {
    let row = dot.insertRow(-1);
    for (let j = 0; j <= dotCell - 1; j++) {
      let cell = row.insertCell(-1);
      cell.onclick = function () {
        this.style.backgroundColor = selectColor;
      }
    }
  }
}

カラーピッカーを使用する

カラーピッカーはコード7区さんの記事を参考にしました。
使ったコードはたったこれだけ。意外と簡単に設置できました。

$(function () {
  $("#color-picker").on("change", function () {
    selectColor = $("#color-picker").val();
  });
});

最後に

今回は主に本を参考にしたため、制作物や全てのソースコードは公開していません。
参考にさせていただいた方々、ありがとうございました。

参考

いちばんやさしいJavaScript入門教室
コード7区

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

v1.0が新しく公開されたDenoを使ってみる

はじめに

5月13日にTypeScript, JavaScriptのためのランタイムdeno1.0がリリースされました。JavaScript、TypeScriptを最近勉強しているのでどんなものかと触ってみて自分なりに軽くまとめてみました。また、最近Rust(denoはRustで実装されているらしい)も気になり調べていたので興味を持ちました。
公式サイトはこちら

こちらのqiita記事がとても参考になりました。

denoとは?

Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.

DenoはV8エンジンを使用しRustに組み込まれたJavaScriptとTypeScriptを実行するためのシンプルかつモダンで安全なランタイム。
らしいです。

Secure by default. No file, network, or environment access, unless explicitly enabled.

明示的に指定しなければネットワークやファイルにはアクセスできないらしいです。

Denoを使ってみる

まずはDenoをインストールしましょう。
今回はbrewを使います。こちらにいろいろなインストール方法が乗っているのでbrew以外で試したい方はどうぞ

brew install deno

基本的に事前にインストールするものはこれだけ。アンインストールする時もDenoをアンインストールするだけでいいそうです。
パッケージをインストールしないのでこの辺が楽ですね。

早速何か動かすためにターミナルで以下を実行してみましょう。

deno run https://deno.land/std/examples/welcome.ts

http通信をしているところから推測できるようにDenoの場合は事前にnode_modulesフォルダにいろいろとインストールする必要はないようです。しかし、実行時毎回http通信するわけではなく、一回実行すればメモリに保存されます。

これの実行結果は

Welcome to Deno ?

となります。 恐竜?のようなキャラクターがいいかんじです。

Hello World実装

何はともあれHello World

hello.tsを作成します

import { serve } from "https://deno.land/std@v0.50.0/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");

for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

以下コマンドを実行します

deno run --allow-net hello.ts

http://localhost:8000
に接続すると"Hello World"と入力されると思います。

Remote code is fetched and cached on first execution, and never updated until the code is run with the --reload flag. (So, this will still work on an airplane.)

--allow-netが冒頭で言っていた"明示的に指定しなければネットワークやファイルにはアクセスできない"っていうところだと思います。これで明示的に指定している。
--allow-netがなければ、
"error: Uncaught PermissionDenied: network access to "0.0.0.0:8000", run again with the --allow-net flag"
というエラーがでます。

またimport文のところはHTTPサーバーモジュールが依存関係を気にせず、1行追加すれば使えるような状態になっています。node.jsの場合はnode_modulesに依存関係を解決しながら(npmがこの辺はやってくれますが)多数のファイルをインストールしていると思いますが、その必要がないってことだと思います。

htmlのコードを出力するサンプルコード

サンプルコードは公式サイトに記述されているのを元にコードを書いてます。

index.tsを作成

const url = Deno.args[0];
const res = await fetch(url);

const body = new Uint8Array(await res.arrayBuffer());
await Deno.stdout.write(body);

下記コマンドを実行

deno run --allow-net=example.com index.ts https://example.com

結果、ターミナルにhtmlのコードが出力されます。

ファイルを読み取るサンプルコード

任意のテキストファイルをルートに置いておきます。
sample.txt
下記は任意で書き換えていいです
txt
denoサンプルコード

index.tsを作成

for (let i = 0; i < Deno.args.length; i++) {
  let filename = Deno.args[i];
  let file = await Deno.open(filename);
  await Deno.copy(file, Deno.stdout);
  file.close();
}

下記コマンド実行

deno run --allow-read index.ts ./sample.txt

sample.txtに記述したテキストを読み取ってターミナルに表示されているはずです。

デバッグ

VScodeでデバッグをしてみます。

launch.jsonを作成してみます。

の箇所はあとで作成したファイル名(この場ではindex.ts)をいれます。

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Deno",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "deno",
      "runtimeArgs": ["run", "--inspect-brk", "-A", "<entry_point>"],
      "port": 9229
    }
  ]
}

index.tsを作成します。

import { serve } from "https://deno.land/std@v0.50.0/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");

for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

これでデバッグをしてみるとできました。

dinatraを使用したルーティング処理のサンプルコード

Denoのフレームワークでdinatraというものがあるらしいです。名前だけ聞いたらSinatraを意識してるのかな?とおもいました。(実際そうっぽい)
ただ、Denoはnodeのように事前にインストールしないのでimport文にモジュールを追加するだけです。
ほぼインストールしないで使っているのと同じような感覚になります。

こちらを参考にさせていただきました。

index.tsを作成しましょう(まずはgithubからコピペ)

import {
  app,
  get,
  post,
  redirect,
  contentType,
} from "https://denopkg.com/syumai/dinatra@0.12.1/mod.ts";

app(
  get("/hello", () => "hello"),
  get("/hello/:id", ({ params }) => params.id),
  get(
    "/hello/:id/and/:name",
    ({ params }) => `:id is ${params.id}, :name is ${params.name}`,
  ),
  get("/error", () => [500, "an error has occured"]),
  get("/callName", ({ params }) => `Hi, ${params.name}!`),
  post("/callName", ({ params }) => `Hi, ${params.name}!`),
  get("/foo", () => redirect("/hello", 302)), // redirect from /foo to /hello
  get("/info", () => [
    200,
    contentType("json"),
    JSON.stringify({ app: "dinatra", version: "0.0.1" }),
  ]),
);
deno run --allow-net index.ts

上記コマンドを実行すれば "http://0.0.0.0:8080/hello" や "http://0.0.0.0:8080/foo" などにつながると思います。

ここから変更を加えて使っていきます
index.tsを以下のように変更

const { cwd, open, stdout, copy } = Deno;
import {
  app,
  get,
  post,
  redirect,
  contentType,
} from "https://denopkg.com/syumai/dinatra@0.12.1/mod.ts";
import { renderFile } from "https://deno.land/x/dejs/mod.ts";

const currentDir = cwd();
const indexPath = `${currentDir}/index.html`;

app(
  get("/", async () => await open(indexPath)),
  post(
    "/posts",
    async ({ params: { username, password } }) =>
      await renderFile(`${cwd()}/top.ejs`, { username })
  ),
  get(
    "/form",
    async () =>
      await renderFile(`${cwd()}/form.ejs`, { title: "フォーム送信" })
  ),
);

form.ejsを作成

<html>

<head>
  <meta charset="utf-8">
  <title><%= title %></title>
</head>

<body>
  <h1><%= title %></h1>
  <form action="/posts" method="POST">
    <label>ユーザ名</label>
    <input type="text" name="username" placeholder="username" /><br>
    <button>送信</button>
  </form>
</body>

</html>

top.ejsを作成

<html>

<head>
  <meta charset="utf-8">
  <title>フォーム送信成功</title>
</head>

<body>
  <h1>フォーム送信成功</h1>
  <h1><%= username %>さん</h1>
  <h1></h1>
</body>

</html>

以下コマンドを実行します。

deno run --allow-net --allow-read index.ts 

"http://0.0.0.0:8080"に接続すると
この画面が表示されます

Screen Shot 2020-05-21 at 16.06.38.png

ログイン画面ボタンを押すと下記表示が出ます。(当初はログインのような機能を作ろうと思ってたのでその名残が残ってます笑)

Screen Shot 2020-05-21 at 16.07.41.png

上記画面でテキストボックスにテキストをいれるとフォームの送信ができます。

Screen Shot 2020-05-21 at 16.07.48.png

もっといろいろやりたかったですが、とりあえずはこんなとこで終わりにします。

ちなみにDBはこんな感じでつかうらしい

postgress

import { Client } from "https://deno.land/x/postgres/mod.ts";

async function main() {
  const client = new Client({
    user: "user",
    database: "test",
    host: "localhost",
    port: "5432"
  });
  await client.connect();
  const result = await client.query("SELECT * FROM people;");
  console.log(result.rows);
  await client.end();
}

mongoDB

import { init, MongoClient } from "https://deno.land/x/mongo@v0.5.2/mod.ts";

// Initialize the plugin
await init();

const client = new MongoClient();
client.connectWithUri("mongodb://localhost:27017");

const db = getClient().database("test");
const users = db.collection("users");

// insert
const insertId = await users.insertOne({
  username: "user1",
  password: "pass1"
});

// insertMany
const insertIds = await users.insertMany([
  {
    username: "user1",
    password: "pass1"
  },
  {
    username: "user2",
    password: "pass2"
  }
]);

// findOne
const user1 = await users.findOne({ _id: insertId });

// find
const users = await users.find({ username: { $ne: null } });

// count
const count = await users.count({ username: { $ne: null } });

まとめ

npm installでいろいろインストールする必要がないのは楽だなーとおもいました。特に最近ネットが遅いのでありがたい。。
また、TypeScriptをそのまま実行できるので最初のトランスパイルするための開発環境構築はしなくて済んだのがよかったです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初学者がjavascript教えるならどうするか内容を考えてみた!

前提

本記事はプログラミング初学者の筆者が
「プログラミング初学者に対し、JavaScriptのイベントを分かりやすく教えるならどう教えるか」
という目線で書いております。
筆者もプログラミング初学者のため拙いところや詰めが甘い箇所があると思います。
その際はご指摘頂ければ幸いです。

目次

  1. JavaScriptのイベントとは?
  2. ドッキリに例えて考えてみよう!
  3. clickイベントの実例

 1. JavaScriptのイベントとは?

  • JavaScriptにおける「イベント」とはウェブページ上で発生するあらゆるアクションの総称のことです。
    例えば、下記の様な内容がイベントとして挙げられます。
    ・ボタンをクリックすると入力フォームが表示される。
    ・ボタンがクリックされた際にアラートを表示させる。
    ・マウスポインタが重なると色が変わる。
    クリックするとカテゴリーが表示される。

このイベントをする(処理)することをイベントハンドラとはと言います。
ちなみにハンドラとは英語だと「handler」で動詞の「handle(扱う)」から由来しているそうです。
イベントハンドラの一例は下記内容が挙げられます。

イベント名 イベント
ハンドラ名
説明
click onclick 要素がクリックされた時
change onchange 要素の内容が変更された時
keypress onkeypress キーボードのキーを押した時
mouseover onmouseover 要素にマウスのカーソルが重なった時
select onselect 要素がダブルクリックされた時
drag ondrag 要素がドラッグされた時
scroll onscroll スクロールバーがスクロールされた時

2. ドッキリに例えて考えてみよう!

今回は「HTMLにscriptタグを読み込む」「JavaScriptは別のファイルに書く」という前提で記載しております。。

実際にJavaScriptを書くときは以下のステップを踏みます。
なお、今回はドッキリに例えながら記載していきます。
①scriptタグをhtmlに記載する。
イベントを発生させるためにはJavaScriptのコードを読み込む必要があります。
そのため、htmlのファイル内に「JavaScriptのコードを読んでね!」とお願いするコードを記入します。
その際に注意して欲しいのが、head要素に書かず、body要素の最後にコードを書くことです。

プログラミングは基本的にコードは上から順に読み込むので、
head要素にコードを書き読み込むとbody要素とその子孫はまだ取得できないのでエラーになります。

sample.html
<!DOCTYPE html>
<html>
  <head>
    <title>Javascript</title>
    <link rel="stylesheet" href="style.css">
       <!-- head要素の中にscriptコードを書いてみる -->
      <script type="text/javascript" src="test.js"></script>
  </head>
  <body>
    <div id="gold-medal" class="medal"></div>
    <div id="silver-medal" class="medal"></div>
    <div id="bronze-medal" class="medal"></div>
  </body>
</html>

上記コードを例にすると、onclickの結果は「該当要素なし」としてnullになり、
Uncaught TypeError: Cannot set property 'onclick' of null at test.js:12
コンソールに出力されます。しかし、body要素の最後であれば、全てのHTMLを読み込んだ後なので、
情報を取得してイベントを発生させることができます。

②イベントハンドラする対象を決めイベント発生後のアクションを書く。
例えばclickしたらclickしたモノが消えるイベントを発生させることにしましょう。
対象となるモノをイベントの内容を紐付けてコードを書く必要があるので、以下の手順でコードを書いていきます。
(1)「html内に対象となるモノにidやclassを指定する。」
(2)「指定したモノに対しJavaScriptのファイルにイベント発生のコードを書く」

ドッキリに置き換えると

①〜②の動作をドッキリに置き換えると以下の様に例えることができます。

①どこでドッキリをやるのかを決める。
まず、どこでドッキリをするのかを決めます。
なお、早とちりしてドッキリを台無しにしないために最後の方で仕掛けます。

②ターゲットとどんなドッキリを仕掛けるのかを決める。
ターゲットを決めないとドッキリは始まりません。
対象となるターゲットに対しidの指定やclassの指定を行いましょう。
そして、仕掛けるドッキリ(アクションの内容)を仕掛け人(ジャバスクリプト やCSSのファイル)に対して
指示し(コードを書き)ましょう。

3. clickイベントの実例

今回は「金メダル、銀メダル、銅メダルをクリックしたら消える。」というイベントを書きました。
簡易的なモノなのでイメージがしやすいと思います。
詳細を記載したコードを記入したので、実際に挙動を確認していただければ幸いです。

Gyazo gif

sample.html
<!DOCTYPE html>
<html>
  <head>
    <title>Javascript</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div id="gold-medal" class="medal"></div>
    <div id="silver-medal" class="medal"></div>
    <div id="bronze-medal" class="medal"></div>
    <script type="text/javascript" src="test.js"></script>
</body>
</html>
style.css
body {
  font-family: Helvetica, Arial, sans-serif;
  margin: 0;
  padding: 0;
}
#gold-medal {
  background-color: #DBB400;
}
#silver-medal {
  background-color: #C9CACA;
}
#bronze-medal {
  background-color: #C47022;
}
.medal {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  float: left;
  margin: 20px;
}
test.js
//document.getElementById("").onclickでクリックしたターゲットの要素を取得。
//function(){ }でドッキリの内容を記載
//document.getElementById("").style.display = "none";でドッキリの内容が記載されている。

//(1行目)金メダルをクリックするとイベントが発生。 ← (2行目)金メダルのCSS要素にdisplya: none;が追加される。
document.getElementById("gold-medal").onclick = function(){
  document.getElementById("gold-medal").style.display = "none";
  }
//(1行目)銀メダルをクリックするとイベントが発生。 ← (2行目)銀メダルのCSS要素にdisplya: none;が追加される。
  document.getElementById("silver-medal").onclick = function(){
    document.getElementById("silver-medal").style.display = "none";
  }
//(1行目)銅メダルをクリックするとイベントが発生。 ← (2行目)銅メダルのCSS要素にdisplya: none;が追加される。
  document.getElementById("bronze-medal").onclick = function(){
    document.getElementById("bronze-medal").style.display = "none";
  }

引用
teratail(Javascriptで記述したコードでエラーが出てしまう。)
【JavaScript入門】初心者でも分かるイベント処理の作り方まとめ!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「初学者がJavaScriptのイベントを分かりやすく教えるならどう教えるか?」を考えてみた!

前提

本記事はプログラミング初学者の筆者が
「プログラミング初学者に対し、JavaScriptのイベントを分かりやすく教えるならどう教えるか」という目線で書いております。
筆者もプログラミング初学者のため拙いところや詰めが甘い箇所があると思います。
その際はご指摘頂ければ幸いです。

目次

  1. JavaScriptのイベントとは?
  2. ドッキリに例えて考えてみよう!
  3. clickイベントの実例

 1. JavaScriptのイベントとは?

  • JavaScriptにおける「イベント」とはウェブページ上で発生するあらゆるアクションの総称のことです。
    例えば、下記の様な内容がイベントとして挙げられます。
    ・ボタンをクリックすると入力フォームが表示される。
    ・ボタンがクリックされた際にアラートを表示させる。
    ・マウスポインタが重なると色が変わる。
    クリックするとカテゴリーが表示される。

このイベントをする(処理)することをイベントハンドラとはと言います。
ちなみにハンドラとは英語だと「handler」で動詞の「handle(扱う)」から由来しているそうです。
イベントハンドラの一例は下記内容が挙げられます。

イベント名 イベント
ハンドラ名
説明
click onclick 要素がクリックされた時
change onchange 要素の内容が変更された時
keypress onkeypress キーボードのキーを押した時
mouseover onmouseover 要素にマウスのカーソルが重なった時
select onselect 要素がダブルクリックされた時
drag ondrag 要素がドラッグされた時
scroll onscroll スクロールバーがスクロールされた時

2. ドッキリに例えて考えてみよう!

今回は「HTMLにscriptタグを読み込む」「JavaScriptは別のファイルに書く」という前提で記載しております。。

実際にJavaScriptを書くときは以下のステップを踏みます。
なお、今回はドッキリに例えながら記載していきます。
①scriptタグをhtmlに記載する。
イベントを発生させるためにはJavaScriptのコードを読み込む必要があります。
そのため、htmlのファイル内に「JavaScriptのコードを読んでね!」とお願いするコードを記入します。
その際に注意して欲しいのが、head要素に書かず、body要素の最後にコードを書くことです。

プログラミングは基本的にコードは上から順に読み込むので、
head要素にコードを書き読み込むとbody要素とその子孫はまだ取得できないのでエラーになります。

sample.html
<!DOCTYPE html>
<html>
  <head>
    <title>Javascript</title>
    <link rel="stylesheet" href="style.css">
       <!-- head要素の中にscriptコードを書いてみる -->
      <script type="text/javascript" src="test.js"></script>
  </head>
  <body>
    <div id="gold-medal" class="medal"></div>
    <div id="silver-medal" class="medal"></div>
    <div id="bronze-medal" class="medal"></div>
  </body>
</html>

上記コードを例にすると、onclickの結果は「該当要素なし」としてnullになり、
Uncaught TypeError: Cannot set property 'onclick' of null at test.js:12
コンソールに出力されます。しかし、body要素の最後であれば、全てのHTMLを読み込んだ後なので、
情報を取得してイベントを発生させることができます。

②イベントハンドラする対象を決めイベント発生後のアクションを書く。
例えばclickしたらclickしたモノが消えるイベントを発生させることにしましょう。
対象となるモノをイベントの内容を紐付けてコードを書く必要があるので、以下の手順でコードを書いていきます。
(1)「html内に対象となるモノにidやclassを指定する。」
(2)「指定したモノに対しJavaScriptのファイルにイベント発生のコードを書く」

ドッキリに置き換えると

①〜②の動作をドッキリに置き換えると以下の様に例えることができます。

①どこでドッキリをやるのかを決める。
まず、どこでドッキリをするのかを決めます。
なお、早とちりしてドッキリを台無しにしないために最後の方で仕掛けます。

②ターゲットとどんなドッキリを仕掛けるのかを決める。
ターゲットを決めないとドッキリは始まりません。
対象となるターゲットに対しidの指定やclassの指定を行いましょう。
そして、仕掛けるドッキリ(アクションの内容)を仕掛け人(ジャバスクリプト やCSSのファイル)に対して
指示し(コードを書き)ましょう。

3. clickイベントの実例

今回は「金メダル、銀メダル、銅メダルをクリックしたら消える。」というイベントを書きました。
簡易的なモノなのでイメージがしやすいと思います。
詳細を記載したコードを記入したので、実際に挙動を確認していただければ幸いです。

Gyazo gif

sample.html
<!DOCTYPE html>
<html>
  <head>
    <title>Javascript</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <div id="gold-medal" class="medal"></div>
    <div id="silver-medal" class="medal"></div>
    <div id="bronze-medal" class="medal"></div>
    <script type="text/javascript" src="test.js"></script>
</body>
</html>
style.css
body {
  font-family: Helvetica, Arial, sans-serif;
  margin: 0;
  padding: 0;
}
#gold-medal {
  background-color: #DBB400;
}
#silver-medal {
  background-color: #C9CACA;
}
#bronze-medal {
  background-color: #C47022;
}
.medal {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  float: left;
  margin: 20px;
}
test.js
//document.getElementById("").onclickでクリックしたターゲットの要素を取得。
//function(){ }でドッキリの内容を記載
//document.getElementById("").style.display = "none";でドッキリの内容が記載されている。

//(1行目)金メダルをクリックするとイベントが発生。 ← (2行目)金メダルのCSS要素にdisplya: none;が追加される。
document.getElementById("gold-medal").onclick = function(){
  document.getElementById("gold-medal").style.display = "none";
  }
//(1行目)銀メダルをクリックするとイベントが発生。 ← (2行目)銀メダルのCSS要素にdisplya: none;が追加される。
  document.getElementById("silver-medal").onclick = function(){
    document.getElementById("silver-medal").style.display = "none";
  }
//(1行目)銅メダルをクリックするとイベントが発生。 ← (2行目)銅メダルのCSS要素にdisplya: none;が追加される。
  document.getElementById("bronze-medal").onclick = function(){
    document.getElementById("bronze-medal").style.display = "none";
  }

引用
teratail(Javascriptで記述したコードでエラーが出てしまう。)
【JavaScript入門】初心者でも分かるイベント処理の作り方まとめ!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

OpenWeatherMapAPIのアイコンを変える方法

1. OpenWeatherMapのお天気アイコン

OpenWeatherMapには予め気象コードに合わせたアイコンが用意されています。
無料なのにとっても親切!
そんな初期設定のアイコンの一例がこちらです。
スクリーンショット 2019-11-13 15.00.06.pngスクリーンショット 2019-11-13 15.00.02.pngスクリーンショット 2019-11-13 14.59.51.png
なんとも画質も微妙ですし、あまりイケてない気がしますね・・・
変えれるものなら変えたいと思っていじってみたら
とっても簡単にできたので、その方法を記録しておきます。

2. ドキュメントを確認してみると...

OpenWeatherMapのドキュメントを確認してみると、下記アイコンの一覧が確認できます。
実際に表示されるものとは少し違いますが、
ファイル名がわかるのでこちらをいじればどうにかなりそうということが分かります。
https://openweathermap.org/weather-conditions
スクリーンショット 2019-11-13 16.05.08.png

3. 変更方法

weather.js
$.ajax({
        url: "https://api.openweathermap.org/data/2.5/weather",
        dataType: "jsonp",
        data: "q=Tokyo,jp&appid=" + APIKEY + "&lang=ja&units=metric",
        //天気データ呼び出し成功時
        success: function(data) {
          //〜中略〜
          $(".dayWeatherIcon").attr(
            "src",
            "img_tenki/" + data.weather[0].icon + ".png "
          );

他にやり方はあるかと思いますが、今回はこんな感じの手順で実装しました!

1、まず、上記アイコン一覧のファイル名を参考にお天気アイコンの画像を準備し、フォルダに格納
今回はimg_tenkiというフォルダに画像を保存
2、画像を表示させたいimgタグを予め用意しておき、そのsrcを
"img_tenki/" + data.weather[0].icon + ".png "と指定する

4. 完成したのがこちら...

少し良い感じになりました!
スクリーンショット 2019-11-13 16.08.49.png

5. 参考サイト

https://qiita.com/b-wind/items/06e19043a0cd70b10b03

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者に捧げるハンバーガーメニューの作り方

対象読者

・ハンバーガーメニューを作りたい人
・細かく作り方の手順を知りたい人
・自分でオリジナルの見た目を作って応用したい人
・(ちなみに筆者の自己学習のためにも記事を書いているので間違っている箇所があれば是非とも教えて頂きたい)

目次

・HTMLのソース
・CSSのソース
・JavaScript(jQuery)のソース
・ハンバーガーメニュー作成の手順
・まとめ

HTMLのソース

Hamburger.html
<body>
    <div class="container">
        <button class="click-menu-icon">
            <span></span>
            <span></span>
            <span></span>
        </button>
        <nav>
            <ul>
                <li>111</li>
                <li>222</li>
                <li>333</li>
            </ul>
        </nav>
        <div class="overlay"></div>
    </div>
</body>

CSSのソース

style.css
* {
            margin: 0;
            padding: 0;
        }
        .overlay {
            display: block;
            width: 0;
            height: 0;
            background-color: rgba(0,0,0,0.5);
            position: absolute;
            top: 0;
            right: 0;
            z-index: 2;
            opacity: 0;
            transition: opacity 0.5s;
        }
        .overlay.open {
            width: 100%;
            height: 100%;
            opacity: 1;
        }

        .click-menu-icon {
            border: none;
            display: inline-block;
            width: 36px;
            height: 28px;
            vertical-align: middle;
            cursor: pointer;
            position: fixed;
            top: 30px;
            left: 30px;
            z-index: 100;
        }
        .click-menu-icon span {
            display: inline-block;
            box-sizing: border-box;
            position: absolute;
            left: 0;
            width: 100%;
            height: 4px;
            background-color: #000;
            transition: all 0.5s;
        }
        .click-menu-icon.active span {
            background-color: #000;
        }
        .click-menu-icon span:nth-of-type(1) {
            top: 0;
        }
        .click-menu-icon.active span:nth-of-type(1){
            transform: translateY(12px) rotate(-225deg);
        }
        .click-menu-icon span:nth-last-of-type(2) {
            top: 12px;
        }
        .click-menu-icon.active span:nth-of-type(2){
            opacity: 0;
        }
        .click-menu-icon span:nth-of-type(3) {
            bottom: 0;
        }
        .click-menu-icon.active span:nth-of-type(3){
            transform: translateY(-12px) rotate(225deg);
        }

        nav {
            width: 250px;
            height: 100%;
            padding-top: 100px;
            background-color: rgba(16, 69, 153, 0.8);
            position: fixed;
            top: 0;
            left: 0;
            z-index: 10;
            transform: translateX(-250px);
            transition: all 0.5s;
        }
        nav.open {
            transform: translateX(0);
        }
        nav li {
            color: #fff;
            text-align: center;
            padding: 10px 0;
        }

JavaScript(jQuery)のソース

main.js
$('.click-menu-icon').on('click',function(){
            if($(this).hasClass('active')){
                $(this).removeClass('active');
                $('nav').removeClass('open');
                $('.overlay').removeClass('open');
            } else {
                $(this).addClass('active');
                $('nav').addClass('open');
                $('.overlay').addClass('open');
            }
            });
            $('.overlay').on('click',function(){
            if($(this).hasClass('open')){
                $(this).removeClass('open');
                $('.click-menu-icon').removeClass('active');
                $('nav').removeClass('open');      
            }
            });

ハンバーガーメニュー作成の手順

作成手順
<!-- ハンバーガーメニュー作成の手順
*   1:buttonでspan*3を囲う
*   2:nav>ul>liでメニューを作る
*   3:</nav>の下に影をつけるための<div class="overlay">を作る
*   4:・・・・・以下からCSSの記述手順↓・・・・・
*       1.overlay
*           ・blockでwidthとheightを0にする
*           ・色はお好み_rgbaでaを0.5くらいにする(透明度の最大値)
*           ・position:absoluteでtop:0、left/rightはお好みで0
*           ・z-indexで上に来るために2くらいにする
*           ・初期状態はopacity:0にする
*           ・transition: opacity 0.5s;秒数は好み
*           ・overlay.openにwidth/height:100%、opacityを1にして変化させる
*
*       2.button.class名
*           ・buttonのborderを消しておく
*           ・vertical-alignを指定するためにinline-blockにする
*           ・ハンバーガーアイコンのwidth/heightを指定する(お好み)
*           ・vertical-align: middle;で縦に中央揃えする(大きさを変更しても中央揃えになる)
*           ・cursor:pointer;にする(お好み)
*           ・position: fixed;でスクローるしてもついてくるようにする。top/left/rightを指定する(お好みでfixedなしでもok)
*           ・必ず前面に出したいのでz-indexで一番大きい数にする
*       
*       3.button>span
*           ・box-sizing:border-box;でbutton自体の大きさ内でpaddingやborderなどを指定できる
*           ・left:0;width:100%;で左端から100%button内でspanの長さを指定する
*           ・heightはお好み(必ず指定しないと縦が0なので表示されなくなる)
*           ・色をつける(基本は背景に同化せずハッキリとした色にする)
*           ・transitionでクリックした時のspanが動く秒数を指定する
*       
*       4.buttonに.activeが追加された時のspan
*           ・click後の色を指定する
*           ・span:nth-of-type(1)でtop: 0;を指定する
*           ・.active span:nth-of-type(1)でtransform: translateY(お好み) rotate(-お好み);で1つ目の動きをつける
*           ・span:nth-of-type(2)でtop: お好み;で真ん中に表示する
*           ・.active span:nth-of-type(2)でopacity: 0;で表示を消す
*           ・span:nth-of-type(3)でbottom: 0;を指定する
*           ・.active span:nth-of-type(3)でtransform: translateY(-お好み) rotate(お好み);で3つ目の動きをつける
*           ※translateYとrotateは符号違いで数値は同じにする
*
*       5.nav
*           ・navのwidth:お好み;height: 100%;を指定する  
*           ・padding-topでハンバーガーの下に指定する(お好み)
*           ・background-colorを指定する(お好み)
*           ・position: fixed;でスクロールしてもついてくるようにする
*           ・top: 0;left or right: 0;にする
*           ・z-indexでコンテンツよりは上、overlayより上でハンバーガーより下にする
*           ・transform: translateX(-navのwidth分);で初期位置を画面の外にする
*           ・transitionで動きの秒数を指定する(お好み)
*         
*       6.nav.open
*           ・transform: translateX(0);でclickされた時に画面に表示する
*
*       7.nav li
*           ・colorでliの色を指定する(お好み)
*           ・text-align: center;でnav内の中央に寄せる
*           ・padding:お好み 0;で上下の数値だけ指定する
*
*       ・・・・・以上がCSSの記述手順↑・・・・・
*
*   5.・・・・・以下からJavaScript(jQuery)の記述手順↓・・・・・
*
*
           $('.buttonのクラス名').on('click',function(){    @クリックイベント
            if($(this).hasClass('active')){         @もしbuttonがactiveクラスを持っていたら
                $(this).removeClass('active');      @buttonからactiveクラスを外す
                $('nav').removeClass('open');       @navからopenクラスを外す
                $('.overlay').removeClass('open');  @overlayからopenクラスを外す
            } else {                                @activeクラスを持っていなかったら
                $(this).addClass('active');         @buttonにactiveクラスを追加する
                $('nav').addClass('open');          @navにopenクラスを追加する
                $('.overlay').addClass('open');     @overlayにopenクラスを追加する
            }
            });         @@ overlayをクリックしても開いたメニューを閉じれるようにする @@
            $('.overlay').on('click',function(){                @クリックイベント
            if($(this).hasClass('open')){                   @もしoverlayがopenクラスを持っていたら
                $(this).removeClass('open');                @overlayからopenクラスを外す
                $('buttonのクラス名').removeClass('active');  @buttonからactiveを外す
                $('nav').removeClass('open');               @navからopenクラスを外す
            }
            });
*
*
*
*
*
*
-->

まとめ

自分のコーディング技術向上のためにコードや書き方の手順を記してみました。
実はプログラミング学習を始めてまだ2.3ヶ月の初心者です。なのでもっと効率よく、尚且つ綺麗に記述できると思います。
ですが、過去の自分はただコードだけ見せられてもどう書けばいいか分からなかった経験があります。なので日本語で手順も記載することによって中の値だけ変えれば誰でもとりあえず実装できるようになると思います。
作成の手順通りに作って中の値だけ帰ることによってオリジナルのハンバーガーメニューに応用できると思います。
過去の自分が知りたかったような記事でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【LINE/LIFF入門】LIFFでLINE公式アカウント(旧LINE@)上に予約フォームを作成する(GitHub Pages使用)

はじめに

【LINE/LIFF入門】LIFFでLINE公式アカウント(旧LINE@)上に予約フォームを作成する(Go&Heroku使用)
上記の記事が元です。

本稿はより簡単にLIFFを試せるように、GitHub Pagesを使って実装した手順を改めてまとめた記事です。

こんな感じの作ります

mojikyo45_640-2.gif

使用技術

  • LIFF (LINE Front-end Framework)
  • JavaScript, jQuery
  • GitHub Pages
    • GitHubにプッシュした静的サイトを公開できる機能

ソースコードはこちら(GitHub)

LINE公式アカウント作成

こちらの公式HPを参考に作成します。
line_official.PNG

リッチメニュー設定

リッチメニュー用の画像を作成する

チャット画面下に表示されるバナーのことをリッチメニューというようです。
まずはリッチメニューに設定する画像をCanovaを使って作成します。
Canovaは素材が豊富で無料プランでもいい感じの画像が作れるので便利です。
canova.PNG

リッチメニューを設定する

LINE Offical Account Manager上からいい感じに設定します。(適当)
PCからのみ設定することができ、スマホからは設定できないようです。
リンクは一旦適当なリンクを設定しておきます。
LIFFアプリを作成後にアプリのURLを設定します。

rich1.PNG
rich2.PNG

LIFFアプリ作成

チャネル作成

こちらの公式HPを参考にして、LINE Developersでチャネルを作成します。
LIFFを使うために基本的な情報を登録します。
line_dev.PNG

コーディング

ディレクトリ構成

3ファイルしかないので全部同一ディレクトリにおいちゃいます。

liff-reservation-githubpages/
|- index.html
    -フォームを表示するhtml
|- index.js
    -フォームの制御やliff.jsで定義した関数を呼び出すjs
|- liff.js
    -LIFFのSDKを呼び出すjs

ソースコードは冒頭のGitHubを参照ください。

簡単にLIFF部分だけピックアップしておきます。
初期化してメッセージ送信するだけなのでめっちゃ簡単です!

liff.js
$(document).ready(function () {
    // liffId: LIFF URL "https://liff.line.me/xxx"のxxxに該当する箇所
    // LINE DevelopersのLIFF画面より確認可能
    var liffId = "(自分のLIFFIDを入力)";
    initializeLiff(liffId);
})

function initializeLiff(liffId) {
    liff
        .init({
            liffId: liffId
        })
        .then(() => {
            // Webブラウザからアクセスされた場合は、LINEにログインする
            if (!liff.isInClient() && !liff.isLoggedIn()) {
                window.alert("LINEアカウントにログインしてください。");
                liff.login({redirectUri: location.href});
            }
        })
        .catch((err) => {
            console.log('LIFF Initialization failed ', err);
        });
}

// LINEトーク画面上でメッセージ送信
function sendMessages(text) {
    liff.sendMessages([{
        'type': 'text',
        'text': text
    }]).then(function () {
        liff.closeWindow();
    }).catch(function (error) {
        window.alert('Failed to send message ' + error);
    });
}

GitHubにプッシュして公開

ソースコードをGitHubにプッシュしてGithub Pagesを使って公開します。
以下の記事が参考になります。

GitHub Pages を使った静的サイトの公開方法が、とても簡単になっていた

LIFFアプリをチャネルに追加

こちらの公式HPを参考にして、LIFFアプリがLINEやブラウザ上で動作できるようにするために、LIFFアプリをチャネルに追加します。
エンドポイントURLにはGitHub PagesのURLを設定します。

動作確認

LINE Offical Account ManagerのリッチメニューのリンクをLIFFの設定画面に表示されているLIFF URLをに変更します。
liffurl.PNG

LINE公式アカウントを自分のLINEの友だちに追加して試してみます。
冒頭のデモ画像のようになれば成功です。

LIFFアプリをブラウザ上で動作させたい場合

作成したLIFFアプリをLINE上だけではなく、ブラウザ上で動作させたい場合はShareTargetPickerという機能を使うと実現可能です。

こちらの記事を参考にしてみてください。
【LINE】既存LIFFアプリにShareTargetPickerを導入する

おまけ

ローカルのサーバで動作確認をするにはngrokがとても便利です。
参考リンクを貼っておきます。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm publishせずにパッケージに同梱されるファイルを《ツリー形式で》確認する方法

npm publishせずに、.npmignoreや、.gitignore、そしてpackage.jsonのfilesフィールドの設定がどのように作用しているかを確認する方法として、以前『npm publishせずにパッケージに同梱されるファイルを確認する方法 - Qiita』という記事を投稿しました。

この記事ではnpm pack --dry-runで確認する方法を紹介しましたが、表示されるファイルの順序がばらばらで、ファイル構造が複雑になると読みにくい出力になっていました。

読みやすさを考えると、treeコマンドのようなツリー形式で見たいものです。

ツリー形式で表示するワンライナー

そこで、いい感じにツリー形式でパッケージの中身を一覧するワンライナーを考えました:

PACKAGE=$(npm pack 2> /dev/null) sh -c 'set -eu && tar tf $PACKAGE | tree --fromfile && rm $PACKAGE'

実行例:

.
└── package
    ├── CHANGELOG.md
    ├── LICENSE
    ├── index.d.ts
    ├── index.d.ts.map
    ├── index.js
    ├── index.ts
    └── package.json

1 directory, 7 files

package.jsonにスクリプトとして追加しておくと便利

package.json
{
  "scripts": {
    "pack:preview": "PACKAGE=$(npm pack 2> /dev/null) bash -c 'set -eu && tar tf $PACKAGE | tree --fromfile && rm $PACKAGE'",
  },
}
yarn pack:preview
# or
npm run pack:preview
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ReactNative】react-native-mapsで地図の表示領域が描画される度に領域情報を取得する

概要

先日【ReactNative】react-native-mapsを導入するという記事を書いて、react-native-mapsを導入しましたが、今回はちょっとした応用編です。

最もオーソドックスな使い方としては、下記の実装例のようにMapに初期表示位置としてinitialRegionを指定し、その周辺に表示したい情報をMarkerでプロットしていくのがオーソドックスな使い方だと思います。

TestMap.tsx
import React from 'react';
import MapView, { Region, Marker, LatLng } from 'react-native-maps';

render(){

    // 初期表示位置
    const initialRegion : Region = {
        latitude : 35.689521,
        longitude : 139.691704,
        latitudeDelta : 0.0460,
        longitudeDelta : 0.0260,
    }

    // マーカーの表示位置
    const coordinate : LatLng = {
        latitude : 35.689421,
        longitude :139.691604
    }

    <MapView initialRegion={initialRegion} >
        <Marker coordinate={coordinate} />
    </MapView>
}

しかしながら、プロットする情報が膨大な数の場合は表示領域から近いものに絞って表示するかと思います。
そうなると必要になってくるのが、ユーザ操作によって表示領域が変更された時に、新たに描画される表示領域情報を取得する処理です。
今回は、onRegionChangeCompleteを使った方法を紹介します。

バージョン情報

"react-native": "0.61.5",
"react-native-maps": "^0.26.1",
"native-base": "^2.13.8",

onRegionChangeComplete()を使って表示領域を取得する

onRegionChangeCompleteは、ユーザ操作(もしくはそれ以外の処理)でMapviewの表示領域が変更されるたびに呼ばれます。
下記のようにしてRegion型で再表示後の表示領域情報を受け取ります。

TestMap.tsx
import React from 'react';
import MapView, { Region, Marker, LatLng } from 'react-native-maps';

// 表示領域変更イベントを検知
handleRegionChange = (region : Region) => {
  console.log(region);
  // -> {"latitude": xxx, "latitudeDelta": xxx, "longitude": xxx, "longitudeDelta": xxx}
};

render(){

    // 初期表示位置
    const initialRegion : Region = {
        latitude : 35.689521,
        longitude : 139.691704,
        latitudeDelta : 0.0460,
        longitudeDelta : 0.0260,
    }

    // マーカーの表示位置
    const coordinate : LatLng = {
        latitude : 35.689421,
        longitude :139.691604
    }

    <MapView initialRegion={initialRegion} 
             onRegionChangeComplete={handleRegionChange} >
        <Marker coordinate={coordinate} />
    </MapView>
}

おまけ:getMapBoundaries()でNorthEast,SouthWestを取得

基本的にRegionで事足りるかと思いますが、表示領域の北東(NorthEast)と南西(SouthWest)を取得するバージョンも記載します。
上記のコードに対してMapViewrefを保持しておき、表示領域が切り替わったタイミングでgetMapBoundaries()を実行します。
getMapBoundaries()は非同期ですので注意が必要です。

TestMap.tsx
import React from 'react';
import MapView, { Region, Marker, LatLng } from 'react-native-maps';

// 表示領域変更イベントを検知
handleRegionChange = async (region : Region) => {
  console.log(region);
  // -> {"latitude": xxx, "latitudeDelta": xxx, "longitude": xxx, "longitudeDelta": xxx}
  const mapBoundaries = await this.map.getMapBoundaries();
  console.log(mapBoundaries);
  // -> {"northEast": {"latitude": xxx, "longitude": xxx}, "southWest": {"latitude": xxx, "longitude": xxx}}
};

render(){

    // 初期表示位置
    const initialRegion : Region = {
        latitude : 35.689521,
        longitude : 139.691704,
        latitudeDelta : 0.0460,
        longitudeDelta : 0.0260,
    }

    // マーカーの表示位置
    const coordinate : LatLng = {
        latitude : 35.689421,
        longitude :139.691604
    }

    <MapView initialRegion={initialRegion} 
             ref={ref => {this.map = ref}}
             onRegionChangeComplete={handleRegionChange} >
        <Marker coordinate={coordinate} />
    </MapView>
}

おわりに

MapViewの表示領域が変わるたびに、その表示範囲に近いデータを都度取得してプロットするのはよくあるパターンだと思います。
覚えてしまえばなんてことない方法ですが、備忘として残しておきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クリックした際に、「ⅴ」の向きを変更する方法

<section>
      <h1 id="one">&nbsp; メニュー1</h1>
       <ul class="menu" id="open-menu">
             <li>中身1</li>
             <li>中身2</li>
             <li>中身3</li>
        </ul>
    </section>

ulなどは、関係ない要素だが、日常で使う場合はアコーディオンメニューなどで
使うことが多いと予想されるため記載

続いてCSS

/* 記号> */
section > h1::before {
  content: '';
  width: 4px;
  height: 4px;
  border: 0;
  border-right: 2px solid black;
  border-bottom: 2px solid black;
  transform: rotate(45deg); 
  position: absolute;
  margin-top: 15px;
}
/* クリックした際に上向きにする */
.up::before {
  content: '';
  width: 4px;
  height: 4px;
  border: 0;
  border-right: 2px solid black;
  border-bottom: 2px solid black;
  transform: rotate(-135deg); 
  position: absolute;
  margin-top: 15px;
}

まず、疑似要素で>の記号を作る。
その後、jsでクリックした際に、section > h1::beforeに.up::beforeが
つくようなCSSを用意する。

const clickone = document.getElementById('one');
clickone.addEventListener('click', ()=>{
  clickone.classList.toggle('up');
});

まず、h1要素を取得して、そのh1(clickone)をクリックした場合に
.up::beforeがCSSにつくようにする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新型コロナの自宅待機中に、ビデオチャット上で遊べるアプリを作った話

ビデオチャット上で遊べるアプリを何人かに手伝ってもらいながら3週間で作りました。自分でSFUサーバ立てたりと、思っていたより力作になったので、得られた知見やノウハウを共有したいと思います。

自己紹介

普段はpythonを用いてデータ分析やサービス設計を研究している、東京大学の大学院生です。最近新型コロナの影響でリモート授業になり、家にいる時間を活用してWebアプリを作っています。

作ったサービス

「オンライン遊び場サービス wh.im

要はビデオチャット上で色々ゲーム等を遊べるアプリです。早押しクイズとかワードウルフとか遊べます。オンライン飲み会とかで活躍すると思います。ちなみにサービス名は最近流行りのurlそのままパターンを採用してみました。

開発の背景

新型コロナで外出抑制が続き、人とのコミュニケーションが不足してます。特に自分みたいな一人暮らしの大学生とかは2,3日しゃべらないとかもザラにあり、なんとか友達と前みたいに遊べないかと考えていました。最近はzoom飲みとかも流行っていますが、それもだんだん飽きてきたのは自分だけではないはず。

オンラインでどうコミュニケーションするか

まずは、どうしたらオンラインのコミュニケーションが良くなるかを考えました。オンラインではオフラインコミュニケーションに比べ色々な制約があります。画面の部分しか映らない、声が聞き取りづらい、遅延がある、複数人で喋ったらもう聞き取れない、通信量使う、、、。それを踏まえ、オンラインで上手いことコミュニケーションするには、オンラインの良さを生かさないといけません。

オンラインの強み

オンラインでは、オフラインと違い目の前に超高性能計算機・パソコンがあります。これはオフラインにはない特徴です。オンラインではコミュニケーションの最中、常にパソコンを触っているのです。となると、パソコンでゲームしながらコミュニケーションしたらいいのではないのだろうか。こうして私はビデオチャット&ゲームが同時にできる、「オンライン遊び場サービス wh.im」を開発に着手しました。

サービスとして工夫したこと

サービスを設計にあたって、次の3つのことを意識しました。
- ビデオチャットとゲームの分離
- 会員登録は一切なし、遊び場を作るボタンを押すだけ
- 導線は極力シンプルに

ビデオチャットとゲームの分離

wh.im上に色々なアプリを作りたかったので、ビデオチャット部分とゲーム部分が分離された作りにしようと思いました。vueのコンポーネントで分ける等色々考えましたが、最終的にiframeでゲーム部分を上から載せるのが、シンプルで良いのではないかと考えました。以下イメージ図です。

会員登録は一切なし、遊び場を作るボタンを押すだけ

会員登録は一切ありません。urlを発行するだけです。ビデオチャットが始めやすく、誘いやすいように設計しました。招待を受けた側もurlをクリックするだけで wh.im を始めることができます。

導線は極力シンプルに

ボタン等の導線を極力減らし、サービス上でできることを制限しました。ここはzoom等のビジネスチャットとは違う部分で、ミュート、画面off、画面共有といった機能は省き、友達と会話できる、ゲームできるというコアな部分を重視しました。

開発に使った技術

主に以下の技術を使いました。
- mediasoup(webRTC)
- firebase(BaaS)
- Nuxt.js(JavaScriptフレームワーク)

webRTC

一番苦労しました。日本語の文献も少ないです。

今回一番ネックであったのはログイン不要という部分です。ログイン不要というのは、ユーザーにとって便利な反面、サービス開始の敷居が低くて意図しないサービスの使われ方や、ユーザー数制限といった利用量を抑えるのは難しいのではないか、という懸念がありました。結果として利用が増えても金銭的な負担が発生しにくい方法を考える必要がありました。

webRTCを実装する上で最も簡単なのはNTTのやっているSkyWayだと思います。日本語の記事も比較的多いです。ただ一方、無料枠には500GBの通信量制限があり、有料プランは月額基本費用として10万円かかるということだったので、諦めました。
他にも有名どころですと、agora.io, opentok, twilio, AWS Chimeなどがあります。これらは大体料金は従量制で相場は1人1分0.5円といったところです。4人で一時間使うと120円ほどになります。個人で利用する分にはそれほど高くはありませんが、サービスリリースとなるとどれぐらいの利用料金になるか未知数なので今回は採用しませんでした。

結局無償で使用できるOSSを使い実装することにし、その中でもmediasoupを選びました。

webRTCでの技術選択に関しては時雨堂さんの記事がすごい参考になりました。フローチャート形式で、自分のやるべきことがわかります。感謝。

webRTC OSS 個人的比較

webRTCのOSSで有名どころをStarHistoryで比較してみました。

人気が最もあるjanus-gatewayか、後発で順調にスターが伸びてるmediasoupかで悩みましたが、以下の理由でmediasoupに決めました。(webRTCを触るのは初めてだったのでここらへんはフィーリングです。)
- mediasoupはNode.jsのライブラリになっており、とっつきやすい。janus-gatewayはC言語。
- janusはmeetechoという会社が作っており、meetechoがjanusの導入サポートっぽいことを行っているので、逆にjanusをサポートなしで導入するのは難しいのではないかという邪な推測
- ドキュメントがかっこいい
- 作者がイケメン

Firebase

バックエンドとして使ってます。神サービス。感謝。
FirebaseからはAuthentication, Firestore, Strage, Hostingを使っています。(メインはFirestoreです。)
今回は複数のユーザーが通信することが前提にあるので、firestoreの変更があればクライアント側に通知するリアルタイムアップデート機能が大助かりしました。

Nuxt.js

実は今回始めてNuxt.jsを使ってみました(今まではVue.js)。サーバーサイドレンダリングを使わなかったらVueCLIでもいいかなというのが正直な感想です。ちなみにデザインやcssは苦手なのでVuetifyを使いました。Vuetifyはすごくいいライブラリなのですが、一度使い出したらなかなか抜けれないという沼のようなライブラリでもあります(個人の感想です)。

ゲーム部分の作り込み

ゲーム部分はVue.jsで作りました。友達の協力も得ながら、じゃんけん、早押しゲーム、ワードウルフなどを作ってみました。
ビデオチャット部分とゲーム部分は別々に動いているので、そのwindow間でプレイヤー情報等を送信するためにWebの仕様であるpostMessageを用いました(このAPIは今回初めてしりました)。

オンライン通信ゲームなので、ユーザー間でデータを同期しなければなりません。データ同期用にfirestoreを使うことを考えましたが、ゲームを作るたびにfirebaseでproject立ち上げて、、とやるのは些か面倒でした。ビデオチャット部分ではすでにfirebaseを使っていたので、上記と同様postMessageを活用することで、データ同期をビデオチャット経由で簡単に行えるようにしました。ここらへんの詳細はこちらにまとめました。

作ってみたゲーム

  • じゃんけん
  • ワードウルフ
  • 早押しクイズ
  • NGワードゲーム
  • YesNoゲーム
  • ジェスチャーゲーム

さらに、wh.im では、ゲーム部分を外に切り出したことで、ビデオチャットの部分を実装しなくても、簡単にオリジナルなゲームを作ることができます。フロントの技術をある程度持っていれば、自分のやりたいゲームを開発可能です。例えば...
- 麻雀
- サッカー
- おおぎり
- その他オリジナルゲーム

などなど...!
簡単に作れるように、アプリ開発のためのドキュメントをまとめたので、是非参考にしてください。

まとめ

アイデアの着想したときの見積もりより壮大になり合計3週間ほどかかりました。しかしビデオチャットとゲームを分離することで、設計がシンプルになり、ゲームの開発に集中することができました。 wh.im を使うと、簡単にチャットビデオ上で動くゲームを作れるので、是非使ってみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新型コロナの自宅待機が寂しかったので、ビデオチャットしながらゲームで遊べるサービスを作った話

ビデオチャット上で遊べるアプリを何人かに手伝ってもらいながら3週間で作りました。自分でSFUサーバ立てたりと、思っていたより力作になったので、得られた知見やノウハウを共有したいと思います。

自己紹介

普段はpythonを用いてデータ分析やサービス設計を研究している、東京大学の大学院生です。最近新型コロナの影響でリモート授業になり、家にいる時間を活用してWebアプリを作っています。

作ったサービス

「オンライン遊び場サービス wh.im

要はビデオチャット上で色々なゲームで遊べるアプリです。早押しクイズとかワードウルフとか遊べます。オンライン飲み会とかで活躍すると思います。ちなみにサービス名は最近流行りのurlそのままパターンを採用してみました。lineとかslackに投稿するとそのままurlとして認識してくれます。ウィム!

開発の背景

新型コロナで外出抑制が続き、人とのコミュニケーションが不足してます。特に自分みたいな一人暮らしの大学生とかは2,3日人としゃべらないとかもザラにあり、なんとか友達と前みたいに遊べないかと考えていました。最近はzoom飲みとかも流行っていますが、それもだんだん飽きてきたのは自分だけではないはず。

オンラインでどうコミュニケーションするか

まずは、どうしたらオンラインのコミュニケーションが良くなるかを考えました。オンラインではオフラインコミュニケーションに比べ色々な制約があります。画面の部分しか映らない、声が聞き取りづらい、遅延がある、複数人で喋ったらもう聞き取れない、通信量使う、、、。しかし、オンラインにはオンラインの良さがあります。オンラインでしか遊べないなら、そのオンラインの良さを生かさないといけません。

オンラインの強み

オンラインでは、オフラインと違い目の前に超高性能計算機・パソコンがあります。これはオフラインにはない特徴です。オンラインではコミュニケーションの最中、常にパソコンを触っているのです。となると、パソコンでゲームしながらコミュニケーションしたらいいのではないのだろうか。こうして私はビデオチャット&ゲームが同時にできる、「オンライン遊び場サービス wh.im」の開発に着手しました。

サービスとして工夫したこと

サービスを設計にあたって、次の3つのことを意識しました。
- ビデオチャットとゲームの分離
- 会員登録は一切なし、遊び場を作るボタンを押すだけ
- 導線は極力シンプルに

ビデオチャットとゲームの分離

wh.im上に色々なアプリを作りたかったので、ビデオチャット部分とゲーム部分が分離された作りにしようと思いました。vueのコンポーネントで分ける等色々考えましたが、最終的にiframeでゲーム部分を上から載せるのが、シンプルで良いのではないかと考えました。以下イメージ図です。

会員登録は一切なし、遊び場を作るボタンを押すだけ

会員登録は一切ありません。urlを発行するだけです。ビデオチャットが始めやすく、誘いやすいように設計しました。招待を受けた側もurlをクリックするだけで wh.im を始めることができます。

導線は極力シンプルに

ボタン等の導線を極力減らし、サービス上でできることを制限しました。ここはzoom等のビジネスチャットとは違う部分で、ミュート、画面off、画面共有といった機能は省き、友達と会話できる、ゲームできるというコアな部分を重視しました。

開発に使った技術

主に以下の技術を使いました。
- mediasoup(webRTC)
- firebase(BaaS)
- Nuxt.js(JavaScriptフレームワーク)

webRTC

一番苦労しました。日本語の文献も少ないです。
今回一番ネックであったのはログイン不要とい形の実現です。ログイン不要というのは、ユーザーにとって便利な反面サービス開始の敷居が低く、意図しないサービスの使われ方や、ユーザー数制限といった利用量を抑えるのが難しくなってしまうのではないか、という懸念がありました。結果として利用が増えても金銭的な負担が発生しにくい方法を考える必要がありました。
webRTCをマネージドのサーバを使って実装しようとするとある程度のお金が必要になります。webRTCを実装する上で最も簡単なのはNTTのやっているSkyWayだと思います。SkyWayは日本語の記事も比較的多い一方、無料枠には500GBの通信量制限があり、有料プランは月額基本費用として10万円かかるということだったので、諦めました。
他にも有名どころですと、agora.io, opentok, twilio, AWS Chimeなどがあります。これらは大体料金は従量制で相場は1人1分0.5円といったところです。4人で一時間使うと120円ほどになります。個人で利用する分にはそれほど高くはありませんが、サービスリリースとなるとどれぐらいの利用料金になるか未知数なので今回は採用しませんでした。
結局無償で使用できるOSSを使い実装することにし、その中でもmediasoupを選びました。
webRTCでの技術選択に関しては時雨堂さんの記事がすごい参考になりました。フローチャート形式で、自分のやるべきことがわかります。感謝。

webRTC OSS 個人的比較

webRTCのOSSで有名どころをStarHistoryで比較してみました。

人気が最もあるjanus-gatewayか、後発で順調にスターが伸びてるmediasoupかで悩みましたが、以下の理由でmediasoupに決めました。(webRTCを触るのは初めてだったのでここらへんはフィーリングです。)
- mediasoupはNode.jsのライブラリになっており、とっつきやすい。janus-gatewayはC言語。
- janusはmeetechoという会社が作っており、meetechoがjanusの導入サポートっぽいことを行っているので、逆にjanusをサポートなしで導入するのは難しいのではないかという邪な推測
- ドキュメントがかっこいい
- 作者がイケメン

Firebase

バックエンドとして使ってます。神サービス。感謝。
FirebaseからはAuthentication, Firestore, Strage, Hostingを使っています。(メインはFirestoreです。)
今回は複数のユーザーが通信することが前提にあるので、firestoreの変更があればクライアント側に通知するリアルタイムアップデート機能が大助かりしました。

Nuxt.js

実は今回始めてNuxt.jsを使ってみました(今まではVue.js)。サーバーサイドレンダリングを使わなかったらVueCLIでもいいかなというのが正直な感想です。ちなみにデザインやcssは苦手なのでVuetifyを使いました。Vuetifyはすごくいいライブラリなのですが、一度使い出したらなかなか抜けれないという沼のようなライブラリでもあります(個人の感想です)。

ゲーム部分の作り込み

ゲーム部分はVue.jsで作りました。友達の協力も得ながら、じゃんけん、早押しゲーム、ワードウルフなどを作ってみました。
ビデオチャット部分とゲーム部分は別々に動いているので、そのwindow間でプレイヤー情報等を送信するためにWebの仕様であるpostMessageを用いました(このAPIは今回初めて知りました)。
オンライン通信ゲームなので、ユーザー間でデータを同期しなければなりません。データ同期用にfirestoreを使うことを考えましたが、ゲームを作るたびにfirebaseでproject立ち上げて、、とやるのは些か面倒でした。ビデオチャット部分ではすでにfirebaseを使っていたので、上記と同様postMessageを活用することで、データ同期をビデオチャット経由で簡単に行えるようにしました。ここらへんの詳細はこちらにまとめました。

作ってみたゲーム

  • じゃんけん
  • ワードウルフ
  • 早押しクイズ
  • NGワードゲーム
  • YesNoゲーム
  • ジェスチャーゲーム

いい感じのサムネを友達がいらすとやで作ってくれました。いらすとやすごい。感謝。

さらに、wh.im では、ゲーム部分を外に切り出したことで、ビデオチャットの部分を実装しなくても、簡単にオリジナルなゲームを作ることができます。フロントの技術をある程度持っていれば、自分のやりたいゲームを開発可能です。例えば...
- 麻雀
- サッカー
- おおぎり
- その他オリジナルゲーム
などなど...!
簡単に作れるように、アプリ開発のためのドキュメントをまとめたので、是非参考にしてください。開発からwh.im上での公開までに必要な情報をまとめています。

まとめ

アイデアの着想したときの見積もりより壮大になり合計3週間ほどかかりました。しかしビデオチャットとゲームを分離することで、設計がシンプルになり、ゲームの開発に集中することができました。 wh.im を使うと、簡単にチャットビデオ上で動くゲームを作れるので、是非使ってみてください。アイデアの着想したときの見積もりより壮大になり合計3週間ほどかかりました。しかしビデオチャットとゲームを分離することで、設計がシンプルになり、ゲームの開発に集中することができました。 wh.im を使うと、簡単にチャットビデオ上で動くゲームを作れるので、是非使ってみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新型コロナの自宅待機中に、ビデオチャットしながらゲームで遊べるサービスを作った話

ビデオチャット上で遊べるアプリを何人かに手伝ってもらいながら3週間で作りました。自分でSFUサーバ立てたりと、思っていたより力作になったので、得られた知見やノウハウを共有したいと思います。

自己紹介

普段はPythonを用いてデータ分析やサービス設計を研究している、東京大学の大学院生です。最近新型コロナの影響でリモート授業になり、家にいる時間を活用してWebアプリを作っています。

作ったサービス

「オンライン遊び場サービス wh.im

要はビデオチャット上で色々なゲームで遊べるアプリです。早押しクイズとかワードウルフとか遊べます。オンライン飲み会とかで活躍すると思います。ちなみにサービス名は最近流行りのurlそのままパターンを採用してみました。lineとかslackに投稿するとそのままurlとして認識してくれます。ウィム!

開発の背景

新型コロナで外出抑制が続き、人とのコミュニケーションが不足してます。特に自分みたいな一人暮らしの大学生とかは2,3日人としゃべらないとかもザラにあり、なんとか友達と前みたいに遊べないかと考えていました。最近はzoom飲みとかも流行っていますが、それもだんだん飽きてきたのは自分だけではないはず。

オンラインでどうコミュニケーションするか

まずは、どうしたらオンラインのコミュニケーションが良くなるかを考えました。オンラインではオフラインコミュニケーションに比べ色々な制約があります。画面の部分しか映らない、声が聞き取りづらい、遅延がある、複数人で喋ったらもう聞き取れない、通信量使う、、、。しかし、オンラインにはオンラインの良さがあります。オンラインでしか遊べないなら、そのオンラインの良さを生かさないといけません。

オンラインの強み

オンラインでは、オフラインと違い目の前に超高性能計算機・パソコンがあります。これはオフラインにはない特徴です。オンラインではコミュニケーションの最中、常にパソコンを触っているのです。となると、パソコンでゲームしながらコミュニケーションしたらいいのではないのだろうか。こうして私はビデオチャット&ゲームが同時にできる、「オンライン遊び場サービス wh.im」の開発に着手しました。

サービスとして工夫したこと

サービスを設計にあたって、次の3つのことを意識しました。
- ビデオチャットとゲームの分離
- 会員登録は一切なし、遊び場を作るボタンを押すだけ
- 導線は極力シンプルに

ビデオチャットとゲームの分離

wh.im上に色々なアプリを作りたかったので、ビデオチャット部分とゲーム部分が分離された作りにしようと思いました。vueのコンポーネントで分ける等色々考えましたが、最終的にiframeでゲーム部分を上から載せるのが、シンプルで良いのではないかと考えました。以下イメージ図です。

会員登録は一切なし、遊び場を作るボタンを押すだけ

会員登録は一切ありません。urlを発行するだけです。ビデオチャットが始めやすく、誘いやすいように設計しました。招待を受けた側もurlをクリックするだけで wh.im を始めることができます。

導線は極力シンプルに

ボタン等の導線を極力減らし、サービス上でできることを制限しました。ここはzoom等のビジネスチャットとは違う部分で、ミュート、画面off、画面共有といった機能は省き、友達と会話できる、ゲームできるというコアな部分を重視しました。

開発に使った技術

主に以下の技術を使いました。
- mediasoup(webRTC)
- firebase(BaaS)
- Nuxt.js(JavaScriptフレームワーク)

webRTC

一番苦労しました。日本語の文献も少ないです。
今回一番ネックであったのはログイン不要とい形の実現です。ログイン不要というのは、ユーザーにとって便利な反面サービス開始の敷居が低く、意図しないサービスの使われ方や、ユーザー数制限といった利用量を抑えるのが難しくなってしまうのではないか、という懸念がありました。結果として利用が増えても金銭的な負担が発生しにくい方法を考える必要がありました。
webRTCをマネージドのサーバを使って実装しようとするとある程度のお金が必要になります。webRTCを実装する上で最も簡単なのはNTTのやっているSkyWayだと思います。SkyWayは日本語の記事も比較的多い一方、無料枠には500GBの通信量制限があり、有料プランは月額基本費用として10万円かかるということだったので、諦めました。
他にも有名どころですと、agora.io, opentok, twilio, AWS Chimeなどがあります。これらは大体料金は従量制で相場は1人1分0.5円といったところです。4人で一時間使うと120円ほどになります。個人で利用する分にはそれほど高くはありませんが、サービスリリースとなるとどれぐらいの利用料金になるか未知数なので今回は採用しませんでした。
結局無償で使用できるOSSを使い実装することにし、その中でもmediasoupを選びました。
webRTCでの技術選択に関しては時雨堂さんの記事がすごい参考になりました。フローチャート形式で、自分のやるべきことがわかります。感謝。

webRTC OSS 個人的比較

webRTCのOSSで有名どころをStarHistoryで比較してみました。

人気が最もあるjanus-gatewayか、後発で順調にスターが伸びてるmediasoupかで悩みましたが、以下の理由でmediasoupに決めました。(webRTCを触るのは初めてだったのでここらへんはフィーリングです。)
- mediasoupはNode.jsのライブラリになっており、とっつきやすい。janus-gatewayはC言語。
- janusはmeetechoという会社が作っており、meetechoがjanusの導入サポートっぽいことを行っているので、逆にjanusをサポートなしで導入するのは難しいのではないかという邪な推測
- ドキュメントがかっこいい
- 作者がイケメン

Firebase

バックエンドとして使ってます。神サービス。感謝。
FirebaseからはAuthentication, Firestore, Strage, Hostingを使っています。(メインはFirestoreです。)
今回は複数のユーザーが通信することが前提にあるので、firestoreの変更があればクライアント側に通知するリアルタイムアップデート機能が大助かりしました。

Nuxt.js

実は今回始めてNuxt.jsを使ってみました(今まではVue.js)。サーバーサイドレンダリングを使わなかったらVueCLIでもいいかなというのが正直な感想です。ちなみにデザインやcssは苦手なのでVuetifyを使いました。Vuetifyはすごくいいライブラリなのですが、一度使い出したらなかなか抜けれないという沼のようなライブラリでもあります(個人の感想です)。

ゲーム部分の作り込み

ゲーム部分はVue.jsで作りました。友達の協力も得ながら、じゃんけん、早押しゲーム、ワードウルフなどを作ってみました。
ビデオチャット部分とゲーム部分は別々に動いているので、そのwindow間でプレイヤー情報等を送信するためにWebの仕様であるpostMessageを用いました(このAPIは今回初めて知りました)。
オンライン通信ゲームなので、ユーザー間でデータを同期しなければなりません。データ同期用にfirestoreを使うことを考えましたが、ゲームを作るたびにfirebaseでproject立ち上げて、、とやるのは些か面倒でした。ビデオチャット部分ではすでにfirebaseを使っていたので、上記と同様postMessageを活用することで、データ同期をビデオチャット経由で簡単に行えるようにしました。ここらへんの詳細はこちらにまとめました。

作ってみたゲーム

  • じゃんけん
  • ワードウルフ
  • 早押しクイズ
  • NGワードゲーム
  • YesNoゲーム
  • ジェスチャーゲーム

いい感じのサムネを友達がいらすとやで作ってくれました。いらすとやすごい。感謝。

さらに、wh.im では、ゲーム部分を外に切り出したことで、ビデオチャットの部分を実装しなくても、簡単にオリジナルなゲームを作ることができます。フロントの技術をある程度持っていれば、自分のやりたいゲームを開発可能です。例えば...
- 麻雀
- サッカー
- おおぎり
- その他オリジナルゲーム
などなど...!
簡単に作れるように、アプリ開発のためのドキュメントをまとめたので、是非参考にしてください。開発からwh.im上での公開までに必要な情報をまとめています。

まとめ

アイデアの着想したときの見積もりより壮大になり合計3週間ほどかかりました。しかしビデオチャットとゲームを分離することで、設計がシンプルになり、ゲームの開発に集中することができました。 wh.im を使うと、簡単にチャットビデオ上で動くゲームを作れるので、是非使ってみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webカメラを使って、1000px ×1000px のプロフィール画像を量産してくれるサイトをつくった

完成品

サイト名:1000×1000 証明写真
https://id-photo.ml (ダメだったらこっち:https://id-photo.herokuapp.com/)

作り方!!

・まずファイル構造はこんな感じ
スクリーンショット 2020-05-21 0.03.32.png
今回herokuを使ってデプロイするために、gitやら何やらが入っています。(もしかしたら、必要ないファイルもあるかも。すみません...)


・コード記述内容

index.html
<!DOCTYPE html>
<html>

<head>
  <title>1000×1000証明写真</title>
</head>

<body>
  <b>1000×1000証明写真</b>
  <div id="app">
    <div>
      <video ref="video" id="video" width="500" height="500" autoplay></video>
      <div>
        <button color="info" id="snap" v-on:click="capture()"><img src="img/icon.png"></button>
      </div>
      <canvas ref="canvas" id="canvas" width="1000" height="1000"></canvas>
      <ul>
      <li class="capture" v-for="c in captures" v-bind:key="c.d">
        <img v-bind:src="c" height="50" />
      </li>
      </ul>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        video: {},
        canvas: {},
        captures: []
      },
      mounted () {
        this.video = this.$refs.video
        if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
          navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
            this.video.srcObject = stream
            this.video.play()
          })
        }
      },
      methods: {
        capture () {
          this.canvas = this.$refs.canvas
          this.canvas.getContext('2d').drawImage(this.video, 0, 0, 1000, 1000)
          this.captures.push(this.canvas.toDataURL('image/png'))
          console.log(this.captures)
        }
      }
    });
  </script>

</body>
</html>

<style>
#canvas {
  display: none;
}
.capture {
  /* display: inline; */
  padding: 5px;
}
</style>
index.js
const express = require('express');
const app = express();

// public というフォルダに入れられた静的ファイルはそのまま表示
app.use(express.static(__dirname + '/public'));

// bodyParser
var bodyParser = require('body-parser');
app.use(bodyParser.json());

// POSTリクエストを受け付ける
app.post('/post', function (req, res) {

    for (key in req.body) {
        console.log(key, '=', req.body[key]);
    }
    res.end();

    res.send('hello world(POST)');
});

// GET リクエストを受け付ける
app.get('/get', function (req, res) {
    res.send('hello world(GET)');
});

//app.listen(8080);
app.listen(process.env.PORT || 8080);

console.log("server start! (heroku)");
.gitignore
# Dependency directories
node_modules/

# Optional npm cache directory
.npm
Procfile
web: node index.js

そのほかのファイルは下記コマンドからインストールできます。

npm init -y
npm i body-parser express

完成

触って遊んでみましょう。

step1「サイトへ飛ぶ」
https://id-photo.ml (ダメだったらこっち:https://id-photo.herokuapp.com/)

1.jpg


step2「カメラアイコンをクリックして撮影」
カメラのアイコンをクリックすると、1000px × 1000px の画像が下に量産されていきます。
2.jpg


step3「画像をダウンロード」
量産された画像の上で、右クリックをするとメニューがでるので、そこで「名前を付けて画像を保存...」を選択してください。
3.jpg

ダウンロードされた画像が、少し縦に伸びている感じになっているのはスリム効果です。

おわり

一応、今回使用した画像を貼っておきます。ご自由に使用してください。
icon.png

また今回参考にさせていただいた記事です
https://qiita.com/kino15/items/8f8feffca54015555f4b

最後まで読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プチ・クラウドストレージ作ってみた

初めまして

60歳を間近にウェブデザイナーを目指して独学で勉強しているお婆ちゃんです。

去年の暮れからphpを勉強して、初めてシステムらしきものを作ってみました。

やりたいこと
プチ・クラウドストレージ
・ファイルをどこからでもアップ、保管してダウンロードもできる。
・セキュリティも兼ねてIDとパスワードでログイン形式にする。

まずはパワーポイントでサイトの系図を設計しました。
Excel、Word、パワポは商工会議所で習いたてホヤホヤです。
それぞれ基礎編までクリアして1月半くらいかかりました。
全部で5万円くらいはかかったかな。

siteroot.jpg

次は手順を考えてイメージを具体化。
これは無料版のAdobeのXDを使ってみました。

操作も簡単で、感覚的に作れちゃうので便利です。
ページ自体はシンプルにしたかったのでフォントだけで作りました。
コードを書くのもAdobeの無料Brackets。Adobeドップリ。


ログインページ

AdobeXD イメージ図
Top.jpg
パスワードとIDはここで決めてます。
空文字NGの条件も設定。

login.php
//XSS
function html_escape($word){
    return htmlspecialchars($word,ENT_QUOTES,'UTF-8');
}
$logid = '';
$pass = '';
$messege = '';

if($_SERVER['REQUEST_METHOD'] === 'POST'){
    //isset入れると空文字条件が効かない
    $logid = $_POST['logid'];
    $pass = $_POST['pass'];
    $logid = html_escape($logid);
    $pass = html_escape($pass);

    //IDとパスワード設定
    if($logid === 'keserasera' && $pass === 'keserasera'){
        session_start();
        $_SESSION['login'] = 1;
        //ファイル一覧へリロード
        header('Location: file_list.php');
        exit();
    } elseif ($logid === '' || $pass === ''){
        $messege = '<p class="notice"><i class="fas fa-info-circle"></i>IDとパスワードを空文字にしないで入力してください</p>';
    } else {
        $messege = '<p class="notice"><i class="fas fa-info-circle"></i>IDかパスワード、もしくは両方違います</p>';
    }
}

?>
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>ファイル預かり処・マイ保管庫</title>
   <!-- headerインクルード -->
    <?php require_once(dirname(__FILE__).'/header.php'); ?>
<div id="wrapper">
    <header>
       <div id="titleVar">
          <h1><i class="fas fa-circle"></i>ファイル預かり処 マイ保管庫</h1>
          <p class="tx14">ログインページ</p>
       </div>       
       <div id="topvew">My keep folder</div>             
    </header>
    <main id="topMain">
      <form action="" method="post" class="clearfix">
         <label>ログインID</label>
         <input type="text" name="logid">
         <label>パスワード</label>
         <input type="password" name="pass">
         <p id="logBtn"><input type="submit" value="ログイン"></p>
         <?php echo $messege; ?>
      </form>      
    </main>
    <!-- footerインクルード -->
    <?php require_once(dirname(__FILE__).'/footer.php'); ?>

ファイルリスト一覧ページ

AdobeXD イメージ図
file_list.jpg

アップロードとダウンロード、削除ファイルの3つのformがあります。

  • アップロードは同一ページで処理
  • ダウンロードはチェックページに飛ばしてリロード処理
  • 削除ファイルは確認ページを別に作ってpostデータを渡す

ちなみにダウンロードと削除ファイル一覧リストはタブ切替。
フォルダーの中身は一緒でアップすると自動でリストが増えて、削除すると減っていきます。

file_list.php
//ログインしていないとアクセスさせない
session_start();
session_regenerate_id(true);
if(isset($_SESSION['login']) === false){
    header('Location: un_login.php');
    exit();
}

function html_escape($word){
        return htmlspecialchars($word,ENT_QUOTES,'UTF-8');
    }
$up_file = '';
$messege = '';
$select_file = '<p id="take">ファイルを選択して下さい</p>';
$restore = '';
$up_before = 'upBefore';//非表示css
$filename = '';

if($_SERVER['REQUEST_METHOD'] === 'POST'){
    $up_file = $_FILES['file_up'];
    $up_file['name'] = html_escape($up_file['name']);
    $up_file['name'] = strtolower($up_file['name']);//英小文字に変換  
    //var_dump($up_file['name']);

    $extension = Pathinfo($up_file['name'],PATHINFO_EXTENSION);//.以降の拡張子を重複しないよう整形
    $filename = Pathinfo($up_file['name'],PATHINFO_FILENAME);
    $filename = str_replace('.', '', $filename);//ファイル名に.があったら除去

    //ファイルがNGの場合条件処理
    if($up_file['size'] > 30000000 ){
        $messege = '<p id="caution" class="notice"><i class="fas fa-info-circle"></i>ファイルサイズが容量を超えています</p>';
        $up_before = 'upBefore';
        $restore = '<a id="restore" href="file_list.php">こちらからUPし直してください</a>';
        $select_file = '';
        //phpよりエラー表示を出すときファイル選択ボタンを出さないで再読み込みさせる為、空文字設定に
    } else {
        $messege = '';
        $up_before = 'upAfter';
        //fileをアップする関数
        move_uploaded_file($up_file['tmp_name'], './up_file/'.$filename.'.'.$extension);
    }
 }

   //upフォルダの中身
    $dw_items =  glob('./up_file/*');//DL用、同じでも変数変えないとエラーになる
    $del_items = glob('./up_file/*');//削除用、grobは配列形式でファイルパスを取得
?>
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>ファイル一覧 | ファイル預かり処・マイ保管庫</title>  
<!-- headerインクルード -->
<?php require_once(dirname(__FILE__).'/header.php'); ?>
<div id="wrapper">
    <header>
       <div id="titleVar">
          <h1><i class="fas fa-circle"></i>ファイル預かり処 マイ保管庫</h1>
           <p id="logout"><a href="logout.php">ログアウト</a></p>
       </div>       
       <div id="topvew">My keep folder</div>             
    </header>
    <main id="main">
        <h2 class="pageTitle">ファイルアップロード</h2>
     <!-- ファイルup -->
        <form id="upBox" action="" method="post" enctype="multipart/form-data">
              <p class="notice">※ファイルsizeは1つにつき30MBまで、名前は英小文字で</p>
              <!-- UPし直し表示 -->
              <?php echo $restore; ?>
              <p id="<?php echo $up_before; ?>">ファイル「<?php echo $filename; ?>」はアップされました。続けてUPできます</p>
              <label for="up">
                  <!-- ファイルを選択ボタン -->
                  <?php echo $select_file; ?>
              </label>
              <input id="up" type="file" name="file_up">
              <p id="send"><input type="submit" value=""></p>
              <!-- エラーメッセージ -->
              <?php echo $messege; ?>
        </form>

      <div id="fileBox">
          <p id="fileCount">ファイル数<?php echo count($dw_items); ?>項目</p>         
          <h3 class="selectFile" id="dowTab">DL用ファイル一覧</h3>  
          <form id="dowList" action="download_file.php" method="post">
             <!-- 飛び先でLocation リダイレクト処理-->
              <ul>
                 <?php if(count($dw_items) === 0): ?>
                    <li>まだファイルはありません</li>
                    <?php else: ?>
                      <?php foreach($dw_items as $items): $dw_name = Pathinfo($items,PATHINFO_BASENAME); ?>                   
                     <li><input type="radio" name="dowl" value="<?php echo $dw_name; ?>"><?php echo $dw_name; ?></li>                     
                      <?php endforeach; ?>
                  <?php endif; ?>
              </ul>
              <p class="notice">※左のラジオボタンにチェックしてダウンロードボタンをクリックしてください</p>
              <p class="pibtn"><input type="submit" value="Download"></p>
          </form>

          <h3 class="selectFile" id="delTab">削除用ファイル一覧</h3>
          <form id="delList" action="delete_confilm.php" method="post">
              <ul>
                 <?php if(count($dw_items) === 0): ?>
                    <li>まだファイルはありません</li>
                    <?php else: ?>
                      <?php foreach($del_items as $items): $del_name = Pathinfo($items,PATHINFO_BASENAME); ?>
                         <li><input type="checkbox" name="del[]" value="<?php echo $del_name; ?>"><?php echo $del_name; ?></li>  
                      <?php endforeach; ?>
                  <?php endif; ?>
              </ul>
              <p class="notice">※左のチェックボックスを選択(複数可)して確認ボタンをクリックしてください</p>
              <p class="vibtn"><input type="submit" value="削除確認"></p>
          </form>
      </div><!-- //id="fileBox"-->

    </main>  
<!-- footerインクルード -->
<?php require_once(dirname(__FILE__).'/footer.php'); ?>



input type="file"は特殊で、デフォルトのボタンを使うのはビジュアル面で抵抗があったのでカスタマイズしました。

ファイルが選択されたらボタンがファイル名に変わって、エラーだったら上にメッセージ表示。
OKだったらtype="file"ブロックは非表示になって、type="submit"にすり替え。
見た目は一緒のボタンです。

ファイルアップが成功したらボタンの上にファイル名が表示して下のリストに追加といった仕様。

ただ、ファイル選択時はsubmitボタンは押されてないのでphpでの処理が難しい。
そこでjsのchangeイベントを活用。
ここは頭がこんがらがりました。phpとjsとcssのトリプル連携。

submit_btn.js
jQuery('#up').on('change',function(){
      const upfile = jQuery('#up').get(0).files;
      console.log(upfile);
      //多次元配列になるのかな? upfile[[name: xxx,size: xxx]]
      const fileName = upfile[0].name;            
      console.log(fileName);
       //file選択をsubmitにすり替え
      jQuery('#take').css('display','none');
      jQuery('#send').css('display','block');
      jQuery('#send input').val(fileName);
   });


ダウンロードチェックページ

ダウンロードのコードはどうしても分からなかったのでググって動いたものをコピペさせてもらいました。
それまではダウンロードできても開けられなかったり不具合続出。

ここのコードが一番難しい。
今の段階では理解できなかったのですが、そのうち自分でも組めるようになりたいです。

参考サイトはこちら

download_file.php
//ダウンロードチェック
session_start();
session_regenerate_id(true);
if(isset($_SESSION['login']) === false){
    header('Location: un_login.php');
    exit();
}

function html_escape($word){
        return htmlspecialchars($word,ENT_QUOTES,'UTF-8');
    }
$dowload_file = $_POST['dowl'];
$dowload_file = html_escape($dowload_file);

//var_dump($dowload_file);

//ここのコード分からなかったのでネットからググって拾ってきた
    function download($pPath, $pMimeType = null){
    //-- ファイルが読めない時はエラー(もっときちんと書いた方が良いが今回は割愛)
    if (!is_readable($pPath)) { die($pPath); }

    //-- Content-Typeとして送信するMIMEタイプ(第2引数を渡さない場合は自動判定) ※詳細は後述
    $mimeType = (isset($pMimeType)) ? $pMimeType
                                    : (new finfo(FILEINFO_MIME_TYPE))->file($pPath);

    //-- 適切なMIMEタイプが得られない時は、未知のファイルを示すapplication/octet-streamとする
    if (!preg_match('/\A\S+?\/\S+/', $mimeType)) {
        $mimeType = 'application/octet-stream';
    }

    //-- Content-Type
    header('Content-Type: ' . $mimeType);

    //-- ウェブブラウザが独自にMIMEタイプを判断する処理を抑止する
    header('X-Content-Type-Options: nosniff');

    //-- ダウンロードファイルのサイズ
    header('Content-Length: ' . filesize($pPath));

    //-- ダウンロード時のファイル名
    header('Content-Disposition: attachment; filename="' . basename($pPath) . '"');

    //-- keep-aliveを無効にする
    header('Connection: close');

    //-- readfile()の前に出力バッファリングを無効化する ※詳細は後述
    while (ob_get_level()) { ob_end_clean(); }

    //-- 出力
    readfile($pPath);

    //-- 最後に終了させるのを忘れない
    exit;
}

//選択されたファイルがあったらダウンロード、なかったらそのままリダイレクト
//選択されないままダウンロード処理されちゃうと変なファイルがDLされる
if(isset($_POST['dowl'])){
    download('./up_file/'.$dowload_file);
    header('Location: file_list.php');   
} else {
   header('Location: file_list.php');
}


削除確認、完了ページ

AdobeXD イメージ図
check.jpg

削除だけは誤って消してしまって後悔しないように、確認してからの導線にしました。
完了ページのレイアウトもほぼ一緒です。

delete_done.php
//ログインしていないとアクセスさせない
session_start();
session_regenerate_id(true);
if(isset($_SESSION['login']) === false){
    header('Location: un_login.php');
    exit();
}

function html_escape($word){
    return htmlspecialchars($word,ENT_QUOTES,'UTF-8');
}

$delete_file = '';

//POSTで渡されたファイルを削除
if(isset($_POST['check'])){
    for($i = 0; $i < count($_POST['check']); $i++){           
        unlink('./up_file/'.html_escape($_POST['check'][$i]));
        //削除ファイルli書出し
        $delete_file .= '<li><i class="far fa-file"></i>'.html_escape($_POST['check'][$i]).'</li>';
    }    
}

?>
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>ファイル削除完了 | ファイル預かり処・マイ保管庫</title>
<!-- headerインクルード -->
<?php require_once(dirname(__FILE__).'/header.php'); ?>
<div id="wrapper">
    <header>
       <div id="titleVar">
          <h1><i class="fas fa-circle"></i>ファイル預かり処 マイ保管庫</h1>
           <p id="logout"><a href="logout.php">ログアウト</a></p>
       </div>       
       <div id="topvew">My keep folder</div>             
    </header>

    <main id="confiBox">
        <h2 class="pageTitle">ファイル削除完了</h2>
        <p class="confiText">以下のファイルを削除しました</p>
        <ul>
            <?php echo $delete_file; ?>         
        </ul>
        <p id="toListpage"><a href="file_list.php">ファイル一覧ページへ</a></p>
    </main>

<!-- footerインクルード -->
<?php require_once(dirname(__FILE__).'/footer.php'); ?>

ログアウトページ

ログアウトページは、お約束のセッション破棄だけなのでコードは省きます。
レイアウトはログインページと同じにしました。


ひとまず完成

あとは実際のサーバに上げて動作確認。

動いた時は大感動。
あれ?
でもアップできないファイルがある。

色々試して、どうやら日本語名のファイルはアップできないみたい。

XAMPP開発時では日本語のファイル名でも大丈夫だったんですけどね。
そういえば日本語とサーバの相性は良くないと昔から言われてました。

とりあえず注意書きに日本語NGと追加して応急対処。
preg_matchで条件設定した方がいいのかなと思ったりしています。

ひとまず、これで完成として使い込んでみようと思ってます。
制作期間は約1週間、畑仕事と家事の合間にコツコツと作業

お婆ちゃんのweb制作奮闘記でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Gatsby.js】ポートフォリオ作成_環境準備

Node.js をインストール

// nodeをインストール
brew install node

インストール後、バージョンチェック

// nodeのバージョンチェック
node -v
v12.16.3

Gatsby.js をインストール

// gatsby をインストール
npm i -g gatsby-cli

// gatsby のバージョンチェック
gatsby -v
Gatsby CLI version: 2.12.29

// プロジェクトの雛形を生成(TypeScript)
gatsby new gatsby_sample01 https://github.com/haysclark/gatsby-starter-typescript

スタータのひな形は以下にある。
https://www.gatsbyjs.org/starters/?v=2

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む