├── .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 |
--------------------------------------------------------------------------------
/demo/src/App.vue:
--------------------------------------------------------------------------------
1 |
25 |
26 |
27 |
28 |
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 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
83 |
84 |
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 |
--------------------------------------------------------------------------------
/public/icon-512@1x.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dkpfm/minitap/32bd7f4bf2bc6f8f2eb1e94d814d028f9587aec3/public/logo.png
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/icon-512@1x.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dkpfm/minitap/32bd7f4bf2bc6f8f2eb1e94d814d028f9587aec3/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
25 |
26 |
31 |
--------------------------------------------------------------------------------
/src/components/ControllerChannelRandom.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ controllerState.channels[channelIndex].random.amount }}
6 |
7 |
13 |
14 |
27 |
28 |
29 |
30 |
87 |
88 |
178 |
--------------------------------------------------------------------------------
/src/components/ControllerChannelRandomRadarBg.vue:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/ControllerChannelSequencer.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
44 |
45 |
73 |
--------------------------------------------------------------------------------
/src/components/ControllerChannelTap.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ props.letter }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
37 |
38 |
80 |
--------------------------------------------------------------------------------
/src/components/ControllerChannels.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
49 |
50 |
63 |
--------------------------------------------------------------------------------
/src/components/ControllerChannelsItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
20 |
21 |
29 |
30 |
31 |
32 |
33 |
38 |
39 |
40 |
41 |
42 |
43 |
59 |
60 |
127 |
--------------------------------------------------------------------------------
/src/components/ControllerChannelsItemActions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
17 |
24 |
25 |
26 |
27 |
42 |
43 |
70 |
--------------------------------------------------------------------------------
/src/components/ControllerPlayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
17 |
18 |
30 |
31 |
79 |
--------------------------------------------------------------------------------
/src/components/ControllerPlayerCD.vue:
--------------------------------------------------------------------------------
1 |
2 |
64 |
65 |
66 |
75 |
--------------------------------------------------------------------------------
/src/components/ControllerPlayerTimeline.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
209 |
210 |
211 |
212 |
221 |
222 |
232 |
--------------------------------------------------------------------------------
/src/components/ControllerPlayerVisuals.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
20 |
--------------------------------------------------------------------------------
/src/components/ControllerTempo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 | {{ Math.round(controllerClock.bpm.value) }}BPM
10 |
11 |
12 |
{{ controllerClock.barBeat.value + 1 }}/4
13 |
16 |
17 |
18 |
19 |
32 |
33 |
102 |
--------------------------------------------------------------------------------
/src/components/Logo.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
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 |
2 |
23 |
24 |
25 |
32 |
--------------------------------------------------------------------------------
/src/components/icon/Pause.vue:
--------------------------------------------------------------------------------
1 |
2 |
31 |
32 |
33 |
40 |
--------------------------------------------------------------------------------
/src/components/icon/Play.vue:
--------------------------------------------------------------------------------
1 |
2 |
32 |
33 |
34 |
41 |
--------------------------------------------------------------------------------
/src/components/icon/Power.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/icon/Tap.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
31 |
--------------------------------------------------------------------------------
/src/components/icon/TapFx.vue:
--------------------------------------------------------------------------------
1 |
2 |
69 |
70 |
78 |
--------------------------------------------------------------------------------
/src/components/icon/Wave.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
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 |
115 |
116 |
117 |
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 |
2 |
3 |
4 | Options
5 |
6 | {{ $t('options.desc') }}
7 |
8 |
9 |
23 |
24 |
25 | {{ $t('options.powered_by_vite') }}
26 |
27 |
28 |
29 |
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 |
2 |
23 |
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 |