├── .env ├── .eslintignore ├── .eslintrc ├── .eslintrc.cjs ├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── demo ├── .gitignore ├── .vscode │ └── extensions.json ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── audios │ │ ├── bubble-gaming-fx.wav │ │ └── click-button-140881.mp3 │ └── icon-512.svg ├── src │ ├── App.vue │ ├── assets │ │ └── vue.svg │ ├── audio.js │ ├── components │ │ ├── ThreeCanvas.vue │ │ ├── ThreeCirclesGroup.vue │ │ ├── ThreeEffects.vue │ │ ├── ThreeRenderer.vue │ │ └── ThreeSparks.vue │ ├── main.js │ ├── physics.js │ └── style.css ├── vite.config.js └── yarn.lock ├── docs.md ├── package.json ├── pnpm-lock.yaml ├── public ├── icon-512.png ├── icon-512.svg ├── icon-512@1x.svg ├── icon.svg ├── logo.png └── logo.svg ├── scripts ├── manifest-dev.ts ├── manifest-prod.ts ├── mvsw-dev.ts ├── mvsw-prod.ts ├── prepare-dev.ts ├── prepare-prod.ts └── utils.ts ├── shim.d.ts ├── src ├── assets │ ├── icon-512.png │ ├── icon-512.svg │ ├── icon-512@1x.svg │ ├── icon.svg │ ├── logo.png │ └── logo.svg ├── background │ └── index.ts ├── components │ ├── Controller.vue │ ├── ControllerChannelRandom.vue │ ├── ControllerChannelRandomRadarBg.vue │ ├── ControllerChannelSequencer.vue │ ├── ControllerChannelTap.vue │ ├── ControllerChannels.vue │ ├── ControllerChannelsItem.vue │ ├── ControllerChannelsItemActions.vue │ ├── ControllerPlayer.vue │ ├── ControllerPlayerCD.vue │ ├── ControllerPlayerTimeline.vue │ ├── ControllerPlayerVisuals.vue │ ├── ControllerTempo.vue │ ├── Logo.vue │ ├── README.md │ └── icon │ │ ├── Metronome.vue │ │ ├── Pause.vue │ │ ├── Play.vue │ │ ├── Power.vue │ │ ├── Tap.vue │ │ ├── TapFx.vue │ │ └── Wave.vue ├── content │ ├── index.ts │ ├── style.css │ └── utils.ts ├── globab.d.ts ├── iframe │ ├── index.html │ └── main.ts ├── locales │ ├── en.yml │ └── zh-CN.yml ├── logic │ ├── dark.ts │ ├── index.ts │ └── storage.ts ├── manifest.ts ├── options │ ├── Options.vue │ ├── index.html │ └── main.ts ├── plugins │ ├── controllerClock.ts │ ├── controllerOutput.ts │ ├── controllerShortcuts.ts │ ├── controllerState.ts │ └── i18n.ts ├── popup │ ├── Popup.vue │ ├── index.html │ └── main.ts ├── styles │ ├── index.ts │ └── main.css └── utils │ ├── bpmGuesser.js │ └── createRandomSequence.js ├── tsconfig.json ├── video ├── .gitignore ├── .vscode │ └── extensions.json ├── README.md ├── index.html ├── package.json ├── public │ ├── keyboard.png │ ├── keyboard.svg │ └── vite.svg ├── src │ ├── App.vue │ ├── assets │ │ └── vue.svg │ ├── components │ │ ├── ControllerSlide.vue │ │ ├── KeyboardSlide.vue │ │ ├── KeyboardTempoSlide.vue │ │ ├── Logo.vue │ │ ├── SqChannel.vue │ │ ├── SqPills.vue │ │ └── TitleSlide.vue │ ├── main.js │ └── style.scss ├── vite.config.js └── yarn.lock ├── vite.config.ts ├── windi.config.ts └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | VITE_KEY=extension 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | public 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier' 11 | ], 12 | rules: { 13 | 'vue/multi-word-component-names': 'off', 14 | 15 | 'no-unused-vars': 'off', 16 | 'no-undef': 'off', 17 | '@typescript-eslint/no-unused-vars': 'off' 18 | }, 19 | parserOptions: { 20 | ecmaVersion: 'latest' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [xiaoluoboding] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vite-ssg-dist 3 | .vite-ssg-temp 4 | *.local 5 | dist 6 | dist-ssr 7 | node_modules 8 | components.d.ts 9 | .idea/ 10 | *.log 11 | extension 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": false, 6 | "trailingComma": "none", 7 | "printWidth": 80 8 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "johnsoncodehk.volar", 4 | "antfu.iconify", 5 | "dbaeumer.vscode-eslint", 6 | "voorjaar.windicss-intellisense", 7 | "csstools.postcss" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "volar.tsPlugin": true, 4 | "volar.tsPluginStatus": false, 5 | "vite.autoStart": false, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true, 8 | }, 9 | "files.associations": { 10 | "*.css": "postcss", 11 | }, 12 | "i18n-ally.localesPaths": [ 13 | "src/locales" 14 | ], 15 | "i18n-ally.enabledParsers": ["yaml"] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yunwei Xiao 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniTAP 2 | A fun sequencer for The Web that runs as a browser extension. 3 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /demo/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # MiniTAP "Mess" Demo 2 | Vue + Three.js + Matter.js, running on Vite 3 | 4 | Relevant files: 5 | → `/src/physics.js` runs the physics system 6 | → `/src/App.vue` main vue app 7 | → `/src/components/ThreeCirclesGroup.vue.vue` takes care of rendering the circles 8 | → `/src/components/ThreeEffects.vue` takes care of rendering the posprocessing effects 9 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MiniTAP 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo2", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@tresjs/core": "^4.2.10", 13 | "matter-js": "^0.20.0", 14 | "three": "^0.168.0", 15 | "tone": "^15.0.4", 16 | "underscore": "^1.13.7", 17 | "vue": "^3.4.37" 18 | }, 19 | "devDependencies": { 20 | "@vitejs/plugin-vue": "^5.1.2", 21 | "vite": "^5.4.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/public/audios/bubble-gaming-fx.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkpfm/minitap/32bd7f4bf2bc6f8f2eb1e94d814d028f9587aec3/demo/public/audios/bubble-gaming-fx.wav -------------------------------------------------------------------------------- /demo/public/audios/click-button-140881.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkpfm/minitap/32bd7f4bf2bc6f8f2eb1e94d814d028f9587aec3/demo/public/audios/click-button-140881.mp3 -------------------------------------------------------------------------------- /demo/public/icon-512.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon-512 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /demo/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/src/audio.js: -------------------------------------------------------------------------------- 1 | import { Sampler, Noise, AutoFilter, FMOscillator } from 'tone' 2 | import _ from 'underscore' 3 | const notes = ['C1', 'G1', 'E1', 'B1'] 4 | 5 | export default function () { 6 | if (process.client) { 7 | const bubbleSampler = new Sampler({ 8 | urls: { 9 | A1: 'bubble-gaming-fx.wav' 10 | }, 11 | baseUrl: '/audios/', 12 | onload: () => {} 13 | }).toDestination() 14 | 15 | const noiseSubtle = new Noise('pink') 16 | noiseSubtle.volume.value = -12 17 | // make an autofilter to shape the noise 18 | const autoFilterSubtle = new AutoFilter({ 19 | frequency: '8n', 20 | baseFrequency: 1000, 21 | octaves: 8 22 | }).toDestination() 23 | noiseSubtle.connect(autoFilterSubtle) 24 | autoFilterSubtle.start() 25 | 26 | const noise = new Noise('pink') 27 | // make an autofilter to shape the noise 28 | const autoFilter = new AutoFilter({ 29 | frequency: '8n', 30 | baseFrequency: 10000, 31 | octaves: 8 32 | }).toDestination() 33 | noise.connect(autoFilter) 34 | autoFilter.start() 35 | 36 | const autoFilterFws = new AutoFilter({ 37 | frequency: '8n', 38 | baseFrequency: 10000, 39 | octaves: 8 40 | }) 41 | .toDestination() 42 | .start() 43 | const fwdSound = new FMOscillator({ 44 | frequency: 100, 45 | type: 'sine', 46 | modulationType: 'triangle', 47 | harmonicity: 0.2, 48 | modulationIndex: 3 49 | }).connect(autoFilterFws) 50 | 51 | const autoFilterBws = new AutoFilter({ 52 | frequency: '4n', 53 | baseFrequency: 1000, 54 | octaves: 8 55 | }) 56 | .toDestination() 57 | .start() 58 | const bwdSound = new FMOscillator({ 59 | frequency: 80, 60 | type: 'sine', 61 | modulationType: 'triangle', 62 | harmonicity: 0.2, 63 | modulationIndex: 3 64 | }).connect(autoFilterBws) 65 | 66 | const bleepSampler = new Sampler({ 67 | urls: { 68 | A1: 'click-button-140881.mp3' 69 | }, 70 | baseUrl: '/audios/', 71 | onload: () => {} 72 | }).toDestination() 73 | bleepSampler.volume.value = -20 74 | 75 | let noteIndex = 0 76 | return { 77 | triggerBubble() { 78 | bubbleSampler.triggerAttackRelease(notes[noteIndex], 0.25) 79 | noteIndex = (noteIndex + 1) % notes.length 80 | }, 81 | triggerBleep() { 82 | bleepSampler.triggerAttackRelease('A1', 0.5) 83 | // noteIndex = (noteIndex + 1) % notes.length 84 | }, 85 | triggerNoiseSubtle() { 86 | noiseSubtle.start() 87 | }, 88 | stopNoiseSubtle() { 89 | noiseSubtle.stop() 90 | }, 91 | triggerNoise() { 92 | noise.start() 93 | }, 94 | stopNoise() { 95 | noise.stop() 96 | }, 97 | triggerFwdSound() { 98 | fwdSound.start() 99 | }, 100 | stopFwdSound() { 101 | fwdSound.stop() 102 | }, 103 | triggerBwdSound() { 104 | bwdSound.start() 105 | }, 106 | stopBwdSound() { 107 | bwdSound.stop() 108 | } 109 | } 110 | } else { 111 | return { 112 | triggerBubble: () => {} 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /demo/src/components/ThreeCanvas.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 24 | 31 | -------------------------------------------------------------------------------- /demo/src/components/ThreeCirclesGroup.vue: -------------------------------------------------------------------------------- 1 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /demo/src/components/ThreeEffects.vue: -------------------------------------------------------------------------------- 1 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /demo/src/components/ThreeRenderer.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 85 | 86 | 93 | -------------------------------------------------------------------------------- /demo/src/components/ThreeSparks.vue: -------------------------------------------------------------------------------- 1 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /demo/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css' 3 | import App from './App.vue' 4 | 5 | import physics from './physics' 6 | import audio from './audio' 7 | 8 | createApp(App) 9 | .use({ 10 | install(app) { 11 | app.provide('physics', physics()) 12 | } 13 | }) 14 | .use({ 15 | install(app) { 16 | app.provide('audio', audio()) 17 | } 18 | }) 19 | .mount('#app') 20 | -------------------------------------------------------------------------------- /demo/src/physics.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore' 2 | import * as Matter from 'matter-js' 3 | import { ref } from 'vue' 4 | 5 | export default function () { 6 | if (process.client) { 7 | var Engine = Matter.Engine, 8 | Render = Matter.Render, 9 | Runner = Matter.Runner, 10 | Bodies = Matter.Bodies, 11 | Composite = Matter.Composite 12 | 13 | const USE_RENDERER = false 14 | 15 | // create an engine 16 | var engine = Engine.create() 17 | engine.gravity.y = 0 18 | // engine.positionIterations = 10 19 | engine.constraintIterations = 3 20 | 21 | // create a renderer 22 | let render 23 | if (USE_RENDERER) { 24 | render = Render.create({ 25 | element: document.body, 26 | engine: engine 27 | }) 28 | render.canvas.style.position = 'fixed' 29 | render.canvas.style.transform = 'scale(0.75)' 30 | render.canvas.style.opacity = 0.8 31 | } 32 | 33 | const center = Bodies.circle(0, 0, 200) 34 | // Composite.add(engine.world, [center]) 35 | if (USE_RENDERER) { 36 | Render.lookAt(render, [center], Matter.Vector.create(1000, 1000)) 37 | } 38 | 39 | // create two boxes and a ground 40 | const circles = [] 41 | const circlesData = [] 42 | function spawn({ remove = false, highlight = false } = {}) { 43 | // if (circles.length === 10) return 44 | const radius = 50 + Math.random() * 150 45 | const circ = Bodies.circle( 46 | (Math.random() - 0.5) * 1200, 47 | (Math.random() - 0.5) * 1200, 48 | radius 49 | ) 50 | circ.frictionAir = 0.9 51 | circ.friction = 0.1 52 | circ.slop = 0.1 53 | circles.push(circ) 54 | circlesData.push({ radius, highlight }) 55 | Composite.add(engine.world, [circ]) 56 | 57 | if (remove) { 58 | Composite.remove(engine.world, [circles.shift()]) 59 | circlesData.shift() 60 | } 61 | } 62 | _.times(70, spawn) 63 | 64 | // run the renderer 65 | if (USE_RENDERER) { 66 | Render.run(render) 67 | } 68 | 69 | // create runner 70 | var runner = Runner.create() 71 | 72 | // run the engine 73 | Runner.run(runner, engine) 74 | 75 | const force = Matter.Vector.create(0, 1) 76 | const circlesState = ref([]) 77 | function tick() { 78 | requestAnimationFrame(tick) 79 | circles.forEach((c) => { 80 | force.x = -c.position.x * 0.0001 81 | force.y = -c.position.y * 0.0001 82 | Matter.Body.applyForce(c, c.position, force) 83 | }) 84 | circlesState.value = [ 85 | ...circles.map((c, i) => ({ 86 | id: c.id, 87 | pos: { x: c.position.x, y: -c.position.y }, 88 | scale: circlesData[i].radius, 89 | highlight: circlesData[i].highlight 90 | })) 91 | ] 92 | } 93 | tick() 94 | 95 | return { 96 | circlesState, 97 | spawn 98 | } 99 | } else { 100 | return { 101 | circlesState: { value: [] }, 102 | spawn: () => {} 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /demo/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | .card { 58 | padding: 2em; 59 | } 60 | 61 | #app { 62 | max-width: 1280px; 63 | margin: 0 auto; 64 | padding: 2rem; 65 | text-align: center; 66 | } 67 | 68 | @media (prefers-color-scheme: light) { 69 | :root { 70 | color: #213547; 71 | background-color: #ffffff; 72 | } 73 | a:hover { 74 | color: #747bff; 75 | } 76 | button { 77 | background-color: #f9f9f9; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | vue({ 8 | template: { 9 | compilerOptions: { 10 | isCustomElement: (tag) => 11 | tag.startsWith('Tres') && tag !== 'TresCanvas' 12 | } 13 | } 14 | }) 15 | ], 16 | app: { 17 | head: { 18 | link: [{ rel: 'icon', type: 'image/svg', href: '/icon-512.svg' }] 19 | } 20 | }, 21 | define: { 22 | 'process.client': true 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /docs.md: -------------------------------------------------------------------------------- 1 | 2 | ## Events 3 | Global 4 | `mt-play`: when controller starts playing 5 | `mt-stop`: when controller stops playing 6 | `mt-beat`: at the beggining of each beat 7 | - currentBeat: absolute current beat since last play 8 | `mt-sequence`: at the beggining of each sequence 9 | 10 | Channels 11 | `mt-channel-on`: when a channels note starts 12 | - channel: channel index (0-7) 13 | `mt-channel-off`: when a channels note stops 14 | - channel: channel index (0-7) 15 | `mt-channel{index}-on`: when a channels note starts. where index is the channel index (0-7) 16 | `mt-channel{index}-off`: when a channels note stops. where index is the channel index (0-7) 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-ext-mv3-starter", 3 | "displayName": "MiniTAP", 4 | "description": "A fun sequencer for The Web.", 5 | "version": "0.0.3", 6 | "private": true, 7 | "scripts": { 8 | "dev": "rimraf extension/dev && run-p dev:*", 9 | "dev:prepare": "esno scripts/prepare-dev.ts dev", 10 | "dev:web": "vite", 11 | "dev:js": "npm run build:dev -- --watch src", 12 | "dev:demo": "cd demo && vite", 13 | "build": "cross-env NODE_ENV=production run-s clear build:web build:prepare build:prod", 14 | "build:prepare": "esno scripts/prepare-prod.ts build", 15 | "build:web": "vite build", 16 | "build:dev": "tsup src/background src/content --format iife --out-dir extension/dev/dist --no-splitting --onSuccess 'esno scripts/mvsw-dev.ts'", 17 | "build:prod": "tsup src/background src/content --minify --format iife --out-dir extension/prod/dist --no-splitting --onSuccess 'esno scripts/mvsw-prod.ts'", 18 | "clear": "rimraf extension/prod", 19 | "p:ish": "pnpm install --shamefully-hoist", 20 | "p:up": "pnpm up" 21 | }, 22 | "dependencies": { 23 | "@tresjs/post-processing": "^0.7.1", 24 | "autoprefixer": "^10.4.20", 25 | "gsap": "^3.12.5", 26 | "interactjs": "^1.10.27", 27 | "radix-vue": "^1.9.6", 28 | "random-seedable": "^1.0.8", 29 | "tailwindcss": "^3.4.13", 30 | "three": "^0.169.0", 31 | "underscore": "^1.13.7", 32 | "vite": "4.2.1", 33 | "vue-i18n": "^9.2.2", 34 | "webext-bridge": "^6.0.1", 35 | "webextension-polyfill": "^0.12.0" 36 | }, 37 | "devDependencies": { 38 | "@antfu/eslint-config": "^0.7.0", 39 | "@iconify/json": "^2.2.39", 40 | "@intlify/vite-plugin-vue-i18n": "^3.4.0", 41 | "@rushstack/eslint-patch": "^1.2.0", 42 | "@types/chrome": "^0.0.225", 43 | "@types/fs-extra": "^9.0.12", 44 | "@types/node": "^16.4.1", 45 | "@types/webextension-polyfill": "^0.10.0", 46 | "@typescript-eslint/eslint-plugin": "^4.28.4", 47 | "@vitejs/plugin-vue": "^5.1.4", 48 | "@vueuse/core": "^9.13.0", 49 | "chokidar": "^3.5.3", 50 | "cross-env": "^7.0.3", 51 | "eslint": "^7.31.0", 52 | "esno": "^0.16.3", 53 | "fs-extra": "^11.1.1", 54 | "kolorist": "^1.7.0", 55 | "npm-run-all": "^4.1.5", 56 | "rimraf": "^4.4.1", 57 | "sass": "^1.79.3", 58 | "tsup": "^6.7.0", 59 | "typescript": "^4.3.5", 60 | "unplugin-icons": "^0.15.3", 61 | "unplugin-vue-components": "^0.24.1", 62 | "vite-plugin-windicss": "^1.8.10", 63 | "vue": "^3.2.47", 64 | "vue-demi": "^0.13.11", 65 | "vue-global-api": "^0.4.1", 66 | "windicss": "^3.5.6" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkpfm/minitap/32bd7f4bf2bc6f8f2eb1e94d814d028f9587aec3/public/icon-512.png -------------------------------------------------------------------------------- /public/icon-512.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon-512 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/icon-512@1x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon-512@1x 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkpfm/minitap/32bd7f4bf2bc6f8f2eb1e94d814d028f9587aec3/public/logo.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | logo 4 | 9 | -------------------------------------------------------------------------------- /scripts/manifest-dev.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { getManifest } from '../src/manifest' 3 | import { logger, r } from './utils' 4 | 5 | export async function writeManifest() { 6 | await fs.ensureFile(r('extension/dev/manifest.json')) 7 | await fs.writeJSON(r('extension/dev/manifest.json'), await getManifest(), { 8 | spaces: 2 9 | }) 10 | logger('PRE', 'write manifest.json') 11 | } 12 | 13 | writeManifest() 14 | -------------------------------------------------------------------------------- /scripts/manifest-prod.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { getManifest } from '../src/manifest' 3 | import { logger, r } from './utils' 4 | 5 | export async function writeManifest() { 6 | await fs.ensureFile(r('extension/prod/manifest.json')) 7 | await fs.writeJSON(r('extension/prod/manifest.json'), await getManifest(), { 8 | spaces: 2 9 | }) 10 | logger('PRE', 'write manifest.json') 11 | } 12 | 13 | writeManifest() 14 | -------------------------------------------------------------------------------- /scripts/mvsw-dev.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import fs from 'fs-extra' 3 | import { logger } from './utils' 4 | ;(async () => { 5 | try { 6 | // copy icon files 7 | await fs.copy(resolve('public'), resolve('extension/dev/assets'), { 8 | overwrite: true 9 | }) 10 | await fs.move( 11 | resolve('extension/dev/dist/background/index.global.js'), 12 | resolve('extension/dev/background.js'), 13 | { overwrite: true } 14 | ) 15 | await fs.move( 16 | resolve('extension/dev/dist/content'), 17 | resolve('extension/dev/content'), 18 | { overwrite: true } 19 | ) 20 | await fs.remove(resolve('extension/dev/dist')) 21 | // eslint-disable-next-line no-console 22 | logger('BUILD:SW', 'Moved service-worker success!') 23 | } catch (err) { 24 | console.error(err) 25 | } 26 | })() 27 | -------------------------------------------------------------------------------- /scripts/mvsw-prod.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import fs from 'fs-extra' 3 | import { logger } from './utils' 4 | ;(async () => { 5 | try { 6 | // move icon files 7 | await fs.copy(resolve('public'), resolve('extension/prod/assets'), { 8 | overwrite: true 9 | }) 10 | 11 | // move service worker 12 | await fs.move( 13 | resolve('extension/prod/dist/background/index.global.js'), 14 | resolve('extension/prod/background.js'), 15 | { overwrite: true } 16 | ) 17 | await fs.move( 18 | resolve('extension/prod/dist/content'), 19 | resolve('extension/prod/content'), 20 | { overwrite: true } 21 | ) 22 | await fs.remove(resolve('extension/prod/dist')) 23 | // eslint-disable-next-line no-console 24 | logger('BUILD:SW', 'Moved service-worker success!') 25 | } catch (err) { 26 | console.error(err) 27 | } 28 | })() 29 | -------------------------------------------------------------------------------- /scripts/prepare-dev.ts: -------------------------------------------------------------------------------- 1 | // generate stub index.html files for dev entry 2 | import { execSync } from 'child_process' 3 | import fs from 'fs-extra' 4 | import chokidar from 'chokidar' 5 | import { r, PORT, IS_DEV, logger } from './utils' 6 | 7 | /** 8 | * Stub index.html to use Vite in development 9 | */ 10 | async function stubIndexHtml() { 11 | const views = ['options', 'popup', 'iframe'] 12 | 13 | for (const view of views) { 14 | await fs.ensureDir(r(`extension/dev/${view}`)) 15 | let data = await fs.readFile(r(`src/${view}/index.html`), 'utf-8') 16 | data = data 17 | .replace('"./main.ts"', `"http://localhost:${PORT}/${view}/main.ts"`) 18 | .replace( 19 | '
', 20 | '
Vite server did not start
' 21 | ) 22 | await fs.writeFile(r(`extension/dev/${view}/index.html`), data, 'utf-8') 23 | logger('PRE', `stub ${view}`) 24 | } 25 | } 26 | 27 | function writeManifest() { 28 | execSync('npx esno ./scripts/manifest-dev.ts', { stdio: 'inherit' }) 29 | } 30 | 31 | writeManifest() 32 | 33 | if (IS_DEV) { 34 | stubIndexHtml() 35 | chokidar.watch(r('src/**/*.html')).on('change', () => { 36 | stubIndexHtml() 37 | }) 38 | chokidar.watch([r('src/manifest.ts'), r('package.json')]).on('change', () => { 39 | writeManifest() 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /scripts/prepare-prod.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process' 2 | ;(function writeManifest() { 3 | execSync('npx esno ./scripts/manifest-prod.ts', { stdio: 'inherit' }) 4 | })() 5 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { bgCyan, black } from 'kolorist' 3 | 4 | export const PORT = parseInt(process.env.PORT || '') || 3309 5 | export const r = (...args: string[]) => resolve(__dirname, '..', ...args) 6 | export const IS_DEV = process.env.NODE_ENV !== 'production' 7 | 8 | export function logger(name: string, message: string) { 9 | // eslint-disable-next-line no-console 10 | console.log(black(bgCyan(` ${name} `)), message) 11 | } 12 | -------------------------------------------------------------------------------- /shim.d.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolWithReturn } from 'webext-bridge' 2 | 3 | declare module 'webext-bridge' { 4 | export interface ProtocolMap { 5 | // define message protocol types 6 | // see https://github.com/antfu/webext-bridge#type-safe-protocols 7 | 'tab-prev': { title: string | undefined } 8 | 'get-current-tab': ProtocolWithReturn<{ tabId: number }, { title: string }> 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkpfm/minitap/32bd7f4bf2bc6f8f2eb1e94d814d028f9587aec3/src/assets/icon-512.png -------------------------------------------------------------------------------- /src/assets/icon-512.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon-512 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icon-512@1x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | icon-512@1x 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkpfm/minitap/32bd7f4bf2bc6f8f2eb1e94d814d028f9587aec3/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | logo 4 | 14 | 15 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import { onMessage, sendMessage } from 'webext-bridge/background' 2 | 3 | let openTabs = [] 4 | // function getAllTabs() { 5 | // chrome.tabs.query({}, function (tabs) { 6 | // tabs.forEach(function (tab) { 7 | // // console.log(tab.id) 8 | // }) 9 | // }) 10 | // } 11 | // getAllTabs() 12 | 13 | chrome.tabs.onRemoved.addListener((_tabId) => { 14 | openTabs = openTabs.filter(({ tabId }) => tabId !== _tabId) 15 | }) 16 | 17 | onMessage('REQUEST_CONTROLLER_STATUS', async function runAction({ data }) { 18 | // console.log(openTabs) 19 | const tabInfo = openTabs.find(({ tabId }) => tabId === data.tabId) 20 | return { 21 | isOn: !!tabInfo, 22 | ...tabInfo 23 | } 24 | }) 25 | 26 | onMessage('SET_CONTROLLER_POSITION', async function runAction({ data }) { 27 | // console.log(openTabs) 28 | const tabInfo = openTabs.find(({ tabId }) => tabId === data.tabId) 29 | tabInfo.position = data.position 30 | }) 31 | 32 | onMessage('SWITCH_CONTROLLER', async function runAction({ data }) { 33 | const isOpen = !!openTabs.find(({ tabId }) => tabId === data.tabId) 34 | // console.log('SWITCH_CONTROLLER', data.tabId, openTabs) 35 | let isOn = false 36 | if (!isOpen) { 37 | openTabs.push({ tabId: data.tabId }) 38 | isOn = true 39 | } else { 40 | openTabs = openTabs.filter(({ tabId }) => tabId !== data.tabId) 41 | isOn = false 42 | } 43 | chrome.tabs.sendMessage( 44 | data.tabId, 45 | { message: 'SWITCH_CONTROLLER', isOn }, 46 | function (response) { 47 | // console.log('Response from content script:', response) 48 | } 49 | ) 50 | return {} 51 | }) 52 | 53 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 54 | if (request.action === 'getTabId') { 55 | sendResponse({ tabId: sender.tab.id }) 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /src/components/Controller.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/components/ControllerChannelRandom.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 87 | 88 | 178 | -------------------------------------------------------------------------------- /src/components/ControllerChannelRandomRadarBg.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/ControllerChannelSequencer.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 44 | 45 | 73 | -------------------------------------------------------------------------------- /src/components/ControllerChannelTap.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | 38 | 80 | -------------------------------------------------------------------------------- /src/components/ControllerChannels.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 49 | 50 | 63 | -------------------------------------------------------------------------------- /src/components/ControllerChannelsItem.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 59 | 60 | 127 | -------------------------------------------------------------------------------- /src/components/ControllerChannelsItemActions.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | 43 | 70 | -------------------------------------------------------------------------------- /src/components/ControllerPlayer.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | 79 | -------------------------------------------------------------------------------- /src/components/ControllerPlayerCD.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 75 | -------------------------------------------------------------------------------- /src/components/ControllerPlayerTimeline.vue: -------------------------------------------------------------------------------- 1 | 211 | 212 | 221 | 222 | 232 | -------------------------------------------------------------------------------- /src/components/ControllerPlayerVisuals.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /src/components/ControllerTempo.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | 33 | 102 | -------------------------------------------------------------------------------- /src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | ## Components 2 | 3 | Components in this dir will be auto-registered and on-demand, powered by [`vite-plugin-components`](https://github.com/antfu/vite-plugin-components). 4 | 5 | Components can be shared in all views. 6 | 7 | ### Icons 8 | 9 | You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/). 10 | 11 | It will only bundle the icons you use. Check out [vite-plugin-icons](https://github.com/antfu/vite-plugin-icons) for more details. 12 | -------------------------------------------------------------------------------- /src/components/icon/Metronome.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /src/components/icon/Pause.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 40 | -------------------------------------------------------------------------------- /src/components/icon/Play.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 41 | -------------------------------------------------------------------------------- /src/components/icon/Power.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /src/components/icon/Tap.vue: -------------------------------------------------------------------------------- 1 | 24 | 31 | -------------------------------------------------------------------------------- /src/components/icon/TapFx.vue: -------------------------------------------------------------------------------- 1 | 70 | 78 | -------------------------------------------------------------------------------- /src/components/icon/Wave.vue: -------------------------------------------------------------------------------- 1 | 19 | 26 | -------------------------------------------------------------------------------- /src/content/index.ts: -------------------------------------------------------------------------------- 1 | import { onMessage, sendMessage } from 'webext-bridge/content-script' 2 | import './style.css' 3 | 4 | let controllerIsOn = false 5 | onMessage('REQUEST_CONTROLLER_STATUS', async function ({ data }) { 6 | return { 7 | isOn: controllerIsOn 8 | } 9 | }) 10 | 11 | let iframeRef = undefined 12 | let actualIframeRef = undefined 13 | let styleLink = undefined 14 | let handles = null 15 | let tabId = null 16 | 17 | let controllerWidth = 1200 18 | let controllerHeight = 150 19 | let xTarget = 0 20 | let x = 0 21 | let yTarget = 0 22 | let y = 0 23 | 24 | let xOrigin = 0 25 | let yOrigin = 0 26 | let xMouseStart = 0 27 | let yMouseStart = 0 28 | let dragging = false 29 | 30 | const safetyPadding = 20 31 | 32 | function dragStart(event) { 33 | event.stopPropagation() 34 | event.preventDefault() 35 | dragging = true 36 | xMouseStart = event.pageX 37 | yMouseStart = event.pageY 38 | xOrigin = x 39 | yOrigin = y 40 | iframeRef.style.pointerEvents = 'none' 41 | window.addEventListener('mousemove', dragMove) 42 | window.addEventListener('mouseup', dragEnd) 43 | } 44 | 45 | function dragMove(event) { 46 | event.stopPropagation() 47 | event.preventDefault() 48 | const diffX = xMouseStart - event.pageX 49 | const diffY = yMouseStart - event.pageY 50 | xTarget = xOrigin - diffX 51 | yTarget = yOrigin - diffY 52 | } 53 | 54 | function dragEnd() { 55 | makeTargetPositionVisible() 56 | dragging = false 57 | iframeRef.style.pointerEvents = '' 58 | window.removeEventListener('mousemove', dragMove) 59 | window.removeEventListener('mouseup', dragEnd) 60 | sendMessage( 61 | 'SET_CONTROLLER_POSITION', 62 | { tabId, position: { x: xTarget, y: yTarget } }, 63 | 'background' 64 | ) 65 | } 66 | 67 | function handleKeyDown({ key, shiftKey }) { 68 | actualIframeRef.contentWindow.postMessage( 69 | { name: 'mt-key-down', key, shiftKey }, 70 | '*' 71 | ) 72 | } 73 | 74 | function handleKeyUp({ key, shiftKey }) { 75 | actualIframeRef.contentWindow.postMessage( 76 | { name: 'mt-key-up', key, shiftKey }, 77 | '*' 78 | ) 79 | } 80 | 81 | function makeTargetPositionVisible() { 82 | const safetyPadding = 20 83 | if (xTarget + controllerWidth < 0) { 84 | xTarget = -controllerWidth + safetyPadding 85 | } 86 | if (xTarget - innerWidth > 0) { 87 | xTarget = innerWidth - safetyPadding 88 | } 89 | if (yTarget > innerHeight / 2 - safetyPadding - controllerHeight) { 90 | yTarget = innerHeight / 2 - safetyPadding - controllerHeight 91 | } 92 | if (yTarget < -innerHeight / 2 + safetyPadding) { 93 | yTarget = -innerHeight / 2 + safetyPadding 94 | } 95 | } 96 | function setupUi() { 97 | if (controllerIsOn) return 98 | const src = chrome.runtime.getURL('/iframe/index.html') 99 | 100 | if (iframeRef) iframeRef.remove() 101 | iframeRef = new DOMParser().parseFromString( 102 | `
103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 |
115 | 116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
128 |
`, 129 | 'text/html' 130 | ).body.firstElementChild 131 | 132 | if (iframeRef) { 133 | document.body?.append(iframeRef) 134 | } 135 | 136 | actualIframeRef = iframeRef?.querySelector('iframe') 137 | 138 | handles = iframeRef?.querySelectorAll('.mt-controller-grab') 139 | x = innerWidth / 2 - controllerWidth / 2 140 | if (innerWidth < controllerWidth) x = safetyPadding 141 | xTarget = x 142 | y = innerHeight / 2 - safetyPadding - controllerHeight 143 | yTarget = y 144 | 145 | function tick() { 146 | if (!dragging) { 147 | makeTargetPositionVisible() 148 | } 149 | x += (xTarget - x) * 0.2 150 | y += (yTarget - y) * 0.2 151 | iframeRef.style.transform = `translate(${x}px, ${y}px)` 152 | // if (controllerIsOn) requestAnimationFrame(tick) 153 | requestAnimationFrame(tick) 154 | } 155 | tick() 156 | 157 | const styleSrc = chrome.runtime.getURL('/content/style.css') 158 | styleLink = document.createElement('link') 159 | styleLink.href = styleSrc 160 | styleLink.type = 'text/css' 161 | styleLink.rel = 'stylesheet' 162 | document.body?.append(styleLink) 163 | 164 | controllerIsOn = true 165 | 166 | window.addEventListener('keydown', handleKeyDown) 167 | window.addEventListener('keyup', handleKeyUp) 168 | 169 | handles.forEach((h) => h.addEventListener('mousedown', dragStart)) 170 | } 171 | 172 | function removeUi() { 173 | iframeRef && iframeRef.remove() 174 | controllerIsOn = false 175 | window.removeEventListener('keydown', handleKeyDown) 176 | window.removeEventListener('keyup', handleKeyUp) 177 | } 178 | 179 | chrome.runtime.sendMessage({ action: 'getTabId' }, async function (response) { 180 | // console.log('Current tab ID:', response.tabId) 181 | tabId = response.tabId 182 | const { isOn, position } = await sendMessage( 183 | 'REQUEST_CONTROLLER_STATUS', 184 | { tabId }, 185 | 'background' 186 | ) 187 | if (isOn) { 188 | setupUi() 189 | if (position) { 190 | x = position.x 191 | xTarget = position.x 192 | y = position.y 193 | yTarget = position.y 194 | } 195 | } 196 | 197 | chrome.runtime.onMessage.addListener( 198 | function (request, sender, sendResponse) { 199 | if (request.message === 'SWITCH_CONTROLLER') { 200 | // console.log('cs', 'SWITCH_CONTROLLER') 201 | if (request.isOn) { 202 | setupUi() 203 | } else { 204 | removeUi() 205 | } 206 | 207 | sendResponse({ 208 | status: 'success', 209 | isOn: controllerIsOn 210 | }) 211 | } 212 | } 213 | ) 214 | onMessage('SWITCH_CONTROLLER', async function ({ data }) {}) 215 | }) 216 | -------------------------------------------------------------------------------- /src/content/style.css: -------------------------------------------------------------------------------- 1 | .mt-controller { 2 | position: fixed; 3 | display: flex; 4 | align-items: center; 5 | z-index: 9999999; 6 | top: 50%; 7 | left: 0; 8 | } 9 | .mt-controller-grab { 10 | cursor: grab; 11 | width: 10px; 12 | height: 100%; 13 | position: absolute; 14 | left: -15px; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | /* background: red; */ 19 | 20 | &.right { 21 | left: auto; 22 | right: -15px; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/content/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2017 Adam Miskiewicz 4 | * 5 | * Use of this source code is governed by a MIT-style license that can be found 6 | * in the LICENSE file or at https://opensource.org/licenses/MIT. 7 | */ 8 | 9 | export function invariant(condition: boolean, message: string): void { 10 | if (!condition) { 11 | throw new Error(message) 12 | } 13 | } 14 | 15 | export function withDefault(maybeValue: X | undefined, defaultValue: X): X { 16 | return typeof maybeValue !== 'undefined' && maybeValue !== null 17 | ? (maybeValue as X) 18 | : defaultValue 19 | } 20 | -------------------------------------------------------------------------------- /src/globab.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | const component: any 3 | export default component 4 | } 5 | -------------------------------------------------------------------------------- /src/iframe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MiniTAP Controller 6 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/iframe/main.ts: -------------------------------------------------------------------------------- 1 | import 'vue-global-api' 2 | import { createApp } from 'vue' 3 | import App from '~/components/Controller.vue' 4 | import controllerState from '~/plugins/controllerState' 5 | import controllerClock from '~/plugins/controllerClock' 6 | import controllerShortcuts from '~/plugins/controllerShortcuts' 7 | import controllerOutput from '~/plugins/controllerOutput' 8 | import '../styles' 9 | 10 | const app = createApp(App) 11 | app 12 | .use(controllerState) 13 | .use(controllerClock) 14 | .use(controllerOutput) 15 | .use(controllerShortcuts) 16 | .mount('#controller-app') 17 | -------------------------------------------------------------------------------- /src/locales/en.yml: -------------------------------------------------------------------------------- 1 | common: 2 | extension_name: Vitesse Chrome Extensionbutton 3 | button: 4 | toggle_dark: Toggle dark mode 5 | toggle_langs: Change languages 6 | popup: 7 | desc: This is the popup page 8 | open_options: Open Options 9 | storage: Storage 10 | options: 11 | desc: This is the options page 12 | sync_storage: Sync Storage Message 13 | powered_by_vite: Powered By Vite 14 | -------------------------------------------------------------------------------- /src/locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | common: 2 | extension_name: Vitesse Chrome Extensionbutton 3 | button: 4 | toggle_dark: 切换深色模式 5 | toggle_langs: 切换语言 6 | popup: 7 | desc: 弹窗页面 8 | open_options: 打开选项页面 9 | storage: 存储信息 10 | options: 11 | desc: 选项页面 12 | sync_storage: 同步 Storage 信息 13 | powered_by_vite: 由 Vite 强力驱动 -------------------------------------------------------------------------------- /src/logic/dark.ts: -------------------------------------------------------------------------------- 1 | import { useDark, useToggle } from '@vueuse/core' 2 | 3 | export const isDark = useDark() 4 | export const toggleDark = useToggle(isDark) 5 | -------------------------------------------------------------------------------- /src/logic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './storage' 2 | export * from './dark' 3 | -------------------------------------------------------------------------------- /src/logic/storage.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@vueuse/core' 2 | 3 | export const storageDemo = useLocalStorage('webext-demo', 'Storage Demo', { listenToStorageChanges: true }) 4 | -------------------------------------------------------------------------------- /src/manifest.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from 'webextension-polyfill' 2 | import pkg from '../package.json' 3 | import { IS_DEV, PORT } from '../scripts/utils' 4 | 5 | export async function getManifest(): Promise { 6 | // update this file to update this manifest.json 7 | // can also be conditional based on your need 8 | 9 | const manifest: Manifest.WebExtensionManifest = { 10 | manifest_version: 3, 11 | name: pkg.displayName || pkg.name, 12 | version: pkg.version, 13 | description: pkg.description, 14 | action: { 15 | default_icon: './assets/icon-512.png', 16 | default_popup: './popup/index.html' 17 | }, 18 | options_ui: { 19 | page: './options/index.html', 20 | open_in_tab: true 21 | }, 22 | background: { 23 | service_worker: 'background.js' 24 | }, 25 | content_scripts: [ 26 | { 27 | matches: ['http://*/*', 'https://*/*'], 28 | js: ['./content/index.global.js'] 29 | } 30 | ], 31 | icons: { 32 | 16: './assets/icon-512.png', 33 | 48: './assets/icon-512.png', 34 | 128: './assets/icon-512.png' 35 | }, 36 | // permissions: ['contextMenus', 'storage'], 37 | // permissions: ['tabs'], 38 | // host_permissions: ['http://*/*', 'https://*/*'], 39 | // optional_permissions: ['*://*/*'], 40 | content_security_policy: {}, 41 | web_accessible_resources: [ 42 | { 43 | resources: ['iframe/index.html', 'content/style.css'], 44 | matches: ['http://*/*', 'https://*/*'] 45 | } 46 | ] 47 | } 48 | 49 | if (IS_DEV) { 50 | // this is required on dev for Vite script to load 51 | manifest.content_security_policy = { 52 | extension_pages: `script-src 'self' http://localhost:${PORT}; object-src 'self'` 53 | } 54 | } 55 | 56 | return manifest 57 | } 58 | -------------------------------------------------------------------------------- /src/options/Options.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 34 | -------------------------------------------------------------------------------- /src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Options 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/options/main.ts: -------------------------------------------------------------------------------- 1 | import 'vue-global-api' 2 | import { createApp } from 'vue' 3 | import App from './Options.vue' 4 | // import i18n from '~/plugins/i18n' 5 | import '../styles' 6 | 7 | const app = createApp(App) 8 | app 9 | // .use(i18n) 10 | .mount('#app') 11 | -------------------------------------------------------------------------------- /src/plugins/controllerClock.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed, watch } from 'vue' 2 | 3 | const bpm = ref(100) 4 | const currentBeat = ref(0) 5 | const currentQuaver = ref(0) 6 | const currentTime = ref(0) 7 | const currentSequence = computed(() => Math.floor(currentTime.value / 16)) 8 | const isPlaying = ref(false) 9 | const stepsPerBar = ref(4) 10 | let lastTimestamp = 0 11 | let startTime = 0 12 | let lastBeat = 0 13 | let animationFrameId = null 14 | let onBeatListeners = [] 15 | let onTickListeners = [] 16 | let onSequenceListeners = [] 17 | 18 | const start = () => { 19 | const tick = () => { 20 | const timestamp = performance.now() 21 | const beatInterval = (60 / (bpm.value * 2)) * 1000 22 | currentTime.value = (timestamp - startTime) / beatInterval 23 | const beat = Math.floor(currentTime.value) 24 | if (beat !== lastBeat) { 25 | onBeat() 26 | lastBeat = beat 27 | } 28 | 29 | if (isPlaying.value) { 30 | animationFrameId = requestAnimationFrame(tick) 31 | } 32 | 33 | onTickListeners.forEach((cb) => cb({ currentTime: currentTime.value })) 34 | } 35 | 36 | lastTimestamp = performance.now() 37 | startTime = performance.now() 38 | currentQuaver.value = -1 39 | currentBeat.value = -1 40 | tick() 41 | onBeatListeners.forEach((cb) => 42 | cb({ currentBeat: currentBeat.value, currentQuaver: currentQuaver.value }) 43 | ) 44 | } 45 | 46 | const stop = () => { 47 | cancelAnimationFrame(animationFrameId) 48 | } 49 | 50 | const onBeat = () => { 51 | currentQuaver.value = Math.floor(currentTime.value) 52 | currentBeat.value = Math.floor(currentTime.value / 2) 53 | // currentBeat.value = Math.floor(currentQuaver.value / 2) 54 | onBeatListeners.forEach((cb) => 55 | cb({ currentBeat: currentBeat.value, currentQuaver: currentQuaver.value }) 56 | ) 57 | } 58 | 59 | watch( 60 | currentSequence, 61 | (val) => { 62 | onSequenceListeners.forEach((cb) => cb()) 63 | }, 64 | { immediate: true } 65 | ) 66 | 67 | // const onBeat = () => { 68 | // currentBeat.value = currentBeat.value + 1 69 | // onBeatListeners.forEach((cb) => cb({ currentBeat: currentBeat.value })) 70 | // } 71 | 72 | const toggle = () => { 73 | isPlaying.value = !isPlaying.value 74 | if (isPlaying.value) { 75 | start() 76 | } else { 77 | stop() 78 | } 79 | } 80 | 81 | const controllerClockPlugin = { 82 | install(app) { 83 | const controllerClock = { 84 | start, 85 | stop, 86 | toggle, 87 | bpm, 88 | currentTime, 89 | currentBeat, 90 | currentQuaver, 91 | offsetTime: (val = 1) => { 92 | startTime += val 93 | }, 94 | stepsPerBar, 95 | barBeat: computed(() => currentBeat.value % stepsPerBar.value), 96 | isPlaying, 97 | listenOnBeat(cb) { 98 | onBeatListeners.push(cb) 99 | }, 100 | removeOnBeat(cb) { 101 | onBeatListeners = onBeatListeners.filter((_cb) => _cb !== cb) 102 | }, 103 | listenOnTick(cb) { 104 | onTickListeners.push(cb) 105 | }, 106 | removeOnTick(cb) { 107 | onTickListeners = onTickListeners.filter((_cb) => _cb !== cb) 108 | }, 109 | listenOnSequence(cb) { 110 | onSequenceListeners.push(cb) 111 | }, 112 | removeOnSequence(cb) { 113 | onSequenceListeners = onSequenceListeners.filter((_cb) => _cb !== cb) 114 | } 115 | } 116 | app.provide('controllerClock', controllerClock) 117 | app.config.globalProperties.controllerClock = controllerClock 118 | } 119 | } 120 | 121 | export default controllerClockPlugin 122 | -------------------------------------------------------------------------------- /src/plugins/controllerOutput.ts: -------------------------------------------------------------------------------- 1 | import { watch, computed } from 'vue' 2 | import createRandomSequence from './../utils/createRandomSequence' 3 | 4 | export default { 5 | install(app) { 6 | const controllerState = app.config.globalProperties.controllerState 7 | const controllerClock = app.config.globalProperties.controllerClock 8 | 9 | function triggerNote(channelIndex) { 10 | window.parent.postMessage( 11 | { name: 'mt-channel-on', channel: channelIndex }, 12 | '*' 13 | ) 14 | window.parent.postMessage( 15 | { name: `mt-channel${channelIndex}-on`, channel: channelIndex }, 16 | '*' 17 | ) 18 | setTimeout(() => { 19 | window.parent.postMessage( 20 | { name: 'mt-channel-off', channel: channelIndex }, 21 | '*' 22 | ) 23 | window.parent.postMessage( 24 | { 25 | name: `mt-channel${channelIndex}-off`, 26 | channel: channelIndex 27 | }, 28 | '*' 29 | ) 30 | }, 200) 31 | } 32 | 33 | // STUFF THAT HAPPENS AT THE BEGINNING OF EACH SEQUENCE 34 | let randomsState = null 35 | function updateRandomState() { 36 | randomsState = [] 37 | controllerState.channels.forEach((channelData, channelIndex) => { 38 | randomsState[channelIndex] = createRandomSequence({ 39 | amount: channelData.random.amount, 40 | seed: channelData.random.seed 41 | }).map((v) => { 42 | return { 43 | time: v, 44 | done: v <= controllerClock.currentTime.value % 16 45 | } 46 | }) 47 | }) 48 | } 49 | controllerClock.listenOnSequence(() => { 50 | window.parent.postMessage({ name: 'mt-sequence' }, '*') 51 | updateRandomState() 52 | }) 53 | 54 | // STUFF THAT HAPPENS ON BEAT 55 | controllerClock.listenOnBeat(({ currentBeat, currentQuaver }) => { 56 | window.parent.postMessage({ name: 'mt-beat', currentBeat }, '*') 57 | controllerState.channels.forEach((channelData, channelIndex) => { 58 | if (channelData.mode === 0) { 59 | // SEQUENCER 60 | const beatIndex = currentQuaver % 16 61 | if (channelData.sequencer[beatIndex]) { 62 | triggerNote(channelIndex) 63 | } 64 | } 65 | }) 66 | }) 67 | 68 | // STUFF THAT HAPPENS ON EACH FRAME 69 | controllerClock.listenOnTick(({ currentTime }) => { 70 | controllerState.channels.forEach((channelData, channelIndex) => { 71 | // Update randoms 72 | if (randomsState) { 73 | randomsState[channelIndex].forEach((rand, index) => { 74 | if (rand.time <= currentTime % 16 && !rand.done) { 75 | rand.done = true 76 | if (channelData.mode === 2) { 77 | triggerNote(channelIndex) 78 | window.postMessage( 79 | { 80 | name: 'mt-internal-rand-trigger', 81 | channel: channelIndex, 82 | randIndex: index 83 | }, 84 | '*' 85 | ) 86 | } 87 | } 88 | }) 89 | } 90 | }) 91 | }) 92 | 93 | // TAPS 94 | const tapDowns = computed(() => 95 | controllerState.channels.map((c) => c.tapDown) 96 | ) 97 | watch(tapDowns, (channels) => { 98 | channels.forEach((isDown, channelIndex) => { 99 | if (isDown) { 100 | window.parent.postMessage( 101 | { name: 'mt-channel-on', channel: channelIndex }, 102 | '*' 103 | ) 104 | window.parent.postMessage( 105 | { name: `mt-channel${channelIndex}-on`, channel: channelIndex }, 106 | '*' 107 | ) 108 | } else { 109 | window.parent.postMessage( 110 | { name: 'mt-channel-off', channel: channelIndex }, 111 | '*' 112 | ) 113 | window.parent.postMessage( 114 | { name: `mt-channel${channelIndex}-off`, channel: channelIndex }, 115 | '*' 116 | ) 117 | } 118 | }) 119 | }) 120 | 121 | const randoms = computed(() => 122 | controllerState.channels.map((c) => ({ ...c.random.amount })) 123 | ) 124 | watch( 125 | () => randoms.value, 126 | () => { 127 | // could update only the changed channel for better perf 128 | setTimeout(() => { 129 | updateRandomState() 130 | }) 131 | }, 132 | { immediate: true } 133 | ) 134 | 135 | const controllerOutput = { 136 | triggerNote 137 | } 138 | 139 | app.provide('controllerOutput', controllerOutput) 140 | app.config.globalProperties.controllerOutput = controllerOutput 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/plugins/controllerShortcuts.ts: -------------------------------------------------------------------------------- 1 | import BpmGuesser from './../utils/bpmGuesser' 2 | const chanelModeKeys = 'qweruiop'.split('') 3 | const chanelTriggerKeys = 'asdfhjkl'.split('') 4 | let shiftDown = false 5 | 6 | export default { 7 | install(app) { 8 | const controllerState = app.config.globalProperties.controllerState 9 | const controllerClock = app.config.globalProperties.controllerClock 10 | const controllerOutput = app.config.globalProperties.controllerOutput 11 | 12 | function handleKeyDown(key, shiftKey) { 13 | shiftDown = shiftKey 14 | if (key === 'Shift') { 15 | shiftDown = true 16 | } else if (key === 'ArrowUp') { 17 | controllerClock.bpm.value += 0.05 18 | } else if (key === 'ArrowDown') { 19 | controllerClock.bpm.value -= 0.05 20 | } else if (key === 'ArrowLeft') { 21 | controllerClock.offsetTime(25) 22 | } else if (key === 'ArrowRight') { 23 | controllerClock.offsetTime(-25) 24 | } else if (chanelTriggerKeys.includes(key.toLowerCase())) { 25 | const index = chanelTriggerKeys.indexOf(key.toLowerCase()) 26 | if (controllerState.channels[index].mode === 0) { 27 | // TRIGGER KEYS 28 | } else if (controllerState.channels[index].mode === 1) { 29 | // Tap mode 30 | controllerState.channels[index].tapDown = true 31 | } else if (controllerState.channels[index].mode === 2) { 32 | // Random mode 33 | if (shiftDown) { 34 | controllerState.channels[index].random.amount = Math.max( 35 | 0, 36 | controllerState.channels[index].random.amount - 1 37 | ) 38 | } else { 39 | controllerState.channels[index].random.amount = Math.min( 40 | 24, 41 | controllerState.channels[index].random.amount + 1 42 | ) 43 | } 44 | } 45 | } 46 | } 47 | 48 | function handleKeyUp(key) { 49 | if (key === 'Shift') { 50 | shiftDown = false 51 | } else if (key === ' ') { 52 | controllerClock.toggle() 53 | } else if (key === 'Enter') { 54 | const bpm = BpmGuesser.guess() 55 | if (bpm) controllerClock.bpm.value = bpm 56 | } else if (chanelModeKeys.includes(key.toLowerCase())) { 57 | // Change mode 58 | const index = chanelModeKeys.indexOf(key.toLowerCase()) 59 | controllerState.channels[index].mode = 60 | (controllerState.channels[index].mode + 1) % 4 61 | // 62 | } else if (chanelTriggerKeys.includes(key.toLowerCase())) { 63 | // TRIGGER KEYS 64 | const index = chanelTriggerKeys.indexOf(key.toLowerCase()) 65 | if (controllerState.channels[index].mode === 0) { 66 | // Sequencer mode 67 | if (shiftDown) { 68 | controllerState.channels[index].sequencer.forEach((val, i) => { 69 | controllerState.channels[index].sequencer[i] = 0 70 | }) 71 | } else { 72 | // tap 73 | if (controllerClock.isPlaying.value) { 74 | const quav = Math.floor( 75 | (controllerClock.currentTime.value % 16) - 0.1 76 | ) 77 | const newVal = !controllerState.channels[index].sequencer[quav] 78 | controllerState.channels[index].sequencer[quav] = newVal 79 | if (newVal) controllerOutput.triggerNote(index) 80 | } 81 | } 82 | } else if (controllerState.channels[index].mode === 1) { 83 | // Tap mode 84 | controllerState.channels[index].tapDown = false 85 | } else if (controllerState.channels[index].mode === 2) { 86 | // Random mode 87 | } 88 | } 89 | } 90 | 91 | if (process.client) { 92 | window.addEventListener('message', ({ data }) => { 93 | if (data.name === 'mt-key-down') { 94 | handleKeyDown(data.key, data.shiftKey) 95 | } 96 | if (data.name === 'mt-key-up') { 97 | handleKeyUp(data.key, data.shiftKey) 98 | } 99 | }) 100 | window.addEventListener('keydown', (event) => { 101 | if (event.key === ' ') event.preventDefault() 102 | handleKeyDown(event.key, event.shiftKey) 103 | }) 104 | window.addEventListener('keyup', (event) => 105 | handleKeyUp(event.key, event.shiftKey) 106 | ) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/plugins/controllerState.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | const state = { 3 | channels: reactive([ 4 | { 5 | name: 'Flash', 6 | mode: 0, 7 | sequencer: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 8 | tapDown: false, 9 | random: { 10 | amount: 8, 11 | seed: 0 12 | } 13 | }, 14 | { 15 | name: 'Spawn', 16 | mode: 2, 17 | sequencer: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 18 | tapDown: false, 19 | random: { 20 | amount: 12, 21 | seed: 2 22 | } 23 | }, 24 | { 25 | name: 'Fwd', 26 | mode: 1, 27 | sequencer: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 28 | tapDown: false, 29 | random: { 30 | amount: 4, 31 | seed: 2 32 | } 33 | }, 34 | { 35 | name: 'Bwd', 36 | mode: 1, 37 | sequencer: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 38 | tapDown: false, 39 | random: { 40 | amount: 0, 41 | seed: 0 42 | } 43 | }, 44 | { 45 | name: 'Pixel', 46 | mode: 0, 47 | sequencer: [0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0], 48 | tapDown: false, 49 | random: { 50 | amount: 0, 51 | seed: 0 52 | } 53 | }, 54 | { 55 | name: 'PixelXL', 56 | mode: 1, 57 | sequencer: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 58 | tapDown: false, 59 | random: { 60 | amount: 8, 61 | seed: 4 62 | } 63 | }, 64 | { 65 | name: '', 66 | mode: 4, 67 | sequencer: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 68 | tapDown: false, 69 | random: { 70 | amount: 0, 71 | seed: 0 72 | } 73 | }, 74 | { 75 | name: 'Trails', 76 | mode: 2, 77 | sequencer: [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 78 | tapDown: false, 79 | random: { 80 | amount: 8, 81 | seed: 0 82 | } 83 | } 84 | ]) 85 | } 86 | const controllerStatePlugin = { 87 | install(app) { 88 | app.provide('controllerState', state) // provide is used on the whole application 89 | app.config.globalProperties.controllerState = state 90 | } 91 | } 92 | 93 | export default controllerStatePlugin 94 | -------------------------------------------------------------------------------- /src/plugins/i18n.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | // import { createI18n } from 'vue-i18n' 3 | 4 | const localPathPrefix = '../locales/' 5 | 6 | // import i18n resources 7 | // https://vitejs.dev/guide/features.html#glob-import 8 | const messages = Object.fromEntries( 9 | Object.entries( 10 | import.meta.globEager('../locales/*.y(a)?ml')) 11 | .map(([key, value]) => { 12 | const yaml = key.endsWith('.yaml') 13 | return [key.slice(localPathPrefix.length, yaml ? -5 : -4), value.default] 14 | }), 15 | ) 16 | 17 | const install = (app: App) => { 18 | const i18n = createI18n({ 19 | legacy: false, 20 | locale: 'en', 21 | globalInjection: true, 22 | messages, 23 | }) 24 | 25 | app.use(i18n) 26 | } 27 | 28 | export default install 29 | -------------------------------------------------------------------------------- /src/popup/Popup.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 85 | 86 | 162 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Popup 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/popup/main.ts: -------------------------------------------------------------------------------- 1 | import 'vue-global-api' 2 | import { createApp } from 'vue' 3 | import App from './Popup.vue' 4 | // import i18n from '~/plugins/i18n' 5 | import '../styles' 6 | 7 | const app = createApp(App) 8 | app.mount('#app') 9 | -------------------------------------------------------------------------------- /src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import 'virtual:windi.css' 2 | import './main.css' 3 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | /* html.dark { */ 10 | /* background: #121212; */ 11 | /* } */ 12 | 13 | .btn { 14 | @apply px-4 py-1 rounded-xl inline-block 15 | bg-white text-black cursor-pointer 16 | hover:bg-white 17 | disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50; 18 | } 19 | 20 | button:focus { 21 | outline: 0; 22 | } 23 | 24 | .flex-fill { 25 | flex: 1; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/bpmGuesser.js: -------------------------------------------------------------------------------- 1 | // Initialize variables 2 | let timestamps = [] 3 | let isCalculating = false 4 | let startTime = 0 5 | 6 | // Function to calculate BPM 7 | function calculateBPM() { 8 | if (timestamps.length < 2) return 0 9 | 10 | const intervals = [] 11 | for (let i = 1; i < timestamps.length; i++) { 12 | intervals.push(timestamps[i] - timestamps[i - 1]) 13 | } 14 | 15 | const averageInterval = intervals.reduce((a, b) => a + b) / intervals.length 16 | const bpm = 60000 / averageInterval 17 | 18 | return bpm 19 | } 20 | 21 | export default { 22 | guess: function () { 23 | const currentTime = Date.now() 24 | 25 | if (!isCalculating) { 26 | isCalculating = true 27 | startTime = currentTime 28 | // console.log('Started BPM calculation. Press Enter in time with the beat.') 29 | } else { 30 | const t = currentTime - startTime 31 | const diff = t - timestamps[timestamps.length - 1] || 0 32 | if (diff > 900) { 33 | isCalculating = false 34 | timestamps = [] 35 | return 36 | } 37 | timestamps.push(t) 38 | 39 | if (timestamps.length >= 4) { 40 | const bpm = calculateBPM() 41 | // console.log(`Estimated BPM: ${bpm}`) 42 | return bpm 43 | } else { 44 | // console.log(`Tap ${4 - timestamps.length} more times...`) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/createRandomSequence.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore' 2 | // import { XORShift } from 'random-seedable' 3 | 4 | export default function createRandomSequence({ amount = 1, seed = 0 } = {}) { 5 | const random = new SeededRandom(seed) 6 | const numbers = _.range(amount).map((i) => random.range(0, 16)) 7 | // return [0, 4, 8, 12] 8 | return _.sortBy(numbers, (n) => n) 9 | } 10 | 11 | class SeededRandom { 12 | constructor(seed = Date.now()) { 13 | this.state0 = this._murmurHash3(seed) 14 | this.state1 = this._murmurHash3(this.state0) 15 | } 16 | 17 | _murmurHash3(h) { 18 | h = Math.imul(h, 0xcc9e2d51) 19 | h = Math.imul((h << 15) | (h >>> 17), 0x1b873593) 20 | h = Math.imul((h << 13) | (h >>> 19), 5) + 0xe6546b64 21 | h ^= h >>> 16 22 | h = Math.imul(h, 0x85ebca6b) 23 | h ^= h >>> 13 24 | h = Math.imul(h, 0xc2b2ae35) 25 | h ^= h >>> 16 26 | return h >>> 0 27 | } 28 | 29 | next() { 30 | const s1 = this.state0 31 | const s0 = this.state1 32 | this.state0 = s0 33 | let s1_ = s1 34 | s1_ ^= s1_ << 23 35 | s1_ ^= s1_ >>> 17 36 | s1_ ^= s0 37 | s1_ ^= s0 >>> 26 38 | this.state1 = s1_ 39 | return (this.state0 + this.state1) >>> 0 40 | } 41 | 42 | float() { 43 | return this.next() / 4294967296 44 | } 45 | 46 | range(min, max) { 47 | return this.float() * (max - min) + min 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "es2016", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "incremental": false, 10 | "skipLibCheck": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "types": [ 17 | "vite/client", 18 | "chrome" 19 | ], 20 | "paths": { 21 | "~/*": ["src/*"] 22 | } 23 | }, 24 | "exclude": ["dist", "node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /video/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /video/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /video/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /video/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "gsap": "^3.12.5", 13 | "scss": "^0.2.4", 14 | "vue": "^3.4.37" 15 | }, 16 | "devDependencies": { 17 | "@vitejs/plugin-vue": "^5.1.2", 18 | "vite": "^5.4.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /video/public/keyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkpfm/minitap/32bd7f4bf2bc6f8f2eb1e94d814d028f9587aec3/video/public/keyboard.png -------------------------------------------------------------------------------- /video/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /video/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 38 | -------------------------------------------------------------------------------- /video/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /video/src/components/ControllerSlide.vue: -------------------------------------------------------------------------------- 1 | 120 | 121 | 137 | 138 | 154 | -------------------------------------------------------------------------------- /video/src/components/KeyboardSlide.vue: -------------------------------------------------------------------------------- 1 | 101 | 102 | 120 | 121 | 145 | -------------------------------------------------------------------------------- /video/src/components/KeyboardTempoSlide.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 125 | 126 | 158 | -------------------------------------------------------------------------------- /video/src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 128 | 129 | 201 | 202 | 207 | -------------------------------------------------------------------------------- /video/src/components/SqChannel.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 77 | 78 | 101 | -------------------------------------------------------------------------------- /video/src/components/SqPills.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 73 | 74 | 106 | -------------------------------------------------------------------------------- /video/src/components/TitleSlide.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 74 | 75 | 96 | -------------------------------------------------------------------------------- /video/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.scss' 3 | import App from './App.vue' 4 | import controllerState from './../../src/plugins/controllerState' 5 | import controllerClock from './../../src/plugins/controllerClock' 6 | import controllerShortcuts from './../../src/plugins/controllerShortcuts' 7 | import controllerOutput from './../../src/plugins/controllerOutput' 8 | 9 | createApp(App) 10 | .use(controllerState) 11 | .use(controllerClock) 12 | .use(controllerOutput) 13 | .use(controllerShortcuts) 14 | .mount('#app') 15 | -------------------------------------------------------------------------------- /video/src/style.scss: -------------------------------------------------------------------------------- 1 | /* windicss layer base */ 2 | *, 3 | ::before, 4 | ::after { 5 | -webkit-box-sizing: border-box; 6 | box-sizing: border-box; 7 | border-width: 0; 8 | border-style: solid; 9 | border-color: #e5e7eb; 10 | } 11 | * { 12 | --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/); 13 | --tw-ring-offset-width: 0px; 14 | --tw-ring-offset-color: #fff; 15 | --tw-ring-color: rgba(59, 130, 246, 0.5); 16 | --tw-ring-offset-shadow: 0 0 #0000; 17 | --tw-ring-shadow: 0 0 #0000; 18 | --tw-shadow: 0 0 #0000; 19 | } 20 | :root { 21 | -moz-tab-size: 4; 22 | -o-tab-size: 4; 23 | tab-size: 4; 24 | } 25 | :-moz-focusring { 26 | outline: 1px dotted ButtonText; 27 | } 28 | :-moz-ui-invalid { 29 | box-shadow: none; 30 | } 31 | ::moz-focus-inner { 32 | border-style: none; 33 | padding: 0; 34 | } 35 | ::-webkit-inner-spin-button, 36 | ::-webkit-outer-spin-button { 37 | height: auto; 38 | } 39 | ::-webkit-search-decoration { 40 | -webkit-appearance: none; 41 | } 42 | ::-webkit-file-upload-button { 43 | -webkit-appearance: button; 44 | font: inherit; 45 | } 46 | [type='search'] { 47 | -webkit-appearance: textfield; 48 | outline-offset: -2px; 49 | } 50 | abbr[title] { 51 | -webkit-text-decoration: underline dotted; 52 | text-decoration: underline dotted; 53 | } 54 | body { 55 | margin: 0; 56 | font-family: inherit; 57 | line-height: inherit; 58 | } 59 | button { 60 | text-transform: none; 61 | background-color: transparent; 62 | background-image: none; 63 | } 64 | button, 65 | [type='button'], 66 | [type='reset'], 67 | [type='submit'] { 68 | -webkit-appearance: button; 69 | } 70 | button, 71 | [role='button'] { 72 | cursor: pointer; 73 | } 74 | html { 75 | -webkit-text-size-adjust: 100%; 76 | font-family: 77 | ui-sans-serif, 78 | system-ui, 79 | -apple-system, 80 | 'Helvetica Neue', 81 | Arial, 82 | 'Apple Color Emoji', 83 | sans-serif; 84 | line-height: 1.5; 85 | background: #ececf0; 86 | --accent: #3b84e3; 87 | --accent-faded: #3b84e388; 88 | } 89 | input, 90 | button { 91 | font-family: inherit; 92 | font-size: 100%; 93 | line-height: 1.15; 94 | margin: 0; 95 | padding: 0; 96 | line-height: inherit; 97 | color: inherit; 98 | } 99 | img { 100 | border-style: solid; 101 | max-width: 100%; 102 | height: auto; 103 | } 104 | input::placeholder { 105 | opacity: 1; 106 | color: #9ca3af; 107 | } 108 | input::webkit-input-placeholder { 109 | opacity: 1; 110 | color: #9ca3af; 111 | } 112 | input::-moz-placeholder { 113 | opacity: 1; 114 | color: #9ca3af; 115 | } 116 | input:-ms-input-placeholder { 117 | opacity: 1; 118 | color: #9ca3af; 119 | } 120 | input::-ms-input-placeholder { 121 | opacity: 1; 122 | color: #9ca3af; 123 | } 124 | p { 125 | margin: 0; 126 | } 127 | small { 128 | font-size: 80%; 129 | } 130 | svg, 131 | img { 132 | display: block; 133 | vertical-align: middle; 134 | } 135 | /* windicss layer components */ 136 | 137 | /* windicss layer utilities */ 138 | [bg~='transparent'] { 139 | background-color: transparent; 140 | } 141 | [border~='gray-200'] { 142 | --tw-border-opacity: 1; 143 | border-color: rgba(229, 231, 235, var(--tw-border-opacity)); 144 | } 145 | .dark [border~='dark:gray-700'] { 146 | --tw-border-opacity: 1; 147 | border-color: rgba(55, 65, 81, var(--tw-border-opacity)); 148 | } 149 | .rounded { 150 | border-radius: 0.25rem; 151 | } 152 | [border~='rounded'] { 153 | border-radius: 0.25rem; 154 | } 155 | [border~='~'] { 156 | border-width: 1px; 157 | } 158 | .hidden { 159 | display: none; 160 | } 161 | .text-2xl { 162 | font-size: 1.5rem; 163 | line-height: 2rem; 164 | } 165 | .mx-2 { 166 | margin-left: 0.5rem; 167 | margin-right: 0.5rem; 168 | } 169 | .my-2 { 170 | margin-top: 0.5rem; 171 | margin-bottom: 0.5rem; 172 | } 173 | .mt-4 { 174 | margin-top: 1rem; 175 | } 176 | .mt-2 { 177 | margin-top: 0.5rem; 178 | } 179 | .opacity-50 { 180 | opacity: 0.5; 181 | } 182 | [opacity~='0'] { 183 | opacity: 0; 184 | } 185 | [outline~='none'] { 186 | outline: 2px solid transparent; 187 | outline-offset: 2px; 188 | } 189 | [outline~='active:none']:active { 190 | outline: 2px solid transparent; 191 | outline-offset: 2px; 192 | } 193 | .px-4 { 194 | padding-left: 1rem; 195 | padding-right: 1rem; 196 | } 197 | .py-10 { 198 | padding-top: 2.5rem; 199 | padding-bottom: 2.5rem; 200 | } 201 | [p~='x-4'] { 202 | padding-left: 1rem; 203 | padding-right: 1rem; 204 | } 205 | [p~='y-2'] { 206 | padding-top: 0.5rem; 207 | padding-bottom: 0.5rem; 208 | } 209 | [placeholder~='$t(']::-webkit-input-placeholder { 210 | color: var(--t); 211 | } 212 | [placeholder~='$t(']::-moz-placeholder { 213 | color: var(--t); 214 | } 215 | [placeholder~='$t(']:-ms-input-placeholder { 216 | color: var(--t); 217 | } 218 | [placeholder~='$t(']::-ms-input-placeholder { 219 | color: var(--t); 220 | } 221 | [placeholder~='$t(']::placeholder { 222 | color: var(--t); 223 | } 224 | [fill~='none'] { 225 | fill: none; 226 | } 227 | [stroke~='none'] { 228 | stroke: none; 229 | } 230 | .text-center { 231 | text-align: center; 232 | } 233 | [text~='center'] { 234 | text-align: center; 235 | } 236 | .text-gray-700 { 237 | --tw-text-opacity: 1; 238 | color: rgba(55, 65, 81, var(--tw-text-opacity)); 239 | } 240 | .dark .dark\:text-gray-200 { 241 | --tw-text-opacity: 1; 242 | color: rgba(229, 231, 235, var(--tw-text-opacity)); 243 | } 244 | .align-middle { 245 | vertical-align: middle; 246 | } 247 | [w~='250px'] { 248 | width: 250px; 249 | } 250 | .transform { 251 | --tw-translate-x: 0; 252 | --tw-translate-y: 0; 253 | --tw-translate-z: 0; 254 | --tw-rotate: 0; 255 | --tw-rotate-x: 0; 256 | --tw-rotate-y: 0; 257 | --tw-rotate-z: 0; 258 | --tw-skew-x: 0; 259 | --tw-skew-y: 0; 260 | --tw-scale-x: 1; 261 | --tw-scale-y: 1; 262 | --tw-scale-z: 1; 263 | -webkit-transform: translateX(var(--tw-translate-x)) 264 | translateY(var(--tw-translate-y)) translateZ(var(--tw-translate-z)) 265 | rotate(var(--tw-rotate)) rotateX(var(--tw-rotate-x)) 266 | rotateY(var(--tw-rotate-y)) rotateZ(var(--tw-rotate-z)) 267 | skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) 268 | scaleY(var(--tw-scale-y)) scaleZ(var(--tw-scale-z)); 269 | -ms-transform: translateX(var(--tw-translate-x)) 270 | translateY(var(--tw-translate-y)) translateZ(var(--tw-translate-z)) 271 | rotate(var(--tw-rotate)) rotateX(var(--tw-rotate-x)) 272 | rotateY(var(--tw-rotate-y)) rotateZ(var(--tw-rotate-z)) 273 | skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) 274 | scaleY(var(--tw-scale-y)) scaleZ(var(--tw-scale-z)); 275 | transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) 276 | translateZ(var(--tw-translate-z)) rotate(var(--tw-rotate)) 277 | rotateX(var(--tw-rotate-x)) rotateY(var(--tw-rotate-y)) 278 | rotateZ(var(--tw-rotate-z)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) 279 | scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) 280 | scaleZ(var(--tw-scale-z)); 281 | } 282 | 283 | .flex-fill { 284 | flex: 1; 285 | } 286 | 287 | h1, 288 | h2, 289 | h3, 290 | h4, 291 | h5, 292 | h6 { 293 | text-wrap: pretty; 294 | } 295 | 296 | body { 297 | display: flex; 298 | align-items: center; 299 | justify-content: center; 300 | height: 100vh; 301 | } 302 | 303 | #app { 304 | display: flex; 305 | align-items: center; 306 | justify-content: center; 307 | height: 100vh; 308 | } 309 | 310 | .center { 311 | position: absolute; 312 | left: 50%; 313 | top: 50%; 314 | transform: translate(-50%, -50%); 315 | } 316 | -------------------------------------------------------------------------------- /video/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | define: { 8 | 'process.client': true 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | // import Icons from 'unplugin-icons/vite' 5 | // import IconsResolver from 'unplugin-icons/resolver' 6 | // import Components from 'unplugin-vue-components/vite' 7 | import WindiCSS from 'vite-plugin-windicss' 8 | // import VueI18n from '@intlify/vite-plugin-vue-i18n' 9 | import windiConfig from './windi.config' 10 | 11 | const port = parseInt(process.env.PORT || '') || 3309 12 | const r = (...args: string[]) => resolve(__dirname, ...args) 13 | 14 | export default defineConfig(({ command }) => { 15 | const isDev = command === 'serve' 16 | 17 | return { 18 | root: r('src'), 19 | base: isDev ? `http://localhost:${port}/` : undefined, 20 | resolve: { 21 | alias: { 22 | '~/': `${r('src')}/` 23 | } 24 | }, 25 | server: { 26 | port, 27 | hmr: { 28 | host: 'localhost' 29 | } 30 | }, 31 | define: { 32 | 'process.client': true 33 | }, 34 | build: { 35 | outDir: r('extension/prod'), 36 | emptyOutDir: false, 37 | sourcemap: isDev ? 'inline' : false, 38 | rollupOptions: { 39 | input: { 40 | popup: r('src/popup/index.html'), 41 | options: r('src/options/index.html'), 42 | iframe: r('src/iframe/index.html'), 43 | contentStyle: r('src/content/style.css') 44 | } 45 | } 46 | }, 47 | plugins: [ 48 | Vue(), 49 | 50 | // Components({ 51 | // dirs: [r('src/components')], 52 | // // auto import icons 53 | // resolvers: [ 54 | // IconsResolver({ 55 | // prefix: '' 56 | // }) 57 | // ] 58 | // }), 59 | 60 | // Icons(), 61 | 62 | // https://github.com/antfu/vite-plugin-windicss 63 | WindiCSS({ 64 | config: windiConfig 65 | }), 66 | 67 | // https://github.com/intlify/vite-plugin-vue-i18n 68 | // VueI18n({ 69 | // include: [resolve(__dirname, 'src/locales/**')] 70 | // }), 71 | 72 | // rewrite assets to use relative path 73 | { 74 | name: 'assets-rewrite', 75 | enforce: 'post', 76 | apply: 'build', 77 | transformIndexHtml(html) { 78 | return html.replace(/"\/assets\//g, '"../assets/') 79 | } 80 | } 81 | ], 82 | 83 | optimizeDeps: { 84 | include: ['vue', '@vueuse/core'], 85 | exclude: ['vue-demi'] 86 | } 87 | } 88 | }) 89 | -------------------------------------------------------------------------------- /windi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'windicss/helpers' 2 | 3 | export default defineConfig({ 4 | darkMode: 'class', 5 | // https://windicss.org/posts/v30.html#attributify-mode 6 | attributify: true, 7 | extract: { 8 | include: [ 9 | '**/*.{vue,html}', 10 | ], 11 | }, 12 | }) 13 | --------------------------------------------------------------------------------