From 2332932e039859d598dce3a860f6d012e220b703 Mon Sep 17 00:00:00 2001
From: Greg Fodor <gfodor@gmail.com>
Date: Wed, 14 Nov 2018 03:04:17 +0000
Subject: [PATCH] WIP

---
 src/components/follow-entity.js      |  42 ++++++++++
 src/hub.js                           |  14 +++-
 src/react-components/chat-message.js | 118 +++++++++++++++++++++------
 src/react-components/ui-root.js      |   4 +-
 4 files changed, 148 insertions(+), 30 deletions(-)
 create mode 100644 src/components/follow-entity.js

diff --git a/src/components/follow-entity.js b/src/components/follow-entity.js
new file mode 100644
index 000000000..6eccb76d2
--- /dev/null
+++ b/src/components/follow-entity.js
@@ -0,0 +1,42 @@
+AFRAME.registerComponent("follow-entity", {
+  schema: {
+    target: { type: "selector" },
+    offset: { type: "vec3" },
+    fixedY: { type: "number" }
+  },
+
+  init() {
+    this.targetPos = new THREE.Vector3();
+    this.offset = new THREE.Vector3();
+    this.offset.copy(this.data.offset);
+  },
+
+  tick(t, dt) {
+    const obj = this.el.object3D;
+    const target = this.data.target.object3D;
+
+    this.targetPos.copy(this.offset);
+    target.localToWorld(this.targetPos);
+
+    if (obj.parent) {
+      obj.parent.worldToLocal(this.targetPos);
+    }
+
+    if (this.data.fixedY) {
+      this.targetPos.y = this.data.fixedY;
+    }
+
+    if (!this.started) {
+      obj.position.copy(this.targetPos);
+      this.started = true;
+    } else {
+      obj.position.set(
+        obj.position.x + (this.targetPos.x - obj.position.x) * 0.0005 * dt,
+        obj.position.y + (this.targetPos.y - obj.position.y) * 0.0005 * dt,
+        obj.position.z + (this.targetPos.z - obj.position.z) * 0.0005 * dt
+      );
+    }
+
+    target.getWorldQuaternion(obj.quaternion);
+  }
+});
diff --git a/src/hub.js b/src/hub.js
index b52c11955..90d743472 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -68,6 +68,7 @@ import "./components/emit-state-change";
 import "./components/action-to-event";
 import "./components/stop-event-propagation";
 import "./components/animation";
+import "./components/follow-entity";
 
 import ReactDOM from "react-dom";
 import React from "react";
@@ -80,7 +81,7 @@ import { proxiedUrlFor } from "./utils/media-utils";
 import MessageDispatch from "./message-dispatch";
 import SceneEntryManager from "./scene-entry-manager";
 import Subscriptions from "./subscriptions";
-import { spawnChatMessage } from "./react-components/chat-message";
+import { createInWorldChatMessage } from "./react-components/chat-message";
 
 import "./systems/nav";
 import "./systems/personal-space-bubble";
@@ -562,8 +563,13 @@ document.addEventListener("DOMContentLoaded", async () => {
   linkChannel.setSocket(socket);
 
   setInterval(() => {
-    spawnChatMessage(
-      "```Hello asdjf sdkfadsfadsfadsfadf\nalkdsf dsflsafadsfadsfadfsj\n asdf dsf adf adf sdkfadsfafd\n lasdfadsfadsfldsfldsfja\n asdkf sdfjadsadsfadsf\n aldf sdjf sdkfasdf ```"
-    );
+    createInWorldChatMessage("hello :heart:", AFRAME.utils.device.isMobile());
+
+    setTimeout(() => {
+      createInWorldChatMessage(
+        "This is some long text. Will it wrapn? I don't know but I sure hope so. That would be neat.",
+        AFRAME.utils.device.isMobile()
+      );
+    }, 2000);
   }, 5000);
 });
diff --git a/src/react-components/chat-message.js b/src/react-components/chat-message.js
index 613118004..1ff291fd6 100644
--- a/src/react-components/chat-message.js
+++ b/src/react-components/chat-message.js
@@ -12,6 +12,34 @@ const messageCanvas = document.createElement("canvas");
 const emojiRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c[\ude32-\ude3a]|[\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/;
 const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)$/;
 
+// Hacky word wrapping, needed because the SVG conversion doesn't properly deal
+// with wrapping in Chrome for some reason. (The CSS white-space is set to pre)
+const wordWrap = body => {
+  const maxCharsPerLine = 40;
+  const words = body.split(" ");
+  const outWords = [];
+  let c = 0;
+
+  for (let i = 0; i < words.length; i++) {
+    const word = words[i];
+
+    if (word.startsWith(":") && word.endsWith(":")) {
+      c++;
+    } else {
+      c += word.length;
+    }
+
+    outWords.push(word);
+
+    if (c >= maxCharsPerLine) {
+      c = 0;
+      outWords.push("\n");
+    }
+  }
+
+  return outWords.join(" ");
+};
+
 const messageBodyDom = body => {
   // Support wrapping text in ` to get monospace, and multiline.
   const multiLine = body.split("\n").length > 1;
@@ -22,6 +50,10 @@ const messageBodyDom = body => {
     [styles.messageBodyMono]: mono
   };
 
+  if (!multiLine) {
+    body = wordWrap(body);
+  }
+
   const cleanedBody = (mono ? body.substring(1, body.length - 1) : body).trim();
 
   return (
@@ -31,14 +63,7 @@ const messageBodyDom = body => {
   );
 };
 
-export function renderChatMessage(body) {
-  if (body.length === 0) return;
-
-  if (body.match(urlRegex)) {
-    document.querySelector("a-scene").emit("add_media", body);
-    return;
-  }
-
+export function renderChatMessage(body, lowResolution) {
   const isOneLine = body.split("\n").length === 1;
   const context = messageCanvas.getContext("2d");
   const emoji = toEmojis(body);
@@ -68,14 +93,20 @@ export function renderChatMessage(body) {
   return new Promise(resolve => {
     ReactDOM.render(entryDom, el, () => {
       // Scale by 12x
-      //messageCanvas.width = el.offsetWidth * 12.1;
-      //messageCanvas.height = el.offsetHeight * 12.1;
-      messageCanvas.width = el.offsetWidth * 4.1;
-      messageCanvas.height = el.offsetHeight * 4.1;
+      let objectScale = "8.33%";
+      let scale = 12;
+
+      if (lowResolution) {
+        objectScale = "25%";
+        scale = 4;
+      }
+
+      messageCanvas.width = el.offsetWidth * (scale + 0.1);
+      messageCanvas.height = el.offsetHeight * (scale + 0.1);
 
       const xhtml = encodeURIComponent(`
           <svg xmlns="http://www.w3.org/2000/svg" width="${messageCanvas.width}" height="${messageCanvas.height}">
-            <foreignObject width="25%" height="25%" style="transform: scale(4.0);">
+            <foreignObject width="${objectScale}" height="${objectScale}" style="transform: scale(${scale});">
               ${serializeElement(el)}
             </foreignObject>
           </svg>
@@ -94,38 +125,77 @@ export function renderChatMessage(body) {
   });
 }
 
-export async function spawnChatMessage(body) {
-  const blob = await renderChatMessage(body);
-  //document.querySelector("a-scene").emit("add_media", new File([blob], "message.png", { type: "image/png" }));
-  //
+export async function createInWorldChatMessage(body, lowResolution) {
+  const blob = await renderChatMessage(body, lowResolution);
   const entity = document.createElement("a-entity");
-  const offset = { x: 0, y: 0, z: -1.5 };
+  const meshEntity = document.createElement("a-entity");
+
+  document.querySelector("a-scene").appendChild(entity);
 
-  entity.setAttribute("offset-relative-to", {
+  entity.appendChild(meshEntity);
+  entity.setAttribute("follow-entity", {
     target: "#player-camera",
-    offset
+    offset: { x: 0, y: 0, z: -1 },
+    fixedY: 0.7
   });
 
-  document.querySelector("a-scene").appendChild(entity);
-
   await nextTick();
 
   const textureLoader = new THREE.TextureLoader();
   const blobUrl = URL.createObjectURL(blob);
+  meshEntity.object3D.position.set(0, 0, 0);
+  meshEntity.object3D.rotation.set(0, 0, 0);
+
+  meshEntity.setAttribute("animation__float", {
+    property: "position",
+    dur: 15000,
+    from: { x: 0, y: 0, z: 0 },
+    to: { x: 0, y: 0.4, z: -0.4 },
+    easing: "easeOutQuad"
+  });
+
+  meshEntity.setAttribute("animation__opacity", {
+    property: "meshMaterial.opacity",
+    isRawProperty: true,
+    delay: 3000,
+    dur: 13000,
+    from: 1.0,
+    to: 0.0,
+    easing: "easeInQuad"
+  });
+
+  meshEntity.addEventListener("animationcomplete__opacity", () => {
+    entity.parentNode.removeChild(entity);
+  });
 
   textureLoader.load(blobUrl, texture => {
     const material = new THREE.MeshBasicMaterial();
-    material.side = THREE.DoubleSide;
     material.transparent = true;
     material.map = texture;
+    material.generateMipmaps = false;
     material.needsUpdate = true;
 
     const geometry = new THREE.PlaneGeometry();
     const mesh = new THREE.Mesh(geometry, material);
-    entity.setObject3D("mesh", mesh);
+    meshEntity.setObject3D("mesh", mesh);
+    meshEntity.meshMaterial = material;
+    const scaleFactor = 4000.0 / (lowResolution ? 3.0 : 1.0);
+    meshEntity.object3DMap.mesh.scale.set(texture.image.width / scaleFactor, texture.image.height / scaleFactor, 1);
   });
 }
 
+export async function spawnChatMessage(body, lowResolution) {
+  if (body.length === 0) return;
+
+  if (body.match(urlRegex)) {
+    document.querySelector("a-scene").emit("add_media", body);
+    return;
+  }
+
+  const blob = await renderChatMessage(body, false);
+  document.querySelector("a-scene").emit("add_media", new File([blob], "message.png", { type: "image/png" }));
+}
+
 export default function ChatMessage(props) {
   const isOneLine = props.body.split("\n").length === 1;
 
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 07a85c470..bfd7ce45a 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -1123,7 +1123,7 @@ class UIRoot extends Component {
                       if (e.keyCode === 13 && !e.shiftKey) {
                         this.sendMessage(e);
                       } else if (e.keyCode === 13 && e.shiftKey && e.ctrlKey) {
-                        spawnChatMessage(e.target.value);
+                        spawnChatMessage(e.target.value, false);
                         this.setState({ pendingMessage: "" });
                       } else if (e.keyCode === 27) {
                         e.target.blur();
@@ -1141,7 +1141,7 @@ class UIRoot extends Component {
                       className={classNames([styles.messageEntrySpawn])}
                       onClick={() => {
                         if (this.state.pendingMessage.length > 0) {
-                          spawnChatMessage(this.state.pendingMessage);
+                          spawnChatMessage(this.state.pendingMessage, false);
                           this.setState({ pendingMessage: "" });
                         } else {
                           this.showCreateObjectDialog();
-- 
GitLab