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