diff --git a/client/src/assets/icons/messageIcon.svg b/client/src/assets/icons/messageIcon.svg
new file mode 100644
index 00000000..bce658ce
--- /dev/null
+++ b/client/src/assets/icons/messageIcon.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/client/src/pixi/World.tsx b/client/src/pixi/World.tsx
index 296d5a74..ce721fa9 100644
--- a/client/src/pixi/World.tsx
+++ b/client/src/pixi/World.tsx
@@ -3,6 +3,7 @@ import Rectangle from "./components/Rectangle";
import "@pixi/events";
import { DisplayPosition, Position } from "./Position";
import React from "react";
+import Message from "./components/Message";
interface Props {
userPosition: Position;
@@ -55,6 +56,14 @@ const World: React.FC = ({ userPosition, userDisplayPosition }) => {
width={100}
height={100}
/>
+
);
};
diff --git a/client/src/pixi/components/Message.tsx b/client/src/pixi/components/Message.tsx
new file mode 100644
index 00000000..a628f3ad
--- /dev/null
+++ b/client/src/pixi/components/Message.tsx
@@ -0,0 +1,113 @@
+import { Container, Graphics, Sprite, Text } from "@pixi/react";
+import messageIcon from "/src/assets/icons/messageIcon.svg";
+import React, { useRef, useState, useEffect, useCallback } from "react";
+import { Graphics as PIXIGraphics, TextStyle } from "pixi.js";
+import PIXI from "pixi.js";
+import { DisplayPosition } from "../Position";
+import { themeColors } from "../theme";
+
+const messageIconSize = 30;
+
+interface MessageBubbleProps {
+ width: number;
+ height: number;
+}
+
+const MessageBubble: React.FC = (props) => {
+ const draw = useCallback(
+ (g: PIXIGraphics) => {
+ g.clear();
+ g.lineStyle(2, 0x000000);
+ g.beginFill(themeColors.backgroundPrimary);
+ g.drawRoundedRect(0, 0, props.width, props.height, 10);
+ g.endFill();
+ },
+ [props],
+ );
+ return ;
+};
+
+interface Props {
+ messageText: string;
+ displayPosition: DisplayPosition;
+ user: {
+ name: string;
+ iconUrl: string;
+ };
+}
+
+const userNameTextStyle = new TextStyle({ fontSize: 14, fill: "black" });
+const messageTextStyle = new TextStyle({
+ fontSize: 16,
+ fill: "black",
+ wordWrap: true,
+ wordWrapWidth: 180,
+ breakWords: true,
+});
+
+const Message: React.FC = ({ messageText, displayPosition, user }) => {
+ const [showMessage, setShowMessage] = useState(false);
+ const textRef = useRef(null);
+ const [bubbleSize, setBubbleSize] = useState({ width: 200, height: 100 });
+
+ const handleMouseOver = useCallback(() => setShowMessage(true), []);
+ const handleMouseOut = useCallback(() => setShowMessage(false), []);
+
+ useEffect(() => {
+ if (!showMessage) return;
+ if (textRef.current) {
+ const { width, height } = textRef.current;
+ setBubbleSize({
+ width: width + 20,
+ height: height + 20,
+ });
+ }
+ }, [showMessage]);
+
+ const iconImageSrc = showMessage ? user.iconUrl : messageIcon;
+
+ return (
+
+
+ {showMessage && (
+ <>
+
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default Message;
diff --git a/client/src/pixi/theme.ts b/client/src/pixi/theme.ts
new file mode 100644
index 00000000..7652acc1
--- /dev/null
+++ b/client/src/pixi/theme.ts
@@ -0,0 +1,31 @@
+// @ts-expect-error tailwind config
+import tailwindConfig from "../../tailwind.config";
+
+const { colors } = tailwindConfig.theme.extend;
+
+type ThemeColors = Record<
+ | "backgroundPrimary"
+ | "backgroundSecondary"
+ | "backgroundTertiary"
+ | "textPrimary"
+ | "textSecondary"
+ | "textTertiary"
+ | "accentPrimary"
+ | "accentSecondary"
+ | "accentTertiary"
+ | "accentHover",
+ string
+>;
+
+export const themeColors: ThemeColors = {
+ backgroundPrimary: colors["background-primary"],
+ backgroundSecondary: colors["background-secondary"],
+ backgroundTertiary: colors["background-tertiary"],
+ textPrimary: colors["text-primary"],
+ textSecondary: colors["text-secondary"],
+ textTertiary: colors["text-tertiary"],
+ accentPrimary: colors["accent-primary"],
+ accentSecondary: colors["accent-secondary"],
+ accentTertiary: colors["accent-tertiary"],
+ accentHover: colors["accent-hover"],
+};