diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a06a8c6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "Vue.volar", + "vitest.explorer", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "esbenp.prettier-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..df9b23b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,103 @@ +{ + // https://github.com/tailwindlabs/tailwindcss/discussions/5258#discussioncomment-1979394 + "css.customData": [ + ".vscode/tailwind.json" + ], + // Disable the default formatter, use eslint instead + "prettier.enable": false, + "editor.formatOnSave": false, + // Auto fix + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" + }, + // Silent the stylistic rules in your IDE, but still auto fix them + "eslint.rules.customizations": [ + { + "rule": "style/*", + "severity": "off", + "fixable": true + }, + { + "rule": "format/*", + "severity": "off", + "fixable": true + }, + { + "rule": "*-indent", + "severity": "off", + "fixable": true + }, + { + "rule": "*-spacing", + "severity": "off", + "fixable": true + }, + { + "rule": "*-spaces", + "severity": "off", + "fixable": true + }, + { + "rule": "*-order", + "severity": "off", + "fixable": true + }, + { + "rule": "*-dangle", + "severity": "off", + "fixable": true + }, + { + "rule": "*-newline", + "severity": "off", + "fixable": true + }, + { + "rule": "*quotes", + "severity": "off", + "fixable": true + }, + { + "rule": "*semi", + "severity": "off", + "fixable": true + } + ], + // Enable eslint for all supported languages + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + "html", + "markdown", + "json", + "jsonc", + "yaml", + "toml", + "xml", + "gql", + "graphql", + "astro", + "svelte", + "css", + "less", + "scss", + "pcss", + "postcss" + ], + "workspaceKeybindings.manimPreviewTask.enabled": true, + "typescript.format.enable": false, + "typescript.tsdk": "./Frontend/node_modules/typescript/lib", + "[vue]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[typescript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[javascript]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + } +} \ No newline at end of file diff --git a/.vscode/tailwind.json b/.vscode/tailwind.json new file mode 100644 index 0000000..53078b1 --- /dev/null +++ b/.vscode/tailwind.json @@ -0,0 +1,95 @@ +{ + "version": 4.0, + "atDirectives": [ + { + "name": "@theme", + "description": "Use the `@theme` directive to define your project's custom design tokens, like fonts, colors, and breakpoints.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#theme-directive" + } + ] + }, + { + "name": "@source", + "description": "Use the `@source` directive to explicitly specify source files that aren't picked up by Tailwind's automatic content detection.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#source-directive" + } + ] + }, + { + "name": "@utility", + "description": "Use the `@utility` directive to add custom utilities to your project that work with variants like `hover`, `focus` and `lg`.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#utility-directive" + } + ] + }, + { + "name": "@variant", + "description": "Use the `@variant` directive to apply a Tailwind variant to styles in your CSS.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#variant-directive" + } + ] + }, + { + "name": "@custom-variant", + "description": "Use the `@custom-variant` directive to add a custom variant in your project.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#custom-variant-directive" + } + ] + }, + { + "name": "@apply", + "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#apply-directive" + } + ] + }, + { + "name": "@reference", + "description": "If you want to use `@apply` or `@variant` in the ` diff --git a/Frontend/src/assets/fonts/3270-Regular.woff b/Frontend/src/assets/fonts/3270-Regular.woff new file mode 100644 index 0000000..d74887d Binary files /dev/null and b/Frontend/src/assets/fonts/3270-Regular.woff differ diff --git a/Frontend/src/assets/playhead-main.png b/Frontend/src/assets/playhead-main.png new file mode 100644 index 0000000..1e43c43 Binary files /dev/null and b/Frontend/src/assets/playhead-main.png differ diff --git a/Frontend/src/assets/playhead-top.png b/Frontend/src/assets/playhead-top.png new file mode 100644 index 0000000..15c38dd Binary files /dev/null and b/Frontend/src/assets/playhead-top.png differ diff --git a/Frontend/src/audio/AudioEngine.ts b/Frontend/src/audio/AudioEngine.ts new file mode 100644 index 0000000..0d274c6 --- /dev/null +++ b/Frontend/src/audio/AudioEngine.ts @@ -0,0 +1,554 @@ +/* + AudioEngine.ts + A small singleton wrapper around the WebAudio AudioContext that: + - lazily creates the AudioContext + - provides fetch/decode + simple caching + - schedules intro and loop buffers to play seamlessly + - exposes play/pause/stop and a volume control via a master GainNode + - provides a short fade-in/out GainNode to avoid clicks (few ms) + - exposes getPosition() to read current playback time relative to intro start +*/ + +import type { ConfigurableWindow } from '@vueuse/core' +import type { MaybeRefOrGetter, Ref } from 'vue' +import type { AudioTrack } from '@/lib/AudioTrack' +import type { Seconds } from '@/lib/units' +import { + + tryOnScopeDispose, + useRafFn, + useThrottleFn, + watchImmediate, +} from '@vueuse/core' +import { + + shallowRef, + toValue, + watch, +} from 'vue' +import { useWrapTime, wrapTimeFn } from '@/lib/AudioTrack' + +export const VOLUME_MAX: number = 1.5 + +interface PlayerHandle { + /** + * The `stop()` method schedules a sound to cease playback at the specified time. + */ + stop: (when?: Seconds) => void +} + +interface AudioTrackBuffersHandle extends PlayerHandle { + /** + * Time in AudioContext coordinate system of a moment which lines up with the start of the intro audio buffer. + * If the startPosition was greater than zero, this time is already in the past when the function returns. + */ + readonly introStartTime: Seconds +} + +/** + * Start playing intro + loop buffers at given position. + * + * @returns Handle with introStartTime and stop() method. + */ +function playAudioTrackBuffers( + audioCtx: AudioContext, + destinationNode: AudioNode, + audioTrack: AudioTrack, + /** + * Position in seconds from the start of the intro + */ + startPosition: Seconds = 0, +): AudioTrackBuffersHandle { + const now = audioCtx.currentTime + + const introBuffer = audioTrack.loadedIntro! + const loopBuffer = audioTrack.loadedLoop! + + const introDuration = introBuffer.duration + const loopDuration = loopBuffer.duration + + const wrapper = wrapTimeFn(audioTrack) + startPosition = wrapper(startPosition) + + let currentIntro: AudioBufferSourceNode | null + let currentLoop: AudioBufferSourceNode | null + let introStartTime: Seconds + + // figure out where to start + if (startPosition < introDuration) { + // start intro with offset, schedule loop after remaining intro time + const introOffset = startPosition + const timeUntilLoop = introDuration - introOffset + + const introNode = audioCtx.createBufferSource() + introNode.buffer = introBuffer + introNode.connect(destinationNode) + introNode.start(now, introOffset) + + const loopNode = audioCtx.createBufferSource() + loopNode.buffer = loopBuffer + loopNode.loop = true + loopNode.connect(destinationNode) + loopNode.start(now + timeUntilLoop, 0) + + currentIntro = introNode + currentLoop = loopNode + introStartTime = now - startPosition + } + else { + // start directly in loop with proper offset into loop + const loopOffset = (startPosition - introDuration) % loopDuration + const loopNode = audioCtx.createBufferSource() + loopNode.buffer = loopBuffer + loopNode.loop = true + loopNode.connect(destinationNode) + loopNode.start(now, loopOffset) + + currentIntro = null + currentLoop = loopNode + // Note: using wrapping loop breaks logical position when starting playback from the second loop repetition onward. + // introStartTime = now - introDuration - loopOffset; + introStartTime = now - startPosition + } + + function stop(when?: Seconds) { + try { + currentIntro?.stop(when) + } + catch { + /* ignore */ + } + try { + currentLoop?.stop(when) + } + catch { + /* ignore */ + } + currentIntro = null + currentLoop = null + } + + return { introStartTime, stop } +} + +interface PlayWithFadeInOut extends PlayerHandle { + playerResult: Omit +} + +/** + * 25 ms for fade-in/fade-out + */ +const DEFAULT_FADE_DURATION = 0.025 + +/** + * Wrap the given player function with a Gain node. Applies fade in effect on start and fade out on stop. + * + * @returns Handle with introStartTime and stop() method. + */ +function playWithFadeInOut( + audioCtx: AudioContext, + destinationNode: AudioNode, + player: (destinationNode: AudioNode) => T, + /** + * Duration of fade in/out in seconds. Fade out extends past the stop() call. + */ + fadeDuration: Seconds = DEFAULT_FADE_DURATION, +): PlayWithFadeInOut { + const GAIN_MIN = 0.0001 + const GAIN_MAX = 1.0 + + const fadeGain = audioCtx.createGain() + fadeGain.connect(destinationNode) + fadeGain.gain.value = GAIN_MIN + + const playerHandle = player(fadeGain) + + // fade in + const now = audioCtx.currentTime + const fadeEnd = now + fadeDuration + fadeGain.gain.setValueAtTime(GAIN_MIN, now) + fadeGain.gain.linearRampToValueAtTime(GAIN_MAX, fadeEnd) + + // TODO: setTimeout to actually stop after `when`? + function stop(_when?: Seconds) { + // fade out + const now = audioCtx.currentTime + const fadeEnd = now + fadeDuration + fadeGain.gain.cancelScheduledValues(now) + fadeGain.gain.setValueAtTime(GAIN_MAX, now) + fadeGain.gain.linearRampToValueAtTime(GAIN_MIN, fadeEnd) + + playerHandle.stop(fadeEnd) + } + + return { playerResult: playerHandle, stop } +} + +/** + * Properties relates to the state of playback. + */ +export interface PlaybackState { + /** + * Readonly reference to whether audio is currently playing. + */ + readonly isPlaying: Readonly> + /** + * Readonly reference to the last remembered start-of-playback position. + * + * Will only update if stop(rememberPosition=true) or seek() is called. + */ + readonly startPosition: Readonly> + /** + * Returns current playback position in seconds based on AudioContext time. + * + * Hook it up to requestAnimationFrame while isPlaying is true for live updates. + */ + getCurrentPosition: () => Seconds +} + +export interface StopOptions { + /** + * If true, update remembered playback position to current position, otherwise revert to last remembered one. + * + * Defaults to false. + */ + rememberPosition?: boolean +} + +export interface SeekOptions { + /** + * If scrub is requested, plays a short sample at that position. + * + * Defaults to false. + */ + scrub?: boolean + // TODO: optionally keep playing after seeking? +} + +/** + * Player controls and properties relates to the state of playback. + */ +export interface PlayerControls { + /** + * Start playing audio buffers from the last remembered position. + */ + play: () => void + /** + * Stop playing audio buffers. + * + * If rememberPosition is true, update remembered playback position, otherwise revert to the last remembered one. + */ + stop: (options?: StopOptions) => void + /** + * Seek to given position in seconds. + * + * - Stop the playback. + * - If scrub is requested, plays a short sample at that position. + */ + seek: (position: Seconds, options?: SeekOptions) => void + /** + * Properties relates to the state of playback. + */ + readonly playback: PlaybackState +} + +interface ReusableAudioBuffersTrackPlayer extends PlayerControls { +} + +function reusableAudioBuffersTrackPlayer( + audioCtx: AudioContext, + destinationNode: AudioNode, + audioTrack: AudioTrack, +): ReusableAudioBuffersTrackPlayer { + let currentHandle: PlayWithFadeInOut | null = null + const isPlaying = shallowRef(false) + const wrapper = wrapTimeFn(audioTrack) + const startPosition = useWrapTime(audioTrack, 0) + + function play() { + if (currentHandle) { + return + } + currentHandle = playWithFadeInOut( + audioCtx, + destinationNode, + destinationNode => + playAudioTrackBuffers( + audioCtx, + destinationNode, + audioTrack, + startPosition.value, + ), + ) + isPlaying.value = true + } + + function stop(options?: { rememberPosition?: boolean }) { + const { + rememberPosition = false, + } = options ?? {} + + if (currentHandle) { + isPlaying.value = false + + if (rememberPosition) { + startPosition.value = getCurrentPosition() + } + + // stop and discard current handle + currentHandle.stop() + currentHandle = null + } + } + + // Scrub is subject to debouncing/throttling, so it doesn't start + // playing samples too often before previous ones could stop. + const doThrottledScrub = useThrottleFn(() => { + // play a short sample at the seeked position + const scrubHandle = playWithFadeInOut( + audioCtx, + destinationNode, + destinationNode => + playAudioTrackBuffers( + audioCtx, + destinationNode, + audioTrack, + startPosition.value, + ), + 0.01, // short fade of 10 ms + ) + setTimeout(() => { + scrubHandle.stop(0.01) + }, 80) // stop after N ms + }, 80) + + function seek(seekPosition: Seconds, options?: SeekOptions) { + const { + scrub = false, + } = options ?? {} + + stop({ rememberPosition: false }) + + startPosition.value = seekPosition + + if (scrub) { + doThrottledScrub() + } + } + + function getCurrentPosition(): Seconds { + if (!currentHandle) { + return startPosition.value + } + + const elapsed = audioCtx.currentTime + - currentHandle.playerResult.introStartTime + + return wrapper(elapsed) + } + + return { + play, + stop, + seek, + playback: { + isPlaying, + startPosition, + getCurrentPosition, + }, + } +} + +interface LivePlaybackPositionOptions extends ConfigurableWindow { +} + +interface LivePlaybackPositionReturn { + stop: () => void + position: Readonly> +} + +export function useLivePlaybackPosition( + playback: MaybeRefOrGetter, + options?: LivePlaybackPositionOptions, +): LivePlaybackPositionReturn { + const cleanups: (() => void)[] = [] + const cleanup = () => { + cleanups.forEach(fn => fn()) + cleanups.length = 0 + } + + const getPosition = () => { + return toValue(playback)?.getCurrentPosition() ?? 0 + } + + const position = shallowRef(getPosition()) + + const updatePosition = () => { + position.value = getPosition() + } + + const raf = useRafFn(() => { + updatePosition() + }, { + ...options, + immediate: false, + once: false, + }) + + const stopWatch = watchImmediate(() => [ + toValue(playback), + ], ([playback]) => { + cleanup() + + updatePosition() + + if (!playback) { + return + } + + cleanups.push(watch(playback.isPlaying, (isPlaying) => { + if (isPlaying) { + raf.resume() + } + else { + raf.pause() + updatePosition() + } + })) + + cleanups.push(watch(playback.startPosition, () => { + raf.pause() + updatePosition() + if (playback.isPlaying.value) { + raf.resume() + } + })) + + cleanups.push(() => raf.pause()) + }) + + const stop = () => { + stopWatch() + cleanup() + } + + tryOnScopeDispose(cleanup) + + return { stop, position } +} + +export function togglePlayStop( + player: PlayerControls | null, + options?: StopOptions, +) { + if (!player) { + return + } + if (player.playback.isPlaying.value) { + player.stop(options) + } + else { + player.play() + } +} + +class AudioEngine { + audioCtx: AudioContext | null = null + masterGain: GainNode | null = null // controlled by UI volume slider + // fadeGain: GainNode | null = null; // tiny fade to avoid clicks + + // cache of decoded buffers by URL + bufferCache = new Map() + + private _player: Ref = shallowRef(null) + // readonly player: Readonly> = this._player; + + // settings + fadeDuration = 0.025 // 25 ms for fade-in/fade-out + + init() { + if (this.audioCtx) { + return + } + this.audioCtx + = new (window.AudioContext || (window as any).webkitAudioContext)() + + this.masterGain = this.audioCtx.createGain() + + // routing: sources -> fadeGain -> masterGain -> destination + this.masterGain.connect(this.audioCtx.destination) + // default full volume + this.masterGain.gain.value = 1 + } + + shutdown() { + this.stopPlayer() + this.audioCtx?.close() + this.audioCtx = null + this.masterGain = null + } + + async fetchAudioBuffer( + url: string, + signal?: AbortSignal, + ): Promise { + this.init() + if (this.bufferCache.has(url)) { + return this.bufferCache.get(url)! + } + const res = await fetch(url, { signal }) + if (!res.ok) { + throw new Error(`Network error ${res.status} when fetching ${url}`) + } + const arrayBuffer = await res.arrayBuffer() + const audioBuffer = await this.audioCtx!.decodeAudioData(arrayBuffer) + this.bufferCache.set(url, audioBuffer) + return audioBuffer + } + + // set UI volume 0..VOLUME_MAX + setVolume(value: number) { + this.init() + if (!this.masterGain || !this.audioCtx) { + return + } + const now = this.audioCtx.currentTime + // small linear ramp to avoid jumps + this.masterGain.gain.cancelScheduledValues(now) + this.masterGain.gain.setValueAtTime(this.masterGain.gain.value, now) + this.masterGain.gain.linearRampToValueAtTime(value, now + 0.05) + } + + initPlayer( + audioTrack: AudioTrack, + ): PlayerControls | null { + this.init() + if (!this.audioCtx || !this.masterGain) { + return null + } + + this.stopPlayer() + + if (!audioTrack.loadedIntro || !audioTrack.loadedLoop) { + return null + } + + const player = reusableAudioBuffersTrackPlayer( + this.audioCtx, + this.masterGain, + audioTrack, + ) + this._player.value = player + return player + } + + private stopPlayer() { + if (this._player.value) { + this._player.value.stop() + this._player.value = null + } + } +} + +const audioEngine = new AudioEngine() +export default audioEngine diff --git a/Frontend/src/audio/AudioWaveform.ts b/Frontend/src/audio/AudioWaveform.ts new file mode 100644 index 0000000..7bf5164 --- /dev/null +++ b/Frontend/src/audio/AudioWaveform.ts @@ -0,0 +1,190 @@ +import type { Fn } from '@vueuse/core' +import type { MaybeRefOrGetter, Ref } from 'vue' +import type { Px } from '@/lib/units' +import { tryOnScopeDispose, watchImmediate } from '@vueuse/core' +import { computed, shallowRef, toValue, triggerRef } from 'vue' +import { useWeakCache } from '@/lib/useWeakCache' + +// Result of async computation +interface UseWaveform { + readonly isDone: Readonly> + readonly peaks: Readonly> + stop: () => void +} + +interface WaveformComputation { + readonly isDone: Readonly> + readonly peaks: Readonly> + /** Start or continue asynchronous computation. */ + run: () => void + /** Stops any ongoing asynchronous computation. */ + stop: () => void +} + +const waveformsCache = useWeakCache>( + () => new Map(), +) + +const WAVEFORM_MIN_WIDTH = 10 + +const emptyComputation: WaveformComputation = { + isDone: shallowRef(false), + peaks: shallowRef(new Float32Array(0)), + run() {}, + stop() {}, +} + +export function useWaveform( + buffer: MaybeRefOrGetter, + width: MaybeRefOrGetter, +): UseWaveform { + const cleanups: Fn[] = [] + const cleanup = () => { + cleanups.forEach(fn => fn()) + cleanups.length = 0 + } + + const compRef: Ref = shallowRef(emptyComputation) + + const stopWatch = watchImmediate( + () => + [ + toValue(buffer), + toValue(width), + ] as const, + ([b, w]) => { + cleanup() + + const map = waveformsCache.getOrNew(b) + + if (w < WAVEFORM_MIN_WIDTH) { + compRef.value = emptyComputation + return + } + + let comp = map.get(w) + if (!comp) { + comp = useWaveformComputation(b, w) + map.set(w, comp) + } + compRef.value = comp + comp.run() + cleanups.push(() => { + compRef.value = emptyComputation + comp.stop() + }) + }, + ) + + const stop = () => { + stopWatch() + cleanup() + } + + tryOnScopeDispose(stop) + + return { + isDone: computed(() => compRef.value.isDone.value), + peaks: computed(() => compRef.value.peaks.value), + stop, + } +} + +function useWaveformComputation(buffer: AudioBuffer, width: Px): WaveformComputation { + // How many times run() has been called without stop(). + // This whole computation should not stop until there is at least one user out there. + let users = 0 + + // How many pixels of `width` have been processed so far + let progress = 0 + + let timeoutID: ReturnType | undefined + + // Waveform data, length shall be equal to the requested width + const waveform = new Float32Array(width) + + const isDone = shallowRef(false) + const peaks = shallowRef(waveform) + + const nChannels = buffer.numberOfChannels + + const samplesPerPx = buffer.length / width + const blocksPerChannel: Float32Array[] = [] + for (let channel = 0; channel < nChannels; channel++) { + blocksPerChannel[channel] = new Float32Array(Math.ceil(samplesPerPx)) + } + + const areWeDoneYet = () => progress >= width + + function stepBlock() { + const blockStart = Math.floor(progress * samplesPerPx) + const blockEnd = Math.floor((progress + 1) * samplesPerPx) + const blockSize = blockEnd - blockStart + + for (let channel = 0; channel < nChannels; channel++) { + buffer.copyFromChannel(blocksPerChannel[channel]!, channel, blockStart) + } + + waveform[progress] = compressBlock(blocksPerChannel, blockSize) + progress += 1 + } + + function stepBatchOfBlocks() { + // run blocks for up to ~10ms to keep UI responsive + const start = performance.now() + const progressStart = progress + while (!areWeDoneYet()) { + stepBlock() + if (performance.now() - start >= 10 || progress - progressStart > 100) { + break + } + } + + triggerRef(peaks) + // triggerRef may as well not trigger refs + // https://github.com/vuejs/core/issues/9579 + // Combined with a throttled drawing function, + // this is a slightly better-than-worse workaround. + peaks.value = new Float32Array(0) + peaks.value = waveform + + if (areWeDoneYet()) { + isDone.value = true + timeoutID = undefined + } + else { + timeoutID = setTimeout(stepBatchOfBlocks, 1) + } + } + + return { + isDone, + peaks, + run() { + users += 1 + + if (timeoutID === undefined && users === 1) { + timeoutID = setTimeout(stepBatchOfBlocks, 0) + } + }, + stop() { + users -= 1 + + if (!timeoutID === undefined && users === 0) { + window.clearTimeout(timeoutID) + timeoutID = undefined + } + }, + } +}; + +function compressBlock(channels: Float32Array[], blockSize: number): number { + let peak = 0.0 + + for (let i = 0; i < blockSize; i++) { + for (let channel = 0; channel < channels.length; channel++) { + peak = Math.max(peak, Math.abs(channels[channel]![i]!)) + } + } + return peak +} diff --git a/Frontend/src/components/ErrorScreen.vue b/Frontend/src/components/ErrorScreen.vue new file mode 100644 index 0000000..460cc57 --- /dev/null +++ b/Frontend/src/components/ErrorScreen.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/Frontend/src/components/Footer.vue b/Frontend/src/components/Footer.vue new file mode 100644 index 0000000..406601b --- /dev/null +++ b/Frontend/src/components/Footer.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/Frontend/src/components/LoadingScreen.vue b/Frontend/src/components/LoadingScreen.vue new file mode 100644 index 0000000..5ef03fb --- /dev/null +++ b/Frontend/src/components/LoadingScreen.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/Frontend/src/components/ScreenTransition.vue b/Frontend/src/components/ScreenTransition.vue new file mode 100644 index 0000000..21251cf --- /dev/null +++ b/Frontend/src/components/ScreenTransition.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/Frontend/src/components/SearchField.vue b/Frontend/src/components/SearchField.vue new file mode 100644 index 0000000..c8e74ad --- /dev/null +++ b/Frontend/src/components/SearchField.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/Frontend/src/components/editor/PreviewScnene.vue b/Frontend/src/components/editor/PreviewScnene.vue new file mode 100644 index 0000000..f20eb77 --- /dev/null +++ b/Frontend/src/components/editor/PreviewScnene.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/Frontend/src/components/editor/TrackInfo.vue b/Frontend/src/components/editor/TrackInfo.vue new file mode 100644 index 0000000..108d8b1 --- /dev/null +++ b/Frontend/src/components/editor/TrackInfo.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/Frontend/src/components/inspector/InspectorPanel.vue b/Frontend/src/components/inspector/InspectorPanel.vue new file mode 100644 index 0000000..6f7ed65 --- /dev/null +++ b/Frontend/src/components/inspector/InspectorPanel.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/Control.vue b/Frontend/src/components/inspector/controls/Control.vue new file mode 100644 index 0000000..f0a36da --- /dev/null +++ b/Frontend/src/components/inspector/controls/Control.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/ControlsView.vue b/Frontend/src/components/inspector/controls/ControlsView.vue new file mode 100644 index 0000000..c885507 --- /dev/null +++ b/Frontend/src/components/inspector/controls/ControlsView.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/impl/BaseNamedControlView.vue b/Frontend/src/components/inspector/controls/impl/BaseNamedControlView.vue new file mode 100644 index 0000000..b6a2f07 --- /dev/null +++ b/Frontend/src/components/inspector/controls/impl/BaseNamedControlView.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/impl/ButtonControlView.vue b/Frontend/src/components/inspector/controls/impl/ButtonControlView.vue new file mode 100644 index 0000000..823b508 --- /dev/null +++ b/Frontend/src/components/inspector/controls/impl/ButtonControlView.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/impl/CheckboxControlView.vue b/Frontend/src/components/inspector/controls/impl/CheckboxControlView.vue new file mode 100644 index 0000000..9f3482a --- /dev/null +++ b/Frontend/src/components/inspector/controls/impl/CheckboxControlView.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/impl/DropDownControlView.vue b/Frontend/src/components/inspector/controls/impl/DropDownControlView.vue new file mode 100644 index 0000000..23eb6cc --- /dev/null +++ b/Frontend/src/components/inspector/controls/impl/DropDownControlView.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/impl/HrControlView.vue b/Frontend/src/components/inspector/controls/impl/HrControlView.vue new file mode 100644 index 0000000..75baf9e --- /dev/null +++ b/Frontend/src/components/inspector/controls/impl/HrControlView.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/impl/NotImplementedControlView.vue b/Frontend/src/components/inspector/controls/impl/NotImplementedControlView.vue new file mode 100644 index 0000000..5bf2206 --- /dev/null +++ b/Frontend/src/components/inspector/controls/impl/NotImplementedControlView.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/impl/NumberControlView.vue b/Frontend/src/components/inspector/controls/impl/NumberControlView.vue new file mode 100644 index 0000000..74f02d1 --- /dev/null +++ b/Frontend/src/components/inspector/controls/impl/NumberControlView.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/impl/RangeControlView.vue b/Frontend/src/components/inspector/controls/impl/RangeControlView.vue new file mode 100644 index 0000000..f1367ab --- /dev/null +++ b/Frontend/src/components/inspector/controls/impl/RangeControlView.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/Frontend/src/components/inspector/controls/impl/TextAreaControlView.vue b/Frontend/src/components/inspector/controls/impl/TextAreaControlView.vue new file mode 100644 index 0000000..7f5ebae --- /dev/null +++ b/Frontend/src/components/inspector/controls/impl/TextAreaControlView.vue @@ -0,0 +1,28 @@ + + +