From 5a4666ae49c7874f30a885b8d6687124d6b3fb24 Mon Sep 17 00:00:00 2001 From: cha0s Date: Sun, 14 Jul 2024 07:24:15 -0500 Subject: [PATCH] feat: chat and dialogue++ --- app/dialogue.js | 65 +++++++++++++ app/react-components/dom/chat.jsx | 91 ------------------- app/react-components/dom/chat/chat.jsx | 28 ++++++ app/react-components/dom/chat/chat.module.css | 15 +++ app/react-components/dom/chat/input.jsx | 84 +++++++++++++++++ .../input.module.css} | 12 +-- app/react-components/dom/chat/message.jsx | 33 +++++++ .../dom/chat/message.module.css | 4 + app/react-components/dom/chat/messages.jsx | 16 ++++ .../dom/chat/messages.module.css | 5 + app/react-components/dom/dialogue.jsx | 64 ++----------- app/react-components/dom/dialogue.module.css | 2 +- app/react-components/dom/entities.jsx | 8 +- app/react-components/ui.jsx | 21 ++++- app/root.css | 16 ++++ 15 files changed, 306 insertions(+), 158 deletions(-) delete mode 100644 app/react-components/dom/chat.jsx create mode 100644 app/react-components/dom/chat/chat.jsx create mode 100644 app/react-components/dom/chat/chat.module.css create mode 100644 app/react-components/dom/chat/input.jsx rename app/react-components/dom/{chat.module.css => chat/input.module.css} (71%) create mode 100644 app/react-components/dom/chat/message.jsx create mode 100644 app/react-components/dom/chat/message.module.css create mode 100644 app/react-components/dom/chat/messages.jsx create mode 100644 app/react-components/dom/chat/messages.module.css diff --git a/app/dialogue.js b/app/dialogue.js index b839ef7..ed40c4e 100644 --- a/app/dialogue.js +++ b/app/dialogue.js @@ -1,8 +1,11 @@ +import {createElement} from 'react'; import mdx from 'remark-mdx'; import parse from 'remark-parse'; import {unified} from 'unified'; import {visitParents as visit} from 'unist-util-visit-parents'; +import {TAU} from '@/util/math.js'; + const parser = unified().use(parse).use(mdx); function computeParams(ancestors) { @@ -54,3 +57,65 @@ export function parseLetters(source) { }); return letters; } + +export function render(letters, className) { + return (caret, radians) => ( + letters + .map(({character, indices, params}, i) => { + let color = 'inherit'; + let fade = 0; + let fontStyle = 'normal'; + let fontWeight = 'normal'; + let left = 0; + let opacity = 1; + let top = 0; + if (params.blink) { + const {frequency = 1} = params.blink; + opacity = (radians * (2 / frequency) % TAU) > (TAU / 2) ? opacity : 0; + } + if (params.fade) { + const {frequency = 1} = params.fade; + fade = frequency; + } + if (params.wave) { + const {frequency = 1, magnitude = 3} = params.wave; + top += magnitude * Math.cos((radians * (1 / frequency)) + TAU * indices.wave / params.wave.length); + } + if (params.rainbow) { + const {frequency = 1} = params.rainbow; + color = `hsl(${(radians * (1 / frequency)) + TAU * indices.rainbow / params.rainbow.length}rad 100 50)`; + } + if (params.shake) { + const {magnitude = 1} = params.shake; + left += (Math.random() * magnitude * 2) - magnitude; + top += (Math.random() * magnitude * 2) - magnitude; + } + if (params.em) { + fontStyle = 'italic'; + } + if (params.strong) { + fontWeight = 'bold'; + } + return ( + createElement( + 'span', + { + className, + key: i, + style: { + color, + fontStyle, + fontWeight, + left: `${left}px`, + opacity: i <= caret ? opacity : 0, + position: 'relative', + top: `${top}px`, + transition: `opacity ${fade}s`, + }, + }, + character, + ) + ); + }) + ); +} diff --git a/app/react-components/dom/chat.jsx b/app/react-components/dom/chat.jsx deleted file mode 100644 index 1ba950c..0000000 --- a/app/react-components/dom/chat.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import {useEffect, useState} from 'react'; - -import {useClient} from '@/context/client.js'; - -import styles from './chat.module.css'; - -export default function Chat({ - chatHistory, - onClose, - message, - setChatHistory, - setMessage, -}) { - const client = useClient(); - const [disabled, setDisabled] = useState(true); - const [historyCaret, setHistoryCaret] = useState(0); - useEffect(() => { - setDisabled(false); - }, []); - return ( -
-
{ - if (message) { - client.send({ - type: 'Action', - payload: {type: 'chat', value: message}, - }); - setChatHistory([message, ...chatHistory]); - setMessage(''); - onClose(); - } - event.preventDefault(); - }} - > - { - setMessage(event.target.value); - }} - onKeyDown={(event) => { - switch (event.key) { - case 'ArrowDown': { - if (0 === historyCaret) { - break; - } - let localHistoryCaret = historyCaret - 1; - setMessage(chatHistory[localHistoryCaret]) - setHistoryCaret(localHistoryCaret); - if (0 === localHistoryCaret) { - setChatHistory(chatHistory.slice(1)); - } - break; - } - case 'ArrowUp': { - if (historyCaret === chatHistory.length - 1) { - break; - } - let localHistoryCaret = historyCaret; - let localChatHistory = chatHistory; - if (0 === historyCaret) { - localChatHistory = [message, ...localChatHistory]; - setChatHistory(localChatHistory); - } - localHistoryCaret += 1; - setMessage(localChatHistory[localHistoryCaret]) - setHistoryCaret(localHistoryCaret); - break; - } - case 'Escape': { - onClose(); - break; - } - } - }} - onMouseDown={(event) => { - event.stopPropagation(); - }} - maxLength="255" - ref={(element) => { - if (element) { - element.focus(); - } - }} - type="text" - value={message} - /> -
-
- ); -} \ No newline at end of file diff --git a/app/react-components/dom/chat/chat.jsx b/app/react-components/dom/chat/chat.jsx new file mode 100644 index 0000000..b073be4 --- /dev/null +++ b/app/react-components/dom/chat/chat.jsx @@ -0,0 +1,28 @@ +import styles from './chat.module.css'; + +import Input from './input.jsx'; +import Messages from './messages.jsx'; + +export default function Chat({ + chatHistory, + chatMessages, + onClose, + message, + setChatHistory, + setMessage, +}) { + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/app/react-components/dom/chat/chat.module.css b/app/react-components/dom/chat/chat.module.css new file mode 100644 index 0000000..1f006cd --- /dev/null +++ b/app/react-components/dom/chat/chat.module.css @@ -0,0 +1,15 @@ +.chat { + background-color: #00000044; + display: flex; + flex-direction: column; + font-family: Cookbook, Georgia, 'Times New Roman', Times, serif; + position: absolute; + bottom: 0; + max-height: 25%; + width: 100%; +} + +@font-face { + font-family: "Cookbook"; + src: url("/assets/fonts/Cookbook.woff") format("woff"); +} diff --git a/app/react-components/dom/chat/input.jsx b/app/react-components/dom/chat/input.jsx new file mode 100644 index 0000000..bbd812a --- /dev/null +++ b/app/react-components/dom/chat/input.jsx @@ -0,0 +1,84 @@ +import {useEffect, useState} from 'react'; + +import {useClient} from '@/context/client.js'; + +import styles from './input.module.css'; + +export default function ChatInput({ + chatHistory, + message, + setChatHistory, + setMessage, +}) { + const client = useClient(); + const [disabled, setDisabled] = useState(true); + const [historyCaret, setHistoryCaret] = useState(0); + useEffect(() => { + setDisabled(false); + }, []); + return ( +
{ + if (message) { + client.send({ + type: 'Action', + payload: {type: 'chat', value: message}, + }); + setChatHistory([message, ...chatHistory]); + setMessage(''); + } + event.preventDefault(); + }} + > + { + setMessage(event.target.value); + }} + onKeyDown={(event) => { + switch (event.key) { + case 'ArrowDown': { + if (0 === historyCaret) { + break; + } + let localHistoryCaret = historyCaret - 1; + setMessage(chatHistory[localHistoryCaret]) + setHistoryCaret(localHistoryCaret); + if (0 === localHistoryCaret) { + setChatHistory(chatHistory.slice(1)); + } + break; + } + case 'ArrowUp': { + if (historyCaret === chatHistory.length - 1) { + break; + } + let localHistoryCaret = historyCaret; + let localChatHistory = chatHistory; + if (0 === historyCaret) { + localChatHistory = [message, ...localChatHistory]; + setChatHistory(localChatHistory); + } + localHistoryCaret += 1; + setMessage(localChatHistory[localHistoryCaret]) + setHistoryCaret(localHistoryCaret); + break; + } + } + }} + onMouseDown={(event) => { + event.stopPropagation(); + }} + maxLength="255" + ref={(element) => { + if (element) { + element.focus(); + } + }} + type="text" + value={message} + /> +
+ ); +} \ No newline at end of file diff --git a/app/react-components/dom/chat.module.css b/app/react-components/dom/chat/input.module.css similarity index 71% rename from app/react-components/dom/chat.module.css rename to app/react-components/dom/chat/input.module.css index 41151e7..342c9bb 100644 --- a/app/react-components/dom/chat.module.css +++ b/app/react-components/dom/chat/input.module.css @@ -1,17 +1,15 @@ -.chat { - background-color: #00000044; - position: absolute; - bottom: 0; - width: 100%; -} -.chat form { +.input { line-height: 0; input[type="text"] { background-color: #00000044; border: 1px solid #333333; color: #ffffff; + font-family: "Cookbook"; + font-size: 16px; margin: 4px; + padding: 0; + padding-left: 4px; width: calc(100% - 8px); &:focus-visible { border: 1px solid #999999; diff --git a/app/react-components/dom/chat/message.jsx b/app/react-components/dom/chat/message.jsx new file mode 100644 index 0000000..a9eaef3 --- /dev/null +++ b/app/react-components/dom/chat/message.jsx @@ -0,0 +1,33 @@ +import {useEffect, useState} from 'react'; + +import {render} from '@/dialogue.js'; +import {TAU} from '@/util/math.js'; + +import styles from './message.module.css'; + +export default function Message({letters}) { + const [radians, setRadians] = useState(0); + useEffect(() => { + setRadians(0); + let handle; + let last; + const spin = (ts) => { + if ('undefined' === typeof last) { + last = ts; + } + const elapsed = (ts - last) / 1000; + last = ts; + setRadians((radians) => radians + (elapsed * TAU)); + handle = requestAnimationFrame(spin); + }; + handle = requestAnimationFrame(spin); + return () => { + cancelAnimationFrame(handle); + }; + }, []); + return ( +
+ {render(letters, '')(letters.length - 1, radians)} +
+ ) +} \ No newline at end of file diff --git a/app/react-components/dom/chat/message.module.css b/app/react-components/dom/chat/message.module.css new file mode 100644 index 0000000..90e0a1b --- /dev/null +++ b/app/react-components/dom/chat/message.module.css @@ -0,0 +1,4 @@ +.message { + color: #ffffff; + margin-left: 8px; +} diff --git a/app/react-components/dom/chat/messages.jsx b/app/react-components/dom/chat/messages.jsx new file mode 100644 index 0000000..08b3880 --- /dev/null +++ b/app/react-components/dom/chat/messages.jsx @@ -0,0 +1,16 @@ +import Message from './message.jsx'; + +import styles from './messages.module.css'; + +export default function Messages({chatMessages}) { + return ( +
+ {chatMessages.map((letters, i) => ( + + ))} +
+ ) +} \ No newline at end of file diff --git a/app/react-components/dom/chat/messages.module.css b/app/react-components/dom/chat/messages.module.css new file mode 100644 index 0000000..b9ff1fb --- /dev/null +++ b/app/react-components/dom/chat/messages.module.css @@ -0,0 +1,5 @@ +.messages { + display: flex; + flex-direction: column-reverse; + overflow-y: auto; +} \ No newline at end of file diff --git a/app/react-components/dom/dialogue.jsx b/app/react-components/dom/dialogue.jsx index 852732f..d26878c 100644 --- a/app/react-components/dom/dialogue.jsx +++ b/app/react-components/dom/dialogue.jsx @@ -1,7 +1,8 @@ -import {useEffect, useRef, useState} from 'react'; +import {useEffect, useMemo, useRef, useState} from 'react'; import {RESOLUTION} from '@/constants.js'; import {useDomScale} from '@/context/dom-scale.js'; +import {render} from '@/dialogue.js'; import {TAU} from '@/util/math.js'; import styles from './dialogue.module.css'; @@ -88,6 +89,10 @@ export default function Dialogue({ cancelAnimationFrame(handle); }; }, [dialogue, domScale, ref]); + const localRender = useMemo( + () => render(dialogue.letters, styles.letter), + [dialogue.letters], + ); const origin = 'function' === typeof dialogue.origin ? dialogue.origin() : dialogue.origin || {x: 0, y: 0}; @@ -175,62 +180,7 @@ export default function Dialogue({

- { - dialogue.letters - .map(({character, indices, params}, i) => { - let color = 'inherit'; - let fade = 0; - let fontStyle = 'normal'; - let fontWeight = 'normal'; - let left = 0; - let opacity = 1; - let top = 0; - if (params.blink) { - const {frequency = 1} = params.blink; - opacity = (radians * (2 / frequency) % TAU) > (TAU / 2) ? opacity : 0; - } - if (params.fade) { - const {frequency = 1} = params.fade; - fade = frequency; - } - if (params.wave) { - const {frequency = 1, magnitude = 3} = params.wave; - top += magnitude * Math.cos((radians * (1 / frequency)) + TAU * indices.wave / params.wave.length); - } - if (params.rainbow) { - const {frequency = 1} = params.rainbow; - color = `hsl(${(radians * (1 / frequency)) + TAU * indices.rainbow / params.rainbow.length}rad 100 50)`; - } - if (params.shake) { - const {magnitude = 1} = params.shake; - left += (Math.random() * magnitude * 2) - magnitude; - top += (Math.random() * magnitude * 2) - magnitude; - } - if (params.em) { - fontStyle = 'italic'; - } - if (params.strong) { - fontWeight = 'bold'; - } - return ( - - {character} - - ); - }) - } + {localRender(caret, radians)}

); diff --git a/app/react-components/dom/dialogue.module.css b/app/react-components/dom/dialogue.module.css index 9afb5c1..2b2a057 100644 --- a/app/react-components/dom/dialogue.module.css +++ b/app/react-components/dom/dialogue.module.css @@ -11,6 +11,7 @@ border: solid 1px white; border-radius: 8px; color: white; + overflow-wrap: break-word; padding: 1em; position: fixed; margin: 0; @@ -26,5 +27,4 @@ .letter { opacity: 0; - position: relative; } diff --git a/app/react-components/dom/entities.jsx b/app/react-components/dom/entities.jsx index 003f1a3..37ebc20 100644 --- a/app/react-components/dom/entities.jsx +++ b/app/react-components/dom/entities.jsx @@ -6,7 +6,12 @@ import {parseLetters} from '@/dialogue.js'; import Entity from './entity.jsx'; -export default function Entities({camera, scale, setMonopolizers}) { +export default function Entities({ + camera, + scale, + setChatMessages, + setMonopolizers, +}) { const [ecs] = useEcs(); const [entities, setEntities] = useState({}); usePacket('EcsChange', async () => { @@ -34,6 +39,7 @@ export default function Entities({camera, scale, setMonopolizers}) { for (const key in dialogue) { dialogues[key] = dialogue[key]; dialogues[key].letters = parseLetters(dialogues[key].body); + setChatMessages((chatMessages) => [dialogues[key].letters, ...chatMessages]); const skipListeners = new Set(); dialogues[key].addSkipListener = (listener) => { skipListeners.add(listener); diff --git a/app/react-components/ui.jsx b/app/react-components/ui.jsx index a2980e6..337e366 100644 --- a/app/react-components/ui.jsx +++ b/app/react-components/ui.jsx @@ -9,7 +9,7 @@ import {useEcs, useEcsTick} from '@/context/ecs.js'; import {useMainEntity} from '@/context/main-entity.js'; import Disconnected from './dom/disconnected.jsx'; -import Chat from './dom/chat.jsx'; +import Chat from './dom/chat/chat.jsx'; import Dom from './dom/dom.jsx'; import Entities from './dom/entities.jsx'; import HotBar from './dom/hotbar.jsx'; @@ -69,6 +69,7 @@ export default function Ui({disconnected}) { const [message, setMessage] = useState(''); const [chatIsOpen, setChatIsOpen] = useState(false); const [chatHistory, setChatHistory] = useState([]); + const [chatMessages, setChatMessages] = useState([]); useEffect(() => { async function setEcsStuff() { const {default: Components} = await import('@/ecs-components/index.js'); @@ -97,6 +98,10 @@ export default function Ui({disconnected}) { }, [disconnected]); useEffect(() => { return addKeyListener(document.body, ({event, type, payload}) => { + if ('Escape' === payload && 'keyDown' === type && chatIsOpen) { + setChatIsOpen(false); + return; + } if (chatIsOpen) { return; } @@ -323,6 +328,10 @@ export default function Ui({disconnected}) {
{ + if (chatIsOpen) { + event.preventDefault(); + return; + } switch (event.button) { case 0: if (devtoolsIsOpen) { @@ -372,6 +381,10 @@ export default function Ui({disconnected}) { } }} onMouseUp={(event) => { + if (chatIsOpen) { + event.preventDefault(); + return; + } switch (event.button) { case 0: client.send({ @@ -389,6 +402,10 @@ export default function Ui({disconnected}) { event.preventDefault(); }} onWheel={(event) => { + if (chatIsOpen) { + event.preventDefault(); + return; + } if (event.deltaY > 0) { client.send({ type: 'Action', @@ -424,11 +441,13 @@ export default function Ui({disconnected}) { {chatIsOpen && (