roxy-cobewebgl 1.0.0 → 1.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/README.md CHANGED
@@ -37,6 +37,100 @@ pnpm dev
37
37
 
38
38
  浏览器将打开 [examples/playground/index.html](examples/playground/index.html):本地直连 `src/` 源码,带参数面板与示例弧线(**不**打进 npm 包)。
39
39
 
40
+ ## Vue 3 组件
41
+
42
+ 依赖 **Vue ^3.3**,构建工具需正确处理 `.vue`(如 Vite + `@vitejs/plugin-vue`)。
43
+
44
+ ```bash
45
+ pnpm add roxy-cobewebgl vue
46
+ ```
47
+
48
+ ```vue
49
+ <script setup>
50
+ import RoxyCobewebgl from 'roxy-cobewebgl/vue';
51
+
52
+ const arcs = [
53
+ { startLat: 39.9, startLng: 116.4, endLat: 35.6, endLng: 139.6, color: '#ff6633' },
54
+ ];
55
+ </script>
56
+
57
+ <template>
58
+ <div style="width: 100%; height: 400px">
59
+ <RoxyCobewebgl
60
+ :arcs="arcs"
61
+ :dots="1800"
62
+ :globe-radius="0.55"
63
+ @error="(e) => console.error(e)"
64
+ @ready="() => {}"
65
+ />
66
+ </div>
67
+ </template>
68
+ ```
69
+
70
+ - **Props**:与 `createGlobe` 的 `CreateGlobeOptions` 一致,**无** `onError`;错误用 **`@error`**。
71
+ - **`@ready`**:参数为 `GlobeInstance | null`(与 `createGlobe` 返回值相同语义)。
72
+ - **`ref` 暴露**:`setState`、`resize`、`destroy`、`getGlobe()`。
73
+ - 修改 **`map` / `mapUrl` / `arcs` / `preferWorker` / `debugShowTexture`** 会销毁并重建地球;其余视觉参数走 `setState`。
74
+
75
+ 示例项目(通过 `file:../..` 依赖本地包,可对照集成方式):
76
+
77
+ ```bash
78
+ pnpm install
79
+ pnpm dev:vue
80
+ ```
81
+
82
+ 或进入 [examples/vue-demo](examples/vue-demo) 执行 `pnpm install` 与 `pnpm dev`(默认端口 `5174`)。
83
+
84
+ ## React 18+ 组件
85
+
86
+ 依赖 **React / React-DOM ^18 或 ^19**,需能解析 **JSX** 与 `.jsx`(如 Vite + `@vitejs/plugin-react`)。
87
+
88
+ ```bash
89
+ pnpm add roxy-cobewebgl react react-dom
90
+ ```
91
+
92
+ ```jsx
93
+ import { useMemo } from 'react';
94
+ import RoxyCobewebgl from 'roxy-cobewebgl/react';
95
+
96
+ export function GlobeCard() {
97
+ const arcs = useMemo(
98
+ () => [
99
+ { startLat: 39.9, startLng: 116.4, endLat: 35.6, endLng: 139.6, color: '#ff6633' },
100
+ ],
101
+ []
102
+ );
103
+
104
+ return (
105
+ <div style={{ width: '100%', height: 400 }}>
106
+ <RoxyCobewebgl
107
+ arcs={arcs}
108
+ dots={1800}
109
+ globeRadius={0.55}
110
+ onError={(e) => console.error(e)}
111
+ onReady={() => {}}
112
+ />
113
+ </div>
114
+ );
115
+ }
116
+ ```
117
+
118
+ 另支持 **`import { RoxyCobewebgl } from 'roxy-cobewebgl/react'`**。可传 **`className` / `style`** 到内部 `<canvas>`。
119
+
120
+ - **Props**:与 `CreateGlobeOptions` 一致,并增加 **`onReady`**(`onError` 仍与底层 API 相同)。
121
+ - **`ref`**:`setState`、`resize`、`destroy`、`getGlobe()`。
122
+ - **`map` / `mapUrl` / `arcs` / `preferWorker` / `debugShowTexture`** 变化会整实例重建;其余走 `setState`。
123
+ - **`onError` / `onReady`** 通过 ref 保持最新,避免因父组件重渲染导致整球重建。
124
+
125
+ 示例:
126
+
127
+ ```bash
128
+ pnpm install
129
+ pnpm dev:react
130
+ ```
131
+
132
+ 或 [examples/react-demo](examples/react-demo)(默认端口 `5175`)。
133
+
40
134
  ## 地图数据
41
135
 
42
136
  | 文件 | 说明 |
@@ -119,6 +213,8 @@ pnpm run build:globe
119
213
  ## 子路径导出(package exports)
120
214
 
121
215
  - `roxy-cobewebgl` — 主入口
216
+ - `roxy-cobewebgl/vue` — Vue 3 封装组件 `RoxyCobewebgl`
217
+ - `roxy-cobewebgl/react` — React 封装组件 `RoxyCobewebgl`
122
218
  - `roxy-cobewebgl/globe.worker.js` — Worker 源码(多数场景无需直接 import)
123
219
  - `roxy-cobewebgl/globe.min.json` / `roxy-cobewebgl/globe.json` — 地图文件
124
220
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxy-cobewebgl",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Cobe-style dotted globe with WebGL (ESM): OffscreenCanvas worker or main-thread fallback",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -13,17 +13,47 @@
13
13
  },
14
14
  "./globe.worker.js": "./src/globe.worker.js",
15
15
  "./globe.json": "./globe.json",
16
- "./globe.min.json": "./globe.min.json"
16
+ "./globe.min.json": "./globe.min.json",
17
+ "./vue": {
18
+ "types": "./vue.d.ts",
19
+ "import": "./src/vue-entry.js",
20
+ "default": "./src/vue-entry.js"
21
+ },
22
+ "./react": {
23
+ "types": "./react.d.ts",
24
+ "import": "./src/react-entry.js",
25
+ "default": "./src/react-entry.js"
26
+ }
27
+ },
28
+ "peerDependencies": {
29
+ "vue": "^3.3.0",
30
+ "react": "^18.0.0 || ^19.0.0",
31
+ "react-dom": "^18.0.0 || ^19.0.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "vue": {
35
+ "optional": true
36
+ },
37
+ "react": {
38
+ "optional": true
39
+ },
40
+ "react-dom": {
41
+ "optional": true
42
+ }
17
43
  },
18
44
  "files": [
19
45
  "src",
20
46
  "globe.json",
21
47
  "globe.min.json",
22
48
  "index.d.ts",
49
+ "vue.d.ts",
50
+ "react.d.ts",
23
51
  "README.md"
24
52
  ],
25
53
  "scripts": {
26
54
  "dev": "vite",
55
+ "dev:vue": "pnpm --dir examples/vue-demo dev",
56
+ "dev:react": "pnpm --dir examples/react-demo dev",
27
57
  "build:globe": "node scripts/slim-globe.mjs"
28
58
  },
29
59
  "keywords": [
package/react.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type {
2
+ ForwardRefExoticComponent,
3
+ RefAttributes,
4
+ CSSProperties,
5
+ } from 'react';
6
+ import type { CreateGlobeOptions, GlobeInstance } from './index';
7
+
8
+ export type RoxyCobewebglProps = Partial<CreateGlobeOptions> & {
9
+ /** 地球实例创建完成(与 `createGlobe` 返回值相同语义) */
10
+ onReady?: (instance: GlobeInstance | null) => void;
11
+ className?: string;
12
+ style?: CSSProperties;
13
+ };
14
+
15
+ export type RoxyCobewebglRef = {
16
+ setState: GlobeInstance['setState'];
17
+ resize: GlobeInstance['resize'];
18
+ destroy: GlobeInstance['destroy'];
19
+ getGlobe: () => GlobeInstance | null;
20
+ };
21
+
22
+ declare const RoxyCobewebgl: ForwardRefExoticComponent<
23
+ RoxyCobewebglProps & RefAttributes<RoxyCobewebglRef>
24
+ >;
25
+
26
+ export { RoxyCobewebgl };
27
+ export default RoxyCobewebgl;
@@ -0,0 +1,157 @@
1
+ import React, {
2
+ forwardRef,
3
+ useImperativeHandle,
4
+ useRef,
5
+ useEffect,
6
+ } from 'react';
7
+ import { createGlobe } from '../index.js';
8
+
9
+ const SET_STATE_KEYS = [
10
+ 'phi', 'theta', 'baseColor', 'glowColor', 'dotColor', 'arcColor',
11
+ 'dots', 'dotSize', 'globeRadius', 'glowOn', 'debug', 'opacity',
12
+ 'autoRotate', 'rotationSpeed',
13
+ ];
14
+
15
+ function pickGlobeInitProps(props) {
16
+ const { onReady: _r, onError: _e, ...globe } = props;
17
+ return Object.fromEntries(
18
+ Object.entries(globe).filter(([, v]) => v !== undefined)
19
+ );
20
+ }
21
+
22
+ function pickSetStatePatch(props) {
23
+ const patch = {};
24
+ for (const k of SET_STATE_KEYS) {
25
+ if (props[k] !== undefined) patch[k] = props[k];
26
+ }
27
+ return patch;
28
+ }
29
+
30
+ /**
31
+ * React 封装:props 与 `createGlobe` 一致,另可传 `onReady(instance)`、`onError(err)`。
32
+ * ref:`setState`、`resize`、`destroy`、`getGlobe`
33
+ */
34
+ export const RoxyCobewebgl = forwardRef(function RoxyCobewebgl(props, ref) {
35
+ const {
36
+ onReady,
37
+ onError,
38
+ preferWorker = true,
39
+ arcs = [],
40
+ debugShowTexture = false,
41
+ className,
42
+ style,
43
+ ...globeRest
44
+ } = props;
45
+
46
+ const fullProps = {
47
+ preferWorker,
48
+ arcs,
49
+ debugShowTexture,
50
+ ...globeRest,
51
+ };
52
+
53
+ const onErrorRef = useRef(onError);
54
+ const onReadyRef = useRef(onReady);
55
+ onErrorRef.current = onError;
56
+ onReadyRef.current = onReady;
57
+
58
+ const canvasRef = useRef(null);
59
+ const globeRef = useRef(null);
60
+ const readyRef = useRef(false);
61
+ const mountSeqRef = useRef(0);
62
+
63
+ useImperativeHandle(ref, () => ({
64
+ setState: (partial) => globeRef.current?.setState(partial),
65
+ resize: () => globeRef.current?.resize(),
66
+ destroy: () => {
67
+ globeRef.current?.destroy();
68
+ globeRef.current = null;
69
+ readyRef.current = false;
70
+ },
71
+ getGlobe: () => globeRef.current,
72
+ }), []);
73
+
74
+ useEffect(() => {
75
+ const canvas = canvasRef.current;
76
+ if (!canvas) return undefined;
77
+
78
+ const id = ++mountSeqRef.current;
79
+ readyRef.current = false;
80
+ globeRef.current?.destroy();
81
+ globeRef.current = null;
82
+
83
+ let cancelled = false;
84
+
85
+ (async () => {
86
+ const instance = await createGlobe(canvas, {
87
+ ...pickGlobeInitProps(fullProps),
88
+ onError: (e) => onErrorRef.current?.(e),
89
+ });
90
+ if (cancelled || id !== mountSeqRef.current) {
91
+ instance?.destroy();
92
+ return;
93
+ }
94
+ globeRef.current = instance;
95
+ readyRef.current = true;
96
+ onReadyRef.current?.(instance);
97
+ })();
98
+
99
+ return () => {
100
+ cancelled = true;
101
+ mountSeqRef.current += 1;
102
+ readyRef.current = false;
103
+ globeRef.current?.destroy();
104
+ globeRef.current = null;
105
+ };
106
+ }, [
107
+ fullProps.map,
108
+ fullProps.mapUrl,
109
+ fullProps.arcs,
110
+ fullProps.preferWorker,
111
+ fullProps.debugShowTexture,
112
+ ]);
113
+
114
+ useEffect(() => {
115
+ if (!globeRef.current || !readyRef.current) return;
116
+ const patch = pickSetStatePatch(fullProps);
117
+ if (Object.keys(patch).length) globeRef.current.setState(patch);
118
+ }, [
119
+ fullProps.phi,
120
+ fullProps.theta,
121
+ fullProps.baseColor,
122
+ fullProps.glowColor,
123
+ fullProps.dotColor,
124
+ fullProps.arcColor,
125
+ fullProps.dots,
126
+ fullProps.dotSize,
127
+ fullProps.globeRadius,
128
+ fullProps.glowOn,
129
+ fullProps.debug,
130
+ fullProps.opacity,
131
+ fullProps.autoRotate,
132
+ fullProps.rotationSpeed,
133
+ ]);
134
+
135
+ const canvasClass =
136
+ className != null && className !== ''
137
+ ? `roxy-cobewebgl-canvas ${className}`
138
+ : 'roxy-cobewebgl-canvas';
139
+
140
+ return (
141
+ <canvas
142
+ ref={canvasRef}
143
+ className={canvasClass}
144
+ style={{
145
+ display: 'block',
146
+ width: '100%',
147
+ height: '100%',
148
+ verticalAlign: 'middle',
149
+ ...style,
150
+ }}
151
+ />
152
+ );
153
+ });
154
+
155
+ RoxyCobewebgl.displayName = 'RoxyCobewebgl';
156
+
157
+ export default RoxyCobewebgl;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * React 子路径:import RoxyCobewebgl from 'roxy-cobewebgl/react'
3
+ */
4
+ export { RoxyCobewebgl, default } from './react/RoxyCobewebgl.jsx';
@@ -0,0 +1,151 @@
1
+ <template>
2
+ <canvas
3
+ ref="canvasRef"
4
+ class="roxy-cobewebgl-canvas"
5
+ />
6
+ </template>
7
+
8
+ <script setup>
9
+ import { ref, watch, onMounted, onBeforeUnmount, shallowRef } from 'vue';
10
+ import { createGlobe } from '../index.js';
11
+
12
+ const SET_STATE_KEYS = [
13
+ 'phi', 'theta', 'baseColor', 'glowColor', 'dotColor', 'arcColor',
14
+ 'dots', 'dotSize', 'globeRadius', 'glowOn', 'debug', 'opacity',
15
+ 'autoRotate', 'rotationSpeed',
16
+ ];
17
+
18
+ const RECREATE_KEYS = ['map', 'mapUrl', 'arcs', 'preferWorker', 'debugShowTexture'];
19
+
20
+ const props = defineProps({
21
+ map: { type: Object, default: undefined },
22
+ mapUrl: { type: [String, Object], default: undefined },
23
+ preferWorker: { type: Boolean, default: true },
24
+ arcs: { type: Array, default: () => [] },
25
+ debugShowTexture: { type: Boolean, default: false },
26
+ phi: { type: Number, default: undefined },
27
+ theta: { type: Number, default: undefined },
28
+ baseColor: { type: Array, default: undefined },
29
+ glowColor: { type: Array, default: undefined },
30
+ dotColor: { type: Array, default: undefined },
31
+ arcColor: { type: Array, default: undefined },
32
+ dots: { type: Number, default: undefined },
33
+ dotSize: { type: Number, default: undefined },
34
+ globeRadius: { type: Number, default: undefined },
35
+ glowOn: { type: Number, default: undefined },
36
+ debug: { type: Number, default: undefined },
37
+ opacity: { type: Number, default: undefined },
38
+ autoRotate: { type: Boolean, default: undefined },
39
+ rotationSpeed: { type: Number, default: undefined },
40
+ });
41
+
42
+ const emit = defineEmits(['error', 'ready']);
43
+
44
+ const canvasRef = ref(null);
45
+ const globeRef = shallowRef(null);
46
+ const ready = ref(false);
47
+ let mountSeq = 0;
48
+
49
+ function toCreateOptions() {
50
+ const o = {
51
+ map: props.map,
52
+ mapUrl: props.mapUrl,
53
+ preferWorker: props.preferWorker,
54
+ arcs: props.arcs,
55
+ debugShowTexture: props.debugShowTexture,
56
+ onError: (e) => emit('error', e),
57
+ phi: props.phi,
58
+ theta: props.theta,
59
+ baseColor: props.baseColor,
60
+ glowColor: props.glowColor,
61
+ dotColor: props.dotColor,
62
+ arcColor: props.arcColor,
63
+ dots: props.dots,
64
+ dotSize: props.dotSize,
65
+ globeRadius: props.globeRadius,
66
+ glowOn: props.glowOn,
67
+ debug: props.debug,
68
+ opacity: props.opacity,
69
+ autoRotate: props.autoRotate,
70
+ rotationSpeed: props.rotationSpeed,
71
+ };
72
+ return Object.fromEntries(
73
+ Object.entries(o).filter(([, v]) => v !== undefined)
74
+ );
75
+ }
76
+
77
+ function pickSetStatePatch() {
78
+ const patch = {};
79
+ for (const k of SET_STATE_KEYS) {
80
+ if (props[k] !== undefined) patch[k] = props[k];
81
+ }
82
+ return patch;
83
+ }
84
+
85
+ async function mountGlobe() {
86
+ const canvas = canvasRef.value;
87
+ if (!canvas) return null;
88
+ const id = ++mountSeq;
89
+ ready.value = false;
90
+ globeRef.value?.destroy();
91
+ globeRef.value = null;
92
+ const instance = await createGlobe(canvas, toCreateOptions());
93
+ if (id !== mountSeq) {
94
+ instance?.destroy();
95
+ return null;
96
+ }
97
+ globeRef.value = instance;
98
+ ready.value = true;
99
+ emit('ready', instance);
100
+ return instance;
101
+ }
102
+
103
+ onMounted(() => {
104
+ mountGlobe();
105
+ });
106
+
107
+ onBeforeUnmount(() => {
108
+ mountSeq++;
109
+ ready.value = false;
110
+ globeRef.value?.destroy();
111
+ globeRef.value = null;
112
+ });
113
+
114
+ watch(
115
+ () => RECREATE_KEYS.map((k) => props[k]),
116
+ async () => {
117
+ if (!canvasRef.value) return;
118
+ await mountGlobe();
119
+ },
120
+ { deep: true }
121
+ );
122
+
123
+ watch(
124
+ () => pickSetStatePatch(),
125
+ (patch) => {
126
+ if (!globeRef.value || !ready.value) return;
127
+ if (Object.keys(patch).length) globeRef.value.setState(patch);
128
+ },
129
+ { deep: true }
130
+ );
131
+
132
+ defineExpose({
133
+ setState: (partial) => globeRef.value?.setState(partial),
134
+ resize: () => globeRef.value?.resize(),
135
+ destroy: () => {
136
+ globeRef.value?.destroy();
137
+ globeRef.value = null;
138
+ ready.value = false;
139
+ },
140
+ getGlobe: () => globeRef.value,
141
+ });
142
+ </script>
143
+
144
+ <style scoped>
145
+ .roxy-cobewebgl-canvas {
146
+ display: block;
147
+ width: 100%;
148
+ height: 100%;
149
+ vertical-align: middle;
150
+ }
151
+ </style>
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Vue 3 组件子路径:import RoxyCobewebgl from 'roxy-cobewebgl/vue'
3
+ */
4
+ export { default } from './vue/RoxyCobewebgl.vue';
5
+ export { default as RoxyCobewebgl } from './vue/RoxyCobewebgl.vue';
package/vue.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { DefineComponent } from 'vue';
2
+ import type { CreateGlobeOptions, GlobeInstance } from './index';
3
+
4
+ /** 与 CreateGlobeOptions 一致,无 onError(请用 @error 事件) */
5
+ export type RoxyCobewebglProps = Partial<Omit<CreateGlobeOptions, 'onError'>>;
6
+
7
+ export type RoxyCobewebglExpose = {
8
+ setState: GlobeInstance['setState'];
9
+ resize: GlobeInstance['resize'];
10
+ destroy: GlobeInstance['destroy'];
11
+ getGlobe: () => GlobeInstance | null;
12
+ };
13
+
14
+ declare const RoxyCobewebgl: DefineComponent<
15
+ RoxyCobewebglProps,
16
+ RoxyCobewebglExpose
17
+ >;
18
+
19
+ export { RoxyCobewebgl };
20
+ export default RoxyCobewebgl;