Merge remote-tracking branch 'origin/project_refactor'

This commit is contained in:
Enrique Hernandez 2021-01-31 17:47:06 -06:00
commit 9a4f0fe1b3
36 changed files with 388 additions and 580 deletions

2
.gitignore vendored
View file

@ -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

Binary file not shown.

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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();
}) })

View file

@ -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,
}: { }: {

View file

@ -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,
}); });

View file

@ -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 {

View file

@ -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) {

View file

@ -158,4 +158,4 @@ const Messages: Message[] = [
time: "" time: ""
}, },
]; ];
export default Messages; export default Messages;

View 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>

View file

@ -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 {

View file

@ -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,

View 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
}
}

View file

@ -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>

View 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
}
}

View 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
}
}

View file

@ -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();

View 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())
}

View file

@ -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
} }

View file

@ -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
}
}

View file

@ -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);
}

View file

@ -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,
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -6,7 +6,7 @@ const routes: Array<RouteRecordRaw> = [
path: "/", path: "/",
name: "Home", name: "Home",
component: HomeIndex component: HomeIndex
} },
]; ];
const router = createRouter({ const router = createRouter({

View file

@ -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;
}

View file

@ -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>