├── .gitignore
├── LICENSE
├── README.md
├── index.css
├── index.html
├── index.js
├── package-lock.json
├── package.json
└── scripts
└── deploy.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache/
2 | node_modules/
3 | dist/
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 webvrnsfw
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 | # vibe sequencer
2 |
3 | https://webvrnsfw.github.io/vibe-sequencer/
4 |
5 | A [Buttplug](https://buttplug.io/) client application that lets you control your vibrating sex toys with sequencers.
6 |
7 | 
8 |
--------------------------------------------------------------------------------
/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg: #222;
3 | }
4 |
5 | body, select, button {
6 | background: var(--bg);
7 | color: white;
8 | }
9 | select, button {
10 | border: 2px solid grey;
11 | }
12 | button:disabled {
13 | color: darkgrey;
14 | }
15 | button {
16 | padding: 8px 16px;
17 | }
18 |
19 | body {
20 | padding: 0 16px;
21 | }
22 | * {
23 | font-size: 16pt;
24 | }
25 | h1 {
26 | font-size: 20pt;
27 | }
28 |
29 | .device {
30 | display: block;
31 | }
32 | .device select {
33 | width: 16em;
34 | }
35 |
36 | .sequencer {
37 | margin: 32px 0;
38 | width: 852px;
39 | }
40 | .sequencer input, .sequencer button {
41 | width: 115px;
42 | margin: 4px 16px;
43 | }
44 | .spacer {
45 | flex: 1;
46 | }
47 | .controls {
48 | display: flex;
49 | }
50 | .grid {
51 | display: flex;
52 | user-select: none;
53 | margin: 8px;
54 | }
55 |
56 | .column {
57 | border: 2px solid var(--bg);
58 | display: flex;
59 | flex-direction: column-reverse;
60 | padding: 0 2px;
61 | }
62 | .column.playing {
63 | border: 2px solid #aaf;
64 | }
65 |
66 | .cell {
67 | width: 30px;
68 | height: 30px;
69 | margin: 2px 0;
70 | border: 2px solid grey;
71 | }
72 | .cell:hover {
73 | background: #ccc;
74 | }
75 | .cell.selected {
76 | background: #ddf;
77 | }
78 | .cell.selected:hover {
79 | background: #ccf;
80 | }
81 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useEffect } from "react";
2 | import ReactDOM from "react-dom";
3 | import cx from "classnames";
4 | import "@spectrum-web-components/theme/sp-theme.js";
5 | import "@spectrum-web-components/theme/theme-dark.js";
6 | import "@spectrum-web-components/theme/scale-medium.js";
7 | import "@spectrum-web-components/slider/sp-slider.js";
8 | import "./index.css";
9 |
10 | function range(n) {
11 | return Array.from({ length: n }).map((_, i) => i);
12 | }
13 |
14 | function Cell({ select, selected }) {
15 | return (
16 | e.buttons && select()}
20 | >
21 | );
22 | }
23 |
24 | function Column({ playing, val, setVal }) {
25 | return (
26 |
27 | {range(5).map((i) => (
28 | setVal(i)} selected={val === i} />
29 | ))}
30 | |
31 | );
32 | }
33 |
34 | function Sequencer({
35 | sequence,
36 | setSequence,
37 | device,
38 | paused,
39 | onToggle,
40 | onRemove,
41 | }) {
42 | const [playing, setPlaying] = useState(0);
43 |
44 | const setVal = useCallback(
45 | (v, i) => {
46 | const newSequence = {...sequence};
47 | newSequence.values[i] = v;
48 | setSequence(newSequence);
49 | },
50 | [sequence, setSequence]
51 | );
52 |
53 | const setDuration = useCallback(
54 | (duration) => {
55 | const newSequence = {...sequence};
56 | newSequence.duration = duration;
57 | setSequence(newSequence);
58 | },
59 | [sequence, setSequence]
60 | );
61 |
62 | useEffect(() => {
63 | if (paused) return;
64 |
65 | const id = setInterval(() => {
66 | setPlaying((playing) => {
67 | playing = (playing + 1) % 20;
68 | const allowedMessages = device.AllowedMessages;
69 | const messageTypes = Buttplug.ButtplugDeviceMessageType;
70 | if (allowedMessages.includes(messageTypes.LinearCmd)) {
71 | device.linear(sequence.values[playing] / 4, Math.floor(sequence.duration * 0.9));
72 | } else if (allowedMessages.includes(messageTypes.VibrateCmd)) {
73 | device.vibrate(sequence.values[playing] / 4);
74 | }
75 | return playing;
76 | });
77 | }, sequence.duration);
78 |
79 | return () => clearInterval(id);
80 | }, [device, paused, sequence]);
81 |
82 | return (
83 |
84 |
85 | {sequence.values.map((v, i) => (
86 | setVal(v, i)}
91 | />
92 | ))}
93 |
94 |
95 |
98 |
setDuration(parseInt(e.target.value, 10))}
105 | >
106 |
107 |
108 |
109 |
110 | );
111 | }
112 |
113 | const initialSequences = localStorage.sequences
114 | ? JSON.parse(localStorage.sequences)
115 | : [{values: range(20).map(() => 0), duration: 250}];
116 |
117 | function App() {
118 | const [connecting, setConnecting] = useState(true);
119 | const [devices, setDevices] = useState([]);
120 | const [device, setDevice] = useState();
121 | const [sequences, setSequences] = useState(initialSequences);
122 | const [playing, setPlaying] = useState();
123 |
124 | const setDeviceIndex = useCallback((index, devices) => {
125 | index = Number(index);
126 | if (isNaN(index)) return;
127 | localStorage.deviceIndex = index;
128 | const device = devices.find((device) => device.Index === index);
129 | if (device) {
130 | setDevice(device);
131 | } else if (devices.length) {
132 | setDevice(devices[0]);
133 | localStorage.deviceIndex = 0;
134 | }
135 | });
136 |
137 | useEffect(async () => {
138 | await Buttplug.buttplugInit();
139 |
140 | const client = new Buttplug.ButtplugClient("vibe sequencer");
141 | client.on("deviceadded", () => {
142 | setDevices(client.Devices);
143 | setDeviceIndex(localStorage.deviceIndex, client.Devices);
144 | });
145 |
146 | await client.connect(new Buttplug.ButtplugWebsocketConnectorOptions());
147 |
148 | setDevices(client.Devices);
149 | setDeviceIndex(localStorage.deviceIndex, client.Devices);
150 |
151 | client.startScanning();
152 |
153 | setConnecting(false);
154 | }, []);
155 |
156 | return (
157 |
158 | vibe sequencer
159 |
160 |
176 |
177 | {sequences.map((sequence, i) => {
178 | return (
179 | {
182 | if (play) setPlaying(i);
183 | else {
184 | device.stop();
185 | setPlaying(null);
186 | }
187 | }}
188 | onRemove={() => {
189 | if (playing === i) {
190 | if (device) device.stop();
191 | setPlaying(null);
192 | }
193 | const newSequences = sequences.slice(0);
194 | newSequences.splice(i, 1);
195 | localStorage.sequences = JSON.stringify(newSequences);
196 | setSequences(newSequences);
197 | }}
198 | key={i}
199 | sequence={sequence}
200 | setSequence={(sequence) => {
201 | const newSequences = sequences.slice(0);
202 | newSequences[i] = sequence;
203 | localStorage.sequences = JSON.stringify(newSequences);
204 | setSequences(newSequences);
205 | }}
206 | device={device}
207 | />
208 | );
209 | })}
210 |
211 |
221 |
222 | );
223 | }
224 |
225 | ReactDOM.render(, document.getElementById("root"));
226 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "start": "parcel serve index.html",
5 | "build": "parcel build --public-url './' index.html"
6 | },
7 | "browserslist": [
8 | "last 3 Chrome versions",
9 | "last 3 Firefox versions",
10 | "last 3 Safari versions"
11 | ],
12 | "dependencies": {
13 | "@spectrum-web-components/slider": "^0.9.3",
14 | "@spectrum-web-components/theme": "^0.8.4",
15 | "classnames": "^2.3.1",
16 | "react": "^17.0.2",
17 | "react-dom": "^17.0.2"
18 | },
19 | "devDependencies": {
20 | "parcel-bundler": "^1.12.5"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/scripts/deploy.sh:
--------------------------------------------------------------------------------
1 | rm -rf dist
2 | npm run build
3 | pushd dist
4 | git init
5 | git add .
6 | git commit -m 'deploy'
7 | git remote add origin https://github.com/webvrnsfw/vibe-sequencer
8 | git push -u origin --force master:gh-pages
9 | popd
10 | rm -rf dist
11 |
--------------------------------------------------------------------------------