slidev-theme-watabegg 1.1.1 → 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 +20 -1
- package/README.md +20 -1
- package/example.md +17 -4
- package/layouts/image-scroll.vue +148 -60
- package/package.json +1 -1
- package/styles/layout.css +14 -4
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,11 +82,30 @@ 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
|
|
|
92
|
+
image-scroll オプション(frontmatter `imageScroll`):
|
|
93
|
+
- `offsetY`: 画像の中心からの初期スクロール量(px)
|
|
94
|
+
|
|
95
|
+
操作:
|
|
96
|
+
- Wheel / トラックパッド: 縦スクロール
|
|
97
|
+
- タッチ: 縦スクロール
|
|
98
|
+
|
|
99
|
+
初期位置の例:
|
|
100
|
+
```markdown
|
|
101
|
+
---
|
|
102
|
+
layout: image-scroll
|
|
103
|
+
image: /path/to/long-bg.jpg
|
|
104
|
+
imageScroll:
|
|
105
|
+
offsetY: 180
|
|
106
|
+
---
|
|
107
|
+
```
|
|
108
|
+
|
|
90
109
|
## コンポーネント
|
|
91
110
|
### QuestionList
|
|
92
111
|
入れ子質問/解答・Markdown 埋め込み・階層別スタイル。
|
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,11 +86,30 @@ 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
|
|
|
96
|
+
image-scroll options (frontmatter `imageScroll`):
|
|
97
|
+
- `offsetY`: initial scroll offset from image center in px
|
|
98
|
+
|
|
99
|
+
Controls:
|
|
100
|
+
- Wheel / trackpad: vertical scroll
|
|
101
|
+
- Touch: vertical scroll
|
|
102
|
+
|
|
103
|
+
Example with initial position:
|
|
104
|
+
```markdown
|
|
105
|
+
---
|
|
106
|
+
layout: image-scroll
|
|
107
|
+
image: /path/to/long-bg.jpg
|
|
108
|
+
imageScroll:
|
|
109
|
+
offsetY: 180
|
|
110
|
+
---
|
|
111
|
+
```
|
|
112
|
+
|
|
94
113
|
## Components
|
|
95
114
|
### QuestionList
|
|
96
115
|
Nested questions/answers with per-level styles and Markdown rendering.
|
package/example.md
CHANGED
|
@@ -139,13 +139,26 @@ image: 'https://images.unsplash.com/photo-1501594907352-04cda38ebc29?auto=format
|
|
|
139
139
|
---
|
|
140
140
|
layout: image-scroll
|
|
141
141
|
image: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=2400&q=80'
|
|
142
|
+
imageScroll:
|
|
143
|
+
offsetY: -120
|
|
142
144
|
---
|
|
143
145
|
|
|
144
|
-
-
|
|
145
|
-
- Shift + ホイール: 縮小
|
|
146
|
-
- 左クリックホールド + ドラッグ: 移動
|
|
146
|
+
## image-scroll: 縦長画像を「横幅フィット+縦スクロール」で表示
|
|
147
147
|
|
|
148
|
-
|
|
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
|
+
背景の見せたい位置が決まっているときに便利です。
|
|
149
162
|
|
|
150
163
|
---
|
|
151
164
|
|
package/layouts/image-scroll.vue
CHANGED
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
<div
|
|
3
3
|
class="slidev-layout image-scroll"
|
|
4
4
|
ref="wrapperRef"
|
|
5
|
-
@wheel="onWheel"
|
|
6
|
-
@
|
|
5
|
+
@wheel.prevent="onWheel"
|
|
6
|
+
@pointerdown="onPointerDown"
|
|
7
|
+
@pointermove="onPointerMove"
|
|
8
|
+
@pointerup="onPointerUp"
|
|
9
|
+
@pointercancel="onPointerUp"
|
|
7
10
|
>
|
|
8
|
-
<div class="image-
|
|
11
|
+
<div class="image-scroll__scroller" ref="scrollerRef">
|
|
9
12
|
<img
|
|
10
13
|
v-if="imageSrc"
|
|
14
|
+
ref="imageRef"
|
|
11
15
|
:src="imageSrc"
|
|
12
16
|
alt="Scrollable background"
|
|
17
|
+
class="image-scroll__image"
|
|
13
18
|
:style="imageStyle"
|
|
14
19
|
draggable="false"
|
|
20
|
+
@load="onImageLoad"
|
|
15
21
|
/>
|
|
16
22
|
</div>
|
|
17
23
|
<div class="content">
|
|
@@ -21,7 +27,7 @@
|
|
|
21
27
|
</template>
|
|
22
28
|
|
|
23
29
|
<script setup lang="ts">
|
|
24
|
-
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
30
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
25
31
|
import { useSlideContext } from '@slidev/client'
|
|
26
32
|
|
|
27
33
|
const { $frontmatter } = useSlideContext()
|
|
@@ -29,77 +35,158 @@ const { $frontmatter } = useSlideContext()
|
|
|
29
35
|
const imageSrc = computed(() => $frontmatter.image)
|
|
30
36
|
|
|
31
37
|
const wrapperRef = ref<HTMLElement | null>(null)
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const isPanning = ref(false)
|
|
35
|
-
const lastPointer = reactive({ x: 0, y: 0 })
|
|
38
|
+
const scrollerRef = ref<HTMLElement | null>(null)
|
|
39
|
+
const imageRef = ref<HTMLImageElement | null>(null)
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
type ImageScrollFrontmatter = {
|
|
42
|
+
offsetY?: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const imageScrollOptions = computed<ImageScrollFrontmatter>(() => {
|
|
46
|
+
const raw = $frontmatter.imageScroll
|
|
47
|
+
if (raw && typeof raw === 'object') return raw as ImageScrollFrontmatter
|
|
48
|
+
return {}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const offsetY = computed(() => {
|
|
52
|
+
const raw = imageScrollOptions.value.offsetY ?? 0
|
|
53
|
+
const value = Number(raw)
|
|
54
|
+
return Number.isFinite(value) ? value : 0
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const imageSize = reactive({ width: 0, height: 0 })
|
|
58
|
+
const wrapperSize = reactive({ width: 0, height: 0 })
|
|
40
59
|
|
|
41
60
|
const clamp = (val: number, min: number, max: number) => Math.min(max, Math.max(min, val))
|
|
42
61
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
const isTouching = ref(false)
|
|
63
|
+
const lastTouchY = ref(0)
|
|
64
|
+
|
|
65
|
+
const normalizeWheel = (event: WheelEvent) => {
|
|
66
|
+
let { deltaY } = event
|
|
67
|
+
if (event.deltaMode === 1) {
|
|
68
|
+
deltaY *= 16
|
|
69
|
+
} else if (event.deltaMode === 2) {
|
|
70
|
+
deltaY *= wrapperSize.height || 1
|
|
71
|
+
}
|
|
72
|
+
return deltaY
|
|
47
73
|
}
|
|
48
74
|
|
|
49
75
|
const onWheel = (event: WheelEvent) => {
|
|
50
|
-
|
|
76
|
+
const scroller = scrollerRef.value
|
|
77
|
+
if (!scroller || !imageSrc.value) return
|
|
78
|
+
scroller.scrollTop += normalizeWheel(event)
|
|
79
|
+
}
|
|
51
80
|
|
|
52
|
-
|
|
53
|
-
const direction = event.shiftKey ? -1 : event.deltaY < 0 ? 1 : -1
|
|
54
|
-
const next = clamp(scale.value + direction * ZOOM_STEP, MIN_SCALE, MAX_SCALE)
|
|
81
|
+
let resizeObserver: ResizeObserver | null = null
|
|
55
82
|
|
|
56
|
-
|
|
83
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
84
|
+
if (!imageSrc.value) return
|
|
85
|
+
if (event.pointerType !== 'touch') return
|
|
86
|
+
wrapperRef.value?.setPointerCapture(event.pointerId)
|
|
87
|
+
isTouching.value = true
|
|
88
|
+
lastTouchY.value = event.clientY
|
|
89
|
+
}
|
|
57
90
|
|
|
91
|
+
const onPointerMove = (event: PointerEvent) => {
|
|
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
|
|
58
98
|
event.preventDefault()
|
|
59
|
-
scale.value = next
|
|
60
99
|
}
|
|
61
100
|
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
lastPointer.y = event.clientY
|
|
67
|
-
window.addEventListener('mousemove', onMouseMove)
|
|
68
|
-
window.addEventListener('mouseup', onMouseUp)
|
|
101
|
+
const onPointerUp = (event: PointerEvent) => {
|
|
102
|
+
if (event.pointerType !== 'touch') return
|
|
103
|
+
isTouching.value = false
|
|
104
|
+
wrapperRef.value?.releasePointerCapture(event.pointerId)
|
|
69
105
|
}
|
|
70
106
|
|
|
71
|
-
const
|
|
72
|
-
if (!
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
107
|
+
const scaledHeight = computed(() => {
|
|
108
|
+
if (!imageSize.width || !imageSize.height || !wrapperSize.width) return 0
|
|
109
|
+
return (imageSize.height * wrapperSize.width) / imageSize.width
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const getRenderedImageHeight = () => {
|
|
113
|
+
const image = imageRef.value
|
|
114
|
+
if (!image) return 0
|
|
115
|
+
const rect = image.getBoundingClientRect()
|
|
116
|
+
return rect.height
|
|
80
117
|
}
|
|
81
118
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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)
|
|
86
129
|
}
|
|
87
130
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
131
|
+
const scheduleApplyDefaultScroll = () => {
|
|
132
|
+
if (!imageSrc.value) return
|
|
133
|
+
nextTick(() => {
|
|
134
|
+
requestAnimationFrame(() => {
|
|
135
|
+
applyDefaultScroll()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const onImageLoad = (event: Event) => {
|
|
141
|
+
const target = event.target as HTMLImageElement
|
|
142
|
+
imageSize.width = target.naturalWidth
|
|
143
|
+
imageSize.height = target.naturalHeight
|
|
144
|
+
scheduleApplyDefaultScroll()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const updateWrapperSize = () => {
|
|
148
|
+
const rect = wrapperRef.value?.getBoundingClientRect()
|
|
149
|
+
if (!rect) return
|
|
150
|
+
wrapperSize.width = rect.width
|
|
151
|
+
wrapperSize.height = rect.height
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const imageStyle = computed(() => {
|
|
155
|
+
if (!imageSize.width || !imageSize.height) return {}
|
|
156
|
+
return {
|
|
157
|
+
aspectRatio: `${imageSize.width} / ${imageSize.height}`,
|
|
158
|
+
}
|
|
159
|
+
})
|
|
91
160
|
|
|
92
161
|
onMounted(() => {
|
|
93
|
-
|
|
162
|
+
updateWrapperSize()
|
|
163
|
+
if (imageRef.value?.complete && imageRef.value.naturalWidth) {
|
|
164
|
+
imageSize.width = imageRef.value.naturalWidth
|
|
165
|
+
imageSize.height = imageRef.value.naturalHeight
|
|
166
|
+
}
|
|
167
|
+
scheduleApplyDefaultScroll()
|
|
168
|
+
if (wrapperRef.value) {
|
|
169
|
+
resizeObserver = new ResizeObserver(() => {
|
|
170
|
+
updateWrapperSize()
|
|
171
|
+
scheduleApplyDefaultScroll()
|
|
172
|
+
})
|
|
173
|
+
resizeObserver.observe(wrapperRef.value)
|
|
174
|
+
}
|
|
94
175
|
})
|
|
95
176
|
|
|
96
177
|
onBeforeUnmount(() => {
|
|
97
|
-
|
|
98
|
-
|
|
178
|
+
resizeObserver?.disconnect()
|
|
179
|
+
resizeObserver = null
|
|
99
180
|
})
|
|
100
181
|
|
|
101
182
|
watch(imageSrc, () => {
|
|
102
|
-
|
|
183
|
+
imageSize.width = 0
|
|
184
|
+
imageSize.height = 0
|
|
185
|
+
if (scrollerRef.value) scrollerRef.value.scrollTop = 0
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
watch([scaledHeight, offsetY], () => {
|
|
189
|
+
scheduleApplyDefaultScroll()
|
|
103
190
|
})
|
|
104
191
|
</script>
|
|
105
192
|
|
|
@@ -107,26 +194,26 @@ watch(imageSrc, () => {
|
|
|
107
194
|
.slidev-layout.image-scroll {
|
|
108
195
|
position: relative;
|
|
109
196
|
height: 100%;
|
|
197
|
+
padding: 0;
|
|
110
198
|
overflow: hidden;
|
|
199
|
+
touch-action: none;
|
|
111
200
|
}
|
|
112
201
|
|
|
113
|
-
.image-
|
|
202
|
+
.image-scroll__scroller {
|
|
114
203
|
position: absolute;
|
|
115
204
|
inset: 0;
|
|
116
|
-
overflow:
|
|
117
|
-
|
|
205
|
+
overflow-y: auto;
|
|
206
|
+
overflow-x: hidden;
|
|
207
|
+
scrollbar-gutter: stable;
|
|
208
|
+
overscroll-behavior: contain;
|
|
118
209
|
}
|
|
119
210
|
|
|
120
|
-
.image-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.image-layer img {
|
|
211
|
+
.image-scroll__image {
|
|
212
|
+
display: block;
|
|
125
213
|
width: 100%;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
transition: transform 80ms ease-out;
|
|
214
|
+
max-width: none;
|
|
215
|
+
pointer-events: none;
|
|
216
|
+
user-select: none;
|
|
130
217
|
}
|
|
131
218
|
|
|
132
219
|
.content {
|
|
@@ -134,5 +221,6 @@ watch(imageSrc, () => {
|
|
|
134
221
|
z-index: 1;
|
|
135
222
|
height: 100%;
|
|
136
223
|
padding: 2rem;
|
|
224
|
+
box-sizing: border-box;
|
|
137
225
|
}
|
|
138
226
|
</style>
|
package/package.json
CHANGED
package/styles/layout.css
CHANGED
|
@@ -110,24 +110,34 @@
|
|
|
110
110
|
position: relative;
|
|
111
111
|
padding: 0;
|
|
112
112
|
overflow: hidden;
|
|
113
|
+
touch-action: none;
|
|
113
114
|
|
|
114
115
|
.image-layer {
|
|
115
116
|
position: absolute;
|
|
116
117
|
inset: 0;
|
|
117
118
|
cursor: grab;
|
|
118
119
|
overflow: hidden;
|
|
120
|
+
user-select: none;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
.image-layer.is-panning {
|
|
122
124
|
cursor: grabbing;
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
.image-translate {
|
|
128
|
+
position: absolute;
|
|
129
|
+
top: 0;
|
|
130
|
+
left: 0;
|
|
131
|
+
will-change: transform;
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
.image-layer img {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
display: block;
|
|
136
|
+
max-width: none;
|
|
137
|
+
max-height: none;
|
|
138
|
+
pointer-events: none;
|
|
139
|
+
user-select: none;
|
|
129
140
|
will-change: transform;
|
|
130
|
-
transition: transform 80ms ease-out;
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
.content {
|