Merge remote-tracking branch 'origin/project_refactor'
This commit is contained in:
commit
9a4f0fe1b3
36 changed files with 388 additions and 580 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,6 +4,8 @@ node_modules
|
||||||
/crate/target/*
|
/crate/target/*
|
||||||
/.log
|
/.log
|
||||||
|
|
||||||
|
rawAudio.wav
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|
|
||||||
BIN
rawAudio.wav
BIN
rawAudio.wav
Binary file not shown.
|
|
@ -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,68 +0,0 @@
|
||||||
<template>
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
id="Rectangle_478"
|
|
||||||
data-name="Rectangle 478"
|
|
||||||
:width="bubbleWidth"
|
|
||||||
height="34"
|
|
||||||
rx="10"
|
|
||||||
transform="translate(0 0)"
|
|
||||||
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>
|
|
||||||
</svg>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, computed, inject } from "vue";
|
|
||||||
export default defineComponent({
|
|
||||||
name: "AudioInput",
|
|
||||||
props: {
|
|
||||||
bubbleWidth: Number,
|
|
||||||
paths: Array,
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
|
|
||||||
const emitter: any = inject("mitt");
|
|
||||||
function audioSend() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const center = computed(() => {
|
|
||||||
if (props.bubbleWidth) {
|
|
||||||
return 175 - props.bubbleWidth / 2;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { center };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -6,24 +6,26 @@
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
@click="emittMessage"
|
@click="emittMessage"
|
||||||
>
|
>
|
||||||
<!-- <AI /> -->
|
<MessageVisualizer />
|
||||||
<MessageRenderer />
|
<TextAudioInput />
|
||||||
<InputItem />
|
|
||||||
<!-- <TitleBar /> -->
|
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AI from "../AI/index.vue";
|
// mock messages
|
||||||
import MessageRenderer from "./UIDetails/messageRenderer.vue";
|
import Messages from "./mockMessages";
|
||||||
import InputItem from "@/components/input/inputItem.vue";
|
|
||||||
import TitleBar from "./UIDetails/titleBar.vue";
|
// child components
|
||||||
import Messages from "./UIDetails/messages";
|
import MessageVisualizer from "./messageVisualizer/index.vue";
|
||||||
|
import TextAudioInput from "@/components/taInput/index.vue";
|
||||||
|
|
||||||
|
// event emitter
|
||||||
import { Mitt } from "@/types/mitt/index";
|
import { Mitt } from "@/types/mitt/index";
|
||||||
|
|
||||||
import { defineComponent, ref, inject } from "vue";
|
import { defineComponent, ref, inject } from "vue";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "UI",
|
name: "Messenger",
|
||||||
components: { MessageRenderer, InputItem },
|
components: { MessageVisualizer, TextAudioInput },
|
||||||
setup() {
|
setup() {
|
||||||
const messages = Messages;
|
const messages = Messages;
|
||||||
const count = ref(0);
|
const count = ref(0);
|
||||||
|
|
@ -42,9 +44,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
window.ipcRenderer.on("render-message", (event, payload) => {
|
window.ipcRenderer.on("render-message", (event, payload) => {
|
||||||
console.log(payload.message)
|
console.log(payload.message);
|
||||||
emitter.emit("renderMessage", payload.message);
|
emitter.emit("renderMessage", payload.message);
|
||||||
})
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
emittMessage,
|
emittMessage,
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
export default function useMessageItemComp(m: string, id: string) {
|
|
||||||
|
/**
|
||||||
|
* single message svg item computed properties
|
||||||
|
*/
|
||||||
|
export default function useComputedBubble(m: string, id: string) {
|
||||||
const x = ref(0)
|
const x = ref(0)
|
||||||
const paddingLeft = 30;
|
const paddingLeft = 30;
|
||||||
const winW = 350;
|
const winW = 350;
|
||||||
|
|
@ -45,9 +49,9 @@ export default function useMessageItemComp(m: string, id: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const startingOffset = computed(() => {
|
const startingOffset = computed(() => {
|
||||||
const messageRenderer = document.getElementById('messageRenderer');
|
const parentView = document.getElementById('messageVisualizer');
|
||||||
if (messageRenderer) {
|
if (parentView) {
|
||||||
const rect = messageRenderer.getBoundingClientRect();
|
const rect = parentView.getBoundingClientRect();
|
||||||
let Y = 600;
|
let Y = 600;
|
||||||
if (rect.height > 500) {
|
if (rect.height > 500) {
|
||||||
Y = rect.height + messageMarginTop;
|
Y = rect.height + messageMarginTop;
|
||||||
|
|
@ -57,10 +61,7 @@ export default function useMessageItemComp(m: string, id: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// to calculate X offset:
|
|
||||||
// First: set message width ref
|
|
||||||
await setMessageWidthRef();
|
await setMessageWidthRef();
|
||||||
// Then: calculate message X offset
|
|
||||||
calculateXoffset();
|
calculateXoffset();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { Ref } from "@/types/vueRef/index";
|
import { Ref } from "@/types/vueRef/index";
|
||||||
export default function useMessageItemAnims({
|
|
||||||
|
/**
|
||||||
|
* handles visualization of single message svg items
|
||||||
|
*/
|
||||||
|
export default function useBubbleDrawer({
|
||||||
mr,
|
mr,
|
||||||
tr,
|
tr,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -27,10 +27,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 { 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({
|
export default defineComponent({
|
||||||
name: "MessageItem",
|
name: "MessageItem",
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -46,12 +49,12 @@ export default defineComponent({
|
||||||
const messageRect = ref(`message${msgID.value}`);
|
const messageRect = ref(`message${msgID.value}`);
|
||||||
const messageText = ref(`text${msgID.value}`);
|
const messageText = ref(`text${msgID.value}`);
|
||||||
|
|
||||||
const { textFill, bubbleFill, startingOffset } = useMessageItemComp(
|
const { textFill, bubbleFill, startingOffset } = useComputedBubble(
|
||||||
props.item.modifier,
|
props.item.modifier,
|
||||||
props.item.id
|
props.item.id
|
||||||
);
|
);
|
||||||
const { textWrap } = useTextWrap();
|
const { textWrap } = useTextWrap();
|
||||||
const { drawMessage, signatureTranslate } = useMessageItemAnims({
|
const { drawMessage, signatureTranslate } = useBubbleDrawer({
|
||||||
mr: messageRect,
|
mr: messageRect,
|
||||||
tr: messageText,
|
tr: messageText,
|
||||||
});
|
});
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
:viewBox="scrollView"
|
:viewBox="scrollView"
|
||||||
>
|
>
|
||||||
<g id="messageRenderer" transform="translate(0, 5)">
|
<g id="messageVisualizer" transform="translate(0, 5)">
|
||||||
<g
|
<g
|
||||||
v-for="(item, index) in renderArr"
|
v-for="(item, index) in renderArr"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|
@ -27,14 +27,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import useMessageView from "@/core/messenger/messageView/useMessageView";
|
import useMessageVisualizer from "./viewDetails/visualizer";
|
||||||
import useScrollView from "@/core/UI/scrollView/useScrollView";
|
|
||||||
import { defineComponent, reactive, inject } from "vue";
|
import useScrollView from "@/composables/scrollView";
|
||||||
import MessageItem from "./messageItem.vue";
|
|
||||||
|
import MessageItem from "../messageBubble/index.vue";
|
||||||
|
|
||||||
import { Message } from "@/types/message/index";
|
import { Message } from "@/types/message/index";
|
||||||
|
|
||||||
import { Mitt } from "@/types/mitt/index";
|
import { Mitt } from "@/types/mitt/index";
|
||||||
|
import { defineComponent, reactive, inject } from "vue";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "MessageRenderer",
|
name: "MessageVisualizer",
|
||||||
components: { MessageItem },
|
components: { MessageItem },
|
||||||
setup() {
|
setup() {
|
||||||
const renderArr: Message[] = reactive([]);
|
const renderArr: Message[] = reactive([]);
|
||||||
|
|
@ -49,9 +53,9 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { beforeEnter, enter, afterEnter } = useMessageView();
|
const { beforeEnter, enter, afterEnter } = useMessageVisualizer();
|
||||||
const { scrollView } = useScrollView({
|
const { scrollView } = useScrollView({
|
||||||
targetId: "messageRenderer",
|
targetId: "messageVisualizer",
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import useMessageViewAnims from "./viewDetails/useAnimations";
|
|
||||||
import useOffsetCalculator from './viewDetails/useOffsetCalculator';
|
|
||||||
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
|
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
|
||||||
|
import useMessageViewAnims from "./anime";
|
||||||
|
import useOffsetCalculator from './offsetCalculator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* messageView component logic
|
* handles visualization of view messages through
|
||||||
* handles visualization of exisiting user messages
|
* vue transition callbacks and anime-js animations
|
||||||
* as well as incoming messages
|
|
||||||
*/
|
*/
|
||||||
export default function useMessageView(): {
|
export default function useMessageVisualizer(): {
|
||||||
beforeEnter: VueTransitionCallback;
|
beforeEnter: VueTransitionCallback;
|
||||||
enter: VueTransitionCallback;
|
enter: VueTransitionCallback;
|
||||||
afterEnter: VueTransitionCallback;
|
afterEnter: VueTransitionCallback;
|
||||||
|
|
@ -24,9 +22,8 @@ export default function useMessageView(): {
|
||||||
let shift = false;
|
let shift = false;
|
||||||
let shiftOffset = 0;
|
let shiftOffset = 0;
|
||||||
|
|
||||||
|
|
||||||
// animates message item to 'enter' renderView
|
// animates message item to 'enter' renderView
|
||||||
const animateMessage = (el: SVGGElement) => {
|
const animateMessage = (el: SVGGElement): void => {
|
||||||
|
|
||||||
const transform = {
|
const transform = {
|
||||||
initialTranslate: `translate(${messageOffset.x} ${messageOffset.y})`,
|
initialTranslate: `translate(${messageOffset.x} ${messageOffset.y})`,
|
||||||
|
|
@ -35,13 +32,11 @@ export default function useMessageView(): {
|
||||||
shift ? shiftAnimate(el, transform, shiftOffset, messageList) : stackAnimate(el, transform);
|
shift ? shiftAnimate(el, transform, shiftOffset, messageList) : stackAnimate(el, transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback called before message enter
|
|
||||||
const beforeEnter: VueTransitionCallback = () => {
|
const beforeEnter: VueTransitionCallback = () => {
|
||||||
const queryResult = document.querySelectorAll(".messageList");
|
const queryResult = document.querySelectorAll(".messageList");
|
||||||
if (queryResult.length) messageList = queryResult;
|
if (queryResult.length) messageList = queryResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback called on message enter
|
|
||||||
const enter: VueTransitionCallback = (el) => {
|
const enter: VueTransitionCallback = (el) => {
|
||||||
// query DOM for latest message as soon as it enters
|
// query DOM for latest message as soon as it enters
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|
@ -49,7 +44,6 @@ export default function useMessageView(): {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// callback called after message enter
|
|
||||||
const afterEnter: VueTransitionCallback = (el) => {
|
const afterEnter: VueTransitionCallback = (el) => {
|
||||||
// get message positioning & shift state before animating
|
// get message positioning & shift state before animating
|
||||||
if (message) {
|
if (message) {
|
||||||
|
|
@ -158,4 +158,4 @@ const Messages: Message[] = [
|
||||||
time: ""
|
time: ""
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
export default Messages;
|
export default Messages;
|
||||||
65
src/components/taInput/audioDetails/audio.vue
Normal file
65
src/components/taInput/audioDetails/audio.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<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">
|
||||||
|
<rect
|
||||||
|
id="Rectangle_478"
|
||||||
|
data-name="Rectangle 478"
|
||||||
|
:width="bubbleWidth"
|
||||||
|
height="34"
|
||||||
|
rx="10"
|
||||||
|
transform="translate(0 0)"
|
||||||
|
fill="#b9b9b9"
|
||||||
|
/>
|
||||||
|
<svg>
|
||||||
|
<g
|
||||||
|
rx="20"
|
||||||
|
id="Group_125"
|
||||||
|
data-name="Group 125"
|
||||||
|
v-for="(path, index) in paths"
|
||||||
|
:key="path.identifier"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed, inject } from "vue";
|
||||||
|
export default defineComponent({
|
||||||
|
name: "AudioStream",
|
||||||
|
props: {
|
||||||
|
bubbleWidth: Number,
|
||||||
|
paths: Array,
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const emitter: any = inject("mitt");
|
||||||
|
|
||||||
|
const center = computed(() => {
|
||||||
|
if (props.bubbleWidth) {
|
||||||
|
return 175 - props.bubbleWidth / 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { center };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -9,7 +9,7 @@ interface AudioPath {
|
||||||
color: string;
|
color: string;
|
||||||
draw: boolean;
|
draw: boolean;
|
||||||
}
|
}
|
||||||
export default function useStreamVisualizer(draw: Ref<boolean>) {
|
export default function useAudioVisualizer(draw: Ref<boolean>) {
|
||||||
const audioContext = new AudioContext();
|
const audioContext = new AudioContext();
|
||||||
const analyzer = audioContext.createAnalyser();
|
const analyzer = audioContext.createAnalyser();
|
||||||
analyzer.fftSize = 128;
|
analyzer.fftSize = 128;
|
||||||
|
|
@ -24,7 +24,7 @@ export default function useStreamVisualizer(draw: Ref<boolean>) {
|
||||||
/*
|
/*
|
||||||
* Draws MediaStream audioTrack as 'AudioPath' objects
|
* Draws MediaStream audioTrack as 'AudioPath' objects
|
||||||
*/
|
*/
|
||||||
function drawStreamAs(paths: Ref<any>, bubbleWidth: Ref<number>): void {
|
function drawStream(paths: Ref<any>, bubbleWidth: Ref<number>): void {
|
||||||
const xInitial = ref(10);
|
const xInitial = ref(10);
|
||||||
const maxAmplitude = 15;
|
const maxAmplitude = 15;
|
||||||
const maxBubbleWidth = 300;
|
const maxBubbleWidth = 300;
|
||||||
|
|
@ -51,7 +51,7 @@ export default function useStreamVisualizer(draw: Ref<boolean>) {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 0
|
duration: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +77,7 @@ export default function useStreamVisualizer(draw: Ref<boolean>) {
|
||||||
paths.value.push(p);
|
paths.value.push(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
paths.value = [];
|
paths.value = [];
|
||||||
function drawPathStream(): void {
|
function drawPathStream(): void {
|
||||||
|
|
||||||
let streamPath;
|
let streamPath;
|
||||||
|
|
@ -94,11 +94,11 @@ export default function useStreamVisualizer(draw: Ref<boolean>) {
|
||||||
* To Draw pathStream
|
* To Draw pathStream
|
||||||
*/
|
*/
|
||||||
if (bubbleWidth.value === 300) {
|
if (bubbleWidth.value === 300) {
|
||||||
xInitial.value = bubbleWidth.value - 10;
|
xInitial.value = bubbleWidth.value - 10;
|
||||||
} else if (bubbleWidth.value > 20) {
|
} else if (bubbleWidth.value > 20) {
|
||||||
xInitial.value = bubbleWidth.value;
|
xInitial.value = bubbleWidth.value;
|
||||||
} else {
|
} else {
|
||||||
xInitial.value +=3;
|
xInitial.value += 3;
|
||||||
}
|
}
|
||||||
streamPath = setTimeout(async () => {
|
streamPath = setTimeout(async () => {
|
||||||
const pathList = document.querySelectorAll('path');
|
const pathList = document.querySelectorAll('path');
|
||||||
|
|
@ -106,8 +106,8 @@ export default function useStreamVisualizer(draw: Ref<boolean>) {
|
||||||
drawNewPath();
|
drawNewPath();
|
||||||
if (bubbleWidth.value === maxBubbleWidth) {
|
if (bubbleWidth.value === maxBubbleWidth) {
|
||||||
shiftPathStream(pathList);
|
shiftPathStream(pathList);
|
||||||
}
|
}
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
drawPathStream();
|
drawPathStream();
|
||||||
}
|
}
|
||||||
|
|
@ -123,9 +123,9 @@ export default function useStreamVisualizer(draw: Ref<boolean>) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
animation = requestAnimationFrame(increase);
|
animation = requestAnimationFrame(increase);
|
||||||
if (bubbleRef.value < 300) {
|
if (bubbleRef.value < 300) {
|
||||||
bubbleRef.value++;
|
bubbleRef.value++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
increase();
|
increase();
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +137,7 @@ export default function useStreamVisualizer(draw: Ref<boolean>) {
|
||||||
function visualizeStreamAsPaths(s: MediaStream, paths: Ref<any>, bubbleWidth: Ref<number>): void {
|
function visualizeStreamAsPaths(s: MediaStream, paths: Ref<any>, bubbleWidth: Ref<number>): void {
|
||||||
const source = audioContext.createMediaStreamSource(s);
|
const source = audioContext.createMediaStreamSource(s);
|
||||||
source.connect(analyzer);
|
source.connect(analyzer);
|
||||||
drawStreamAs(paths, bubbleWidth);
|
drawStream(paths, bubbleWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<audio-input v-if="visualizeStream" :bubbleWidth="audioBubbleWidth" :paths="paths" />
|
<audio-stream v-if="visualizeStream" :bubbleWidth="audioBubbleWidth" :paths="paths" />
|
||||||
<text-input v-else :input="input" :textXoffset="textInputXoffset" />
|
<text-input v-else :input="input" :textXoffset="textInputXoffset" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
// input child components
|
||||||
|
import TextInput from "./textDetails/text.vue";
|
||||||
|
import AudioStream from "./audioDetails/audio.vue";
|
||||||
|
|
||||||
|
import useInputController from "./inputController";
|
||||||
|
|
||||||
import { defineComponent } from "vue";
|
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({
|
export default defineComponent({
|
||||||
name: "InputItem",
|
name: "TextAudioInput",
|
||||||
components: { TextInput, AudioInput },
|
components: { TextInput, AudioStream },
|
||||||
setup() {
|
setup() {
|
||||||
|
// get text input
|
||||||
|
|
||||||
|
// get audio stream
|
||||||
|
|
||||||
|
// use Input Handler
|
||||||
|
|
||||||
const {
|
const {
|
||||||
input,
|
input,
|
||||||
visualizeStream,
|
visualizeStream,
|
||||||
93
src/components/taInput/inputController.ts
Normal file
93
src/components/taInput/inputController.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { ref, watch, onUnmounted, onMounted } from "vue";
|
||||||
|
|
||||||
|
import useKeyDownHandler from "@/composables/keyDownHandler/useKeyDownHandler";
|
||||||
|
|
||||||
|
import useMediaStream from "@/composables/mediaStream";
|
||||||
|
|
||||||
|
import useAudioVisualizer from './audioDetails/audioVisualizer';
|
||||||
|
|
||||||
|
import useTextVisualizer from './textDetails/textVisualizer';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Controls which input to visualize
|
||||||
|
* 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
|
||||||
|
// TODO: visualize audio stream from nodeJS background process
|
||||||
|
export default function useInputController() {
|
||||||
|
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 } = useAudioVisualizer(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({ audio: true });
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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,11 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<foreignObject
|
<foreignObject class="inputContainer" :x="textXoffset" y="450" width="55%" height="50%">
|
||||||
class="inputContainer"
|
|
||||||
:x="textXoffset"
|
|
||||||
y="450"
|
|
||||||
width="55%"
|
|
||||||
height="50%"
|
|
||||||
>
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml">
|
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||||
<div class="inputBubble" contenteditable v-show="input.length > 0">
|
<div class="inputBubble" contenteditable v-show="input.length > 0">
|
||||||
<p>{{ input }}</p>
|
<p>{{ input }}</p>
|
||||||
|
|
@ -15,12 +9,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, watch, onMounted, ref } from "vue";
|
import { defineComponent } from "vue";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "TextInput",
|
name: "TextInput",
|
||||||
props: {
|
props: {
|
||||||
input: String,
|
input: String,
|
||||||
textXoffset: String
|
textXoffset: String,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
57
src/components/taInput/textDetails/textAnime.ts
Normal file
57
src/components/taInput/textDetails/textAnime.ts
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
src/components/taInput/textDetails/textVisualizer.ts
Normal file
49
src/components/taInput/textDetails/textVisualizer.ts
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { defineComponent, onMounted, ref, inject, watch } from "vue";
|
import { defineComponent, onMounted, ref, inject, watch } from "vue";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "AI",
|
name: "voiceBackground",
|
||||||
setup() {
|
setup() {
|
||||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
const analyzer = audioCtx.createAnalyser();
|
const analyzer = audioCtx.createAnalyser();
|
||||||
13
src/composables/mediaStream.ts
Normal file
13
src/composables/mediaStream.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default async function useMediaStream(constraints: {
|
||||||
|
audio: boolean;
|
||||||
|
video?: boolean;
|
||||||
|
}): Promise<MediaStream> {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
return stream;
|
||||||
|
} catch (e) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
// var audioTack = stream.getAudioTracks()[0];
|
||||||
|
// console.log(audioTrack.getSettings())
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handles visualization of view messages through
|
||||||
|
* vue transition callbacks and anime-js animations
|
||||||
|
*/
|
||||||
export default function useScrollView({ targetId }: { targetId: string }) {
|
export default function useScrollView({ targetId }: { targetId: string }) {
|
||||||
|
|
||||||
const scroll = ref(0);
|
const scroll = ref(0);
|
||||||
let scrollTarget: HTMLElement | SVGElement | null;
|
let scrollTarget: HTMLElement | SVGElement | null;
|
||||||
const targetHeight = ref(0);
|
const targetHeight = ref(0);
|
||||||
|
|
@ -27,7 +33,7 @@ export default function useScrollView({ targetId }: { targetId: string }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScrollEvent = (verticalScroll: number) => {
|
const updateView = (verticalScroll: number) => {
|
||||||
if (scrollTarget) {
|
if (scrollTarget) {
|
||||||
const heightResult = getHeight();
|
const heightResult = getHeight();
|
||||||
if (heightResult) {
|
if (heightResult) {
|
||||||
|
|
@ -40,16 +46,22 @@ export default function useScrollView({ targetId }: { targetId: string }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scrollUpdate = (e: any): void => {
|
||||||
|
const verticalScroll = e.deltaY * 0.25;
|
||||||
|
updateView(verticalScroll);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
scrollTarget = document.getElementById(targetId);
|
scrollTarget = document.getElementById(targetId);
|
||||||
if (scrollTarget) {
|
if (scrollTarget) {
|
||||||
window.addEventListener("wheel", (e) => {
|
window.addEventListener("wheel", scrollUpdate);
|
||||||
const verticalScroll = e.deltaY * 0.25;
|
|
||||||
handleScrollEvent(verticalScroll);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("wheel", scrollUpdate);
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrollView
|
scrollView
|
||||||
}
|
}
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import AnimeFunc from "@/types/animejs/index";
|
|
||||||
import {inject} from "vue";
|
|
||||||
import {Ref} from "@/types/vueRef/index";
|
|
||||||
|
|
||||||
export default function useAudioAnimations() {
|
|
||||||
// imports animejs safely
|
|
||||||
let anime: AnimeFunc;
|
|
||||||
const animeInject: AnimeFunc | undefined = inject("animejs");
|
|
||||||
if (animeInject) anime = animeInject;
|
|
||||||
|
|
||||||
function animateSend(el: Element, trigger: Ref<boolean>) {
|
|
||||||
console.log(el);
|
|
||||||
anime({
|
|
||||||
targets: el,
|
|
||||||
opacity: 0,
|
|
||||||
translateY: -100,
|
|
||||||
duration: 0,
|
|
||||||
complete: function() {
|
|
||||||
trigger.value = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
animateSend
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
|
|
||||||
interface Window {
|
|
||||||
AudioContext: any;
|
|
||||||
webkitAudioContext: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Event {
|
|
||||||
inputBuffer: AudioBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
import { Ref } from '@/types/vueRef/index';
|
|
||||||
import { watch } from 'vue';
|
|
||||||
|
|
||||||
export default function useAudioRecorder(record: Ref<boolean>, stream: MediaStream) {
|
|
||||||
|
|
||||||
let audioArray: Int16Array[] = [];
|
|
||||||
const bufferLen = 4096;
|
|
||||||
const numChannels = 1;
|
|
||||||
|
|
||||||
//window.AudioContext = window.AudioContext;
|
|
||||||
const audioContext = new AudioContext({ sampleRate: 16000 });
|
|
||||||
|
|
||||||
const source = audioContext.createMediaStreamSource(stream);
|
|
||||||
const context = source.context;
|
|
||||||
const sampleRate = context.sampleRate;
|
|
||||||
|
|
||||||
const node = context.createScriptProcessor.call(
|
|
||||||
context,
|
|
||||||
bufferLen,
|
|
||||||
numChannels,
|
|
||||||
numChannels
|
|
||||||
);
|
|
||||||
|
|
||||||
node.onaudioprocess = (e: Event) => {
|
|
||||||
const inputBuffer = new Float32Array(bufferLen);
|
|
||||||
|
|
||||||
e.inputBuffer.copyFromChannel(inputBuffer, 0);
|
|
||||||
const int16 = new Int16Array(inputBuffer.buffer);
|
|
||||||
audioArray.push(int16);
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(record, (beginRecording) => {
|
|
||||||
if (beginRecording) {
|
|
||||||
node.connect(context.destination);
|
|
||||||
} else {
|
|
||||||
node.disconnect(context.destination);
|
|
||||||
|
|
||||||
const message = {
|
|
||||||
text: '',
|
|
||||||
audio: {
|
|
||||||
sampleRate: sampleRate,
|
|
||||||
channelCount: numChannels,
|
|
||||||
audio: audioArray
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.postMessage({
|
|
||||||
myTypeField: 'send-message',
|
|
||||||
message: message
|
|
||||||
}, '*')
|
|
||||||
|
|
||||||
audioArray = [];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
source.connect(node);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import { inject } from 'vue';
|
|
||||||
export default function useStreamRecord(): {
|
|
||||||
initRecorder: (stream: MediaStream) => MediaRecorder;
|
|
||||||
} {
|
|
||||||
let streamRecorder: MediaRecorder;
|
|
||||||
const chunks: Blob[] = [];
|
|
||||||
const emitter: any = inject("mitt");
|
|
||||||
const options = {
|
|
||||||
mimeType: "audio/webm; codecs=opus",
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('ogg supported',MediaRecorder.isTypeSupported('audio/mpeg; '))
|
|
||||||
const initRecorder = (stream: MediaStream) => {
|
|
||||||
|
|
||||||
const audioTrack = stream.getAudioTracks()[0];
|
|
||||||
const audioSettings = audioTrack.getSettings()
|
|
||||||
|
|
||||||
streamRecorder = new MediaRecorder(stream, options);
|
|
||||||
streamRecorder.ondataavailable = (e: any) => {
|
|
||||||
if (e.data.size > 0) {
|
|
||||||
chunks.push(e.data);
|
|
||||||
} else {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
//chunks.push(e.data)
|
|
||||||
}
|
|
||||||
streamRecorder.onstop = async (e: any) => {
|
|
||||||
const audioBlob = new Blob(chunks, {
|
|
||||||
type: options.mimeType
|
|
||||||
});
|
|
||||||
const arrayBuffer = await audioBlob.arrayBuffer()
|
|
||||||
console.log(arrayBuffer)
|
|
||||||
const message = {
|
|
||||||
text: '',
|
|
||||||
audio: {
|
|
||||||
sampleRate: audioSettings.sampleRate,
|
|
||||||
sampleSize: audioSettings.sampleSize,
|
|
||||||
channelCount: audioSettings.channelCount,
|
|
||||||
mimeType: options.mimeType,
|
|
||||||
arrayBuffer: arrayBuffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.postMessage({
|
|
||||||
myTypeField: 'send-message',
|
|
||||||
message: message
|
|
||||||
}, '*')
|
|
||||||
|
|
||||||
}
|
|
||||||
return streamRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
initRecorder,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import { ref, computed, inject } from 'vue';
|
|
||||||
import useTextInputAnims from "./textDetails/useAnimations";
|
|
||||||
import { Ref } from "@/types/vueRef/index";
|
|
||||||
/*
|
|
||||||
* text rendering logic used by inputController
|
|
||||||
*/
|
|
||||||
export default function useTextRender(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
import { ref, watch, onUnmounted, onMounted } from "vue";
|
|
||||||
import useKeyDownHandler from "@/utils/keyDownHandler/useKeyDownHandler";
|
|
||||||
import useMediaStream from "@/utils/mediaStream/useMediaStream";
|
|
||||||
import useStreamRecord from "./audio/useStreamRecord";
|
|
||||||
import useStreamVisualizer from './audio/useStreamVisualizer';
|
|
||||||
import useTextRender from './text/useTextRender';
|
|
||||||
import useAudioAnimations from './audio/audioDetails/useAnimations';
|
|
||||||
import useAudioRecorder from './audio/useAudioRecorder';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Input controller logic
|
|
||||||
* 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 useInputController() {
|
|
||||||
const visualizeStream = ref(false);
|
|
||||||
const paths = ref([]);
|
|
||||||
const audioBubbleWidth = ref(10);
|
|
||||||
let stream: MediaStream;
|
|
||||||
let recorder: MediaRecorder;
|
|
||||||
|
|
||||||
const { keyDownHandler, input } = useKeyDownHandler();
|
|
||||||
const { textInputXoffset, renderTextInput } = useTextRender(input);
|
|
||||||
const { visualizeStreamAsPaths, expandBubble } = useStreamVisualizer(visualizeStream);
|
|
||||||
const { initRecorder } = useStreamRecord()
|
|
||||||
const { animateSend } = useAudioAnimations();
|
|
||||||
|
|
||||||
function audioSend() {
|
|
||||||
const domQuery = document.getElementsByClassName("audioInput");
|
|
||||||
const inputEl = domQuery[0];
|
|
||||||
animateSend(inputEl, 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();
|
|
||||||
// useAudioRecorder(visualizeStream, stream)
|
|
||||||
recorder = initRecorder(stream);
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "Home",
|
name: "Home",
|
||||||
component: HomeIndex
|
component: HomeIndex
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export default async function useMediaStream() {
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
// var audioTrack = stream.getAudioTracks()[0];
|
|
||||||
// console.log(audioTrack.getSettings())
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<UI />
|
<Messenger />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import UI from "@/components/UI/index.vue";
|
import Messenger from "@/components/messenger/index.vue";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "HomeIndex",
|
name: "HomeIndex",
|
||||||
components: { UI },
|
components: { Messenger },
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue