Browse Source

优化舌象拍照

cc12458 7 months ago
parent
commit
12374f01b9

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "p5": "^1.11.0",
     "p5": "^1.11.0",
     "pinia": "^2.2.4",
     "pinia": "^2.2.4",
     "pinia-plugin-persistedstate": "^4.1.1",
     "pinia-plugin-persistedstate": "^4.1.1",
+    "svg-pathdata": "^7.1.0",
     "vant": "4",
     "vant": "4",
     "vconsole": "^3.15.1",
     "vconsole": "^3.15.1",
     "vue": "^3.5.11",
     "vue": "^3.5.11",

+ 15 - 0
pnpm-lock.yaml

@@ -41,6 +41,9 @@ dependencies:
   pinia-plugin-persistedstate:
   pinia-plugin-persistedstate:
     specifier: ^4.1.1
     specifier: ^4.1.1
     version: 4.1.1(pinia@2.2.4)
     version: 4.1.1(pinia@2.2.4)
+  svg-pathdata:
+    specifier: ^7.1.0
+    version: 7.1.0
   vant:
   vant:
     specifier: '4'
     specifier: '4'
     version: 4.9.8(vue@3.5.11)
     version: 4.9.8(vue@3.5.11)
@@ -3736,6 +3739,13 @@ packages:
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
     dev: true
     dev: true
 
 
+  /svg-pathdata@7.1.0:
+    resolution: {integrity: sha512-wrvKHXZSYZyODOj5E1l1bMTIo8sR7YCH0E4SA8IgLgMsZq4RypslpYvNSsrdg4ThD6du2KWPyVeKinkqUelGhg==}
+    engines: {node: '>=20.11.1'}
+    dependencies:
+      yerror: 8.0.0
+    dev: false
+
   /svg-tags@1.0.0:
   /svg-tags@1.0.0:
     resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
     resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
     dev: true
     dev: true
@@ -4349,6 +4359,11 @@ packages:
     hasBin: true
     hasBin: true
     dev: true
     dev: true
 
 
+  /yerror@8.0.0:
+    resolution: {integrity: sha512-FemWD5/UqNm8ffj8oZIbjWXIF2KE0mZssggYpdaQkWDDgXBQ/35PNIxEuz6/YLn9o0kOxDBNJe8x8k9ljD7k/g==}
+    engines: {node: '>=18.16.0'}
+    dev: false
+
   /yocto-queue@0.1.0:
   /yocto-queue@0.1.0:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
     engines: {node: '>=10'}

+ 35 - 24
src/modules/camera/ShadeFace.vue

@@ -1,32 +1,43 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import { SVGPathData, SVGPathDataTransformer } from 'svg-pathdata';
 
 
+
+const Shade: SVGPathData[] = [
+  'M0 183A136 183 0 11272 183 136 183 0 110 183Z',
+].map(d => new SVGPathData(d));
+
+const {
+  translateX = 0, translateY = 0,
+  scaleX = 1, scaleY = 1,
+} = defineProps<{
+  translateX?: number; translateY?: number;
+  scaleX?: number; scaleY?: number
+}>();
+
+const paths = ref<string[]>([]);
+
+watchEffect(() => {
+  paths.value = Shade.map(data => data
+    .transform(SVGPathDataTransformer.TRANSLATE(translateX, translateY))
+    .transform(SVGPathDataTransformer.SCALE(scaleX, scaleY))
+    .encode(),
+  );
+});
+</script>
 <template>
 <template>
-  <svg width="225px" height="305px" viewBox="0 0 225 305" xmlns="http://www.w3.org/2000/svg">
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 366">
     <clipPath id="shade">
     <clipPath id="shade">
-      <ellipse
-        cx="135"
-        cy="183"
-        rx="135"
-        ry="183"
-        fill="none"
-        stroke="#FEFEFE"
-        stroke-width="2"
-        stroke-linecap="round"
-        stroke-linejoin="round"
-      />
+      <path v-for="(d,i) in paths" :id="'outline-'+i" stroke="#fefefe" stroke-width="2" fill="none" :d="d" />
     </clipPath>
     </clipPath>
-    <ellipse
-      cx="112.5"
-      cy="152.5"
-      rx="112.5"
-      ry="152.5"
-      fill="none"
-      stroke="#FEFEFE"
+    <path
+      id="face"
+      stroke="#fefefe"
       stroke-width="2"
       stroke-width="2"
-      stroke-linecap="round"
-      stroke-linejoin="round"
+      fill="none"
+      d="M0 183A136 183 0 11272 183 136 183 0 110 183Z"
     />
     />
   </svg>
   </svg>
 </template>
 </template>
-
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+svg { opacity: 0.85; }
+</style>

+ 28 - 12
src/modules/camera/ShadeTongueDown.vue

@@ -1,15 +1,33 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import { SVGPathData, SVGPathDataTransformer } from 'svg-pathdata';
 
 
+
+const Shade: SVGPathData[] = [
+  'M5 195C5 195 31 45 71 12 72 11 73 9 74 8 77 5 86-1 117 5 144 9 156 7 161 5 168 2 175 1 182 3 190 5 198 9 206 18 224 39 248 87 255 153 255 153 262 192 269 195 269 195 248 311 215 332 213 333 211 334 209 336 202 341 187 350 165 350 144 349 118 350 105 351 99 351 93 350 87 348 71 341 40 323 26 280 7 221 3 205 3 205 3 205-0 195 5 195Z',
+].map(d => new SVGPathData(d));
+
+const {
+  translateX = 0, translateY = 0,
+  scaleX = 1, scaleY = 1,
+} = defineProps<{
+  translateX?: number; translateY?: number;
+  scaleX?: number; scaleY?: number
+}>();
+
+const paths = ref<string[]>([]);
+
+watchEffect(() => {
+  paths.value = Shade.map(data => data
+    .transform(SVGPathDataTransformer.TRANSLATE(translateX, translateY))
+    .transform(SVGPathDataTransformer.SCALE(scaleX, scaleY))
+    .encode(),
+  );
+});
+</script>
 <template>
 <template>
   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 366">
   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 366">
     <clipPath id="shade">
     <clipPath id="shade">
-      <path
-        id="outline"
-        stroke="#fefefe"
-        stroke-width="2"
-        fill="none"
-        d="M5 195C5 195 31 45 71 12 72 11 73 9 74 8 77 5 86-1 117 5 144 9 156 7 161 5 168 2 175 1 182 3 190 5 198 9 206 18 224 39 248 87 255 153 255 153 262 192 269 195 269 195 248 311 215 332 213 333 211 334 209 336 202 341 187 350 165 350 144 349 118 350 105 351 99 351 93 350 87 348 71 341 40 323 26 280 7 221 3 205 3 205 3 205-0 195 5 195Z"
-      />
+      <path v-for="(d,i) in paths" :id="'outline-'+i" stroke="#fefefe" stroke-width="2" fill="none" :d="d" />
     </clipPath>
     </clipPath>
     <path
     <path
       id="outline"
       id="outline"
@@ -27,9 +45,7 @@
     />
     />
   </svg>
   </svg>
 </template>
 </template>
-
 <style scoped lang="scss">
 <style scoped lang="scss">
-svg {
-  opacity: 0.85;
-}
+svg { opacity: 0.85; }
 </style>
 </style>
+

+ 28 - 19
src/modules/camera/ShadeTongueUp.vue

@@ -1,23 +1,34 @@
 <script setup lang="ts">
 <script setup lang="ts">
-</script>
+import { SVGPathData, SVGPathDataTransformer } from 'svg-pathdata';
+
+
+const Shade: SVGPathData[] = [
+  'M21 193C21 193-46 60 78 9 84 7 91 4 98 4 105 3 115 3 122 4 136 7 140 7 160 3 182 0 324 31 249 192Z',
+  'M139 127C139 127 49 81 27 152 6 223 51 357 130 364 205 370 244 331 249 181 249 181 259 75 139 127Z',
+].map(d => new SVGPathData(d));
+
+const {
+  translateX = 0, translateY = 0,
+  scaleX = 1, scaleY = 1,
+} = defineProps<{
+  translateX?: number; translateY?: number;
+  scaleX?: number; scaleY?: number
+}>();
 
 
+const paths = ref<string[]>([]);
+
+watchEffect(() => {
+  paths.value = Shade.map(data => data
+    .transform(SVGPathDataTransformer.TRANSLATE(translateX, translateY))
+    .transform(SVGPathDataTransformer.SCALE(scaleX, scaleY))
+    .encode(),
+  );
+});
+</script>
 <template>
 <template>
   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 366">
   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 366">
     <clipPath id="shade">
     <clipPath id="shade">
-      <path
-        id="outline-up"
-        stroke="#fefefe"
-        stroke-width="2"
-        fill="none"
-        d="M21 193C21 193-46 60 78 9 84 7 91 4 98 4 105 3 115 3 122 4 136 7 140 7 160 3 182 0 324 31 249 192Z"
-      />
-      <path
-        id="outline-down"
-        stroke="#fefefe"
-        stroke-width="2"
-        fill="none"
-        d="M139 127C139 127 49 81 27 152 6 223 51 357 130 364 205 370 244 331 249 181 249 181 259 75 139 127Z"
-      />
+      <path v-for="(d,i) in paths" :id="'outline-'+i" stroke="#fefefe" stroke-width="2" fill="none" :d="d" />
     </clipPath>
     </clipPath>
     <path
     <path
       id="outline"
       id="outline"
@@ -42,9 +53,7 @@
     />
     />
   </svg>
   </svg>
 </template>
 </template>
-
 <style scoped lang="scss">
 <style scoped lang="scss">
-svg {
-  opacity: 0.85;
-}
+svg { opacity: 0.85; }
 </style>
 </style>
+

+ 86 - 37
src/modules/camera/camera.vue

@@ -1,26 +1,43 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import { withResolvers }                                  from '@/tools';
-import { getMediaStream }                                 from '@/tools/camera.tool';
-import { tryOnMounted, tryOnUnmounted, useEventListener } from '@vueuse/core';
+import { withResolvers }                                                  from '@/tools';
+import { getMediaStream }                                                 from '@/tools/camera.tool';
+import { tryOnMounted, tryOnUnmounted, useElementSize, useEventListener } from '@vueuse/core';
 
 
 
 
-const { constraints = {} } = defineProps<{ constraints?: MediaTrackConstraints }>();
+let _stream: MediaStream;
+
+
+const { constraints = {} } = defineProps<{ constraints?: MediaTrackConstraints, multiple?: number; }>();
 const width = defineModel('width', { default: 0 });
 const width = defineModel('width', { default: 0 });
 const height = defineModel('height', { default: 0 });
 const height = defineModel('height', { default: 0 });
+const multiple = defineModel('multiple', { default: 1 });
+
+const offsetX = ref(0);
+const offsetY = ref(0);
+
+watch([ width, height, multiple ], ([ w, h, m ]) => {
+  applyConstraints({ width: w * m, height: h * m });
+});
+
 
 
 const styleValue = computed(() => {
 const styleValue = computed(() => {
   return width.value && height.value
   return width.value && height.value
-         ? {
-      width: `${ width.value }px`,
-      height: `${ height.value }px`,
-    }
+         ? { width: `${ width.value }px`, height: `${ height.value }px` }
          : void 0;
          : void 0;
 });
 });
 
 
+const containerStyleValue = reactive({
+  width: `${ width.value }px`,
+  height: `${ height.value }px`,
+});
+
 const container = useTemplateRef<HTMLElement>('camera-container');
 const container = useTemplateRef<HTMLElement>('camera-container');
 const videoRef = ref<HTMLVideoElement | null>(null);
 const videoRef = ref<HTMLVideoElement | null>(null);
 const canvasRef = ref<HTMLCanvasElement | null>(null);
 const canvasRef = ref<HTMLCanvasElement | null>(null);
 
 
+/* 根容器 宽高 */
+const root = useElementSize(() => container.value?.parentElement);
+
 const getVideoTrack = () => {
 const getVideoTrack = () => {
   return videoRef.value?.srcObject instanceof MediaStream
   return videoRef.value?.srcObject instanceof MediaStream
          ? videoRef.value.srcObject.getVideoTracks()[ 0 ]
          ? videoRef.value.srcObject.getVideoTracks()[ 0 ]
@@ -36,52 +53,81 @@ useEventListener(videoRef, 'canplay', (event) => {
   const settings = track.getSettings();
   const settings = track.getSettings();
   const { width: w = 1, height: h = 1, aspectRatio = w / h } = settings;
   const { width: w = 1, height: h = 1, aspectRatio = w / h } = settings;
   const reverse = aspectRatio > 1;
   const reverse = aspectRatio > 1;
-  width.value = reverse ? h : w;
-  height.value = reverse ? w : h;
-  video.setAttribute('width', `${ width.value }`);
-  video.setAttribute('height', `${ height.value }`);
-  canvas!.width = width.value;
-  canvas!.height = height.value;
+  const _width = reverse ? h : w;
+  const _height = reverse ? w : h;
+  video.setAttribute('width', `${ _width }`);
+  video.setAttribute('height', `${ _height }`);
+
+  containerStyleValue.width = `${ _width }px`;
+  containerStyleValue.height = `${ _height }px`;
+
+  offsetX.value = (
+                    _width - width.value
+                  ) / 2;
+  offsetY.value = (
+                    _height - height.value
+                  ) / 2;
 
 
   video.play().then(
   video.play().then(
     () => {},
     () => {},
     // (_) => (error.value = _)
     // (_) => (error.value = _)
   );
   );
   console.group('[log] camera:', '流媒体可播放');
   console.group('[log] camera:', '流媒体可播放');
-  console.log(`width=${ width.value }, height=${ height.value }`);
+  console.log(`width=${ _width }, height=${ _height }`);
   console.log(`浏览器支持: 可缩放 ${ !!(
   console.log(`浏览器支持: 可缩放 ${ !!(
     <any> navigator.mediaDevices.getSupportedConstraints()
     <any> navigator.mediaDevices.getSupportedConstraints()
   ).zoom }`);
   ).zoom }`);
   console.log(`摄像头支持: 可缩放 ${ !!(
   console.log(`摄像头支持: 可缩放 ${ !!(
     <any> track.getCapabilities()
     <any> track.getCapabilities()
   )[ 'zoom' ] }`);
   )[ 'zoom' ] }`);
+  console.log(`当前设置缩放: ${ multiple.value }`);
+  console.log(`当前实际缩放: (${ _width / width.value }, ${ _height / height.value })`);
+  console.log(`图象偏移距离: (${ offsetX.value }, ${ offsetY.value })`);
   console.groupEnd();
   console.groupEnd();
 });
 });
 
 
 tryOnMounted(() => {
 tryOnMounted(() => {
-  const style = getComputedStyle(container.value!);
-  width.value ??= Number.parseInt(style.width);
-  height.value ??= Number.parseInt(style.height);
-  init(container.value!, { width: width.value, height: height.value, ...constraints });
-});
+  const _container = container.value!;
+  canvasRef.value = container.value!.querySelector('canvas');
+  videoRef.value = container.value!.querySelector('video');
 
 
-tryOnUnmounted(() => {
-  getVideoTrack().stop();
+  const style = getComputedStyle(_container);
+  const _width = width.value || Number.parseInt(style.width);
+  const _height = height.value || Number.parseInt(style.height);
+  const _multiple = multiple.value ?? 1;
 
 
-  const video = videoRef.value;
-  if ( video ) {
-    video.pause();
-    video.srcObject = null;
-  }
+  canvasRef.value!.width = _width;
+  canvasRef.value!.height = _height;
+
+  applyConstraints({ ...constraints, width: _width * _multiple, height: _height * _multiple });
+});
 
 
+tryOnUnmounted(() => {
+  destroy();
   videoRef.value = null;
   videoRef.value = null;
   canvasRef.value = null;
   canvasRef.value = null;
 });
 });
 
 
-async function init(container: HTMLElement, constraints: MediaTrackConstraints) {
-  canvasRef.value = container.querySelector('canvas');
-  videoRef.value = container.querySelector('video');
-  videoRef.value!.srcObject = await getMediaStream(<any> constraints);
+async function applyConstraints(constraints: Partial<MediaTrackConstraints>) {
+  const { width, height, ..._constraints } = constraints;
+  // @ts-ignore
+  if ( !!width && !!height ) {
+    destroy();
+    _stream = await getMediaStream(<any> { ..._constraints, width, height, facingMode: 'user' });
+    videoRef.value!.srcObject = _stream;
+  } else {
+    await _stream.getVideoTracks()[ 0 ]?.applyConstraints(constraints);
+  }
+}
+
+function destroy() {
+  const tracks = _stream?.getTracks() ?? [];
+  for ( const track of tracks ) track.stop();
+  _stream = void 0 as any;
+  if ( videoRef.value ) {
+    videoRef.value.pause();
+    videoRef.value.srcObject = null as any;
+  }
 }
 }
 
 
 
 
@@ -92,7 +138,8 @@ const handle = async (promise: Promise<boolean>) => {
   if ( video.paused ) await video.play();
   if ( video.paused ) await video.play();
 
 
   const context = canvas.getContext('2d')!;
   const context = canvas.getContext('2d')!;
-  context.drawImage(video, 0, 0, canvas.width, canvas.height);
+  context.clearRect(0, 0, canvas.width, canvas.height);
+  context.drawImage(video, offsetX.value, offsetY.value, video.width, video.height, 0, 0, canvas.width, canvas.height);
   video.pause();
   video.pause();
 
 
   promise.then(value => video.paused && value ? video.play() : void 0);
   promise.then(value => video.paused && value ? video.play() : void 0);
@@ -119,20 +166,22 @@ defineExpose({
 });
 });
 </script>
 </script>
 <template>
 <template>
-  <div class="camera-container size-full" ref="camera-container" :style="styleValue">
+  <div class="camera-container size-full" ref="camera-container" :style="containerStyleValue">
     <canvas style="display: none;"></canvas>
     <canvas style="display: none;"></canvas>
     <video></video>
     <video></video>
-    <slot></slot>
+    <slot :style="styleValue" :offsetX="offsetX" :offsetY="offsetY"></slot>
   </div>
   </div>
 </template>
 </template>
 <style scoped lang="scss">
 <style scoped lang="scss">
 .camera-container {
 .camera-container {
-  position: relative;
+  position: absolute;
+  z-index: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 
 
   > * {
   > * {
     position: absolute;
     position: absolute;
-    width: 100%;
-    height: 100%;
   }
   }
 
 
   video {
   video {

+ 19 - 10
src/modules/camera/page.vue

@@ -24,6 +24,7 @@ const area = reactive({
   // height: 400,
   // height: 400,
   width: 270,
   width: 270,
   height: 366,
   height: 366,
+  multiple: 2,
 });
 });
 
 
 onBeforeMount(() => next());
 onBeforeMount(() => next());
@@ -165,18 +166,21 @@ const transparency = ref(1);
       <header class="flex flex-col justify-center px-24" style="flex: 1 1 20%">
       <header class="flex flex-col justify-center px-24" style="flex: 1 1 20%">
         <div class="text-3xl text-center" :class="{ required: config?.required }">{{ config?.label }}</div>
         <div class="text-3xl text-center" :class="{ required: config?.required }">{{ config?.label }}</div>
         <div class="mt-8 text-lg text-center tracking-wider leading-10">{{ config?.description }}</div>
         <div class="mt-8 text-lg text-center tracking-wider leading-10">{{ config?.description }}</div>
+        <div>w: {{ area.width }}, h: {{ area.height }}, m: {{ area.multiple }}</div>
       </header>
       </header>
       <main class="relative flex justify-center items-center" style="flex: 1 1 40%">
       <main class="relative flex justify-center items-center" style="flex: 1 1 40%">
-        <Camera ref="camera" v-bind="area">
-          <component :is="config?.shade"></component>
-          <img
-            v-if="showExample"
-            class="example"
-            :style="{ opacity: transparency }"
-            :src="config?.example"
-            alt="示例"
-            @click="showExample = false"
-          />
+        <Camera ref="camera" v-model:width="area.width" v-model:height="area.height" v-model:multiple="area.multiple">
+          <template #default="{style, offsetX, offsetY}">
+            <component :is="config?.shade" :style="style" :translateX="offsetX" :translateY="offsetY"></component>
+            <img
+              v-if="showExample"
+              class="example"
+              :style="{ opacity: transparency, ...style }"
+              :src="config?.example"
+              alt="示例"
+              @click="showExample = false"
+            />
+          </template>
         </Camera>
         </Camera>
         <div
         <div
           v-if="config?.example"
           v-if="config?.example"
@@ -211,4 +215,9 @@ const transparency = ref(1);
     color: #f53030;
     color: #f53030;
   }
   }
 }
 }
+
+header, footer {
+  position: relative;
+  z-index: 11;
+}
 </style>
 </style>