@gradio/imageslider 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1115 -0
- package/Example.svelte +69 -0
- package/Index.svelte +206 -0
- package/LICENSE +201 -0
- package/README.md +67 -0
- package/dist/Example.svelte +68 -0
- package/dist/Example.svelte.d.ts +19 -0
- package/dist/Index.svelte +178 -0
- package/dist/Index.svelte.d.ts +153 -0
- package/dist/img_01.png +0 -0
- package/dist/img_02.png +0 -0
- package/dist/img_03.png +0 -0
- package/dist/shared/ClearImage.svelte +28 -0
- package/dist/shared/ClearImage.svelte.d.ts +18 -0
- package/dist/shared/Image.svelte +208 -0
- package/dist/shared/Image.svelte.d.ts +41 -0
- package/dist/shared/ImageEl.svelte +119 -0
- package/dist/shared/ImageEl.svelte.d.ts +258 -0
- package/dist/shared/Slider.svelte +183 -0
- package/dist/shared/Slider.svelte.d.ts +28 -0
- package/dist/shared/SliderPreview.svelte +201 -0
- package/dist/shared/SliderPreview.svelte.d.ts +32 -0
- package/dist/shared/SliderUpload.svelte +41 -0
- package/dist/shared/SliderUpload.svelte.d.ts +71 -0
- package/dist/shared/zoom.d.ts +69 -0
- package/dist/shared/zoom.js +349 -0
- package/img_01.png +0 -0
- package/img_02.png +0 -0
- package/img_03.png +0 -0
- package/package.json +47 -0
- package/shared/ClearImage.svelte +30 -0
- package/shared/Image.svelte +235 -0
- package/shared/ImageEl.svelte +165 -0
- package/shared/Slider.svelte +200 -0
- package/shared/SliderPreview.svelte +229 -0
- package/shared/SliderUpload.svelte +46 -0
- package/shared/zoom.ts +487 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import type { HTMLImgAttributes } from "svelte/elements";
|
3
|
+
import { createEventDispatcher, onMount, tick } from "svelte";
|
4
|
+
interface Props extends HTMLImgAttributes {
|
5
|
+
"data-testid"?: string;
|
6
|
+
fixed?: boolean;
|
7
|
+
transform?: string;
|
8
|
+
img_el?: HTMLImageElement;
|
9
|
+
hidden?: boolean;
|
10
|
+
variant?: "preview" | "upload";
|
11
|
+
max_height?: number;
|
12
|
+
fullscreen?: boolean;
|
13
|
+
}
|
14
|
+
type $$Props = Props;
|
15
|
+
|
16
|
+
import { resolve_wasm_src } from "@gradio/wasm/svelte";
|
17
|
+
|
18
|
+
export let src: HTMLImgAttributes["src"] = undefined;
|
19
|
+
export let fullscreen = false;
|
20
|
+
|
21
|
+
let resolved_src: typeof src;
|
22
|
+
|
23
|
+
export let fixed = false;
|
24
|
+
export let transform = "translate(0px, 0px) scale(1)";
|
25
|
+
export let img_el: HTMLImageElement | null = null;
|
26
|
+
export let hidden = false;
|
27
|
+
export let variant = "upload";
|
28
|
+
export let max_height = 500;
|
29
|
+
// The `src` prop can be updated before the Promise from `resolve_wasm_src` is resolved.
|
30
|
+
// In such a case, the resolved value for the old `src` has to be discarded,
|
31
|
+
// This variable `latest_src` is used to pick up only the value resolved for the latest `src` prop.
|
32
|
+
let latest_src: typeof src;
|
33
|
+
$: {
|
34
|
+
// In normal (non-Wasm) Gradio, the `<img>` element should be rendered with the passed `src` props immediately
|
35
|
+
// without waiting for `resolve_wasm_src()` to resolve.
|
36
|
+
// If it waits, a blank image is displayed until the async task finishes
|
37
|
+
// and it leads to undesirable flickering.
|
38
|
+
// So set `src` to `resolved_src` here.
|
39
|
+
resolved_src = src;
|
40
|
+
|
41
|
+
latest_src = src;
|
42
|
+
const resolving_src = src;
|
43
|
+
resolve_wasm_src(resolving_src).then((s) => {
|
44
|
+
if (latest_src === resolving_src) {
|
45
|
+
resolved_src = s;
|
46
|
+
}
|
47
|
+
});
|
48
|
+
}
|
49
|
+
|
50
|
+
const dispatch = createEventDispatcher<{
|
51
|
+
load: {
|
52
|
+
top: number;
|
53
|
+
left: number;
|
54
|
+
width: number;
|
55
|
+
height: number;
|
56
|
+
};
|
57
|
+
}>();
|
58
|
+
|
59
|
+
function get_image_size(img: HTMLImageElement | null): {
|
60
|
+
top: number;
|
61
|
+
left: number;
|
62
|
+
width: number;
|
63
|
+
height: number;
|
64
|
+
} {
|
65
|
+
if (!img) return { top: 0, left: 0, width: 0, height: 0 };
|
66
|
+
const container = img.parentElement?.getBoundingClientRect();
|
67
|
+
|
68
|
+
if (!container) return { top: 0, left: 0, width: 0, height: 0 };
|
69
|
+
|
70
|
+
const naturalAspect = img.naturalWidth / img.naturalHeight;
|
71
|
+
const containerAspect = container.width / container.height;
|
72
|
+
let displayedWidth, displayedHeight;
|
73
|
+
|
74
|
+
if (naturalAspect > containerAspect) {
|
75
|
+
displayedWidth = container.width;
|
76
|
+
displayedHeight = container.width / naturalAspect;
|
77
|
+
} else {
|
78
|
+
displayedHeight = container.height;
|
79
|
+
displayedWidth = container.height * naturalAspect;
|
80
|
+
}
|
81
|
+
|
82
|
+
const offsetX = (container.width - displayedWidth) / 2;
|
83
|
+
const offsetY = (container.height - displayedHeight) / 2;
|
84
|
+
|
85
|
+
return {
|
86
|
+
top: offsetY,
|
87
|
+
left: offsetX,
|
88
|
+
width: displayedWidth,
|
89
|
+
height: displayedHeight
|
90
|
+
};
|
91
|
+
}
|
92
|
+
|
93
|
+
onMount(() => {
|
94
|
+
const resizer = new ResizeObserver(async (entries) => {
|
95
|
+
for (const entry of entries) {
|
96
|
+
await tick();
|
97
|
+
dispatch("load", get_image_size(img_el));
|
98
|
+
}
|
99
|
+
});
|
100
|
+
|
101
|
+
resizer.observe(img_el!);
|
102
|
+
|
103
|
+
return () => {
|
104
|
+
resizer.disconnect();
|
105
|
+
};
|
106
|
+
});
|
107
|
+
</script>
|
108
|
+
|
109
|
+
<!-- svelte-ignore a11y-missing-attribute -->
|
110
|
+
<img
|
111
|
+
src={resolved_src}
|
112
|
+
{...$$restProps}
|
113
|
+
class:fixed
|
114
|
+
style:transform
|
115
|
+
bind:this={img_el}
|
116
|
+
class:hidden
|
117
|
+
class:preview={variant === "preview"}
|
118
|
+
class:slider={variant === "upload"}
|
119
|
+
style:max-height={max_height && !fullscreen ? `${max_height}px` : null}
|
120
|
+
class:fullscreen
|
121
|
+
class:small={!fullscreen}
|
122
|
+
on:load={() => dispatch("load", get_image_size(img_el))}
|
123
|
+
/>
|
124
|
+
|
125
|
+
<style>
|
126
|
+
.preview {
|
127
|
+
object-fit: contain;
|
128
|
+
width: 100%;
|
129
|
+
transform-origin: top left;
|
130
|
+
margin: auto;
|
131
|
+
}
|
132
|
+
|
133
|
+
.small {
|
134
|
+
max-height: 500px;
|
135
|
+
}
|
136
|
+
|
137
|
+
.upload {
|
138
|
+
object-fit: contain;
|
139
|
+
max-height: 500px;
|
140
|
+
}
|
141
|
+
|
142
|
+
.fixed {
|
143
|
+
position: absolute;
|
144
|
+
top: 0;
|
145
|
+
left: 0;
|
146
|
+
right: 0;
|
147
|
+
bottom: 0;
|
148
|
+
}
|
149
|
+
|
150
|
+
.fullscreen {
|
151
|
+
width: 100%;
|
152
|
+
height: 100%;
|
153
|
+
}
|
154
|
+
|
155
|
+
:global(.image-container:fullscreen) img {
|
156
|
+
width: 100%;
|
157
|
+
height: 100%;
|
158
|
+
max-height: none;
|
159
|
+
max-width: none;
|
160
|
+
}
|
161
|
+
|
162
|
+
.hidden {
|
163
|
+
opacity: 0;
|
164
|
+
}
|
165
|
+
</style>
|
@@ -0,0 +1,200 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import { onMount } from "svelte";
|
3
|
+
import { drag } from "d3-drag";
|
4
|
+
import { select } from "d3-selection";
|
5
|
+
|
6
|
+
function clamp(value: number, min: number, max: number): number {
|
7
|
+
return Math.min(Math.max(value, min), max);
|
8
|
+
}
|
9
|
+
|
10
|
+
export let position = 0.5;
|
11
|
+
export let disabled = false;
|
12
|
+
|
13
|
+
export let slider_color = "var(--border-color-primary)";
|
14
|
+
export let image_size: {
|
15
|
+
top: number;
|
16
|
+
left: number;
|
17
|
+
width: number;
|
18
|
+
height: number;
|
19
|
+
} = { top: 0, left: 0, width: 0, height: 0 };
|
20
|
+
export let el: HTMLDivElement | undefined = undefined;
|
21
|
+
export let parent_el: HTMLDivElement | undefined = undefined;
|
22
|
+
let inner: Element;
|
23
|
+
let px = 0;
|
24
|
+
let active = false;
|
25
|
+
let container_width = 0;
|
26
|
+
|
27
|
+
function set_position(width: number): void {
|
28
|
+
container_width = parent_el?.getBoundingClientRect().width || 0;
|
29
|
+
if (width === 0) {
|
30
|
+
image_size.width = el?.getBoundingClientRect().width || 0;
|
31
|
+
}
|
32
|
+
|
33
|
+
px = clamp(
|
34
|
+
image_size.width * position + image_size.left,
|
35
|
+
0,
|
36
|
+
container_width
|
37
|
+
);
|
38
|
+
}
|
39
|
+
|
40
|
+
function round(n: number, points: number): number {
|
41
|
+
const mod = Math.pow(10, points);
|
42
|
+
return Math.round((n + Number.EPSILON) * mod) / mod;
|
43
|
+
}
|
44
|
+
|
45
|
+
function update_position(x: number): void {
|
46
|
+
px = clamp(x, 0, container_width);
|
47
|
+
position = round((x - image_size.left) / image_size.width, 5);
|
48
|
+
}
|
49
|
+
|
50
|
+
function drag_start(event: any): void {
|
51
|
+
if (disabled) return;
|
52
|
+
active = true;
|
53
|
+
update_position(event.x);
|
54
|
+
}
|
55
|
+
|
56
|
+
function drag_move(event: any): void {
|
57
|
+
if (disabled) return;
|
58
|
+
update_position(event.x);
|
59
|
+
}
|
60
|
+
|
61
|
+
function drag_end(): void {
|
62
|
+
if (disabled) return;
|
63
|
+
active = false;
|
64
|
+
}
|
65
|
+
|
66
|
+
function update_position_from_pc(pc: number): void {
|
67
|
+
px = clamp(image_size.width * pc + image_size.left, 0, container_width);
|
68
|
+
}
|
69
|
+
|
70
|
+
$: set_position(image_size.width);
|
71
|
+
$: update_position_from_pc(position);
|
72
|
+
|
73
|
+
onMount(() => {
|
74
|
+
set_position(image_size.width);
|
75
|
+
const drag_handler = drag()
|
76
|
+
.on("start", drag_start)
|
77
|
+
.on("drag", drag_move)
|
78
|
+
.on("end", drag_end);
|
79
|
+
select(inner).call(drag_handler);
|
80
|
+
});
|
81
|
+
</script>
|
82
|
+
|
83
|
+
<svelte:window on:resize={() => set_position(image_size.width)} />
|
84
|
+
|
85
|
+
<div class="wrap" role="none" bind:this={parent_el}>
|
86
|
+
<div class="content" bind:this={el}>
|
87
|
+
<slot />
|
88
|
+
</div>
|
89
|
+
<div
|
90
|
+
class="outer"
|
91
|
+
class:disabled
|
92
|
+
bind:this={inner}
|
93
|
+
role="none"
|
94
|
+
style="transform: translateX({px}px)"
|
95
|
+
class:grab={active}
|
96
|
+
>
|
97
|
+
<span class="icon-wrap" class:active class:disabled
|
98
|
+
><span class="icon left">◢</span><span
|
99
|
+
class="icon center"
|
100
|
+
style:--color={slider_color}
|
101
|
+
></span><span class="icon right">◢</span></span
|
102
|
+
>
|
103
|
+
<div class="inner" style:--color={slider_color}></div>
|
104
|
+
</div>
|
105
|
+
</div>
|
106
|
+
|
107
|
+
<style>
|
108
|
+
.wrap {
|
109
|
+
position: relative;
|
110
|
+
width: 100%;
|
111
|
+
height: 100%;
|
112
|
+
z-index: var(--layer-1);
|
113
|
+
overflow: hidden;
|
114
|
+
}
|
115
|
+
|
116
|
+
.icon-wrap {
|
117
|
+
display: block;
|
118
|
+
position: absolute;
|
119
|
+
top: 50%;
|
120
|
+
transform: translate(-20.5px, -50%);
|
121
|
+
left: 10px;
|
122
|
+
width: 40px;
|
123
|
+
transition: 0.2s;
|
124
|
+
color: var(--body-text-color);
|
125
|
+
height: 30px;
|
126
|
+
border-radius: 5px;
|
127
|
+
background-color: var(--color-accent);
|
128
|
+
display: flex;
|
129
|
+
align-items: center;
|
130
|
+
justify-content: center;
|
131
|
+
z-index: var(--layer-3);
|
132
|
+
box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.3);
|
133
|
+
font-size: 12px;
|
134
|
+
}
|
135
|
+
|
136
|
+
.icon.left {
|
137
|
+
transform: rotate(135deg);
|
138
|
+
text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1);
|
139
|
+
}
|
140
|
+
|
141
|
+
.icon.right {
|
142
|
+
transform: rotate(-45deg);
|
143
|
+
text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1);
|
144
|
+
}
|
145
|
+
|
146
|
+
.icon.center {
|
147
|
+
display: block;
|
148
|
+
width: 1px;
|
149
|
+
height: 100%;
|
150
|
+
background-color: var(--color);
|
151
|
+
opacity: 0.1;
|
152
|
+
}
|
153
|
+
|
154
|
+
.icon-wrap.active {
|
155
|
+
opacity: 0;
|
156
|
+
}
|
157
|
+
|
158
|
+
.icon-wrap.disabled {
|
159
|
+
opacity: 0;
|
160
|
+
}
|
161
|
+
|
162
|
+
.outer {
|
163
|
+
width: 20px;
|
164
|
+
height: 100%;
|
165
|
+
position: absolute;
|
166
|
+
cursor: grab;
|
167
|
+
position: absolute;
|
168
|
+
top: 0;
|
169
|
+
left: -10px;
|
170
|
+
pointer-events: auto;
|
171
|
+
z-index: var(--layer-2);
|
172
|
+
}
|
173
|
+
.grab {
|
174
|
+
cursor: grabbing;
|
175
|
+
}
|
176
|
+
|
177
|
+
.inner {
|
178
|
+
width: 1px;
|
179
|
+
height: 100%;
|
180
|
+
background: var(--color);
|
181
|
+
position: absolute;
|
182
|
+
left: calc((100% - 2px) / 2);
|
183
|
+
}
|
184
|
+
|
185
|
+
.disabled {
|
186
|
+
cursor: auto;
|
187
|
+
}
|
188
|
+
|
189
|
+
.disabled .inner {
|
190
|
+
box-shadow: none;
|
191
|
+
}
|
192
|
+
|
193
|
+
.content {
|
194
|
+
width: 100%;
|
195
|
+
height: 100%;
|
196
|
+
display: flex;
|
197
|
+
justify-content: center;
|
198
|
+
align-items: center;
|
199
|
+
}
|
200
|
+
</style>
|
@@ -0,0 +1,229 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import Slider from "./Slider.svelte";
|
3
|
+
import ImageEl from "./ImageEl.svelte";
|
4
|
+
import {
|
5
|
+
BlockLabel,
|
6
|
+
Empty,
|
7
|
+
IconButton,
|
8
|
+
IconButtonWrapper,
|
9
|
+
FullscreenButton
|
10
|
+
} from "@gradio/atoms";
|
11
|
+
import { Image, Download, Undo, Clear } from "@gradio/icons";
|
12
|
+
import { type FileData } from "@gradio/client";
|
13
|
+
import type { I18nFormatter } from "@gradio/utils";
|
14
|
+
import { DownloadLink } from "@gradio/wasm/svelte";
|
15
|
+
import { ZoomableImage } from "./zoom";
|
16
|
+
import { onMount } from "svelte";
|
17
|
+
import { tweened, type Tweened } from "svelte/motion";
|
18
|
+
import { createEventDispatcher } from "svelte";
|
19
|
+
|
20
|
+
export let value: [null | FileData, null | FileData] = [null, null];
|
21
|
+
export let label: string | undefined = undefined;
|
22
|
+
export let show_download_button = true;
|
23
|
+
export let show_label: boolean;
|
24
|
+
export let i18n: I18nFormatter;
|
25
|
+
export let position: number;
|
26
|
+
export let layer_images = true;
|
27
|
+
export let show_single = false;
|
28
|
+
export let slider_color: string;
|
29
|
+
export let show_fullscreen_button = true;
|
30
|
+
export let el_width = 0;
|
31
|
+
export let max_height: number;
|
32
|
+
export let interactive = true;
|
33
|
+
const dispatch = createEventDispatcher<{ clear: void }>();
|
34
|
+
|
35
|
+
let img: HTMLImageElement;
|
36
|
+
let slider_wrap: HTMLDivElement;
|
37
|
+
let image_container: HTMLDivElement;
|
38
|
+
|
39
|
+
let transform: Tweened<{ x: number; y: number; z: number }> = tweened(
|
40
|
+
{ x: 0, y: 0, z: 1 },
|
41
|
+
{
|
42
|
+
duration: 75
|
43
|
+
}
|
44
|
+
);
|
45
|
+
let parent_el: HTMLDivElement;
|
46
|
+
|
47
|
+
$: coords_at_viewport = get_coords_at_viewport(
|
48
|
+
position,
|
49
|
+
viewport_width,
|
50
|
+
image_size.width,
|
51
|
+
image_size.left,
|
52
|
+
$transform.x,
|
53
|
+
$transform.z
|
54
|
+
);
|
55
|
+
$: style = layer_images
|
56
|
+
? `clip-path: inset(0 0 0 ${coords_at_viewport * 100}%)`
|
57
|
+
: "";
|
58
|
+
|
59
|
+
function get_coords_at_viewport(
|
60
|
+
viewport_percent_x: number, // 0-1
|
61
|
+
viewportWidth: number,
|
62
|
+
image_width: number,
|
63
|
+
img_offset_x: number,
|
64
|
+
tx: number, // image translation x (in pixels)
|
65
|
+
scale: number // image scale (uniform)
|
66
|
+
): number {
|
67
|
+
const px_relative_to_image = viewport_percent_x * image_width;
|
68
|
+
const pixel_position = px_relative_to_image + img_offset_x;
|
69
|
+
|
70
|
+
const normalised_position = (pixel_position - tx) / scale;
|
71
|
+
const percent_position = normalised_position / viewportWidth;
|
72
|
+
|
73
|
+
return percent_position;
|
74
|
+
}
|
75
|
+
|
76
|
+
let img_width = 0;
|
77
|
+
let viewport_width = 0;
|
78
|
+
|
79
|
+
let zoomable_image: ZoomableImage | null = null;
|
80
|
+
let observer: ResizeObserver | null = null;
|
81
|
+
|
82
|
+
function init_image(
|
83
|
+
img: HTMLImageElement,
|
84
|
+
slider_wrap: HTMLDivElement
|
85
|
+
): void {
|
86
|
+
if (!img || !slider_wrap) return;
|
87
|
+
zoomable_image?.destroy();
|
88
|
+
observer?.disconnect();
|
89
|
+
img_width = img?.getBoundingClientRect().width || 0;
|
90
|
+
viewport_width = slider_wrap?.getBoundingClientRect().width || 0;
|
91
|
+
zoomable_image = new ZoomableImage(slider_wrap, img);
|
92
|
+
zoomable_image.subscribe(({ x, y, scale }) => {
|
93
|
+
transform.set({ x, y, z: scale });
|
94
|
+
});
|
95
|
+
|
96
|
+
observer = new ResizeObserver((entries) => {
|
97
|
+
for (const entry of entries) {
|
98
|
+
if (entry.target === slider_wrap) {
|
99
|
+
viewport_width = entry.contentRect.width;
|
100
|
+
}
|
101
|
+
|
102
|
+
if (entry.target === img) {
|
103
|
+
img_width = entry.contentRect.width;
|
104
|
+
}
|
105
|
+
}
|
106
|
+
});
|
107
|
+
observer.observe(slider_wrap);
|
108
|
+
observer.observe(img);
|
109
|
+
}
|
110
|
+
|
111
|
+
$: init_image(img, slider_wrap);
|
112
|
+
|
113
|
+
onMount(() => {
|
114
|
+
return () => {
|
115
|
+
zoomable_image?.destroy();
|
116
|
+
observer?.disconnect();
|
117
|
+
};
|
118
|
+
});
|
119
|
+
|
120
|
+
let is_full_screen = false;
|
121
|
+
let slider_wrap_parent: HTMLDivElement;
|
122
|
+
|
123
|
+
let image_size: { top: number; left: number; width: number; height: number } =
|
124
|
+
{ top: 0, left: 0, width: 0, height: 0 };
|
125
|
+
|
126
|
+
function handle_image_load(event: CustomEvent): void {
|
127
|
+
image_size = event.detail;
|
128
|
+
}
|
129
|
+
</script>
|
130
|
+
|
131
|
+
<BlockLabel {show_label} Icon={Image} label={label || i18n("image.image")} />
|
132
|
+
{#if (value === null || value[0] === null || value[1] === null) && !show_single}
|
133
|
+
<Empty unpadded_box={true} size="large"><Image /></Empty>
|
134
|
+
{:else}
|
135
|
+
<div class="image-container" bind:this={image_container}>
|
136
|
+
<IconButtonWrapper>
|
137
|
+
<IconButton
|
138
|
+
Icon={Undo}
|
139
|
+
label={i18n("common.undo")}
|
140
|
+
disabled={$transform.z === 1}
|
141
|
+
on:click={() => zoomable_image?.reset_zoom()}
|
142
|
+
/>
|
143
|
+
{#if show_fullscreen_button}
|
144
|
+
<FullscreenButton container={image_container} bind:is_full_screen />
|
145
|
+
{/if}
|
146
|
+
|
147
|
+
{#if show_download_button}
|
148
|
+
<DownloadLink
|
149
|
+
href={value[1]?.url}
|
150
|
+
download={value[1]?.orig_name || "image"}
|
151
|
+
>
|
152
|
+
<IconButton Icon={Download} label={i18n("common.download")} />
|
153
|
+
</DownloadLink>
|
154
|
+
{/if}
|
155
|
+
{#if interactive}
|
156
|
+
<IconButton
|
157
|
+
Icon={Clear}
|
158
|
+
label="Remove Image"
|
159
|
+
on:click={(event) => {
|
160
|
+
value = [null, null];
|
161
|
+
dispatch("clear");
|
162
|
+
event.stopPropagation();
|
163
|
+
}}
|
164
|
+
/>
|
165
|
+
{/if}
|
166
|
+
</IconButtonWrapper>
|
167
|
+
<div
|
168
|
+
class="slider-wrap"
|
169
|
+
bind:this={slider_wrap_parent}
|
170
|
+
bind:clientWidth={el_width}
|
171
|
+
class:limit_height={!is_full_screen}
|
172
|
+
>
|
173
|
+
<Slider
|
174
|
+
bind:position
|
175
|
+
{slider_color}
|
176
|
+
bind:el={slider_wrap}
|
177
|
+
bind:parent_el
|
178
|
+
{image_size}
|
179
|
+
>
|
180
|
+
<ImageEl
|
181
|
+
src={value?.[0]?.url}
|
182
|
+
alt=""
|
183
|
+
loading="lazy"
|
184
|
+
bind:img_el={img}
|
185
|
+
variant="preview"
|
186
|
+
transform="translate({$transform.x}px, {$transform.y}px) scale({$transform.z})"
|
187
|
+
fullscreen={is_full_screen}
|
188
|
+
{max_height}
|
189
|
+
on:load={handle_image_load}
|
190
|
+
/>
|
191
|
+
<ImageEl
|
192
|
+
variant="preview"
|
193
|
+
fixed={layer_images}
|
194
|
+
hidden={!value?.[1]?.url}
|
195
|
+
src={value?.[1]?.url}
|
196
|
+
alt=""
|
197
|
+
loading="lazy"
|
198
|
+
{style}
|
199
|
+
transform="translate({$transform.x}px, {$transform.y}px) scale({$transform.z})"
|
200
|
+
fullscreen={is_full_screen}
|
201
|
+
{max_height}
|
202
|
+
on:load={handle_image_load}
|
203
|
+
/>
|
204
|
+
</Slider>
|
205
|
+
</div>
|
206
|
+
</div>
|
207
|
+
{/if}
|
208
|
+
|
209
|
+
<style>
|
210
|
+
.slider-wrap {
|
211
|
+
user-select: none;
|
212
|
+
height: 100%;
|
213
|
+
width: 100%;
|
214
|
+
position: relative;
|
215
|
+
display: flex;
|
216
|
+
align-items: center;
|
217
|
+
justify-content: center;
|
218
|
+
}
|
219
|
+
|
220
|
+
.limit_height :global(img) {
|
221
|
+
max-height: 500px;
|
222
|
+
}
|
223
|
+
|
224
|
+
.image-container {
|
225
|
+
height: 100%;
|
226
|
+
position: relative;
|
227
|
+
min-width: var(--size-20);
|
228
|
+
}
|
229
|
+
</style>
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<svelte:options accessors={true} />
|
2
|
+
|
3
|
+
<script lang="ts">
|
4
|
+
import type { I18nFormatter } from "@gradio/utils";
|
5
|
+
import Image from "./Image.svelte";
|
6
|
+
import { type Client } from "@gradio/client";
|
7
|
+
|
8
|
+
import type { FileData } from "@gradio/client";
|
9
|
+
|
10
|
+
export let value: [FileData | null, FileData | null] = [null, null];
|
11
|
+
export let upload: Client["upload"];
|
12
|
+
export let stream_handler: Client["stream"];
|
13
|
+
export let label: string;
|
14
|
+
export let show_label: boolean;
|
15
|
+
export let i18n: I18nFormatter;
|
16
|
+
export let root: string;
|
17
|
+
export let upload_count = 1;
|
18
|
+
export let dragging: boolean;
|
19
|
+
export let max_height: number;
|
20
|
+
export let max_file_size: number | null = null;
|
21
|
+
</script>
|
22
|
+
|
23
|
+
<Image
|
24
|
+
slider_color="var(--border-color-primary)"
|
25
|
+
position={0.5}
|
26
|
+
bind:value
|
27
|
+
bind:dragging
|
28
|
+
{root}
|
29
|
+
on:edit
|
30
|
+
on:clear
|
31
|
+
on:stream
|
32
|
+
on:drag={({ detail }) => (dragging = detail)}
|
33
|
+
on:upload
|
34
|
+
on:select
|
35
|
+
on:share
|
36
|
+
{label}
|
37
|
+
{show_label}
|
38
|
+
{upload_count}
|
39
|
+
{stream_handler}
|
40
|
+
{upload}
|
41
|
+
{max_file_size}
|
42
|
+
{max_height}
|
43
|
+
{i18n}
|
44
|
+
>
|
45
|
+
<slot />
|
46
|
+
</Image>
|