16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クソアプリAdvent Calendar 2024

Day 4

手ブレ写真が撮れるウェブアプリを作った

Last updated at Posted at 2024-12-03

本記事はクソアプリ Advent Calendar 2024の4日目の記事です。

クソアプリAdvent Calendarに参加してみたかったので作りました。手ブレ写真が撮れるウェブアプリです。何の役にも立ちません。

今回作ったアプリ

Shake Snap」と名付けました。

デバイスを振ることで写真を撮影できます。スマートフォンでの利用を想定しています。スマートフォンでなくとも加速度センサーを内蔵しているデバイスであれば利用できると思いますが、検証は手元のiPhone 12 miniでのみ行っています。

アニメーションGIFでアプリの動作を確認する

20241204_shake_snap_01.gif

アプリのURL:

ソースコード:

使用した技術

Viteを使ってサクッと作りました。テンプレートはvanilla-tsです。始めはCSSをスクラッチで書いていたのですが、途中から書くのが面倒になったのでTailwind CSSを導入しました。

コア機能の解説

ここではアプリのコア機能である次の2点について簡単に説明します。

  • カメラを使った写真の撮影
  • デバイスの振動判定

カメラを使った写真の撮影

デバイスのカメラ利用は、mediaDevices.getUserMedia()を使いました。このメソッドでカメラから映像ストリームを取得し、それを<video>要素に流し込んでいます。

/src/assets/js/camera.ts
try {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: { facingMode: 'environment' },
    audio: false,
  });
  this.#videoElement.srcObject = stream;
  this.#onCameraStart();
  return true;
} catch (error) {
  console.error(error);
  alert(
    'カメラの起動に失敗しました。カメラへのアクセスを許可してください。'
  );
}

写真の撮影は<canvas>を使っています。<video>の現在のフレームをキャプチャして、toDataURL()で画像をデータURLに変換しています。

/src/assets/js/camera.ts
capture(ctx: CanvasRenderingContext2D): string {
  const canvas = ctx.canvas;
  canvas.width = this.#videoElement.videoWidth;
  canvas.height = this.#videoElement.videoHeight;
  ctx.drawImage(this.#videoElement, 0, 0, canvas.width, canvas.height);
  return canvas.toDataURL('image/jpeg');
}

デバイスの振動判定

デバイスの振動検出ではDeviceMotionEventを利用しました。

accelerationの値を取得し、X・Y・Z軸方向の加速度を基に、振動の強さを計算しています。計算のロジックは単純で、加速度ベクトルの大きさを求めているだけです。

振動の強さが閾値を超えた場合、写真の撮影をトリガーします。

const handler = (event: DeviceMotionEvent) => {
  const { acceleration } = event;
  const x = acceleration?.x ?? 0;
  const y = acceleration?.y ?? 0;
  const z = acceleration?.z ?? 0;
  const totalAcceleration = Math.sqrt(x ** 2 + y ** 2 + z ** 2);
  this.#onShake(totalAcceleration);

  if (totalAcceleration > 0) {
    this.#isShaken = true;
  }
};

window.addEventListener(
  'devicemotion',
  event => {
    requestAnimationFrame(() => handler(event));
  },
  {
    passive: true,
  }
);

また現状のiOSデバイス(iOS13以降?)ではDeviceDeviceMotionEventを使用するためにユーザーへの権限のリクエストが必要です。つまり明示的にDeviceMotionEvent.requestPermission()を実行する必要があります。

#getRequestPermission() {
  const requestPermission = (
    DeviceMotionEvent as unknown as DeviceMotionEventIOS
  ).requestPermission;
  const isRequestNeeded = typeof requestPermission === 'function';

  if (!isRequestNeeded) {
    return async () => 'granted' as const;
  }
  return requestPermission;
}

async requestPermission(): Promise<boolean> {
  const requestPermission = this.#getRequestPermission();

  try {
    const permissionState = await requestPermission();
    if (permissionState === 'granted') {
      this.#onPermissionGranted();
      this.#registerEventHandlers();
      return true;
    } else {
      alert('加速度センサーの許可が得られませんでした');
      console.warn('permissionState:', permissionState);
    }
  } catch (error) {
    console.error(error);
  }
  return false;
}

以上の流れで、デバイスの振動を検知し、写真の撮影を行っています。

おわりに

クソアプリAdvent Calendarには今回はじめて参加したこともあり、楽しみながら実装できました。アイディア出しはAIに手伝ってもらいました。

短い期間で役に立たないアプリを作ることに焦点を当てていため、技術的な深掘りをほとんどできなかったことが今回の反省点です。もしまた参加する機会があれば、技術的な工夫も取り入れながら取り組みたいと思います。

参考

16
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?