nocebo.jp フリーランスWEBエンジニア https://nocebo.jp 多分フロントエンドエンジニア寄りのフリーランスWeb制作者のサイトです。他バンドでギター弾いたり、max/mspいじったり等もしております。あと犬が飼いたい。仕事のご依頼その他・お気軽にお問い合わせください。 Fri, 01 Jul 2022 07:50:52 +0000 ja hourly 1 https://wordpress.org/?v=5.0.4 https://nocebo.jp/wp/wp-content/uploads/2019/02/cropped-acc-32x32.png nocebo.jp フリーランスWEBエンジニア https://nocebo.jp 32 32 エックスサーバーにLaravel+Nextjs製のアプリをデプロイ(deployer) https://nocebo.jp/post-4469/ Fri, 20 May 2022 03:49:53 +0000 https://nocebo.jp/?p=4469 求職マッチングサイト的なものの制作相談が入り、
普段はフロントの仕事メインでやってるもので
バックエンドとかインフラだったりは
あんまり経験ある訳では無いのだけど受けてみた。時の話。

とりあえず結論から言うと
エックスサーバーでlaravel+Nextjsで公開できたので
何点か詰まったところとか気になった所等を書いてみようと思います。

サーバー周りとか経緯(エックスサーバー)

何となくの印象だけどこうゆうrestful?的な物は
AWSでやるのが今っぽいイメージがある。

僕の通常業務の環境。と言っていいのか分からないけど
普段の業務はフリーでやりだした時に契約した
さくらのVPSでしつこく頑張っております。
OSのサポートとかが切れてるのでいい加減
AWSとかにしないとかなぁ、と思いつつ。

弱小フリーランスにはAWS高い。気がする。
と移行とか色々考えると面倒でなかなか腰が上がらなくて
未だにAWS触った事がなく、

どうしようかなと思って軽く調べてたら

エックスサーバー

でLaravelインストールできるっぽいので
いわゆる普通のレンタルサーバー(今回はXserverビジネス)
でできる範囲の事だったらやってみますよって形でお引き受けしました。

普段VPSメインで利用してるのでVPSユーザー視点で
所々ちょっと気になる点はあったのでその辺書いてく

でも大体やりたい事はなんとかなりました。
エックスサーバーおすすめです。

Laravel

インストールとかの手順はググると他にも色々記事あるので
あんまり書くことないけど
SSHが使えるのでまぁ色々Composerだったりのインストールしたり
とかは必要ですが問題なく稼働しました。

昔は普通のレンタルサーバーだとSSH使えるってあんまりなかった?
ようなイメージがある(知らなかっただけかも)けど
エックスサーバーだとスタンダードプランとかでもSSH使えるみたい。

PHPもバージョンは固定ではあるけど
いくつかver切り替えできるので
dockerとかで事前に合わせておけば
多分本番デプロイ時もそんなに困る事ない。と思う。

気になった点

ドキュメントルートが固定

一点ちょっと不便というか、仕方ない所だと思うけど

 domain.com/public_html

とかってドキュメントルートが固定な所。

VPSとかだとapacheなりnginxで設定できるから
そんなに気にしなくてもいいところだけど

public_html以下に配置すると
色々見えちゃいけない所がpublicになっちゃうので
別ディレクトリに配置しつつシンボリックリンクで
publicへ紐付ける必要がありました。

Nextjs

フロントはnextjsで作りました。

SSGで静的サイト生成してから反映する形にした。
のでサーバー側でnodeは走らせてる訳ではないのですが

エックスサーバーでnode

エックスサーバーだとnodeもインストールできるらしいので
多分SSRとかもできると思う。
これも一昔前はレンサバだと出来なかったような?気がする。
ただ公式でサポートはしていない?とは思う。

htaccess

因みにだけどnextで反映するにあたって
ちょこちょこhtaccess調整したので覚え書き

フロントのディレクトリまとめる

公開ディレクトリのルートに書き出した静的ファイルが
ゴチャゴチャするが嫌だったので
nextから出力した静的ファイルは

RewriteRule ^(.*)$ front/$1 [L]

的な感じでまとめた。

ダイナミックルーティング

SSGでダイナミックルーティング使ってると
リロードとかした時に404になると思うので

RewriteRule ^front/info/.*/(.*)$ front/info/[id]/$1 [L]

的な感じでリライト。

nextからLaravelへのリンク

nextからのlaravelへのストレージとかだったりAPIリクエストも
シンボリックリンクでなんとかしました。

ln -s /home/○○○○○/○○○○○/path/to/laravel/public /home/○○○○○/○○○○○/public_html/api

let’s encryptのおかげなのかSSLも最近は無料でできるので
サブドメインとかでやってもいいかも?とも思ったけど。

deployer

Laravelでデプロイって言うと
deployer。だと思う。多分。

https://deployer.org/

これも多分公式だとサポート外だけど
gitインストールしたり
sshの公開鍵登録なんかもできるので
色々準備は必要だけど出来た。

シンボリックリンク(気になった点)

ここは結構気になった点
laravelでもnextでも言える事なんだけど
エックスサーバーのシンボリックリンクで
気になるところがあった。

というか未だに解決してない。
公開自体は問題ないんだけど
なんかシンボリックリンクのキャッシュ?っていうのか
なんなのかもわからないんだけど
deployer使うと

~~/releases/{デプロイのcuurent_number}/laravel/public

って感じで配置されるけど
public_htmlからのapiへのカレントへの

~~/current/laravel/public

シンボリックリンクの参照がクリアできない事が多い。

色々試してみたけど、一応


① サーバーキャッシュ設定からキャッシュ削除
②X アクセラレータの設定変更(OFFへの)

でリンクキャッシュクリアできるっぽい

※サーバーキャッシュ設定はOFFじゃないと残る
※ブラウザキャッシュはONでOK?なような


って感じで一応消せる?というか解消するっぽい
なので現状デプロイする度管理コンソール開いて
設定カチャカチャする必要があって面倒。
とどういう経緯でクリアされるのかあまり分からないのでスッキリしない。
2022年5月時点の事なので変わる可能性ありますが

まとめ

という事で多少の気になる点はあったものの
とりあえず公開まで問題なくいけたかなと思うので
もしレンタルサーバーでLaravelとかnextとかdeployerを。という機会がありましたら
エックスサーバーおすすめです。参考にしていただけましたら幸いでございます。

]]>
jsで画像のトリミング(回転とか拡大とかも) https://nocebo.jp/post-4345/ Wed, 11 May 2022 04:55:34 +0000 https://nocebo.jp/?p=4345 個人開発サイト

https://wordstacks.nocebo.jp/

にツイッターとかにあるような
プロフィール画像調整できるような機能追加
ちょっと自前で実装してみようかなー

って軽い気持ちで始めたら
思ってた3倍。いや5倍ぐらい面倒くさい作業になってしまった。

画像処理?とかゲーム作ってるようなのがメインの人
からしたら大した実装じゃないんだろうけど、

まぁ意地で頑張ってなんとか形にできたので忘れない為にも記事にしてみる。

Cropper.js

まず、なんでもいいから
トリミングができればいいですって人は

Cropper.js

とか使った方がいいと思う
記事にしといてなんだけど、自前で実装はおすすめしない。

けどCropper.jsはスマホとかのジェスチャーでの
回転とかスケーリングは対応してない?と思うので
そのあたりも実装しないと、って方には多少参考になるかもしれないです。

実装サンプル(codesandbox)


リンターとかなんか色々でてるけど
面倒いので動けばヨシ。としてる。

以降は実装のちょっとした補足とか

全部説明しようとすると
また結構時間かかるので個人的なポイントだけ

React

React使ってます。
でもメインの実装部分は
素のjsなのでReact以外でも適宜調整すれば
流用できるとは思う。

コンポーネント

src/components/Form/
以下でコンポーネント部分実装している

Image以下

フォーム部分はこのあたり

src/components/Form/Image/Root.tsx

(async () => {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();
  const blob = new Blob([arrayBuffer], {
    type: "image/jpeg",
  });

  const reader: FileReader = await readerOnLoadend(blob);
  setFile({
    file: new File([blob], "name"),
    previewUrl: reader.result,
  });
})();

ここはDBに画像がある場合の想定で、
あった場合には
画像からblob?に変換してfileにデータセットしてプレビューできるようにしている。

src/components/Form/Image/Files.tsx

“ファイルを選択”の部分
ドラッグ・ドロップでもfile読めるようにしてる。

src/components/Form/Image/Preview.tsx

モーダルじゃない普通の画像のプレビュー部分

Edit以下

src/components/Form/Image/Edit/Root.tsx

モーダル部分+デフォのジェスチャー無効にしたり
エスケープでモーダル閉じたりとか

Adjust以下

Root.tsx

スケールとかオリジンとかのstateは
UIからも変更できるように
このコンポーネントから子に渡してる

modules.tsx

リサイズ周りの処理がゴチャゴチャしたので
reducerとかでまとめてみたけど
お作法的にあってるかは知らない

Main.tsx

マウスとかタッチとかのイベント周り拾ったりするのがメイン

const dataInit = () => {
  return {
    isDown: false,
    start: [new Point()], // 始点(ジェスチャー用に配列で)
    current: [new Point()], // 現在値(ジェスチャー用に配列で)
    diff: new Point(), //start - currentの差分
    temp: new Point(), //ダウン時の前回値保持
    tempScale: 1,
    rotMinScale: 1,

    // ジェスチャー用
    tempRotate: 0,
    tempDeg: 0
  };
};
export let data = dataInit();

このあたりでマウスとかジェスチャーの移動値とかを保持しているけど
Reactで管理するとリレンダーかかりまくってよくないっぽいので
普通の?って言っていいのかわからんけど変数で管理。

Img.tsx

実際に操作する画像要素。

const transform = (target: any, size: any) => {
  const x = target.currentPos.x * size.base; // 正規化した数値からpx指定に
  const y = target.currentPos.y * size.base;
  const originX = target.origin.x * 100; // パーセント指定に*100
  const originY = target.origin.y * 100;
  return {
    transform: `translate(${x}px, ${y}px) rotate(${target.rotate}deg) scale(${target.scale})`,
    transformOrigin: `${originX}% ${originY}%`
  };
};

ここで位置とか回転とかスケーリングとかここで
cssに変換&useRefで直接スタイル更新してる。

ちなみにここもcanvasでいいかって、試しにやってみたら妙に重くなった覚えがあるので
画像の方が処理早い?のかも。ただの予想だけど

Canvas.tsx

ここで編集してる画像の位置とか回転とかの値取得して
オフスクリーンのcanvasに描画->canvasのblobをfileにセットって感じの流れです。

ctx.save();
ctx.translate(reference.x, reference.y);
ctx.rotate(radian);
ctx.scale(target.scale, target.scale);
ctx.drawImage(img, 0, 0, drw, drh);
ctx.restore();

このあたりで実際に描画してるけど
ctxの説明は canvas 描画 とかで調べれば色々出てくるので割愛。

Debug以下

はデバッグ用で非表示になってるけど、
display: none;
消せば色々値を表示したり頂点座標表示したりできるので
もしよければ試してください。
自分用なのでゴチャゴチャだけど。

utils以下

あとでutils以下でクラスとか関数とかまとめてるので補足。

point.ts

pointクラス、vectorクラスの方が命名的にはあってる?かもしれない。しらんけど。
距離とったり。補完点とったりで使う。

functions.tsx

ジェスチャーとかマウス関連の関数が多い、
ジェスチャーの座標を配列でとったり。
ジェスチャーから回転とかスケール値計算したりしてる。

あと他拾い物のコピペだけど
useAnimationFrameって
tick処理用のカスタムフックとかもここに書いてる。

RectTarget.tsx

ここがメインの実装部分かと思っている。

rotateMinScale

  public rotateMinScale(size: any): number {
    const radian = this.radian();
    let rot_w =
      size.viewH * Math.abs(Math.sin(radian)) +
      size.viewW * Math.abs(Math.cos(radian));
    let rot_h =
      size.viewH * Math.abs(Math.cos(radian)) +
      size.viewW * Math.abs(Math.sin(radian));
    // 背景画像かつ横長画像の場合の考慮
    if (this.isBg && this.sizeRatio.x > this.sizeRatio.y) {
      rot_w = rot_w * (1 / this.sizeRatio.x);
      rot_h = rot_h * (1 / this.sizeRatio.y);
    }
    this.minScale = Math.max(rot_w, rot_h);
    return this.minScale;
  }

回転するとエリアからはみ出す部分が出てくるので
回転によって最小スケール計算して
最小値以下にはスケーリングできないようにしている。

なんかちょっとスマートじゃない気もするけど
縦長だったり横長だったりの場合も対応するようにこんな感じになった。

diagonal

  private diagonal() {
    return {
      length: Math.sqrt(
        Math.pow(this.sizeRatio.x, 2) + Math.pow(this.sizeRatio.y, 2)
      ),
      radian: Math.atan(this.sizeRatio.y / this.sizeRatio.x),
      angle: Math.atan(this.sizeRatio.y / this.sizeRatio.x) * (180 / Math.PI)
    };
  }

対角線求めるやつです。
この後の編集する画像(矩形)の頂点座標求めたりに使ってます。

vertex

  // 矩形の各頂点を返す
  public vertex() {
    const dlength = this.diagonal().length * 0.5; //矩形対角線の長さの半分(中心点からの距離)
    const dradian = this.diagonal().radian; //矩形対角線のラジアン
    //----------------------------
    // 矩形中心を基準点とした頂点(左上から時計回り)の初期値
    let vertices = [
      new Point(
        Math.cos(Math.PI + dradian) * dlength,
        Math.sin(Math.PI + dradian) * dlength
      ),
      new Point(Math.cos(-dradian) * dlength, Math.sin(-dradian) * dlength),
      new Point(Math.cos(dradian) * dlength, Math.sin(dradian) * dlength),
      new Point(
        Math.cos(Math.PI - dradian) * dlength,
        Math.sin(Math.PI - dradian) * dlength
      ),
    ];
    //----------------------------
    // アフィン変換で回転・拡大・平行移動する
    // まとめて合成した方がいいんだろうけど一旦順にやる
    //----------------------------
    // 要素センターから左上(0,0)基準に移動
    vertices = vertices.map((p) => {
      return new Point(p.x + 0.5, p.y + 0.5);
    });
    // アフィン変換の基準点(cssのoriginは下にプラスなので注意)
    const reference = new Point(+this.origin.x, +this.origin.y);
    // 拡大縮小
    vertices = vertices.map((p) => {
      return new Point(
        (p.x - reference.x) * this.scale + reference.x,
        (p.y - reference.y) * this.scale + reference.y
      );
    });
    // 回転反映
    const radian = this.rotate * (Math.PI / 180);
    vertices = vertices.map((p) => {
      return new Point(
        (p.x - reference.x) * Math.cos(radian) +
          (p.y - reference.y) * -Math.sin(radian) +
          reference.x,
        (p.x - reference.x) * Math.sin(radian) +
          (p.y - reference.y) * Math.cos(radian) +
          reference.y
      );
    });
    // 移動値反映
    vertices = vertices.map((p) => {
      return new Point(p.x + this.targetPos.x, p.y + this.targetPos.y);
    });
    // イプシロン見にくいのでデバッグ用丸め込み
    vertices = vertices.map((p) => {
      return p.rounded();
    });
    return vertices;
  }

矩形の頂点座標求めるやつ、vertexっていいつつvertices返すのがなんか気持ち悪いかもだけどいいや。

アフィン変換

アフィン変換って存在を知らず
最初はどう計算すれば、というか、どう考えれば良いのか、
だけでかなりネットの海を彷徨ったので
これの存知ってるだけでだいぶ違うと思う。
昔の自分にアフィン変換使えばいいよって教えてあげたい。

昔の記事だけどこれもアフィン変換使った方がわかりやすいかったかも?と思っている。

アフィン変換は使ってないけど
近しい実装でちょっと詳しく説明とかもしているので
他の部分でも良く分からない箇所とかある場合は
こっちも参考にしてもらってもいいかもしれない。

inside 衝突(包含)判定

  // エリア内に収める
  public inside(): any {
    // 再帰で移動繰り返してループしちゃった場合は元の場所に
    if (this.insideLoop == 0) {
      this.tempPos = new Point(this.currentPos.x, this.currentPos.y);
    }
    if (this.insideLoop > 100) {
      this.insideLoop = 0;
      this.targetPos.x = this.tempPos.x;
      this.targetPos.y = this.tempPos.y;
      return;
    }
    this.insideLoop++;

    const radian = this.radian();
    const vertex = this.vertex();
    const frame = this.viewArea();

    // 衝突(包含)判定
    // https://yttm-work.jp/collision/collision_0007.html#head_line_03
    // centerの座標を原点として逆回転ってなってるけど別に原点(0,0)でいい気がする
    const vertex_rot = vertex.map((p) => {
      return new Point(
        p.x * Math.cos(-radian) + p.y * -Math.sin(-radian),
        p.x * Math.sin(-radian) + p.y * Math.cos(-radian)
      );
    });
    // 表示領域も逆回転
    const frame_rot = frame.map((p) => {
      return new Point(
        p.x * Math.cos(-radian) + p.y * -Math.sin(-radian),
        p.x * Math.sin(-radian) + p.y * Math.cos(-radian)
      );
    });

    const eps = Number.EPSILON;
    // 回転した座標で当たり(包含)判定
    // [0]:左上 [1]:右上 [2]:右下 [3]:左下
    const frame_bool = frame_rot.map((frame, index) => {
      let bool = true;
      const topLeft = vertex_rot[0];
      if (frame.x + eps < topLeft.x) bool = false; // 左
      if (frame.y + eps < topLeft.y) bool = false; // 上
      const bottomRight = vertex_rot[2];
      if (frame.x - eps > bottomRight.x) bool = false; // 右
      if (frame.y - eps > bottomRight.y) bool = false; // 下
      return bool;
    });

    const isInside =
      frame_bool[0] && frame_bool[1] && frame_bool[2] && frame_bool[3];

    // 判定自体はここまででOK
    this.isInside = isInside;

    // エリア外に出た場合はinsideMoveから再帰
    if (!isInside) {
      this.insideMove(vertex_rot, frame_rot);
    } else {
      this.insideLoop = 0;
    }

    return this.isInside;
  }

エリア内しか移動できないようにするやつ。
これもアフィン変換の応用?になると思う。

insideMove

  // ここではみ出した分移動制御用に数値取って収める
  private insideMove(vertex_rot: any, frame_rot: any): void {
    // console.log("insideMove");
    const radian = this.radian();
    // ゼロだと無限に再帰しちゃう時があるのでイプシロンで
    const eps = Number.EPSILON;
    // 差分
    const frame_diff: any = frame_rot.map((frame: any, index: any) => {
      const diff: any = {};
      const topLeft = vertex_rot[0];
      const bottomRight = vertex_rot[2];

      // 各辺との距離
      if (topLeft.x - frame.x > eps) {
        diff.left = topLeft.x - frame.x; // 左
      }
      if (topLeft.y - frame.y > eps) {
        diff.top = topLeft.y - frame.y; // 上
      }
      if (bottomRight.x - frame.x < -eps) {
        diff.right = bottomRight.x - frame.x; // 右
      }
      if (bottomRight.y - frame.y < -eps) {
        diff.bottom = bottomRight.y - frame.y; // 下
      }

      return diff;
    });

    // mapはbreakができないのでentries forで
    for (const [index, _frame] of Object.entries(frame_diff)) {
      const frame: any = _frame;
      if (frame.left) {
        const len = frame.left;
        const add = new Point(len * Math.cos(radian), len * Math.sin(radian));
        this.targetPos.x = this.targetPos.x - add.x;
        this.targetPos.y = this.targetPos.y - add.y;
        this.inside();
        break;
      }
      if (frame.right) {
        const len = frame.right;
        const add = new Point(len * Math.cos(radian), len * Math.sin(radian));
        this.targetPos.x = this.targetPos.x - add.x;
        this.targetPos.y = this.targetPos.y - add.y;
        this.inside();
        break;
      }
      if (frame.top) {
        const len = frame.top;
        const add = new Point(len * Math.sin(radian), len * Math.cos(radian));
        this.targetPos.x = this.targetPos.x + add.x;
        this.targetPos.y = this.targetPos.y - add.y;
        this.inside();
        break;
      }
      if (frame.bottom) {
        const len = frame.bottom;
        const add = new Point(len * Math.sin(radian), len * Math.cos(radian));
        this.targetPos.x = this.targetPos.x + add.x;
        this.targetPos.y = this.targetPos.y - add.y;
        this.inside();
        break;
      }
    }
  }

はみ出た場合にはみ出した(各辺との距離)値をaddする。
+addした分で他の辺でみ出す可能性があるので
再帰でinside呼び出す。って感じです。

単純にx,yにaddすると
回転してた場合におかしくなるので
各辺毎に上記みたいな処理になった。

以上です

クライアント仕事の合間縫ってで全く触れてない期間もあったりだけど
gitの履歴見ると一年前ぐらいからやってたみたい、
掛かった時間との費用対効果とかは考えたくない感じである。

もし実装のご依頼なんかございましたら連絡ください。

]]>
iOSとかの実機でPerfectPixel的な事やる https://nocebo.jp/post-4311/ Thu, 25 Feb 2021 01:27:32 +0000 https://nocebo.jp/?p=4311 ピクセルパーフェクトなんて言葉があるらしい。
デザイナーやチェックする方の意思だったりで
案件によってはかっちりpx単位で
デザインラフに合わせる必要が出てくる場合があります。

個人的にはそこまでやるの?って思っちゃう方で、
論争だったりにもなってるらしいけど、そこは置いといて。

まぁプロですから出来る限りはやりますとも。予算にもよるけど

で、まぁ細かく合わせようとすると
chrome拡張とかのPerfectPixel使うといいと思います。

https://chrome.google.com/webstore/detail/perfectpixel-by-welldonec/dkaagdgjmgdmbnecmcefdhjekcoceebi?hl=ja

大体はこれで合わせれば納得してくれる場合がほとんどだけど、

さらにiOSとかの実機での表示もpx単位でバックもらう事もあります。
ってなると拡張機能とか多分読めないので

どうしようかなって悩んだ末それっぽい拡張も無さそう?なので
自作でカンプ重ねるの作ってみました。

See the Pen ダブルクリックorタップでカンプ表示 by admin@nocebo.jp (@nocebojp) on CodePen.


やってることとしては
bodyにクラスつけたりで表示・非表示切替えてるだけです。

一応キーボードとダブルクリック/タップで変えれるようにしてみた。
キーボードの反応するボタンとかマススの長押しとかでもお好みで変えれば良いと思います。

]]>
jsでjsonの整形したのをhtmlのpreで整形表示する。 https://nocebo.jp/post-4300/ Fri, 29 Jan 2021 07:06:27 +0000 https://nocebo.jp/?p=4300 調べたら

JSON.stringify(localStorage, null, "\t")

ってのがすぐ見つかったんだけども

手元で作業中のはなんかこんな感じでバックスラッシュが入ってしまってみずらい。

{
	"isSessionFirst": "{\"value\":false}",
	"notAgain": "{\"value\":false}",
	"lastAccessedDate": "{\"value\":\"2021-01-29T07:02:25.896Z\"}",
	"isClose": "{\"expire\":1611932340000,\"value\":true}"
}

多次元だから?かはよくわからんが
下記みたいな感じでとりあえずバックスラッシュは取れた。

JSON.stringify(localStorage, null, "\t").replace(/\\/g, "")

が、このままだと二次元目以降がただの文字列になってしまっておりpreでの改行が反映されない。

のでなんとかしたいと思いあれこれやってみて
最終的にこんな感じに変換したら一応キレイに見れた。

const values = {};
const keys = Object.keys(localStorage);
let i = keys.length;
while (i--) {
  values[keys[i]] = JSON.parse(localStorage.getItem(keys[i]));
}
const data = JSON.stringify(values, null, "\t").replace(/\\/g, "");

]]>
vscodeでプロジェクト毎/特定ファイルのみ自動整形無効 https://nocebo.jp/post-4297/ Thu, 28 Jan 2021 01:20:41 +0000 https://nocebo.jp/?p=4297 vscodeで自動整形するprettierとか便利なんだけど
チェックする担当者によってはコードの細かいこだわりなんかもあったりで、
linterとか設定するまでも無いような時はサクッと自動整形OFFにしたい時もある。

プロジェクト毎

プロジェクトのworkspace毎の設定で変えられる。
ハズなのだけどもなんかsave on formatのチェック外しても整形走ってしまったので
なんで?って思ってたら

{
  "folders": [
    {
      "path": "."
    }
  ],
  "settings": {
    "editor.formatOnSave": false,
  }
}

こんな感じで手動で

“editor.formatOnSave”: false,

あてればoffになりました。

特定ファイル

といいつつ、自動整形になれてると快適でない。

今回はhtmlだけoffにできればよかったので
特定ファイルというか拡張子なのか言語なのか
はなんともだけども、.htmlだけ除外したい場合は

{
  "settings": {
    "[html]": {
      "editor.formatOnSave": false
    }
  }
}

こんな感じにすれば良いっぽい。

]]>
dropboxで同期したくないディレクトリignore https://nocebo.jp/post-4291/ Fri, 22 Jan 2021 02:18:20 +0000 https://nocebo.jp/?p=4291 いつのまにかdropboxで同期したくない
ディレクトリ選べるようになったぽい。

けど、わざわざアプリ側から個別に設定しないといけないっぽいので微妙にコレジャナイ感。
個人的にはgitignore的なもの求めていたんだけどなぁ

コマンドでも出来るっぽいので
node_modulesとか一括で出来ないかなとちょっと試したら多分出来てる?ような

find /Users/name/Dropbox/ -name "node_modules" -type d -prune | xargs -t xattr -w com.dropbox.ignored

最後のxargsの引数にスラッシュ付けたいんだけど、
パッと分からなくて諦めた。

]]>
Movable Typeのアップデートでやった事(MT4->MT7) https://nocebo.jp/post-4261/ Mon, 04 Jan 2021 05:39:39 +0000 https://nocebo.jp/?p=4261 Movable Typeアップデートの相談があったので
やってみたのですが思いの他結構面倒だったので
忘れないように復習がてら書いてみます。

自己流の部分結構あるので
公式とかに聞くのが一番かと思いますが、

MT4(sqlite2)->MT7のアップデート

の話です。作業はmacです。

サーバー上のデータをローカル環境に

レンタルサーバーのアップデートでperlが動かなくなったみたいなので
とりあえずリモートの公開データとかMTのデータを一式を一旦ローカルにもってきます。
今回相談受けた案件のリモートはディレクトリまるっとcgi使えるサーバーで

/公開ディレクトリ/mt/

って感じだったのだけど

ローカルはmampでやったので
MT自体のデータはmampの

Applications/MAMP/cgi-bin/mt/

ってな感じでいれました。
(途中dockerでやってみようかなとも思ったのだけども面倒くさくなってあきらめました。)

あと/cgi-bin/の方は
パーミッションも変更、ローカルなので適当に

find . -type d -exec chmod 755 {} +
find . -type f -exec chmod 750 {} +

こんな感じに

perlの準備

perlあんまり触る機会が無いのもあると思うけど
準備とかが必要な場合はここも面倒

とりあえず旧環境のMT4が動かないと何がなんやら、 な感じだったので

perlbrew

perlbrew入れる。
でperlbrewでバージョン落としたperlをインストール&切り替える
今回僕の場合はMT4なので5.8~とかにしました。

perlbrew switch perl-5.8.9

MT7用にも入れたり切り替えたりで色々入れる。

perlbrew list

shebang

上記のperlbrewの切り替えでいけるかと思ったけども

#!/usr/bin/env perl

とかだとmamp利用だからかも?ですが
macデフォのperl参照してしまい、ダメでした。

よく分からない&面倒だったので直パスに変更。

#!/Users/user/perl5/perlbrew/perls/perl-5.8.9/bin/perl -w

でとりあえずローカルでMT4にアクセスはできるようになりました。

モジュール類

ブラウザからインストールする上でモジュール関連が無いとか
色々エラーが出まくるので諸々入れます。

cpanm

モジュールインストールするにあたり
cpanmってやつ入れる。

perlbrew install-cpanm

多分依存関係とかやってくれるnpmとかcomposer的な物?だと思う。
でブラウザでエラーでたの全部入れちゃう。

cpanm DBI.pm
cpanm DBD::mysql
cpanm HTML::Entities
cpanm --force DBD::SQLite
cpanm --force DBD::SQLite2

とかだったと思う

DBD::SQLiteとかはMT4側ですでにmysql設定になっている場合はいらないと思います。

DBD::mysql

DBD::mysqlだけ
cpanm DBD::mysqlだけだと入らなかったので

cpanm DBD::mysql --configure-args="--libs='-L/usr/local/opt/openssl/lib -lssl -lcrypto -L/usr/local/lib -lmysqlclient'"

ってやればインストールできました。

big sur

big surにしたら色々動かなくなった。

基本的にはprelbrewで一回バージョン毎にアンインストール・もっかいインストールって手順で動いたんだけどもdbd::mysqlだけ上記でも入らなくなった。

なんとなくopensslのバージョン?とかが問題だったような気はしてる。試した事としては

一回opensslアンインストール

brew uninstall openssl

logとかに1.0.0が無い的なエラーがでてたので
brewでインストール。

しようと思ったら公式?では公開されなくなったみたいなので下記からインストール

brew install rbenv/tap/openssl@1.0

一応もっかい普通にinstallもした。

brew install openssl
PATH="$(brew --prefix mysql-client)/bin:$PATH"
export LIBRARY_PATH=$(brew --prefix openssl)/lib:$LIBRARY_PATH

パスの設定とかもあるのかも、
関係ないかもだけど。

どれやったから解消出来たのか不明

MT側

mt-config.cgiで設定等

mt-config.cgiをリネームとか削除してからブラウザでmt.cgiにアクセスすると
インストール画面になるので色々設定する。もしくはmt-config.cgiに
CGIPathとかStaticWebPathとかの設定直接書いてもよいと思います。

ログイン

上記まで多分MT4(sqlite2)のリモートの旧環境の再現が
できるのでログインできるハズ。
後は移行する。

サイトデータのエクスポート

システムメニューからエクスポート(sqlite版)する。

DB設定をmysqlに

mt-config.cgiでsqlite->mysqlに変更します。

ObjectDriver DBI::mysql
Database mt
DBUser root
DBPassword root
DBHost localhost

DBSocket /Applications/MAMP/tmp/mysql/mysql.sock
#(mamp の mysql に向かないので追加)

こんな感じ
今回ローカルはmampなので
DBSocketも設定必要でした。

その後さっきsqlite版からエクスポートした
データをインポートしてmysqlにデータ変換。

アップデート

あとは最新のMTにアップデートします。
僕の作業時はMT7でしたがこの辺は公式見た方が早いと思われる

https://www.movabletype.jp/documentation/mt7/installation/upgrade/upgrade/

あとはmtタグとかプラグインで色々調整必要な
箇所ありそうなのですがとりあえずここまで

]]>
Slickのslick activeの初期表示でCSSアニメーション https://nocebo.jp/post-4253/ Mon, 14 Dec 2020 02:29:22 +0000 https://nocebo.jp/?p=4253 結構ありそうなパターンだけど、
今まであんまり遭遇した事なくて
ちょっと困ったのでメモ。

Slick

カルーセルとかスライドの名プラグイン・モジュール。
jqueryが有名だと思うけど、
最近はvueとかReactに対応したのもあるので
カルーセルっていえばSlick使っちゃうタイプです。

SlickでCSSアニメーション

+αでちょっとしたアニメーション付加したいって要望とかでも
アクティブなスライドには.slick-activeってクラスがつくので
適当にCSS書けばアニメーション付けれます。

.slick-active {
  .elm {
    .txt {
      transition: all 1s linear 0s;
      opacity: 1;
    }
  }
}

こんな感じ。

初期表示でCSS transsion

が、どうも
初期表示というか一枚目のアニメーションが動かなかった。
なんぞ?って思ったら

どうも最初の初期処理は一枚目に最初っから.slick-activeがついているからか
transsionアニメがうまく動いていない様子。

$(".js-slick").slick({
  autoplay: true,
  arrows: false,
});
$(".slick-slide").removeClass("slick-active");
window.setTimeout(() => {
  $(".slick-slide").eq(0).addClass("slick-active");
}, 10);

こんな感じで一旦.slick-activeremoveしつつ
setTimeoutでちょっとだけ時間おいて再度.slick-active付加したら
無事動きました。

以上です。


]]>
deployer/composerで遭遇したエラー達 https://nocebo.jp/post-4225/ Wed, 09 Dec 2020 07:06:44 +0000 https://nocebo.jp/?p=4225 laravel使ってるサイト
久々deploy試してみたら
composerで色々エラーに遭遇したので
書いておく。

memory_limit

composerでmemory_limitのエラーが出た時は

COMPOSER_MEMORY_LIMIT=-1 composer install

的な事やればいいらしい

deployerでやる場合はどうすればいいのかなと思い、調べてみると

vendors.phpに

<?php
/* (c) Anton Medvedev <anton@medv.io>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Deployer;

desc('Installing vendors');
task('deploy:vendors', function () {
    if (!commandExist('unzip')) {
        writeln('<comment>To speed up composer installation setup "unzip" command with PHP zip extension https://goo.gl/sxzFcD</comment>');
    }
    run('cd {{release_path}} && {{bin/composer}} {{composer_options}}');
});

こんな記述があったので

deploy:vendorsのoverride

多分deploy.phpに書けばオーバーライドできるだろうと書いてみたらできた。

set('composer_options', 'install --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader');

task('deploy:vendors', function () {
    if (!commandExist('unzip')) {
        writeln('<comment>To speed up composer installation setup "unzip" command with PHP zip extension https://goo.gl/sxzFcD</comment>');
    }
    run('cd {{release_path}} && /usr/bin/php -d memory_limit=-1 /usr/local/bin/composer {{composer_options}}');
});

こんなかんじ

You are using an outdated version of Composer.

今度はこんなのが出た
多分Composer2.0にアップデートしなさいよ的な事だと思う。

なのでローカルとリモートでselfアップデートする

composer self-update

You are using the deprecated option “–no-suggest”

手元のバージョンのcomposer_optionsが

/usr/local/bin/composer install --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader

ってのがデフォルトみたいで
多分2.0でdeprecatedになってるのがエラー出た?ようなので
‘composer_options’もsetする。

上のdeploy.phpと同じだけどこの部分

set('composer_options', 'install --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader');

your PHP version does not satisfy that requirement.

今度はphpのバージョンが違うと怒られる。

laradock使っていて
laradockのphp-fpmだと7.1とか7.2とかまでしか指定出来ない?と思うので
どうしたもんかと思っていたら

composer.jsonでバージョン指定

composer.jsonに

~~
"config": {
        "platform": {
            "php": "7.1.30"
        }
    },
~~

こんな感じで書いとけばよくは分かってないけど
うまい事やってくれるっぽい。

php artisan package:discover –ansi / Undefined indexname

今度はこれでエラー。

laravel/frameworkのアップデート

なんかcomposer2.0以降でのエラーらしい。

laravel側アップデートすれば解消するとの事で
アップデートする。
各メジャーバージョンでパッチ的なのがあるあらしいので

5.8.30 -> 5.8.38

みたいな感じマイナー部分?だけアップデートする。
依存関係でアラートでたらその部分もアップデート必要。

で、無事deployできました。

]]>
バンドの練習風景撮ったのを webgl(three.js)で GPGPU 使いつつ curlnoise の particle で遊んでみました。 https://nocebo.jp/post-3924/ https://nocebo.jp/post-3924/#respond Fri, 06 Nov 2020 04:17:41 +0000 https://nocebo.jp/?p=3924 仕事だったり育児でなかなか集まれず、
どこか目指してる訳でもないですが続けていきたいなーと思ってるバンドの練習風景撮ったのをwebgl(three.js)で GPGPU とか使いつつ curlnoise で遊んでみました。

ちょっと時間に余裕があった時に気まぐれに始めたらなかなか時間がかかってしまった。

普段独り言みたいなツイートでほぼ反応ない
僕のツイッターにしては多少反応があったので記事書いてみました。

成果物等

URL

https://nocebo.jp/band/prs/public/

YOUTUBE

ソース

CodeSandbox

ごちゃごちゃだけどcodesandbox用意してみました。

ざっくり概要

ざっくり概要としては

  • 複数 video からランダムでビデオテクスチャ切り替え
  • 画角もランダムに切り取ってビデオ描画(背景ビデオ)
  • その上にビデオテクスチャから色情報参照したパーティクル(カールノイズ)も描画
  • 切り替えや動きはなんとなく音に合わせたタイミング

という感じです。
そこそこ時間かかったので自分で忘れない用に実装内容ある程度記事にしてみます。

ざっくり環境

React

React 歴浅いので多分だめな所いっぱいあります。

styled-components

スタイル系はstyled-components使ってます。

react-three-fiber

ReactThree 使うならコレがいいっよて情報みかけたので使ってます。
Three とか GLSL も趣味で触るぐらい。

Typescript

Typescript でやってますが、
個人的には Typescript 嫌い、linter もちょっと頑張ろうかなーって一瞬思ったけど、
ものの数分であきらめました。
遠慮なく any 使って、自分勝手に動けばいいやという感じなのであしからずです。

————–以降はソースの軽いおぼえ書き————–

目次的にちょっとおかしくなるけど以降はソース各所の軽いおぼえ書きです。

codesandboxだとtsconfigpathsが効かない?気がするので
相対で色々importしてます。

あとローカルだとshader-loaderとか使ってたけど
これもcodesandboxだと駄目っぽい?かも

なのでtsでテキストとしてimportしてます。

index

// =======================================
// メインの呼び出し元
// =======================================

import React, { createContext, useState, useRef } from "react";
import ReactDOM from "react-dom";

import { library } from "@fortawesome/fontawesome-svg-core";
import { fab } from "@fortawesome/free-brands-svg-icons";
import { far } from "@fortawesome/free-regular-svg-icons";
import { fas } from "@fortawesome/free-solid-svg-icons";

import { Reset } from "styled-reset";
import styled from "styled-components";
import "./style.scss";

import GL from "./gl/index";
import StreamLoader from "./media/streamLoader";
import Video from "./media/video";
import Audio from "./media/audio";
// import Recorder from "./media/recorder";
import UI from "./ui/";
import Data from "./data/";

library.add(fab, fas, far);
// console.log(process.env.NODE_ENV);
// =======================================
export const Conf: any = {
  VIDEO_TEXTURE_NUM: 5,
  GL_SIZE_FIX: false, //サイズ固定
  QUALITY: "HIGH",
  TEXTURE_WIDTH: 1920,
  TEXTURE_HEIGHT: 1080,
  PARTICLE_COUNT: 1920 * 1080,
  NORMAL: {
    TEXTURE_WIDTH: 1280,
    TEXTURE_HEIGHT: 720,
    PARTICLE_COUNT: 1280 * 720
  },
  HIGH: {
    TEXTURE_WIDTH: 1920,
    TEXTURE_HEIGHT: 1080,
    PARTICLE_COUNT: 1920 * 1080
  }
};
// =======================================
const Container = styled.div`
  width: 100%;
  height: 100%;
`;

const Root = styled.div`
  width: 100%;
  height: 100%;
`;
// =======================================

const obj: any = {};
export const ResourceContext = createContext([obj, () => {}]);

const Content: React.FC = () => {
  const [resource, setResource] = useState({
    videoState: "pause",
    isDataShow: false,
    canvas: null,
    audio: null
  });

  const videoRef = new Array(Conf.VIDEO_TEXTURE_NUM).fill(
    useRef<HTMLVideoElement>()
  );
  const [videoBuffer, setVideoBuffer] = useState<Uint8Array[]>();
  const [isLoaded, setIsLoaded] = useState<boolean>(false);
  const [isReady, setIsReady] = useState<boolean>(false);

  return !isLoaded ? (
    <StreamLoader setIsLoaded={setIsLoaded} setVideoBuffer={setVideoBuffer} />
  ) : (
    <Root>
      <Video
        videoRef={videoRef}
        videoBuffer={videoBuffer}
        setIsReady={setIsReady}
      />
      {isReady && (
        <ResourceContext.Provider value={[resource, setResource]}>
          <Data videoRef={videoRef} />
          <UI videoRef={videoRef} />
          <Audio videoRef={videoRef} />
          <GL videoRef={videoRef} />
          {/* {process.env.NODE_ENV === "development" && (
            <Recorder videoRef={videoRef} />
          )} */}
        </ResourceContext.Provider>
      )}
    </Root>
  );
};
// =======================================
ReactDOM.render(
  <Container>
    <Reset />
    <Content />
  </Container>,
  document.getElementById("root")
);
// --------------------

indexです。呼び出し元
全体的なstyleとかfontawesome読み込んだり、
あとクオリティ設定とかできるようにしてみた。

あとなんとなくuseContext使ってみたくて入れてみた、が
思ったようなタイミングでsetできなかたりするので使い方間違ってるかも?
useReducer使わないと?なのか?

useContextで利用する部分のメインはRecorderなのだけども
Recorderは手元のローカルで使うだけで、
見るだけなら不要なので多分問題ないはず
コメントアウトしてるけど、一応どういった内容かは追って軽く覚えがき入れる予定。

reduxは使うと結構大げさになっちゃう気がするのでもうちょい暇な時にみてみよう、とは思っている。

data

// =======================================
// データ確認表示用
// あとアップデート値(updateValue)の
// tick処理もここでやっちゃってる
// =======================================

import React, { useEffect, useCallback, useContext, useState, useRef } from "react";
import { ResourceContext, Conf } from "../index";
import styled from "styled-components";

const Root = styled.div`
  position: fixed;
  z-index: 1000;
  top: 20px;
  left: 20px;
  background-color: rgba(0, 0, 0, 0.5);
  color: #fff;
  width: 300px;

  padding: 10px;
  opacity: 0.5;
  line-height: 1.4;
  border-radius: 10px;
  box-sizing: border-box;
`;

const BPM_NUM = 1.565;
const START_TIME = 5.0;

export const updateValue: any = {
  videoState: "pause",
  videoCurrentTime: 0,
  videoTempTime: 0,
  duration: 0,

  renderState: false,

  bar: 0,
  beat: 0,
  beatState: false,
  barState: false,
  lim01State: false,
  lim02State: false,
  kick01State: false,
  kick02State: false,
  copyTexState01: false,
  copyTexState02: false,
  copyTexState03: false,
  copyTexState04: false,

  videoId: 0,
  range: 1.0,
  originX: 0.0,
  originY: 0.0,
  zoom: 0.0,

  seed: Math.random() * 1.0,
  copyTex: false,
  bgOpacity: 1.0,
  referenceColor: 1.0,
  referenceVelocity: 1.0,
  delta: 0.02,
  lim: 0.0,
  blurStrength: 0.0,
  flow01: 1.0,
  flow02: 1.0,
  flow03: 1.0,
  flow04: 1.0,
  flow05: 1.0,
  prev: {},
};

const swicher = () => {
  // ビデオテクスチャ切り替え
  if (updateValue.bar === 28) return;

  // 前回のと重複しないように
  let nextId = Math.floor(Math.random() * Conf.VIDEO_TEXTURE_NUM);
  do {
    nextId = Math.floor(Math.random() * Conf.VIDEO_TEXTURE_NUM);
  } while (updateValue.videoId === nextId);
  updateValue.tempId = updateValue.videoId;
  updateValue.prev = { ...updateValue };
  updateValue.videoId = nextId;
  // ------
  // 拡大位置・スケール
  const range = Math.random() * 0.4 + 0.3;
  const originX = Math.random() * (1.0 - range);
  const originY = Math.random() * (1.0 - range);
  updateValue.range = range;
  updateValue.originX = originX;
  updateValue.originY = originY;
  updateValue.zoom = 0.085;
};

const clear = () => {
  updateValue.lim01State = false;
  updateValue.lim02State = false;
  updateValue.kick01State = false;
  updateValue.kick02State = false;
  updateValue.copyTexState01 = false;
  updateValue.copyTexState02 = false;
  updateValue.copyTexState03 = false;
  updateValue.copyTexState04 = false;
  updateValue.bgOpacity = 1.0;
  updateValue.referenceColor = 1.0;
  updateValue.referenceVelocity = 1.0;
  updateValue.blurStrength = 0.0;
};

const perticleMotion = (barNum: number, beatNum: number) => {
  //  パーティクル止めるやつ
  if (updateValue.videoTempTime === updateValue.videoCurrentTime) return;

  // 再生時
  const n = 0.0;
  const rc = 0.06;
  const bo = 0.06;

  // // 少し前のバッファ保存
  if (beatNum > n - 0.02 && !updateValue.copyTexState01) {
    updateValue.copyTexState01 = true;
    updateValue.copyTex = true;
  }
  // 止めるタイミング
  if (beatNum > n && !updateValue.copyTexState02) {
    updateValue.copyTexState02 = true;
    updateValue.bgOpacity = 0.0;
    updateValue.referenceColor = 0.0;
    updateValue.referenceVelocity = 0.0;
  }
  if (beatNum > 0.1) {
    updateValue.referenceColor += rc;
  }
  // 戻すタイミング
  if (beatNum > 0.23 && !updateValue.copyTexState03) {
    updateValue.copyTexState03 = true;
    updateValue.referenceVelocity = 1.0;
  }
  if (beatNum > 0.23 && beatNum < 1.4) {
    updateValue.bgOpacity += bo;
  }
  if (beatNum > 0.5) {
  }
  // リム2
  if (barNum >= 36) {
    if (!(barNum >= 68 && barNum < 76)) {
      if (beatNum > 1.4) {
        updateValue.bgOpacity = 0.0;
      }
    }
  }
};

const Data: React.FC<any> = (props) => {
  const [resource] = useContext(ResourceContext);
  const videoRef = props.videoRef[1];
  // ----------------------
  // render表示用
  // ----------------------
  const [bar, setBar] = useState(0);
  const [time, setTime] = useState<number>(0);
  const [millisecond, setMillisecond] = useState<number>(0);

  //   -----------------------
  //   update
  //   -----------------------
  const requestRef = useRef<any>();
  const tick = useCallback(() => {
    requestRef.current = requestAnimationFrame(tick);

    // onTimeUpdateだとchromeで?間隔が遅いので 自前tick処理でcurrentTime設定
    const time = videoRef.current.currentTime;
    updateValue.videoTempTime = updateValue.videoCurrentTime;
    updateValue.videoCurrentTime = time;

    setTime(updateValue.videoCurrentTime);
    setMillisecond(Math.abs((updateValue.videoCurrentTime - START_TIME) % BPM_NUM));

    if (time < START_TIME) return;

    // bar 頭 // beat 小節
    const beatNum = (updateValue.videoCurrentTime - START_TIME) % BPM_NUM;
    const barNum = Math.floor((updateValue.videoCurrentTime - START_TIME) / BPM_NUM);

    // 終盤まで再生で一旦クリア
    if (barNum >= 108) {
      updateValue.barState = false;
      clear();
      return;
    }

    // 小節頭
    if (Math.floor(beatNum) === 0 && !updateValue.barState) {
      updateValue.barState = true;
      updateValue.seed = Math.random() * 1.0;
      clear();
      setBar(barNum);
      swicher();
    }

    // 2小節目
    if (Math.floor(beatNum) === 1) {
      updateValue.barState = false;
    }

    // キック2
    if (beatNum > 1.2 && !updateValue.kick02State) {
      updateValue.kick02State = true;
      updateValue.blurStrength = Math.random() * 0.2 + 0.1;
      barNum >= 84 && swicher();
    }

    // リム1
    if (barNum >= 36) {
      if (!(barNum >= 68 && barNum < 76)) {
        if (beatNum > 0.5 && !updateValue.lim01State) {
          updateValue.lim01State = true;
          updateValue.lim = 1.0;
          updateValue.flow02 = 1.0;
          updateValue.seed = Math.random() * 1.0;
        }
      }
    }

    // リム2
    if (barNum >= 36) {
      if (!(barNum >= 68 && barNum < 76)) {
        if (beatNum > 1.4 && !updateValue.lim02State) {
          updateValue.lim02State = true;
          updateValue.lim = 1.0;
          updateValue.speedToggle = 1.0;
        }
      }
    }

    perticleMotion(barNum, beatNum);
  }, [setTime, setMillisecond, videoRef]);
  useEffect(() => {
    requestRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(requestRef.current);
  }, [tick]);

  // ----------------------
  // render
  // ----------------------
  return resource.isDataShow ? (
    <Root>
      <p>videoState: {resource.videoState} </p>
      <p>currentTime: {time} </p>
      <p>bar: {bar}</p>
      <p>Millisecond: {millisecond.toFixed(3)} </p>
    </Root>
  ) : null;
};

export default Data;

ここで毎フレームアップデートする数値とか色々まとめてます。
あとそれの確認用の数値とかの表示も
Reactstate とかで管理すると無駄に再レンダリングはしっちゃう?と思うので
(なんかそれっぽい hook とかあるかもですが)
素で export したupdateValueってのを各所で参照するような作りです。

元はaudioの波形データに合わせて変化付けようと思ってたけども
動画の音素材的にFFTやってもイマイチ描画に使えなさそうな気がしたので
videocurrentTimeから小節とか拍的なもの計算して若干無理やり変化つけてます。

ifネストだらけなのはご愛嬌

videoのontimeupdate

コメントにも書いてるけど、videoontimeupdateだと
eventの間隔が結構遅めなので
自前でrequestAnimationFrame処理入れてます。

gl

// =======================================
// threeの呼び出し元
// =======================================
import React, {
  useRef,
  useState,
  useCallback,
  useEffect,
  useContext
} from "react";
import { Vector3, PerspectiveCamera } from "three";
import { Canvas, useThree } from "react-three-fiber";
import styled from "styled-components";
import Compute from "./compute";
import Scene from "./scene";
import Particles from "./particles/index";
import Effect from "./effect/";
import { Conf } from "../index";
import { ResourceContext } from "../index";

const Root = styled.div`
  position: absolute;
  background-color: #fff;
  background-color: #000;
  z-index: 100;
  width: 100vw;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  canvas {
    width: 100vw;
    height: calc(100vw * 9 / 16);
  }
`;

const Camera = () => {
  const ref = useRef<PerspectiveCamera>();
  const { setDefaultCamera } = useThree();
  useEffect(() => {
    if (!ref.current) return;
    setDefaultCamera(ref.current);
    ref.current.lookAt(new Vector3(0, 0, 0));
  }, [ref, setDefaultCamera]);

  return (
    <perspectiveCamera
      position={[0, 0, 10]}
      args={[45, 16 / 9, 1, 2000]}
      ref={ref}
    />
  );
};

// -------------------------------

const GL: React.FC<any> = (props) => {
  const [resource, setResource] = useContext(ResourceContext);
  const { videoRef } = props;
  const [canvas, setCanvas] = useState<any>();

  const [canvasSize, setCanvasSize] = useState({
    width: Conf.GL_SIZE_FIX ? Conf.TEXTURE_WIDTH : window.innerWidth,
    height: Conf.GL_SIZE_FIX
      ? Conf.TEXTURE_HEIGHT
      : window.innerWidth * (9 / 16)
  });
  const resize = useCallback(() => {
    setCanvasSize({
      width: Conf.GL_SIZE_FIX ? Conf.TEXTURE_WIDTH : window.innerWidth,
      height: Conf.GL_SIZE_FIX
        ? Conf.TEXTURE_HEIGHT
        : window.innerWidth * (9 / 16)
    });
  }, []);

  useEffect(() => {
    window.addEventListener("resize", resize);
    return () => window.removeEventListener("resize", resize);
  });

  const res = useCallback(() => {
    setResource({
      ...resource,
      canvas: canvas
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [resource, setResource, canvas]);

  useEffect(() => {
    // res();// なんかこうしないresourceに反映されない
  }, [canvas, res]);

  const setCanvasRef = useCallback(
    (node: any) => {
      if (node) setCanvas(node.getElementsByTagName("canvas")[0]);
    },
    [setCanvas]
  );

  const [compute, setCompute] = useState<any>({
    isSet: false
  });

  return (
    <Root ref={setCanvasRef} className="GL">
      <Canvas
        gl={{ preserveDrawingBuffer: true }}
        style={{ width: canvasSize.width, height: canvasSize.height }}
      >
        <Camera />
        <Compute
          videoRef={videoRef}
          compute={compute}
          setCompute={setCompute}
        />
        <Scene compute={compute} />
        <Particles compute={compute} />
        <Effect />
      </Canvas>
    </Root>
  );
};
export default GL;
// -------------------------------

three の 呼び出し元です。
ウィンドウに合わしてのリサイズ処理とか、カメラ配置したり、要素配置したり、等。
多分特別な事はしてないけど
react-three-fiber使ってっていうのが
あんまり情報が少なかった気がするのでなんか面倒くさかった気はする。

CanvasRef

react-three-fiberでcanvasのdom要素を取得する方法がよく分からなかったので
結構無理くりかもだけど、

node.getElementsByTagName("canvas")[0]

って感じで取れました。

preserveDrawingBuffer

preserveDrawingBufferってのをtrueにしないと
canvas.toDataURL(“image/jpeg”);
ができなかった。気がする。

compute

// =======================================
// GPGPU的なものは基本ここでまとめて処理
// =======================================
import React, { useRef, useEffect, useCallback } from "react";
import * as THREE from "three";
import { useFrame, useThree } from "react-three-fiber";

// npmからだとバージョンの問題っぽいのか何かエラーでちゃうので別途配置
// import { GPUComputationRenderer } from "gpucomputationrender-three";
import { GPUComputationRenderer } from "../../vendor/GPUComputationRenderer.js";
import { snoise } from "./shader/snoise"; // シンプレックスノイズ + カールノイズとか追記
import { PositionShader } from "./shader/position";
import { VelocityShader } from "./shader/velocity";
import { ColorShader } from "./shader/color";
import { MonoShader } from "./shader/mono";
import { RbgShader } from "./shader/rgb";
import { TempColorShader } from "./shader/color_temp";
import { TempPositionShader } from "./shader/position_temp";
import { updateValue } from "../../data";
import { Conf } from "../../index";

const Compute: React.FC<any> = (props) => {
  const { compute, setCompute, videoRef } = props;
  const { gl } = useThree(); // glってのがrenderer
  // gl.setPixelRatio(window.devicePixelRatio);// retinaだとここでテクスチャずれるかも

  const elapsedTime = useRef<any>(0);

  const videoTexture: THREE.VideoTexture[] = new Array(Conf.VIDEO_TEXTURE_NUM)
    .fill(0)
    .map((v: any, i: any) => new THREE.VideoTexture(videoRef[i].current));

  useEffect(() => {
    const computeRenderer = new GPUComputationRenderer(
      Conf.TEXTURE_WIDTH,
      Conf.TEXTURE_HEIGHT,
      gl
    );

    const textureArraySize = Conf.TEXTURE_WIDTH * Conf.TEXTURE_HEIGHT * 4;

    const tempColor = computeRenderer.createTexture();
    const color = computeRenderer.createTexture();
    const mono = computeRenderer.createTexture();
    const tempPosition = computeRenderer.createTexture();
    const position = computeRenderer.createTexture();
    const rgb = computeRenderer.createTexture();
    const velocity = computeRenderer.createTexture();

    // postion初期位置
    for (let i = 0; i < textureArraySize; i += 4) {
      position.image.data[i] = Math.random() * 16.0 - 8.0;
      position.image.data[i + 1] = Math.random() * 9.0 - 4.5;
      position.image.data[i + 2] = Math.random() * 6.0 - 3.0;
    }

    // initで作った配列渡す
    const textureTempColor = computeRenderer.addVariable(
      "textureTempColor",
      `${TempColorShader}`,
      tempColor
    );
    const textureColor = computeRenderer.addVariable(
      "textureColor",
      `${snoise} ${ColorShader}`,
      color
    );
    const textureMono = computeRenderer.addVariable(
      "textureMono",
      `${MonoShader}`,
      mono
    );
    const textureTempPosition = computeRenderer.addVariable(
      "textureTempPosition",
      `${TempPositionShader}`,
      tempPosition
    );
    const texturePosition = computeRenderer.addVariable(
      "texturePosition",
      `${snoise} ${PositionShader}`,
      position
    );
    const textureRgb = computeRenderer.addVariable(
      "textureRgb",
      `${snoise} ${RbgShader}`,
      rgb
    );
    const textureVelocity = computeRenderer.addVariable(
      "textureVelocity",
      `${snoise} ${VelocityShader}`,
      velocity
    );

    // テクスチャ参照の依存関係を設定
    computeRenderer.setVariableDependencies(textureTempColor, [
      texturePosition,
      textureColor,
      textureTempColor
    ]);
    computeRenderer.setVariableDependencies(textureColor, [
      texturePosition,
      textureColor,
      textureTempColor
    ]);
    computeRenderer.setVariableDependencies(textureMono, [textureMono]);
    computeRenderer.setVariableDependencies(texturePosition, [
      texturePosition,
      textureVelocity
    ]);
    computeRenderer.setVariableDependencies(textureTempPosition, [
      texturePosition,
      textureTempPosition
    ]);
    computeRenderer.setVariableDependencies(textureRgb, [
      textureRgb,
      textureMono
    ]);
    computeRenderer.setVariableDependencies(textureVelocity, [
      texturePosition,
      textureVelocity
    ]);

    textureTempColor.material.uniforms = {
      copyTex: { value: 0.0 }
    };
    textureColor.material.uniforms = {
      time: { value: 0.0 },
      vTex: { value: videoTexture[1] }, //ビデオテクスチャ
      originX: { value: 0.0 }, //切り取る画角の基点X
      originY: { value: 0.0 }, //切り取る画角の基点Y
      range: { value: 0.0 }, //切り取る範囲
      zoom: { value: 0.0 } //ズーム用
    };
    textureMono.material.uniforms = {
      time: { value: 0.0 },
      vTex: { value: 0.0 },
      range: { value: 0.0 },
      originX: { value: 0.0 },
      originY: { value: 0.0 },
      zoom: { value: 0.0 },
      blurStrength: { value: 0.0 }
    };
    textureTempPosition.material.uniforms = {
      copyTex: { value: 0.0 }
    };
    texturePosition.material.uniforms = {
      time: { value: 0.0 }
    };
    textureRgb.material.uniforms = {
      time: { value: 0.0 }
    };
    textureVelocity.material.uniforms = {
      time: { value: 0.0 },
      seed: { value: 0.0 },
      flow01: { value: 0.0 },
      flow02: { value: 0.0 },
      referenceVelocity: { value: 0.0 },
      lim: { value: 0.0 },
      renderState: { value: 0.0 }
    };

    const computeRendererError = computeRenderer.init();
    if (computeRendererError) {
      console.error("ERROR", computeRendererError);
    }

    setCompute({
      isSet: true,
      computeRenderer: computeRenderer,
      videoTexture: videoTexture,
      texturePosition: texturePosition,
      textureVelocity: textureVelocity,
      textureTempPosition: textureTempPosition,
      textureColor: textureColor,
      textureMono: textureMono,
      textureRgb: textureRgb,
      textureTempColor: textureTempColor
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // ----------------------
  const update = useCallback(() => {
    // ---------------
    compute.computeRenderer.compute();
    // ---------------
    if (compute.textureVelocity.material.uniforms.renderState.value === 1.0) {
      updateValue.flow01 =
        updateValue.flow01 <= 0 ? 0.001 : updateValue.flow01 - 0.095;
      updateValue.flow02 =
        updateValue.flow02 <= 0.03 ? 0.001 : updateValue.flow02 - 0.03;
      updateValue.zoom =
        updateValue.zoom <= 0.009 ? 0.009 : updateValue.zoom - 0.04;
      updateValue.lim = updateValue.lim <= 0.0 ? 0.0 : updateValue.lim - 0.04;
      updateValue.referenceColor =
        updateValue.referenceColor >= 1.0 ? 1.0 : updateValue.referenceColor;
      updateValue.bgOpacity =
        updateValue.bgOpacity >= 1.0 ? 1.0 : updateValue.bgOpacity;

      if (updateValue.kick02State) {
        updateValue.blurStrength =
          updateValue.lim >= 1.5 ? 1.5 : updateValue.blurStrength + 0.01;
      }
    }

    // ---------------
    compute.texturePosition.material.uniforms.time.value = elapsedTime.current;
    // ---------------
    compute.textureVelocity.material.uniforms.time.value = elapsedTime.current;
    compute.textureVelocity.material.uniforms.seed.value = updateValue.seed;
    compute.textureVelocity.material.uniforms.flow01.value = updateValue.flow01;
    compute.textureVelocity.material.uniforms.flow02.value = updateValue.flow02;
    compute.textureVelocity.material.uniforms.referenceVelocity.value =
      updateValue.referenceVelocity;
    compute.textureVelocity.material.uniforms.speedToggle.value =
      updateValue.speedToggle;
    compute.textureVelocity.material.uniforms.lim.value = updateValue.lim;
    // ---------------
    compute.textureColor.material.uniforms.time.value = elapsedTime.current;
    compute.textureColor.material.uniforms.vTex.value =
      videoTexture[updateValue.videoId];
    compute.textureColor.material.uniforms.originX.value = updateValue.originX;
    compute.textureColor.material.uniforms.originY.value = updateValue.originY;
    compute.textureColor.material.uniforms.range.value = updateValue.range;
    compute.textureColor.material.uniforms.zoom.value = updateValue.zoom;
    // ---------------
    compute.textureMono.material.uniforms.time.value = elapsedTime.current;
    compute.textureMono.material.uniforms.vTex.value =
      compute.videoTexture[updateValue.videoId];
    compute.textureMono.material.uniforms.range.value = updateValue.range;
    compute.textureMono.material.uniforms.originX.value = updateValue.originX;
    compute.textureMono.material.uniforms.originY.value = updateValue.originY;
    compute.textureMono.material.uniforms.blurStrength.value =
      updateValue.blurStrength;
    // ---------------
    compute.textureRgb.material.uniforms.time.value = elapsedTime.current;
    // ---------------

    if (updateValue.copyTex) {
      updateValue.copyTex = false;
      compute.textureTempColor.material.uniforms.copyTex.value = 1.0;
      compute.textureTempPosition.material.uniforms.copyTex.value = 1.0;
    } else {
      compute.textureTempColor.material.uniforms.copyTex.value = 0.0;
      compute.textureTempPosition.material.uniforms.copyTex.value = 0.0;
    }
    // ---------------
    compute.textureVelocity.material.uniforms.renderState.value = 0.0;
    // コマ送り用の対策
  }, [compute, videoTexture]);

  useFrame((frame) => {
    // elapsedTime.current = frame.clock.elapsedTime;

    update();
    if (updateValue.videoState === "play") {
      compute.textureVelocity.material.uniforms.renderState.value = 1.0;
      // なんかこうしないとレンダリングのコマ送りで
      // computeRendererがうまく動かない?ので
      // (updateはし続けてベロシティ係数を0.0,1.0でトグルしてる)
    }
    if (updateValue.renderState) {
      elapsedTime.current += 0.03;
      compute.textureVelocity.material.uniforms.renderState.value = 1.0;
      updateValue.renderState = false;
    }
  });

  return <></>;
};

export default Compute;

GPGPU 的な処理はここにまとめてます。
three 側の変化はほぼここでつけて他の箇所からはこの compute 参照するだけ。

react-three-fiberでTHREE.WebGLRenderer

react-three-fiberだと

const { gl } = useThree();

ってやればTHREE.WebGLRendererが取れるっぽい。

ちなみにsetPixelRatio設定すると
retinaの対応でshader側の調整も必要そうな感じだったので面倒なので無しにした。

GPUComputationRenderer

用語的にあってるかすら自信ないけど、GPGPU で色々計算すると早い、はず。GPUComputationRenderer っていうの使うと割と手軽にできました。

ライブラリ無しでやると
レンダーバッファのフリップとかが必要だったような?覚えがあるような?ないような。

コメントにも書いてるけど、npm で入れるとなんかエラーが出たので別途手動で配置してます。
多分バージョンとかの問題だとは思うけども、地味に苦戦した覚えがある。

const computeRenderer = new GPUComputationRenderer(Conf.TEXTURE_WIDTH, Conf.TEXTURE_HEIGHT, gl);

って感じで前述のTHREE.WebGLRenderer(gl)を渡す。

余談だけどaudio の波形なんかも GPU でなんか色々できるらしい。
今後なんかやってみようかな。

useFrame

react-three-fiber だとuseFrame で毎フレームの処理できる様子。

shader

glsl も色々自信ない部分が多いし散らかってるけど
一応なんとなくコメントは入れてる。

■snoise(とcurlnoise)

カールノイズと Simplex noise一緒に書いてる。

https://al-ro.github.io/projects/curl/

何となくこのあたり参考にはしました。

vec3 curlNoise(vec3 p) {
  const float e = .1;
  const float e2 = 2.0 * e;

  vec3 dx = vec3(e, 0.0, 0.0);
  vec3 dy = vec3(0.0, e, 0.0);
  vec3 dz = vec3(0.0, 0.0, e);

  vec3 p_x0 = snoiseVec3(p - dx);
  vec3 p_x1 = snoiseVec3(p + dx);
  vec3 p_y0 = snoiseVec3(p - dy);
  vec3 p_y1 = snoiseVec3(p + dy);
  vec3 p_z0 = snoiseVec3(p - dz);
  vec3 p_z1 = snoiseVec3(p + dz);

  float x = (p_y1.z - p_y0.z) - (p_z1.y - p_z0.y);
  float y = (p_z1.x - p_z0.x) - (p_x1.z - p_x0.z);
  float z = (p_x1.y - p_x0.y) - (p_y1.x - p_y0.x);

  return normalize(vec3(x, y, z) / e2);
}

発散とか内積とか回転とか色々よくはわかってないけど
一応何となく分かったような気がしてるのは

epsNumber.EPSILONって意味だと思う、偏微分?のほんの少しだけの数値

dxとかdyってのがxyz上のepsで少しだけ動かす、の値。

3点近似でなんやかんやして回転?を計算する。

と、あってる自信もないし適当なので参考にならないかもですが。

■position/velocity

パーティクルの動き周りでいうとここがメインです。
position の位置からカールノイズで速度を取得、
velocity に反映->position に反映。の繰り返し。

position は 16:9 の枠に収まるように適当な位置に更新している
が今気づいたけど、はZ軸考慮しないといけない?かも
けどパッと見は個人的に気にならないので良しとする

■color/mono

ビデオテクスチャ切り取りとりつつ
色情報とったりってのはここでやってます。
monoはモノクロ処理したやつ+何となく雰囲気でgaussianブラーかけてる

■rgb

rgb シフト拾ってきやつ、にsimplexnoiseで揺らぎ的なもの加えてみてます。
EffectComposer でやってもいいような処理かもですが
なんとなくパーティクルと背景ビデオで分けたかったのでここに配置

■color_temp/postion_temp

ここは位置・色情報コピーして一旦保持しつつ
参照切り替えて一瞬停止したっぽい演出したくてやってみました。

effect

// =======================================
// effectComposerはここで
// =======================================

import React, { useRef, useEffect } from "react";
import { useFrame, useThree } from "react-three-fiber";
import { extend } from "react-three-fiber";

import * as THREE from "three";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass";
import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass";
import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader";
import { SSAOPass } from "three/examples/jsm/postprocessing/SSAOPass";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass";

extend({ EffectComposer, RenderPass, ShaderPass, FXAAShader, SSAOPass, UnrealBloomPass });

// インターフェイスIntrinsicElementsにorbitControls の定義を追加
// typescriptめんどい
declare global {
  namespace JSX {
    interface IntrinsicElements {
      effectComposer: any;
      renderPass: any;
      shaderPass: any;
      unrealBloomPass: any;
    }
  }
}

const Effect: React.FC<any> = (props) => {
  const { gl, scene, camera, size } = useThree();
  const composer: any = useRef();

  useFrame(() => {
    if (!composer.current) return;
    composer.current.render();
  }, 30);
  useEffect(() => {
    composer.current.setSize(size.width, size.height);
  });

  const bloom: any = {
    resolution: new THREE.Vector2(size.width, size.height),
    strength: 0.5,
    radius: 0.01,
    threshold: 0.3,
  };

  return (
    <effectComposer ref={composer} args={[gl]}>
      <renderPass attachArray="passes" scene={scene} camera={camera} />
      <unrealBloomPass attachArray="passes" args={[bloom.resolution, bloom.strength, bloom.radius, bloom.threshold]} />
    </effectComposer>
  );
};

export default Effect;

EffectComposer はココ
Typescript + react-three-fiber だと
IntrinsicElementsinterface 定義がないと
駄目っぽい、めんどくさい。

なんで Typescript 普及してるのか謎。
っていいつつ流れには逆らえないので頑張って使う。

particles

パーティクルの vertices の座標とか、
初期処理がほとんど。で前述だけど更新はほとんど compute 側でやってる

// =======================================
// Particles周り
// 動きは基本compute側で処理したの参照するだけ
// =======================================

import React, { useRef, useMemo, useEffect } from "react";
import * as THREE from "three";
import { useFrame } from "react-three-fiber";
import { FragmentShader } from "./shader/fragment";
import { VertexShader } from "./shader/vertex";
import { updateValue } from "../../data";
import { Conf } from "../../index";

const Particles: React.FC<any> = (props) => {
  //   console.log("Particles");
  const material: any = useRef();
  const mesh: any = useRef();
  const { compute } = props;
  // ==================================================================

  // オブジェクトの初期頂点とかUV座標の設定
  const particles = useMemo(() => {
    const vertices = new Float32Array(Conf.PARTICLE_COUNT * 3).fill(0);
    // vertices初期位置は0
    const uv = new Float32Array(Conf.PARTICLE_COUNT * 2);
    for (let i = 0; i < uv.length; i += 2) {
      const indexVertex = i / 2;
      uv[i] = (indexVertex % Conf.TEXTURE_HEIGHT) / Conf.TEXTURE_WIDTH;
      uv[i + 1] =
        Math.floor(indexVertex / Conf.TEXTURE_WIDTH) / Conf.TEXTURE_HEIGHT;
    }
    // uv座標
    // const colors = new Float32Array(Conf.PARTICLE_COUNT * 3).fill(0);// 色情報
    return {
      vertices: vertices,
      uv: uv,
      //   colors: colors,
      uniforms: {
        time: { value: 0.0 },
        lim: { value: 0.0 },
        bgOpacity: { value: 0.0 },
        referenceColor: { value: 0.0 },
        resolution: {
          value: new THREE.Vector2(Conf.TEXTURE_WIDTH, Conf.TEXTURE_HEIGHT)
        },
        texturePosition: { value: null },
        textureColor: { value: null },
        textureTempColor: { value: null },
        textureTempPosition: { value: null }
      },
      VertexShader: VertexShader,
      FragmentShader: FragmentShader
    };
  }, []);

  // ==================================================================
  useFrame((frame) => {
    if (material.current) return;
    // ===============
    particles.uniforms.lim.value = updateValue.lim;
    particles.uniforms.bgOpacity.value = updateValue.bgOpacity;
    particles.uniforms.referenceColor.value = updateValue.referenceColor;
    particles.uniforms.texturePosition.value = compute.computeRenderer.getCurrentRenderTarget(
      compute.texturePosition
    ).texture;
    particles.uniforms.textureColor.value = compute.computeRenderer.getCurrentRenderTarget(
      compute.textureColor
    ).texture;
    particles.uniforms.textureTempColor.value = compute.computeRenderer.getCurrentRenderTarget(
      compute.textureTempColor
    ).texture;
    particles.uniforms.textureTempPosition.value = compute.computeRenderer.getCurrentRenderTarget(
      compute.textureTempPosition
    ).texture;
  });
  // ==================================================================
  useEffect(() => {
    if (!mesh.current) return;
    mesh.current.position.z = 0.0;
  }, []);

  return (
    <points ref={mesh}>
      <bufferGeometry attach="geometry">
        <bufferAttribute
          attachObject={["attributes", "position"]}
          //   ref={attrib}
          count={particles.vertices.length / 3} //countはreact-three-fiberで必要っぽい
          array={particles.vertices}
          itemSize={3}
        />
        <bufferAttribute
          attachObject={["attributes", "uv"]}
          count={particles.uv.length / 2}
          array={particles.uv}
          itemSize={2}
        />
        {/* <bufferAttribute
          attachObject={["attributes", "colors"]}
          count={particles.colors.length / 3}
          array={particles.colors}
          itemSize={3}
        /> */}
      </bufferGeometry>

      <shaderMaterial
        attach="material"
        uniforms={particles.uniforms}
        vertexShader={particles.VertexShader}
        fragmentShader={particles.FragmentShader}
        transparent={true}
        blending={THREE.AdditiveBlending}
        side={THREE.DoubleSide}
        depthTest={false}
        depthWrite={false}
      />
    </points>
  );
};

export default Particles;

shader

■position

位置は compute 側参照してるだけ、

あと色情報 varying してる。

何といっていいか
パーティクルの動きは3Dなんだけど動画自体は2Dなので
そのままXY座標から色取るだけだと奥行きが変に見えるので
ちょっとz軸で何かやってる。
適当なんだけどパッ見でそれっぽくなったので
コレでいいや的な感じです。良くは分かってない。

■fragment

上の色情報こっちでも取れそうな物な気がしたのだけども知識不足か、
うまくいかなかったので varying で送られた色を mix で切り替えてます。
多分もっとスマートな方法あると思う。

scene

ここは背景(ベースとなる画角のビデオテクスチャ)をplaneBufferGeometryに反映しているだけ
shader側も透過とかちょっと変えてるぐらい。

// =======================================
// 背景の動画
// =======================================
import React, { useRef, useEffect, useMemo } from "react";
import * as THREE from "three";
import { useFrame } from "react-three-fiber";
import { updateValue } from "../../data";

import { Conf } from "../../index";

import { snoise } from "../compute/shader/snoise";
import { Fragment } from "./shader/fragment";
const FragmentShader = `${snoise} ${Fragment}`;

const Scene: React.FC<any> = (props) => {
  const { compute } = props;
  const material: any = useRef();
  const mesh: any = useRef();

  const args = useMemo(() => {
    return {
      uniforms: {
        textureRgb: { type: "t", value: null },
        time: { type: "f", value: 0.0 },
        bgOpacity: { type: "f", value: 0.0 },
        resolution: {
          type: "v2",
          value: new THREE.Vector2(Conf.TEXTURE_WIDTH, Conf.TEXTURE_HEIGHT)
        }
      },
      vertexShader: `
            varying vec2 vUv;
              void main() {
                  vUv = uv;
                  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
              }  
            `,
      fragmentShader: FragmentShader
    };
  }, []);

  useFrame((frame) => {
    if (!material.current) return;

    material.current.uniforms.time.value = frame.clock.elapsedTime;
    material.current.uniforms.bgOpacity.value = updateValue.bgOpacity;
    material.current.uniforms.textureRgb.value = compute.computeRenderer.getCurrentRenderTarget(
      compute.textureRgb
    ).texture;
  });
  useEffect(() => {
    if (!mesh.current) return;
    mesh.current.position.z = -0.05;
    // z軸ちょっとずらしてパーティクルとかぶらなように
  }, []);

  return (
    <mesh ref={mesh}>
      <planeBufferGeometry attach="geometry" args={[16, 9]} />
      <shaderMaterial
        attach="material"
        ref={material}
        args={[args]}
        transparent={true}
      />
    </mesh>
  );
};

export default Scene;

media

video とか WebAudio、ストリーミングあと
youtube 用として連番の書き出しとか作ってみました。

audio

// =======================================
// WEB AUDIO API周り
// fftでなんか同期しようと思ったけど
// 現状ただグラフとか描画してるだけ
// =======================================

import React, {
  useEffect,
  useCallback,
  useRef,
  useState,
  useContext
} from "react";
import styled from "styled-components";
import { ResourceContext } from "../index";

const CV_WIDTH = 300;
const CV_HEIGHT = 150;

declare global {
  interface Window {
    webkitAudioContext: typeof AudioContext;
  }
}

const Root = styled.div`
  position: fixed;
  z-index: 1000;
  top: 0;
  left: 0;
`;
const LevelCanvas = styled.canvas`
  width: 300px;
  height: 50px;
  position: fixed;

  top: 140px;
  left: 20px;
  background-color: rgba(0, 0, 0, 0.5);
  border-radius: 10px;
`;

const TimeCanvas = styled.canvas`
  position: fixed;
  width: 300px;
  height: 150px;
  top: 205px;
  left: 20px;
  background-color: rgba(0, 0, 0, 0.5);
  border-radius: 10px;
`;
const FrequencyCanvas = styled.canvas`
  width: 300px;
  height: 150px;
  position: fixed;
  top: 372px;
  left: 20px;
  background-color: rgba(0, 0, 0, 0.5);
  border-radius: 10px;
`;

const Audio: React.FC<any> = (props) => {
  const [resource, setResource] = useContext(ResourceContext);
  const [audio, setAudio] = useState<any>();
  const videoRef = props.videoRef[1]; // audioはvideoBから
  const [isData, setIsData] = useState(false);
  const timeRef = useRef();
  const frequencyRef = useRef();
  const levelRef = useRef();

  const [levelCanvasCtx, setLevelCanvasCtx] = useState<
    CanvasRenderingContext2D
  >();
  const [timeCanvasCtx, setTimeCanvasCtx] = useState<
    CanvasRenderingContext2D
  >();
  const [frequencyCanvasCtx, setFrequencyCanvasCtx] = useState<
    CanvasRenderingContext2D
  >();

  const setLevelRef = useCallback(
    (node) => {
      console.log(node);
      levelRef.current = node;
      setLevelCanvasCtx(node.getContext("2d"));
    },
    [levelRef]
  );
  const setTimeRef = useCallback(
    (node) => {
      timeRef.current = node;
      setTimeCanvasCtx(node.getContext("2d"));
    },
    [timeRef]
  );
  const setFrequencyRef = useCallback(
    (node) => {
      frequencyRef.current = node;
      setFrequencyCanvasCtx(node.getContext("2d"));
    },
    [frequencyRef]
  );

  useEffect(() => {
    if (!timeRef || !frequencyRef || !levelRef || !!isData) return;

    const _AudioContext = window.AudioContext || window.webkitAudioContext;
    const audioCtx: AudioContext = new _AudioContext();
    const source = audioCtx.createMediaElementSource(videoRef.current);

    const gainNode = audioCtx.createGain();
    gainNode.gain.value = 1.0;

    const analyserNode: AnalyserNode = audioCtx.createAnalyser();
    analyserNode.fftSize = 512; // frequencyBinCountはナイキストレートでfftSizeの半分
    analyserNode.smoothingTimeConstant = 0.6;

    const levelBuffer: Float32Array = new Float32Array(analyserNode.fftSize);
    const timesData: Uint8Array = new Uint8Array(
      analyserNode.frequencyBinCount
    );
    const freqsData: Uint8Array = new Uint8Array(
      analyserNode.frequencyBinCount
    );

    const processor = audioCtx.createScriptProcessor(1024, 1, 1);
    source.connect(processor); // スクリプトプロセッサ。はいらないかも

    const mediaStreamDest = audioCtx.createMediaStreamDestination();
    source.connect(mediaStreamDest); // mediaStream(録音用)

    source.connect(gainNode);
    gainNode.connect(analyserNode);
    analyserNode.connect(audioCtx.destination);

    // なんか下のuseEffectと合わせて
    // こうしないとresourceに反映されない
    // 追ってuseContext確認
    //   window.setTimeout(() => {
    setAudio({
      audioCtx: audioCtx,
      processor: processor,
      analyserNode: analyserNode,
      levelBuffer: levelBuffer,
      timesData: timesData,
      freqsData: freqsData,
      mediaStreamDest: mediaStreamDest
    });
    setIsData(true);
  }, [levelRef, timeRef, frequencyRef, videoRef, setAudio, isData]);
  // ----------------------
  useEffect(() => {
    setResource({
      ...resource,
      audio: audio
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [audio, setResource]);
  // ----------------------
  // draw
  // ----------------------
  const levelDraw = useCallback(() => {
    // console.log("levelDraw");
    audio.analyserNode.getFloatTimeDomainData(audio.levelBuffer);
    // ---------------------
    // // Compute average power over the interval.
    // for (let i = 0; i < sampleBuffer.length; i++) {
    //   sumOfSquares += sampleBuffer[i] ** 2;
    // }
    // const avgPowerDecibels = 10 * Math.log10(sumOfSquares / sampleBuffer.length);
    // ---------------------
    // Compute peak instantaneous power over the interval.
    let peak = 0;
    // let sumOfSquares = 0;
    for (let i = 0; i < audio.levelBuffer.length; i++) {
      const power = audio.levelBuffer[i] ** 2;
      peak = Math.max(power, peak);
      //   sumOfSquares += power;
    }
    const peakDecibels = 10 * Math.log10(peak);
    // ピークデシベル
    // const avgDecibels = 10 * Math.log10(sumOfSquares / audio.levelBuffer.length);
    // デシベル平均値
    const peakLinear = Math.pow(10, peakDecibels / 20);
    // const avgLinear = Math.pow(10, avgDecibels / 20);
    // リニア値

    levelCanvasCtx!.clearRect(0, 0, 1000, 100);
    const grd = levelCanvasCtx!.createLinearGradient(0, 0, CV_WIDTH, 0);

    grd.addColorStop(0.0, "lime");
    grd.addColorStop(0.7, "yellow");
    grd.addColorStop(1.0, "red");
    levelCanvasCtx!.fillStyle = grd;
    levelCanvasCtx!.fillRect(0, 0, CV_WIDTH * peakLinear * 2.0, 50);
  }, [audio, levelCanvasCtx]);

  const timeDraw = useCallback(() => {
    // console.log("timeDraw");
    audio.analyserNode!.getByteTimeDomainData(audio.timesData!);
    timeCanvasCtx!.clearRect(0, 0, CV_WIDTH, CV_HEIGHT);
    timeCanvasCtx!.strokeStyle = "rgb(255, 255, 255)";
    timeCanvasCtx!.beginPath();
    for (var i = 0; i < audio.analyserNode!.frequencyBinCount; ++i) {
      var x = (i / audio.analyserNode!.frequencyBinCount) * CV_WIDTH;
      var val = 1.0 - audio.timesData![i] / 255; //   0.0 ~ 1.0の数値に
      val = (val - 0.5) * 1.0 + 0.5; //スケーリング
      var y = val * CV_HEIGHT;

      if (i === 0) {
        timeCanvasCtx!.moveTo(x, y);
      } else {
        timeCanvasCtx!.lineTo(x, y);
      }
    }
    timeCanvasCtx!.stroke();
  }, [audio, timeCanvasCtx]);

  const frequencyDraw = useCallback(() => {
    // console.log("frequencyDraw");
    audio.analyserNode!.getByteFrequencyData(audio.freqsData!);
    frequencyCanvasCtx!.clearRect(0, 0, CV_WIDTH, CV_HEIGHT);
    const frequencyArea = ~~(audio.analyserNode!.frequencyBinCount * 1.0);
    frequencyCanvasCtx!.strokeStyle = "rgb(255, 255, 255)";
    frequencyCanvasCtx!.beginPath();
    for (var i = 0; i < frequencyArea; ++i) {
      var x = (i / frequencyArea) * CV_WIDTH;
      var y = (1 - audio.freqsData![i] / 255) * CV_HEIGHT;

      if (i === 0) {
        frequencyCanvasCtx!.moveTo(x, y);
      } else {
        frequencyCanvasCtx!.lineTo(x, y);
      }
    }
    frequencyCanvasCtx!.stroke();
  }, [audio, frequencyCanvasCtx]);

  // ----------------------
  // update処理
  // ----------------------
  const tick = useCallback(() => {
    requestAnimationFrame(tick);
    if (!isData) return;
    if (!resource.isDataShow) return;
    levelDraw();
    timeDraw();
    frequencyDraw();
  }, [isData, resource, frequencyDraw, timeDraw, levelDraw]);
  useEffect(() => {
    const _tick = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(_tick);
  }, [tick, resource]);

  // ----------------------
  // render
  // ----------------------
  return (
    <Root
      className="Audio"
      style={{ display: resource.isDataShow ? "block" : "none" }}
    >
      <LevelCanvas ref={setLevelRef} width={CV_WIDTH} height={50} />
      <TimeCanvas ref={setTimeRef} width={CV_WIDTH} height={CV_HEIGHT} />
      <FrequencyCanvas
        ref={setFrequencyRef}
        width={CV_WIDTH}
        height={CV_HEIGHT}
      />
    </Root>
  );
};

export default Audio;

Web Audio APIとかFFTでなんかしようかなと思ってものの
あまり同期で使えなさそうな気がしたので
パーティクルとかの動作は連動はしてない。

のでおまけ的な内容だけど
せっかく作ったのでそのまま配置してる、
時間領域とか周波数領域のグラフ描画だけ、
あとあんまり自信ないけど、ピークメーターも作ってみた。

createMediaElementSource

因みに動画ファイルの音声データはcreateMediaElementSourcevideo要素渡せば
Web Audio APIで使えるっぽい。

createMediaStreamDestination

使えばMediaStreamで録音とか出来た、
けどやり方が悪いのかもだけど、
動画の録画側でコマ落ちがでてしまうので不使用。

recorder

YOUTUBE とかにもアップしよう思ったのですが
webgl ってどうやってファイルとして書き出せばいいのか分からず
ここが実は結構苦戦した。

// =======================================
// canvasを一コマづつjpegキャプチャとって
// 手動でffmpegで連結する。
// MediaStreamは一応動いたけどコマ落ちするので一旦無し
// 公開サーバーではいらないと思うのでdevelopmentだけ
// =======================================

import React, { useContext, useCallback } from "react";
import styled from "styled-components";
import { updateValue } from "../data";
import { ResourceContext } from "../index";

const APP_URL = process.env.APP_URL;

let currentTime = 0;
let currentFrame = 0;

const Root = styled.div`
  position: fixed;
  bottom: 30px;
  right: 30px;
  z-index: 20000;

  button {
    cursor: pointer;
    border: 0px solid #fff;
    margin: 0 10px;
    background-color: rgba(255, 255, 255, 0.9);
    color: #000;
    padding: 10px 15px;
    border-radius: 10px;
    font-size: 16px;
  }
`;

const Recorder: React.FC<any> = (props) => {
  const { videoRef } = props;
  const [resource] = useContext(ResourceContext);
  const { canvas } = resource;

  const output = useCallback(
    async (currentFrame: number) => {
      canvas.getContext("experimental-webgl", {
        preserveDrawingBuffer: true
      });
      const base64 = canvas
        .toDataURL("image/jpeg", 0.5)
        .replace(new RegExp("data:image/jpeg;base64,"), "");
      const formData = new FormData();
      formData.append("base64", base64);
      formData.append("currentFrame", "" + currentFrame);
      const response = await fetch(APP_URL + "/api/generator/", {
        method: "POST",
        body: formData
      });
      if (!response.ok) return;
      const path = await response.json();
      return new Promise((resolve) => {
        window.setTimeout(() => {
          resolve(path);
        }, 1000);
      });
    },
    [canvas]
  );

  const update = useCallback(() => {
    currentTime += 1 / 30; // 30FPS
    videoRef.forEach((video: any) => {
      video.current.currentTime = currentTime;
    });
    updateValue.renderState = true;
    // ここでthree側をコマ送りするようにしている
  }, [videoRef]);

  const rendering = useCallback(() => {
    currentFrame += 1;
    const per = ((currentTime / updateValue.duration) * 100).toFixed(2);
    console.log(per + "%");
    if (currentTime > updateValue.duration) return;
    (async () => {
      update();
      await output(currentFrame);
      rendering();
    })();
  }, [output, update]);

  const renderingStart = useCallback(() => {
    if (!canvas) return;
    currentTime = 0;
    rendering();
  }, [canvas, rendering]);

  // ----------------------------
  const screenshot = useCallback(() => {
    canvas.getContext("experimental-webgl", {
      preserveDrawingBuffer: true
    });
    const dataUrl = canvas.toDataURL("image/jpeg");
    const w: any = window.open("about:blank");
    w.document.write("<img src='" + dataUrl + "'/>");
  }, [canvas]);
  // ----------------------------

  return (
    <Root className="Recorder">
      <button onClick={renderingStart}>rendering</button>
      <button onClick={screenshot}>screenshot</button>
    </Root>
  );
};

export default React.memo(Recorder);

// ===========================================
// MediaStream動いたけどコマ落ちするので
// 一コマづつjpegキャプチャとってffmpegで連結する方向で

// const [href, setHref] = useState<any>();
//   const [download, setDownload] = useState<any>();

// const recorder = useMemo(() => {
//     if (!canvas) return;

//     const audioStream = audio.mediaStreamDest.stream;
//     const canvasStream = canvas.captureStream();

//     const mediaStream = new MediaStream();
//     [canvasStream, audioStream].forEach((stream) => {
//       stream.getTracks().forEach((track: any) => mediaStream.addTrack(track));
//     });

//     const recorder = new MediaRecorder(mediaStream, {
//       audioBitsPerSecond: 128000,
//       videoBitsPerSecond: 12800000,
//       mimeType: "video/webm;codecs=vp9",
//     });
//     recorder.ondataavailable = function (e) {
//       var videoBlob = new Blob([e.data], { type: e.data.type });
//       const blobUrl = window.URL.createObjectURL(videoBlob);
//       setDownload("movie.webm");
//       setHref(blobUrl);
//     };

//     return recorder;
//   }, [canvas, audio]);

//   const recorderStart = useCallback(() => {
//     console.log("start");
//     recorder!.start();
//   }, [recorder]);
//   const recorderStop = useCallback(() => {
//     console.log("stop");
//     recorder!.stop();
//   }, [recorder]);
//   // ----------------------------

webglの動画ファイルへの書き出し

がしたくて調べたら MediaRecorder/MediaStream なるものがあったので、コレだな、
と思って実装してみた録音録画自体はできたものの
どうにもコマ落ちだったりが起こってしまい微妙な気がした。
単に PC の負荷とかスペックだったり、設定が悪いだけかもしれないけど、

一応手元で動いたのはコメントとして残している。

一コマづつjpgで書き出し->ffmpeg

なので代替方法として three 側を await で1フレームづつ
コマ送りしつつ canvasbsae64POST。
phpjpg に書き出し。といゆう手段をとってみた。

<?php
/* =========================================
jsから受け取ったbase64をjpgに変換
手動でffmpegで動画にする
========================================= */
$TEMP_PATH = "どっかパス";

$base64 = $_POST['base64'];
$currentFrame = $_POST['currentFrame'];
$file_path = $_SERVER['DOCUMENT_ROOT'] . $TEMP_PATH . $currentFrame  . ".jpg";

$new_file_name = $file_path;
$fp = fopen($new_file_name, 'w');
fwrite($fp, base64_decode($base64));
fclose($fp);

PHP側はこんな感じ

その後手動で ffmpeg で連結。
で一応割と満足行く画質で書き出し?できた。
youtube にアップしたら自動圧縮で荒れましたが、
まぁ作ってる側だから気になる程度だと思う。

streamLoader

// =======================================
// サーバー配信
// 分割ロードとかシークバーなんかは未実装
// =======================================
import React, { useEffect, useCallback, useRef, useState } from "react";
import styled from "styled-components";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Conf } from "../index";

const BASE_PATH = "//nocebo.jp/band/prs/public/api/stream/";
let _contentLength = 0;
let _totalLength = 0;


const StreamLoader: React.FC<any> = (props) => {
  const [isLoadStart, setIsLoadStart] = useState<boolean>(false);
  const [percent, setPercent] = useState<number>(0);
  const [totalLength, setTotalLength] = useState<number>(0);
  const [currentLength, setCurrentLength] = useState<number>(0);
  const { setIsLoaded, setVideoBuffer } = props;

  const loadLength = async (
    request: string,
    id: string,
    quality: string
  ): Promise<number> => {
    const formData = new FormData();
    formData.append("id", id);
    formData.append("quality", quality);
    formData.append("isReadLength", "true");
    const response: any = await fetch(request, {
      method: "POST",
      body: formData
    });
    // const contentLength = +response.headers.get("Content-Length");
    if (!response.ok) return 0;
    const body = await response.json();
    const contentLength: number = +body;
    return new Promise((resolve) => {
      resolve(contentLength);
    });
  };
  const stream = async (request: string, id: string, quality: string) => {
    const formData = new FormData();
    formData.append("id", id);
    formData.append("quality", quality);
    const response: any = await fetch(request, {
      method: "POST",
      // headers: { Range: "bytes=0-22344789" },// 分割する場合はこの辺でなんとかなりそうな
      body: formData
    });
    const reader = response.body.getReader();

    let receivedLength = 0;
    const chunks = [];

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(value);
      receivedLength += value.length;
      _contentLength += value.length;
      //   console.log(`Received ${receivedLength} of ${_contentLength}`);
    }
    const chunksAll = new Uint8Array(receivedLength);
    let position = 0;
    for (const chunk of chunks) {
      chunksAll.set(chunk, position);
      position += chunk.length;
    }
    const arrayBuffer = chunksAll;

    return new Promise((resolve) => {
      resolve(arrayBuffer);
    });
  };

  const loadStart = (quality: string) => {
    setIsLoadStart(true);
    // -------------
    // 場所がいまいちな気がするけどここでクオリティ設定
    Conf.QUALITY = quality;
    Conf.TEXTURE_WIDTH = Conf[Conf.QUALITY].TEXTURE_WIDTH;
    Conf.TEXTURE_HEIGHT = Conf[Conf.QUALITY].TEXTURE_HEIGHT;
    Conf.PARTICLE_COUNT = Conf[Conf.QUALITY].PARTICLE_COUNT;
    // -------------
    (async () => {
      //   console.log("load start");
      for (let i = 0; i <= Conf.VIDEO_TEXTURE_NUM - 1; i++) {
        const l: number = await loadLength(BASE_PATH, "" + i, quality);
        _totalLength += l;
      }
      setTotalLength(_totalLength);

      const arrayBuffer = [];
      for (let j = 0; j <= Conf.VIDEO_TEXTURE_NUM - 1; j++) {
        arrayBuffer[j] = await stream(BASE_PATH, "" + j, quality);
      }
      setVideoBuffer(arrayBuffer);
    })();
  };

  const requestRef = useRef<any>();
  const tick = useCallback(() => {
    requestRef.current = requestAnimationFrame(tick);
    setCurrentLength(_contentLength);
    const p = (_contentLength / _totalLength) * 100;
    setPercent(p);
    if (p === 100) {
      window.setTimeout(() => {
        setIsLoaded(true);
      }, 1000);
      // setTimeoutは保険
    }
  }, [setCurrentLength, setIsLoaded]);

  useEffect(() => {
    requestRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(requestRef.current);
  }, [tick]);

  return isLoadStart ? (
    <Root>
      <Info>
        <p className="percent">
          {("000" + ~~percent).slice(-3)}
          <span>%</span>
        </p>
        <div className="bar">
          <span
            style={{
              width: percent + "%"
            }}
          ></span>
        </div>
        <div className="data">
          {currentLength} / {totalLength}
        </div>
      </Info>
    </Root>
  ) : (
    <Root>
      <Header>
        <h1>nocebo - [ prs ] studio rehearsal</h1>
        <p>
          バンドの練習風景撮ったのをwebgl(three.js)でGPGPU使いつつcurlnoiseのparticleで遊んでみました
        </p>
        <Share>
          <a
            href="//twitter.com/share?text=nocebo - [ prs ] studio rehearsal&url=https://nocebo.jp/band/prs/public/&hashtags=webgl,threejs"
            title="Tweet"
            target="_blank"
            rel="noopener noreferrer nofollow"
          >
            SHARE
            <FontAwesomeIcon icon={["fab", "twitter"]} />
          </a>
        </Share>
      </Header>
      <Select>
        <h4>QUALITY SELECT</h4>
        <nav>
          <button
            onClick={() => {
              loadStart("NORMAL");
            }}
          >
            720P
          </button>
          <button
            onClick={() => {
              loadStart("HIGH");
            }}
          >
            1080P
          </button>
        </nav>
        <div className="url">
          <h5>URL</h5>
          <a
            href="https://nocebo.jp/band/prs/public/"
            target="_blank"
            rel="noopener noreferrer"
          >
            https://nocebo.jp/band/prs/public/
          </a>
        </div>
        <div className="link">
          <h5>LINKS</h5>
          <a
            href="https://nocebo.jp/"
            target="_blank"
            rel="noopener noreferrer"
          >
            nocebo.jp
          </a>
          <a
            href="https://nocebo.jp/band/"
            target="_blank"
            rel="noopener noreferrer"
          >
            band
          </a>
          <a
            href="https://youtu.be/tD1X_cUVVrY"
            target="_blank"
            rel="noopener noreferrer"
          >
            youtube
          </a>
          <a
            href="https://twitter.com/nocebojp"
            target="_blank"
            rel="noopener noreferrer"
          >
            twitter
          </a>
          <a
            href="https://nocebo.jp/post-3924/"
            target="_blank"
            rel="noopener noreferrer"
          >
            explain
          </a>
        </div>
        <div className="iframe">
          <div className="iframe__inner">
            <iframe
              title="nocebo - [ prs ] studio rehearsal"
              src="https://www.youtube.com/embed/tD1X_cUVVrY"
              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
            ></iframe>
          </div>
        </div>
      </Select>
    </Root>
  );
};

export default React.memo(StreamLoader);

動画のサーバー配信?を何となくやってみたくて
やってみたらここも地味に苦戦した。

ローダーの割合出すのにloadLengthってので先に
一旦リクエストだしてるけど何かいい方法ありそうな。

<?php
/* =========================================
サーバーから動画配信するやつ
読み込み速度そこそこな気がするので
結局分割とかはしてない。
========================================= */
$STORAGE_PATH = パスとか";
$id = $video[$_POST['id']];
$q = $quality[$_POST['quality']];

$file = $_SERVER['DOCUMENT_ROOT'] . $STORAGE_PATH . "{$id}_{$q}p.webm";
$fp = @fopen($file, 'rb');

$size   = filesize($file); // File size
$length = $size;           // Content length
$start  = 0;               // Start byte
$end    = $size - 1;       // End byte

// ローダー用にlengthだけ返す場合
if (isset($_POST['isReadLength'])) {
    echo $length;
    exit();
}

// =============================
// Content-Range返す
// (結局分割してない)
// =============================
header('Content-type: video/webm');
header("Accept-Ranges: 0-$length");

if (isset($_SERVER['HTTP_RANGE'])) {

    $c_start = $start;
    $c_end   = $end;

    list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
    if (strpos($range, ',') !== false) {
        header('HTTP/1.1 416 Requested Range Not Satisfiable');
        header("Content-Range: bytes $start-$end/$size");
        exit;
    }
    if ($range == '-') {
        $c_start = $size - substr($range, 1);
    } else {
        $range  = explode('-', $range);
        $c_start = $range[0];
        $c_end   = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
    }
    $c_end = ($c_end > $end) ? $ensd : $c_end;
    if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
        header('HTTP/1.1 416 Requested Range Not Satisfiable');
        header("Content-Range: bytes $start-$end/$size");
        exit;
    }
    $start  = $c_start;
    $end    = $c_end;
    $length = $end - $start + 1;
    fseek($fp, $start);
    header('HTTP/1.1 206 Partial Content');
}

header("Content-Range: bytes $start-$end/$size");
header("Content-Length: " . $length);

$buffer = 1024 * 8;
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
    if ($p + $buffer > $end) {
        $buffer = $end - $p + 1;
    }
    // set_time_limit(0);
    echo fread($fp, $buffer);
    flush();
}

fclose($fp);
exit();

広い集めだけども、PHP側はこんな感じ。
分割配信みたいなのもやらないとなーと思ってたんだけど、
webm にしたら軽くなってそこまで気になる速度じゃないかなとも思ったので
一括で返してます。
一応headers送れば分割で返すようにはなっている。はず。


webm すごい、変換クソ時間かかるけど

video

ここは video の配置

// =======================================
// video要素として配置はしてるけど
// 実際には参照してGLでcanvasに描画したもの表示している
// =======================================
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { updateValue } from "../data";
import { Conf } from "../index";

const Root = styled.div`
  width: 640px;
  height: 360px;
  position: fixed;
  top: 0;
  right: 0;
  z-index: 10;
  visibility: hidden;
  /* noneだとtexture参照できない */
  video {
    position: absolute;
    right: 0;
    bottom: 0;
    width: 640px;
    z-index: 1000;
  }
`;

const createBlobURL = async (videoBuffer: Uint8Array) => {
  const mediaSource: any = new MediaSource();
  mediaSource.addEventListener("sourceopen", () => {
    const sourceBuffer = mediaSource.addSourceBuffer(
      `video/webm; codecs="opus, vp9"`
    );
    sourceBuffer.addEventListener("updateend", () => mediaSource.endOfStream());
    sourceBuffer.appendBuffer(videoBuffer);
  });
  const src = URL.createObjectURL(mediaSource);
  return new Promise((resolve) => {
    resolve(src);
  });
};

const Video: React.FC<any> = (props) => {
  const [isLoaded, setIsLoaded] = useState<any>();
  const [blobUrl, setBlobUrl] = useState<any>();
  const { videoRef, setIsReady, videoBuffer } = props;

  useEffect(() => {
    (async () => {
      const blob_url = [];
      for (let i = 0; i < Conf.VIDEO_TEXTURE_NUM; i++) {
        blob_url[i] = await createBlobURL(videoBuffer[i]);
      }
      setBlobUrl(blob_url);
      setIsLoaded(true);
    })();
    // return () => {// cleanup};
  }, [videoBuffer]);

  useEffect(() => {
    let isCurrent = false;
    videoRef.forEach((video: any) => {
      isCurrent = !!video.current;
    });

    if (!isCurrent) return;
    videoRef[0].current.addEventListener("loadedmetadata", function () {
      updateValue.duration = videoRef[0].current.duration;
      setIsReady(true);
    });
  }, [isLoaded, setIsReady, videoRef]);

  return isLoaded ? (
    <Root className="Video">
      {videoRef.map((v: any, i: number) => {
        return <video src={blobUrl[i]} ref={videoRef[i]} key={i} />;
      })}
    </Root>
  ) : (
    <></>
  );
};

export default React.memo(Video);

上のstreamLoaderで受け取った Bufferblob に変換して src に設定してる。
で合ってると思う。

ui

ここはなんか再生UIのドラッグとかフルスクリーンとかつけてみたぐらい。なので省く。

以上です。

]]>
https://nocebo.jp/post-3924/feed/ 0