|
1 | 1 | <script lang="ts"> |
| 2 | + import { boundingBoxesArray, type Faces } from '$lib/stores/people.store'; |
2 | 3 | import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store'; |
3 | 4 | import { |
4 | 5 | EquirectangularAdapter, |
|
8 | 9 | type PluginConstructor, |
9 | 10 | } from '@photo-sphere-viewer/core'; |
10 | 11 | import '@photo-sphere-viewer/core/index.css'; |
| 12 | + import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin'; |
| 13 | + import '@photo-sphere-viewer/markers-plugin/index.css'; |
11 | 14 | import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin'; |
12 | 15 | import { SettingsPlugin } from '@photo-sphere-viewer/settings-plugin'; |
13 | 16 | import '@photo-sphere-viewer/settings-plugin/index.css'; |
14 | 17 | import { onDestroy, onMount } from 'svelte'; |
15 | 18 |
|
| 19 | + // Adapted as well as possible from classlist 'border-solid border-white border-3 rounded-lg' |
| 20 | + const FACE_BOX_SVG_STYLE = { |
| 21 | + fill: 'rgba(0, 0, 0, 0)', |
| 22 | + stroke: '#ffffff', |
| 23 | + strokeWidth: '3px', |
| 24 | + strokeLinejoin: 'round', |
| 25 | + }; |
| 26 | +
|
16 | 27 | interface Props { |
17 | 28 | panorama: string | { source: string }; |
18 | 29 | originalPanorama?: string | { source: string }; |
|
26 | 37 | let container: HTMLDivElement | undefined = $state(); |
27 | 38 | let viewer: Viewer; |
28 | 39 |
|
| 40 | + let animationInProgress: { cancel: () => void } | undefined; |
| 41 | + let previousFaces: Faces[] = []; |
| 42 | +
|
| 43 | + const boundingBoxesUnsubscribe = boundingBoxesArray.subscribe((faces: Faces[]) => { |
| 44 | + // Debounce; don't do anything when the data didn't actually change. |
| 45 | + if (faces === previousFaces) { |
| 46 | + return; |
| 47 | + } |
| 48 | + previousFaces = faces; |
| 49 | +
|
| 50 | + if (animationInProgress) { |
| 51 | + animationInProgress.cancel(); |
| 52 | + animationInProgress = undefined; |
| 53 | + } |
| 54 | + if (!viewer || !viewer.state.textureData || !viewer.getPlugin(MarkersPlugin)) { |
| 55 | + return; |
| 56 | + } |
| 57 | + const markersPlugin = viewer.getPlugin<MarkersPlugin>(MarkersPlugin); |
| 58 | +
|
| 59 | + // croppedWidth is the size of the texture, which might be cropped to be less than 360/180 degrees. |
| 60 | + // This is what we want because the facial recognition is done on the image, not the sphere. |
| 61 | + const currentTextureWidth = viewer.state.textureData.panoData.croppedWidth; |
| 62 | +
|
| 63 | + markersPlugin.clearMarkers(); |
| 64 | + for (const [index, face] of faces.entries()) { |
| 65 | + const { boundingBoxX1: x1, boundingBoxY1: y1, boundingBoxX2: x2, boundingBoxY2: y2 } = face; |
| 66 | + const ratio = currentTextureWidth / face.imageWidth; |
| 67 | + // Pixel values are translated to spherical coordinates and only then added to the panorama; |
| 68 | + // no need to recalculate when the texture image changes to the original size. |
| 69 | + markersPlugin.addMarker({ |
| 70 | + id: `face_${index}`, |
| 71 | + polygonPixels: [ |
| 72 | + [x1 * ratio, y1 * ratio], |
| 73 | + [x2 * ratio, y1 * ratio], |
| 74 | + [x2 * ratio, y2 * ratio], |
| 75 | + [x1 * ratio, y2 * ratio], |
| 76 | + ], |
| 77 | + svgStyle: FACE_BOX_SVG_STYLE, |
| 78 | + }); |
| 79 | + } |
| 80 | +
|
| 81 | + // Smoothly pan to the highlighted (hovered-over) face. |
| 82 | + if (faces.length === 1) { |
| 83 | + const { boundingBoxX1: x1, boundingBoxY1: y1, boundingBoxX2: x2, boundingBoxY2: y2, imageWidth: w } = faces[0]; |
| 84 | + const ratio = currentTextureWidth / w; |
| 85 | + const x = ((x1 + x2) * ratio) / 2; |
| 86 | + const y = ((y1 + y2) * ratio) / 2; |
| 87 | + animationInProgress = viewer.animate({ |
| 88 | + textureX: x, |
| 89 | + textureY: y, |
| 90 | + zoom: Math.min(viewer.getZoomLevel(), 75), |
| 91 | + speed: 500, // duration in ms |
| 92 | + }); |
| 93 | + } |
| 94 | + }); |
| 95 | +
|
29 | 96 | onMount(() => { |
30 | 97 | if (!container) { |
31 | 98 | return; |
|
34 | 101 | viewer = new Viewer({ |
35 | 102 | adapter, |
36 | 103 | plugins: [ |
| 104 | + MarkersPlugin, |
37 | 105 | SettingsPlugin, |
38 | 106 | [ |
39 | 107 | ResolutionPlugin, |
|
68 | 136 | zoomSpeed: 0.5, |
69 | 137 | fisheye: false, |
70 | 138 | }); |
71 | | - const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin; |
| 139 | + const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin); |
72 | 140 | const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => { |
73 | 141 | // zoomLevel range: [0, 100] |
74 | 142 | if (Math.round(zoomLevel) >= 75) { |
|
89 | 157 | if (viewer) { |
90 | 158 | viewer.destroy(); |
91 | 159 | } |
| 160 | + boundingBoxesUnsubscribe(); |
92 | 161 | }); |
93 | 162 | </script> |
94 | 163 |
|
|
0 commit comments