Pārlūkot izejas kodu

feat[krpano]: Layer and Action

chenlei 1 gadu atpakaļ
vecāks
revīzija
0eec792b73

+ 27 - 0
packages/krpano/src/components/Action.tsx

@@ -0,0 +1,27 @@
+import { FC, useContext, useEffect } from "react";
+import { KrpanoRendererContext } from "../contexts";
+
+export interface ActionProps {
+  name: string;
+  content: string;
+  [key: string]: unknown;
+}
+
+export const Action: FC<ActionProps> = ({ name, content, ...attrs }) => {
+  const renderer = useContext(KrpanoRendererContext);
+
+  useEffect(() => {
+    if (!renderer) return;
+
+    renderer.tagAction.pushSyncTag(
+      "action",
+      {
+        name,
+        ...attrs,
+      },
+      content
+    );
+  }, [renderer]);
+
+  return <></>;
+};

+ 12 - 8
packages/krpano/src/components/Events.tsx

@@ -1,4 +1,4 @@
-import { FC, memo, useContext, useEffect } from "react";
+import { FC, useContext, useEffect } from "react";
 import { KrpanoRendererContext } from "../contexts/KrpanoRendererContext";
 import { EventCallback } from "../types";
 import { mapEventPropsToJSCall } from "../utils";
@@ -9,6 +9,7 @@ import { mapEventPropsToJSCall } from "../utils";
 export interface EventsConfig {
   /** 事件名,若存在该参数则为局部事件 */
   name?: string;
+  keep?: boolean;
   onEnterFullscreen?: EventCallback;
   onExitFullscreen?: EventCallback;
   onXmlComplete?: EventCallback;
@@ -57,7 +58,7 @@ export interface EventsProps extends EventsConfig {}
 
 const GlobalEvents = "__GlobalEvents";
 
-export const Events: FC<EventsProps> = memo(({ name, ...EventsAttrs }) => {
+export const Events: FC<EventsProps> = ({ name, keep, ...EventsAttrs }) => {
   const renderer = useContext(KrpanoRendererContext);
   const EventSelector = `events[${name || GlobalEvents}]`;
 
@@ -76,13 +77,16 @@ export const Events: FC<EventsProps> = memo(({ name, ...EventsAttrs }) => {
       "events",
       // 全局事件直接设置
       name || null,
-      mapEventPropsToJSCall(
-        { ...EventsAttrs },
-        (eventName) =>
-          `js(${renderer.name}.fire(${eventName},${EventSelector}))`
-      )
+      {
+        ...mapEventPropsToJSCall(
+          { ...EventsAttrs },
+          (eventName) =>
+            `js(${renderer.name}.fire(${eventName},${EventSelector}))`
+        ),
+        keep,
+      }
     );
   }, [name, renderer]);
 
   return <div className="events"></div>;
-});
+};

+ 108 - 25
packages/krpano/src/components/Krpano.tsx

@@ -1,10 +1,13 @@
-import React, { useCallback, useEffect, useState } from "react";
+import React, { useCallback, useEffect, useRef, useState } from "react";
 import { KrpanoActionProxy } from "../models";
 import { useMounted, useEventCallback } from "../hooks";
 import { IKrpanoConfig, NativeKrpanoRendererObject } from "../types";
 import { CurrentSceneContext, KrpanoRendererContext } from "../contexts";
 import { buildKrpanoAction } from "../utils";
 import { WebVR } from "./WebVR";
+import { Action } from "./Action";
+import { Layer } from "./Layer";
+import { Events } from "./Events";
 
 export interface KrpanoProps extends Omit<IKrpanoConfig, "onready" | "target"> {
   className?: string;
@@ -17,6 +20,10 @@ export interface KrpanoProps extends Omit<IKrpanoConfig, "onready" | "target"> {
    */
   webvrUrl?: string;
   webvrConfig?: Record<string, unknown>;
+  /**
+   * 小行星视角
+   */
+  littlePlanetIntro?: boolean;
   onReady?: (renderer: KrpanoActionProxy) => void;
 }
 
@@ -28,51 +35,60 @@ export const Krpano: React.FC<KrpanoProps> = ({
   target = "krpano",
   webvrUrl,
   webvrConfig,
+  littlePlanetIntro,
   onReady,
   ...rest
 }) => {
+  const loaded = useRef(false);
   const [renderer, setRenderer] = useState<KrpanoActionProxy | null>(null);
   const onReadyRef = useEventCallback(onReady);
   const onReadyCallback = useCallback(
-    (obj: NativeKrpanoRendererObject) => {
-      const renderer = new KrpanoActionProxy(obj);
-      (window as any)[renderer.name] = renderer;
-      setRenderer(renderer);
+    async (obj: NativeKrpanoRendererObject) => {
+      const krpano = new KrpanoActionProxy(obj);
+
+      (window as any)[krpano.name] = krpano;
+      setRenderer(krpano);
 
       if (onReadyRef.current) {
-        onReadyRef.current(renderer);
+        onReadyRef.current(krpano);
       }
     },
     [onReadyRef]
   );
 
   useEffect(() => {
-    if (!renderer) return;
+    if (!renderer || !currentScene) return;
 
-    const reloadXML = async () => {
-      if (renderer.tagAction.syncTagStack.length) {
-        // krpano 1.21 版本以下不支持动态插入 include,只能在文本中插入后重新加载
-        const updateXmlString = new XMLSerializer().serializeToString(
-          await renderer.tagAction.createSyncTags()
-        );
+    renderer.tagAction.waitIncludeLoaded(true).then(() => {
+      renderer.loadScene(currentScene);
 
-        renderer.call(buildKrpanoAction("loadxml", updateXmlString));
-      }
+      littlePlanetIntro &&
+        !loaded.current &&
+        renderer.call("skin_setup_littleplanetintro()");
 
-      renderer.tagAction.syncTagsLoaded = true;
-      renderer.tagAction.queue.flushResolve(true);
-    };
+      loaded.current = true;
+    });
+  }, [renderer, currentScene]);
+
+  useEffect(() => {
+    if (!renderer) return;
 
-    reloadXML();
+    reloadXML(renderer);
   }, [renderer]);
 
-  useEffect(() => {
-    if (!renderer || !currentScene) return;
+  const reloadXML = async (krpano: KrpanoActionProxy) => {
+    if (krpano.tagAction.syncTagStack.length) {
+      // krpano 1.21 版本以下不支持动态插入 include,只能在文本中插入后重新加载
+      const updateXmlString = new XMLSerializer().serializeToString(
+        await krpano.tagAction.createSyncTags()
+      );
 
-    renderer.tagAction.waitIncludeLoaded(true).then(() => {
-      renderer.loadScene(currentScene);
-    });
-  }, [renderer, currentScene]);
+      krpano.call(buildKrpanoAction("loadxml", updateXmlString));
+    }
+
+    krpano.tagAction.syncTagsLoaded = true;
+    krpano.tagAction.queue.flushResolve(true);
+  };
 
   const initKrpano = () => {
     const defaultConfig: Partial<IKrpanoConfig> = {
@@ -93,6 +109,20 @@ export const Krpano: React.FC<KrpanoProps> = ({
     }
   };
 
+  const handleNewPano = () => {
+    renderer?.set("layer[skin_loadingtext].visible", true);
+  };
+
+  const handleRemovePano = () => {
+    renderer?.set("layer[skin_loadingtext].visible", true);
+  };
+
+  const handleLoadComplete = () => {
+    setTimeout(() => {
+      renderer?.set("layer[skin_loadingtext].visible", false);
+    }, 200);
+  };
+
   useMounted(() => {
     initKrpano();
   });
@@ -105,6 +135,59 @@ export const Krpano: React.FC<KrpanoProps> = ({
         <div id={target} className={className} style={style}>
           {renderer ? children : null}
         </div>
+
+        <Events
+          onNewPano={handleNewPano}
+          onRemovePano={handleRemovePano}
+          onLoadComplete={handleLoadComplete}
+        />
+
+        <Layer
+          name="skin_loadingtext"
+          type="text"
+          align="center"
+          x={5}
+          y={-5}
+          keep={true}
+          html="加载中..."
+          visible={false}
+          background={false}
+          border={false}
+          enabled={false}
+          css="color:#FFFFFF; font-family:Arial; text-align:center; font-style:italic; font-size:22px;"
+        />
+
+        <Action
+          name="skin_setup_littleplanetintro"
+          content={`
+            copy(lp_scene, xml.scene);
+            copy(lp_hlookat, view.hlookat);
+            copy(lp_vlookat, view.vlookat);
+            copy(lp_fov, view.fov);
+            copy(lp_fovmax, view.fovmax);
+            copy(lp_limitview, view.limitview);
+            set(view.fovmax, 170);
+            set(view.limitview, lookto);
+            set(view.vlookatmin, 90);
+            set(view.vlookatmax, 90);
+            lookat(calc(lp_hlookat - 180), 90, 150, 1, 0, 0);
+            set(events[lp_events].onloadcomplete,
+              delayedcall(0.5,
+                if(lp_scene === xml.scene,
+                  set(control.usercontrol, off);
+                  copy(view.limitview, lp_limitview);
+                  set(view.vlookatmin, null);
+                  set(view.vlookatmax, null);
+                  tween(view.hlookat|view.vlookat|view.fov|view.distortion, calc('' + lp_hlookat + '|' + lp_vlookat + '|' + lp_fov + '|' + 0.0),
+                    3.0, easeOutQuad,
+                    set(control.usercontrol, all);
+                    tween(view.fovmax, get(lp_fovmax));
+                    );
+                  );
+                );
+              );
+          `}
+        />
       </CurrentSceneContext.Provider>
     </KrpanoRendererContext.Provider>
   );

+ 46 - 0
packages/krpano/src/components/Layer.tsx

@@ -0,0 +1,46 @@
+import { FC, memo, useContext, useEffect } from "react";
+import { KrpanoRendererContext } from "../contexts";
+
+/**
+ * @see https://krpano.com/docu/xml/#layer.html
+ */
+export interface LayerProps {
+  name: string;
+  keep?: boolean;
+  type?: "image" | "text";
+  align?:
+    | "lefttop"
+    | "left"
+    | "leftbottom"
+    | "top"
+    | "center"
+    | "bottom"
+    | "righttop"
+    | "right";
+  x?: number;
+  y?: number;
+  html?: string;
+  visible?: boolean;
+  background?: boolean;
+  border?: boolean;
+  enabled?: boolean;
+  css?: string;
+}
+
+export const Layer: FC<LayerProps> = memo(({ name, ...rest }) => {
+  const renderer = useContext(KrpanoRendererContext);
+
+  useEffect(() => {
+    renderer?.addLayer(name, {});
+
+    return () => {
+      renderer?.removeLayer(name);
+    };
+  }, []);
+
+  useEffect(() => {
+    renderer?.setTag("layer", name, { ...rest });
+  }, [renderer, name, rest]);
+
+  return <div className="layer" />;
+});

+ 7 - 3
packages/krpano/src/components/WebVR/index.tsx

@@ -7,12 +7,12 @@ export interface WebVRProps {
   [key: string]: unknown;
 }
 
-const WEBVR_121_CONFIG = {
+const DEFAULT_WEBVR_121_CONFIG = {
   keep: true,
   devices: "webgl",
 };
 
-const WEBVR_119_CONFIG = {
+const DEFAULT_WEBVR_119_CONFIG = {
   name: "WebVR",
   keep: true,
   devices: "html5",
@@ -29,7 +29,11 @@ export const WebVR: FC<WebVRProps> = memo(({ url, ...attrs }) => {
       <Plugin
         name="WebVR"
         {...Object.assign(
-          { ...(is121Version ? WEBVR_121_CONFIG : WEBVR_119_CONFIG) },
+          {
+            ...(is121Version
+              ? DEFAULT_WEBVR_121_CONFIG
+              : DEFAULT_WEBVR_119_CONFIG),
+          },
           attrs
         )}
       />

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

@@ -1,3 +1,4 @@
+export * from "./Action";
 export * from "./Krpano";
 export * from "./Scene";
 export * from "./View";
@@ -6,3 +7,4 @@ export * from "./Autorotate";
 export * from "./Events";
 export * from "./Include";
 export * from "./Plugin";
+export * from "./Layer";

+ 13 - 4
packages/krpano/src/models/KrpanoActionProxy.ts

@@ -58,12 +58,11 @@ export class KrpanoActionProxy {
   ) {
     let nexttick = false;
 
-    if (tag === "hotspot" || tag === "events") {
+    if (["events", "hotspot", "layer"].includes(tag)) {
       nexttick = true;
     }
 
     await this.tagAction.waitIncludeLoaded();
-
     this.call(
       buildKrpanoTagSetterActions(name ? `${tag}[${name}]` : tag, attrs),
       nexttick
@@ -84,8 +83,6 @@ export class KrpanoActionProxy {
       typeof this.get("scene").removeItem === "function"
     ) {
       this.get("scene").removeItem(name);
-    } else {
-      // TODO: report Error
     }
   }
 
@@ -212,4 +209,16 @@ export class KrpanoActionProxy {
   removeHotspot(name: string): void {
     this.call(buildKrpanoAction("removehotspot", name), true);
   }
+
+  async addLayer(
+    name: string,
+    attrs: Record<string, string | boolean | number | undefined>
+  ) {
+    await this.tagAction.waitIncludeLoaded();
+    this.call(buildKrpanoAction("addlayer", name), true);
+    this.setTag("layer", name, attrs);
+  }
+  removeLayer(name: string): void {
+    this.call(buildKrpanoAction("removelayer", name), true);
+  }
 }

+ 20 - 2
packages/krpano/src/models/TagActionProxy.ts

@@ -9,10 +9,13 @@ export class TagActionProxy {
    * 同步标签是否加载完成
    */
   syncTagsLoaded = false;
+
   syncTagStack: {
     tagName: string;
     attribute: Record<string, unknown>;
+    children?: string;
   }[] = [];
+  syncXMLStringStack: string[] = [];
 
   constructor(krpanoRenderer?: NativeKrpanoRendererObject) {
     this.krpanoRenderer = krpanoRenderer;
@@ -31,10 +34,15 @@ export class TagActionProxy {
   /**
    * 将异步标签推入堆中
    */
-  pushSyncTag(tagName: string, attribute: Record<string, unknown>) {
+  pushSyncTag(
+    tagName: string,
+    attribute: Record<string, unknown>,
+    children?: string
+  ) {
     this.syncTagStack.unshift({
       tagName,
       attribute,
+      children,
     });
   }
 
@@ -46,8 +54,18 @@ export class TagActionProxy {
     const krpanoElement = xmlDoc.querySelector("krpano");
 
     while (this.syncTagStack.length) {
+      let element: HTMLElement | null = null;
       const tag = this.syncTagStack.pop()!;
-      const element = xmlDoc.createElement(tag.tagName);
+
+      if (!tag.children) {
+        element = xmlDoc.createElement(tag.tagName);
+      } else {
+        const parser = new DOMParser();
+        element = parser.parseFromString(
+          `<${tag.tagName}>${tag.children}</${tag.tagName}>`,
+          "text/xml"
+        ).documentElement;
+      }
 
       for (const key in tag.attribute) {
         element.setAttribute(key, tag.attribute[key] as string);

+ 3 - 0
packages/krpano/src/utils.tsx

@@ -17,6 +17,9 @@ type FuncName =
   | "addhotspot"
   | "removehotspot"
   | "includexml"
+  | "includexmlstring"
+  | "addlayer"
+  | "removelayer"
   | "nexttick";
 
 /**