refactor core into composables & components
This commit is contained in:
parent
3be7023792
commit
a84019da0d
37 changed files with 409 additions and 469 deletions
|
|
@ -1,64 +0,0 @@
|
|||
<template>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="linear-gradient"
|
||||
x1="0.5"
|
||||
y1="-1.438"
|
||||
x2="0.5"
|
||||
y2="0.633"
|
||||
gradientUnits="objectBoundingBox"
|
||||
>
|
||||
<stop offset="0" stop-color="#fff33b" stop-opacity="0" />
|
||||
<stop offset="0.152" stop-color="#ffde46" stop-opacity="0.141" />
|
||||
<stop offset="0.467" stop-color="#ffa764" stop-opacity="0.435" />
|
||||
<stop offset="0.581" stop-color="#ff916f" stop-opacity="0.541" />
|
||||
<stop offset="1" stop-color="#ff5782" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linear-gradient-2"
|
||||
x1="0.498"
|
||||
y1="0.728"
|
||||
x2="0.498"
|
||||
y2="-0.433"
|
||||
gradientUnits="objectBoundingBox"
|
||||
>
|
||||
<stop offset="0" stop-color="#f7cf83" />
|
||||
<stop offset="1" stop-color="#fff" />
|
||||
</linearGradient>
|
||||
<rect id="rect" width="29" height="29" rx="10" />
|
||||
<clipPath id="clip">
|
||||
<use xlink:href="#rect" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<g id="titlebar" transform="translate(0, 0)">
|
||||
<g data-name="Group 73" transform="translate(20 20)">
|
||||
<ellipse
|
||||
id="Oval"
|
||||
cx="8.134"
|
||||
cy="8.134"
|
||||
rx="8.134"
|
||||
ry="8.134"
|
||||
transform="translate(0 0)"
|
||||
fill="url(#linear-gradient)"
|
||||
/>
|
||||
<ellipse
|
||||
id="Oval-2"
|
||||
data-name="Oval"
|
||||
cx="8.134"
|
||||
cy="8.134"
|
||||
rx="8.134"
|
||||
ry="8.134"
|
||||
transform="translate(27 0)"
|
||||
fill="url(#linear-gradient-2)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
export default defineComponent({
|
||||
name: "TitleBar",
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<template>
|
||||
<audio-input v-if="visualizeStream" :bubbleWidth="audioBubbleWidth" :paths="paths" />
|
||||
<text-input v-else :input="input" :textXoffset="textInputXoffset" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import TextInput from "./inputDetails/textInput.vue";
|
||||
import AudioInput from "./inputDetails/audioInput.vue";
|
||||
import useInputController from "@/core/UI/input/useInputController";
|
||||
export default defineComponent({
|
||||
name: "InputItem",
|
||||
components: { TextInput, AudioInput },
|
||||
setup() {
|
||||
const {
|
||||
input,
|
||||
visualizeStream,
|
||||
textInputXoffset,
|
||||
audioBubbleWidth,
|
||||
paths,
|
||||
} = useInputController();
|
||||
|
||||
return {
|
||||
input,
|
||||
textInputXoffset,
|
||||
visualizeStream,
|
||||
audioBubbleWidth,
|
||||
paths,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
41
src/components/inputVisualizer/index.vue
Normal file
41
src/components/inputVisualizer/index.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<audio-stream v-if="visualizeStream" :bubbleWidth="audioBubbleWidth" :paths="paths" />
|
||||
<text-input v-else :input="input" :textXoffset="textInputXoffset" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// input child components
|
||||
import TextInput from "./visualizerDetails/text.vue";
|
||||
import AudioStream from "./visualizerDetails/audio.vue";
|
||||
|
||||
import useInputVisualizer from "./visualizer";
|
||||
|
||||
import { defineComponent } from "vue";
|
||||
export default defineComponent({
|
||||
name: "InputVisualizer",
|
||||
components: { TextInput, AudioStream },
|
||||
setup() {
|
||||
// get text input
|
||||
|
||||
// get audio stream
|
||||
|
||||
// use Input Handler
|
||||
|
||||
const {
|
||||
input,
|
||||
visualizeStream,
|
||||
textInputXoffset,
|
||||
audioBubbleWidth,
|
||||
paths,
|
||||
} = useInputVisualizer();
|
||||
|
||||
return {
|
||||
input,
|
||||
textInputXoffset,
|
||||
visualizeStream,
|
||||
audioBubbleWidth,
|
||||
paths,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
92
src/components/inputVisualizer/visualizer.ts
Normal file
92
src/components/inputVisualizer/visualizer.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { ref, watch, onUnmounted, onMounted } from "vue";
|
||||
|
||||
import useKeyDownHandler from "@/composables/keyDownHandler/useKeyDownHandler";
|
||||
|
||||
import useMediaStream from "@/utils/mediaStream/useMediaStream";
|
||||
|
||||
import useStreamVisualizer from './visualizerDetails/audioVisualizer';
|
||||
|
||||
import useTextVisualizer from './visualizerDetails/textVisualizer';
|
||||
|
||||
/*
|
||||
* Input visualizer
|
||||
* Will visualize text input by default as user types or
|
||||
* if user holds space bar down, input audio stream
|
||||
* will be visualized & recorded
|
||||
*/
|
||||
// NOTE: input params should probably be input and MediaStream
|
||||
export default function useInputVisualizer() {
|
||||
const visualizeStream = ref(false);
|
||||
const paths = ref([]);
|
||||
const audioBubbleWidth = ref(10);
|
||||
let stream: MediaStream;
|
||||
|
||||
const { keyDownHandler, input } = useKeyDownHandler();
|
||||
const { textInputXoffset, renderTextInput } = useTextVisualizer(input);
|
||||
const { visualizeStreamAsPaths, expandBubble } = useStreamVisualizer(visualizeStream);
|
||||
|
||||
// stops stream recording on space key up
|
||||
function keyupHandler(e: any) {
|
||||
if (e.key === " " && visualizeStream.value) {
|
||||
console.log("stop recording");
|
||||
|
||||
// window.postMessage({
|
||||
// myTypeField: 'update-recorder',
|
||||
// record: false
|
||||
// }, '*')
|
||||
|
||||
visualizeStream.value = false;
|
||||
paths.value = []
|
||||
audioBubbleWidth.value = 10;
|
||||
input.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// add window event keyboard listeners
|
||||
window.addEventListener("keydown", keyDownHandler);
|
||||
window.addEventListener('keyup', keyupHandler);
|
||||
|
||||
// get stream & initialize recorder object on mount
|
||||
onMounted(async () => {
|
||||
stream = await useMediaStream();
|
||||
})
|
||||
|
||||
// determine whether to render text or audio based one the keyboard input
|
||||
watch(input, (input, prevInput) => {
|
||||
if (prevInput !== "" || prevInput.length > 0) {
|
||||
if (!visualizeStream.value) {
|
||||
renderTextInput();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (input === " " && visualizeStream.value === false) {
|
||||
console.log("record MediaStream");
|
||||
visualizeStream.value = true;
|
||||
|
||||
// window.postMessage({
|
||||
// myTypeField: 'update-recorder',
|
||||
// record: true
|
||||
// }, '*')
|
||||
|
||||
expandBubble(audioBubbleWidth);
|
||||
visualizeStreamAsPaths(stream, paths, audioBubbleWidth);
|
||||
return;
|
||||
}
|
||||
renderTextInput();
|
||||
});
|
||||
|
||||
// remove Event Listeners on component unMount
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", keyDownHandler);
|
||||
window.removeEventListener("keyup", keyupHandler);
|
||||
});
|
||||
|
||||
return {
|
||||
input,
|
||||
visualizeStream,
|
||||
textInputXoffset,
|
||||
audioBubbleWidth,
|
||||
paths
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,19 +1,14 @@
|
|||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="bubbleWidth"
|
||||
height="34"
|
||||
:x="center"
|
||||
y="440"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="bubbleWidth"
|
||||
height="34"
|
||||
:x="center"
|
||||
y="440"
|
||||
rx="10"
|
||||
class="audioInput"
|
||||
>
|
||||
<g
|
||||
id="Group_126"
|
||||
data-name="Group 126"
|
||||
transform="translate(0 0)"
|
||||
rx="10"
|
||||
>
|
||||
<g id="Group_126" data-name="Group 126" transform="translate(0 0)" rx="10">
|
||||
<rect
|
||||
id="Rectangle_478"
|
||||
data-name="Rectangle 478"
|
||||
|
|
@ -24,19 +19,19 @@
|
|||
fill="#b9b9b9"
|
||||
/>
|
||||
<svg>
|
||||
<g rx="20" id="Group_125" data-name="Group 125" v-for="(path, index) in paths">
|
||||
<path
|
||||
:key="index"
|
||||
id="path"
|
||||
:data-name="path.identifier"
|
||||
:d="path.d"
|
||||
:transform="`translate(${path.offset} 0)`"
|
||||
fill="none"
|
||||
:stroke="path.color"
|
||||
stroke-linecap="round"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</g>
|
||||
<g rx="20" id="Group_125" data-name="Group 125" v-for="(path, index) in paths">
|
||||
<path
|
||||
:key="index"
|
||||
id="path"
|
||||
:data-name="path.identifier"
|
||||
:d="path.d"
|
||||
:transform="`translate(${path.offset} 0)`"
|
||||
fill="none"
|
||||
:stroke="path.color"
|
||||
stroke-linecap="round"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
@ -45,17 +40,14 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, computed, inject } from "vue";
|
||||
export default defineComponent({
|
||||
name: "AudioInput",
|
||||
name: "AudioStream",
|
||||
props: {
|
||||
bubbleWidth: Number,
|
||||
paths: Array,
|
||||
},
|
||||
setup(props) {
|
||||
|
||||
const emitter: any = inject("mitt");
|
||||
function audioSend() {
|
||||
|
||||
}
|
||||
function audioSend() {}
|
||||
|
||||
const center = computed(() => {
|
||||
if (props.bubbleWidth) {
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import { Ref } from '@/types/vueRef/index';
|
||||
import AnimeFunc from "@/types/animejs/index";
|
||||
import { inject, ref } from "vue";
|
||||
|
||||
interface AudioPath {
|
||||
identifier: string;
|
||||
d: string;
|
||||
offset: number;
|
||||
color: string;
|
||||
draw: boolean;
|
||||
}
|
||||
export default function useStreamVisualizer(draw: Ref<boolean>) {
|
||||
const audioContext = new AudioContext();
|
||||
const analyzer = audioContext.createAnalyser();
|
||||
analyzer.fftSize = 128;
|
||||
const bufferLength = analyzer.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
|
||||
// imports animejs safely
|
||||
let anime: AnimeFunc;
|
||||
const animeInject: AnimeFunc | undefined = inject("animejs");
|
||||
if (animeInject) anime = animeInject;
|
||||
|
||||
/*
|
||||
* Draws MediaStream audioTrack as 'AudioPath' objects
|
||||
*/
|
||||
function drawStream(paths: Ref<any>, bubbleWidth: Ref<number>): void {
|
||||
const xInitial = ref(10);
|
||||
const maxAmplitude = 15;
|
||||
const maxBubbleWidth = 300;
|
||||
|
||||
function shiftPathStream(pathList: NodeList): void {
|
||||
for (let i = 0; i < pathList.length; i++) {
|
||||
const stick = pathList[i] as HTMLElement;
|
||||
const computedQuery = window.getComputedStyle(stick);
|
||||
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
|
||||
anime({
|
||||
targets: stick,
|
||||
translateX: [matrix.m41, matrix.m41 - 6],
|
||||
})
|
||||
}
|
||||
}
|
||||
function filterPathList(pathList: NodeList) {
|
||||
for (let i = 0; i < pathList.length; i++) {
|
||||
const stick = pathList[i] as HTMLElement;
|
||||
const computedQuery = window.getComputedStyle(stick);
|
||||
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
|
||||
if (matrix.m41 < 10) {
|
||||
anime({
|
||||
targets: stick,
|
||||
opacity: 0,
|
||||
duration: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function drawNewPath(): void {
|
||||
|
||||
const calculateAmplitude = (): number => (
|
||||
Math.min(Math.max(
|
||||
(dataArray.reduce((a, b) => a + b) / bufferLength) * 0.45,
|
||||
0), maxAmplitude)
|
||||
)
|
||||
|
||||
const createAudioPath = (a: number, o: number): AudioPath => ({
|
||||
identifier: `path_${o}`,
|
||||
d: `M0,25v${-a}`,
|
||||
offset: o,
|
||||
color: '#000',
|
||||
draw: true
|
||||
})
|
||||
|
||||
const amplitude = calculateAmplitude();
|
||||
const p: AudioPath = createAudioPath(amplitude, xInitial.value);
|
||||
paths.value.push(p);
|
||||
}
|
||||
|
||||
paths.value = [];
|
||||
function drawPathStream(): void {
|
||||
|
||||
let streamPath;
|
||||
if (!draw.value) {
|
||||
window.clearTimeout(streamPath)
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(drawPathStream);
|
||||
analyzer.getByteFrequencyData(dataArray);
|
||||
}, 1000 / 10)
|
||||
|
||||
/*
|
||||
* To Draw pathStream
|
||||
*/
|
||||
if (bubbleWidth.value === 300) {
|
||||
xInitial.value = bubbleWidth.value - 10;
|
||||
} else if (bubbleWidth.value > 20) {
|
||||
xInitial.value = bubbleWidth.value;
|
||||
} else {
|
||||
xInitial.value += 3;
|
||||
}
|
||||
streamPath = setTimeout(async () => {
|
||||
const pathList = document.querySelectorAll('path');
|
||||
filterPathList(pathList);
|
||||
drawNewPath();
|
||||
if (bubbleWidth.value === maxBubbleWidth) {
|
||||
shiftPathStream(pathList);
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
drawPathStream();
|
||||
}
|
||||
|
||||
function increaseBubbleWidth(bubbleRef: Ref<number>) {
|
||||
const increase = () => {
|
||||
|
||||
let animation;
|
||||
if (!draw.value) {
|
||||
if (animation) {
|
||||
cancelAnimationFrame(animation)
|
||||
}
|
||||
return;
|
||||
}
|
||||
animation = requestAnimationFrame(increase);
|
||||
if (bubbleRef.value < 300) {
|
||||
bubbleRef.value++;
|
||||
}
|
||||
}
|
||||
increase();
|
||||
}
|
||||
|
||||
function expandBubble(bubbleWidth: Ref<number>) {
|
||||
increaseBubbleWidth(bubbleWidth);
|
||||
}
|
||||
|
||||
function visualizeStreamAsPaths(s: MediaStream, paths: Ref<any>, bubbleWidth: Ref<number>): void {
|
||||
const source = audioContext.createMediaStreamSource(s);
|
||||
source.connect(analyzer);
|
||||
drawStream(paths, bubbleWidth);
|
||||
}
|
||||
|
||||
return {
|
||||
visualizeStreamAsPaths,
|
||||
expandBubble
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,5 @@
|
|||
<template>
|
||||
<foreignObject
|
||||
class="inputContainer"
|
||||
:x="textXoffset"
|
||||
y="450"
|
||||
width="55%"
|
||||
height="50%"
|
||||
>
|
||||
<foreignObject class="inputContainer" :x="textXoffset" y="450" width="55%" height="50%">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||
<div class="inputBubble" contenteditable v-show="input.length > 0">
|
||||
<p>{{ input }}</p>
|
||||
|
|
@ -15,12 +9,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, onMounted, ref } from "vue";
|
||||
import { defineComponent } from "vue";
|
||||
export default defineComponent({
|
||||
name: "TextInput",
|
||||
props: {
|
||||
input: String,
|
||||
textXoffset: String
|
||||
textXoffset: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import AnimeFunc from "@/types/animejs/index";
|
||||
import { inject } from "vue";
|
||||
import { Ref } from "@/types/vueRef/index";
|
||||
/*
|
||||
* Anime js animations used by input textController
|
||||
*/
|
||||
export default function useTextInputAnims() {
|
||||
|
||||
// imports animejs safely
|
||||
let anime: AnimeFunc;
|
||||
const animeInject: AnimeFunc | undefined = inject("animejs");
|
||||
if (animeInject) anime = animeInject;
|
||||
|
||||
const animateSend = (el: HTMLElement, elHeight: number, input: Ref<string>) => {
|
||||
anime({
|
||||
targets: el,
|
||||
translateY: -elHeight + 15,
|
||||
duration: 200,
|
||||
easing: "easeInQuad",
|
||||
complete: function() {
|
||||
anime({
|
||||
targets: el,
|
||||
translateY: 50,
|
||||
duration: 200,
|
||||
easing: "easeOutQuad",
|
||||
complete: function() {
|
||||
input.value = "";
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const inputAppear = (el: HTMLElement) => {
|
||||
anime({
|
||||
targets: el,
|
||||
translateY: -10,
|
||||
duration: 0
|
||||
});
|
||||
}
|
||||
|
||||
const verticalShiftInput = (el: HTMLElement, yTrans: number) => {
|
||||
anime({
|
||||
targets: el,
|
||||
translateY: yTrans,
|
||||
easing: "easeOutQuad",
|
||||
duration: 150,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
animateSend,
|
||||
verticalShiftInput,
|
||||
inputAppear
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { ref, computed, inject } from 'vue';
|
||||
import useTextInputAnims from "./textAnime";
|
||||
import { Ref } from "@/types/vueRef/index";
|
||||
/*
|
||||
* text rendering logic used by inputController
|
||||
*/
|
||||
export default function useTextVisualizer(input: Ref<string>) {
|
||||
const inputHeight = ref(42);
|
||||
const inputWidth = ref(0);
|
||||
|
||||
const emitter: any = inject("mitt");
|
||||
const { animateSend, verticalShiftInput, inputAppear } = useTextInputAnims();
|
||||
|
||||
async function renderTextInput() {
|
||||
// first, get input dimensions
|
||||
const inputDivs = await document.getElementsByClassName("inputBubble");
|
||||
const height = inputDivs[0].getBoundingClientRect().height;
|
||||
inputWidth.value = inputDivs[0].getBoundingClientRect().width;
|
||||
// then, query for foreignObject div
|
||||
const foreignObjectDiv = document.getElementsByClassName(
|
||||
"inputContainer"
|
||||
);
|
||||
const foreignEl = foreignObjectDiv[0] as HTMLElement;
|
||||
if (height !== inputHeight.value) {
|
||||
inputHeight.value = height;
|
||||
}
|
||||
if (inputHeight.value === 0) {
|
||||
inputAppear(foreignEl)
|
||||
} else if (inputHeight.value > 36) {
|
||||
const yTrans = -inputHeight.value + 30;
|
||||
verticalShiftInput(foreignEl, yTrans);
|
||||
}
|
||||
}
|
||||
|
||||
emitter.on("newSelfMessage", async (payload: any) => {
|
||||
const inputEl = document.getElementsByClassName("inputContainer");
|
||||
const foreignEl = inputEl[0] as HTMLElement;
|
||||
animateSend(foreignEl, inputHeight.value, input);
|
||||
});
|
||||
|
||||
const textInputXoffset = computed(() => {
|
||||
return `calc(50% - ${inputWidth.value / 2})`;
|
||||
});
|
||||
|
||||
return {
|
||||
textInputXoffset,
|
||||
renderTextInput
|
||||
}
|
||||
}
|
||||
|
|
@ -6,24 +6,26 @@
|
|||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
@click="emittMessage"
|
||||
>
|
||||
<!-- <AI /> -->
|
||||
<MessageRenderer />
|
||||
<InputItem />
|
||||
<!-- <TitleBar /> -->
|
||||
<MessageVisualizer />
|
||||
<InputVisualizer />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import AI from "../AI/index.vue";
|
||||
import MessageRenderer from "./UIDetails/messageRenderer.vue";
|
||||
import InputItem from "@/components/input/inputItem.vue";
|
||||
import TitleBar from "./UIDetails/titleBar.vue";
|
||||
import Messages from "./UIDetails/messages";
|
||||
// mock messages
|
||||
import Messages from "./mockMessages";
|
||||
|
||||
// child components
|
||||
import MessageVisualizer from "./messageVisualizer/index.vue";
|
||||
import InputVisualizer from "@/components/inputVisualizer/index.vue";
|
||||
|
||||
// event emitter
|
||||
import { Mitt } from "@/types/mitt/index";
|
||||
|
||||
import { defineComponent, ref, inject } from "vue";
|
||||
export default defineComponent({
|
||||
name: "UI",
|
||||
components: { MessageRenderer, InputItem },
|
||||
name: "Messenger",
|
||||
components: { MessageVisualizer, InputVisualizer },
|
||||
setup() {
|
||||
const messages = Messages;
|
||||
const count = ref(0);
|
||||
|
|
@ -42,9 +44,9 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
window.ipcRenderer.on("render-message", (event, payload) => {
|
||||
console.log(payload.message)
|
||||
console.log(payload.message);
|
||||
emitter.emit("renderMessage", payload.message);
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
emittMessage,
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
/**
|
||||
* single message svg item computed properties
|
||||
*/
|
||||
export default function useComputedBubble(m: string, id: string) {
|
||||
const x = ref(0)
|
||||
const paddingLeft = 30;
|
||||
const winW = 350;
|
||||
const messageWidth = ref(0);
|
||||
const messageMarginTop = 75;
|
||||
|
||||
const textFill = computed(() => {
|
||||
switch (m) {
|
||||
case "sf":
|
||||
return "#fff";
|
||||
default:
|
||||
return "#000";
|
||||
}
|
||||
});
|
||||
|
||||
const bubbleFill = computed(() => {
|
||||
switch (m) {
|
||||
case "sf":
|
||||
return "#61b4f4";
|
||||
default:
|
||||
return "#fff";
|
||||
}
|
||||
});
|
||||
|
||||
const setMessageWidthRef = async () => {
|
||||
const msg = await document.getElementById(id);
|
||||
if (msg) {
|
||||
messageWidth.value = msg.getBoundingClientRect().width;
|
||||
}
|
||||
}
|
||||
|
||||
const selfMessageMarginRight = 2.5;
|
||||
const calculateXoffset = async () => {
|
||||
switch (m) {
|
||||
case "sf":
|
||||
x.value = winW - messageWidth.value - selfMessageMarginRight;
|
||||
break;
|
||||
default:
|
||||
x.value = paddingLeft;
|
||||
// to center a message
|
||||
// x = messageMargin + (winW -messageWidth) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
const startingOffset = computed(() => {
|
||||
const parentView = document.getElementById('messageVisualizer');
|
||||
if (parentView) {
|
||||
const rect = parentView.getBoundingClientRect();
|
||||
let Y = 600;
|
||||
if (rect.height > 500) {
|
||||
Y = rect.height + messageMarginTop;
|
||||
}
|
||||
return `translate(${x.value} ${Y})`;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await setMessageWidthRef();
|
||||
calculateXoffset();
|
||||
})
|
||||
|
||||
return {
|
||||
textFill,
|
||||
bubbleFill,
|
||||
startingOffset
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { ref, onMounted } from "vue";
|
||||
import { Ref } from "@/types/vueRef/index";
|
||||
|
||||
/**
|
||||
* handles visualization of single message svg items
|
||||
*/
|
||||
export default function useBubbleDrawer({
|
||||
mr,
|
||||
tr,
|
||||
}: {
|
||||
mr: Ref<string>;
|
||||
tr: Ref<string>;
|
||||
}) {
|
||||
const signatureTranslate = ref("");
|
||||
let messageRect: SVGSVGElement & SVGRectElement;
|
||||
let messageText: SVGSVGElement & SVGTextElement;
|
||||
|
||||
onMounted(async () => {
|
||||
messageRect = (mr.value as unknown) as SVGSVGElement & SVGRectElement;
|
||||
messageText = (tr.value as unknown) as SVGSVGElement & SVGTextElement;
|
||||
});
|
||||
|
||||
function adjustMessageRect(): void {
|
||||
// must get dimensions of text box to fit bubble around
|
||||
const padding = 12.5;
|
||||
const textBox = messageText.getBBox();
|
||||
messageRect.setAttribute("x", String(textBox.x - padding));
|
||||
messageRect.setAttribute("y", String(textBox.y - padding));
|
||||
messageRect.setAttribute("width", String(textBox.width + 2 * padding));
|
||||
messageRect.setAttribute("height", String(textBox.height + 2 * padding));
|
||||
}
|
||||
|
||||
function translateMsgSignature(): void {
|
||||
if (messageRect.getAttribute("height") !== null) {
|
||||
const height = Number(messageRect.getAttribute("height"));
|
||||
signatureTranslate.value = `translate(-10 ${height})`;
|
||||
}
|
||||
}
|
||||
|
||||
async function drawMessage() {
|
||||
// ignore lint error: await needed for proper render
|
||||
await adjustMessageRect();
|
||||
translateMsgSignature();
|
||||
}
|
||||
|
||||
return {
|
||||
drawMessage,
|
||||
signatureTranslate,
|
||||
};
|
||||
}
|
||||
|
|
@ -27,10 +27,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import useComputedBubble from "./bubbleDetails/computed";
|
||||
import useBubbleDrawer from "./bubbleDetails/drawer";
|
||||
|
||||
// text wrap composition
|
||||
import useTextWrap from "@/composables/textWrap";
|
||||
|
||||
import { defineComponent, onMounted, ref, reactive } from "vue";
|
||||
import useMessageItemComp from "@/core/messenger/message/messageDetails/useComputed";
|
||||
import useMessageItemAnims from "@/core/messenger/message/messageDetails/useAnimations";
|
||||
import useTextWrap from "@/utils/textWrap/useTextWrap";
|
||||
export default defineComponent({
|
||||
name: "MessageItem",
|
||||
props: {
|
||||
|
|
@ -46,12 +49,12 @@ export default defineComponent({
|
|||
const messageRect = ref(`message${msgID.value}`);
|
||||
const messageText = ref(`text${msgID.value}`);
|
||||
|
||||
const { textFill, bubbleFill, startingOffset } = useMessageItemComp(
|
||||
const { textFill, bubbleFill, startingOffset } = useComputedBubble(
|
||||
props.item.modifier,
|
||||
props.item.id
|
||||
);
|
||||
const { textWrap } = useTextWrap();
|
||||
const { drawMessage, signatureTranslate } = useMessageItemAnims({
|
||||
const { drawMessage, signatureTranslate } = useBubbleDrawer({
|
||||
mr: messageRect,
|
||||
tr: messageText,
|
||||
});
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
:viewBox="scrollView"
|
||||
>
|
||||
<g id="messageRenderer" transform="translate(0, 5)">
|
||||
<g id="messageVisualizer" transform="translate(0, 5)">
|
||||
<g
|
||||
v-for="(item, index) in renderArr"
|
||||
:key="index"
|
||||
|
|
@ -27,14 +27,18 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import useMessageView from "@/core/messenger/messageView/useMessageView";
|
||||
import useScrollView from "@/core/UI/scrollView/useScrollView";
|
||||
import { defineComponent, reactive, inject } from "vue";
|
||||
import MessageItem from "./messageItem.vue";
|
||||
import useMessageVisualizer from "./viewDetails/visualizer";
|
||||
|
||||
import useScrollView from "@/composables/scrollView";
|
||||
|
||||
import MessageItem from "../messageBubble/index.vue";
|
||||
|
||||
import { Message } from "@/types/message/index";
|
||||
|
||||
import { Mitt } from "@/types/mitt/index";
|
||||
import { defineComponent, reactive, inject } from "vue";
|
||||
export default defineComponent({
|
||||
name: "MessageRenderer",
|
||||
name: "MessageVisualizer",
|
||||
components: { MessageItem },
|
||||
setup() {
|
||||
const renderArr: Message[] = reactive([]);
|
||||
|
|
@ -49,9 +53,9 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
|
||||
const { beforeEnter, enter, afterEnter } = useMessageView();
|
||||
const { beforeEnter, enter, afterEnter } = useMessageVisualizer();
|
||||
const { scrollView } = useScrollView({
|
||||
targetId: "messageRenderer",
|
||||
targetId: "messageVisualizer",
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { inject } from "vue";
|
||||
import AnimeFunc from "@/types/animejs/index";
|
||||
import { MessageAnimeFunc } from '@/types/message/index';
|
||||
|
||||
/**
|
||||
* anime js animations used by message renderer
|
||||
*/
|
||||
export default function useMessageViewAnims(): {
|
||||
shiftAnimate: MessageAnimeFunc;
|
||||
stackAnimate: MessageAnimeFunc;
|
||||
} {
|
||||
// imports animejs safely
|
||||
let anime: AnimeFunc;
|
||||
const animeInject: AnimeFunc | undefined = inject("animejs");
|
||||
if (animeInject) anime = animeInject;
|
||||
|
||||
// shifts message list by a given value
|
||||
function shiftList({ targetList, sv }: { targetList: NodeList | undefined; sv: number | undefined }) {
|
||||
if (targetList) {
|
||||
for (const element of targetList) {
|
||||
const el = element.parentNode as HTMLElement;
|
||||
anime({
|
||||
targets: el,
|
||||
transform: `translate(0 ${sv})`,
|
||||
easing: "easeInOutQuad",
|
||||
duration: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adds new message at the bottom of the list and shifts existing message list
|
||||
const shiftAnimate: MessageAnimeFunc = (el, transform, shiftValue, targetList) => {
|
||||
anime({
|
||||
targets: el,
|
||||
transform: transform.initialTranslate,
|
||||
easing: "easeInOutQuad",
|
||||
duration: 500,
|
||||
begin: function() {
|
||||
shiftList({
|
||||
targetList,
|
||||
sv: shiftValue
|
||||
});
|
||||
},
|
||||
complete: function() {
|
||||
anime({
|
||||
targets: el,
|
||||
transform: transform.finalTranslate,
|
||||
easing: "easeInOutQuad",
|
||||
duration: 500,
|
||||
offset: 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// stacks message from top to bottom
|
||||
const stackAnimate: MessageAnimeFunc = (el, transform) => {
|
||||
anime({
|
||||
targets: el,
|
||||
transform: transform.initialTranslate,
|
||||
easing: "easeInOutQuad",
|
||||
duration: 1000,
|
||||
complete: function() {
|
||||
anime({
|
||||
targets: el,
|
||||
transform: transform.finalTranslate,
|
||||
easing: "easeInOutQuad",
|
||||
duration: 500,
|
||||
offset: 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
shiftAnimate,
|
||||
stackAnimate
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { ref } from "vue";
|
||||
/**
|
||||
* Calculates vertical and horizontal message offsets
|
||||
*/
|
||||
export default function useOffsetCalculator() {
|
||||
const xOffset = ref(0);
|
||||
const yOffset = ref(0);
|
||||
const firstChildMarginTop = 0;
|
||||
let previousMessageHeight = 0;
|
||||
const messagePadding = 15;
|
||||
|
||||
function calculateX(message: HTMLElement): void {
|
||||
const computedQuery = window.getComputedStyle(message);
|
||||
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
|
||||
xOffset.value = matrix.m41;
|
||||
}
|
||||
|
||||
function calculateY(messageHeight: number): void {
|
||||
if (previousMessageHeight === 0) {
|
||||
yOffset.value = firstChildMarginTop;
|
||||
} else {
|
||||
yOffset.value += previousMessageHeight + messagePadding;
|
||||
}
|
||||
previousMessageHeight = messageHeight;
|
||||
}
|
||||
|
||||
const calculateMessageOffsets = (msg: HTMLElement): { x: number; y: number } => {
|
||||
const msgRect = msg.getBoundingClientRect();
|
||||
calculateX(msg);
|
||||
calculateY(msgRect.height);
|
||||
const offsets = {
|
||||
x: xOffset.value as number,
|
||||
y: yOffset.value as number
|
||||
};
|
||||
return offsets;
|
||||
}
|
||||
|
||||
return {
|
||||
calculateMessageOffsets,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
|
||||
import useMessageViewAnims from "./anime";
|
||||
import useOffsetCalculator from './offsetCalculator';
|
||||
|
||||
/**
|
||||
* handles visualization of view messages through
|
||||
* vue transition callbacks and anime-js animations
|
||||
*/
|
||||
export default function useMessageVisualizer(): {
|
||||
beforeEnter: VueTransitionCallback;
|
||||
enter: VueTransitionCallback;
|
||||
afterEnter: VueTransitionCallback;
|
||||
} {
|
||||
// uses offsetCalculator logic and renderer animations
|
||||
const { calculateMessageOffsets } = useOffsetCalculator();
|
||||
const { shiftAnimate, stackAnimate } = useMessageViewAnims();
|
||||
|
||||
const messagePadding = 15;
|
||||
let messageList: NodeList;
|
||||
let message: HTMLElement | null;
|
||||
let messageOffset = { x: 0, y: 0 };
|
||||
let shift = false;
|
||||
let shiftOffset = 0;
|
||||
|
||||
// animates message item to 'enter' renderView
|
||||
const animateMessage = (el: SVGGElement): void => {
|
||||
|
||||
const transform = {
|
||||
initialTranslate: `translate(${messageOffset.x} ${messageOffset.y})`,
|
||||
finalTranslate: `translate(${messageOffset.x} ${messageOffset.y})`
|
||||
}
|
||||
shift ? shiftAnimate(el, transform, shiftOffset, messageList) : stackAnimate(el, transform);
|
||||
}
|
||||
|
||||
const beforeEnter: VueTransitionCallback = () => {
|
||||
const queryResult = document.querySelectorAll(".messageList");
|
||||
if (queryResult.length) messageList = queryResult;
|
||||
}
|
||||
|
||||
const enter: VueTransitionCallback = (el) => {
|
||||
// query DOM for latest message as soon as it enters
|
||||
if (el) {
|
||||
message = document.getElementById(el.id);
|
||||
}
|
||||
}
|
||||
|
||||
const afterEnter: VueTransitionCallback = (el) => {
|
||||
// get message positioning & shift state before animating
|
||||
if (message) {
|
||||
messageOffset = calculateMessageOffsets(message);
|
||||
shift = messageOffset.y > 400 ? true : false;
|
||||
}
|
||||
if (el) {
|
||||
if (shift) {
|
||||
|
||||
console.log(messageOffset.y)
|
||||
if (messageOffset.y < 445) {
|
||||
shiftOffset -= 15;
|
||||
} else {
|
||||
shiftOffset -= el.getBBox().height + messagePadding;
|
||||
}
|
||||
}
|
||||
animateMessage(el);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
beforeEnter,
|
||||
enter,
|
||||
afterEnter
|
||||
};
|
||||
}
|
||||
|
|
@ -158,4 +158,4 @@ const Messages: Message[] = [
|
|||
time: ""
|
||||
},
|
||||
];
|
||||
export default Messages;
|
||||
export default Messages;
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
<script>
|
||||
import { defineComponent, onMounted, ref, inject, watch } from "vue";
|
||||
export default defineComponent({
|
||||
name: "AI",
|
||||
name: "voiceBackground",
|
||||
setup() {
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const analyzer = audioCtx.createAnalyser();
|
||||
Loading…
Reference in a new issue