lecodes-viewer 0.19.2 → 0.20.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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "lecodes-viewer",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "main": "src/createViewer.ts",
5
5
  "dependencies": {
6
- "lecodes-renderer": "0.19.2",
6
+ "lecodes-renderer": "0.20.0",
7
7
  "ogl": "^1.0.11",
8
- "lecodes-viewer-lite": "0.19.2"
8
+ "lecodes-viewer-lite": "0.20.0"
9
9
  },
10
10
  "scripts": {
11
11
  "dev": "vite",
@@ -25,6 +25,10 @@
25
25
  "type": "module",
26
26
  "files": [
27
27
  "src",
28
+ "!src/assets/*.mp4",
29
+ "!src/assets/*.png",
30
+ "!src/assets/*.svg",
31
+ "!src/assets/neutral_ibl256.ktx",
28
32
  "dist-embed",
29
33
  "embed.html",
30
34
  "README.md"
package/src/host.ts CHANGED
@@ -13,6 +13,7 @@ import { createCanvasHost } from './canvas/createCanvasHost'
13
13
  import { putBuffer, getBuffer } from './utils/fetch'
14
14
  import { createNavHostBridge } from 'lecodes-viewer-lite/src/gl/core/navBridge'
15
15
  import { createInputHost } from "lecodes-viewer-lite/src/utils/input"
16
+ import { registerWebPlugins } from "lecodes-viewer-lite/src/plugins"
16
17
  import { createUtils } from './utils/createUtils'
17
18
  import { ensureFontsLoaded, loadBootFonts, type BootFont } from './utils/fonts'
18
19
  import type { SafeAreaSpec } from './ui/safeAreaSpec'
@@ -217,6 +218,8 @@ export const createHost = async (root: HTMLElement, options: HostOptions = {}) =
217
218
  }
218
219
 
219
220
  const _creatorUI = options.ui ? await options.ui(root, canvasHost) : undefined
221
+ // The web twins of the first-party NativeView plugins (camera, map) — shared with the lite host.
222
+ registerWebPlugins(_creatorUI)
220
223
  const _creatorUtils = createUtils({ launchUrl: options.launchUrl, onOrientation: options.onOrientation })
221
224
  _creatorUI?._setCreator(_creator)
222
225
  // Before the ResizeObserver's initial fire, so the first applied layout already has the insets.
File without changes
@@ -1,91 +1,2 @@
1
- // NativeView on the web host (docs/navigation-presentable-plan.md registerView + the
2
- // call/event channel + the "native" layout node). The embedding app registers DOM factories:
3
- //
4
- // host._creatorUI._registerView("map", (params, channel) => {
5
- // const el = document.createElement("div")
6
- // return { el, call: (method, args) => {...}, destroy: () => {...} }
7
- // })
8
- //
9
- // One SDK NativeView instance = one live DOM instance (keyed by its _viewId), reused across
10
- // embed/fullscreen/pager mounts — that reuse IS the promotion semantics. An unregistered
11
- // name renders a labeled placeholder so a bundle stays inspectable on hosts without the plugin.
12
-
13
- export type WebViewChannel = { emit(event: string, data?: any): void }
14
- export type WebViewInstanceHandle = {
15
- el: HTMLElement
16
- call?(method: string, args: any[]): any | Promise<any>
17
- destroy?(): void
18
- }
19
- export type WebViewFactory = (params: any, channel: WebViewChannel) => WebViewInstanceHandle
20
-
21
- // The SDK-side NativeView element/descriptor surface the system needs (see sdk NativeView.ts).
22
- type SdkNativeView = {
23
- viewName: string
24
- params: any
25
- _viewId: number
26
- _emitViewEvent(event: string, dataJson?: string): void
27
- }
28
-
29
- export const createNativeViewSystem = () => {
30
- const factories = new Map<string, WebViewFactory>()
31
- const instances = new Map<number, WebViewInstanceHandle>()
32
-
33
- const registerView = (name: string, factory: WebViewFactory) => { factories.set(name, factory) }
34
- const isViewSupported = (name: string) => factories.has(name)
35
-
36
- const placeholder = (name: string): WebViewInstanceHandle => {
37
- const el = document.createElement("div")
38
- el.style.cssText = "display:flex;align-items:center;justify-content:center;background:#333;color:#999;font:12px sans-serif"
39
- el.textContent = `NativeView("${name}") — no web factory registered`
40
- return { el }
41
- }
42
-
43
- /** Resolve (or lazily create) the live instance for an SDK NativeView. */
44
- const ensureInstance = (view: SdkNativeView): WebViewInstanceHandle => {
45
- let instance = instances.get(view._viewId)
46
- if (!instance) {
47
- const factory = factories.get(view.viewName)
48
- instance = factory
49
- ? factory(view.params, { emit: (event, data) => view._emitViewEvent(event, data === undefined ? undefined : JSON.stringify(data)) })
50
- : placeholder(view.viewName)
51
- instances.set(view._viewId, instance)
52
- }
53
- return instance
54
- }
55
-
56
- /** The `_creatorUI.viewCall` bridge — JSON in/out, promise-friendly. */
57
- const viewCall = (viewId: number, method: string, args: string, onComplete: (result?: string) => void, onError: (err: string) => void) => {
58
- const instance = instances.get(viewId)
59
- if (!instance?.call) {
60
- onError(`NativeView call "${method}": no live instance or no call handler (viewId ${viewId})`)
61
- return
62
- }
63
- try {
64
- Promise.resolve(instance.call(method, JSON.parse(args))).then(
65
- (result) => onComplete(result === undefined ? undefined : JSON.stringify(result)),
66
- (e) => onError(e instanceof Error ? e.message : String(e)),
67
- )
68
- } catch (e) {
69
- onError(e instanceof Error ? e.message : String(e))
70
- }
71
- }
72
-
73
- /** Mount the live instance into a layout cell (the "native" node's DOM element). A view lives
74
- * in ONE place: mounting here steals it from wherever it was (promotion works the same way
75
- * in reverse — openView steals it from the cell, close returns it via re-mount). */
76
- const mountInto = (view: SdkNativeView, cell: HTMLElement) => {
77
- const { el } = ensureInstance(view)
78
- el.style.position = "absolute"
79
- el.style.inset = "0"
80
- cell.appendChild(el)
81
- }
82
-
83
- const destroy = () => {
84
- for (const i of instances.values()) i.destroy?.()
85
- instances.clear()
86
- }
87
-
88
- return { registerView, isViewSupported, ensureInstance, viewCall, mountInto, destroy }
89
- }
90
-
91
- export type NativeViewSystem = ReturnType<typeof createNativeViewSystem>
1
+ // Moved to viewer-lite (the NativeView registry + channel are shared by both web hosts); re-export shim.
2
+ export * from "lecodes-viewer-lite/src/ui/nativeViewSystem"
@@ -1,156 +1,2 @@
1
- // Destination transition player (docs/navigation-presentable-plan.md, phase 2 web reference).
2
- // Named transitions are predefined TransitionSpecs; both shapes play through element.animate(),
3
- // so a custom spec from the SDK needs no extra machinery.
4
- //
5
- // The static rule (mirrors the Android SurfaceView constraint so apps feel the same): a side
6
- // marked `static` (the scene canvas) never animates — only the screen side moves. Screens are
7
- // position:absolute so they always paint above the in-flow canvas, which makes the degradations
8
- // coherent: entering a scene slides the old screen away to reveal it; leaving a scene slides the
9
- // new screen in over it.
10
-
11
- export type SpecTransform = { x?: number | string, y?: number | string, scale?: number, opacity?: number }
12
- export type TransitionSpecLike = {
13
- duration?: number
14
- easing?: [number, number, number, number]
15
- incoming?: { from?: SpecTransform }
16
- outgoing?: { to?: SpecTransform }
17
- dim?: number
18
- onTop?: "incoming" | "outgoing"
19
- }
20
-
21
- const STACK_EASING: [number, number, number, number] = [0.2, 0, 0, 1]
22
-
23
- const NAMED: Record<string, TransitionSpecLike> = {
24
- "none": {},
25
- "fade": { duration: 280, incoming: { from: { opacity: 0 } } },
26
- "slide-from-right": { duration: 400, incoming: { from: { x: "100%" } }, outgoing: { to: { x: "-30%" } } },
27
- "slide-from-left": { duration: 400, incoming: { from: { x: "-100%" } }, outgoing: { to: { x: "30%" } } },
28
- "slide-from-top": { duration: 400, incoming: { from: { y: "-100%" } }, outgoing: { to: { y: "30%" } } },
29
- "slide-from-bottom": { duration: 400, incoming: { from: { y: "100%" } }, outgoing: { to: { y: "-30%" } } },
30
- "zoom": { duration: 400, easing: [0.33, 1.2, 0.62, 1], incoming: { from: { scale: 0.985 } } },
31
- "zoom-in": { duration: 400, easing: [0.33, 1.2, 0.62, 1], incoming: { from: { scale: 1.08, opacity: 0 } }, onTop: "incoming" },
32
- "zoom-out": { duration: 420, outgoing: { to: { scale: 0.92, opacity: 0 } }, onTop: "outgoing" },
33
- "push": { duration: 350, easing: STACK_EASING, incoming: { from: { x: "100%" } }, outgoing: { to: { x: "-25%" } }, dim: 0.3, onTop: "incoming" },
34
- "pop": { duration: 350, easing: STACK_EASING, incoming: { from: { x: "-25%" } }, outgoing: { to: { x: "100%" } }, dim: 0.3, onTop: "outgoing" },
35
- }
36
-
37
- const toTransform = (t: SpecTransform): string => {
38
- const parts: string[] = []
39
- if (t.x !== undefined || t.y !== undefined) {
40
- const px = (v: number | string | undefined) => v === undefined ? "0px" : typeof v === "number" ? v + "px" : v
41
- parts.push(`translate(${px(t.x)}, ${px(t.y)})`)
42
- }
43
- if (t.scale !== undefined) parts.push(`scale(${t.scale})`)
44
- return parts.length > 0 ? parts.join(" ") : "none"
45
- }
46
-
47
- const poseKeyframe = (t: SpecTransform): Keyframe => {
48
- const kf: Keyframe = { transform: toTransform(t) }
49
- if (t.opacity !== undefined) kf.opacity = t.opacity
50
- return kf
51
- }
52
- const IDENTITY: Keyframe = { transform: "none", opacity: 1 }
53
-
54
- // The exit pose for an outgoing side when the incoming one is static (a scene): mirror the
55
- // incoming entry so the reveal keeps the transition's direction (push's 100%-from-right becomes
56
- // a full slide-out to the left); pure opacity entries become a fade-out.
57
- const mirrorExit = (from: SpecTransform): SpecTransform => {
58
- const neg = (v: number | string | undefined) =>
59
- v === undefined ? undefined : typeof v === "number" ? -v : v.startsWith("-") ? v.slice(1) : "-" + v
60
- const out: SpecTransform = {}
61
- if (from.x !== undefined) out.x = neg(from.x)
62
- if (from.y !== undefined) out.y = neg(from.y)
63
- if (from.scale !== undefined) out.scale = 2 - from.scale
64
- if (from.opacity !== undefined || Object.keys(out).length === 0) out.opacity = 0
65
- return out
66
- }
67
-
68
- type Side = { el: HTMLElement, static?: boolean }
69
-
70
- export const createPresentPlayer = (container: HTMLElement) => {
71
- let finishActive: (() => void) | null = null
72
-
73
- /** Play `transition` between the mounted `incoming` and the still-mounted `outgoing`; calls
74
- * `onDone` exactly once (after the animation, or immediately for none/unsupported) — the
75
- * caller physically unmounts the outgoing side there. A new play() snaps the previous
76
- * transition to its end first. */
77
- const play = (opts: {
78
- incoming?: Side
79
- outgoing?: Side
80
- transition: string | TransitionSpecLike | null | undefined
81
- onDone: () => void
82
- }) => {
83
- finishActive?.()
84
-
85
- const spec = typeof opts.transition === "string" ? NAMED[opts.transition] : opts.transition ?? undefined
86
- const inSide = opts.incoming && !opts.incoming.static ? opts.incoming.el : null
87
- const outSide = opts.outgoing && !opts.outgoing.static ? opts.outgoing.el : null
88
-
89
- let inFrom = spec?.incoming?.from
90
- let outTo = spec?.outgoing?.to
91
-
92
- // Static-side degradations (see header). A poseless spec ("none") stays instant — the
93
- // degradations only reshape transitions that actually wanted to move something.
94
- if (opts.incoming?.static && outSide && (spec?.incoming || spec?.outgoing)) {
95
- // The incoming scene can't animate: unless this is an exit-shaped transition (onTop:
96
- // "outgoing" — pop/zoom-out already fully remove the old side), the outgoing screen leaves
97
- // by the mirror of the entry it was supposed to be covered by.
98
- if (spec?.onTop !== "outgoing") outTo = inFrom ? mirrorExit(inFrom) : (outTo ?? { opacity: 0 })
99
- inFrom = undefined
100
- }
101
- if (opts.outgoing?.static && inSide && !inFrom && spec && Object.keys(spec).length > 0) {
102
- inFrom = { opacity: 0 } // something must move; fade the screen in over the scene
103
- }
104
-
105
- const hasWork = (inSide && inFrom) || (outSide && outTo)
106
- if (!spec || !hasWork || typeof (inSide ?? outSide)?.animate !== "function") {
107
- opts.onDone()
108
- return
109
- }
110
-
111
- const duration = spec.duration ?? 300
112
- const easing = spec.easing ? `cubic-bezier(${spec.easing.join(",")})` : "cubic-bezier(0.25, 0.1, 0.25, 1)"
113
- const animations: Animation[] = []
114
-
115
- // z-order for the duration of the transition (screens only — the static canvas stays in
116
- // flow at the bottom). Dim sits between the two.
117
- const onTop = spec.onTop ?? "incoming"
118
- const topEl = onTop === "incoming" ? inSide : outSide
119
- const bottomEl = onTop === "incoming" ? outSide : inSide
120
- if (topEl) topEl.style.zIndex = "3"
121
- if (bottomEl) bottomEl.style.zIndex = "1"
122
-
123
- let dimEl: HTMLElement | null = null
124
- if (spec.dim && spec.dim > 0) {
125
- dimEl = document.createElement("div")
126
- dimEl.style.cssText = "position:absolute;inset:0;background:#000;pointer-events:none;z-index:2"
127
- container.appendChild(dimEl)
128
- // Entering (push): the under layer darkens; exiting (pop): it un-darkens.
129
- const [a, b] = onTop === "incoming" ? [0, spec.dim] : [spec.dim, 0]
130
- animations.push(dimEl.animate([{ opacity: a }, { opacity: b }], { duration, easing }))
131
- }
132
-
133
- if (inSide && inFrom) {
134
- animations.push(inSide.animate([poseKeyframe(inFrom), IDENTITY], { duration, easing }))
135
- }
136
- if (outSide && outTo) {
137
- animations.push(outSide.animate([IDENTITY, poseKeyframe(outTo)], { duration, easing, fill: "forwards" }))
138
- }
139
-
140
- let done = false
141
- const cleanup = () => {
142
- if (done) return
143
- done = true
144
- finishActive = null
145
- for (const a of animations) a.cancel() // drops fill:forwards so a detached-not-freed screen restores clean
146
- if (topEl) topEl.style.zIndex = ""
147
- if (bottomEl) bottomEl.style.zIndex = ""
148
- dimEl?.remove()
149
- opts.onDone()
150
- }
151
- finishActive = cleanup
152
- Promise.all(animations.map(a => a.finished)).then(cleanup, cleanup)
153
- }
154
-
155
- return { play, finish: () => finishActive?.() }
156
- }
1
+ // Moved to viewer-lite (both web hosts play the same destination transitions); re-export shim.
2
+ export * from "lecodes-viewer-lite/src/ui/presentPlayer"