Merge branch 'core' into 'master'

refactor composables into core directory

See merge request crimata-ai/crimata-electron!16
This commit is contained in:
Enrique Hernandez 2020-12-15 21:46:30 +00:00
commit 52f6893c50
25 changed files with 353 additions and 332 deletions

View file

@ -28,9 +28,9 @@
<script lang="ts">
import { defineComponent, onMounted, ref, reactive } from "vue";
import useMessageItemComp from "@/composables/messageItem/useMessageItemComp";
import useMessageItemAnims from "@/composables/messageItem/useMessageItemAnims";
import useTextWrap from "@/composables/textWrap/useTextWrap";
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: {

View file

@ -28,8 +28,8 @@
</template>
<script lang="ts">
import useMessageRenderer from "@/composables/messageRenderer/useMessageRenderer";
import useScrollView from "@/composables/scrollView/useScrollView";
import useRenderView from '@/core/messenger/renderView/useRenderView';
import useScrollView from "@/core/UI/scrollView/useScrollView";
import {
defineComponent,
reactive,
@ -54,8 +54,10 @@ export default defineComponent({
});
}
const { beforeEnter, enter, afterEnter } = useMessageRenderer();
const {scrollView} = useScrollView({ targetId: "messageRenderer"});
const { beforeEnter, enter, afterEnter } = useRenderView();
const { scrollView } = useScrollView({
targetId: "messageRenderer"
});
return {
renderArr,

View file

@ -12,10 +12,10 @@
</template>
<script lang="ts">
import {defineComponent} from 'vue'
import { defineComponent } from 'vue'
import TextInput from './inputDetails/textInput.vue';
import AudioInput from './inputDetails/audioInput.vue';
import useInputRenderer from "@/composables/inputItem/useInputRenderer";
import useInputController from "@/core/UI/input/useInputController";
export default defineComponent({
name: "InputItem",
components: {TextInput, AudioInput},
@ -26,7 +26,7 @@ export default defineComponent({
renderAudio,
reactiveWidth,
textInputXoffset,
paths } = useInputRenderer();
paths } = useInputController();
return {
input,

View file

@ -1,38 +0,0 @@
export default function useAudioRenderer(stream: MediaStream) {
const audioCtx = new (window.AudioContext)();
const analyzer = audioCtx.createAnalyser();
const source = audioCtx.createMediaStreamSource(stream);
source.connect(analyzer);
analyzer.fftSize = 2048;
const xInitial = 24;
const xFinal = 640;
analyzer.minDecibels = -90;
analyzer.smoothingTimeConstant = 0.85;
const dataArray = new Uint8Array(analyzer.frequencyBinCount);
// const sampleRate = 16000;
const renderAudio = (audio: Float32Array, sampleRate: number) => {
// step 2: source audio frequency {ffft} data
const buffer = audioCtx.createBuffer(1, audio.length, sampleRate);
buffer.copyToChannel(audio, 0, 0);
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(analyzer);
source.start();
analyzer.getByteFrequencyData(dataArray);
// step 3: process audio frequency data
const dataView = dataArray.slice(xInitial, xFinal);
const sum = dataView.reduce((a, b) => a + b);
const average = sum / dataView.length;
const scaled = average / 255;
const zeroed = scaled - 0.6;
return average;
}
return {
renderAudio
}
}

View file

@ -1,75 +0,0 @@
import {ref, watch, onUnmounted, onMounted } from "vue";
import useKeyDownHandler from "@/composables/keyDownHandler/useKeyDownHandler";
import useMediaStream from "@/composables/mediaStream/useMediaStream";
import useStreamRecorder from "@/composables/streamRecorder/useStreamRecorder";
import useTextInput from '@/composables/inputItem/useTextInput';
/*
* Renders input to the user
*/
export default function useInputRenderer() {
// program ref varaiables
const renderAudio = ref(false);
const reactiveWidth = ref(10);
const pathOffset = ref(10);
const paths = ref([]);
// get input feedback & handler function
const { keyDownHandler, input } = useKeyDownHandler();
// get text Offset and & textRender function
const { textInputXoffset, renderTextInput } = useTextInput(input, renderAudio);
// why do we have audio stuff here ?
let stream: MediaStream;
const {renderStream} = useStreamRecorder(renderAudio)
onMounted(async () => {
const streamRes = await useMediaStream()
stream = streamRes.stream;
})
// stop stream recording on space keyup event
function keyupHandler(e: any) {
if (e.key === " " && renderAudio.value) {
console.log("stop recording");
renderAudio.value = false;
input.value = "";
reactiveWidth.value = 10;
pathOffset.value = 10;
}
}
// add window event listener
window.addEventListener("keydown", keyDownHandler);
window.addEventListener('keyup', keyupHandler);
// remove Event Listeners on component unMount
onUnmounted(() => {
window.removeEventListener("keydown", keyDownHandler);
window.removeEventListener("keyup", keyupHandler);
});
// determine whether to render text or audio based one the keyboard input
watch(input, async (input, prevInput) => {
if (prevInput !== "" || prevInput.length > 0) {
if (!renderAudio.value) {
renderTextInput();
}
return;
}
if (input === " " && renderAudio.value === false) {
console.log("record");
renderAudio.value = true;
renderStream(stream, reactiveWidth, pathOffset, paths);
return;
}
renderTextInput();
});
return {
input,
renderAudio,
reactiveWidth,
textInputXoffset,
paths
}
}

View file

@ -1,46 +0,0 @@
import { ref, onMounted } from "vue";
import {Ref} from "@/types/vueRef/index";
export default function useMessageItemAnims({
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,
};
}

View file

@ -1,73 +0,0 @@
import { computed, onMounted, ref } from "vue";
export default function useMessageItemComp(m: string, id: string) {
const x = ref(0)
const paddingLeft = 30;
const winW = 350;
const messageWidth = ref(0);
const messageMarginTop = 75;
const test = ref("");
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 messageRenderer = document.getElementById('messageRenderer');
if (messageRenderer) {
const rect = messageRenderer.getBoundingClientRect();
let Y = 600;
if (rect.height > 500) {
Y = rect.height + messageMarginTop;
}
return `translate(${x.value} ${Y})`;
}
});
onMounted(async () => {
// to calculate X offset:
// First: set message width ref
await setMessageWidthRef();
// Then: calculate message X offset
calculateXoffset();
})
return {
textFill,
bubbleFill,
startingOffset
};
}

View file

@ -1,49 +0,0 @@
import useRendererAnims from "./useRendererAnims";
import useOffsetCalculator from './useOffsetCalculator';
export default function useMessageRenderer() {
const { calculateMessageOffsets } = useOffsetCalculator();
const { shiftAnimate, stackAnimate } = useRendererAnims();
let messageList: NodeList;
let message: HTMLElement | null;
let messageOffset = { x: 0, y: 0 };
let shift = false;
let shiftValue = 0;
function animateMessage(el: SVGGElement): void {
const transform = {
initialTranslate: `translate(${messageOffset.x} ${messageOffset.y})`,
finalTranslate: `translate(${messageOffset.x} ${messageOffset.y})`
}
shift ? shiftAnimate(el, transform, shiftValue, messageList) : stackAnimate(el, transform);
}
function beforeEnter(el: SVGGElement): void {
const queryResult = document.querySelectorAll(".messageList");
if (queryResult.length) messageList = queryResult;
}
function enter(el: SVGGElement): void {
message = document.getElementById(el.id);
}
function afterEnter(el: SVGGElement) {
const messagePadding = 15;
if (message) {
messageOffset = calculateMessageOffsets(message);
shift = messageOffset.y > 445 ? true : false;
}
if (shift) {
shiftValue -= el.getBBox().height + messagePadding;
}
animateMessage(el);
}
return {
beforeEnter,
enter,
afterEnter
};
}

View file

@ -0,0 +1,20 @@
export default function useStreamRecord(): {
initRecorder: (stream: MediaStream) => MediaRecorder;
} {
let streamRecorder: MediaRecorder;
const chunks: Blob[] = [];
const initRecorder = (stream: MediaStream) => {
streamRecorder = new MediaRecorder(stream);
streamRecorder.ondataavailable = (e: any) => { chunks.push(e.data) }
streamRecorder.onstop = (e: any) => {
// send audio to BE
const audioBlob = new Blob(chunks, { 'type': 'audio/ogg; codecs=opus' });
}
return streamRecorder
}
return {
initRecorder,
}
}

View file

@ -1,28 +1,14 @@
import {Ref} from '@/types/vueRef/index';
import { ref } from 'vue';
export default function useStreamRecorder(render: Ref<boolean>) {
let streamRecorder: MediaRecorder;
const chunks: Blob[] = [];
export default function useStreamRender(render: Ref<boolean>) {
// window.AudioContext = window.AudioContext;
const audioContext = new AudioContext();
const analyzer = audioContext.createAnalyser();
analyzer.fftSize = 2048;
const xInitial = 24;
const xFinal = 640;
const dataArray = new Uint8Array(analyzer.frequencyBinCount);
const initRecorder = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
streamRecorder = new MediaRecorder(stream);
streamRecorder.ondataavailable = (e: any) => { chunks.push(e.data) }
streamRecorder.onstop = (e: any) => {
// send audio to BE
const audioBlob = new Blob(chunks, { 'type': 'audio/ogg; codecs=opus' });
}
return {
streamRecorder
}
}
interface AmplitudePath {
id: string;
name: string;
@ -41,8 +27,6 @@ export default function useStreamRecorder(render: Ref<boolean>) {
return path;
}
const xInitial = 24;
const xFinal = 640;
function visualize(w: Ref<number>, p: Ref<number>, paths: Ref<any>) {
@ -92,7 +76,6 @@ export default function useStreamRecorder(render: Ref<boolean>) {
}
return {
initRecorder,
renderStream
}
}

View file

@ -1,7 +1,10 @@
import AnimeFunc from "@/types/animejs/index";
import {inject} from "vue";
import {Ref} from "@/types/vueRef/index";
export default function useInputAnims() {
/*
* Anime js animations used by input textController
*/
export default function useTextInputAnims() {
// imports animejs safely
let anime: AnimeFunc;

View file

@ -1,10 +1,13 @@
import {ref, computed, watch, inject} from 'vue';
import useInputAnims from "./useInputAnims";
import useInputRenderer from './useInputRenderer';
export default function useTextInput(input: any, audioRender: any) {
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 emitter: any = inject("mitt");
const {animateSend, verticalShiftInput, inputAppear} = useInputAnims();
const {animateSend, verticalShiftInput, inputAppear} = useTextInputAnims();
const inputHeight = ref(42);
const inputWidth = ref(0);

View file

@ -0,0 +1,88 @@
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 useStreamRender from './audio/useStreamRender';
import useTextRender from './text/useTextRender';
/*
* Input controller logic
* Will render text input by default as user types or
* if user holds space bar down, audio media-stream
* will be rendered & recorded
*/
export default function useInputController() {
// program ref varaiables
const renderAudio = ref(false);
const reactiveWidth = ref(10);
const pathOffset = ref(10);
const paths = ref([]);
// audioStream variables
let stream: MediaStream;
let recorder: MediaRecorder;
// use input ref & keydown handler function
const { keyDownHandler, input } = useKeyDownHandler();
// use text Offset computed prop and & textRender function
const { textInputXoffset, renderTextInput } = useTextRender(input);
// use stream render and record logic
const { renderStream } = useStreamRender(renderAudio);
const { initRecorder } = useStreamRecord()
// get stream & initialize recorder object on mount
onMounted(async () => {
stream = await useMediaStream();
recorder = initRecorder(stream);
})
// callback function called on key up event
function keyupHandler(e: any) {
// stop stream recording on space key up
if (e.key === " " && renderAudio.value) {
console.log("stop recording");
recorder.stop();
// reset controller ref variables
renderAudio.value = false;
input.value = "";
reactiveWidth.value = 10;
pathOffset.value = 10;
}
}
// step 1: add window event keyboard listeners
window.addEventListener("keydown", keyDownHandler);
window.addEventListener('keyup', keyupHandler);
// step 2: determine whether to render text or audio based one the keyboard input
watch(input, (input, prevInput) => {
if (prevInput !== "" || prevInput.length > 0) {
if (!renderAudio.value) {
renderTextInput();
}
return;
}
if (input === " " && renderAudio.value === false) {
console.log("record");
renderAudio.value = true;
recorder.start();
renderStream(stream, reactiveWidth, pathOffset, paths);
return;
}
renderTextInput();
});
// remove Event Listeners on component unMount
onUnmounted(() => {
window.removeEventListener("keydown", keyDownHandler);
window.removeEventListener("keyup", keyupHandler);
});
return {
input,
renderAudio,
reactiveWidth,
textInputXoffset,
paths
}
}

View file

@ -1,6 +1,5 @@
import {ref, computed, onMounted} from "vue";
export default function useScrollView({ targetId }: {targetId: string}) {
const scroll = ref(0);
let scrollTarget: HTMLElement | SVGElement | null;
const targetHeight = ref(0);

View file

@ -0,0 +1,46 @@
import { ref, onMounted } from "vue";
import { Ref } from "@/types/vueRef/index";
export default function useMessageItemAnims({
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,
};
}

View file

@ -0,0 +1,72 @@
import { computed, onMounted, ref } from "vue";
export default function useMessageItemComp(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 messageRenderer = document.getElementById('messageRenderer');
if (messageRenderer) {
const rect = messageRenderer.getBoundingClientRect();
let Y = 600;
if (rect.height > 500) {
Y = rect.height + messageMarginTop;
}
return `translate(${x.value} ${Y})`;
}
});
onMounted(async () => {
// to calculate X offset:
// First: set message width ref
await setMessageWidthRef();
// Then: calculate message X offset
calculateXoffset();
})
return {
textFill,
bubbleFill,
startingOffset
};
}

View file

@ -1,12 +1,20 @@
import { inject } from "vue";
import AnimeFunc from "@/types/animejs/index";
import { MessageAnimeFunc } from '@/types/message/index';
export default function useRendererAnims() {
/**
* anime js animations used by message renderer
*/
export default function useRendererAnims(): {
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) {
@ -21,9 +29,10 @@ export default function useRendererAnims() {
}
}
// adds new message at the bottom of the list and shifts existing message list
const shiftAnimate: MessageAnimeFunc = (el, transform, shiftValue, targetList) => {
anime({
targets: el,
targets: el,
transform: transform.initialTranslate,
easing: "easeInOutQuad",
duration: 500,
@ -45,6 +54,7 @@ export default function useRendererAnims() {
});
}
// stacks message from top to bottom
const stackAnimate: MessageAnimeFunc = (el, transform) => {
anime({
targets: el,

View file

@ -1,4 +1,7 @@
import { ref } from "vue";
/**
* Calculates vertical and horizontal message offsets
*/
export default function useOffsetCalculator() {
const xOffset = ref(0);
const yOffset = ref(0);
@ -6,13 +9,13 @@ export default function useOffsetCalculator() {
let previousMessageHeight = 0;
const messagePadding = 15;
function setXoffset(message: HTMLElement): void {
const computedQuery = window.getComputedStyle(message)
function calculateX(message: HTMLElement): void {
const computedQuery = window.getComputedStyle(message);
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
xOffset.value = matrix.m41;
}
function setYoffset(messageHeight: number): void {
function calculateY(messageHeight: number): void {
if (previousMessageHeight === 0) {
yOffset.value = firstChildMarginTop;
} else {
@ -21,10 +24,10 @@ export default function useOffsetCalculator() {
previousMessageHeight = messageHeight;
}
const calculateMessageOffsets = (msg: HTMLElement) => {
const calculateMessageOffsets = (msg: HTMLElement): { x: number; y: number } => {
const msgRect = msg.getBoundingClientRect();
setXoffset(msg);
setYoffset(msgRect.height);
calculateX(msg);
calculateY(msgRect.height);
const offsets = {
x: xOffset.value as number,
y: yOffset.value as number

View file

@ -0,0 +1,72 @@
import useRendererAnims from "./renderDetails/useAnimations";
import useOffsetCalculator from './renderDetails/useOffsetCalculator';
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
/**
* renderView component logic
* handles rendering of exisiting user messages
* as well as incoming messages
*/
export default function useRenderView(): {
beforeEnter: VueTransitionCallback;
enter: VueTransitionCallback;
afterEnter: VueTransitionCallback;
} {
// uses offsetCalculator logic and renderer animations
const { calculateMessageOffsets } = useOffsetCalculator();
const { shiftAnimate, stackAnimate } = useRendererAnims();
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) => {
const transform = {
initialTranslate: `translate(${messageOffset.x} ${messageOffset.y})`,
finalTranslate: `translate(${messageOffset.x} ${messageOffset.y})`
}
shift ? shiftAnimate(el, transform, shiftOffset, messageList) : stackAnimate(el, transform);
}
// callback called before message enter
const beforeEnter: VueTransitionCallback = () => {
const queryResult = document.querySelectorAll(".messageList");
if (queryResult.length) messageList = queryResult;
}
// callback called on message enter
const enter: VueTransitionCallback = (el) => {
// query DOM for latest message as soon as it enters
if (el) {
message = document.getElementById(el.id);
}
}
// callback called after message enter
const afterEnter: VueTransitionCallback = (el) => {
// get message positioning & shift state before animating
if (message) {
messageOffset = calculateMessageOffsets(message);
shift = messageOffset.y > 445 ? true : false;
}
if (el) {
if (shift) {
shiftOffset -= el.getBBox().height + messagePadding;
}
animateMessage(el);
}
}
return {
beforeEnter,
enter,
afterEnter
};
}

View file

@ -0,0 +1,3 @@
export type VueTransitionCallback = {
(el?: SVGGElement | null): void;
}

View file

@ -1,6 +1,6 @@
import { ref, inject } from "vue";
import keyboardNameMap from "./keyboardMaps/keyboardNameMap";
import keyboardCharMap from "./keyboardMaps/keyboardCharMap";
import keyboardNameMap from "./keyBoardMaps/keyboardNameMap";
import keyboardCharMap from "./keyBoardMaps/keyboardCharMap";
// This catches keys from a physical keyboard, a soft keyboard on a tablet, or a
// scanner attached via USB
export default function useKeyDownHandler() {
@ -65,4 +65,4 @@ export default function useKeyDownHandler() {
keyDownHandler,
input
};
}
}

View file

@ -1,6 +1,4 @@
export default async function useMediaStream() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
return {
stream
}
return stream;
}