slidev-theme-watabegg 1.1.2 → 1.1.3
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/README.ja.md +8 -12
- package/README.md +8 -12
- package/example.md +16 -6
- package/layouts/image-scroll.vue +86 -273
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -52,7 +52,7 @@ transition: fade
|
|
|
52
52
|
| cover | 表紙 | グラデーション波 + title/subtitle/author |
|
|
53
53
|
| two-cols | 2カラム | `::left::` / `::right::` スロット |
|
|
54
54
|
| image | 背景画像 | `image:` 指定 + `TextBox` で自由配置 |
|
|
55
|
-
| image-scroll |
|
|
55
|
+
| image-scroll | 背景画像(縦スクロール) | `image:` 指定 + 縦長背景 + `TextBox` |
|
|
56
56
|
| end | 終了スライド | シンプルな終了画面 |
|
|
57
57
|
|
|
58
58
|
### two-cols 例
|
|
@@ -82,31 +82,27 @@ image: /path/to/bg.jpg
|
|
|
82
82
|
---
|
|
83
83
|
layout: image-scroll
|
|
84
84
|
image: /path/to/long-bg.jpg
|
|
85
|
+
imageScroll:
|
|
86
|
+
offsetY: -120
|
|
85
87
|
---
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|

|
|
89
91
|
|
|
90
92
|
image-scroll オプション(frontmatter `imageScroll`):
|
|
91
|
-
- `
|
|
92
|
-
- `position`: `center | top | bottom | left | right` もしくは組み合わせ(例: `top left`)
|
|
93
|
-
- `offset`: `[x, y]` または `{ x, y }`(px)
|
|
93
|
+
- `offsetY`: 画像の中心からの初期スクロール量(px)
|
|
94
94
|
|
|
95
95
|
操作:
|
|
96
|
-
- Wheel
|
|
97
|
-
-
|
|
98
|
-
- Alt + Wheel: 10% ずつ拡大縮小
|
|
99
|
-
- タッチ: ドラッグでパン、ピンチでズーム
|
|
96
|
+
- Wheel / トラックパッド: 縦スクロール
|
|
97
|
+
- タッチ: 縦スクロール
|
|
100
98
|
|
|
101
|
-
|
|
99
|
+
初期位置の例:
|
|
102
100
|
```markdown
|
|
103
101
|
---
|
|
104
102
|
layout: image-scroll
|
|
105
103
|
image: /path/to/long-bg.jpg
|
|
106
104
|
imageScroll:
|
|
107
|
-
|
|
108
|
-
position: center
|
|
109
|
-
offset: [0, -120]
|
|
105
|
+
offsetY: 180
|
|
110
106
|
---
|
|
111
107
|
```
|
|
112
108
|
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Below: `color: blue` example.
|
|
|
56
56
|
| cover | Title slide | Gradient wave + title/subtitle/author |
|
|
57
57
|
| two-cols | Two columns | Slots: `::left::` / `::right::` |
|
|
58
58
|
| image | Background image | Use `image:` + absolutely placed `TextBox` |
|
|
59
|
-
| image-scroll | Background image (
|
|
59
|
+
| image-scroll | Background image (vertical scroll) | Use `image:` with tall background + `TextBox` |
|
|
60
60
|
| end | Ending slide | Simple finish screen |
|
|
61
61
|
|
|
62
62
|
### two-cols Example
|
|
@@ -86,31 +86,27 @@ image: /path/to/bg.jpg
|
|
|
86
86
|
---
|
|
87
87
|
layout: image-scroll
|
|
88
88
|
image: /path/to/long-bg.jpg
|
|
89
|
+
imageScroll:
|
|
90
|
+
offsetY: -120
|
|
89
91
|
---
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|

|
|
93
95
|
|
|
94
96
|
image-scroll options (frontmatter `imageScroll`):
|
|
95
|
-
- `
|
|
96
|
-
- `position`: `center | top | bottom | left | right` or combined (e.g. `top left`)
|
|
97
|
-
- `offset`: `[x, y]` or `{ x, y }` in px
|
|
97
|
+
- `offsetY`: initial scroll offset from image center in px
|
|
98
98
|
|
|
99
99
|
Controls:
|
|
100
|
-
- Wheel:
|
|
101
|
-
-
|
|
102
|
-
- Alt + Wheel: zoom in/out by 10%
|
|
103
|
-
- Touch: drag to pan, pinch to zoom
|
|
100
|
+
- Wheel / trackpad: vertical scroll
|
|
101
|
+
- Touch: vertical scroll
|
|
104
102
|
|
|
105
|
-
Example with initial
|
|
103
|
+
Example with initial position:
|
|
106
104
|
```markdown
|
|
107
105
|
---
|
|
108
106
|
layout: image-scroll
|
|
109
107
|
image: /path/to/long-bg.jpg
|
|
110
108
|
imageScroll:
|
|
111
|
-
|
|
112
|
-
position: center
|
|
113
|
-
offset: [0, -120]
|
|
109
|
+
offsetY: 180
|
|
114
110
|
---
|
|
115
111
|
```
|
|
116
112
|
|
package/example.md
CHANGED
|
@@ -140,15 +140,25 @@ image: 'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?auto=format
|
|
|
140
140
|
layout: image-scroll
|
|
141
141
|
image: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=2400&q=80'
|
|
142
142
|
imageScroll:
|
|
143
|
-
|
|
144
|
-
position: center
|
|
143
|
+
offsetY: -120
|
|
145
144
|
---
|
|
146
145
|
|
|
147
|
-
-
|
|
148
|
-
- Shift + ホイール: 縮小
|
|
149
|
-
- 左クリックホールド + ドラッグ: 移動
|
|
146
|
+
## image-scroll: 縦長画像を「横幅フィット+縦スクロール」で表示
|
|
150
147
|
|
|
151
|
-
|
|
148
|
+
- 画像はスライド幅に合わせて表示
|
|
149
|
+
- 縦方向はホイール / タッチでスクロール
|
|
150
|
+
- `offsetY` で初期位置を調整(中心からのpx)
|
|
151
|
+
|
|
152
|
+
ここでは `offsetY: -120` で少し上寄せしています。
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
layout: image-scroll
|
|
156
|
+
image: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=2400&q=80'
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## offsetY の例(中心から上へ 300px)
|
|
160
|
+
|
|
161
|
+
背景の見せたい位置が決まっているときに便利です。
|
|
152
162
|
|
|
153
163
|
---
|
|
154
164
|
|
package/layouts/image-scroll.vue
CHANGED
|
@@ -2,24 +2,23 @@
|
|
|
2
2
|
<div
|
|
3
3
|
class="slidev-layout image-scroll"
|
|
4
4
|
ref="wrapperRef"
|
|
5
|
-
@wheel="onWheel"
|
|
5
|
+
@wheel.prevent="onWheel"
|
|
6
6
|
@pointerdown="onPointerDown"
|
|
7
7
|
@pointermove="onPointerMove"
|
|
8
8
|
@pointerup="onPointerUp"
|
|
9
9
|
@pointercancel="onPointerUp"
|
|
10
10
|
>
|
|
11
|
-
<div class="image-
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
</div>
|
|
11
|
+
<div class="image-scroll__scroller" ref="scrollerRef">
|
|
12
|
+
<img
|
|
13
|
+
v-if="imageSrc"
|
|
14
|
+
ref="imageRef"
|
|
15
|
+
:src="imageSrc"
|
|
16
|
+
alt="Scrollable background"
|
|
17
|
+
class="image-scroll__image"
|
|
18
|
+
:style="imageStyle"
|
|
19
|
+
draggable="false"
|
|
20
|
+
@load="onImageLoad"
|
|
21
|
+
/>
|
|
23
22
|
</div>
|
|
24
23
|
<div class="content">
|
|
25
24
|
<slot />
|
|
@@ -28,7 +27,7 @@
|
|
|
28
27
|
</template>
|
|
29
28
|
|
|
30
29
|
<script setup lang="ts">
|
|
31
|
-
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
30
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
32
31
|
import { useSlideContext } from '@slidev/client'
|
|
33
32
|
|
|
34
33
|
const { $frontmatter } = useSlideContext()
|
|
@@ -36,15 +35,10 @@ const { $frontmatter } = useSlideContext()
|
|
|
36
35
|
const imageSrc = computed(() => $frontmatter.image)
|
|
37
36
|
|
|
38
37
|
const wrapperRef = ref<HTMLElement | null>(null)
|
|
38
|
+
const scrollerRef = ref<HTMLElement | null>(null)
|
|
39
39
|
const imageRef = ref<HTMLImageElement | null>(null)
|
|
40
40
|
|
|
41
|
-
type FitMode = 'contain' | 'cover' | 'width' | 'height'
|
|
42
|
-
|
|
43
41
|
type ImageScrollFrontmatter = {
|
|
44
|
-
fit?: FitMode | string
|
|
45
|
-
position?: string
|
|
46
|
-
offset?: [number, number] | { x?: number; y?: number }
|
|
47
|
-
offsetX?: number
|
|
48
42
|
offsetY?: number
|
|
49
43
|
}
|
|
50
44
|
|
|
@@ -54,266 +48,100 @@ const imageScrollOptions = computed<ImageScrollFrontmatter>(() => {
|
|
|
54
48
|
return {}
|
|
55
49
|
})
|
|
56
50
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (normalized === 'cover') return 'cover'
|
|
62
|
-
if (normalized === 'width' || normalized === 'fit-width') return 'width'
|
|
63
|
-
if (normalized === 'height' || normalized === 'fit-height') return 'height'
|
|
64
|
-
return 'contain'
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
const positionMode = computed(() => imageScrollOptions.value.position ?? 'center')
|
|
68
|
-
|
|
69
|
-
const offsetMode = computed(() => {
|
|
70
|
-
const raw = imageScrollOptions.value
|
|
71
|
-
const offset = raw.offset
|
|
72
|
-
let x = Number(raw.offsetX ?? 0)
|
|
73
|
-
let y = Number(raw.offsetY ?? 0)
|
|
74
|
-
if (Array.isArray(offset)) {
|
|
75
|
-
x = Number(offset[0] ?? x)
|
|
76
|
-
y = Number(offset[1] ?? y)
|
|
77
|
-
} else if (offset && typeof offset === 'object') {
|
|
78
|
-
x = Number((offset as { x?: number }).x ?? x)
|
|
79
|
-
y = Number((offset as { y?: number }).y ?? y)
|
|
80
|
-
}
|
|
81
|
-
return { x, y }
|
|
51
|
+
const offsetY = computed(() => {
|
|
52
|
+
const raw = imageScrollOptions.value.offsetY ?? 0
|
|
53
|
+
const value = Number(raw)
|
|
54
|
+
return Number.isFinite(value) ? value : 0
|
|
82
55
|
})
|
|
83
56
|
|
|
84
|
-
const baseTransform = reactive({ x: 0, y: 0, scale: 1 })
|
|
85
|
-
const userTransform = reactive({ x: 0, y: 0, scale: 1 })
|
|
86
|
-
|
|
87
57
|
const imageSize = reactive({ width: 0, height: 0 })
|
|
88
58
|
const wrapperSize = reactive({ width: 0, height: 0 })
|
|
89
59
|
|
|
90
|
-
const isPanning = ref(false)
|
|
91
|
-
const lastPointer = reactive({ x: 0, y: 0 })
|
|
92
|
-
const activePointers = new Map<number, { x: number; y: number }>()
|
|
93
|
-
const pinchState = reactive({
|
|
94
|
-
active: false,
|
|
95
|
-
startDistance: 0,
|
|
96
|
-
startUserScale: 1,
|
|
97
|
-
anchorImagePoint: { x: 0, y: 0 },
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
const MIN_SCALE = 0.4
|
|
101
|
-
const MAX_SCALE = 4
|
|
102
|
-
const ZOOM_STEP = 0.1
|
|
103
|
-
|
|
104
60
|
const clamp = (val: number, min: number, max: number) => Math.min(max, Math.max(min, val))
|
|
105
61
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
baseTransform.x = 0
|
|
109
|
-
baseTransform.y = 0
|
|
110
|
-
userTransform.scale = 1
|
|
111
|
-
userTransform.x = 0
|
|
112
|
-
userTransform.y = 0
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const panBy = (dx: number, dy: number) => {
|
|
116
|
-
userTransform.x += dx
|
|
117
|
-
userTransform.y += dy
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const getWrapperPoint = (event: { clientX: number; clientY: number }) => {
|
|
121
|
-
const rect = wrapperRef.value?.getBoundingClientRect()
|
|
122
|
-
if (!rect) return { x: 0, y: 0 }
|
|
123
|
-
return { x: event.clientX - rect.left, y: event.clientY - rect.top }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const getTotalTransform = () => ({
|
|
127
|
-
x: baseTransform.x + userTransform.x,
|
|
128
|
-
y: baseTransform.y + userTransform.y,
|
|
129
|
-
scale: baseTransform.scale * userTransform.scale,
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
const getImagePoint = (anchor: { x: number; y: number }, total: { x: number; y: number; scale: number }) => ({
|
|
133
|
-
x: (anchor.x - total.x) / total.scale,
|
|
134
|
-
y: (anchor.y - total.y) / total.scale,
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
const applyScaleAtPoint = (nextUserScale: number, anchor: { x: number; y: number }, anchorImagePoint?: { x: number; y: number }) => {
|
|
138
|
-
const currentTotal = getTotalTransform()
|
|
139
|
-
const imagePoint = anchorImagePoint ?? getImagePoint(anchor, currentTotal)
|
|
140
|
-
const nextTotalScale = baseTransform.scale * nextUserScale
|
|
141
|
-
const nextTotalX = anchor.x - imagePoint.x * nextTotalScale
|
|
142
|
-
const nextTotalY = anchor.y - imagePoint.y * nextTotalScale
|
|
143
|
-
userTransform.scale = nextUserScale
|
|
144
|
-
userTransform.x = nextTotalX - baseTransform.x
|
|
145
|
-
userTransform.y = nextTotalY - baseTransform.y
|
|
146
|
-
}
|
|
62
|
+
const isTouching = ref(false)
|
|
63
|
+
const lastTouchY = ref(0)
|
|
147
64
|
|
|
148
65
|
const normalizeWheel = (event: WheelEvent) => {
|
|
149
|
-
let {
|
|
66
|
+
let { deltaY } = event
|
|
150
67
|
if (event.deltaMode === 1) {
|
|
151
|
-
deltaX *= 16
|
|
152
68
|
deltaY *= 16
|
|
153
69
|
} else if (event.deltaMode === 2) {
|
|
154
|
-
deltaX *= wrapperSize.height || 1
|
|
155
70
|
deltaY *= wrapperSize.height || 1
|
|
156
71
|
}
|
|
157
|
-
return
|
|
72
|
+
return deltaY
|
|
158
73
|
}
|
|
159
74
|
|
|
160
75
|
const onWheel = (event: WheelEvent) => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (event.altKey) {
|
|
166
|
-
const anchor = getWrapperPoint(event)
|
|
167
|
-
const zoomFactor = event.deltaY < 0 ? 1 + ZOOM_STEP : 1 - ZOOM_STEP
|
|
168
|
-
const nextScale = clamp(userTransform.scale * zoomFactor, MIN_SCALE, MAX_SCALE)
|
|
169
|
-
if (nextScale === userTransform.scale) return
|
|
170
|
-
applyScaleAtPoint(nextScale, anchor)
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const { deltaX, deltaY } = normalizeWheel(event)
|
|
175
|
-
if (event.shiftKey) {
|
|
176
|
-
panBy(-deltaY, 0)
|
|
177
|
-
} else {
|
|
178
|
-
panBy(0, -deltaY)
|
|
179
|
-
}
|
|
76
|
+
const scroller = scrollerRef.value
|
|
77
|
+
if (!scroller || !imageSrc.value) return
|
|
78
|
+
scroller.scrollTop += normalizeWheel(event)
|
|
180
79
|
}
|
|
181
80
|
|
|
182
81
|
let resizeObserver: ResizeObserver | null = null
|
|
183
82
|
|
|
184
83
|
const onPointerDown = (event: PointerEvent) => {
|
|
185
84
|
if (!imageSrc.value) return
|
|
186
|
-
if (event.pointerType
|
|
85
|
+
if (event.pointerType !== 'touch') return
|
|
187
86
|
wrapperRef.value?.setPointerCapture(event.pointerId)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (activePointers.size === 1) {
|
|
191
|
-
isPanning.value = true
|
|
192
|
-
lastPointer.x = point.x
|
|
193
|
-
lastPointer.y = point.y
|
|
194
|
-
}
|
|
195
|
-
if (activePointers.size === 2) {
|
|
196
|
-
isPanning.value = false
|
|
197
|
-
const [first, second] = Array.from(activePointers.values())
|
|
198
|
-
const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 }
|
|
199
|
-
const distance = Math.hypot(first.x - second.x, first.y - second.y)
|
|
200
|
-
const total = getTotalTransform()
|
|
201
|
-
pinchState.active = true
|
|
202
|
-
pinchState.startDistance = distance
|
|
203
|
-
pinchState.startUserScale = userTransform.scale
|
|
204
|
-
pinchState.anchorImagePoint = getImagePoint(center, total)
|
|
205
|
-
}
|
|
87
|
+
isTouching.value = true
|
|
88
|
+
lastTouchY.value = event.clientY
|
|
206
89
|
}
|
|
207
90
|
|
|
208
91
|
const onPointerMove = (event: PointerEvent) => {
|
|
209
|
-
if (!
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
panBy(dx, dy)
|
|
217
|
-
lastPointer.x = point.x
|
|
218
|
-
lastPointer.y = point.y
|
|
219
|
-
event.preventDefault()
|
|
220
|
-
return
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (activePointers.size >= 2 && pinchState.active) {
|
|
224
|
-
const [first, second] = Array.from(activePointers.values())
|
|
225
|
-
const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 }
|
|
226
|
-
const distance = Math.hypot(first.x - second.x, first.y - second.y)
|
|
227
|
-
if (pinchState.startDistance > 0) {
|
|
228
|
-
const factor = distance / pinchState.startDistance
|
|
229
|
-
const nextScale = clamp(pinchState.startUserScale * factor, MIN_SCALE, MAX_SCALE)
|
|
230
|
-
applyScaleAtPoint(nextScale, center, pinchState.anchorImagePoint)
|
|
231
|
-
event.preventDefault()
|
|
232
|
-
}
|
|
233
|
-
}
|
|
92
|
+
if (!isTouching.value || event.pointerType !== 'touch') return
|
|
93
|
+
const scroller = scrollerRef.value
|
|
94
|
+
if (!scroller) return
|
|
95
|
+
const deltaY = event.clientY - lastTouchY.value
|
|
96
|
+
scroller.scrollTop -= deltaY
|
|
97
|
+
lastTouchY.value = event.clientY
|
|
98
|
+
event.preventDefault()
|
|
234
99
|
}
|
|
235
100
|
|
|
236
101
|
const onPointerUp = (event: PointerEvent) => {
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (activePointers.size < 2) {
|
|
241
|
-
pinchState.active = false
|
|
242
|
-
}
|
|
243
|
-
if (activePointers.size === 1) {
|
|
244
|
-
const [remaining] = Array.from(activePointers.values())
|
|
245
|
-
lastPointer.x = remaining.x
|
|
246
|
-
lastPointer.y = remaining.y
|
|
247
|
-
isPanning.value = true
|
|
248
|
-
} else {
|
|
249
|
-
isPanning.value = false
|
|
250
|
-
}
|
|
102
|
+
if (event.pointerType !== 'touch') return
|
|
103
|
+
isTouching.value = false
|
|
104
|
+
wrapperRef.value?.releasePointerCapture(event.pointerId)
|
|
251
105
|
}
|
|
252
106
|
|
|
253
|
-
const
|
|
254
|
-
if (!
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const scaleX = wrapperSize.width / imageSize.width
|
|
259
|
-
const scaleY = wrapperSize.height / imageSize.height
|
|
260
|
-
let scale = scaleX
|
|
261
|
-
switch (fitMode.value) {
|
|
262
|
-
case 'cover':
|
|
263
|
-
scale = Math.max(scaleX, scaleY)
|
|
264
|
-
break
|
|
265
|
-
case 'height':
|
|
266
|
-
scale = scaleY
|
|
267
|
-
break
|
|
268
|
-
case 'width':
|
|
269
|
-
scale = scaleX
|
|
270
|
-
break
|
|
271
|
-
default:
|
|
272
|
-
scale = Math.min(scaleX, scaleY)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const scaledWidth = imageSize.width * scale
|
|
276
|
-
const scaledHeight = imageSize.height * scale
|
|
277
|
-
const position = String(positionMode.value).toLowerCase()
|
|
278
|
-
const parts = position.split(/\s+/).filter(Boolean)
|
|
279
|
-
let xAlign: 'left' | 'center' | 'right' = 'center'
|
|
280
|
-
let yAlign: 'top' | 'center' | 'bottom' = 'center'
|
|
107
|
+
const scaledHeight = computed(() => {
|
|
108
|
+
if (!imageSize.width || !imageSize.height || !wrapperSize.width) return 0
|
|
109
|
+
return (imageSize.height * wrapperSize.width) / imageSize.width
|
|
110
|
+
})
|
|
281
111
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
yAlign = parts[0] as typeof yAlign
|
|
289
|
-
xAlign = 'center'
|
|
290
|
-
}
|
|
291
|
-
if (parts[0] === 'left' || parts[0] === 'right') {
|
|
292
|
-
xAlign = parts[0] as typeof xAlign
|
|
293
|
-
yAlign = 'center'
|
|
294
|
-
}
|
|
295
|
-
}
|
|
112
|
+
const getRenderedImageHeight = () => {
|
|
113
|
+
const image = imageRef.value
|
|
114
|
+
if (!image) return 0
|
|
115
|
+
const rect = image.getBoundingClientRect()
|
|
116
|
+
return rect.height
|
|
117
|
+
}
|
|
296
118
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
119
|
+
const applyDefaultScroll = () => {
|
|
120
|
+
const scroller = scrollerRef.value
|
|
121
|
+
if (!scroller) return
|
|
122
|
+
const renderedHeight = getRenderedImageHeight()
|
|
123
|
+
const totalHeight = renderedHeight || scaledHeight.value
|
|
124
|
+
if (!totalHeight) return
|
|
125
|
+
const viewportHeight = scroller.clientHeight || wrapperSize.height
|
|
126
|
+
const maxScroll = Math.max(0, totalHeight - viewportHeight)
|
|
127
|
+
const desired = maxScroll / 2 + offsetY.value
|
|
128
|
+
scroller.scrollTop = clamp(desired, 0, maxScroll)
|
|
129
|
+
}
|
|
302
130
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
131
|
+
const scheduleApplyDefaultScroll = () => {
|
|
132
|
+
if (!imageSrc.value) return
|
|
133
|
+
nextTick(() => {
|
|
134
|
+
requestAnimationFrame(() => {
|
|
135
|
+
applyDefaultScroll()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
310
138
|
}
|
|
311
139
|
|
|
312
140
|
const onImageLoad = (event: Event) => {
|
|
313
141
|
const target = event.target as HTMLImageElement
|
|
314
142
|
imageSize.width = target.naturalWidth
|
|
315
143
|
imageSize.height = target.naturalHeight
|
|
316
|
-
|
|
144
|
+
scheduleApplyDefaultScroll()
|
|
317
145
|
}
|
|
318
146
|
|
|
319
147
|
const updateWrapperSize = () => {
|
|
@@ -323,28 +151,24 @@ const updateWrapperSize = () => {
|
|
|
323
151
|
wrapperSize.height = rect.height
|
|
324
152
|
}
|
|
325
153
|
|
|
326
|
-
const imageStyle = computed(() =>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
const translateStyle = computed(() => ({
|
|
334
|
-
transform: `translate3d(${baseTransform.x + userTransform.x}px, ${baseTransform.y + userTransform.y}px, 0)`,
|
|
335
|
-
}))
|
|
154
|
+
const imageStyle = computed(() => {
|
|
155
|
+
if (!imageSize.width || !imageSize.height) return {}
|
|
156
|
+
return {
|
|
157
|
+
aspectRatio: `${imageSize.width} / ${imageSize.height}`,
|
|
158
|
+
}
|
|
159
|
+
})
|
|
336
160
|
|
|
337
161
|
onMounted(() => {
|
|
338
162
|
updateWrapperSize()
|
|
339
163
|
if (imageRef.value?.complete && imageRef.value.naturalWidth) {
|
|
340
164
|
imageSize.width = imageRef.value.naturalWidth
|
|
341
165
|
imageSize.height = imageRef.value.naturalHeight
|
|
342
|
-
computeBaseTransform()
|
|
343
166
|
}
|
|
167
|
+
scheduleApplyDefaultScroll()
|
|
344
168
|
if (wrapperRef.value) {
|
|
345
169
|
resizeObserver = new ResizeObserver(() => {
|
|
346
170
|
updateWrapperSize()
|
|
347
|
-
|
|
171
|
+
scheduleApplyDefaultScroll()
|
|
348
172
|
})
|
|
349
173
|
resizeObserver.observe(wrapperRef.value)
|
|
350
174
|
}
|
|
@@ -353,17 +177,16 @@ onMounted(() => {
|
|
|
353
177
|
onBeforeUnmount(() => {
|
|
354
178
|
resizeObserver?.disconnect()
|
|
355
179
|
resizeObserver = null
|
|
356
|
-
activePointers.clear()
|
|
357
180
|
})
|
|
358
181
|
|
|
359
182
|
watch(imageSrc, () => {
|
|
360
183
|
imageSize.width = 0
|
|
361
184
|
imageSize.height = 0
|
|
362
|
-
|
|
185
|
+
if (scrollerRef.value) scrollerRef.value.scrollTop = 0
|
|
363
186
|
})
|
|
364
187
|
|
|
365
|
-
watch([
|
|
366
|
-
|
|
188
|
+
watch([scaledHeight, offsetY], () => {
|
|
189
|
+
scheduleApplyDefaultScroll()
|
|
367
190
|
})
|
|
368
191
|
</script>
|
|
369
192
|
|
|
@@ -376,32 +199,21 @@ watch([fitMode, positionMode, offsetMode], () => {
|
|
|
376
199
|
touch-action: none;
|
|
377
200
|
}
|
|
378
201
|
|
|
379
|
-
.image-
|
|
202
|
+
.image-scroll__scroller {
|
|
380
203
|
position: absolute;
|
|
381
204
|
inset: 0;
|
|
382
|
-
overflow:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
.image-layer.is-panning {
|
|
388
|
-
cursor: grabbing;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
.image-translate {
|
|
392
|
-
position: absolute;
|
|
393
|
-
top: 0;
|
|
394
|
-
left: 0;
|
|
395
|
-
will-change: transform;
|
|
205
|
+
overflow-y: auto;
|
|
206
|
+
overflow-x: hidden;
|
|
207
|
+
scrollbar-gutter: stable;
|
|
208
|
+
overscroll-behavior: contain;
|
|
396
209
|
}
|
|
397
210
|
|
|
398
|
-
.image-
|
|
211
|
+
.image-scroll__image {
|
|
399
212
|
display: block;
|
|
213
|
+
width: 100%;
|
|
400
214
|
max-width: none;
|
|
401
|
-
max-height: none;
|
|
402
215
|
pointer-events: none;
|
|
403
216
|
user-select: none;
|
|
404
|
-
will-change: transform;
|
|
405
217
|
}
|
|
406
218
|
|
|
407
219
|
.content {
|
|
@@ -409,5 +221,6 @@ watch([fitMode, positionMode, offsetMode], () => {
|
|
|
409
221
|
z-index: 1;
|
|
410
222
|
height: 100%;
|
|
411
223
|
padding: 2rem;
|
|
224
|
+
box-sizing: border-box;
|
|
412
225
|
}
|
|
413
226
|
</style>
|