소스 검색

feat[krpano]: hotspot

chenlei 1 년 전
부모
커밋
934503fa11

BIN
packages/krpano-cli/template/public/images/guide.png


+ 0 - 140
packages/krpano-cli/template/src/pages/Home/constants.tsx

@@ -1,140 +0,0 @@
-import { View } from "@dage/krpano";
-import { ISceneProps } from "@/types";
-
-const URL = "https://houseoss.4dkankan.com/project/leifeng-transfer";
-
-export const CENTER_SCENE_LIST: ISceneProps[] = [
-  {
-    name: "center1",
-    thumbUrl: URL + "/panos/center1.tiles/thumb.jpg",
-    previewUrl: URL + "/panos/center1.tiles/preview.jpg",
-    imageTagAttributes: {
-      type: "cube",
-      tileSize: 512,
-      multires: true,
-    },
-    images: [
-      {
-        tiledImageWidth: 2624,
-        tiledImageHeight: 2624,
-        url: URL + "/panos/center1.tiles/%s/l3/%v/l3_%s_%v_%h.jpg",
-      },
-      {
-        tiledImageWidth: 1280,
-        tiledImageHeight: 1280,
-        url: URL + "/panos/center1.tiles/%s/l2/%v/l2_%s_%v_%h.jpg",
-      },
-      {
-        tiledImageWidth: 640,
-        tiledImageHeight: 640,
-        url: URL + "/panos/center1.tiles/%s/l1/%v/l1_%s_%v_%h.jpg",
-      },
-    ],
-    children: (
-      <View
-        hlookat={0}
-        vlookat={0}
-        fovType="MFOV"
-        fov={120}
-        maxPixelZoom={2}
-        fovMin={70}
-        fovMax={140}
-        limitView="auto"
-      />
-    ),
-  },
-  {
-    name: "center2",
-    thumbUrl: URL + "/panos/center2.tiles/thumb.jpg",
-    previewUrl: URL + "/panos/center2.tiles/preview.jpg",
-    imageTagAttributes: {
-      type: "cube",
-      tileSize: 512,
-      multires: true,
-    },
-    images: [
-      {
-        tiledImageWidth: 2624,
-        tiledImageHeight: 2624,
-        url: URL + "/panos/center2.tiles/%s/l3/%v/l3_%s_%v_%h.jpg",
-      },
-      {
-        tiledImageWidth: 1280,
-        tiledImageHeight: 1280,
-        url: URL + "/panos/center2.tiles/%s/l2/%v/l2_%s_%v_%h.jpg",
-      },
-      {
-        tiledImageWidth: 640,
-        tiledImageHeight: 640,
-        url: URL + "/panos/center2.tiles/%s/l1/%v/l1_%s_%v_%h.jpg",
-      },
-    ],
-    children: (
-      <View
-        hlookat={0}
-        vlookat={0}
-        fovType="MFOV"
-        fov={120}
-        maxPixelZoom={2}
-        fovMin={70}
-        fovMax={140}
-        limitView="auto"
-      />
-    ),
-  },
-];
-
-export const SERVICE_SCENE_LIST: ISceneProps[] = [
-  {
-    name: "service1",
-    thumbUrl: URL + "/panos/service1.tiles/thumb.jpg",
-    previewUrl: URL + "/panos/service1.tiles/preview.jpg",
-    imageTagAttributes: {
-      type: "cube",
-      tileSize: 512,
-      multires: true,
-    },
-    images: [
-      {
-        tiledImageWidth: 2624,
-        tiledImageHeight: 2624,
-        url: URL + "/panos/service1.tiles/%s/l3/%v/l3_%s_%v_%h.jpg",
-      },
-      {
-        tiledImageWidth: 1280,
-        tiledImageHeight: 1280,
-        url: URL + "/panos/service1.tiles/%s/l2/%v/l2_%s_%v_%h.jpg",
-      },
-      {
-        tiledImageWidth: 640,
-        tiledImageHeight: 640,
-        url: URL + "/panos/service1.tiles/%s/l1/%v/l1_%s_%v_%h.jpg",
-      },
-    ],
-    children: (
-      <View
-        hlookat={0}
-        vlookat={0}
-        fovType="MFOV"
-        fov={120}
-        maxPixelZoom={2}
-        fovMin={70}
-        fovMax={140}
-        limitView="auto"
-      />
-    ),
-  },
-];
-
-export const MENUS = [
-  {
-    title: "长沙国防教育馆",
-    scenes: CENTER_SCENE_LIST,
-  },
-  {
-    title: "游客服务中心",
-    scenes: SERVICE_SCENE_LIST,
-  },
-];
-
-export const ALL_SCENES = [...CENTER_SCENE_LIST, ...SERVICE_SCENE_LIST];

+ 161 - 6
packages/krpano-cli/template/src/pages/Home/index.tsx

@@ -1,14 +1,169 @@
 import { useMemo, useState } from "react";
 import classnames from "classnames";
-import { Krpano, ROTATE_DIRECTION, Scene, ZOOM_ACTION } from "@dage/krpano";
-import { ALL_SCENES, MENUS } from "./constants";
+import {
+  HotSpot,
+  Krpano,
+  ROTATE_DIRECTION,
+  Scene,
+  View,
+  ZOOM_ACTION,
+} from "@dage/krpano";
 import { MouseHoldView } from "@/components";
+import { ISceneProps } from "@/types";
 import "./index.scss";
 
+const URL = "https://houseoss.4dkankan.com/project/leifeng-transfer";
+
 export default function HomePage() {
   const [currentMenu, setCurrentMenu] = useState(0);
-  const sceneList = useMemo(() => MENUS[currentMenu].scenes, [currentMenu]);
-  const [currentScene, setCurrentScene] = useState(sceneList[0].name);
+  const [currentScene, setCurrentScene] = useState("center1");
+  const CENTER_SCENE_LIST = useMemo<ISceneProps[]>(
+    () => [
+      {
+        name: "center1",
+        thumbUrl: URL + "/panos/center1.tiles/thumb.jpg",
+        previewUrl: URL + "/panos/center1.tiles/preview.jpg",
+        imageTagAttributes: {
+          type: "cube",
+          tileSize: 512,
+          multires: true,
+        },
+        images: [
+          {
+            tiledImageWidth: 2624,
+            tiledImageHeight: 2624,
+            url: URL + "/panos/center1.tiles/%s/l3/%v/l3_%s_%v_%h.jpg",
+          },
+          {
+            tiledImageWidth: 1280,
+            tiledImageHeight: 1280,
+            url: URL + "/panos/center1.tiles/%s/l2/%v/l2_%s_%v_%h.jpg",
+          },
+          {
+            tiledImageWidth: 640,
+            tiledImageHeight: 640,
+            url: URL + "/panos/center1.tiles/%s/l1/%v/l1_%s_%v_%h.jpg",
+          },
+        ],
+        children: (
+          <>
+            <View
+              hlookat={0}
+              vlookat={0}
+              fovType="MFOV"
+              fov={120}
+              maxPixelZoom={2}
+              fovMin={70}
+              fovMax={140}
+              limitView="auto"
+            />
+
+            <HotSpot
+              name="hotspot1"
+              url="/images/guide.png"
+              atv={5}
+              scale={0.5}
+              onClick={() => setCurrentScene("center2")}
+            />
+          </>
+        ),
+      },
+      {
+        name: "center2",
+        thumbUrl: URL + "/panos/center2.tiles/thumb.jpg",
+        previewUrl: URL + "/panos/center2.tiles/preview.jpg",
+        imageTagAttributes: {
+          type: "cube",
+          tileSize: 512,
+          multires: true,
+        },
+        images: [
+          {
+            tiledImageWidth: 2624,
+            tiledImageHeight: 2624,
+            url: URL + "/panos/center2.tiles/%s/l3/%v/l3_%s_%v_%h.jpg",
+          },
+          {
+            tiledImageWidth: 1280,
+            tiledImageHeight: 1280,
+            url: URL + "/panos/center2.tiles/%s/l2/%v/l2_%s_%v_%h.jpg",
+          },
+          {
+            tiledImageWidth: 640,
+            tiledImageHeight: 640,
+            url: URL + "/panos/center2.tiles/%s/l1/%v/l1_%s_%v_%h.jpg",
+          },
+        ],
+        children: (
+          <View
+            hlookat={0}
+            vlookat={0}
+            fovType="MFOV"
+            fov={120}
+            maxPixelZoom={2}
+            fovMin={70}
+            fovMax={140}
+            limitView="auto"
+          />
+        ),
+      },
+    ],
+    []
+  );
+  const SERVICE_SCENE_LIST = useMemo<ISceneProps[]>(
+    () => [
+      {
+        name: "service1",
+        thumbUrl: URL + "/panos/service1.tiles/thumb.jpg",
+        previewUrl: URL + "/panos/service1.tiles/preview.jpg",
+        imageTagAttributes: {
+          type: "cube",
+          tileSize: 512,
+          multires: true,
+        },
+        images: [
+          {
+            tiledImageWidth: 2624,
+            tiledImageHeight: 2624,
+            url: URL + "/panos/service1.tiles/%s/l3/%v/l3_%s_%v_%h.jpg",
+          },
+          {
+            tiledImageWidth: 1280,
+            tiledImageHeight: 1280,
+            url: URL + "/panos/service1.tiles/%s/l2/%v/l2_%s_%v_%h.jpg",
+          },
+          {
+            tiledImageWidth: 640,
+            tiledImageHeight: 640,
+            url: URL + "/panos/service1.tiles/%s/l1/%v/l1_%s_%v_%h.jpg",
+          },
+        ],
+        children: (
+          <View
+            hlookat={0}
+            vlookat={0}
+            fovType="MFOV"
+            fov={120}
+            maxPixelZoom={2}
+            fovMin={70}
+            fovMax={140}
+            limitView="auto"
+          />
+        ),
+      },
+    ],
+    []
+  );
+  const MENUS = [
+    {
+      title: "长沙国防教育馆",
+      scenes: CENTER_SCENE_LIST,
+    },
+    {
+      title: "游客服务中心",
+      scenes: SERVICE_SCENE_LIST,
+    },
+  ];
 
   const handleSceneClick = (name: string) => {
     setCurrentScene(name);
@@ -33,7 +188,7 @@ export default function HomePage() {
         currentScene={currentScene}
         passQueryParameters={true}
       >
-        {ALL_SCENES.map((sc) => (
+        {[...CENTER_SCENE_LIST, ...SERVICE_SCENE_LIST].map((sc) => (
           <Scene key={sc.name} {...sc} />
         ))}
       </Krpano>
@@ -55,7 +210,7 @@ export default function HomePage() {
         </div>
 
         <div className="scene-panel__list">
-          {sceneList.map((sc) => (
+          {MENUS[currentMenu].scenes.map((sc) => (
             <div
               key={sc.name}
               className={`scene-panel__list__item ${

+ 66 - 0
packages/krpano/src/KrpanoActionProxy.ts

@@ -125,4 +125,70 @@ export class KrpanoActionProxy {
       buildKrpanoAction("tween", "view.fov", (view.fov || 0) + targetFov, 1)
     );
   }
+
+  on(eventName: string, selector: string, handler: HandlerFunc): this {
+    this.eventHandlers.push({
+      eventName: eventName.toLowerCase(),
+      selector,
+      handler,
+    });
+    return this;
+  }
+
+  off(eventName: string, selector: string, handler: HandlerFunc): void {
+    this.eventHandlers = this.eventHandlers.filter(
+      (e) =>
+        !(
+          e.eventName === eventName.toLowerCase() &&
+          e.selector === selector &&
+          e.handler === handler
+        )
+    );
+  }
+
+  fire(eventName: string, selector: string): void {
+    this.eventHandlers
+      .filter(
+        (e) =>
+          e.eventName === eventName.toLowerCase() && e.selector === selector
+      )
+      .map(({ handler }) => handler(this));
+  }
+
+  bindEvents(
+    selector: string,
+    mapEventsToHandler: Record<string, HandlerFunc | undefined>
+  ): void {
+    Object.keys(mapEventsToHandler).map((eventName) => {
+      const func = mapEventsToHandler[eventName];
+
+      if (func) {
+        this.on(eventName, selector, func);
+      }
+    });
+  }
+
+  unbindEvents(
+    selector: string,
+    mapEventsToHandler: Record<string, HandlerFunc | undefined>
+  ): void {
+    Object.keys(mapEventsToHandler).map((eventName) => {
+      const func = mapEventsToHandler[eventName];
+
+      if (func) {
+        this.off(eventName, selector, func);
+      }
+    });
+  }
+
+  addHotspot(
+    name: string,
+    attrs: Record<string, string | boolean | number | undefined>
+  ): void {
+    this.call(buildKrpanoAction("addhotspot", name), true);
+    this.setTag("hotspot", name, attrs);
+  }
+  removeHotspot(name: string): void {
+    this.call(buildKrpanoAction("removehotspot", name), true);
+  }
 }

+ 93 - 0
packages/krpano/src/components/HotSpot.tsx

@@ -0,0 +1,93 @@
+import { FC, ReactNode, useContext, useEffect, useMemo } from "react";
+import { EventCallback } from "../types";
+import { KrpanoRendererContext } from "../contexts";
+import { buildKrpanoAction, mapEventPropsToJSCall, mapObject } from "../utils";
+
+export interface HotspotProps {
+  name: string;
+  children?: ReactNode;
+  url?: string;
+  type?: string;
+  keep?: boolean;
+  visible?: boolean;
+  enabled?: boolean;
+  handCursor?: boolean;
+  cursor?: string;
+  maskChildren?: boolean;
+  zOrder?: string;
+  style?: string;
+  ath?: number;
+  atv?: number;
+  edge?: string;
+  zoom?: boolean;
+  distorted?: boolean;
+  rx?: number;
+  ry?: number;
+  rz?: number;
+  width?: string;
+  height?: string;
+  /**
+   * 比例
+   * @default 0.5
+   */
+  scale?: number;
+  rotate?: number;
+  alpha?: number;
+  onOver?: EventCallback;
+  onHover?: EventCallback;
+  onOut?: EventCallback;
+  onDown?: EventCallback;
+  onUp?: EventCallback;
+  onClick?: EventCallback;
+  onLoaded?: EventCallback;
+}
+
+export const HotSpot: FC<HotspotProps> = ({ name, children, ...rest }) => {
+  const EventSelector = `hotspot[${name}]`;
+  const renderer = useContext(KrpanoRendererContext);
+  const options = useMemo(() => {
+    const { scale = 0.5, ...r } = rest;
+
+    return {
+      scale,
+      onOver: buildKrpanoAction("tween", "scale", scale + 0.05),
+      onOut: buildKrpanoAction("tween", "scale", scale),
+      ...r,
+    };
+  }, [rest]);
+
+  useEffect(() => {
+    const eventsObj = mapObject({ ...options }, (key, value) => {
+      if (key.startsWith("on") && typeof value === "function") {
+        return {
+          [key]: value,
+        };
+      }
+      return {};
+    });
+    renderer?.bindEvents(EventSelector, eventsObj as any);
+
+    renderer?.addHotspot(name, {});
+
+    return () => {
+      renderer?.unbindEvents(EventSelector, eventsObj as any);
+      renderer?.removeHotspot(name);
+    };
+  }, []);
+
+  useEffect(() => {
+    renderer?.setTag(
+      "hotspot",
+      name,
+      Object.assign(
+        { ...options },
+        mapEventPropsToJSCall(
+          { ...options },
+          (key) => `js(${renderer?.name}.fire(${key},${EventSelector}))`
+        )
+      )
+    );
+  }, [renderer, name, options]);
+
+  return <div className="hotspot">{children}</div>;
+};

+ 1 - 0
packages/krpano/src/components/index.ts

@@ -1,3 +1,4 @@
 export * from "./Krpano";
 export * from "./Scene";
 export * from "./View";
+export * from "./HotSpot";

+ 30 - 0
packages/krpano/src/utils.ts

@@ -70,3 +70,33 @@ export const buildXML = ({ tag, attrs, children }: XMLMeta): string => {
   }
   return `<${tag} ${attributes} />`;
 };
+
+/**
+ * 对Object进行map操作
+ */
+export const mapObject = (
+  obj: Record<string, unknown>,
+  mapper: (key: string, value: unknown) => Record<string, unknown>
+): Record<string, unknown> => {
+  return Object.assign(
+    {},
+    ...Object.keys(obj).map((key) => {
+      const value = obj[key];
+      return mapper(key, value);
+    })
+  );
+};
+
+/**
+ * 主要用于绑定Krpano事件和js调用。提取某个对象中的onXXX属性并替换为对应的调用字符串,丢弃其他属性
+ */
+export const mapEventPropsToJSCall = (
+  obj: Record<string, unknown>,
+  getString: (key: string, value: unknown) => string
+): Record<string, string> =>
+  mapObject(obj, (key, value) => {
+    if (key.startsWith("on") && typeof value === "function") {
+      return { [key]: getString(key, value) };
+    }
+    return {};
+  }) as Record<string, string>;