openFrameworksを別スレッドでTwitter連携させる

  • 41
    Like
  • 0
    Comment

Life is Tech ! Advent Calendar 2016 の24日目の記事です。
クリエイティブコーディング Advent Calendar 2016に書くか迷いました。
ということで、あまり初心者向けな丁寧な解説はそこまで載せてないので、またまだまだ修正できるとこがあると思うので書いていただける方編集リクエストお待ちしております。

この記事では、openFrameworksTwitter Streaming APIsNode.js を使って、リアルタイムにTweetを拾ってきてそれの描画みたいなことをしようと思います。

そもそも、この記事で紹介するものはは京大生の @YoshimasaSakuragi の多大な contribute があり、去年一緒にやった武蔵野美術大学芸術祭プロジェクションマッピング企画とかに向けて作ったものです(この時のものを少し修正しました)

123

例えば、東工大メディア研究会OFFTHERAILSというイベントで作ったものとか、大勢の観客がいて気軽にインタラクションみたいなのできるのはTwitterなのではという考えの元に、そういったイベントで使ったりしています。
123

FacebookのGraph APIと連携するバージョンもそのうち書きたいと思います。

環境

今回はOSX環境のみの紹介になります。基本的に私の環境が少し古いですが、ほとんど、最新バージョンと互換性があるみたいなので最新に全部持っていけば問題ないかと思います。

本記事の環境 Image 最新版のURL
Xcode 7.3.1 xcode https://developer.apple.com/download/
OSX 10.11.4 osx https://support.apple.com/ja_JP/downloads/macos
openFrameworks 0.9.2 openFrameworks http://openframeworks.cc/ja/download/
ofxJSON Screen Shot 2016-12-03 at 12.58.39.png https://github.com/jefftimesten/ofxJSON
ofxTrueTypeFontUL2 Screen Shot 2016-12-03 at 12.58.39.png https://github.com/kentaroid/ofxTrueTypeFontUL2
ofxOpenGLContextScope Screen Shot 2016-12-03 at 12.58.39.png https://github.com/satoruhiga/ofxOpenGLContextScope
Node.js v4.1.1 Screen Shot 2016-12-21 at 10.49.55.png https://nodejs.org/en/
twit 2.0.1 Screen Shot 2016-12-21 at 10.58.00.png https://www.npmjs.com/package/twit

事前準備

node.jsとStreaming APIs (twitを使うけどkeyとかアクセストークンが必要なので登録)に関しては他の方が色々まとめてくれているので割愛しますが、この準備が以下の前提になっているので参考サイトをリンクしていますのでご準備ください。twitが便利だと思います。

ここまでで

streaming.js
var tweet = require('twit');
var fs = require('fs');
var file = 'twitter.json';
var T = new tweet({
  consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
  consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
  access_token: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
  access_token_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
});

var stream = T.stream('statuses/filter', {track: '#NowPlaying'});
stream.on('tweet', function(tw) {
  fs.writeFile(file, JSON.stringify(tw), null, null);
  fs.readFile(file, 'utf8', function (err, tw) {
    console.log(JSON.stringify(tw));
  });
});

みたいな、適当なハッシュタグとかでつぶやかれたもの(今回は#NowPlaying)を、
sample.gif

のような感じで、

$ node streaming.js

と実行すればtwitter.jsonとかいう名前(任意の名前で大丈夫です)のファイルに書き込むようなもの(今回の場合はstreaming.jsというファイル)を用意できていれば事前準備は完了です。

作るものの整理(概観)

以下、クラスの命名とかちゃんとできてないかもしれないので、この記事とか参考に、編集リクエストお待ちしております。また、たまに箇条書きの形で貼っているURLは参考になるかも!と思ったリンクです。

1. Projectの作成

AddonにofxJSON,ofxTrueTypeFontUL2,ofxOpenGLContextScopeを入れてプロジェクトを作成します。ofxJSONはJsonを監視したり読み取るため、ofxTrueTypeFontUL2は日本語などの描画のため、ofxOpenGLContextScopeは別スレッドでの画像の読み込みのために必要です。

2. TweetDataみたいなクラスを作る

取得したデータをopenFrameworksのプロジェクトで使いやすいようデータに格納しますが、その際のクラスを用意しておきます。

3. WStringConverterの準備

日本語などのマルチバイト文字列を扱うために、wstringというクラスを使います、その変換のためのWStringConverterを準備します。

4. FontManagerの準備

日本語などのマルチバイト文字列を表示するためにofxTrueTypeFontUL2を使いますが、その管理をするクラスを作ります。

5. TweetJsonReceiverの準備

Node.jsで取得し続けているJSONファイルをパースして先ほど用意したTweetDataみたいなクラスを準備してそれに格納していくみたいなことをします。

6. JsonReceiveThreadの準備

メインスレッドでJsonから読み取った画像のURLをたたいて画像を取得していたりするとFPSが落ちるので、別スレッドで実行します。そのためのJsonReceiveThreadを用意します。
また、このスレッドで画像の取得をするためにofxOpenGLContextScopeを使っています。

7. TwitterStreamingManagerみたいなManagerクラスを作る

上記の、Twitterから取得したJsonファイル読み取ったり、ThreadにしたりをまとめるManagerみたいなものを、用意します。

8. 取得データを描画する何かを書く

せっかく取得しましたが、ここまででは何も描画しないので、適当に描画するコードを書きます。ここまでの労力の1/10くらいでできます。

実装

1. Projectの作成

こんな感じで。
Screen Shot 2016-12-21 at 09.39.06.png

また、今回はTwitterStreamingSampleという名前にしましたが、このディレクトリと、Node.jsで書いたstreaming.jsが含まれるディレクトリ(TweetStreamという名前にしてみました)が同じディレクトリ内にあるようにしておいてください(コード中に出てくるパスの設定のため)。

Screen Shot 2016-12-21 at 14.38.53.png

2. TweetDataみたいなクラスを作る

コードを書きます。特に特別なことはせずに、Twitterから取得できるデータをデータごとに記述し、一度取得すればいいデータなので中身がいろいろ変更になることはなく、コンストラクタ内の引数でそのデータを渡す形をとりました。

TweetData.hpp
#include "ofMain.h"
class TweetData{
public:
    wstring tweet_text; //twwet contents
    wstring user_name;  //user name
    string twitter_id;  //screen_name @account name
    string icon_url;    //icon image url

    int friends_count;  //number of following
    int statuses_count; //number of tweet
    int followers_count;//number of follower

    ofImage image;      //to store icon image

    TweetData();
    TweetData(const wstring &tweet_text,
                   const wstring &user_name,
                   const string &twitter_id,
                   const string &icon_url,
                   const int &friends_count,
                   const int &statuses_count,
                   const int &followers_count);
};
TweetData.cpp
#include "TweetData.hpp"

//------------------------------------------------------------------------------------
TweetData::TweetData(){

}

//------------------------------------------------------------------------------------
TweetData::TweetData(const wstring &tweet_text,
                               const wstring &user_name,
                               const string &twitter_id,
                               const string &icon_url,
                               const int &friends_count,
                               const int &statuses_count,
                               const int &followers_count){
    cout << twitter_id << endl;  // output console
    this->tweet_text = tweet_text;
    this->user_name = user_name;
    this->twitter_id = twitter_id;
    this->icon_url = icon_url;
    this->friends_count = friends_count;
    this->statuses_count = statuses_count;
    this->followers_count = followers_count;
    this->image.load(this->icon_url);
}

ここでは、jsonで取得したtweetに紐づく内容などをまとめて一つのクラスにするためのもので、コンストラクタに引数でデータをあげています。

3. WStringConverterの準備

ここはもう難しいので、気にせずコピペしてみてください。
C++と一概に言っても、windowsとOSX環境でも文字コードなどの違いからいろんなことを機にする必要があります。

WStringConverter.hpp
//  This was quoted from @YoshimasaSakuragi
#include "ofMain.h"

class WStringConverter{
public:
    static wstring convToWString(string src);
    template <class T>
    static wstring convToUCS4(basic_string<T> src);
};
WStringConverter.cpp
#include "WStringConverter.hpp"

//--------------------------------------------------------------------------------------------
wstring WStringConverter::convToWString(string src) {
#ifdef TARGET_WIN32
    wstring dst = L"";
    typedef codecvt<wchar_t, char, mbstate_t> codecvt_t;

    locale loc = locale("");
    if(!std::has_facet<codecvt_t>(loc))
        return dst;

    const codecvt_t & conv = use_facet<codecvt_t>(loc);

    const std::size_t size = src.length();
    std::vector<wchar_t> dst_vctr(size);

    if (dst_vctr.size() == 0)
        return dst;

    wchar_t * const buf = &dst_vctr[0];

    const char * dummy;
    wchar_t * next;
    mbstate_t state = {0};
    const char * const s = src.c_str();

    if (conv.in(state, s, s + size, dummy, buf, buf + size, next) == codecvt_t::ok)
        dst = std::wstring(buf, next - buf);

    return dst;
#elif defined __clang__
    wstring dst = L"";
    for (int i=0; i<src.size(); ++i)
        dst += src[i];
#if defined(__clang_major__) && (__clang_major__ >= 4)
    dst = convToUCS4<wchar_t>(dst);
#endif
    return dst;
#else
    return convToUCS4<char>(src);
#endif
}


//--------------------------------------------------------------------------------------------
template <class T>
wstring WStringConverter::convToUCS4(basic_string<T> src) {
    wstring dst = L"";
    int size = src.size();
    int index = 0;
    while (index < size) {
        wchar_t c = (unsigned char)src[index];
        if (c < 0x80) {
            dst += (c);
        }else if (c < 0xe0) {
            if (index + 1 < size) {
                dst += (((c & 0x1f) << 6) | (src[index+1] & 0x3f));
                index++;
            }
        }else if (c < 0xf0) {
            if (index + 2 < size) {
                dst += (((c & 0x0f) << 12) | ((src[index+1] & 0x3f) << 6) |
                        (src[index+2] & 0x3f));
                index += 2;
            }
        }else if (c < 0xf8) {
            if (index + 3 < size) {
                dst += (((c & 0x07) << 18) | ((src[index+1] & 0x3f) << 12) |
                        ((src[index+2] & 0x3f) << 6) | (src[index+3] & 0x3f));
                index += 3;
            }
        }else if (c < 0xfc) {
            if (index + 4 < size) {
                dst += (((c & 0x03) << 24) | ((src[index+1] & 0x3f) << 18) |
                        ((src[index+2] & 0x3f) << 12) | ((src[index+3] & 0x3f) << 6) |
                        (src[index+4] & 0x3f));
                index += 4;
            }
        }else if (c < 0xfe) {
            if (index + 5 < size) {
                dst += (((c & 0x01) << 30) | ((src[index+1] & 0x3f) << 24) |
                        ((src[index+2] & 0x3f) << 18) | ((src[index+3] & 0x3f) << 12) |
                        ((src[index+4] & 0x3f) << 6) | (src[index+5] & 0x3f));
                index += 5;
            }
        }
        index++;
    }
    return dst;
}

とりあえず、WStringConverter::convToWString(string str)strがwstringに変換されると思ってもらえれば大丈夫です。

4. FontManagerの準備

日本語などのマルチバイト文字列を表示できるようにしたいので、まず適当なフォントファイルそれをdata/font/hoge.otfみたいな感じで用意しておきます。
次にFontのFontManagerクラスを作ります。

sc.png

配置が終われば、このフォントを使って文字を画面に出力するためのクラスを作ります。FontManagerは不変で毎度インスタンス化する必要はないので、staticなメソッドとしていろいろ定義しています。

FontManager.hpp
#include "ofMain.h"
#include "ofxTrueTypeFontUL2.h"

class FontManager{
public:

    FontManager(){};

    static ofxTrueTypeFontUL2 mbigfont;
    static ofxTrueTypeFontUL2 msmallfont;

    static void init();

    static void loadSmallFont(const string &arg,const int &size);
    static void loadBigFont(const string &arg,const int &size);

    static void drawSmallString(const string &arg ,const float &x,const float &y);
    static void drawBigString(const string &arg ,const float &x,const float &y);

    static void drawSmallWString(const wstring &arg ,const float &x,const float &y);
    static void drawBigWString(const wstring &arg ,const float &x,const float &y);
};
FontManager.cpp
#include "FontManager.hpp"

ofxTrueTypeFontUL2 FontManager::msmallfont;
ofxTrueTypeFontUL2 FontManager::mbigfont;

//--------------------------------------------------------------------------------
void FontManager::init(){
    setlocale(LC_CTYPE,"");
    if(msmallfont.isLoaded())return;
    FontManager::loadSmallFont("font/hoge.otf", 12);
    FontManager::loadBigFont("font/hoge.otf", 22);
}

//--------------------------------------------------------------------------------
void FontManager::loadSmallFont(const string &arg,const int &size){
    msmallfont.loadFont(arg, size);
}

//--------------------------------------------------------------------------------
void FontManager::drawSmallString(const string &arg ,const float &x,const float &y){
    msmallfont.drawString(arg, x, y);
}

//--------------------------------------------------------------------------------
void FontManager::drawSmallWString(const wstring &arg ,const float &x,const float &y){
    msmallfont.drawString(arg, x, y);
    msmallfont.useProportional(true);
    msmallfont.useVrt2Layout(true);
}

//--------------------------------------------------------------------------------
void FontManager::loadBigFont(const string &arg,const int &size){
    mbigfont.loadFont(arg, size);
}

//--------------------------------------------------------------------------------
void FontManager::drawBigString(const string &arg ,const float &x,const float &y){
    mbigfont.drawString(arg, x, y);
}

//--------------------------------------------------------------------------------
void FontManager::drawBigWString(const wstring &arg ,const float &x,const float &y){
    mbigfont.drawString(arg, x, y);
    mbigfont.useProportional(true);
    mbigfont.useVrt2Layout(true);
}

細かな設定の命令などはこちらのREADMEを参考にしてください。

5. TweetJsonReceiverの準備

C++では通常のクラスはコンストラクタの呼び出し(newの使用や非ポインタ、非参照変数の宣言など) 毎に実体が生成されます。
しかしログや重いオブジェクトなど、プログラム実行中に実体は一つのみ生成し、その実体を使いまわしたい場合があります。
この様な場合にシングルトンパターンを適用すると、 実体がプログラム中で一つであることが保証されます。

こちらは先ほどの、FontManagerとは違い、状態が不変ではないので、シングルトンパターンっぽく書いています。先ほどのものはモノステイトパターンっぽいです(namespaceで区切るほうが良いかもしれません)。

TweetJsonReceiver.hpp
#include "ofxJSONElement.h"
#include "TweetData.hpp"
#include "ofMain.h"
#include "WStringConverter.hpp"

//保持するtweetのデータの最大値
#define DATA_MAX_NUM 50

class TweetJsonReceiver {
public:
    int64_t cachedTweetId;
    ofxJSONElement jsonElement;
    static int data_num;
    static vector<TweetData> tweetData;
    static TweetJsonReceiver &getInstance();

    TweetJsonReceiver(){}
    TweetJsonReceiver &operator=(const TweetJsonReceiver &other){}

    vector<TweetData> getTweetData();
    bool checkUpdateJson();
    void parseJson();
    void init();
    void update();
    bool receive();
};

更新を追いかけるため、tweetの固有番号であるidをcheckUpdateJson()内で毎回確認するような仕組みです。

TweetJsonReceiver.cpp
#include "TweetJsonReceiver.hpp"

vector<TweetData> TweetJsonReceiver::tweetData;
int TweetJsonReceiver::data_num;

//--------------------------------------------------------------------------------------------
TweetJsonReceiver &TweetJsonReceiver::getInstance() {
    static TweetJsonReceiver instance;
    return instance;
}

//--------------------------------------------------------------------------------------------
vector<TweetData> TweetJsonReceiver::getTweetData(){
    return tweetData;
}

//--------------------------------------------------------------------------------------------
bool TweetJsonReceiver::checkUpdateJson(){
    if(jsonElement["id"].asInt64() != cachedTweetId){
        cachedTweetId = jsonElement["id"].asInt64();
        return true;
    }else{
        return false;
    }
}

//--------------------------------------------------------------------------------------------
void TweetJsonReceiver::init(){
    data_num = 0;
    TweetJsonReceiver::tweetData.clear();
    bool parsingSuccessful = jsonElement.openLocal("../../../TweetStream/twitter.json");//Nodeで取得したJSON
    if (parsingSuccessful){
        cout << "Parsing Successful" << endl;
        cachedTweetId = jsonElement["id"].asInt64();
    }else{
        cout << "Failed to parse JSON" << endl;
    }
}

//--------------------------------------------------------------------------------------------
void TweetJsonReceiver::parseJson(){
    string tmptext = jsonElement["text"].asCString() ;
    string tmpusername = jsonElement["user"]["name"].asCString();
    wstring wtmptext = WStringConverter::convToWString(tmptext);
    wstring wtmpusername = WStringConverter::convToWString(tmpusername);

    tweetData.push_back((TweetData){
        WStringConverter::convToWString(tmptext),
        WStringConverter::convToWString(tmpusername),
        jsonElement["user"]["screen_name"].asCString(),
        jsonElement["user"]["profile_image_url"].asCString(),
        jsonElement["user"]["friends_count"].asInt(),
        jsonElement["user"]["statuses_count"].asInt(),
        jsonElement["user"]["followers_count"].asInt()
    });
    data_num++;
    return;
}


//--------------------------------------------------------------------------------------------
//Jsonの数に変化があればその分をtweetDataに追加
void TweetJsonReceiver::update(){

    //tweetDataが無限にたまらないようにFIFOさせる
    if(TweetJsonReceiver::tweetData.size()>DATA_MAX_NUM){
        TweetJsonReceiver::tweetData.erase(TweetJsonReceiver::tweetData.begin());
        cout << "CLear data" <<endl;
    }
    receive();
}


//--------------------------------------------------------------------------------------------
bool TweetJsonReceiver::receive(){
    bool parsingSuccessful = jsonElement.openLocal("../../../TweetStream/twitter.json");//Nodeで取得したJSON
    if (parsingSuccessful){
        if(checkUpdateJson()){
            parseJson();
            return true;
        }
    }else{
        cout << "Failed to parse JSON" << endl;
    }
    return false;
}

init()を一度読んであげたのち、update()で新しいtweetの情報が更新されていればTweetData型のvectorであるtweetDataに要素が追加されていくようになっています。またコメントでも書きましたが、TweetJsonReceiver::tweetDataに要素が追加されすぎてメモリが足りなくなることを防ぐため、update()内で要素数が増えすぎたら消すようにしています。

6. JsonReceiveThreadの準備

先ほど用意したTweetJsonReceiverをメインスレッドで呼ぶとFPSが落ちたりあまりいいことがないので、tweetDataへの要素の追加や画像の読み込みなどは、別スレッドで行いましょうということをするための準備をします。
このスレッドを用いない場合はメインスレッドで画像取得なども行うことになり処理速度が劇的に低下します。

JsonReceiveThread.hpp
#include "ofMain.h"
#include "TweetJsonReceiver.hpp"
#include "ofxOpenGLContextScope.h"

class JsonReceiveThread : public ofThread{

public:
    void threadedFunction(){
        ofxOpenGLContextScope scope;
        while (isThreadRunning()) {
            if(lock()){
                TweetJsonReceiver::getInstance().update();
                unlock();
                sleep(100);
            }
        }
    }
};

ヘッダファイルに実装も書いたので、JsonReceiveThread.cppはいじらなくて大丈夫です。(これwindowsの環境だと動かないかも)
このスレッドでは100msに一回TweetJsonReceiver::getInstance().update();を読んでいるので、メインスレッドでアップデートしなくても、このスレッドで、tweetDataへの要素の追加や画像の読み込みなどやってくれることになります。
また、ofxOpenGLContextScope scopeというおまじないがないと、メインスレッド外での画像のallocate的なことができないっぽいのでおまじないだと思って書いておきましょう。

7. TwitterStreamingManagerみたいなManagerクラスを作る

今までのを簡単に使えるようにManagerクラスとしてまとめたいと思います、(componentとかの方が名前としてはいいのかな?)
これも実体は一つでいいので、そのことを保証するためにも先ほどのデザインパターンを適用してもいいかもしれません。

TwitterStreamingManager.hpp
#include "ofMain.h"
#include "TweetJsonReceiver.hpp"
#include "TweetData.hpp"
#include "ofxOpenGLContextScope.h"
#include "JsonReceiveThread.hpp"

class TwitterStreamingManager{
private:
    JsonReceiveThread mJsonReceieveThread;
public:
    void setup();
    void exit();
};
TwitterStreamingManager.cpp
#include "TwitterStreamingManager.hpp"

//------------------------------------------------------------------------------------
void TwitterStreamingManager::setup(){

    //Twitter初期化はメインスレッドで行う
    if(TweetJsonReceiver::tweetData.size()>0)return;
    TweetJsonReceiver::getInstance().init();

    //別スレッドで画像読み込みも含めてデータ取得する
    ofxOpenGLContextScope::setup();
    mJsonReceieveThread.startThread();
}

//------------------------------------------------------------------------------------
void TwitterStreamingManager::exit(){
    mJsonReceieveThread.stopThread();
}

mJsonReceieveThread.startThread()してしまえば、100msに一回TweetJsonReceiver::getInstance().update()されるはずなので大丈夫です。また、end()でしっかりスレッドをstopThread()してあげましょう。

8. 取得データを描画する何かを書く

これはもうなんでも大丈夫です。
わかりやすく参考までにofApp.hofApp.cppに書いてみようと思います。今回は簡単にこんな感じのものを作ります。リアルタイムで #NowPlaying とつぶやいた人の情報を表示し続けるものです。

gif

今回の場合以下のように、ofApp.hofApp.cppに描画関係の命令を書いていきます。(普通ならクラスとかに切り分けたほうがいいですが、今回は簡単にするため直書きです)

ofApp.h
#include "ofMain.h"
#include "TwitterStreamingManager.hpp"
#include "FontManager.hpp"

class ofApp : public ofBaseApp{

    public:
        void setup();
        void update();
        void draw();

        void exit();

        void keyPressed(int key);
        void keyReleased(int key);
        void mouseMoved(int x, int y );
        void mouseDragged(int x, int y, int button);
        void mousePressed(int x, int y, int button);
        void mouseReleased(int x, int y, int button);
        void mouseEntered(int x, int y);
        void mouseExited(int x, int y);
        void windowResized(int w, int h);
        void dragEvent(ofDragInfo dragInfo);
        void gotMessage(ofMessage msg);

    TwitterStreamingManager twitterStreamingManager;

TwitterStreamingManagerのインスタンスを一つ用意しておきました。またexit()命令を追加しました。

ofApp.cpp
#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
    twitterStreamingManager.setup();
    FontManager::init();
}

//--------------------------------------------------------------
void ofApp::update(){
    ofSetWindowTitle("FPS: " + ofToString(ofGetFrameRate()) +"/  num of element: "+ofToString(TweetJsonReceiver::data_num));
}

//--------------------------------------------------------------
void ofApp::draw(){
    ofBackground(0);
    ofPushMatrix();
    ofTranslate(ofGetWidth()/2-300, ofGetHeight()/2);
    if(TweetJsonReceiver::tweetData.size()>0){
        TweetJsonReceiver::tweetData.back().image.draw(-100,-100,200,200);
        FontManager::drawSmallString("@"+ofToString(TweetJsonReceiver::tweetData.back().twitter_id),150,-60);
        FontManager::drawSmallWString(TweetJsonReceiver::tweetData.back().user_name,150,-40);
        FontManager::drawSmallString("followers_count: "+ofToString(TweetJsonReceiver::tweetData.back().followers_count),150,-20);
        FontManager::drawSmallString("friends_count: "+ofToString(TweetJsonReceiver::tweetData.back().friends_count),150,0);
        FontManager::drawSmallWString(TweetJsonReceiver::tweetData.back().tweet_text,150,20);
    }
    ofPopMatrix();
}

//--------------------------------------------------------------
void ofApp::exit() {
    twitterStreamingManager.exit();
}

//~~以降特に追記しないので省略~~

ここでは、特に難しいことはしていません、画面の中央付近にtweetした人のいろんな情報を表示しているだけです。

この描画部分を頑張ってたくさん書くと、最初の例ようなリッチな画面ができます。

最後に

最後に今回のサンプルはgithubにあげてますのでそちらも参考にしてみてください。

今回Twitterに特化して扱ったように書きましたが、ofxJSONは便利で、例えばFaceBook Graph API使って取得した何かとか、とにかくJSONであれば、この記事の 実装のところを読めば汎用的だと思うので、大体できると思うので試してみてください。

また、いろいろ修正してくださる方いらっしゃいましたら修正リクエストお願いします🙇