prendy
Version:
Make games with prerendered backdrops using babylonjs and repond
237 lines (236 loc) • 10.9 kB
JavaScript
import { sizeFromRef } from "chootils/dist/elements";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { animated, interpolate, useSpring } from "react-spring";
import { getState, useStore, useStoreEffect } from "repond";
import { getScreenSize } from "../../../helpers/babylonjs/slate";
import { getCharDollStuff } from "../../../helpers/prendyUtils/characters";
import { meta } from "../../../meta";
import { BubbleTriangle } from "./BubbleTriangle";
const BUBBLE_WIDTH = 230;
const BUBBLE_HEIGHT_RATIO = 0.74814;
const BUBBLE_HEIGHT = BUBBLE_WIDTH * BUBBLE_HEIGHT_RATIO;
const TRIANGLE_SIZE = 25;
const SHARED_THEME = {
fontSize: "20px",
lineHeight: "33px",
padding: "8px",
borderColor: "rgb(227, 181, 106)",
backgroundColor: "rgb(249, 235, 146)",
borderWidth: 2,
rounding: 12,
};
// when the TRIANGLE_SIZE is 50, the SVG is unscaled, so the border width is correct to pixels
// when the TRIANGLE_SIZE is different the svgs border width (stroke) needs to be scaled accordingly
const TRIANGLE_BORDER_WIDTH_SCALE = 50 / TRIANGLE_SIZE;
export function SpeechBubble({ name }) {
const { speechVidFiles } = meta.assets;
const theRectangle = useRef(null);
const theTextRectangle = useRef(null);
const theTriangle = useRef(null);
const theText = useRef(null);
const theTextHolder = useRef(null);
const forCharacter = getState().speechBubbles[name].forCharacter ?? "walker";
const [measuredHeight, setMeasuredHeight] = useState(0);
const refs = {
theRectangle,
theTextRectangle,
theTriangle,
theText,
theTextHolder,
};
const { goalText, isVisible, font, zIndex, visibleLetterAmount, _goalTextWordLetterArrays, _specialTextByLetterIndex, stylesBySpecialText, nowVideoName, } = useStore((state) => state.speechBubbles[name], {
id: name,
type: ["speechBubbles"],
prop: [
// TODO fix types?
"goalText",
"isVisible",
"font",
"zIndex",
"visibleLetterAmount",
"_goalTextWordLetterArrays",
"_specialTextByLetterIndex",
"stylesBySpecialText",
"nowVideoName",
],
});
const { nowPlaceName } = useStore((state) => state.global.main, {
type: "global",
prop: ["nowPlaceName"],
});
const videoIsPlaying = !!nowVideoName;
const randomRotation = useMemo(() => {
return Math.random() * 6 - 3;
}, [goalText, isVisible]);
const [theSpring, theSpringApi] = useSpring(() => {
let height = 0;
if (videoIsPlaying) {
height = 240;
}
else {
if (isVisible)
height = measuredHeight;
}
return {
height: isVisible ? measuredHeight : 0,
position: [0, 0],
opacity: isVisible ? 1 : 0,
scale: isVisible ? 1 : 0.1,
rotation: randomRotation,
config: { tension: 400, friction: 50 },
onChange() {
positionSpeechBubbleToCharacter();
},
};
}, [isVisible, videoIsPlaying, randomRotation]);
useEffect(() => {
const newMeasuredHeight = sizeFromRef(refs.theTextHolder.current).height;
if (newMeasuredHeight !== 0) {
setMeasuredHeight(newMeasuredHeight);
}
}, [goalText, refs.theTextHolder, nowVideoName]);
useEffect(() => {
theSpringApi.start({ height: measuredHeight });
}, [measuredHeight, theSpringApi]);
const positionSpeechBubbleToCharacter = useCallback(() => {
const { forCharacter } = getState().speechBubbles[name];
if (!forCharacter)
return;
const { dollState, dollName } = getCharDollStuff(forCharacter) ?? {};
if (!dollState || !dollName)
return;
const { focusedDoll, focusedDollIsInView } = getState().global.main;
const positionOnScreen = dollState.positionOnScreen;
// if (dollName === focusedDoll && !focusedDollIsInView) {}
const viewSize = getScreenSize();
const farLeft = -viewSize.x / 2;
const farRight = viewSize.x / 2;
const farTop = -viewSize.y / 2;
const farBottom = viewSize.y / 2;
const bubbleHeight = refs.theTextRectangle.current?.offsetHeight ?? 190;
const halfBubbleHeight = bubbleHeight / 2;
const halfBubbleWidth = BUBBLE_WIDTH / 2;
const halfTriangleSize = TRIANGLE_SIZE / 2;
const screenSize = getScreenSize();
// need function to get position on screen
let newPositionX = positionOnScreen.x - screenSize.x / 2;
let yOffset = bubbleHeight / 2;
let newPositionY = positionOnScreen.y - yOffset - screenSize.y / 2;
// Keep the focused dolls speech bubble inside the view
if (dollName === focusedDoll) {
if (newPositionX - halfBubbleWidth < farLeft) {
newPositionX = farLeft + halfBubbleWidth;
}
if (newPositionX + halfBubbleWidth > farRight) {
newPositionX = farRight - halfBubbleWidth;
}
if (newPositionY - halfBubbleHeight - halfTriangleSize < farTop) {
newPositionY = farTop + halfBubbleHeight + halfTriangleSize;
}
if (newPositionY + halfBubbleHeight + halfTriangleSize > farBottom) {
newPositionY = farBottom - halfBubbleHeight - halfTriangleSize;
}
}
theSpringApi.start({
position: [newPositionX, newPositionY],
immediate: true,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useStoreEffect(positionSpeechBubbleToCharacter, [
{ type: ["dolls"], id: forCharacter, prop: ["positionOnScreen"] },
{ type: ["global"], id: "main", prop: ["slatePos"] },
{ type: ["global"], id: "main", prop: ["slateZoom"] },
{ type: ["story"], id: "main", prop: ["storyPart"] },
{ type: ["global"], id: "main", prop: ["nowCamName"] },
], [nowPlaceName]);
// const textIsVeryShort = goalText.length < 10;
const styles = useMemo(() => ({
hiddenGoalText: {
position: "absolute",
top: 0,
left: 0,
right: 0,
// color: "rgb(197, 217, 61)",
// opacity: 0,
fontSize: SHARED_THEME.fontSize,
lineHeight: SHARED_THEME.lineHeight,
padding: videoIsPlaying ? SHARED_THEME.padding : SHARED_THEME.padding,
fontFamily: font,
// textAlign: "center",
// verticalAlign: "middle", // to center emojis with text?
display: "flex",
alignItems: "center",
justifyContent: "center",
// justifyContent: textIsVeryShort ? "center" : "flex-start",
flexDirection: "row",
flexWrap: "wrap",
},
}), [font, videoIsPlaying]);
const containerStyle = useMemo(() => ({
pointerEvents: "none",
position: "absolute",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
zIndex: zIndex,
fontFamily: font,
textAlign: "center",
verticalAlign: "middle", // to center emojis with text?
fontSize: SHARED_THEME.fontSize,
lineHeight: SHARED_THEME.lineHeight,
color: "rgb(68, 68, 68)",
}), [zIndex]);
return (React.createElement("div", { id: `speech-bubble-${name}`, style: containerStyle },
React.createElement(animated.div, { ref: refs.theRectangle, key: `theRectangle`, id: `theRectangle`, style: {
width: `${BUBBLE_WIDTH}px`,
// zIndex: 100,
opacity: theSpring.opacity,
transform: interpolate([
theSpring.position.to((x, y) => `translate(${x}px , ${y}px)`),
theSpring.scale.to((scale) => `scale(${scale})`),
theSpring.rotation.to((rotation) => `rotate(${rotation}deg)`),
], (translate, scale, rotate) => `${translate} ${scale} ${rotate}`),
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
willChange: "transform, opacity",
} },
React.createElement(animated.div, { ref: refs.theTextRectangle, key: `textRectangle`, id: `textRectangle`, style: {
backgroundColor: "#fafafa",
width: `${BUBBLE_WIDTH}px`,
paddingBottom: "5px",
zIndex: 100,
height: theSpring.height,
overflow: "hidden",
willChange: "height",
position: "relative", // fixes overflow not working
// borderRadius: "20px",
// borderWidth: "1px",
borderRadius: SHARED_THEME.rounding + "px",
borderWidth: SHARED_THEME.borderWidth,
borderColor: SHARED_THEME.borderColor,
borderStyle: "solid",
} },
React.createElement("div", { ref: theTextHolder, style: styles.hiddenGoalText },
videoIsPlaying && (React.createElement("video", {
// width="100%"
width: `${BUBBLE_WIDTH}px`, height: `${BUBBLE_HEIGHT}px`, autoPlay: true, loop: true, src: speechVidFiles[nowVideoName ?? ""] ?? "" })),
_goalTextWordLetterArrays.map((wordLetters, wordIndex) => {
let letterAmountFromPreviousWords = wordIndex > 0 ? _goalTextWordLetterArrays.slice(0, wordIndex - 1).flat().length : 0;
return (React.createElement("span", { className: "SpeechBubble-wordLettersHolder", key: "" + wordLetters + wordIndex }, wordLetters.map((letter, wordLetterIndex) => {
const textLetterIndex = (letterAmountFromPreviousWords || -1) + wordLetterIndex;
const isVisible = textLetterIndex < visibleLetterAmount;
// NOTE this is maybe undefiend, but typescript rules dont treat it like that atm
const customStyle = stylesBySpecialText[_specialTextByLetterIndex[textLetterIndex + 1]];
return (React.createElement("div", { key: "" + letter + wordLetterIndex, className: isVisible ? "SpeechBubble-visibleLetter" : "SpeechBubble-hiddenLetter", style: customStyle }, `${letter}`));
})));
}))),
React.createElement(BubbleTriangle, null))));
}