カメラ機能

<template>
  <div class="root">
    <div>
      <div class="button1" @click="pickImagesOrSelect()">
        写真選択 or 写真撮影
      </div>
      <div class="resetButton" @click="captures = []">リセット</div>

      <div class="captureWrap">
        <div class="capture" v-for="c in captures" v-bind:key="c.d">
          <img v-bind:src="c" />
        </div>
        <p>{{ dataUrlString }}</p>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";

export default defineComponent({
  setup() {
    const video = ref();
    const canvas = ref();
    const captures = ref([]) as any;
    const dataUrlString = ref();

    const pickImagesOrSelect = async () => {
      const image = await Camera.getPhoto({
        quality: 60, // 0~100まで
        source: CameraSource.Prompt, // 写真を撮るorライブラリから選択or両方
        resultType: CameraResultType.DataUrl, // 実行後のデータの種類を指定
        presentationStyle: "popover", // 画像の表示
        webUseInput: true, // ブラウザで実行した時の処理
        promptLabelHeader: "写真を選択", // 以下の文言のタイトル的なもの
        promptLabelPicture: "写真を撮る", // 写真を撮るときに表示される文言
        promptLabelPhoto: "アルバルから選択", // ライブラリにアクセスする時に表示される文言
        promptLabelCancel: "キャンセル", // 実行を取りやめる時の文言
        correctOrientation: true,
      });
      dataUrlString.value = image.dataUrl;
      captures.value.push(image.dataUrl);
    };

    return {
      video,
      canvas,
      captures,
      pickImagesOrSelect,
      dataUrlString,
    };
  },
});
</script>

<style lang="scss" scoped>
.root {
  margin: 0 auto;
  width: 390px;
  position: relative;
  padding: 16px;
}

.photo {
  width: 390px;
  height: 390px;
  margin-bottom: 24px;
}
.l2 {
  top: 0;
  position: absolute;
}
#camera_record_box {
  position: absolute;
  width: 390px;
  height: 390px;
  background-image: url("../square-1.png");
  opacity: 0.5;
}
#canvas {
  display: none;
}

.captureWrap {
  width: 100%;
  display: flex;

  flex-flow: column;
  gap: 12px;

  .capture {
    img {
      width: 100%;
    }
  }
}

.button1 {
  background-color: #005bac;
  color: #fff;
  padding: 16px;
  margin-bottom: 12px;
  border-radius: 32px;
  cursor: pointer;
  font-weight: bold;
}

.resetButton {
  color: #1a1c21;
  background-color: #ffcc00;
  font-weight: bold;

  padding: 16px;
  margin-bottom: 12px;
  border-radius: 32px;
  cursor: pointer;
}
</style>

スクロールでフェードインするステップバー

<template>
  <div class="root">
    <q-dialog v-model="seamless" seamless position="bottom">
      <div class="dialog">
        <div class="container">
          <div class="stepbar">
            <div class="stepbarwrap">
              <div class="steptitle">
                <div class="stepcircle">
                  <span>STEP<br />1</span>
                </div>
                <p class="title">テキスト1</p>
              </div>
              <div class="steptxt">
                <span class="txt"
                  >テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストト</span
                >
              </div>
              <span class="stepline"></span>
            </div>

            <div class="stepbarwrap">
              <div class="steptitle">
                <div class="stepcircle">
                  <span>STEP<br />2</span>
                </div>
                <p class="title">テキスト2</p>
              </div>
              <div class="steptxt">
                <span class="txt"
                  >テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストト</span
                >
              </div>
              <span class="stepline"></span>
            </div>

            <div class="stepbarwrap">
              <div class="steptitle">
                <div class="stepcircle">
                  <span>STEP<br />3</span>
                </div>
                <p class="title">テキスト3</p>
              </div>
              <div class="steptxt">
                <span class="txt"
                  >テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストト</span
                >
              </div>
              <span class="stepline"></span>
            </div>
          </div>
        </div>
      </div>
    </q-dialog>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";
const seamless = ref(true);
</script>

<style lang="scss" scoped>
.root {
  width: 100%;
  margin: 0 auto;
}
.dialog {
  filter: drop-shadow(0 0 10px #000);
  border-radius: 16px 16px 0 0;
  width: 100%;
  height: 500px;
  background-color: #fff;
  padding: 30px;
  /*上下方向にはみ出した要素ををスクロールさせる*/
  overflow-y: scroll;
  /*スクロールバー非表示(IE・Edge)*/
  -ms-overflow-style: none;
  /*スクロールバー非表示(Firefox)*/
  scrollbar-width: none;
  &::-webkit-scrollbar {
    display: none;
  }
}

.container {
  width: 100%;
  margin: 0 auto;
  display: flex;
  padding: 0;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  gap: 100px;
  margin-bottom: 120px;

  // スナップ
  scroll-snap-type: y mandatory;
}

.stepbarwrap {
  // フェードインアニメーション
  animation: fadeIn linear both;
  animation-timeline: view();
  animation-range: entry 25% cover 50%;

  // スナップ
  scroll-snap-align: start;
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

.stepbar {
  margin: 0 auto;
}

.stepbar .stepbarwrap {
  margin: 2em 0;
  position: relative;
}

.stepbar .stepbarwrap .steptitle {
  display: inline-flex;
  align-items: center;
}

.stepbar .stepbarwrap .steptitle .stepcircle {
  display: inline-block;
  width: 3em;
  height: 3em;
  content: "";
  border-radius: 50%;
  background-color: #000;
  color: #fff;
  text-align: center;
}

.stepbar .stepbarwrap .steptitle .stepcircle span {
  display: inline-block;
  line-height: 1.2em;
  font-size: 0.8em;
  font-weight: bold;
  position: relative;
  top: 0.9em;
}

.stepbar .stepbarwrap .steptitle .title {
  margin: 0.5em;
  font-weight: bold;
  font-size: 1.2em;
}

.stepbar .stepbarwrap .steptxt {
  padding-left: 3.5em;
}

.stepbar .stepbarwrap .steptxt .txt {
  font-size: 0.9em;
}

.stepbar .stepbarwrap .stepline {
  width: 1px;
  height: calc(100% + 1em);
  background-color: #000;
  position: absolute;
  top: 1em;
  left: 1.5em;
  z-index: -1;
}

.stepbarwrap:last-of-type .stepline:last-of-type {
  display: none;
}
</style>

Webカメラの上に透かし的なものを重ねたい

完成形


コード

<template>
  <div class="root">
    <div class="photo">
      <video ref="video" id="video"></video>
      <canvas ref="canvas" id="canvas"></canvas>

      <div class="l2">
        <canvas id="camera_record_box"></canvas>
      </div>
    </div>

    <div>
      <div class="button1" @click="capture()">写真を撮影</div>
      <div class="button1" @click="takePicture()">写真を選択</div>
      <div class="button1" @click="captures = []">リセット</div>
    </div>

    <div class="captureWrap">
      <div class="capture" v-for="c in captures" v-bind:key="c.d">
        <img v-bind:src="c" />
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from "vue";
import { Camera } from "@capacitor/camera";

export default defineComponent({
  setup() {
    const video = ref();
    const canvas = ref();
    const captures = ref([]) as any;

    const constraints = {
      audio: false,
      video: {
        width: 390,
        height: 390,
        facingMode: "user",
        // facingMode: {
        //   exact: "environment",
        // },
      },
    };

    onMounted(() => {
      if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices
          .getUserMedia(constraints)
          .then((stream) => {
            video.value.srcObject = stream;
            video.value.play();
          })
          .catch((e: any) => {
            console.log("ERROR: ", e);
          });
      }
    });

    const capture = () => {
      if (captures.value.length < 4) {
        canvas.value.getContext("2d").drawImage(video.value, 0, 0, 390, 390);
        captures.value.push(canvas.value.toDataURL("image/png"));
        console.log(captures.value);
      }
    };

    const takePicture = async () => {
      const image = await Camera.pickImages({
        quality: 90,
        limit: 4,
        // source: CameraSource.Photos,
        // allowEditing: true,
        // resultType: CameraResultType.Base64,
      });

      for (let item in image.photos) {
        if (captures.value.length < 4) {
          const base64Data = await base64FromPath(image.photos[item].webPath);
          captures.value.push(base64Data);
        }
      }
    };

    const base64FromPath = async (path: string): Promise<string> => {
      const response = await fetch(path);
      const blob = await response.blob();
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onerror = reject;
        reader.onload = () => {
          if (typeof reader.result === "string") {
            resolve(reader.result);
          } else {
            reject("method did not return a string");
          }
        };
        reader.readAsDataURL(blob);
      });
    };

    return { capture, video, canvas, captures, takePicture };
  },
});
</script>

<style lang="scss">
.root {
  margin: 0 auto;
  width: 390px;
  position: relative;
}

.photo {
  width: 390px;
  height: 390px;
  margin-bottom: 24px;
}
.l2 {
  top: 0;
  position: absolute;
}
#camera_record_box {
  position: absolute;
  width: 390px;
  height: 390px;
  background-image: url("../square-1.png");
  opacity: 0.5;
}
#canvas {
  display: none;
}

.captureWrap {
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  .capture {
    img {
      width: 100px;
      height: 100px;
    }
  }
}

.button1 {
  background-color: red;
  color: #fff;
  padding: 16px;
  margin-bottom: 12px;
}
</style>