├── .babelrc.js ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierrc.json ├── .stylelintrc.json ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── __tests__ ├── core │ ├── ArrayList.spec.js │ ├── Entity.spec.js │ └── EntityList.spec.js └── utils │ ├── array.spec.js │ ├── crypto.spec.js │ ├── easing.spec.js │ ├── format.spec.js │ ├── math.spec.js │ ├── object.spec.js │ └── string.spec.js ├── build ├── background.tiff ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icons │ └── 512x512.png └── install-spinner.gif ├── jest.config.js ├── package.json ├── scripts ├── bootstrap.js ├── install-ffmpeg.js └── notarize.js ├── src ├── audio │ ├── Audio.js │ ├── AudioReactor.js │ ├── FFTParser.js │ ├── Player.js │ ├── SpectrumAnalyzer.js │ └── WaveParser.js ├── build │ ├── app │ │ └── dev-app-update.yml │ └── loaders │ │ └── glsl-loader.js ├── canvas │ ├── CanvasAudio.js │ ├── CanvasBars.js │ ├── CanvasImage.js │ ├── CanvasMeter.js │ ├── CanvasShape.js │ ├── CanvasText.js │ └── CanvasWave.js ├── config │ ├── app.json │ ├── colorPalettes.json │ ├── fonts.json │ ├── menu.json │ └── video.json ├── core │ ├── ArrayList.js │ ├── CanvasDisplay.js │ ├── Clock.js │ ├── Display.js │ ├── Effect.js │ ├── Entity.js │ ├── EntityList.js │ ├── EventEmitter.js │ ├── Logger.js │ ├── Plugin.js │ ├── Process.js │ ├── Reactors.js │ ├── Renderer.js │ ├── Scene.js │ ├── Stage.js │ └── WebGLDisplay.js ├── displays │ ├── BarSpectrumDisplay.js │ ├── GeometryDisplay.js │ ├── ImageDisplay.js │ ├── ShapeDisplay.js │ ├── SoundWaveDisplay.js │ ├── TextDisplay.js │ ├── WaveSpectrumDisplay.js │ └── index.js ├── drawing │ └── bezierSpline.js ├── effects │ ├── BloomEffect.js │ ├── BlurEffect.js │ ├── ColorHalftoneEffect.js │ ├── DistortionEffect.js │ ├── DotScreenEffect.js │ ├── GlitchEffect.js │ ├── GlowEffect.js │ ├── KaleidoscopeEffect.js │ ├── LEDEffect.js │ ├── MirrorEffect.js │ ├── PixelateEffect.js │ ├── RGBShiftEffect.js │ ├── index.js │ └── passes │ │ ├── GaussianBlurPass.js │ │ ├── LensBlurPass.js │ │ └── TriangleBlurPass.js ├── graphics │ ├── BlendPass.js │ ├── CanvasBuffer.js │ ├── ClearMaskPass.js │ ├── Composer.js │ ├── CopyPass.js │ ├── ImagePass.js │ ├── MaskPass.js │ ├── MultiPass.js │ ├── Pass.js │ ├── RenderPass.js │ ├── ShaderPass.js │ ├── TexturePass.js │ ├── WebGLBuffer.js │ ├── blendModes.js │ └── common.js ├── main │ ├── api │ │ ├── audio.js │ │ ├── config.js │ │ ├── image.js │ │ ├── index.js │ │ ├── ipc.js │ │ ├── plugin.js │ │ ├── process.js │ │ ├── project.js │ │ └── window.js │ ├── autoupdate.js │ ├── constants.js │ ├── environment.js │ ├── events.js │ ├── index.js │ ├── init.js │ ├── menu.js │ ├── preload.js │ └── window.js ├── shaders │ ├── BarrelBlurShader.js │ ├── BlendShader.js │ ├── BoxBlurShader.js │ ├── CircularBlurShader.js │ ├── ColorHalftoneShader.js │ ├── ColorShiftShader.js │ ├── CopyShader.js │ ├── DistortionShader.js │ ├── DotScreenShader.js │ ├── FXAAShader.js │ ├── GaussianBlurShader.js │ ├── GlitchShader.js │ ├── GlowShader.js │ ├── GridShader.js │ ├── HalftoneShader.js │ ├── HexagonShader.js │ ├── KaleidoscopeShader.js │ ├── LEDShader.js │ ├── LensBlurShader.js │ ├── LuminanceShader.js │ ├── MirrorShader.js │ ├── PixelateShader.js │ ├── PointShader.js │ ├── RGBShiftShader.js │ ├── RippleShader.js │ ├── TriangleBlurShader.js │ ├── ZoomBlurShader.js │ └── glsl │ │ ├── fragment │ │ ├── barrel-blur.glsl │ │ ├── blend.glsl │ │ ├── box-blur.glsl │ │ ├── circular-blur.glsl │ │ ├── color-halftone.glsl │ │ ├── color-shift.glsl │ │ ├── copy.glsl │ │ ├── distortion.glsl │ │ ├── dot-screen.glsl │ │ ├── fxaa.glsl │ │ ├── gaussian-blur.glsl │ │ ├── glitch.glsl │ │ ├── glow.glsl │ │ ├── grid.glsl │ │ ├── hatch.glsl │ │ ├── hexagon.glsl │ │ ├── kaleidoscope.glsl │ │ ├── led.glsl │ │ ├── lens-blur.glsl │ │ ├── luminance.glsl │ │ ├── mirror.glsl │ │ ├── pixelate.glsl │ │ ├── point.glsl │ │ ├── rgb-shift.glsl │ │ ├── ripple.glsl │ │ ├── triangle-blur.glsl │ │ └── zoom-blur.glsl │ │ ├── func │ │ ├── classic-noise-2d.glsl │ │ ├── classic-noise-3d.glsl │ │ ├── classic-noise-4d.glsl │ │ ├── random.glsl │ │ ├── simplex-noise-2d.glsl │ │ ├── simplex-noise-3d.glsl │ │ └── simplex-noise-4d.glsl │ │ └── vertex │ │ ├── basic.glsl │ │ ├── normal.glsl │ │ ├── point.glsl │ │ ├── position.glsl │ │ └── ripple.glsl ├── utils │ ├── array.js │ ├── audio.js │ ├── canvas.js │ ├── controls.js │ ├── crypto.js │ ├── data.js │ ├── easing.js │ ├── ffmpeg.js │ ├── file.js │ ├── format.js │ ├── io.js │ ├── math.js │ ├── object.js │ ├── react.js │ ├── string.js │ └── work.js ├── video │ ├── AudioProcess.js │ ├── MergeProcess.js │ ├── RenderProcess.js │ └── VideoRenderer.js └── view │ ├── actions │ ├── app.js │ ├── audio.js │ ├── config.js │ ├── error.js │ ├── modals.js │ ├── project.js │ ├── reactors.js │ ├── scenes.js │ ├── stage.js │ ├── updates.js │ └── video.js │ ├── assets │ ├── fonts │ │ ├── LICENSE.txt │ │ ├── abel.woff2 │ │ ├── abril-fatface.woff2 │ │ ├── alegreya.woff2 │ │ ├── arimo.woff2 │ │ ├── bangers.woff2 │ │ ├── cardo.woff2 │ │ ├── caveat.woff2 │ │ ├── chunkfive.woff2 │ │ ├── dynalight.woff2 │ │ ├── felipa.woff2 │ │ ├── fira_sans.woff2 │ │ ├── intro.woff2 │ │ ├── merriweather.woff2 │ │ ├── oswald.woff2 │ │ ├── oxygen.woff2 │ │ ├── permanent-marker.woff2 │ │ ├── playfair-display.woff2 │ │ ├── racing-sans-one.woff2 │ │ ├── raleway.woff2 │ │ ├── roboto-condensed.woff2 │ │ ├── roboto.woff2 │ │ └── vast-shadow.woff2 │ ├── icons │ │ ├── adjust.svg │ │ ├── angle-double-left.svg │ │ ├── angle-double-right.svg │ │ ├── arrows-cw.svg │ │ ├── arrows-h.svg │ │ ├── bar-graph.svg │ │ ├── block.svg │ │ ├── ccw.svg │ │ ├── check-circle.svg │ │ ├── chevron-down.svg │ │ ├── chevron-left.svg │ │ ├── chevron-right.svg │ │ ├── chevron-up.svg │ │ ├── circle-filled.svg │ │ ├── circle-with-cross.svg │ │ ├── circle.svg │ │ ├── cloud.svg │ │ ├── code.svg │ │ ├── cog.svg │ │ ├── contrast.svg │ │ ├── crop.svg │ │ ├── cross.svg │ │ ├── cube.svg │ │ ├── cw.svg │ │ ├── cycle.svg │ │ ├── document-landscape.svg │ │ ├── dots-three-horizontal.svg │ │ ├── download.svg │ │ ├── edit.svg │ │ ├── expand.svg │ │ ├── eye.svg │ │ ├── flash.svg │ │ ├── flashlight.svg │ │ ├── folder-open.svg │ │ ├── gear.svg │ │ ├── hand.svg │ │ ├── help-with-circle.svg │ │ ├── hour-glass.svg │ │ ├── info.svg │ │ ├── levels.svg │ │ ├── light-up.svg │ │ ├── light.svg │ │ ├── lightbulb.svg │ │ ├── link.svg │ │ ├── lock-open.svg │ │ ├── lock.svg │ │ ├── mask.svg │ │ ├── menu.svg │ │ ├── minus.svg │ │ ├── move.svg │ │ ├── multiply.svg │ │ ├── music.svg │ │ ├── pause.svg │ │ ├── pencil.svg │ │ ├── picture.svg │ │ ├── play.svg │ │ ├── plus.svg │ │ ├── puzzle.svg │ │ ├── refresh.svg │ │ ├── retweet.svg │ │ ├── search.svg │ │ ├── selection.svg │ │ ├── sound-bars.svg │ │ ├── sound-waves.svg │ │ ├── square-circle.svg │ │ ├── square.svg │ │ ├── stop.svg │ │ ├── times-circle.svg │ │ ├── times.svg │ │ ├── trash-empty.svg │ │ ├── trash.svg │ │ ├── trashcan.svg │ │ ├── video.svg │ │ ├── videocam.svg │ │ ├── volume.svg │ │ ├── volume2.svg │ │ ├── volume3.svg │ │ ├── volume4.svg │ │ ├── warning.svg │ │ └── wave.svg │ ├── images │ │ ├── about_bg.jpg │ │ ├── blank.gif │ │ ├── controls │ │ │ ├── BarSpectrumDisplay.png │ │ │ ├── BloomEffect.png │ │ │ ├── BlurEffect.png │ │ │ ├── ColorHalftoneEffect.png │ │ │ ├── DistortionEffect.png │ │ │ ├── DotScreenEffect.png │ │ │ ├── GeometryDisplay.png │ │ │ ├── GlitchEffect.png │ │ │ ├── GlowEffect.png │ │ │ ├── ImageDisplay.png │ │ │ ├── KaleidoscopeEffect.png │ │ │ ├── LEDEffect.png │ │ │ ├── MirrorEffect.png │ │ │ ├── PixelateEffect.png │ │ │ ├── Plugin.png │ │ │ ├── RGBShiftEffect.png │ │ │ ├── ShapeDisplay.png │ │ │ ├── SoundWaveDisplay.png │ │ │ ├── TextDisplay.png │ │ │ └── WaveSpectrumDisplay.png │ │ └── point.png │ └── logo.svg │ ├── components │ ├── App.js │ ├── controls │ │ ├── Control.js │ │ ├── Control.less │ │ ├── Option.js │ │ ├── Option.less │ │ ├── OptionGroup.js │ │ ├── OptionGroup.less │ │ ├── Setting.js │ │ ├── Setting.less │ │ ├── Settings.js │ │ ├── Settings.less │ │ ├── index.js │ │ └── inputComponents.js │ ├── dialogs │ │ ├── ErrorDialog.js │ │ └── UnsavedChangesDialog.js │ ├── inputs │ │ ├── BoxInput.js │ │ ├── BoxInput.less │ │ ├── ButtonGroup.js │ │ ├── ButtonGroup.less │ │ ├── ButtonInput.js │ │ ├── ButtonInput.less │ │ ├── CheckboxInput.js │ │ ├── CheckboxInput.less │ │ ├── ColorInput.js │ │ ├── ColorInput.less │ │ ├── ColorRangeInput.js │ │ ├── ColorRangeInput.less │ │ ├── ImageInput.js │ │ ├── ImageInput.less │ │ ├── NumberInput.js │ │ ├── RangeInput.js │ │ ├── RangeInput.less │ │ ├── ReactorButton.js │ │ ├── ReactorButton.less │ │ ├── ReactorInput.js │ │ ├── ReactorInput.less │ │ ├── SelectInput.js │ │ ├── SelectInput.less │ │ ├── TextInput.js │ │ ├── TextInput.less │ │ ├── TimeInput.js │ │ ├── ToggleInput.js │ │ ├── ToggleInput.less │ │ └── index.js │ ├── interface │ │ ├── Button.js │ │ ├── Button.less │ │ ├── Checkmark.js │ │ ├── Checkmark.less │ │ ├── Icon.js │ │ ├── Icon.less │ │ ├── Spinner.js │ │ ├── Spinner.less │ │ └── index.js │ ├── layout │ │ ├── ButtonPanel.js │ │ ├── ButtonPanel.less │ │ ├── ButtonRow.js │ │ ├── ButtonRow.less │ │ ├── Layout.js │ │ ├── Layout.less │ │ ├── Panel.js │ │ ├── Panel.less │ │ ├── PanelDock.js │ │ ├── PanelDock.less │ │ ├── Splitter.js │ │ ├── Splitter.less │ │ ├── TabPanel.js │ │ └── TabPanel.less │ ├── modals │ │ ├── About.js │ │ ├── About.less │ │ ├── AppSettings.js │ │ ├── AppUpdates.js │ │ ├── AppUpdates.less │ │ ├── CanvasSettings.js │ │ ├── ControlPicker.js │ │ ├── ControlPicker.less │ │ ├── VideoSettings.js │ │ ├── VideoSettings.less │ │ └── index.js │ ├── nav │ │ ├── Menu.js │ │ ├── Menu.less │ │ ├── MenuBar.js │ │ ├── MenuBar.less │ │ ├── MenuBarItem.js │ │ ├── MenuBarItem.less │ │ ├── MenuItem.js │ │ └── MenuItem.less │ ├── panels │ │ ├── ControlDock.js │ │ ├── ControlsPanel.js │ │ ├── ControlsPanel.less │ │ ├── Layer.js │ │ ├── Layer.less │ │ ├── LayersPanel.js │ │ ├── LayersPanel.less │ │ ├── ReactorPanel.js │ │ ├── ReactorPanel.less │ │ ├── RenderPanel.js │ │ ├── RenderPanel.less │ │ ├── SceneLayer.js │ │ └── SceneLayer.less │ ├── player │ │ ├── AudioWaveform.js │ │ ├── AudioWaveform.less │ │ ├── Oscilloscope.js │ │ ├── Oscilloscope.less │ │ ├── PlayButtons.js │ │ ├── PlayButtons.less │ │ ├── Player.js │ │ ├── Player.less │ │ ├── ProgressControl.js │ │ ├── ProgressControl.less │ │ ├── Spectrum.js │ │ ├── Spectrum.less │ │ ├── TimeInfo.js │ │ ├── TimeInfo.less │ │ ├── ToggleButtons.js │ │ ├── ToggleButtons.less │ │ ├── VolumeControl.js │ │ └── VolumeControl.less │ ├── stage │ │ ├── Stage.js │ │ └── Stage.less │ └── window │ │ ├── Dialog.js │ │ ├── Dialog.less │ │ ├── ModalWindow.js │ │ ├── ModalWindow.less │ │ ├── Modals.js │ │ ├── Modals.less │ │ ├── Overlay.js │ │ ├── Overlay.less │ │ ├── Preload.js │ │ ├── Preload.less │ │ ├── StatusBar.js │ │ ├── StatusBar.less │ │ ├── TitleBar.js │ │ ├── TitleBar.less │ │ ├── WindowButtons.js │ │ ├── WindowButtons.less │ │ ├── ZoomControl.js │ │ └── ZoomControl.less │ ├── constants.js │ ├── fonts.css │ ├── global.js │ ├── hooks │ ├── useCombinedRefs.js │ ├── useDebounce.js │ ├── useEntity.js │ ├── useForceUpdate.js │ ├── useMeasure.js │ ├── useMergeState.js │ ├── useMouseDrag.js │ ├── useSharedState.js │ ├── useTimeout.js │ └── useWindowState.js │ ├── icons.js │ ├── index.html │ ├── index.js │ ├── stores.js │ └── styles │ ├── global.less │ ├── index.less │ ├── inputs.less │ ├── mixins.less │ ├── scrollbars.less │ └── variables.less ├── test └── crypto.mock.js ├── webpack.config.electron.js ├── webpack.config.js ├── webpack.config.main.js ├── webpack.config.preload.js ├── webpack.config.view.js └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | 4 | const ENV = process.env.BABEL_ENV || process.env.NODE_ENV; 5 | 6 | const presets = [ 7 | [ 8 | '@babel/env', 9 | { 10 | targets: { 11 | electron: '15.3.0', 12 | }, 13 | }, 14 | ], 15 | '@babel/react', 16 | ]; 17 | 18 | const plugins = [ 19 | '@babel/proposal-class-properties', 20 | '@babel/proposal-object-rest-spread', 21 | '@babel/proposal-optional-chaining', 22 | '@babel/proposal-nullish-coalescing-operator', 23 | '@babel/proposal-export-default-from', 24 | ]; 25 | 26 | if (ENV === 'development') { 27 | plugins.push('react-refresh/babel'); 28 | } 29 | 30 | return { 31 | presets, 32 | plugins, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /app/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Common 2 | .DS_Store 3 | .DS_Store? 4 | ._* 5 | .Spotlight-V100 6 | .Trashes 7 | Thumbs.db 8 | *.swp 9 | *.log 10 | 11 | # Environment 12 | .idea 13 | .env 14 | *.iml 15 | *.bat 16 | *.map 17 | 18 | # Project 19 | node_modules 20 | bin 21 | coverage 22 | dist 23 | /app/ 24 | /plugins/ 25 | /resources/ 26 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "endOfLine": "lf", 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-css-modules", 5 | "stylelint-config-prettier" 6 | ], 7 | "rules": { 8 | "no-descending-specificity": null, 9 | "selector-pseudo-class-no-unknown": [ 10 | true, 11 | { 12 | "ignorePseudoClasses": ["global", "horizontal", "vertical"] 13 | } 14 | ] 15 | }, 16 | "ignoreFiles": ["**/*.js"] 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mike Cao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /__tests__/core/ArrayList.spec.js: -------------------------------------------------------------------------------- 1 | import ArrayList from 'core/ArrayList'; 2 | 3 | let a; 4 | 5 | beforeEach(() => { 6 | a = new ArrayList(1, 2, 3); 7 | }); 8 | 9 | test('arraylist is instance of array', () => { 10 | expect(a).toBeInstanceOf(Array); 11 | }); 12 | 13 | describe('isEmpty method working properly', () => { 14 | test('empty arraylist', () => { 15 | expect(new ArrayList().isEmpty()).toBe(true); 16 | }); 17 | 18 | test('non-empty arraylist', () => { 19 | expect(a.isEmpty()).toBe(false); 20 | }); 21 | }); 22 | 23 | test('insert method working properly', () => { 24 | a.insert(5, 1); 25 | expect(a).toEqual([1, 5, 2, 3]); 26 | }); 27 | 28 | test('remove method working properly', () => { 29 | a.remove(0); 30 | expect(a).toEqual([2, 3]); 31 | }); 32 | 33 | describe('swap method working properly', () => { 34 | test('different indexes', () => { 35 | a.swap(0, 2); 36 | expect(a).toEqual([3, 2, 1]); 37 | }); 38 | 39 | test('same indexes', () => { 40 | a.swap(0, 0); 41 | expect(a).toEqual([1, 2, 3]); 42 | }); 43 | }); 44 | 45 | test('clear method working properly', () => { 46 | a.clear(); 47 | expect(a).toEqual([]); 48 | }); 49 | -------------------------------------------------------------------------------- /__tests__/utils/array.spec.js: -------------------------------------------------------------------------------- 1 | import { isDefined, contains, reverse } from 'utils/array'; 2 | 3 | test('check if array is defined', () => { 4 | expect(isDefined([1, 2, 3])).toBe(true); 5 | }); 6 | 7 | test('check if arr1 contains arr2', () => { 8 | expect(contains([1, 2, 4], [1, 2, 3])).toBe(true); 9 | expect(contains([1, 2, 3], [4, 5, 6])).toBe(false); 10 | }); 11 | 12 | test('reverse an array properly', () => { 13 | expect(reverse([1, 2, 3])).toEqual([3, 2, 1]); 14 | expect(reverse([1, 2, 3])).not.toBe([1, 2, 3]); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/utils/crypto.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { uniqueId } from 'utils/crypto'; 6 | 7 | test('uniqueId functions exists', () => { 8 | expect(uniqueId()).toBeDefined(); 9 | }); 10 | 11 | test('unique id has a length of 40 characters', () => { 12 | expect(uniqueId()).toHaveLength(40); 13 | }); 14 | -------------------------------------------------------------------------------- /__tests__/utils/object.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { updateExistingProps } from 'utils/object'; 5 | import cloneDeep from 'lodash/cloneDeep'; 6 | 7 | describe('updateExistingProps works properly', () => { 8 | test('oldProps !== newProps', () => { 9 | const oldProps = { speed: "100", theta: 75, acceleration: "2m/s", undefined: null }; 10 | const newProps = { speed: "200", direction: 75, acceleration: 4.2, undefined: undefined }; 11 | const result = updateExistingProps(oldProps, newProps); 12 | expect(result).toBeTruthy(); 13 | expect(oldProps).toStrictEqual({ speed: "200", theta: 75, acceleration: 4.2, undefined: undefined }); 14 | }); 15 | 16 | test('oldProps === newProps', () => { 17 | const oldProps = { speed: "100", theta: 75, acceleration: "2m/s", undefined: undefined }; 18 | const newProps = { speed: "100", theta: 75, direction: 75, acceleration: "2m/s", undefined: undefined }; 19 | const oldPropsDup = cloneDeep(oldProps); 20 | const result = updateExistingProps(oldProps, newProps); 21 | expect(result).toBeFalsy(); 22 | expect(oldProps).toStrictEqual(oldPropsDup); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/utils/string.spec.js: -------------------------------------------------------------------------------- 1 | import { trimChars } from 'utils/string'; 2 | 3 | test('trim chars from string properly', () => { 4 | expect(trimChars("hello there\v")).toBe("hello there"); 5 | }); 6 | -------------------------------------------------------------------------------- /build/background.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/build/background.tiff -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | 8 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/build/icon.ico -------------------------------------------------------------------------------- /build/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/build/icons/512x512.png -------------------------------------------------------------------------------- /build/install-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/build/install-spinner.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleDirectories: ['node_modules', 'src'], 3 | setupFiles: ['./test/crypto.mock.js'], 4 | }; 5 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const prettier = require('prettier'); 4 | const pkg = require('../package.json'); 5 | 6 | const appFolder = path.resolve(__dirname, '../app'); 7 | 8 | if (!fs.existsSync(appFolder)) { 9 | fs.mkdirSync(appFolder); 10 | } 11 | 12 | const { name, version, productName, description, author, license, homepage, repository } = pkg; 13 | 14 | // Create package.json file in app directory 15 | const json = prettier.format( 16 | JSON.stringify({ 17 | name, 18 | version, 19 | productName, 20 | description, 21 | author, 22 | license, 23 | homepage, 24 | repository, 25 | main: 'main.js', 26 | dependencies: {}, 27 | }), 28 | { parser: 'json' }, 29 | ); 30 | 31 | fs.writeFileSync(path.join(appFolder, 'package.json'), json); 32 | 33 | // Create app-update.yml files 34 | const srcFile = path.resolve(__dirname, '../src/build/app/dev-app-update.yml'); 35 | fs.copyFileSync(srcFile, path.resolve(appFolder, 'dev-app-update.yml')); 36 | -------------------------------------------------------------------------------- /scripts/install-ffmpeg.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | const https = require('https'); 5 | 6 | const dest = path.resolve(__dirname, '../bin'); 7 | 8 | if (!fs.existsSync(dest)) { 9 | fs.mkdirSync(dest); 10 | } 11 | 12 | const platform = os.platform(); 13 | 14 | if (!['win32', 'darwin', 'linux'].includes(platform)) { 15 | throw new Error('Unsupported platform'); 16 | } 17 | 18 | const files = { 19 | win32: ['win', 'ffmpeg.exe'], 20 | darwin: ['mac', 'ffmpeg'], 21 | linux: ['linux', 'ffmpeg'], 22 | }; 23 | 24 | const download = async (url, file) => { 25 | const filename = path.join(dest, file); 26 | 27 | console.log(`Downloading ${url} -> ${filename}`); 28 | 29 | await new Promise(resolve => { 30 | https.get(url, res => { 31 | resolve(res.pipe(fs.createWriteStream(filename))); 32 | }); 33 | }); 34 | }; 35 | 36 | const [dir, file] = files[platform]; 37 | const url = `https://files.astrofox.io/ffmpeg/${dir}/${file}`; 38 | 39 | download(url, file); 40 | -------------------------------------------------------------------------------- /scripts/notarize.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { notarize } = require('electron-notarize'); 3 | 4 | const { APPLEID, APPLEIDPASS } = process.env; 5 | 6 | (async () => { 7 | console.log('Staring notarize...'); 8 | 9 | await notarize({ 10 | appBundleId: 'io.astrofox.app', 11 | appPath: 'dist/mac/Astrofox.app', 12 | appleId: APPLEID, 13 | appleIdPassword: APPLEIDPASS, 14 | }); 15 | 16 | console.log('Notarization complete.'); 17 | })(); 18 | -------------------------------------------------------------------------------- /src/build/app/dev-app-update.yml: -------------------------------------------------------------------------------- 1 | provider: generic 2 | url: 'https://files.astrofox.io/download' 3 | channel: latest 4 | -------------------------------------------------------------------------------- /src/build/loaders/glsl-loader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const glslman = require('glsl-man'); 4 | 5 | const regex = /#include "(.*)"/g; 6 | 7 | function minify(code) { 8 | return glslman.string(glslman.parse(code), { tab: '', space: '', newline: '' }); 9 | } 10 | 11 | function parseInclude(match, context, imports) { 12 | const [, file] = match.split('"'); 13 | 14 | const filePath = path.resolve(context, file); 15 | 16 | if (!imports[filePath]) { 17 | imports[filePath] = fs.readFileSync(filePath, 'utf-8'); 18 | 19 | // eslint-disable-next-line no-use-before-define 20 | return parseContent(imports[filePath], path.dirname(filePath), imports); 21 | } 22 | 23 | return imports[filePath]; 24 | } 25 | 26 | function parseContent(content, context, imports) { 27 | return content.replace(regex, match => parseInclude(match, context, imports)); 28 | } 29 | 30 | module.exports = function glslLoader(content) { 31 | const source = parseContent(content, this.context, {}); 32 | 33 | return `module.exports = ${JSON.stringify(minify(source))}`; 34 | }; 35 | -------------------------------------------------------------------------------- /src/config/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "checkForUpdates": true, 3 | "autoUpdate": true, 4 | "autoPlayAudio": true 5 | } 6 | -------------------------------------------------------------------------------- /src/config/fonts.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Abel", 3 | "Abril Fatface", 4 | "Bangers", 5 | "Cardo", 6 | "Caveat", 7 | "Chunkfive", 8 | "Dynalight", 9 | "Intro", 10 | "Merriweather", 11 | "Playfair Display", 12 | "Permanent Marker", 13 | "Oswald", 14 | "Oxygen", 15 | "Racing Sans One", 16 | "Raleway", 17 | "Roboto", 18 | "Vast Shadow" 19 | ] 20 | -------------------------------------------------------------------------------- /src/core/ArrayList.js: -------------------------------------------------------------------------------- 1 | export default class ArrayList extends Array { 2 | static get [Symbol.species]() { 3 | return Array; 4 | } 5 | 6 | isEmpty() { 7 | return this.length === 0; 8 | } 9 | 10 | insert(item, index) { 11 | this.splice(index, 0, item); 12 | } 13 | 14 | remove(index) { 15 | return !!this.splice(index, 1).length; 16 | } 17 | 18 | swap(index, newIndex) { 19 | if ( 20 | index !== newIndex && 21 | index > -1 && 22 | index < this.length && 23 | newIndex > -1 && 24 | newIndex < this.length 25 | ) { 26 | const tmp = this[index]; 27 | this[index] = this[newIndex]; 28 | this[newIndex] = tmp; 29 | } 30 | } 31 | 32 | clear() { 33 | this.length = 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/core/CanvasDisplay.js: -------------------------------------------------------------------------------- 1 | import Display from 'core/Display'; 2 | 3 | export default class CanvasDisplay extends Display { 4 | constructor(Type, properties) { 5 | super(Type, properties); 6 | 7 | const { width = 1, height = 1 } = this.properties; 8 | 9 | this.canvas = new OffscreenCanvas(width, height); 10 | this.context = this.canvas.getContext('2d'); 11 | } 12 | 13 | render(scene) { 14 | const { canvas, properties } = this; 15 | const { width, height } = canvas; 16 | 17 | if (width === 0 || height === 0) { 18 | return; 19 | } 20 | 21 | const origin = { 22 | x: width / 2, 23 | y: height / 2, 24 | }; 25 | 26 | scene.renderToCanvas(canvas, properties, origin); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/core/Clock.js: -------------------------------------------------------------------------------- 1 | import { clamp, round } from 'utils/math'; 2 | 3 | export default class Clock { 4 | constructor() { 5 | this.time = 0; 6 | this.elapsedTime = 0; 7 | this.frames = 0; 8 | this.delta = 0; 9 | this.startTime = Date.now(); 10 | } 11 | 12 | update() { 13 | const time = Date.now(); 14 | 15 | if (this.time) { 16 | const delta = time - this.time; 17 | 18 | this.elapsedTime += delta; 19 | this.delta = delta; 20 | } 21 | 22 | this.time = time; 23 | this.frames += 1; 24 | } 25 | 26 | getFPS() { 27 | const { time, frames, elapsedTime } = this; 28 | 29 | if (!time) { 30 | return 0; 31 | } 32 | 33 | const seconds = elapsedTime / 1000; 34 | const fps = clamp(round(frames / seconds), 0, 60); 35 | 36 | this.frames = 0; 37 | this.elapsedTime = 0; 38 | 39 | return fps; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/core/Effect.js: -------------------------------------------------------------------------------- 1 | import Display from 'core/Display'; 2 | 3 | export default class Effect extends Display { 4 | constructor(Type, properties) { 5 | super(Type, properties); 6 | 7 | this.type = 'effect'; 8 | } 9 | 10 | update(properties = {}) { 11 | const changed = super.update(properties); 12 | 13 | if (changed) { 14 | this.updatePass(); 15 | } 16 | 17 | return changed; 18 | } 19 | 20 | updatePass() { 21 | const { pass } = this; 22 | 23 | if (pass.setUniforms) { 24 | pass.setUniforms(this.properties); 25 | } 26 | } 27 | 28 | setSize(width, height) { 29 | const { pass } = this; 30 | 31 | if (pass) { 32 | pass.setSize(width, height); 33 | } 34 | } 35 | 36 | render() {} 37 | } 38 | -------------------------------------------------------------------------------- /src/core/EntityList.js: -------------------------------------------------------------------------------- 1 | import ArrayList from 'core/ArrayList'; 2 | 3 | export default class EntityList extends ArrayList { 4 | getElementById(id) { 5 | return this.find(e => e.id === id); 6 | } 7 | 8 | hasElement(obj) { 9 | return this.indexOf(obj) > -1; 10 | } 11 | 12 | addElement(obj, index) { 13 | if (!obj) { 14 | return; 15 | } 16 | 17 | if (index !== undefined) { 18 | this.insert(obj, index); 19 | } else { 20 | this.push(obj); 21 | } 22 | 23 | return obj; 24 | } 25 | 26 | removeElement(obj) { 27 | if (!this.hasElement(obj)) { 28 | return false; 29 | } 30 | 31 | return this.remove(this.indexOf(obj)); 32 | } 33 | 34 | shiftElement(obj, spaces) { 35 | if (!this.hasElement(obj)) { 36 | return false; 37 | } 38 | 39 | const index = this.indexOf(obj); 40 | const newIndex = index + spaces; 41 | 42 | this.swap(index, newIndex); 43 | 44 | return this.indexOf(obj) !== index; 45 | } 46 | 47 | toJSON() { 48 | return this.map(e => e.toJSON()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/core/EventEmitter.js: -------------------------------------------------------------------------------- 1 | export default class EventEmitter { 2 | on(event, fn) { 3 | this.events = this.events || {}; 4 | this.events[event] = this.events[event] || []; 5 | 6 | return this.events[event].push(fn); 7 | } 8 | 9 | off(event, fn) { 10 | if (!this.events || !this.events[event]) return; 11 | 12 | const events = this.events[event]; 13 | 14 | this.events[event] = events.filter(e => e !== fn); 15 | } 16 | 17 | emit(...args) { 18 | this.events = this.events || {}; 19 | 20 | const event = args.shift(); 21 | const events = this.events[event] || []; 22 | 23 | events.forEach(fn => fn(...args)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/core/Plugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import ShaderPass from 'graphics/ShaderPass'; 3 | import Display from './Display'; 4 | import Effect from './Effect'; 5 | 6 | export default class Plugin { 7 | static create(module) { 8 | const Type = module.config.type === 'effect' ? Effect : Display; 9 | 10 | class PluginClass extends Type { 11 | constructor(properties) { 12 | super(module, properties); 13 | 14 | if (module.shader) { 15 | this.pass = new ShaderPass(module.shader, properties); 16 | } 17 | } 18 | } 19 | 20 | // Add static properties 21 | Object.getOwnPropertyNames(module).forEach(name => { 22 | if (PluginClass[name] === undefined) { 23 | PluginClass[name] = module[name]; 24 | } 25 | }); 26 | 27 | // Add methods 28 | Object.getOwnPropertyNames(module.prototype).forEach(name => { 29 | if (name !== 'constructor') { 30 | PluginClass.prototype[name] = module.prototype[name]; 31 | } 32 | }); 33 | 34 | return PluginClass; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/core/Reactors.js: -------------------------------------------------------------------------------- 1 | import AudioReactor from 'audio/AudioReactor'; 2 | import EntityList from 'core/EntityList'; 3 | 4 | export default class Reactors extends EntityList { 5 | constructor() { 6 | super(); 7 | 8 | this.results = {}; 9 | } 10 | 11 | addReactor(reactor) { 12 | return this.addElement(reactor ?? new AudioReactor()); 13 | } 14 | 15 | removeReactor(reactor) { 16 | this.removeElement(reactor); 17 | } 18 | 19 | clearReactors() { 20 | this.clear(); 21 | } 22 | 23 | getResults(data) { 24 | if (data.hasUpdate) { 25 | this.results = this.reduce((memo, reactor) => { 26 | memo[reactor.id] = reactor.parse(data).output; 27 | return memo; 28 | }, {}); 29 | } 30 | return this.results; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/core/WebGLDisplay.js: -------------------------------------------------------------------------------- 1 | import Display from 'core/Display'; 2 | 3 | export default class WebGLDisplay extends Display { 4 | constructor(info, properties) { 5 | super(info, properties); 6 | 7 | this.type = 'webgl'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/displays/index.js: -------------------------------------------------------------------------------- 1 | export BarSpectrumDisplay from './BarSpectrumDisplay'; 2 | export GeometryDisplay from './GeometryDisplay'; 3 | export ImageDisplay from './ImageDisplay'; 4 | export ShapeDisplay from './ShapeDisplay'; 5 | export SoundWaveDisplay from './SoundWaveDisplay'; 6 | export TextDisplay from './TextDisplay'; 7 | export WaveSpectrumDisplay from './WaveSpectrumDisplay'; 8 | -------------------------------------------------------------------------------- /src/effects/MirrorEffect.js: -------------------------------------------------------------------------------- 1 | import Effect from 'core/Effect'; 2 | import ShaderPass from 'graphics/ShaderPass'; 3 | import MirrorShader from 'shaders/MirrorShader'; 4 | 5 | const mirrorOptions = [ 6 | { label: 'Left 🠖 Right', value: 0 }, 7 | { label: 'Right 🠖 Left', value: 1 }, 8 | { label: 'Top 🠖 Bottom', value: 2 }, 9 | { label: 'Bottom 🠖 Top', value: 3 }, 10 | ]; 11 | 12 | export default class MirrorEffect extends Effect { 13 | static config = { 14 | name: 'MirrorEffect', 15 | description: 'Mirror effect.', 16 | type: 'effect', 17 | label: 'Mirror', 18 | defaultProperties: { 19 | side: 0, 20 | }, 21 | controls: { 22 | side: { 23 | label: 'Side', 24 | type: 'select', 25 | items: mirrorOptions, 26 | }, 27 | }, 28 | }; 29 | 30 | constructor(properties) { 31 | super(MirrorEffect, properties); 32 | } 33 | 34 | addToScene() { 35 | this.pass = new ShaderPass(MirrorShader); 36 | } 37 | 38 | removeFromScene() { 39 | this.pass = null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/effects/index.js: -------------------------------------------------------------------------------- 1 | export BloomEffect from './BloomEffect'; 2 | export BlurEffect from './BlurEffect'; 3 | export ColorHalftoneEffect from './ColorHalftoneEffect'; 4 | export DistortionEffect from './DistortionEffect'; 5 | export DotScreenEffect from './DotScreenEffect'; 6 | export GlitchEffect from './GlitchEffect'; 7 | export GlowEffect from './GlowEffect'; 8 | export KaleidoscopeEffect from './KaleidoscopeEffect'; 9 | export LEDEffect from './LEDEffect'; 10 | export MirrorEffect from './MirrorEffect'; 11 | export PixelateEffect from './PixelateEffect'; 12 | export RGBShiftEffect from './RGBShiftEffect'; 13 | -------------------------------------------------------------------------------- /src/effects/passes/GaussianBlurPass.js: -------------------------------------------------------------------------------- 1 | import ShaderPass from 'graphics/ShaderPass'; 2 | import MultiPass from 'graphics/MultiPass'; 3 | import GaussianBlurShader from 'shaders/GaussianBlurShader'; 4 | 5 | const BLUR_PASSES = 8; 6 | 7 | export default class GaussianBlurPass extends MultiPass { 8 | constructor() { 9 | const passes = []; 10 | 11 | for (let i = 0; i < BLUR_PASSES; i++) { 12 | passes.push(new ShaderPass(GaussianBlurShader)); 13 | } 14 | 15 | super(passes); 16 | 17 | this.setUniforms({ amount: 1.0 }); 18 | } 19 | 20 | setUniforms({ amount }) { 21 | this.passes.forEach((pass, index) => { 22 | const radius = (BLUR_PASSES - index) * amount; 23 | 24 | pass.setUniforms({ direction: index % 2 === 0 ? [0, radius] : [radius, 0] }); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/effects/passes/TriangleBlurPass.js: -------------------------------------------------------------------------------- 1 | import ShaderPass from 'graphics/ShaderPass'; 2 | import MultiPass from 'graphics/MultiPass'; 3 | import TriangleBlurShader from 'shaders/TriangleBlurShader'; 4 | 5 | const BLUR_PASSES = 4; 6 | 7 | export default class TriangleBlurPass extends MultiPass { 8 | constructor() { 9 | const passes = []; 10 | 11 | for (let i = 0; i < BLUR_PASSES; i++) { 12 | passes.push(new ShaderPass(TriangleBlurShader)); 13 | } 14 | 15 | super(passes); 16 | } 17 | 18 | setUniforms({ amount, width, height }) { 19 | this.passes.forEach((pass, index) => { 20 | const delta = index % 2 === 0 ? [amount / width, 0] : [0, amount / height]; 21 | 22 | pass.setUniforms({ delta }); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/graphics/BlendPass.js: -------------------------------------------------------------------------------- 1 | import ShaderPass from 'graphics/ShaderPass'; 2 | import BlendShader from 'shaders/BlendShader'; 3 | import blendModes from 'graphics/blendModes'; 4 | 5 | export default class BlendPass extends ShaderPass { 6 | constructor(buffer) { 7 | super(BlendShader); 8 | 9 | this.buffer = buffer; 10 | this.blendMode = 'Normal'; 11 | this.opacity = 1.0; 12 | } 13 | 14 | render(renderer, inputBuffer, outputBuffer) { 15 | const { opacity, blendMode } = this; 16 | 17 | this.setUniforms({ 18 | baseBuffer: this.buffer, 19 | blendBuffer: inputBuffer.texture, 20 | mode: blendModes[blendMode], 21 | opacity, 22 | }); 23 | 24 | super.render(renderer, inputBuffer, outputBuffer); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/graphics/CanvasBuffer.js: -------------------------------------------------------------------------------- 1 | import { Texture } from 'three'; 2 | import TexturePass from './TexturePass'; 3 | 4 | export default class CanvasBuffer { 5 | constructor(width, height) { 6 | this.canvas = new OffscreenCanvas(width, height); 7 | 8 | this.texture = new Texture(this.canvas); 9 | 10 | this.pass = new TexturePass(this.texture); 11 | this.pass.alwaysUpdateTexture = true; 12 | 13 | this.context = this.canvas.getContext('2d'); 14 | 15 | this.setSize(width, height); 16 | } 17 | 18 | getContext() { 19 | return this.context; 20 | } 21 | 22 | setSize(width, height) { 23 | this.canvas.width = width; 24 | this.canvas.height = height; 25 | } 26 | 27 | clear() { 28 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/graphics/ClearMaskPass.js: -------------------------------------------------------------------------------- 1 | import Pass from 'graphics/Pass'; 2 | 3 | export default class ClearMaskPass extends Pass { 4 | render(renderer) { 5 | const { stencil } = renderer.state.buffers; 6 | 7 | stencil.setLocked(false); 8 | stencil.setTest(false); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/graphics/CopyPass.js: -------------------------------------------------------------------------------- 1 | import ShaderPass from 'graphics/ShaderPass'; 2 | import CopyShader from 'shaders/CopyShader'; 3 | 4 | export default class CopyPass extends ShaderPass { 5 | constructor(buffer) { 6 | super(CopyShader); 7 | 8 | this.buffer = buffer; 9 | this.needsSwap = false; 10 | this.copyFromBuffer = false; 11 | this.clearBuffer = false; 12 | } 13 | 14 | dispose() { 15 | this.buffer.dispose(); 16 | } 17 | 18 | render(renderer, inputBuffer) { 19 | const { buffer, copyFromBuffer, clearBuffer } = this; 20 | 21 | if (clearBuffer) { 22 | renderer.setRenderTarget(buffer); 23 | renderer.clear(); 24 | } 25 | 26 | super.render( 27 | renderer, 28 | copyFromBuffer ? buffer : inputBuffer, 29 | copyFromBuffer ? inputBuffer : buffer, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/graphics/ImagePass.js: -------------------------------------------------------------------------------- 1 | import { 2 | MeshBasicMaterial, 3 | OrthographicCamera, 4 | PlaneBufferGeometry, 5 | } from 'three'; 6 | import Pass from './Pass'; 7 | 8 | export default class ImagePass extends Pass { 9 | constructor(texture, resolution) { 10 | super(); 11 | 12 | const { width, height } = resolution; 13 | const { naturalWidth, naturalHeight } = texture.image; 14 | 15 | this.texture = texture; 16 | 17 | const material = new MeshBasicMaterial({ 18 | map: texture, 19 | depthTest: false, 20 | depthWrite: false, 21 | transparent: true, 22 | }); 23 | 24 | const camera = new OrthographicCamera(width / -2, width / 2, height / 2, height / -2, 0, 1); 25 | const geometry = new PlaneBufferGeometry(naturalWidth, naturalHeight); 26 | 27 | this.setFullscreen(material, geometry, camera); 28 | } 29 | 30 | render(renderer, inputBuffer) { 31 | const { scene, camera } = this; 32 | 33 | super.render(renderer, scene, camera, inputBuffer); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/graphics/RenderPass.js: -------------------------------------------------------------------------------- 1 | import Pass from 'graphics/Pass'; 2 | 3 | export default class RenderPass extends Pass { 4 | constructor(scene, camera) { 5 | super(); 6 | 7 | this.scene = scene; 8 | this.camera = camera; 9 | } 10 | 11 | render(renderer, inputBuffer, outputBuffer) { 12 | const { scene, camera } = this; 13 | 14 | super.render(renderer, scene, camera, outputBuffer); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/graphics/TexturePass.js: -------------------------------------------------------------------------------- 1 | import { MeshBasicMaterial } from 'three'; 2 | import Pass from './Pass'; 3 | 4 | export default class TexturePass extends Pass { 5 | constructor(texture) { 6 | super(); 7 | 8 | this.texture = texture; 9 | 10 | this.material = new MeshBasicMaterial({ 11 | map: texture, 12 | depthTest: false, 13 | depthWrite: false, 14 | transparent: true, 15 | }); 16 | 17 | this.setFullscreen(this.material); 18 | } 19 | 20 | render(renderer, inputBuffer) { 21 | const { scene, camera, texture, alwaysUpdateTexture } = this; 22 | 23 | if (alwaysUpdateTexture) { 24 | texture.needsUpdate = true; 25 | } 26 | 27 | super.render(renderer, scene, camera, inputBuffer); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/graphics/WebGLBuffer.js: -------------------------------------------------------------------------------- 1 | import { Scene, Camera } from 'three'; 2 | import { WEBGL_BUFFER_SAMPLES } from 'view/constants'; 3 | import { createRenderTarget } from './common'; 4 | import CopyPass from './CopyPass'; 5 | 6 | export default class WebGLBuffer { 7 | constructor(renderer) { 8 | this.renderer = renderer; 9 | 10 | this.buffer = createRenderTarget({ samples: WEBGL_BUFFER_SAMPLES }); 11 | 12 | this.pass = new CopyPass(this.buffer); 13 | this.pass.copyFromBuffer = true; 14 | 15 | this.scene = new Scene(); 16 | this.camera = new Camera(); 17 | } 18 | 19 | setSize(width, height) { 20 | this.buffer.setSize(width, height); 21 | } 22 | 23 | dispose() { 24 | this.buffer.dispose(); 25 | } 26 | 27 | clear() { 28 | const { renderer, buffer } = this; 29 | 30 | renderer.setRenderTarget(buffer); 31 | renderer.clear(); 32 | } 33 | 34 | render(scene, camera) { 35 | const { renderer, buffer } = this; 36 | 37 | renderer.setRenderTarget(buffer); 38 | renderer.render(scene, camera); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/graphics/blendModes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | None: 0, 3 | Add: 1, 4 | Average: 2, 5 | 'Color Burn': 3, 6 | 'Color Dodge': 4, 7 | Darken: 5, 8 | Difference: 6, 9 | Exclusion: 7, 10 | Glow: 8, 11 | 'Hard Light': 9, 12 | 'Hard Mix': 10, 13 | Lighten: 11, 14 | 'Linear Burn': 12, 15 | 'Linear Dodge': 13, 16 | 'Linear Light': 14, 17 | Multiply: 15, 18 | Negation: 16, 19 | Normal: 17, 20 | Overlay: 18, 21 | Phoenix: 19, 22 | 'Pin Light': 20, 23 | Reflect: 21, 24 | Screen: 22, 25 | 'Soft Light': 23, 26 | Subtract: 24, 27 | 'Vivid Light': 25, 28 | Divide: 26, 29 | }; 30 | -------------------------------------------------------------------------------- /src/main/api/audio.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as id3 from 'id3js'; 3 | import { readFile } from 'utils/io'; 4 | import { blobToArrayBuffer, dataToBlob } from 'utils/data'; 5 | import { log } from './ipc'; 6 | 7 | export async function readAudioFile(file) { 8 | const fileData = await readFile(file); 9 | const blob = await dataToBlob(fileData, path.extname(file)); 10 | 11 | let { type } = blob; 12 | 13 | // mime module does not recognize opus 14 | if (file.endsWith('.opus')) { 15 | type = 'audio/opus'; 16 | } 17 | 18 | if (!/^audio/.test(type)) { 19 | throw new Error(`Unrecognized audio type: ${type}`); 20 | } 21 | 22 | return blobToArrayBuffer(blob); 23 | } 24 | 25 | export async function loadAudioTags(file) { 26 | try { 27 | return await id3.fromPath(file); 28 | } catch (e) { 29 | log(e); 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/api/config.js: -------------------------------------------------------------------------------- 1 | import { fileExists, readFileCompressed, writeFileCompressed } from 'utils/io'; 2 | import { getGlobal } from './ipc'; 3 | 4 | export async function loadConfig() { 5 | const { APP_CONFIG_FILE } = await getGlobal('env'); 6 | 7 | if (fileExists(APP_CONFIG_FILE)) { 8 | const data = await readFileCompressed(APP_CONFIG_FILE); 9 | return JSON.parse(data); 10 | } 11 | 12 | return null; 13 | } 14 | 15 | export async function saveConfig(config) { 16 | const { APP_CONFIG_FILE } = await getGlobal('env'); 17 | 18 | const data = JSON.stringify(config); 19 | 20 | await writeFileCompressed(APP_CONFIG_FILE, data); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/api/image.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { readFile, writeFile } from 'utils/io'; 3 | import { blobToDataUrl, dataToBlob } from 'utils/data'; 4 | 5 | export async function saveImageFile(file, data) { 6 | if (['.jpg', '.png', '.gif'].includes(path.extname(file))) { 7 | return writeFile(file, data); 8 | } 9 | } 10 | 11 | export async function readImageFile(file) { 12 | const fileData = await readFile(file); 13 | const blob = await dataToBlob(fileData, path.extname(file)); 14 | 15 | if (!/^image/.test(blob.type)) { 16 | throw new Error('Invalid image file.'); 17 | } 18 | 19 | return blobToDataUrl(blob); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/api/index.js: -------------------------------------------------------------------------------- 1 | export { loadAudioTags, readAudioFile } from './audio'; 2 | export { loadConfig, saveConfig } from './config'; 3 | export { readImageFile, saveImageFile } from './image'; 4 | export { loadProjectFile, saveProjectFile } from './project'; 5 | export { send, on, once, off, invoke, log, getGlobal } from './ipc'; 6 | export { loadPlugins, getPlugins } from './plugin'; 7 | export { spawnProcess } from './process'; 8 | export { 9 | maximizeWindow, 10 | minimizeWindow, 11 | unmaximizeWindow, 12 | closeWindow, 13 | getWindowState, 14 | showOpenDialog, 15 | showSaveDialog, 16 | openDevTools, 17 | } from './window'; 18 | -------------------------------------------------------------------------------- /src/main/api/ipc.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | 3 | export function send(channel, data) { 4 | ipcRenderer.send(channel, data); 5 | } 6 | 7 | export function on(channel, callback) { 8 | ipcRenderer.on(channel, (event, ...args) => callback(...args)); 9 | } 10 | 11 | export function once(channel, callback) { 12 | ipcRenderer.once(channel, (event, ...args) => callback(...args)); 13 | } 14 | 15 | export function off(channel, callback) { 16 | if (callback) { 17 | ipcRenderer.removeListener(channel, callback); 18 | } else { 19 | ipcRenderer.removeAllListeners(channel); 20 | } 21 | } 22 | 23 | export async function invoke(channel, data) { 24 | return ipcRenderer.invoke(channel, data); 25 | } 26 | 27 | export function log(...args) { 28 | return invoke('log', args.join(' ')); 29 | } 30 | 31 | export async function getGlobal(key) { 32 | return invoke('get-global', key); 33 | } 34 | -------------------------------------------------------------------------------- /src/main/api/process.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | export function spawnProcess(command, args, props = {}) { 4 | const process = spawn(command, args); 5 | const { onStdOut, onStdErr, onClose, onExit, onError } = props; 6 | 7 | if (onStdOut) { 8 | process.stdout.on('data', data => onStdOut(data.toString())); 9 | } 10 | if (onStdErr) { 11 | process.stderr.on('data', data => onStdErr(data.toString())); 12 | } 13 | if (onClose) { 14 | process.on('close', onClose); 15 | } 16 | if (onExit) { 17 | process.on('exit', onExit); 18 | } 19 | if (onError) { 20 | process.on('error', onError); 21 | } 22 | 23 | const stop = signal => process.kill(signal); 24 | const push = data => process.stdin.write(data); 25 | const end = () => process.stdin.destroy(); 26 | 27 | return { stop, push, end }; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/api/project.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { readFile, readFileCompressed, writeFileCompressed } from 'utils/io'; 3 | 4 | export async function loadProjectFile(file) { 5 | try { 6 | const data = await readFileCompressed(file); 7 | 8 | return JSON.parse(data); 9 | } catch (error) { 10 | if (error.message.indexOf('incorrect header check') > -1) { 11 | const data = readFile(file); 12 | 13 | return JSON.parse(data); 14 | } 15 | } 16 | } 17 | 18 | export async function saveProjectFile(file, data) { 19 | if (path.extname(file) !== '.afx') { 20 | file = `${file}.afx`; 21 | } 22 | return writeFileCompressed(file, JSON.stringify(data)); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/api/window.js: -------------------------------------------------------------------------------- 1 | import { invoke } from './ipc'; 2 | 3 | export function maximizeWindow() { 4 | return invoke('maximize-window'); 5 | } 6 | 7 | export function unmaximizeWindow() { 8 | return invoke('unmaximize-window'); 9 | } 10 | 11 | export function minimizeWindow() { 12 | return invoke('minimize-window'); 13 | } 14 | 15 | export function closeWindow() { 16 | return invoke('close-window'); 17 | } 18 | 19 | export function openDevTools() { 20 | return invoke('open-dev-tools'); 21 | } 22 | 23 | export async function showOpenDialog(props) { 24 | return invoke('show-open-dialog', props); 25 | } 26 | 27 | export async function showSaveDialog(props) { 28 | return invoke('show-save-dialog', props); 29 | } 30 | 31 | export async function getWindowState() { 32 | return invoke('get-window-state'); 33 | } 34 | -------------------------------------------------------------------------------- /src/main/constants.js: -------------------------------------------------------------------------------- 1 | export const WINDOW_WIDTH = 1320; 2 | export const WINDOW_HEIGHT = 1200; 3 | export const WINDOW_MINWIDTH = 200; 4 | export const WINDOW_MINHEIGHT = 100; 5 | export const WINDOW_BGCOLOR = '#222222'; 6 | -------------------------------------------------------------------------------- /src/main/menu.js: -------------------------------------------------------------------------------- 1 | import { Menu } from 'electron'; 2 | import { sendMessage } from 'main/window'; 3 | import menuConfig from 'config/menu.json'; 4 | 5 | export default function init() { 6 | const { setApplicationMenu, buildFromTemplate } = Menu; 7 | let menu = [...menuConfig]; 8 | 9 | if (process.env.NODE_ENV === 'production') { 10 | menu = menu.filter(item => !item.hidden); 11 | } 12 | 13 | function executeAction({ action }) { 14 | sendMessage('menu-action', action); 15 | } 16 | 17 | menu.forEach(menuItem => { 18 | if (menuItem.submenu) { 19 | menuItem.submenu.forEach(item => { 20 | if (item.action && !item.role) { 21 | item.click = executeAction; 22 | } 23 | }); 24 | } 25 | }); 26 | 27 | setApplicationMenu(buildFromTemplate(menu)); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/preload.js: -------------------------------------------------------------------------------- 1 | import { contextBridge } from 'electron'; 2 | import * as api from 'main/api'; 3 | 4 | async function init() { 5 | api.log('Preload script loaded'); 6 | 7 | const env = await api.getGlobal('env'); 8 | 9 | await api.loadPlugins(env.PLUGIN_PATH); 10 | 11 | contextBridge.exposeInMainWorld('__ASTROFOX__', { 12 | ...api, 13 | getEnvironment: () => env, 14 | }); 15 | } 16 | 17 | init(); 18 | -------------------------------------------------------------------------------- /src/shaders/BarrelBlurShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/barrel-blur.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 9 | }, 10 | vertexShader, 11 | fragmentShader, 12 | }; 13 | -------------------------------------------------------------------------------- /src/shaders/BlendShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 2 | import fragmentShader from 'shaders/glsl/fragment/blend.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | baseBuffer: { type: 't', value: null }, 7 | blendBuffer: { type: 't', value: null }, 8 | mode: { type: 'i', value: 0 }, 9 | alpha: { type: 'i', value: 1 }, 10 | opacity: { type: 'f', value: 1.0 }, 11 | mask: { type: 'i', value: 0 }, 12 | inverse: { type: 'i', value: 0 }, 13 | }, 14 | vertexShader, 15 | fragmentShader, 16 | }; 17 | -------------------------------------------------------------------------------- /src/shaders/BoxBlurShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/box-blur.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | amount: { type: 'f', value: 0.0 }, 9 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 10 | }, 11 | vertexShader, 12 | fragmentShader, 13 | }; 14 | -------------------------------------------------------------------------------- /src/shaders/CircularBlurShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/circular-blur.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | amount: { type: 'f', value: 1.0 }, 9 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 10 | }, 11 | vertexShader, 12 | fragmentShader, 13 | }; 14 | -------------------------------------------------------------------------------- /src/shaders/ColorHalftoneShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/color-halftone.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | angle: { type: 'f', value: 1.0 }, 9 | scale: { type: 'f', value: 1.0 }, 10 | center: { type: 'v2', value: new Vector2(0, 0) }, 11 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 12 | }, 13 | vertexShader, 14 | fragmentShader, 15 | }; 16 | -------------------------------------------------------------------------------- /src/shaders/ColorShiftShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 2 | import fragmentShader from 'shaders/glsl/fragment/color-shift.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | time: { type: 'f', value: 1.0 }, 7 | }, 8 | vertexShader, 9 | fragmentShader, 10 | }; 11 | -------------------------------------------------------------------------------- /src/shaders/CopyShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 2 | import fragmentShader from 'shaders/glsl/fragment/copy.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | inputTexture: { type: 't', value: null }, 7 | opacity: { type: 'f', value: 1.0 }, 8 | alpha: { type: 'i', value: 0 }, 9 | }, 10 | vertexShader, 11 | fragmentShader, 12 | }; 13 | -------------------------------------------------------------------------------- /src/shaders/DistortionShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/distortion.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | time: { type: 'f', value: 1.0 }, 9 | amount: { type: 'f', value: 1.0 }, 10 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 11 | }, 12 | vertexShader, 13 | fragmentShader, 14 | }; 15 | -------------------------------------------------------------------------------- /src/shaders/DotScreenShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/dot-screen.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | tSize: { type: 'v2', value: new Vector2(256, 256) }, 9 | center: { type: 'v2', value: new Vector2(0.5, 0.5) }, 10 | angle: { type: 'f', value: 1.57 }, 11 | scale: { type: 'f', value: 1.0 }, 12 | }, 13 | vertexShader, 14 | fragmentShader, 15 | }; 16 | -------------------------------------------------------------------------------- /src/shaders/FXAAShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/fxaa.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 9 | }, 10 | vertexShader, 11 | fragmentShader, 12 | }; 13 | -------------------------------------------------------------------------------- /src/shaders/GaussianBlurShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/gaussian-blur.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | direction: { type: 'v2', value: new Vector2(0, 1) }, 9 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 10 | }, 11 | vertexShader, 12 | fragmentShader, 13 | }; 14 | -------------------------------------------------------------------------------- /src/shaders/GlitchShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 2 | import fragmentShader from 'shaders/glsl/fragment/glitch.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | inputTexture: { type: 't', value: null }, 7 | displacementTexture: { type: 't', value: null }, 8 | shift: { type: 'f', value: 0.08 }, 9 | angle: { type: 'f', value: 0.02 }, 10 | seed: { type: 'f', value: 0.02 }, 11 | seed_x: { type: 'f', value: 0.02 }, // -1,1 12 | seed_y: { type: 'f', value: 0.02 }, // -1,1 13 | distortion_x: { type: 'f', value: 0.5 }, 14 | distortion_y: { type: 'f', value: 0.6 }, 15 | col_s: { type: 'f', value: 0.05 }, 16 | horizontal: { type: 'i', value: 1 }, 17 | vertical: { type: 'i', value: 0 }, 18 | }, 19 | vertexShader, 20 | fragmentShader, 21 | }; 22 | -------------------------------------------------------------------------------- /src/shaders/GlowShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/glow.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | amount: { type: 'f', value: 1.0 }, 9 | intensity: { type: 'f', value: 1.0 }, 10 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 11 | }, 12 | vertexShader, 13 | fragmentShader, 14 | }; 15 | -------------------------------------------------------------------------------- /src/shaders/GridShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 2 | import fragmentShader from 'shaders/glsl/fragment/grid.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | inputTexture: { type: 't', value: null }, 7 | }, 8 | vertexShader, 9 | fragmentShader, 10 | }; 11 | -------------------------------------------------------------------------------- /src/shaders/HalftoneShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'src/shaders/glsl/fragment/color-halftone.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | center: { type: 'v2', value: new Vector2(0.5, 0.5) }, 9 | angle: { type: 'f', value: 1.57 }, 10 | scale: { type: 'f', value: 1.0 }, 11 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 12 | }, 13 | vertexShader, 14 | fragmentShader, 15 | }; 16 | -------------------------------------------------------------------------------- /src/shaders/HexagonShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/hexagon.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | center: { type: 'v2', value: new Vector2(0.5, 0.5) }, 9 | size: { type: 'f', value: 10.0 }, 10 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 11 | }, 12 | vertexShader, 13 | fragmentShader, 14 | }; 15 | -------------------------------------------------------------------------------- /src/shaders/KaleidoscopeShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 2 | import fragmentShader from 'shaders/glsl/fragment/kaleidoscope.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | inputTexture: { type: 't', value: null }, 7 | sides: { type: 'f', value: 0 }, 8 | angle: { type: 'f', value: 0 }, 9 | }, 10 | vertexShader, 11 | fragmentShader, 12 | }; 13 | -------------------------------------------------------------------------------- /src/shaders/LEDShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/led.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | spacing: { type: 'f', value: 10.0 }, 9 | size: { type: 'f', value: 4.0 }, 10 | blur: { type: 'f', value: 4.0 }, 11 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 12 | }, 13 | vertexShader, 14 | fragmentShader, 15 | }; 16 | -------------------------------------------------------------------------------- /src/shaders/LensBlurShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/lens-blur.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | extraBuffer: { type: 't', value: null }, 9 | delta0: { type: 'v2', value: new Vector2(1, 1) }, 10 | delta1: { type: 'v2', value: new Vector2(1, 1) }, 11 | power: { type: 'f', value: 1.0 }, 12 | pass: { type: 'i', value: 0 }, 13 | }, 14 | vertexShader, 15 | fragmentShader, 16 | }; 17 | -------------------------------------------------------------------------------- /src/shaders/LuminanceShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 2 | import fragmentShader from 'shaders/glsl/fragment/luminance.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | inputTexture: { type: 't', value: null }, 7 | amount: { type: 'f', value: 0.0 }, 8 | }, 9 | vertexShader, 10 | fragmentShader, 11 | }; 12 | -------------------------------------------------------------------------------- /src/shaders/MirrorShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 2 | import fragmentShader from 'shaders/glsl/fragment/mirror.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | inputTexture: { type: 't', value: null }, 7 | side: { type: 'i', value: 1 }, 8 | }, 9 | vertexShader, 10 | fragmentShader, 11 | }; 12 | -------------------------------------------------------------------------------- /src/shaders/PixelateShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/pixelate.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | size: { type: 'f', value: 10 }, 9 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 10 | }, 11 | vertexShader, 12 | fragmentShader, 13 | }; 14 | -------------------------------------------------------------------------------- /src/shaders/PointShader.js: -------------------------------------------------------------------------------- 1 | import { Color } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/point.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/point.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | opacity: { type: 'f', value: 1.0 }, 9 | color: { type: 'c', value: new Color(0xffffff) }, 10 | }, 11 | vertexShader, 12 | fragmentShader, 13 | alphaTest: 0.9, 14 | }; 15 | -------------------------------------------------------------------------------- /src/shaders/RGBShiftShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 2 | import fragmentShader from 'shaders/glsl/fragment/rgb-shift.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | inputTexture: { type: 't', value: null }, 7 | amount: { type: 'f', value: 0.005 }, 8 | angle: { type: 'f', value: 0.0 }, 9 | }, 10 | vertexShader, 11 | fragmentShader, 12 | }; 13 | -------------------------------------------------------------------------------- /src/shaders/RippleShader.js: -------------------------------------------------------------------------------- 1 | import vertexShader from 'src/shaders/glsl/vertex/ripple.glsl'; 2 | import fragmentShader from 'src/shaders/glsl/fragment/ripple.glsl'; 3 | 4 | export default { 5 | uniforms: { 6 | inputTexture: { type: 't', value: null }, 7 | time: { type: 'f', value: 0.0 }, 8 | size: { type: 'f', value: 0.0 }, 9 | depth: { type: 'f', value: 0.0 }, 10 | }, 11 | vertexShader, 12 | fragmentShader, 13 | }; 14 | -------------------------------------------------------------------------------- /src/shaders/TriangleBlurShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/triangle-blur.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | delta: { type: 'v2', value: new Vector2(1, 1) }, 9 | }, 10 | vertexShader, 11 | fragmentShader, 12 | }; 13 | -------------------------------------------------------------------------------- /src/shaders/ZoomBlurShader.js: -------------------------------------------------------------------------------- 1 | import { Vector2 } from 'three'; 2 | import vertexShader from 'shaders/glsl/vertex/basic.glsl'; 3 | import fragmentShader from 'shaders/glsl/fragment/zoom-blur.glsl'; 4 | 5 | export default { 6 | uniforms: { 7 | inputTexture: { type: 't', value: null }, 8 | center: { type: 'v2', value: new Vector2(0.5, 0.5) }, 9 | amount: { type: 'f', value: 1.0 }, 10 | resolution: { type: 'v2', value: new Vector2(1, 1) }, 11 | }, 12 | vertexShader, 13 | fragmentShader, 14 | }; 15 | -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/color-halftone.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform float angle; 3 | uniform float scale; 4 | uniform vec2 resolution; 5 | varying vec2 vUv; 6 | 7 | float pattern(float angle) { 8 | float s = sin(angle), c = cos(angle); 9 | vec2 center = vec2(resolution.x * 0.5, resolution.y * 0.5); 10 | vec2 tex = vUv * resolution - center; 11 | vec2 point = vec2(c * tex.x - s * tex.y, s * tex.x + c * tex.y) * scale; 12 | 13 | return (sin(point.x) * sin(point.y)) * 4.0; 14 | } 15 | 16 | void main() { 17 | vec4 color = texture2D(inputTexture, vUv); 18 | vec3 cmy = 1.0 - color.rgb; 19 | float k = min(cmy.x, min(cmy.y, cmy.z)); 20 | 21 | cmy = (cmy - k) / (1.0 - k); 22 | cmy = clamp(cmy * 10.0 - 3.0 + vec3(pattern(angle + 0.26179), pattern(angle + 1.30899), pattern(angle)), 0.0, 1.0); 23 | k = clamp(k * 10.0 - 5.0 + pattern(angle + 0.78539), 0.0, 1.0); 24 | 25 | gl_FragColor = vec4(1.0 - cmy - k, color.a); 26 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/color-shift.glsl: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | varying vec2 vUv; 3 | 4 | void main( void ) { 5 | vec2 position = -1.0 + 2.0 * vUv; 6 | 7 | float red = abs(sin(position.x * position.y + time / 5.0)); 8 | float green = abs(sin(position.x * position.y + time / 4.0)); 9 | float blue = abs(sin(position.x * position.y + time / 3.0)); 10 | 11 | gl_FragColor = vec4(red, green, blue, 1.0); 12 | } 13 | -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/copy.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform float opacity; 3 | uniform int alpha; 4 | varying vec2 vUv; 5 | 6 | void main() { 7 | vec4 texture = texture2D(inputTexture, vUv); 8 | 9 | gl_FragColor = opacity * texture; 10 | 11 | if (alpha == 1) { 12 | gl_FragColor.rgb /= gl_FragColor.a + 0.00001; 13 | } 14 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/distortion.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform float time; 3 | uniform float amount; 4 | uniform vec2 resolution; 5 | varying vec2 vUv; 6 | 7 | void main() { 8 | vec2 uv1 = vUv; 9 | vec2 uv = gl_FragCoord.xy/resolution.xy; 10 | float frequency = 6.0; 11 | float amplitude = 0.015 * amount; 12 | float x = uv1.y * frequency + time * .7; 13 | float y = uv1.x * frequency + time * .3; 14 | uv1.x += cos(x+y) * amplitude * cos(y); 15 | uv1.y += sin(x-y) * amplitude * cos(y); 16 | 17 | gl_FragColor = texture2D(inputTexture, uv1); 18 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/dot-screen.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform vec2 center; 3 | uniform float angle; 4 | uniform float scale; 5 | uniform vec2 tSize; 6 | varying vec2 vUv; 7 | 8 | float pattern() { 9 | float s = sin(angle), c = cos(angle); 10 | 11 | vec2 tex = vUv * tSize - center; 12 | vec2 point = vec2(c * tex.x - s * tex.y, s * tex.x + c * tex.y) * scale; 13 | 14 | float p = (sin( point.x ) * sin( point.y )) * 4.0; 15 | 16 | return p; 17 | } 18 | 19 | void main() { 20 | vec4 color = texture2D(inputTexture, vUv); 21 | 22 | float average = (color.r + color.g + color.b) / 3.0; 23 | 24 | gl_FragColor = vec4(vec3(average * 10.0 - 5.0 + pattern()), color.a); 25 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/kaleidoscope.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform float sides; 3 | uniform float angle; 4 | varying vec2 vUv; 5 | 6 | void main() { 7 | vec2 p = vUv - 0.5; 8 | float r = length(p); 9 | float a = atan(p.y, p.x) + angle; 10 | float tau = 2. * 3.1416; 11 | a = mod(a, tau/sides); 12 | a = abs(a - tau/sides/2.); 13 | p = r * vec2(cos(a), sin(a)); 14 | 15 | gl_FragColor = texture2D(inputTexture, p + 0.5); 16 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/led.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform float spacing; 3 | uniform float size; 4 | uniform float blur; 5 | uniform vec2 resolution; 6 | varying vec2 vUv; 7 | 8 | void main() { 9 | vec2 count = vec2(resolution / spacing); 10 | vec2 p = floor(vUv * count) / count; 11 | 12 | vec4 color = texture2D(inputTexture, p); 13 | 14 | vec2 pos = mod(gl_FragCoord.xy, vec2(spacing)) - vec2(spacing / 2.0); 15 | float dist_squared = dot(pos, pos); 16 | 17 | gl_FragColor = mix(color, vec4(0.0), smoothstep(size, size + blur, dist_squared)); 18 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/lens-blur.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform sampler2D extraBuffer; 3 | uniform vec2 delta0; 4 | uniform vec2 delta1; 5 | uniform float power; 6 | uniform int pass; 7 | varying vec2 vUv; 8 | 9 | #include "../func/random.glsl" 10 | 11 | vec4 blur(vec2 delta) { 12 | float offset = random(delta); 13 | vec4 color = vec4(0.0); 14 | float total = 0.0; 15 | 16 | for (float t = 0.0; t <= 30.0; t++) { 17 | float percent = (t + offset) / 30.0; 18 | color += texture2D(inputTexture, vUv + delta * percent); 19 | total += 1.0; 20 | } 21 | 22 | return color / total; 23 | } 24 | 25 | void main() { 26 | if (pass == 0) { 27 | vec4 color = texture2D(inputTexture, vUv); 28 | gl_FragColor = pow(color, vec4(power)); 29 | } 30 | 31 | if (pass == 1) { 32 | gl_FragColor = blur(delta0); 33 | } 34 | 35 | if (pass == 2) { 36 | gl_FragColor = (blur(delta0) + blur(delta1)) * 0.5; 37 | } 38 | 39 | if (pass == 3) { 40 | vec4 color = (blur(delta0) + 2.0 * texture2D(extraBuffer, vUv)) / 3.0; 41 | gl_FragColor = pow(color, vec4(power)); 42 | } 43 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/luminance.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform float amount; 3 | varying vec2 vUv; 4 | 5 | const vec3 vLuma = vec3(0.2126, 0.7152, 0.0722); 6 | 7 | void main(void) { 8 | vec4 src = texture2D(inputTexture, vUv); 9 | float luma = dot(vLuma, src.rgb); 10 | 11 | luma = max(0.0, luma - amount); 12 | 13 | gl_FragColor = src * sign(luma); 14 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/mirror.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform int side; 3 | varying vec2 vUv; 4 | 5 | void main() { 6 | vec2 p = vUv; 7 | 8 | if (side == 0) { 9 | if (p.x > 0.5) p.x = 1.0 - p.x; 10 | } 11 | else if (side == 1) { 12 | if (p.x < 0.5) p.x = 1.0 - p.x; 13 | } 14 | else if (side == 2) { 15 | if (p.y < 0.5) p.y = 1.0 - p.y; 16 | } 17 | else if (side == 3) { 18 | if (p.y > 0.5) p.y = 1.0 - p.y; 19 | } 20 | 21 | vec4 color = texture2D(inputTexture, p); 22 | 23 | gl_FragColor = color; 24 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/pixelate.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform float size; 3 | uniform vec2 resolution; 4 | varying vec2 vUv; 5 | 6 | void main() { 7 | float d = size / resolution.x; 8 | float u = floor(vUv.x / d) * d; 9 | 10 | d = size / resolution.y; 11 | float v = floor(vUv.y / d) * d; 12 | 13 | gl_FragColor = texture2D(inputTexture, vec2(u, v)); 14 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/point.glsl: -------------------------------------------------------------------------------- 1 | uniform vec3 color; 2 | uniform sampler2D inputTexture; 3 | uniform float opacity; 4 | varying vec3 vColor; 5 | 6 | void main() { 7 | gl_FragColor = vec4(color * vColor, 1.0) * texture2D(inputTexture, gl_PointCoord); 8 | 9 | if (gl_FragColor.a < ALPHATEST) { 10 | discard; 11 | } 12 | 13 | gl_FragColor.a = gl_FragColor.a * opacity; 14 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/rgb-shift.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform float amount; 3 | uniform float angle; 4 | varying vec2 vUv; 5 | 6 | void main() { 7 | vec2 offset = amount * vec2(cos(angle), sin(angle)); 8 | vec4 cr = texture2D(inputTexture, vUv + offset); 9 | vec4 cg = texture2D(inputTexture, vUv); 10 | vec4 cb = texture2D(inputTexture, vUv - offset); 11 | float opacity = cr.a + cg.a + cb.a; 12 | 13 | gl_FragColor = vec4(cr.r, cg.g, cb.b, opacity); 14 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/ripple.glsl: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | varying vec2 vUv; 3 | varying float vNoise; 4 | 5 | const vec3 black = vec3(0.0, 0.0, 0.0); 6 | 7 | vec3 hsv2rgb(vec3 c) { 8 | vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); 9 | vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); 10 | return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 11 | } 12 | 13 | void main() { 14 | vec3 c = hsv2rgb(vec3((vUv.x + time, 0.8, 0.7))); 15 | 16 | gl_FragColor = vec4(mix(c, black, -vNoise), 1.0); 17 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/triangle-blur.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform vec2 delta; 3 | varying vec2 vUv; 4 | 5 | #include "../func/random.glsl" 6 | 7 | void main() { 8 | vec4 color = vec4(0.0); 9 | float total = 0.0; 10 | float offset = random(vUv); 11 | 12 | for (float t = -30.0; t <= 30.0; t++) { 13 | float percent = (t + offset - 0.5) / 30.0; 14 | float weight = 1.0 - abs(percent); 15 | 16 | color += texture2D(inputTexture, vUv + delta * percent) * weight; 17 | total += weight; 18 | } 19 | 20 | gl_FragColor = color / total; 21 | } -------------------------------------------------------------------------------- /src/shaders/glsl/fragment/zoom-blur.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D inputTexture; 2 | uniform vec2 center; 3 | uniform float amount; 4 | uniform vec2 resolution; 5 | varying vec2 vUv; 6 | 7 | #include "../func/random.glsl" 8 | 9 | void main() { 10 | vec4 color = vec4(0.0); 11 | float total = 0.0; 12 | vec2 c = center * resolution; 13 | vec2 toCenter = c - vUv * resolution; 14 | float offset = random(vUv); 15 | 16 | for (float t = 0.0; t <= 40.0; t++) { 17 | float percent = (t) / 40.0; 18 | float weight = 4.0 * (percent - percent * percent); 19 | vec4 s = texture2D(inputTexture, vUv + toCenter * percent * amount / resolution); 20 | color += s * weight; 21 | total += weight; 22 | } 23 | 24 | gl_FragColor = color / total; 25 | } 26 | -------------------------------------------------------------------------------- /src/shaders/glsl/func/random.glsl: -------------------------------------------------------------------------------- 1 | float random(vec2 co){ 2 | return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453); 3 | } -------------------------------------------------------------------------------- /src/shaders/glsl/vertex/basic.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() { 4 | vUv = uv; 5 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 6 | } -------------------------------------------------------------------------------- /src/shaders/glsl/vertex/normal.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | varying vec3 vNormal; 3 | 4 | void main() { 5 | vUv = uv; 6 | vNormal = normalize(normalMatrix * normal); 7 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 8 | } -------------------------------------------------------------------------------- /src/shaders/glsl/vertex/point.glsl: -------------------------------------------------------------------------------- 1 | attribute float size; 2 | attribute vec3 customColor; 3 | 4 | varying vec3 vColor; 5 | 6 | void main() { 7 | vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); 8 | 9 | vColor = customColor; 10 | 11 | gl_PointSize = size * (300.0 / -mvPosition.z); 12 | 13 | gl_Position = projectionMatrix * mvPosition; 14 | } -------------------------------------------------------------------------------- /src/shaders/glsl/vertex/position.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | varying vec3 vPos; 3 | 4 | void main() { 5 | vUv = uv; 6 | vPos = position; 7 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 8 | } -------------------------------------------------------------------------------- /src/shaders/glsl/vertex/ripple.glsl: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform float size; 3 | uniform float depth; 4 | varying vec2 vUv; 5 | varying float vNoise; 6 | 7 | #include "../func/simplex-noise-2d.glsl" 8 | 9 | void main() { 10 | vUv = uv; 11 | vNoise = snoise(vUv* size + time); 12 | 13 | vec3 newPosition = position + normal * vNoise * depth; 14 | 15 | gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); 16 | } -------------------------------------------------------------------------------- /src/utils/array.js: -------------------------------------------------------------------------------- 1 | export function isDefined(...arr) { 2 | return arr.filter(e => e !== undefined).length > 0; 3 | } 4 | 5 | export function contains(arr1, arr2) { 6 | return arr1.some(e => arr2.includes(e)); 7 | } 8 | 9 | export function reverse(arr) { 10 | return [...arr].reverse(); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/audio.js: -------------------------------------------------------------------------------- 1 | import Audio from 'audio/Audio'; 2 | import { audioContext } from 'view/global'; 3 | 4 | export function loadAudioData(data) { 5 | return new Promise((resolve, reject) => { 6 | const audio = new Audio(audioContext); 7 | 8 | return audio 9 | .load(data) 10 | .then(() => { 11 | resolve(audio); 12 | }) 13 | .catch(error => { 14 | reject(error); 15 | }); 16 | }); 17 | } 18 | 19 | export function downmix(input) { 20 | const { length, numberOfChannels } = input; 21 | const output = new Float32Array(length); 22 | 23 | if (numberOfChannels < 2) { 24 | return input.getChannelData(0); 25 | } 26 | 27 | for (let i = 0; i < numberOfChannels; i++) { 28 | const ch = input.getChannelData(i); 29 | 30 | for (let j = 0; j < length; j++) { 31 | output[j] += ch[j]; 32 | } 33 | } 34 | 35 | return output.map(x => x / numberOfChannels); 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/crypto.js: -------------------------------------------------------------------------------- 1 | const byteToHex = []; 2 | 3 | for (let n = 0; n <= 0xff; n++) { 4 | byteToHex.push(n.toString(16).padStart(2, '0')); 5 | } 6 | 7 | function toHexString(buffer) { 8 | const hexOctets = new Array(buffer.length); 9 | 10 | for (let i = 0; i < buffer.length; i++) { 11 | hexOctets[i] = byteToHex[buffer[i]]; 12 | } 13 | 14 | return hexOctets.join(''); 15 | } 16 | 17 | export function uniqueId() { 18 | return toHexString(window.crypto.getRandomValues(new Uint8Array(20))); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/file.js: -------------------------------------------------------------------------------- 1 | import path from 'path-browserify'; 2 | 3 | export function replaceExt(file, ext) { 4 | const base = path.basename(file, path.extname(file)) + ext; 5 | return path.join(path.dirname(file), base); 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/object.js: -------------------------------------------------------------------------------- 1 | export function updateExistingProps(obj, props) { 2 | let changed = false; 3 | 4 | for (let keys = Object.keys(props), len = keys.length, i = 0; i < len; ++i) { 5 | const key = keys[i]; 6 | if (key in obj) { 7 | const value = props[key]; 8 | if (value !== obj[key]) { 9 | obj[key] = value; 10 | changed = true; 11 | } 12 | } 13 | } 14 | 15 | return changed; 16 | } 17 | 18 | export function resolve(value, args = []) { 19 | return typeof value === 'function' ? value(...args) : value; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/react.js: -------------------------------------------------------------------------------- 1 | import { Children, Fragment, cloneElement } from 'react'; 2 | 3 | export function ignoreEvents(e) { 4 | e.stopPropagation(); 5 | e.preventDefault(); 6 | } 7 | 8 | export function inputValueToProps(callback) { 9 | return (name, value) => callback({ [name]: value }); 10 | } 11 | 12 | export function mapChildren(children, props, callback) { 13 | return Children.map(children, (child, index) => { 14 | if (child) { 15 | if (child.type === Fragment) { 16 | return mapChildren(child.props.children, props); 17 | } 18 | 19 | const args = callback ? callback(child, props, index) : [child, props]; 20 | 21 | return cloneElement(...args); 22 | } 23 | return child; 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/string.js: -------------------------------------------------------------------------------- 1 | export function trimChars(str) { 2 | // eslint-disable-next-line no-control-regex 3 | return str.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/work.js: -------------------------------------------------------------------------------- 1 | export function sleep(ms) { 2 | return new Promise(resolve => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /src/video/MergeProcess.js: -------------------------------------------------------------------------------- 1 | import Process from 'core/Process'; 2 | 3 | export default class MergeProcess extends Process { 4 | start({ inputFiles, outputFile }) { 5 | return new Promise((resolve, reject) => { 6 | const inputs = inputFiles.flatMap(file => ['-i', file]); 7 | 8 | this.on('close', code => { 9 | if (code !== 0) { 10 | reject(new Error('Process terminated.')); 11 | } 12 | resolve(); 13 | }); 14 | 15 | this.on('error', err => { 16 | reject(err); 17 | }); 18 | 19 | this.on('stderr', data => { 20 | this.emit('output', data); 21 | }); 22 | 23 | super.start([ 24 | '-y', 25 | ...inputs, 26 | '-codec', 27 | 'copy', 28 | '-shortest', 29 | '-movflags', 30 | '+faststart', 31 | outputFile, 32 | ]); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/view/actions/config.js: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | import { api, env, logger } from 'global'; 3 | import { uniqueId } from 'utils/crypto'; 4 | import defaultAppConfig from 'config/app.json'; 5 | import { raiseError } from './error'; 6 | 7 | const { APP_CONFIG_FILE } = env; 8 | 9 | const configStore = create(() => ({ 10 | ...defaultAppConfig, 11 | })); 12 | 13 | export async function saveConfig(config) { 14 | try { 15 | await api.saveConfig(config); 16 | 17 | logger.log('Saved config file', APP_CONFIG_FILE, config); 18 | 19 | configStore.setState(config); 20 | } catch (error) { 21 | raiseError('Failed to save config file.', error); 22 | } 23 | } 24 | 25 | export async function loadConfig() { 26 | try { 27 | const config = await api.loadConfig(); 28 | 29 | if (config === null) { 30 | return configStore.setState({ ...defaultAppConfig, uid: uniqueId() }); 31 | } 32 | 33 | logger.log('Loaded config file', APP_CONFIG_FILE, config); 34 | 35 | configStore.setState(config); 36 | } catch (error) { 37 | raiseError('Failed to load config file.', error); 38 | } 39 | } 40 | 41 | export default configStore; 42 | -------------------------------------------------------------------------------- /src/view/actions/error.js: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | import { logger } from 'global'; 3 | import { showModal } from './modals'; 4 | 5 | const initialState = { 6 | error: null, 7 | message: null, 8 | }; 9 | 10 | const errorStore = create(() => ({ 11 | ...initialState, 12 | })); 13 | 14 | export function clearError() { 15 | errorStore.setState({ ...initialState }); 16 | } 17 | 18 | export function raiseError(message, error) { 19 | if (error) { 20 | logger.error(`${message}\n`, error.toString()); 21 | } 22 | 23 | errorStore.setState({ message, error }); 24 | 25 | showModal('ErrorDialog', { title: 'Error' }); 26 | } 27 | 28 | export default errorStore; 29 | -------------------------------------------------------------------------------- /src/view/actions/modals.js: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | import { uniqueId } from 'utils/crypto'; 3 | 4 | const initialState = { 5 | modals: [], 6 | }; 7 | 8 | const modalStore = create(() => ({ 9 | ...initialState, 10 | })); 11 | 12 | export function showModal(component, modalProps, componentProps) { 13 | modalStore.setState(({ modals }) => ({ 14 | modals: modals.concat({ id: uniqueId(), component, modalProps, componentProps }), 15 | })); 16 | } 17 | 18 | export function closeModal() { 19 | modalStore.setState(({ modals }) => ({ modals: modals.slice(0, -1) })); 20 | } 21 | 22 | export default modalStore; 23 | -------------------------------------------------------------------------------- /src/view/actions/reactors.js: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | import { reactors } from 'global'; 3 | import { setActiveReactorId } from './app'; 4 | 5 | const initialState = { 6 | reactors: [], 7 | }; 8 | 9 | const reactorStore = create(() => ({ 10 | ...initialState, 11 | })); 12 | 13 | export function loadReactors() { 14 | reactorStore.setState({ reactors: reactors.toJSON() }); 15 | } 16 | 17 | export function resetReactors() { 18 | reactorStore.setState({ ...initialState }); 19 | 20 | reactors.clearReactors(); 21 | 22 | setActiveReactorId(null); 23 | } 24 | 25 | export function addReactor(reactor) { 26 | const newReactor = reactors.addReactor(reactor); 27 | 28 | loadReactors(); 29 | 30 | return newReactor; 31 | } 32 | 33 | export function removeReactor(reactor) { 34 | reactors.removeReactor(reactor); 35 | 36 | loadReactors(); 37 | } 38 | 39 | export function clearReactors() { 40 | reactors.clearReactors(); 41 | 42 | loadReactors(); 43 | } 44 | 45 | export default reactorStore; 46 | -------------------------------------------------------------------------------- /src/view/actions/video.js: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | import { videoRenderer, player } from 'global'; 3 | 4 | const initialState = { 5 | active: false, 6 | finished: false, 7 | status: '', 8 | totalFrames: 0, 9 | currentFrame: 0, 10 | lastFrame: 0, 11 | startTime: 0, 12 | }; 13 | 14 | const videoStore = create(() => ({ ...initialState })); 15 | 16 | export function startRender(props) { 17 | player.stop(); 18 | 19 | setTimeout(() => { 20 | videoRenderer.start(props); 21 | }, 500); 22 | 23 | videoStore.setState({ ...initialState, active: true }); 24 | } 25 | 26 | export function stopRender() { 27 | const { active } = videoStore.getState(); 28 | 29 | if (active) { 30 | videoRenderer.stop(); 31 | 32 | videoStore.setState(state => ({ ...state, active: false })); 33 | } 34 | } 35 | 36 | export function updateState(props) { 37 | videoStore.setState(state => ({ ...state, ...props })); 38 | } 39 | 40 | export default videoStore; 41 | -------------------------------------------------------------------------------- /src/view/assets/fonts/abel.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/abel.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/abril-fatface.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/abril-fatface.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/alegreya.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/alegreya.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/arimo.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/arimo.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/bangers.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/bangers.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/cardo.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/cardo.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/caveat.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/caveat.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/chunkfive.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/chunkfive.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/dynalight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/dynalight.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/felipa.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/felipa.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/fira_sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/fira_sans.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/intro.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/intro.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/merriweather.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/merriweather.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/oswald.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/oswald.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/oxygen.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/oxygen.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/permanent-marker.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/permanent-marker.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/playfair-display.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/playfair-display.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/racing-sans-one.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/racing-sans-one.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/raleway.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/raleway.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/roboto-condensed.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/roboto-condensed.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/roboto.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/roboto.woff2 -------------------------------------------------------------------------------- /src/view/assets/fonts/vast-shadow.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/fonts/vast-shadow.woff2 -------------------------------------------------------------------------------- /src/view/assets/icons/adjust.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/angle-double-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/angle-double-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/arrows-cw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/arrows-h.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/bar-graph.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/block.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/ccw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/check-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/chevron-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/circle-filled.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/circle-with-cross.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/cloud.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/contrast.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/crop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/cross.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/cube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/cw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/cycle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/document-landscape.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/dots-three-horizontal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/expand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/flash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/flashlight.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/folder-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/hand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/help-with-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/hour-glass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/levels.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/light-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/lightbulb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/lock-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/mask.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/move.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/multiply.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/music.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/picture.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/view/assets/icons/puzzle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/retweet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/selection.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/sound-bars.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/sound-waves.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/square-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/square.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/times-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/times.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/trashcan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/videocam.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/volume2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/volume3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/volume4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/icons/wave.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/assets/images/about_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/about_bg.jpg -------------------------------------------------------------------------------- /src/view/assets/images/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/blank.gif -------------------------------------------------------------------------------- /src/view/assets/images/controls/BarSpectrumDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/BarSpectrumDisplay.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/BloomEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/BloomEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/BlurEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/BlurEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/ColorHalftoneEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/ColorHalftoneEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/DistortionEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/DistortionEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/DotScreenEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/DotScreenEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/GeometryDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/GeometryDisplay.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/GlitchEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/GlitchEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/GlowEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/GlowEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/ImageDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/ImageDisplay.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/KaleidoscopeEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/KaleidoscopeEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/LEDEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/LEDEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/MirrorEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/MirrorEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/PixelateEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/PixelateEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/Plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/Plugin.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/RGBShiftEffect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/RGBShiftEffect.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/ShapeDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/ShapeDisplay.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/SoundWaveDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/SoundWaveDisplay.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/TextDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/TextDisplay.png -------------------------------------------------------------------------------- /src/view/assets/images/controls/WaveSpectrumDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/controls/WaveSpectrumDisplay.png -------------------------------------------------------------------------------- /src/view/assets/images/point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astrofox-io/astrofox/b75945bda8dcfe1f159522743d070807bf378e9d/src/view/assets/images/point.png -------------------------------------------------------------------------------- /src/view/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/components/controls/Control.less: -------------------------------------------------------------------------------- 1 | .control { 2 | padding-bottom: 8px; 3 | } 4 | 5 | .header { 6 | position: relative; 7 | padding: 0 10px; 8 | height: 30px; 9 | cursor: default; 10 | } 11 | 12 | .title { 13 | display: flex; 14 | justify-content: center; 15 | font-size: var(--font-size-xsmall); 16 | color: var(--text100); 17 | line-height: 30px; 18 | overflow: hidden; 19 | } 20 | 21 | .label { 22 | position: relative; 23 | text-transform: uppercase; 24 | padding-right: 20px; 25 | 26 | &:after { 27 | content: '\2022'; 28 | position: absolute; 29 | right: 7px; 30 | color: var(--text300); 31 | } 32 | } 33 | 34 | .displayName { 35 | color: var(--text200); 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | white-space: nowrap; 39 | min-width: 0; 40 | max-width: 100px; 41 | } 42 | 43 | .active { 44 | //border-top: 1px solid var(--primary100); 45 | } 46 | -------------------------------------------------------------------------------- /src/view/components/controls/Option.less: -------------------------------------------------------------------------------- 1 | .option { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | padding: 8px 0; 6 | margin: 0 10px; 7 | position: relative; 8 | font-size: var(--font-size-small); 9 | color: var(--text300); 10 | line-height: 20px; 11 | 12 | > * { 13 | margin-right: 8px; 14 | 15 | &:last-child { 16 | margin-right: 0; 17 | } 18 | } 19 | } 20 | 21 | .label { 22 | display: flex; 23 | margin-left: 20px; 24 | cursor: default; 25 | min-width: 100px; 26 | } 27 | 28 | .text { 29 | flex: 1; 30 | white-space: nowrap; 31 | text-overflow: ellipsis; 32 | overflow: hidden; 33 | margin-right: 8px; 34 | } 35 | 36 | .linkIcon { 37 | color: var(--text500); 38 | width: 12px; 39 | height: 12px; 40 | } 41 | 42 | .linkIconActive { 43 | color: var(--text100); 44 | } 45 | 46 | .reactorIcon { 47 | position: absolute; 48 | margin-left: -5px; 49 | } 50 | 51 | .hidden { 52 | display: none; 53 | } 54 | -------------------------------------------------------------------------------- /src/view/components/controls/OptionGroup.js: -------------------------------------------------------------------------------- 1 | import React, { Children, cloneElement } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './OptionGroup.less'; 4 | 5 | export default function OptionGroup({ title, className, children, ...props }) { 6 | return ( 7 |
8 | {title &&
{title}
} 9 |
10 | {Children.map(children, child => { 11 | if (child) { 12 | return cloneElement(child, { ...props }); 13 | } 14 | return child; 15 | })} 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/view/components/controls/OptionGroup.less: -------------------------------------------------------------------------------- 1 | .group { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .header { 7 | display: flex; 8 | } 9 | 10 | .body { 11 | position: relative; 12 | } 13 | -------------------------------------------------------------------------------- /src/view/components/controls/Setting.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import inputComponents from './inputComponents'; 4 | import styles from './Setting.less'; 5 | 6 | export default function Setting({ 7 | label, 8 | type, 9 | name, 10 | value, 11 | className, 12 | labelWidth, 13 | inputWidth, 14 | onChange, 15 | hidden, 16 | children, 17 | ...otherProps 18 | }) { 19 | const [InputCompnent, inputProps] = inputComponents[type] ?? []; 20 | 21 | return ( 22 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/view/components/controls/Setting.less: -------------------------------------------------------------------------------- 1 | .setting { 2 | display: flex; 3 | align-items: center; 4 | margin-bottom: 16px; 5 | 6 | > * { 7 | margin-right: 8px; 8 | 9 | &:last-child { 10 | margin-right: 0; 11 | } 12 | } 13 | } 14 | 15 | .label { 16 | color: var(--text200); 17 | } 18 | 19 | .hidden { 20 | display: none; 21 | } 22 | -------------------------------------------------------------------------------- /src/view/components/controls/Settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Setting from 'components/controls/Setting'; 3 | import classNames from 'classnames'; 4 | import { inputValueToProps, mapChildren } from 'utils/react'; 5 | import styles from './Settings.less'; 6 | 7 | export default function Settings({ label, columns = [], className, children, onChange }) { 8 | const [labelWidth, inputWidth] = columns; 9 | 10 | function handleClone(child, props) { 11 | if (child.type === Setting) { 12 | return [child, props]; 13 | } 14 | return [child]; 15 | } 16 | 17 | return ( 18 |
19 | {label &&
{label}
} 20 | {mapChildren( 21 | children, 22 | { labelWidth, inputWidth, onChange: inputValueToProps(onChange) }, 23 | handleClone, 24 | )} 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/view/components/controls/Settings.less: -------------------------------------------------------------------------------- 1 | .settings { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 16px; 5 | } 6 | 7 | .label { 8 | color: var(--text400); 9 | font-size: var(--font-size-small); 10 | text-transform: uppercase; 11 | margin-bottom: 16px; 12 | } 13 | -------------------------------------------------------------------------------- /src/view/components/controls/index.js: -------------------------------------------------------------------------------- 1 | export Control from './Control'; 2 | export Option from './Option'; 3 | export OptionGroup from './OptionGroup'; 4 | export Settings from './Settings'; 5 | export Setting from './Setting'; 6 | -------------------------------------------------------------------------------- /src/view/components/controls/inputComponents.js: -------------------------------------------------------------------------------- 1 | import { 2 | CheckboxInput, 3 | ColorInput, 4 | ColorRangeInput, 5 | ImageInput, 6 | NumberInput, 7 | RangeInput, 8 | SelectInput, 9 | TextInput, 10 | TimeInput, 11 | ToggleInput, 12 | } from 'components/inputs'; 13 | 14 | const inputComponents = { 15 | text: [TextInput, { width: 140 }], 16 | number: [NumberInput, { width: 40 }], 17 | toggle: [ToggleInput], 18 | checkbox: [CheckboxInput], 19 | color: [ColorInput], 20 | colorrange: [ColorRangeInput], 21 | range: [RangeInput], 22 | select: [SelectInput, { width: 140 }], 23 | image: [ImageInput], 24 | time: [TimeInput], 25 | }; 26 | 27 | export default inputComponents; 28 | -------------------------------------------------------------------------------- /src/view/components/dialogs/ErrorDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from 'components/window/Dialog'; 3 | import { Warning } from 'view/icons'; 4 | import useError, { clearError } from 'actions/error'; 5 | 6 | export default function ErrorDialog({ onClose }) { 7 | const message = useError(state => state.message); 8 | 9 | function handleConfirm() { 10 | clearError(); 11 | onClose(); 12 | } 13 | 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /src/view/components/dialogs/UnsavedChangesDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from 'components/window/Dialog'; 3 | import useProject, { newProject, openProjectFile, saveProjectFile } from 'actions/project'; 4 | 5 | export default function UnsavedChangesDialog({ action, onClose }) { 6 | const project = useProject(state => state); 7 | 8 | async function handleAction(action) { 9 | if (action === 'new-project') { 10 | await newProject(); 11 | } else if (action === 'open-project') { 12 | await openProjectFile(); 13 | } 14 | } 15 | 16 | async function handleConfirm(button) { 17 | if (button === 'Yes') { 18 | const saved = await saveProjectFile(project.file); 19 | 20 | if (saved) { 21 | await handleAction(action); 22 | } 23 | } else if (button === 'No') { 24 | await handleAction(action); 25 | } 26 | onClose(); 27 | } 28 | 29 | return ( 30 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/view/components/inputs/BoxInput.less: -------------------------------------------------------------------------------- 1 | .box { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | border: 1px solid var(--primary400); 6 | } 7 | 8 | .center { 9 | position: absolute; 10 | cursor: move; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | 15 | .top { 16 | position: absolute; 17 | cursor: ns-resize; 18 | width: 100%; 19 | height: 10px; 20 | top: -5px; 21 | } 22 | 23 | .right { 24 | position: absolute; 25 | cursor: ew-resize; 26 | width: 10px; 27 | height: 100%; 28 | right: -5px; 29 | } 30 | 31 | .bottom { 32 | position: absolute; 33 | cursor: ns-resize; 34 | width: 100%; 35 | height: 10px; 36 | bottom: -5px; 37 | } 38 | 39 | .left { 40 | position: absolute; 41 | cursor: ew-resize; 42 | width: 10px; 43 | height: 100%; 44 | left: -5px; 45 | } 46 | -------------------------------------------------------------------------------- /src/view/components/inputs/ButtonGroup.js: -------------------------------------------------------------------------------- 1 | import React, { Children, cloneElement } from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './ButtonGroup.less'; 4 | 5 | const ButtonGroup = ({ className, children }) => ( 6 |
7 | {Children.map(children, child => 8 | cloneElement(child, { className: classNames(styles.button, child.props.className) }), 9 | )} 10 |
11 | ); 12 | 13 | export default ButtonGroup; 14 | -------------------------------------------------------------------------------- /src/view/components/inputs/ButtonGroup.less: -------------------------------------------------------------------------------- 1 | .group { 2 | margin-right: 5px; 3 | border-color: var(--input-border-color); 4 | 5 | .button { 6 | margin-right: 0; 7 | border-right: 0; 8 | border-radius: 0; 9 | border-color: inherit; 10 | 11 | &:first-child { 12 | border-top-left-radius: 2px; 13 | border-bottom-left-radius: 2px; 14 | } 15 | 16 | &:last-child { 17 | border-top-right-radius: 2px; 18 | border-bottom-right-radius: 2px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/view/components/inputs/ButtonInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import Icon from 'components/interface/Icon'; 4 | import styles from './ButtonInput.less'; 5 | 6 | const ButtonInput = ({ title, icon, text, active, disabled, onClick, className }) => ( 7 |
19 | {icon && } 20 | {text && {text}} 21 |
22 | ); 23 | 24 | export default ButtonInput; 25 | -------------------------------------------------------------------------------- /src/view/components/inputs/ButtonInput.less: -------------------------------------------------------------------------------- 1 | .button { 2 | color: var(--text100); 3 | background-color: var(--input-bg-color); 4 | min-height: 24px; 5 | min-width: 24px; 6 | text-align: center; 7 | border-radius: 2px; 8 | display: inline-flex; 9 | justify-content: center; 10 | align-items: center; 11 | cursor: default; 12 | flex-shrink: 0; 13 | 14 | &:hover { 15 | background-color: var(--primary100); 16 | } 17 | } 18 | 19 | .icon { 20 | color: var(--text100); 21 | width: 12px; 22 | height: 12px; 23 | } 24 | 25 | .text { 26 | font-size: var(--font-size-small); 27 | } 28 | 29 | .active { 30 | background-color: var(--primary100); 31 | } 32 | 33 | .disabled { 34 | svg { 35 | color: var(--gray500); 36 | } 37 | 38 | &:hover { 39 | background-color: var(--input-bg-color); 40 | } 41 | } 42 | 43 | .input { 44 | margin: 0 5px; 45 | } 46 | -------------------------------------------------------------------------------- /src/view/components/inputs/CheckboxInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './CheckboxInput.less'; 4 | 5 | export default function CheckboxInput({ 6 | name = 'checkbox', 7 | value = false, 8 | label, 9 | labelPosition = 'right', 10 | onChange, 11 | }) { 12 | return ( 13 |
14 |
onChange(name, !value)} 19 | /> 20 | {label && ( 21 |
27 | {label} 28 |
29 | )} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/view/components/inputs/CheckboxInput.less: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .input { 7 | order: 1; 8 | position: relative; 9 | width: 16px; 10 | height: 16px; 11 | line-height: 16px; 12 | background-color: var(--input-bg-color); 13 | border: 1px solid var(--input-border-color); 14 | border-radius: var(--input-border-radius); 15 | overflow: hidden; 16 | 17 | &:before { 18 | content: ''; 19 | position: absolute; 20 | width: 16px; 21 | height: 16px; 22 | line-height: 16px; 23 | color: var(--text100); 24 | background-color: var(--input-bg-color); 25 | font-size: var(--font-size-xsmall); 26 | text-align: center; 27 | transform: scale(0.5); 28 | transition: all 0.3s; 29 | } 30 | 31 | &.checked { 32 | &:before { 33 | content: '\2713'; 34 | background-color: var(--primary400); 35 | transform: scale(1); 36 | } 37 | } 38 | } 39 | 40 | .label { 41 | display: inline-block; 42 | } 43 | 44 | .left { 45 | order: 0; 46 | margin-right: 8px; 47 | } 48 | 49 | .right { 50 | order: 2; 51 | margin-left: 8px; 52 | } 53 | -------------------------------------------------------------------------------- /src/view/components/inputs/ColorInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './ColorInput.less'; 3 | 4 | export default function ColorInput({ name = 'color', value = '#ffffff', onChange = () => {} }) { 5 | return ( 6 |
7 | onChange(name, e.target.value)} 14 | /> 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/view/components/inputs/ColorInput.less: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 25px; 6 | height: 25px; 7 | border-radius: 100%; 8 | border: 1px solid var(--input-border-color); 9 | background-color: var(--input-bg-color); 10 | } 11 | 12 | .input { 13 | width: 15px; 14 | height: 15px; 15 | border-radius: 100%; 16 | border: 0; 17 | 18 | &::-webkit-color-swatch-wrapper { 19 | display: none; 20 | } 21 | 22 | &::-webkit-color-swatch { 23 | display: none; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/view/components/inputs/ColorRangeInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ColorInput } from 'components/inputs/index'; 3 | import styles from './ColorRangeInput.less'; 4 | 5 | export default function ColorRangeInput({ 6 | name = 'color', 7 | value = ['#ffffff', '#ffffff'], 8 | onChange, 9 | }) { 10 | const [startColor, endColor] = value; 11 | 12 | return ( 13 |
14 | onChange(name, [value, endColor])} 18 | /> 19 |
25 | onChange(name, [startColor, value])} 29 | /> 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/view/components/inputs/ColorRangeInput.less: -------------------------------------------------------------------------------- 1 | .input { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | width: 100%; 6 | } 7 | 8 | .range { 9 | flex: 1; 10 | position: relative; 11 | height: 16px; 12 | border: 1px solid var(--input-border-color); 13 | border-radius: 3px; 14 | margin: 0 8px; 15 | } 16 | -------------------------------------------------------------------------------- /src/view/components/inputs/ReactorButton.less: -------------------------------------------------------------------------------- 1 | .icon { 2 | color: var(--text500); 3 | width: 16px; 4 | height: 16px; 5 | 6 | &:hover { 7 | color: var(--text100); 8 | } 9 | } 10 | 11 | .iconActive { 12 | color: var(--text100); 13 | } 14 | -------------------------------------------------------------------------------- /src/view/components/inputs/ReactorInput.less: -------------------------------------------------------------------------------- 1 | .reactor { 2 | position: relative; 3 | display: flex; 4 | flex-direction: row; 5 | } 6 | 7 | .meter { 8 | display: flex; 9 | align-items: center; 10 | height: 24px; 11 | background-color: var(--input-bg-color); 12 | border: 1px solid var(--input-border-color); 13 | border-radius: 2px; 14 | padding: 0 8px; 15 | } 16 | 17 | .closeIcon { 18 | margin: 2px 0 0 8px; 19 | color: var(--text200); 20 | width: 14px; 21 | height: 14px; 22 | 23 | &:hover { 24 | color: var(--text100); 25 | } 26 | } 27 | 28 | .hidden { 29 | display: none; 30 | } 31 | -------------------------------------------------------------------------------- /src/view/components/inputs/TextInput.less: -------------------------------------------------------------------------------- 1 | .input { 2 | font-size: var(--font-size-small); 3 | color: var(--input-text-color); 4 | background-color: var(--input-bg-color); 5 | border: 1px solid var(--input-border-color); 6 | border-radius: 2px; 7 | line-height: 24px; 8 | padding: 0 6px; 9 | outline: none; 10 | 11 | &:focus { 12 | border: 1px solid var(--primary100); 13 | } 14 | 15 | &:read-only { 16 | border-color: var(--input-border-color); 17 | } 18 | 19 | &:disabled { 20 | color: var(--text400); 21 | border-color: var(--input-border-color); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/view/components/inputs/ToggleInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './ToggleInput.less'; 4 | 5 | export default function ToggleInput({ 6 | name = 'toggle', 7 | value = false, 8 | label, 9 | labelPosition = 'right', 10 | onChange, 11 | }) { 12 | return ( 13 |
14 |
onChange(name, !value)} 19 | /> 20 | {label && ( 21 |
27 | {label} 28 |
29 | )} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/view/components/inputs/ToggleInput.less: -------------------------------------------------------------------------------- 1 | .toggle { 2 | display: flex; 3 | } 4 | 5 | .input { 6 | position: relative; 7 | height: 17px; 8 | width: 32px; 9 | border-radius: 17px; 10 | background-color: var(--input-bg-color); 11 | border: 1px solid var(--input-bg-color); 12 | transition: background-color 0.25s; 13 | order: 1; 14 | 15 | &:before { 16 | position: absolute; 17 | top: 50%; 18 | left: 0; 19 | transform: translateY(-50%); 20 | content: ''; 21 | width: 15px; 22 | height: 15px; 23 | border-radius: 15px; 24 | background-color: var(--text100); 25 | border: 1px solid var(--input-border-color); 26 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.3); 27 | transition: left 0.25s; 28 | } 29 | } 30 | 31 | .label { 32 | display: inline-block; 33 | } 34 | 35 | .left { 36 | order: 0; 37 | margin-right: 8px; 38 | } 39 | 40 | .right { 41 | order: 2; 42 | margin-left: 8px; 43 | } 44 | 45 | .on { 46 | background-color: var(--primary100); 47 | 48 | &:before { 49 | left: 15px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/view/components/inputs/index.js: -------------------------------------------------------------------------------- 1 | export BoxInput from './BoxInput'; 2 | export ButtonGroup from './ButtonGroup'; 3 | export ButtonInput from './ButtonInput'; 4 | export CheckboxInput from './CheckboxInput'; 5 | export ColorInput from './ColorInput'; 6 | export ColorRangeInput from './ColorRangeInput'; 7 | export ImageInput from './ImageInput'; 8 | export NumberInput from './NumberInput'; 9 | export RangeInput from './RangeInput'; 10 | export ReactorButton from './ReactorButton'; 11 | export ReactorInput from './ReactorInput'; 12 | export SelectInput from './SelectInput'; 13 | export TextInput from './TextInput'; 14 | export TimeInput from './TimeInput'; 15 | export ToggleInput from './ToggleInput'; 16 | -------------------------------------------------------------------------------- /src/view/components/interface/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Button.less'; 4 | 5 | const Button = ({ text, disabled, className, onClick }) => { 6 | return ( 7 | 13 | {text} 14 | 15 | ); 16 | }; 17 | 18 | export default Button; 19 | -------------------------------------------------------------------------------- /src/view/components/interface/Button.less: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | background-color: var(--primary100); 4 | padding: 8px 10px; 5 | border-radius: 3px; 6 | cursor: default; 7 | margin-right: 10px; 8 | 9 | &:last-child { 10 | margin-right: 0; 11 | } 12 | 13 | &:hover { 14 | background-color: var(--primary200); 15 | } 16 | 17 | &.disabled { 18 | color: var(--text200); 19 | background-color: var(--gray400); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/view/components/interface/Checkmark.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Checkmark.less'; 4 | 5 | const Checkmark = ({ size, className }) => ( 6 |
13 | 14 | 15 | 16 | 17 |
18 | ); 19 | 20 | export default Checkmark; 21 | -------------------------------------------------------------------------------- /src/view/components/interface/Checkmark.less: -------------------------------------------------------------------------------- 1 | @keyframes checkmark { 2 | 0% { 3 | stroke-dashoffset: 50px; 4 | } 5 | 100% { 6 | stroke-dashoffset: 0; 7 | } 8 | } 9 | 10 | @keyframes checkmark-circle { 11 | 0% { 12 | stroke-dashoffset: 240px; 13 | } 14 | 100% { 15 | stroke-dashoffset: 480px; 16 | } 17 | } 18 | 19 | .checkmark { 20 | display: block; 21 | } 22 | 23 | .svg { 24 | display: inline-block; 25 | transform-origin: center center; 26 | } 27 | 28 | .circle { 29 | stroke: var(--primary100); 30 | stroke-width: 2; 31 | fill: none; 32 | stroke-dasharray: 240px, 240px; 33 | stroke-dashoffset: 480px; 34 | animation: checkmark-circle 0.6s ease-in-out backwards; 35 | } 36 | 37 | .path { 38 | stroke: var(--primary100); 39 | stroke-width: 2; 40 | fill: none; 41 | stroke-dasharray: 50px, 50px; 42 | stroke-dashoffset: 0px; 43 | animation: checkmark 0.25s ease-in-out 0.7s backwards; 44 | } 45 | -------------------------------------------------------------------------------- /src/view/components/interface/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Icon.less'; 4 | 5 | const Icon = ({ 6 | className, 7 | width, 8 | height, 9 | title, 10 | glyph: { viewBox, url }, 11 | shapeRendering, 12 | onClick, 13 | }) => ( 14 | 15 | 22 | 23 | 24 | 25 | ); 26 | 27 | Icon.defaultProps = { 28 | shapeRendering: 'geometricPrecision', 29 | }; 30 | 31 | export default Icon; 32 | -------------------------------------------------------------------------------- /src/view/components/interface/Icon.less: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: inline-flex; 3 | align-self: center; 4 | position: relative; 5 | color: var(--text100); 6 | width: 24px; 7 | height: 24px; 8 | 9 | svg { 10 | color: inherit; 11 | fill: currentColor; 12 | width: inherit; 13 | height: inherit; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/view/components/interface/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Spinner.less'; 4 | 5 | const Spinner = ({ size, className }) => ( 6 |
13 | 14 | 23 | 24 |
25 | ); 26 | 27 | export default Spinner; 28 | -------------------------------------------------------------------------------- /src/view/components/interface/Spinner.less: -------------------------------------------------------------------------------- 1 | @keyframes rotate { 2 | 0% { 3 | transform: rotate(0deg); 4 | } 5 | 100% { 6 | transform: rotate(360deg); 7 | } 8 | } 9 | 10 | @keyframes spinner-dash { 11 | 0% { 12 | stroke-dasharray: 1, 200; 13 | stroke-dashoffset: 0; 14 | } 15 | 50% { 16 | stroke-dasharray: 89, 200; 17 | stroke-dashoffset: -35; 18 | } 19 | 100% { 20 | stroke-dasharray: 89, 200; 21 | stroke-dashoffset: -124; 22 | } 23 | } 24 | 25 | .spinner { 26 | position: relative; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | 32 | .svg { 33 | animation: rotate 2s linear infinite; 34 | width: 100%; 35 | height: 100%; 36 | transform-origin: center center; 37 | } 38 | 39 | .circle { 40 | stroke: var(--primary100); 41 | stroke-linecap: round; 42 | stroke-dasharray: 1, 200; 43 | stroke-dashoffset: 0; 44 | animation: spinner-dash 1.5s ease-in-out infinite; 45 | } 46 | -------------------------------------------------------------------------------- /src/view/components/interface/index.js: -------------------------------------------------------------------------------- 1 | export Button from 'components/interface/Button'; 2 | export Checkmark from 'components/interface/Checkmark'; 3 | export Icon from 'components/interface/Icon'; 4 | export Spinner from 'components/interface/Spinner'; 5 | -------------------------------------------------------------------------------- /src/view/components/layout/ButtonPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './ButtonPanel.less'; 3 | 4 | export default function ButtonPanel({ children }) { 5 | return
{children}
; 6 | } 7 | -------------------------------------------------------------------------------- /src/view/components/layout/ButtonPanel.less: -------------------------------------------------------------------------------- 1 | .buttons { 2 | display: flex; 3 | padding: 5px; 4 | width: 100%; 5 | background-color: var(--gray75); 6 | border-top: 1px solid var(--gray300); 7 | border-bottom: 1px solid var(--gray50); 8 | overflow: hidden; 9 | 10 | & > * { 11 | margin-right: 5px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/view/components/layout/ButtonRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './ButtonRow.less'; 3 | 4 | export default function ButtonRow({ children }) { 5 | return
{children}
; 6 | } 7 | -------------------------------------------------------------------------------- /src/view/components/layout/ButtonRow.less: -------------------------------------------------------------------------------- 1 | .buttons { 2 | text-align: center; 3 | margin-bottom: 16px; 4 | } 5 | -------------------------------------------------------------------------------- /src/view/components/layout/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './Layout.less'; 4 | 5 | export default function Layout({ 6 | className, 7 | children, 8 | direction = 'column', 9 | grow = true, 10 | full = false, 11 | width, 12 | height, 13 | padding, 14 | margin, 15 | ...props 16 | }) { 17 | return ( 18 |
28 | {children} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/view/components/layout/Layout.less: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | overflow: hidden; 4 | position: relative; 5 | } 6 | 7 | .row { 8 | flex-direction: row; 9 | } 10 | 11 | .column { 12 | flex-direction: column; 13 | } 14 | 15 | .grow { 16 | flex: 1; 17 | } 18 | 19 | .full { 20 | width: 100%; 21 | height: 100%; 22 | } 23 | -------------------------------------------------------------------------------- /src/view/components/layout/Panel.less: -------------------------------------------------------------------------------- 1 | .panel { 2 | display: flex; 3 | position: relative; 4 | 5 | &.vertical { 6 | flex-direction: column; 7 | flex-shrink: 0; 8 | } 9 | 10 | &.horizontal { 11 | flex-direction: row; 12 | flex-shrink: 0; 13 | } 14 | 15 | &.stretch { 16 | flex: 1; 17 | overflow: hidden; 18 | } 19 | } 20 | 21 | .header { 22 | display: flex; 23 | height: 30px; 24 | line-height: 30px; 25 | flex-shrink: 0; 26 | } 27 | 28 | .title { 29 | font-size: var(--font-size-small); 30 | color: var(--text200); 31 | text-transform: uppercase; 32 | margin-left: 10px; 33 | cursor: default; 34 | } 35 | -------------------------------------------------------------------------------- /src/view/components/layout/PanelDock.less: -------------------------------------------------------------------------------- 1 | .dock { 2 | display: flex; 3 | position: relative; 4 | overflow: hidden; 5 | background-color: var(--gray100); 6 | 7 | &.vertical { 8 | flex-direction: column; 9 | flex-shrink: 0; 10 | } 11 | 12 | &.horizontal { 13 | flex-direction: row; 14 | flex-shrink: 0; 15 | } 16 | 17 | &.left { 18 | border-right: 1px solid var(--gray75); 19 | } 20 | 21 | &.right { 22 | border-left: 1px solid var(--gray75); 23 | } 24 | 25 | &.top { 26 | border-bottom: 1px solid var(--gray75); 27 | } 28 | 29 | &.bottom { 30 | border-top: 1px solid var(--gray75); 31 | } 32 | } 33 | 34 | .hidden { 35 | display: none; 36 | } 37 | -------------------------------------------------------------------------------- /src/view/components/layout/Splitter.less: -------------------------------------------------------------------------------- 1 | .splitter { 2 | background-color: var(--gray75); 3 | text-align: center; 4 | position: relative; 5 | 6 | &.horizontal { 7 | height: 5px; 8 | width: 100%; 9 | cursor: ns-resize; 10 | 11 | > .grip { 12 | display: block; 13 | position: absolute; 14 | margin: 0 auto; 15 | top: -4px; 16 | left: 0; 17 | right: 0; 18 | } 19 | } 20 | 21 | &.vertical { 22 | width: 5px; 23 | height: 100%; 24 | cursor: ew-resize; 25 | } 26 | } 27 | 28 | .grip { 29 | color: var(--text200); 30 | width: 12px; 31 | height: 12px; 32 | } 33 | -------------------------------------------------------------------------------- /src/view/components/layout/TabPanel.less: -------------------------------------------------------------------------------- 1 | .panel { 2 | display: flex; 3 | flex: 1; 4 | } 5 | 6 | .tabs { 7 | background-color: var(--gray75); 8 | } 9 | 10 | .tab { 11 | text-align: center; 12 | list-style: none; 13 | padding: 8px 16px; 14 | cursor: default; 15 | 16 | &.active { 17 | background-color: var(--primary100); 18 | } 19 | } 20 | 21 | .horizontal { 22 | display: flex; 23 | flex-direction: row; 24 | } 25 | 26 | .content { 27 | width: 100%; 28 | overflow: auto; 29 | 30 | .hidden { 31 | display: none; 32 | } 33 | } 34 | 35 | .positionTop { 36 | flex-direction: column; 37 | } 38 | 39 | .positionBottom { 40 | flex-direction: column; 41 | 42 | .tabs { 43 | order: 99; 44 | } 45 | } 46 | 47 | .positionLeft { 48 | flex-direction: row; 49 | 50 | .tabs { 51 | width: 160px; 52 | } 53 | } 54 | 55 | .positionRight { 56 | flex-direction: row; 57 | 58 | .tabs { 59 | order: 99; 60 | width: 160px; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/view/components/modals/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { env } from 'global'; 3 | import Button from 'components/interface/Button'; 4 | import styles from './About.less'; 5 | 6 | const { APP_NAME, APP_VERSION } = env; 7 | 8 | export default function About({ onClose }) { 9 | return ( 10 |
11 |
{APP_NAME}
12 |
{`Version ${APP_VERSION}`}
13 |
{'Copyright \u00A9 Mike Cao'}
14 |
15 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/view/components/modals/About.less: -------------------------------------------------------------------------------- 1 | .about { 2 | color: var(--text100); 3 | text-align: center; 4 | cursor: default; 5 | padding: 30px; 6 | background: url(../../assets/images/about_bg.jpg) no-repeat center center fixed; 7 | } 8 | 9 | .name { 10 | font-family: Oswald, sans-serif; 11 | font-weight: 100; 12 | font-size: 24px; 13 | text-transform: uppercase; 14 | letter-spacing: 4px; 15 | margin-bottom: 30px; 16 | } 17 | 18 | .version { 19 | margin-bottom: 5px; 20 | } 21 | 22 | .copyright { 23 | margin-bottom: 30px; 24 | color: var(--text200); 25 | } 26 | 27 | .license-info { 28 | margin-bottom: 30px; 29 | color: var(--text200); 30 | font-weight: bold; 31 | } 32 | 33 | .buttons { 34 | margin-top: 20px; 35 | } 36 | -------------------------------------------------------------------------------- /src/view/components/modals/AppUpdates.less: -------------------------------------------------------------------------------- 1 | .message { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | padding: 30px; 6 | } 7 | 8 | .icon { 9 | margin-right: 20px; 10 | } 11 | 12 | .buttons { 13 | text-align: center; 14 | } 15 | 16 | .button { 17 | margin: 10px 5px; 18 | } 19 | -------------------------------------------------------------------------------- /src/view/components/modals/ControlPicker.less: -------------------------------------------------------------------------------- 1 | .panel { 2 | width: 720px; 3 | height: 400px; 4 | } 5 | 6 | .picker { 7 | display: flex; 8 | flex-direction: row; 9 | flex-wrap: wrap; 10 | align-items: center; 11 | justify-content: center; 12 | padding: 10px; 13 | } 14 | 15 | .item { 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | margin-bottom: 10px; 20 | width: 110px; 21 | } 22 | 23 | .image { 24 | background: #000 center; 25 | border: 2px solid var(--gray300); 26 | border-radius: 7px; 27 | height: 84px; 28 | width: 84px; 29 | transition: border-color 0.5s; 30 | overflow: hidden; 31 | 32 | img { 33 | width: 100%; 34 | height: auto; 35 | } 36 | 37 | &:hover { 38 | border-color: var(--primary100); 39 | transition: none; 40 | } 41 | } 42 | 43 | .name { 44 | font-size: var(--font-size-small); 45 | text-align: center; 46 | margin: 5px 0; 47 | } 48 | -------------------------------------------------------------------------------- /src/view/components/modals/VideoSettings.less: -------------------------------------------------------------------------------- 1 | .button { 2 | margin-left: 8px; 3 | } 4 | -------------------------------------------------------------------------------- /src/view/components/modals/index.js: -------------------------------------------------------------------------------- 1 | export About from 'components/modals/About'; 2 | export AppUpdates from 'components/modals/AppUpdates'; 3 | export AppSettings from 'components/modals/AppSettings'; 4 | export CanvasSettings from 'components/modals/CanvasSettings'; 5 | export ControlPicker from 'components/modals/ControlPicker'; 6 | export VideoSettings from 'components/modals/VideoSettings'; 7 | export UnsavedChangesDialog from 'components/dialogs/UnsavedChangesDialog'; 8 | export ErrorDialog from 'components/dialogs/ErrorDialog'; 9 | -------------------------------------------------------------------------------- /src/view/components/nav/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import MenuItem from './MenuItem'; 4 | import styles from './Menu.less'; 5 | 6 | const Menu = ({ items, visible, onMenuItemClick }) => ( 7 |
12 | {items.map((item, index) => { 13 | const { type, label, hidden, checked, disabled } = item; 14 | 15 | if (type === 'separator') { 16 | return
; 17 | } else if (label && !hidden) { 18 | return ( 19 | onMenuItemClick(item)} 25 | /> 26 | ); 27 | } 28 | 29 | return null; 30 | })} 31 |
32 | ); 33 | 34 | Menu.defaultProps = { 35 | items: [], 36 | visible: false, 37 | }; 38 | 39 | export default Menu; 40 | -------------------------------------------------------------------------------- /src/view/components/nav/Menu.less: -------------------------------------------------------------------------------- 1 | .menu { 2 | position: absolute; 3 | top: 100%; 4 | left: 0; 5 | list-style: none; 6 | background-color: var(--gray100); 7 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.5); 8 | overflow: hidden; 9 | z-index: var(--z-index-menu); 10 | } 11 | 12 | .separator { 13 | padding: 5px; 14 | 15 | &:after { 16 | content: ''; 17 | border-top: 1px solid var(--primary100); 18 | display: block; 19 | } 20 | 21 | &:hover { 22 | background-color: transparent; 23 | } 24 | } 25 | 26 | .hidden { 27 | display: none; 28 | } 29 | -------------------------------------------------------------------------------- /src/view/components/nav/MenuBar.less: -------------------------------------------------------------------------------- 1 | .bar { 2 | position: relative; 3 | font-size: var(--font-size-normal); 4 | color: var(--text400); 5 | background-color: var(--gray75); 6 | padding: 0 20px; 7 | -webkit-app-region: no-drag; 8 | } 9 | 10 | .focused { 11 | color: var(--text300); 12 | } 13 | -------------------------------------------------------------------------------- /src/view/components/nav/MenuBarItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import Menu from 'components/nav/Menu'; 4 | import styles from './MenuBarItem.less'; 5 | 6 | export default function MenuBarItem({ 7 | label, 8 | items, 9 | active, 10 | onClick, 11 | onMouseOver, 12 | onMenuItemClick, 13 | }) { 14 | function handleClick(e) { 15 | e.stopPropagation(); 16 | onClick(); 17 | } 18 | 19 | function handleMouseOver(e) { 20 | e.stopPropagation(); 21 | onMouseOver(); 22 | } 23 | 24 | return ( 25 |
26 |
31 | {label} 32 |
33 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/view/components/nav/MenuBarItem.less: -------------------------------------------------------------------------------- 1 | .item { 2 | display: inline-block; 3 | position: relative; 4 | } 5 | 6 | .text { 7 | line-height: 40px; 8 | padding: 0 8px; 9 | position: relative; 10 | cursor: default; 11 | 12 | &.active { 13 | color: var(--primary300); 14 | background-color: var(--gray50); 15 | } 16 | 17 | &:hover { 18 | color: var(--primary300); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/view/components/nav/MenuItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './MenuItem.less'; 4 | 5 | const MenuItem = ({ label, checked, disabled, onClick }) => ( 6 |
13 | {label} 14 |
15 | ); 16 | 17 | export default MenuItem; 18 | -------------------------------------------------------------------------------- /src/view/components/nav/MenuItem.less: -------------------------------------------------------------------------------- 1 | .item { 2 | position: relative; 3 | display: block; 4 | font-size: var(--font-size-small); 5 | padding: 5px 5px 5px 20px; 6 | min-width: 140px; 7 | 8 | &:hover { 9 | color: var(--text100); 10 | background-color: var(--primary100); 11 | cursor: default; 12 | } 13 | } 14 | 15 | .checked { 16 | &:before { 17 | content: '\2713'; 18 | color: var(--text100); 19 | position: absolute; 20 | top: 5px; 21 | left: 5px; 22 | } 23 | 24 | &:hover:before { 25 | color: var(--text100); 26 | } 27 | } 28 | 29 | .disabled { 30 | color: var(--text300); 31 | 32 | &:hover { 33 | color: var(--text300); 34 | background-color: transparent; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/view/components/panels/ControlDock.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Panel from 'components/layout/Panel'; 3 | import PanelDock from 'components/layout/PanelDock'; 4 | import ControlsPanel from 'components/panels/ControlsPanel'; 5 | import LayersPanel from 'components/panels/LayersPanel'; 6 | import useApp from 'actions/app'; 7 | 8 | export default function ControlDock() { 9 | const showControlDock = useApp(state => state.showControlDock); 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/view/components/panels/ControlsPanel.less: -------------------------------------------------------------------------------- 1 | .panel { 2 | flex: 1; 3 | overflow: auto; 4 | position: relative; 5 | padding: 0 5px; 6 | margin-bottom: 6px; 7 | } 8 | 9 | .control { 10 | background-color: var(--gray200); 11 | border-top: 1px solid var(--gray400); 12 | border-left: 1px solid var(--gray300); 13 | border-bottom: 1px solid var(--gray50); 14 | margin-bottom: 6px; 15 | 16 | &:last-child { 17 | margin-bottom: 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/view/components/panels/Layer.less: -------------------------------------------------------------------------------- 1 | .layer { 2 | display: flex; 3 | flex-direction: row; 4 | font-size: var(--font-size-small); 5 | color: var(--text100); 6 | background-color: var(--gray200); 7 | border-bottom: 1px solid var(--gray75); 8 | padding: 5px; 9 | margin: 0 5px; 10 | position: relative; 11 | cursor: default; 12 | 13 | & > * { 14 | margin-right: 8px; 15 | 16 | &:last-child { 17 | margin-right: 0; 18 | } 19 | } 20 | 21 | &:after { 22 | content: '\00a0'; 23 | } 24 | } 25 | 26 | .input { 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | min-width: 0; 31 | margin-right: 24px; 32 | } 33 | 34 | .active { 35 | background-color: var(--primary100); 36 | } 37 | 38 | .edit { 39 | background-color: var(--gray100); 40 | } 41 | 42 | .text { 43 | flex: 1; 44 | } 45 | 46 | .icon { 47 | width: 12px; 48 | height: 12px; 49 | } 50 | 51 | .enableIcon { 52 | width: 13px; 53 | height: 13px; 54 | 55 | &.disabled { 56 | opacity: 0.3; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/view/components/panels/LayersPanel.less: -------------------------------------------------------------------------------- 1 | .panel { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1; 5 | position: relative; 6 | overflow: auto; 7 | } 8 | 9 | .layers { 10 | flex: 1; 11 | overflow: auto; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/view/components/panels/RenderPanel.less: -------------------------------------------------------------------------------- 1 | .panel { 2 | background-color: var(--gray75); 3 | overflow: hidden; 4 | z-index: var(--z-index-render-panel); 5 | } 6 | 7 | .progress { 8 | background-color: var(--gray200); 9 | display: flex; 10 | } 11 | 12 | .progressBar { 13 | height: 5px; 14 | flex: 1; 15 | } 16 | 17 | .status { 18 | padding: 10px; 19 | } 20 | 21 | .stats { 22 | padding: 10px; 23 | } 24 | 25 | .row { 26 | display: flex; 27 | flex-direction: row; 28 | flex-wrap: wrap; 29 | justify-content: space-between; 30 | } 31 | 32 | .label { 33 | color: var(--text200); 34 | margin-right: 8px; 35 | } 36 | 37 | .info { 38 | display: flex; 39 | color: var(--text100); 40 | } 41 | -------------------------------------------------------------------------------- /src/view/components/panels/SceneLayer.less: -------------------------------------------------------------------------------- 1 | .contaainer { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | &:first-child { 6 | border-top: 1px solid var(--gray75); 7 | } 8 | } 9 | 10 | .children { 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | .child { 16 | padding-left: 20px; 17 | } 18 | 19 | .hidden { 20 | display: none; 21 | } 22 | -------------------------------------------------------------------------------- /src/view/components/player/AudioWaveform.less: -------------------------------------------------------------------------------- 1 | .waveform { 2 | min-width: 900px; 3 | position: relative; 4 | background-color: var(--gray75); 5 | border-top: 1px solid var(--gray200); 6 | box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.5); 7 | max-height: 200px; 8 | transition: max-height 0.2s ease-out; 9 | overflow: hidden; 10 | } 11 | 12 | .canvas { 13 | margin: 20px auto; 14 | display: block; 15 | } 16 | 17 | .hidden { 18 | max-height: 0; 19 | transition: max-height 0.2s ease-in; 20 | } 21 | -------------------------------------------------------------------------------- /src/view/components/player/Oscilloscope.less: -------------------------------------------------------------------------------- 1 | .oscilloscope { 2 | min-width: 900px; 3 | position: relative; 4 | background-color: var(--gray75); 5 | padding-bottom: 10px; 6 | } 7 | 8 | .canvas { 9 | display: block; 10 | margin: 0 auto; 11 | border: 1px solid var(--gray200); 12 | box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.5); 13 | } 14 | -------------------------------------------------------------------------------- /src/view/components/player/PlayButtons.less: -------------------------------------------------------------------------------- 1 | .buttons { 2 | white-space: nowrap; 3 | } 4 | 5 | .button { 6 | color: var(--text100); 7 | background-color: transparent; 8 | padding: 0; 9 | margin-right: 4px; 10 | display: inline-block; 11 | flex-wrap: nowrap; 12 | border: 2px solid var(--gray300); 13 | height: 40px; 14 | width: 40px; 15 | border-radius: 20px; 16 | line-height: 36px; 17 | text-align: center; 18 | vertical-align: middle; 19 | transition: all 0.2s; 20 | 21 | &:last-child { 22 | margin-right: 0; 23 | } 24 | 25 | &:hover { 26 | border: 2px solid var(--primary100); 27 | } 28 | 29 | &:active { 30 | border-color: var(--text100); 31 | } 32 | } 33 | 34 | .playButton { 35 | .icon { 36 | width: 36px; 37 | height: 36px; 38 | margin-left: 2px; 39 | } 40 | } 41 | 42 | .pauseButton { 43 | .icon { 44 | width: 24px; 45 | height: 24px; 46 | margin: 6px; 47 | } 48 | } 49 | 50 | .stopButton { 51 | .icon { 52 | width: 24px; 53 | height: 24px; 54 | margin: 6px; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/view/components/player/Player.less: -------------------------------------------------------------------------------- 1 | .player { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | min-width: 500px; 6 | overflow: hidden; 7 | padding: 10px 20px; 8 | background-color: var(--gray75); 9 | border-top: 1px solid var(--gray200); 10 | 11 | & > div { 12 | margin-right: 20px; 13 | 14 | &:last-child { 15 | margin-right: 0; 16 | } 17 | } 18 | } 19 | 20 | .hidden { 21 | display: none; 22 | } 23 | -------------------------------------------------------------------------------- /src/view/components/player/ProgressControl.less: -------------------------------------------------------------------------------- 1 | .progress { 2 | display: flex; 3 | align-items: center; 4 | flex: 1; 5 | } 6 | 7 | .bar { 8 | width: 100%; 9 | margin-right: 20px; 10 | } 11 | -------------------------------------------------------------------------------- /src/view/components/player/Spectrum.less: -------------------------------------------------------------------------------- 1 | .spectrum { 2 | min-width: 900px; 3 | position: relative; 4 | background-color: var(--gray75); 5 | padding-bottom: 10px; 6 | } 7 | 8 | .canvas { 9 | display: block; 10 | margin: 0 auto; 11 | border: 1px solid var(--gray200); 12 | box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.5); 13 | } 14 | -------------------------------------------------------------------------------- /src/view/components/player/TimeInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { formatTime } from 'utils/format'; 3 | import styles from './TimeInfo.less'; 4 | 5 | export default function TimeInfo({ currentTime, totalTime }) { 6 | return ( 7 |
8 |
{formatTime(currentTime)}
9 |
10 |
{formatTime(totalTime)}
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/view/components/player/TimeInfo.less: -------------------------------------------------------------------------------- 1 | .timeInfo { 2 | display: flex; 3 | align-items: center; 4 | line-height: 36px; 5 | white-space: nowrap; 6 | 7 | &:hover { 8 | cursor: default; 9 | } 10 | } 11 | 12 | .currentTime { 13 | color: var(--text100); 14 | } 15 | 16 | .totalTime { 17 | color: var(--text200); 18 | } 19 | 20 | .splitter { 21 | display: inline-flex; 22 | height: 30px; 23 | border-right: 1px solid var(--primary100); 24 | margin: 0 8px; 25 | transform: rotate(20deg); 26 | } 27 | -------------------------------------------------------------------------------- /src/view/components/player/ToggleButtons.less: -------------------------------------------------------------------------------- 1 | .buttons { 2 | display: flex; 3 | 4 | &.isFocused { 5 | .button { 6 | opacity: 1; 7 | } 8 | } 9 | } 10 | 11 | .button { 12 | margin-right: 10px; 13 | 14 | &:last-child { 15 | margin-right: 0; 16 | } 17 | 18 | .icon { 19 | color: var(--text500); 20 | width: 16px; 21 | height: 16px; 22 | 23 | &:hover { 24 | color: var(--text300); 25 | } 26 | } 27 | } 28 | 29 | .enabled { 30 | .icon { 31 | color: var(--text100); 32 | 33 | &:hover { 34 | color: var(--text100); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/view/components/player/VolumeControl.less: -------------------------------------------------------------------------------- 1 | .volume { 2 | display: flex; 3 | } 4 | 5 | .slider { 6 | display: flex; 7 | align-items: center; 8 | width: 100px; 9 | } 10 | 11 | .speaker { 12 | margin-right: 10px; 13 | 14 | .icon { 15 | color: var(--text100); 16 | width: 20px; 17 | height: 20px; 18 | } 19 | } 20 | 21 | .mute { 22 | .icon { 23 | color: var(--text300); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/view/components/window/Dialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import Button from 'components/interface/Button'; 4 | import ButtonRow from 'components/layout/ButtonRow'; 5 | import styles from './Dialog.less'; 6 | 7 | export default function Dialog({ icon, message, buttons, onConfirm }) { 8 | return ( 9 |
10 |
11 | {icon &&
} 12 |
{message}
13 |
14 | {buttons && ( 15 | 16 | {buttons.map(button => ( 17 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/view/components/window/Dialog.less: -------------------------------------------------------------------------------- 1 | .dialog { 2 | max-width: 600px; 3 | cursor: default; 4 | width: 100%; 5 | } 6 | 7 | .body { 8 | display: flex; 9 | flex-direction: row; 10 | padding: 40px; 11 | text-align: center; 12 | } 13 | 14 | .icon { 15 | font-size: 48px; 16 | margin-right: 20px; 17 | } 18 | 19 | .message { 20 | flex: 1; 21 | } 22 | -------------------------------------------------------------------------------- /src/view/components/window/Modals.less: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | z-index: var(--z-index-modal-overlay); 10 | perspective: 800px; 11 | } 12 | 13 | .modal { 14 | display: flex; 15 | transform-style: preserve-3d; 16 | z-index: var(--z-index-modal-window); 17 | } 18 | 19 | .hidden { 20 | display: none; 21 | } 22 | -------------------------------------------------------------------------------- /src/view/components/window/Overlay.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { animated, useTransition } from 'react-spring'; 3 | import { easeInQuad } from 'utils/easing'; 4 | import styles from './Overlay.less'; 5 | 6 | export default function Overlay({ show, duration = 300, opacity = 0.5, easing = easeInQuad }) { 7 | const transitions = useTransition(show, { 8 | from: { opacity: 0 }, 9 | enter: { opacity }, 10 | leave: { opacity: 0 }, 11 | config: { duration, easing }, 12 | }); 13 | 14 | return transitions( 15 | (style, item) => item && , 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/view/components/window/Overlay.less: -------------------------------------------------------------------------------- 1 | .overlay { 2 | content: ''; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background: #000; 9 | opacity: 0.5; 10 | z-index: var(--z-index-overlay); 11 | } 12 | -------------------------------------------------------------------------------- /src/view/components/window/Preload.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import fonts from 'config/fonts.json'; 3 | import styles from './Preload.less'; 4 | 5 | export default function Preload() { 6 | return ( 7 |
8 | {fonts.map(font => ( 9 | 10 | {font} 11 | 12 | ))} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/view/components/window/Preload.less: -------------------------------------------------------------------------------- 1 | @import (reference) '~view/styles/mixins'; 2 | 3 | .preload { 4 | .off-screen(); 5 | } 6 | -------------------------------------------------------------------------------- /src/view/components/window/StatusBar.less: -------------------------------------------------------------------------------- 1 | .statusBar { 2 | display: flex; 3 | color: var(--text100); 4 | background-color: var(--primary100); 5 | font-size: 11px; 6 | padding: 0 20px; 7 | cursor: default; 8 | white-space: nowrap; 9 | z-index: var(--z-index-above); 10 | } 11 | 12 | .item { 13 | display: inline-block; 14 | line-height: 28px; 15 | } 16 | 17 | .left { 18 | text-align: left; 19 | width: 33%; 20 | 21 | .item { 22 | margin-right: 20px; 23 | } 24 | } 25 | 26 | .center { 27 | text-align: center; 28 | flex: 1; 29 | width: 34%; 30 | 31 | .item { 32 | margin: 0 10px; 33 | } 34 | } 35 | 36 | .right { 37 | text-align: right; 38 | width: 33%; 39 | 40 | .item { 41 | margin-left: 20px; 42 | } 43 | } 44 | 45 | .itemButton { 46 | padding: 0 8px; 47 | 48 | &:hover { 49 | background-color: var(--primary200); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/view/components/window/TitleBar.less: -------------------------------------------------------------------------------- 1 | .titlebar { 2 | display: flex; 3 | position: relative; 4 | height: 40px; 5 | background-color: var(--gray75); 6 | border-bottom: 1px solid var(--gray300); 7 | -webkit-app-region: no-drag; 8 | 9 | &:before { 10 | content: ''; 11 | position: absolute; 12 | top: 4px; 13 | left: 4px; 14 | right: 4px; 15 | bottom: 0; 16 | margin: auto; 17 | -webkit-app-region: drag; 18 | } 19 | 20 | &.focused { 21 | .title { 22 | color: var(--text300); 23 | } 24 | } 25 | } 26 | 27 | .title { 28 | position: absolute; 29 | left: 50%; 30 | transform: translateX(-50%); 31 | font-size: var(--font-size-normal); 32 | color: var(--text400); 33 | line-height: 40px; 34 | letter-spacing: 5px; 35 | text-transform: uppercase; 36 | cursor: default; 37 | } 38 | 39 | @media only screen and (max-width: 700px) { 40 | .title { 41 | display: none; 42 | } 43 | } 44 | 45 | .icon { 46 | width: 32px; 47 | height: 32px; 48 | top: 4px; 49 | left: 4px; 50 | } 51 | -------------------------------------------------------------------------------- /src/view/components/window/WindowButtons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { api } from 'view/global'; 4 | import styles from './WindowButtons.less'; 5 | 6 | export default function WindowButtons({ focused, maximized }) { 7 | function minimize() { 8 | api.minimizeWindow(); 9 | } 10 | 11 | function maximize() { 12 | api.maximizeWindow(); 13 | } 14 | 15 | function close() { 16 | api.closeWindow(); 17 | } 18 | 19 | return ( 20 |
21 |
22 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/view/components/window/ZoomControl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useStage, { setZoom, zoomIn, zoomOut, fitToScreen } from 'actions/stage'; 3 | import styles from './ZoomControl.less'; 4 | 5 | export default function Zoom() { 6 | const { width, height, zoom } = useStage(state => state); 7 | 8 | return ( 9 |
10 |
setZoom(1)}> 11 | {`${width} x ${height}`} 12 |
13 |
14 | {'\uff0d'} 15 |
16 | setZoom(e.target.value)} 22 | min={0.1} 23 | max={1.0} 24 | step={0.02} 25 | /> 26 |
27 | {'\uff0b'} 28 |
29 |
30 | {`${~~(zoom * 100)}%`} 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/view/constants.js: -------------------------------------------------------------------------------- 1 | export const DISPLAY_TYPE_CANVAS = 'canvas'; 2 | export const DISPLAY_TYPE_WEBGL = 'webgl'; 3 | export const DISPLAY_TYPE_SCENE = 'scene'; 4 | export const DISPLAY_TYPE_EFFECT = 'effect'; 5 | 6 | export const FFT_SIZE = 1024; 7 | export const SAMPLE_RATE = 44100; 8 | export const MAX_FFT_SIZE = 32768; 9 | 10 | export const DEFAULT_CANVAS_WIDTH = 854; 11 | export const DEFAULT_CANVAS_HEIGHT = 480; 12 | export const DEFAULT_CANVAS_BGCOLOR = '#000000'; 13 | export const DEFAULT_ZOOM = 1.0; 14 | export const MIN_CANVAS_WIDTH = 240; 15 | export const MIN_CANVAS_HEIGHT = 240; 16 | export const MAX_CANVAS_WIDTH = 3840; 17 | export const MAX_CANVAS_HEIGHT = 2160; 18 | 19 | export const PRIMARY_COLOR = '#704dd8'; 20 | 21 | export const REACTOR_BARS = 64; 22 | export const REACTOR_BAR_WIDTH = 8; 23 | export const REACTOR_BAR_HEIGHT = 100; 24 | export const REACTOR_BAR_SPACING = 1; 25 | 26 | export const WEBGL_BUFFER_SAMPLES = 4; 27 | 28 | export const BLANK_IMAGE = 29 | 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; 30 | -------------------------------------------------------------------------------- /src/view/global.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'core/EventEmitter'; 2 | import Logger from 'core/Logger'; 3 | import Renderer from 'core/Renderer'; 4 | import Reactors from 'core/Reactors'; 5 | import Stage from 'core/Stage'; 6 | import Player from 'audio/Player'; 7 | import SpectrumAnalyzer from 'audio/SpectrumAnalyzer'; 8 | import VideoRenderer from 'video/VideoRenderer'; 9 | import { SAMPLE_RATE } from './constants'; 10 | 11 | export const api = window.__ASTROFOX__; 12 | export const env = api.getEnvironment(); 13 | export const audioContext = new window.AudioContext({ sampleRate: SAMPLE_RATE}); 14 | export const logger = new Logger('astrofox'); 15 | export const events = new EventEmitter(); 16 | export const stage = new Stage(); 17 | export const player = new Player(audioContext); 18 | export const analyzer = new SpectrumAnalyzer(audioContext); 19 | export const reactors = new Reactors(); 20 | export const renderer = new Renderer(); 21 | export const videoRenderer = new VideoRenderer(renderer); 22 | export const library = new Map(); 23 | -------------------------------------------------------------------------------- /src/view/hooks/useCombinedRefs.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | 3 | export default function useCombinedRefs(...refs) { 4 | const targetRef = useRef(); 5 | 6 | useEffect(() => { 7 | refs.forEach(ref => { 8 | if (!ref) return; 9 | 10 | if (typeof ref === 'function') { 11 | ref(targetRef.current); 12 | } else { 13 | ref.current = targetRef.current; 14 | } 15 | }); 16 | }, [refs]); 17 | 18 | return targetRef; 19 | } 20 | -------------------------------------------------------------------------------- /src/view/hooks/useDebounce.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export default function useDebounce(value, delay) { 4 | const [debouncedValue, setValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => { 8 | setValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(timer); 13 | }; 14 | }, [value]); 15 | 16 | return debouncedValue; 17 | } 18 | -------------------------------------------------------------------------------- /src/view/hooks/useEntity.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import useForceUpdate from 'hooks/useForceUpdate'; 3 | import useTimeout from 'hooks/useTimeout'; 4 | import { touchProject } from 'actions/project'; 5 | 6 | export default function useEntity(entity, touchTimeout = 1000) { 7 | const forceUpdate = useForceUpdate(); 8 | const touch = useTimeout(() => touchProject(), touchTimeout); 9 | 10 | return useCallback( 11 | props => { 12 | if (entity?.update(props)) { 13 | if (touchTimeout) { 14 | touch(); 15 | } 16 | forceUpdate(); 17 | } 18 | }, 19 | [entity], 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/view/hooks/useForceUpdate.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | export default function useForceUpdate() { 4 | const [, update] = useState(Object.create(null)); 5 | 6 | return useCallback(() => { 7 | update(Object.create(null)); 8 | }, [update]); 9 | } 10 | -------------------------------------------------------------------------------- /src/view/hooks/useMeasure.js: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useLayoutEffect } from 'react'; 2 | 3 | export default function useMeasure() { 4 | const [dimensions, setDimensions] = useState({}); 5 | const [node, setNode] = useState(null); 6 | 7 | const ref = useCallback(node => setNode(node), []); 8 | 9 | const measure = useCallback(() => { 10 | window.requestAnimationFrame(() => { 11 | if (node) { 12 | setDimensions(node.getBoundingClientRect().toJSON()); 13 | } 14 | }); 15 | }, [node]); 16 | 17 | useLayoutEffect(() => { 18 | if (node) { 19 | measure(); 20 | 21 | window.addEventListener('resize', measure); 22 | window.addEventListener('scroll', measure); 23 | 24 | return () => { 25 | window.removeEventListener('resize', measure); 26 | window.removeEventListener('scroll', measure); 27 | }; 28 | } 29 | }, [node]); 30 | 31 | return [ref, dimensions, measure]; 32 | } 33 | -------------------------------------------------------------------------------- /src/view/hooks/useMergeState.js: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | 3 | export default function useMergeState(initialState) { 4 | const [state, setState] = useState(initialState); 5 | 6 | const callback = useCallback( 7 | props => { 8 | if (typeof props === 'function') { 9 | setState(state => ({ ...state, ...props(state) })); 10 | } else { 11 | setState(state => ({ ...state, ...props })); 12 | } 13 | }, 14 | [setState], 15 | ); 16 | 17 | return [state, callback]; 18 | } 19 | -------------------------------------------------------------------------------- /src/view/hooks/useMouseDrag.js: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback } from 'react'; 2 | 3 | export default function useMouseDrag() { 4 | const eventHandlers = useRef(); 5 | 6 | const handleMouseMove = useCallback(e => { 7 | const { onDrag } = eventHandlers.current; 8 | 9 | if (onDrag) { 10 | onDrag(e); 11 | } 12 | }, []); 13 | 14 | const handleMouseUp = useCallback(e => { 15 | const { onDragEnd } = eventHandlers.current; 16 | 17 | window.removeEventListener('mousemove', handleMouseMove); 18 | window.removeEventListener('mousemove', handleMouseUp); 19 | 20 | if (onDragEnd) { 21 | onDragEnd(e); 22 | } 23 | }, []); 24 | 25 | function startDrag(e, props = {}) { 26 | e.persist(); 27 | 28 | const { onDrag, onDragStart, onDragEnd } = props; 29 | 30 | eventHandlers.current = { onDrag, onDragStart, onDragEnd }; 31 | 32 | window.addEventListener('mousemove', handleMouseMove); 33 | window.addEventListener('mouseup', handleMouseUp); 34 | 35 | if (onDragStart) { 36 | onDragStart(e); 37 | } 38 | } 39 | 40 | return startDrag; 41 | } 42 | -------------------------------------------------------------------------------- /src/view/hooks/useSharedState.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | let listeners = []; 4 | let state = {}; 5 | 6 | function setState(newState) { 7 | state = { ...state, ...newState }; 8 | listeners.forEach(listener => { 9 | listener(state); 10 | }); 11 | } 12 | 13 | export default function useSharedState(initialState) { 14 | const [, newListener] = useState(); 15 | 16 | if (initialState && Object.keys(state).length === 0) { 17 | state = initialState; 18 | } 19 | 20 | useEffect(() => { 21 | listeners.push(newListener); 22 | 23 | return () => { 24 | listeners = listeners.filter(e => e !== newListener); 25 | }; 26 | }, []); 27 | 28 | return [state, setState]; 29 | } 30 | -------------------------------------------------------------------------------- /src/view/hooks/useTimeout.js: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback } from 'react'; 2 | 3 | export default function useTimeout(func, delay) { 4 | const timer = useRef(); 5 | 6 | return useCallback(() => { 7 | if (timer.current) { 8 | clearTimeout(timer.current); 9 | } 10 | timer.current = setTimeout(func, delay); 11 | }, [func, delay]); 12 | } 13 | -------------------------------------------------------------------------------- /src/view/hooks/useWindowState.js: -------------------------------------------------------------------------------- 1 | import { api } from 'global'; 2 | import { useEffect, useCallback, useState } from 'react'; 3 | 4 | export default function useWindowState() { 5 | const [state, setState] = useState({}); 6 | 7 | const updateState = useCallback( 8 | newState => { 9 | setState(newState); 10 | }, 11 | [api], 12 | ); 13 | 14 | useEffect(() => { 15 | api.on('window-state-changed', updateState); 16 | 17 | return () => { 18 | api.off('window-state-changed', updateState); 19 | }; 20 | }, [api]); 21 | 22 | return state; 23 | } 24 | -------------------------------------------------------------------------------- /src/view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Astrofox 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/view/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from 'components/App'; 4 | import * as globals from './global'; 5 | import 'styles/index.less'; 6 | import './fonts.css'; 7 | import './index.html'; 8 | 9 | // Development settings 10 | if (process.env.NODE_ENV !== 'production') { 11 | window._astrofox = globals; 12 | } 13 | 14 | // Production settings 15 | if (process.env.NODE_ENV === 'production') { 16 | // Disable eval 17 | // eslint-disable-next-line 18 | window.eval = global.eval = undefined; 19 | } 20 | 21 | const container = document.getElementById('app'); 22 | const root = createRoot(container); 23 | 24 | root.render(); 25 | -------------------------------------------------------------------------------- /src/view/stores.js: -------------------------------------------------------------------------------- 1 | export audioStore from './actions/audio'; 2 | export configStore from './actions/config'; 3 | export errorStore from './actions/error'; 4 | export modalStore from './actions/modals'; 5 | export projectStore from './actions/project'; 6 | export reactorStore from './actions/reactors'; 7 | export sceneStore from './actions/scenes'; 8 | export stageStore from './actions/stage'; 9 | export updateStore from './actions/updates'; 10 | export videoStore from './actions/video'; 11 | -------------------------------------------------------------------------------- /src/view/styles/global.less: -------------------------------------------------------------------------------- 1 | :global { 2 | html, 3 | body { 4 | font-family: var(--font-family); 5 | font-size: var(--font-size-normal); 6 | color: var(--text100); 7 | background-color: var(--background-color); 8 | height: 100%; 9 | width: 100%; 10 | margin: 0; 11 | padding: 0; 12 | overflow: hidden; 13 | box-sizing: border-box; 14 | } 15 | 16 | *, 17 | *:before, 18 | *:after { 19 | box-sizing: inherit; 20 | } 21 | 22 | canvas { 23 | display: block; 24 | } 25 | 26 | ::selection { 27 | color: var(--text100); 28 | background-color: var(--primary100); 29 | } 30 | 31 | #app { 32 | width: 100%; 33 | height: 100%; 34 | -webkit-user-select: none; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/view/styles/index.less: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'global'; 3 | @import 'inputs'; 4 | @import 'scrollbars'; 5 | -------------------------------------------------------------------------------- /src/view/styles/inputs.less: -------------------------------------------------------------------------------- 1 | input[type='range'] { 2 | -webkit-appearance: none; 3 | outline: none; 4 | } 5 | 6 | input[type='color'] { 7 | -webkit-appearance: none; 8 | outline: none; 9 | } 10 | -------------------------------------------------------------------------------- /src/view/styles/mixins.less: -------------------------------------------------------------------------------- 1 | .absolute-center() { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | margin: auto; 8 | } 9 | 10 | .off-screen() { 11 | position: fixed; 12 | left: 100%; 13 | bottom: -100%; 14 | height: 0; 15 | width: 0; 16 | overflow: hidden; 17 | z-index: var(--z-index-hidden); 18 | } 19 | -------------------------------------------------------------------------------- /src/view/styles/scrollbars.less: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 10px; 3 | height: 10px; 4 | 5 | border-right: 2px solid var(--gray300); 6 | 7 | &:horizontal { 8 | border-right: 0; 9 | border-bottom: 2px solid var(--gray700); 10 | } 11 | } 12 | 13 | ::-webkit-scrollbar-thumb { 14 | background-color: transparent; 15 | transition: all 3s; 16 | 17 | border-right: 2px solid var(--gray700); 18 | 19 | &:horizontal { 20 | border-right: 0; 21 | border-bottom: 2px solid var(--gray700); 22 | } 23 | 24 | &:hover { 25 | border-width: 9px; 26 | } 27 | 28 | &:active { 29 | border-color: var(--gray800); 30 | } 31 | } 32 | 33 | ::-webkit-scrollbar-button { 34 | display: none; 35 | } 36 | 37 | ::-webkit-scrollbar-corner { 38 | background: var(--gray75); 39 | } 40 | 41 | ::-webkit-resizer { 42 | background: var(--gray75); 43 | } 44 | -------------------------------------------------------------------------------- /test/crypto.mock.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | Object.defineProperty(global, 'crypto', { 4 | value: { 5 | getRandomValues: arr => crypto.randomBytes(arr.length), 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /webpack.config.electron.js: -------------------------------------------------------------------------------- 1 | const main = require('./webpack.config.main.js'); 2 | const preload = require('./webpack.config.preload'); 3 | 4 | module.exports = [main, preload]; 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const view = require('./webpack.config.view.js'); 2 | const main = require('./webpack.config.main.js'); 3 | const preload = require('./webpack.config.preload'); 4 | 5 | module.exports = [view, main, preload]; 6 | --------------------------------------------------------------------------------