@c4h/chuci 0.1.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 +33 -0
- package/LICENSE +21 -0
- package/README.ja.md +144 -0
- package/README.md +225 -0
- package/dist/assets/azumaya_panorama1.png +0 -0
- package/dist/chuci.cjs +4483 -0
- package/dist/chuci.js +21710 -0
- package/dist/chuci.umd.js +4737 -0
- package/dist/index-8VMexD2a.cjs +255 -0
- package/dist/index-kvsisbKS.js +2125 -0
- package/dist/index.d.ts +203 -0
- package/dist/index.html +241 -0
- package/dist/test-image.html +63 -0
- package/package.json +86 -0
- package/src/components/swiper/cc-swiper-slide.ts +50 -0
- package/src/components/swiper/cc-swiper-styles.ts +29 -0
- package/src/components/swiper/cc-swiper.ts +362 -0
- package/src/components/swiper/swiper-styles.css +5 -0
- package/src/components/viewer/cc-viewer-3dmodel.ts +492 -0
- package/src/components/viewer/cc-viewer-base.ts +279 -0
- package/src/components/viewer/cc-viewer-gaussian.ts +381 -0
- package/src/components/viewer/cc-viewer-image.ts +190 -0
- package/src/components/viewer/cc-viewer-panorama.ts +86 -0
- package/src/components/viewer/cc-viewer-styles.ts +56 -0
- package/src/components/viewer/cc-viewer-video.ts +110 -0
- package/src/components/viewer/cc-viewer-youtube.ts +76 -0
- package/src/components/viewer/cc-viewer.ts +291 -0
- package/src/index.ts +25 -0
- package/src/types/css-modules.d.ts +1 -0
- package/src/types/global.d.ts +11 -0
- package/src/utils/base-element.ts +77 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { ChuciElement } from '@/utils/base-element'
|
|
2
|
+
import './cc-viewer-image'
|
|
3
|
+
import './cc-viewer-panorama'
|
|
4
|
+
import './cc-viewer-youtube'
|
|
5
|
+
import './cc-viewer-video'
|
|
6
|
+
import './cc-viewer-3dmodel'
|
|
7
|
+
import './cc-viewer-gaussian'
|
|
8
|
+
|
|
9
|
+
interface HashKeyTag {
|
|
10
|
+
[index: string]: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const typeHashes = {
|
|
14
|
+
image: 'cc-viewer-image',
|
|
15
|
+
panorama: 'cc-viewer-panorama',
|
|
16
|
+
youtube: 'cc-viewer-youtube',
|
|
17
|
+
video: 'cc-viewer-video',
|
|
18
|
+
'3dmodel': 'cc-viewer-3dmodel',
|
|
19
|
+
gaussian: 'cc-viewer-gaussian'
|
|
20
|
+
} as HashKeyTag
|
|
21
|
+
|
|
22
|
+
export class CcViewer extends ChuciElement {
|
|
23
|
+
// Swiper instance from cc-swiper component, no TypeScript types available
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
private swiper: any
|
|
26
|
+
private currentSlideIndex = 0
|
|
27
|
+
private currentType = ''
|
|
28
|
+
private boundHandleNavigatePrev?: (e: Event) => void
|
|
29
|
+
private boundHandleNavigateNext?: (e: Event) => void
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
open(imgUrl: string, type: string, attributes?: Record<string, any>) {
|
|
34
|
+
const previousType = this.currentType
|
|
35
|
+
this.currentType = type
|
|
36
|
+
|
|
37
|
+
// Close previous viewer if type changed
|
|
38
|
+
if (previousType && previousType !== type) {
|
|
39
|
+
const previousTag = typeHashes[previousType]
|
|
40
|
+
const previousHandler = this.query(previousTag)
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
if (previousHandler && (previousHandler as any).close) {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
(previousHandler as any).close()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Re-render if type changed
|
|
49
|
+
if (previousType !== type) {
|
|
50
|
+
this.render()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Wait for render to complete
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
const targetTag = typeHashes[type]
|
|
56
|
+
const handler = this.query(targetTag)
|
|
57
|
+
|
|
58
|
+
// Pass attributes to the viewer if available
|
|
59
|
+
if (handler && attributes) {
|
|
60
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
61
|
+
// Convert camelCase to kebab-case for attributes
|
|
62
|
+
const attrName = key.replace(/([A-Z])/g, '-$1').toLowerCase()
|
|
63
|
+
if (typeof value === 'boolean') {
|
|
64
|
+
if (value) {
|
|
65
|
+
handler.setAttribute(attrName, '')
|
|
66
|
+
} else {
|
|
67
|
+
handler.removeAttribute(attrName)
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
handler.setAttribute(attrName, String(value))
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Update navigation button visibility
|
|
76
|
+
this.updateNavigationButtons()
|
|
77
|
+
|
|
78
|
+
if (handler) {
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
;(handler as any).open(imgUrl)
|
|
81
|
+
}
|
|
82
|
+
}, 0)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected firstUpdated() {
|
|
86
|
+
// Dispatch load event when the component is ready
|
|
87
|
+
this.dispatch('load')
|
|
88
|
+
|
|
89
|
+
// Remove existing listeners if any
|
|
90
|
+
if (this.boundHandleNavigatePrev) {
|
|
91
|
+
this.removeEventListener('navigate-prev', this.boundHandleNavigatePrev)
|
|
92
|
+
}
|
|
93
|
+
if (this.boundHandleNavigateNext) {
|
|
94
|
+
this.removeEventListener('navigate-next', this.boundHandleNavigateNext)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create bound functions once
|
|
98
|
+
this.boundHandleNavigatePrev = this.handleNavigatePrev.bind(this)
|
|
99
|
+
this.boundHandleNavigateNext = this.handleNavigateNext.bind(this)
|
|
100
|
+
|
|
101
|
+
// Listen for navigation events
|
|
102
|
+
this.addEventListener('navigate-prev', this.boundHandleNavigatePrev)
|
|
103
|
+
this.addEventListener('navigate-next', this.boundHandleNavigateNext)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private handleNavigatePrev(_e?: Event) {
|
|
107
|
+
if (!this.swiper) return
|
|
108
|
+
|
|
109
|
+
const totalSlides = this.swiper.slides.length
|
|
110
|
+
if (totalSlides <= 1) return // No navigation for single slide
|
|
111
|
+
|
|
112
|
+
// Check if loop is enabled in swiper
|
|
113
|
+
const hasLoop = this.swiper.slider?.params?.loop === true
|
|
114
|
+
|
|
115
|
+
if (this.currentSlideIndex <= 0) {
|
|
116
|
+
if (hasLoop) {
|
|
117
|
+
this.currentSlideIndex = totalSlides - 1 // Go to last slide
|
|
118
|
+
} else {
|
|
119
|
+
return // Can't go previous
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
this.currentSlideIndex--
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.navigateToSlide(this.currentSlideIndex)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private handleNavigateNext(_e?: Event) {
|
|
129
|
+
if (!this.swiper) return
|
|
130
|
+
|
|
131
|
+
const totalSlides = this.swiper.slides.length
|
|
132
|
+
if (totalSlides <= 1) return // No navigation for single slide
|
|
133
|
+
|
|
134
|
+
// Check if loop is enabled in swiper
|
|
135
|
+
const hasLoop = this.swiper.slider?.params?.loop === true
|
|
136
|
+
|
|
137
|
+
if (this.currentSlideIndex >= totalSlides - 1) {
|
|
138
|
+
if (hasLoop) {
|
|
139
|
+
this.currentSlideIndex = 0 // Go to first slide
|
|
140
|
+
} else {
|
|
141
|
+
return // Can't go next
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
this.currentSlideIndex++
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.navigateToSlide(this.currentSlideIndex)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private navigateToSlide(index: number) {
|
|
151
|
+
if (!this.swiper || !this.swiper.slides[index]) return
|
|
152
|
+
|
|
153
|
+
// Get new slide info first
|
|
154
|
+
const slide = this.swiper.slides[index]
|
|
155
|
+
const imageUrl = slide.getAttribute('image-url') || ''
|
|
156
|
+
const imageType = slide.getAttribute('image-type') || 'image'
|
|
157
|
+
|
|
158
|
+
// Close current viewer
|
|
159
|
+
const currentTag = typeHashes[this.currentType]
|
|
160
|
+
const currentHandler = this.query(currentTag)
|
|
161
|
+
if (currentHandler) {
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
|
+
(currentHandler as any).close()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Gather viewer-specific attributes
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
168
|
+
const attributes: Record<string, any> = {}
|
|
169
|
+
if (slide.hasAttribute('fit-to-container')) {
|
|
170
|
+
attributes.fitToContainer = true
|
|
171
|
+
}
|
|
172
|
+
if (slide.hasAttribute('debug-mode')) {
|
|
173
|
+
attributes.debugMode = true
|
|
174
|
+
}
|
|
175
|
+
if (slide.hasAttribute('camera-position')) {
|
|
176
|
+
attributes.cameraPosition = slide.getAttribute('camera-position')
|
|
177
|
+
}
|
|
178
|
+
if (slide.hasAttribute('camera-target')) {
|
|
179
|
+
attributes.cameraTarget = slide.getAttribute('camera-target')
|
|
180
|
+
}
|
|
181
|
+
if (slide.hasAttribute('show-texture')) {
|
|
182
|
+
attributes.showTexture = slide.getAttribute('show-texture') === 'true'
|
|
183
|
+
}
|
|
184
|
+
if (slide.hasAttribute('material-url')) {
|
|
185
|
+
attributes.materialUrl = slide.getAttribute('material-url')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.currentSlideIndex = index
|
|
189
|
+
this.open(imageUrl, imageType, attributes)
|
|
190
|
+
|
|
191
|
+
// Update swiper position
|
|
192
|
+
if (this.swiper.slider) {
|
|
193
|
+
this.swiper.slider.slideTo(index)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private updateNavigationButtons() {
|
|
198
|
+
if (!this.swiper) return
|
|
199
|
+
|
|
200
|
+
const totalSlides = this.swiper.slides.length
|
|
201
|
+
const hasLoop = this.swiper.slider?.params?.loop === true
|
|
202
|
+
|
|
203
|
+
// For single slide, hide both buttons
|
|
204
|
+
if (totalSlides <= 1) {
|
|
205
|
+
this.setNavigationVisibility(false, false)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// For multiple slides with loop, show both
|
|
210
|
+
if (hasLoop) {
|
|
211
|
+
this.setNavigationVisibility(true, true)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// For multiple slides without loop, check boundaries
|
|
216
|
+
const showPrev = this.currentSlideIndex > 0
|
|
217
|
+
const showNext = this.currentSlideIndex < totalSlides - 1
|
|
218
|
+
this.setNavigationVisibility(showPrev, showNext)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private setNavigationVisibility(showPrev: boolean, showNext: boolean) {
|
|
222
|
+
// Update all viewer instances
|
|
223
|
+
const viewers = [
|
|
224
|
+
this.query('cc-viewer-image'),
|
|
225
|
+
this.query('cc-viewer-youtube'),
|
|
226
|
+
this.query('cc-viewer-panorama'),
|
|
227
|
+
this.query('cc-viewer-video'),
|
|
228
|
+
this.query('cc-viewer-3dmodel'),
|
|
229
|
+
this.query('cc-viewer-gaussian')
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
viewers.forEach(viewer => {
|
|
233
|
+
if (viewer) {
|
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
235
|
+
const v = viewer as any
|
|
236
|
+
v.showPrevButton = showPrev
|
|
237
|
+
v.showNextButton = showNext
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Public method to set swiper reference
|
|
243
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
244
|
+
setSwiper(swiper: any) {
|
|
245
|
+
this.swiper = swiper
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Public method to set current slide index
|
|
249
|
+
setCurrentSlideIndex(index: number) {
|
|
250
|
+
this.currentSlideIndex = index
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
protected render() {
|
|
254
|
+
const styles = this.css`
|
|
255
|
+
:host {
|
|
256
|
+
--cc-viewer-z-index: 1000;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
cc-viewer-panorama, cc-viewer-image, cc-viewer-youtube, cc-viewer-video,
|
|
260
|
+
cc-viewer-3dmodel, cc-viewer-gaussian {
|
|
261
|
+
--cc-viewer-z-index-each: var(--cc-viewer-z-index);
|
|
262
|
+
}
|
|
263
|
+
`
|
|
264
|
+
|
|
265
|
+
// Only render the current viewer type
|
|
266
|
+
let viewerHtml = ''
|
|
267
|
+
if (this.currentType) {
|
|
268
|
+
const tag = typeHashes[this.currentType]
|
|
269
|
+
if (tag) {
|
|
270
|
+
viewerHtml = `<${tag}></${tag}>`
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const html = `
|
|
275
|
+
${styles}
|
|
276
|
+
${viewerHtml}
|
|
277
|
+
`
|
|
278
|
+
|
|
279
|
+
this.updateShadowRoot(html)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!customElements.get('cc-viewer')) {
|
|
284
|
+
customElements.define('cc-viewer', CcViewer)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
declare global {
|
|
288
|
+
interface HTMLElementTagNameMap {
|
|
289
|
+
'cc-viewer': CcViewer
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Export all components
|
|
2
|
+
export * from './components/swiper/cc-swiper'
|
|
3
|
+
export * from './components/swiper/cc-swiper-slide'
|
|
4
|
+
export * from './components/viewer/cc-viewer'
|
|
5
|
+
export * from './components/viewer/cc-viewer-base'
|
|
6
|
+
export * from './components/viewer/cc-viewer-image'
|
|
7
|
+
export * from './components/viewer/cc-viewer-panorama'
|
|
8
|
+
export * from './components/viewer/cc-viewer-youtube'
|
|
9
|
+
export * from './components/viewer/cc-viewer-video'
|
|
10
|
+
export * from './components/viewer/cc-viewer-3dmodel'
|
|
11
|
+
export * from './components/viewer/cc-viewer-gaussian'
|
|
12
|
+
|
|
13
|
+
// Export utilities
|
|
14
|
+
export * from './utils/base-element'
|
|
15
|
+
|
|
16
|
+
// Auto-register components when imported
|
|
17
|
+
import './components/swiper/cc-swiper'
|
|
18
|
+
import './components/swiper/cc-swiper-slide'
|
|
19
|
+
import './components/viewer/cc-viewer'
|
|
20
|
+
import './components/viewer/cc-viewer-image'
|
|
21
|
+
import './components/viewer/cc-viewer-panorama'
|
|
22
|
+
import './components/viewer/cc-viewer-youtube'
|
|
23
|
+
import './components/viewer/cc-viewer-video'
|
|
24
|
+
import './components/viewer/cc-viewer-3dmodel'
|
|
25
|
+
import './components/viewer/cc-viewer-gaussian'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css?inline" { const content: string; export default content; }
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for Chuci web components without Lit dependency
|
|
3
|
+
*/
|
|
4
|
+
export abstract class ChuciElement extends HTMLElement {
|
|
5
|
+
private _shadowRoot: ShadowRoot
|
|
6
|
+
private _connected = false
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
super()
|
|
10
|
+
this._shadowRoot = this.attachShadow({ mode: 'open' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
connectedCallback() {
|
|
14
|
+
if (!this._connected) {
|
|
15
|
+
this._connected = true
|
|
16
|
+
this.render()
|
|
17
|
+
this.firstUpdated()
|
|
18
|
+
} else {
|
|
19
|
+
this.render()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
disconnectedCallback() {
|
|
24
|
+
this._connected = false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
attributeChangedCallback(_name: string, oldValue: string | null, newValue: string | null) {
|
|
28
|
+
if (oldValue !== newValue) {
|
|
29
|
+
this.render()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected firstUpdated(): void {
|
|
34
|
+
// Override in subclasses for initialization logic
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected abstract render(): void
|
|
38
|
+
|
|
39
|
+
// Template literal values can be any type that can be converted to string
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
protected html(strings: TemplateStringsArray, ...values: any[]): string {
|
|
42
|
+
return strings.reduce((result, str, i) => {
|
|
43
|
+
return result + str + (values[i] || '')
|
|
44
|
+
}, '')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Template literal values can be any type that can be converted to string
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
protected css(strings: TemplateStringsArray, ...values: any[]): string {
|
|
50
|
+
const cssText = strings.reduce((result, str, i) => {
|
|
51
|
+
return result + str + (values[i] || '')
|
|
52
|
+
}, '')
|
|
53
|
+
return `<style>${cssText}</style>`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected updateShadowRoot(content: string) {
|
|
57
|
+
this._shadowRoot.innerHTML = content
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected query<T extends HTMLElement>(selector: string): T | null {
|
|
61
|
+
return this._shadowRoot.querySelector<T>(selector)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected queryAll<T extends HTMLElement>(selector: string): NodeListOf<T> {
|
|
65
|
+
return this._shadowRoot.querySelectorAll<T>(selector)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Custom event detail can be any type
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
protected dispatch(eventName: string, detail?: any) {
|
|
71
|
+
this.dispatchEvent(new CustomEvent(eventName, {
|
|
72
|
+
detail,
|
|
73
|
+
bubbles: true,
|
|
74
|
+
composed: true
|
|
75
|
+
}))
|
|
76
|
+
}
|
|
77
|
+
}
|