mirror of
https://github.com/immich-app/immich.git
synced 2026-02-14 04:47:57 +03:00
wip: panorama tiling
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getAssetOriginalUrl, getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
|
||||
import { getAssetThumbnailUrl, getAssetTileUrl } from '$lib/utils';
|
||||
import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk';
|
||||
import { LoadingSpinner } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -13,21 +11,30 @@
|
||||
|
||||
const { asset }: Props = $props();
|
||||
|
||||
const loadAssetData = async (id: string) => {
|
||||
const data = await viewAsset({ ...authManager.params, id, size: AssetMediaSize.Preview });
|
||||
return URL.createObjectURL(data);
|
||||
};
|
||||
const tileconfig =
|
||||
asset.id === '6e899018-32fe-4fd5-b6ac-b3a525b8e61f'
|
||||
? {
|
||||
width: 12_988,
|
||||
// height: 35,
|
||||
cols: 16,
|
||||
rows: 8,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const baseUrl = getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Preview, cacheKey: asset.thumbhash });
|
||||
// TODO: determine whether to return null based on 1. if asset has tiles, 2. if tile is inside 'cropped' bounds.
|
||||
const tileUrl = (col: number, row: number, level: number) =>
|
||||
tileconfig ? getAssetTileUrl({ id: asset.id, level, col, row, cacheKey: asset.thumbhash }) : null;
|
||||
</script>
|
||||
|
||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||
{#await Promise.all([loadAssetData(asset.id), import('./photo-sphere-viewer-adapter.svelte')])}
|
||||
{#await import('./photo-sphere-viewer-adapter.svelte')}
|
||||
<LoadingSpinner />
|
||||
{:then [data, { default: PhotoSphereViewer }]}
|
||||
{:then { default: PhotoSphereViewer }}
|
||||
<PhotoSphereViewer
|
||||
panorama={data}
|
||||
originalPanorama={isWebCompatibleImage(asset)
|
||||
? getAssetOriginalUrl(asset.id)
|
||||
: getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Fullsize, cacheKey: asset.thumbhash })}
|
||||
{baseUrl}
|
||||
{tileUrl}
|
||||
{tileconfig}
|
||||
/>
|
||||
{:catch}
|
||||
{$t('errors.failed_to_load_asset')}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { boundingBoxesArray, type Faces } from '$lib/stores/people.store';
|
||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
||||
import {
|
||||
EquirectangularAdapter,
|
||||
Viewer,
|
||||
events,
|
||||
type AdapterConstructor,
|
||||
type PluginConstructor,
|
||||
} from '@photo-sphere-viewer/core';
|
||||
import { EquirectangularAdapter, Viewer, type PluginConstructor } from '@photo-sphere-viewer/core';
|
||||
import '@photo-sphere-viewer/core/index.css';
|
||||
import { EquirectangularTilesAdapter } from '@photo-sphere-viewer/equirectangular-tiles-adapter';
|
||||
import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin';
|
||||
import '@photo-sphere-viewer/markers-plugin/index.css';
|
||||
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
|
||||
@@ -24,15 +18,30 @@
|
||||
strokeLinejoin: 'round',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
panorama: string | { source: string };
|
||||
originalPanorama?: string | { source: string };
|
||||
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
||||
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
||||
navbar?: boolean;
|
||||
}
|
||||
const SHARED_VIEWER_CONFIG = {
|
||||
touchmoveTwoFingers: false,
|
||||
mousewheelCtrlKey: false,
|
||||
navbar: false,
|
||||
minFov: 15,
|
||||
maxFov: 90,
|
||||
zoomSpeed: 0.5,
|
||||
fisheye: false,
|
||||
};
|
||||
|
||||
let { panorama, originalPanorama, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props();
|
||||
type TileConfig = {
|
||||
width: number;
|
||||
cols: number;
|
||||
rows: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
baseUrl: string;
|
||||
tileUrl: ((col: number, row: number, level: number) => string | null) | undefined;
|
||||
tileconfig: TileConfig | undefined;
|
||||
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
||||
};
|
||||
|
||||
let { baseUrl, tileUrl, tileconfig, plugins = [] }: Props = $props();
|
||||
|
||||
let container: HTMLDivElement | undefined = $state();
|
||||
let viewer: Viewer;
|
||||
@@ -98,59 +107,29 @@
|
||||
return;
|
||||
}
|
||||
|
||||
viewer = new Viewer({
|
||||
adapter,
|
||||
plugins: [
|
||||
MarkersPlugin,
|
||||
SettingsPlugin,
|
||||
[
|
||||
ResolutionPlugin,
|
||||
{
|
||||
defaultResolution: $alwaysLoadOriginalFile && originalPanorama ? 'original' : 'default',
|
||||
resolutions: [
|
||||
{
|
||||
id: 'default',
|
||||
label: 'Default',
|
||||
panorama,
|
||||
},
|
||||
...(originalPanorama
|
||||
? [
|
||||
{
|
||||
id: 'original',
|
||||
label: 'Original',
|
||||
panorama: originalPanorama,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
viewer = tileconfig ? new Viewer({
|
||||
adapter: EquirectangularTilesAdapter,
|
||||
panorama: {
|
||||
...tileconfig,
|
||||
baseUrl,
|
||||
tileUrl,
|
||||
},
|
||||
plugins: [
|
||||
MarkersPlugin,
|
||||
...plugins,
|
||||
],
|
||||
...plugins,
|
||||
],
|
||||
container,
|
||||
touchmoveTwoFingers: false,
|
||||
mousewheelCtrlKey: false,
|
||||
navbar,
|
||||
minFov: 15,
|
||||
maxFov: 90,
|
||||
zoomSpeed: 0.5,
|
||||
fisheye: false,
|
||||
});
|
||||
const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
|
||||
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
||||
// zoomLevel range: [0, 100]
|
||||
if (Math.round(zoomLevel) >= 75) {
|
||||
// Replace the preview with the original
|
||||
void resolutionPlugin.setResolution('original');
|
||||
viewer.removeEventListener(events.ZoomUpdatedEvent.type, zoomHandler);
|
||||
}
|
||||
};
|
||||
|
||||
if (originalPanorama && !$alwaysLoadOriginalFile) {
|
||||
viewer.addEventListener(events.ZoomUpdatedEvent.type, zoomHandler, { passive: true });
|
||||
}
|
||||
|
||||
return () => viewer.removeEventListener(events.ZoomUpdatedEvent.type, zoomHandler);
|
||||
container,
|
||||
...SHARED_VIEWER_CONFIG,
|
||||
}) : new Viewer({
|
||||
adapter: EquirectangularAdapter,
|
||||
panorama: baseUrl,
|
||||
plugins: [
|
||||
MarkersPlugin,
|
||||
...plugins,
|
||||
],
|
||||
container,
|
||||
...SHARED_VIEWER_CONFIG,
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<LoadingSpinner />
|
||||
{:then [PhotoSphereViewer, adapter, videoPlugin]}
|
||||
<PhotoSphereViewer
|
||||
panorama={{ source: getAssetPlaybackUrl(assetId) }}
|
||||
baseUrl={{ source: getAssetPlaybackUrl(assetId) }}
|
||||
originalPanorama={{ source: getAssetOriginalUrl(assetId) }}
|
||||
plugins={[videoPlugin]}
|
||||
{adapter}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getAssetOriginalPath,
|
||||
getAssetPlaybackPath,
|
||||
getAssetThumbnailPath,
|
||||
getAssetTilePath,
|
||||
getBaseUrl,
|
||||
getPeopleThumbnailPath,
|
||||
getUserProfileImagePath,
|
||||
@@ -215,6 +216,13 @@ export const getAssetPlaybackUrl = (options: string | AssetUrlOptions) => {
|
||||
return createUrl(getAssetPlaybackPath(id), { ...authManager.params, c: cacheKey });
|
||||
};
|
||||
|
||||
type TileUrlOptions = { id: string, level: number, col: number, row: number, cacheKey?: string | null };
|
||||
|
||||
export const getAssetTileUrl = (options: TileUrlOptions) => {
|
||||
const { id, level, col, row, cacheKey } = options;
|
||||
return createUrl(getAssetTilePath(id, level, col, row), { ...authManager.params, c: cacheKey });
|
||||
}
|
||||
|
||||
export const getProfileImageUrl = (user: UserResponseDto) =>
|
||||
createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user