Video to Repeated Sequence Generator - Rhythm & Glitch Loop Maker
About this app
Turn any video into custom stutter sequences, glitch loops, beat-synced patterns, and boomerangs with real-time effects and WebM export.
Video to Repeated Sequence Generator - Rhythm & Glitch Loop Maker Video Repeat Sequencer Procedural Sample Stream Upload Video Webcam Sample RECORD SEQUENCE START BPM: 120 Slices 2 Segments 4 Segments 8 Segments Visual FX Filter None Glitch Slice RGB Shift Retro VHS Cyber Neon Invert Pattern Presets Boomerang Stutter Random PingPong Reset Tap step pads to change slice. Click reverse icons to flip direction.
Related apps
Put this on your site
Source
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Video to Repeated Sequence Generator - Rhythm & Glitch Loop Maker</title>
<meta name="description" content="Turn any video into custom stutter sequences, glitch loops, beat-synced patterns, and boomerangs with real-time effects and WebM export.">
<meta name="keywords" content="video sequencer, stutter effect, video repeater, glitch generator, video beat loop, boomerang creator, canvas video player">
<!-- Libraries -->
<!-- jQuery (3.6.0) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js" crossorigin="anonymous"></script>
<!-- Bootstrap CSS (5.3.3) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
<!-- Bootstrap JS (5.3.3) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
<!-- Font Awesome (6.6.0) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" crossorigin="anonymous">
<script type="text/javascript">
try {
document.addEventListener("DOMContentLoaded", function() {
// Core Video Sequencer Application Logic
const app = {
video: document.createElement('video'),
canvas: document.getElementById('previewCanvas'),
ctx: null,
sampleCanvas: document.createElement('canvas'),
sampleCtx: null,
audioCtx: null,
// State
isPlaying: false,
bpm: 120,
sliceCount: 4,
stepCount: 16,
currentStep: 0,
lastStepTime: 0,
sourceType: 'sample', // sample, file, webcam
fxMode: 'none',
audioEnabled: true,
// Step Pattern Data: Array of step objects { slice: 0..N, speed: 1, reverse: false }
sequence: [],
// Recording
mediaRecorder: null,
recordedChunks: [],
isRecording: false,
init() {
this.ctx = this.canvas.getContext('2d');
this.video.setAttribute('playsinline', '');
this.video.setAttribute('webkit-playsinline', '');
this.video.muted = true;
this.video.loop = true;
this.initSampleCanvas();
this.initSequenceData();
this.bindEvents();
this.renderStepGrid();
// Start procedural video generator default
this.loadSampleSource();
// Frame loop
requestAnimationFrame((t) => this.loop(t));
},
initSampleCanvas() {
this.sampleCanvas.width = 640;
this.sampleCanvas.height = 360;
this.sampleCtx = this.sampleCanvas.getContext('2d');
},
initSequenceData() {
this.sequence = [];
for (let i = 0; i < this.stepCount; i++) {
this.sequence.push({
slice: i % this.sliceCount,
speed: 1,
reverse: false,
active: true
});
}
},
loadSampleSource() {
this.sourceType = 'sample';
$('#sourceStatus').text('Procedural Sample Stream');
},
drawSampleFrame(timeSec) {
const ctx = this.sampleCtx;
const w = this.sampleCanvas.width;
const h = this.sampleCanvas.height;
// Background gradient
const grad = ctx.createLinearGradient(0, 0, w, h);
const hue1 = (timeSec * 40) % 360;
const hue2 = (hue1 + 120) % 360;
grad.addColorStop(0, `hsl(${hue1}, 80%, 20%)`);
grad.addColorStop(1, `hsl(${hue2}, 90%, 15%)`);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// Animated Shapes
ctx.save();
ctx.translate(w / 2, h / 2);
// Bouncing/Rotating Star
const rot = timeSec * 3;
ctx.rotate(rot);
ctx.fillStyle = `hsl(${(hue1 + 180) % 360}, 100%, 60%)`;
ctx.beginPath();
for (let i = 0; i < 5; i++) {
ctx.lineTo(Math.cos((18 + i * 72) * Math.PI / 180) * 90, -Math.sin((18 + i * 72) * Math.PI / 180) * 90);
ctx.lineTo(Math.cos((54 + i * 72) * Math.PI / 180) * 40, -Math.sin((54 + i * 72) * Math.PI / 180) * 40);
}
ctx.closePath();
ctx.fill();
ctx.restore();
// Moving Text / Counter
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 36px monospace';
ctx.textAlign = 'center';
const beatNum = Math.floor(timeSec * 2) % 4 + 1;
ctx.fillText(`SAMPLE BEAT: ${beatNum}`, w / 2, 70);
// Frame bar indicator
const progress = (timeSec % 2) / 2;
ctx.fillStyle = '#00f0ff';
ctx.fillRect(50, h - 40, (w - 100) * progress, 10);
},
bindEvents() {
const self = this;
// Play / Pause
$('#btnPlay').on('click', function() {
self.togglePlay();
});
// BPM Control
$('#bpmInput').on('input', function() {
self.bpm = parseInt($(this).val());
$('#bpmVal').text(self.bpm);
});
// Slice Selector
$('#sliceSelect').on('change', function() {
self.sliceCount = parseInt($(this).val());
self.initSequenceData();
self.renderStepGrid();
});
// FX Selector
$('#fxSelect').on('change', function() {
self.fxMode = $(this).val();
});
// Preset Buttons
$('.btn-preset').on('click', function() {
const preset = $(this).data('preset');
self.applyPreset(preset);
});
// Video File Input
$('#videoFileInput').on('change', function(e) {
const file = e.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
self.video.src = url;
self.video.load();
self.video.onloadeddata = () => {
self.sourceType = 'file';
self.video.play();
$('#sourceStatus').text(`Loaded File: ${file.name.substring(0, 18)}...`);
};
}
});
// Webcam Source
$('#btnWebcam').on('click', async function() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
self.video.srcObject = stream;
self.video.play();
self.sourceType = 'webcam';
$('#sourceStatus').text('Live Webcam Active');
} catch (err) {
alert('Could not access webcam: ' + err.message);
}
});
// Preset Sample Video
$('#btnSample').on('click', function() {
if (self.video.srcObject) {
const tracks = self.video.srcObject.getTracks();
tracks.forEach(t => t.stop());
self.video.srcObject = null;
}
self.loadSampleSource();
});
// Sound Toggle
$('#btnAudioToggle').on('click', function() {
self.audioEnabled = !self.audioEnabled;
$(this).toggleClass('btn-outline-info btn-info');
$(this).find('i').toggleClass('fa-volume-xmark fa-volume-high');
});
// Export Recording
$('#btnRecord').on('click', function() {
self.toggleRecord();
});
},
togglePlay() {
this.isPlaying = !this.isPlaying;
if (this.isPlaying) {
$('#btnPlay').html('<i class="fa-solid fa-pause me-1"></i> PAUSE').addClass('btn-warning').removeClass('btn-success');
this.lastStepTime = performance.now();
this.initAudioContext();
} else {
$('#btnPlay').html('<i class="fa-solid fa-play me-1"></i> START').addClass('btn-success').removeClass('btn-warning');
}
},
initAudioContext() {
if (!this.audioCtx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
this.audioCtx = new AudioContext();
}
if (this.audioCtx.state === 'suspended') {
this.audioCtx.resume();
}
},
playBeatSound(stepIdx) {
if (!this.audioEnabled || !this.audioCtx) return;
try {
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.connect(gain);
gain.connect(this.audioCtx.destination);
const now = this.audioCtx.currentTime;
if (stepIdx % 4 === 0) {
// Kick sound
osc.frequency.setValueAtTime(130, now);
osc.frequency.exponentialRampToValueAtTime(0.01, now + 0.15);
gain.gain.setValueAtTime(0.6, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
} else if (stepIdx % 2 === 0) {
// Snare sound
osc.type = 'triangle';
osc.frequency.setValueAtTime(220, now);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.08);
} else {
// Hi-hat tick
osc.type = 'square';
osc.frequency.setValueAtTime(800, now);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.03);
}
osc.start(now);
osc.stop(now + 0.15);
} catch(e){}
},
applyPreset(preset) {
for (let i = 0; i < this.stepCount; i++) {
const step = this.sequence[i];
if (preset === 'boomerang') {
// 0..N then N..0
const cycle = this.sliceCount * 2 - 2;
const pos = i % (cycle || 1);
step.slice = pos < this.sliceCount ? pos : cycle - pos;
step.reverse = pos >= this.sliceCount;
} else if (preset === 'stutter') {
// Stutter first slice 4 times, then second 4 times...
step.slice = Math.floor(i / 2) % this.sliceCount;
step.speed = (i % 2 === 1) ? 2 : 1;
} else if (preset === 'glitch') {
// Alternating slices randomly
step.slice = Math.floor(Math.random() * this.sliceCount);
step.reverse = Math.random() > 0.6;
} else if (preset === 'pingpong') {
step.slice = i % 2 === 0 ? 0 : (i % this.sliceCount);
} else if (preset === 'reset') {
step.slice = i % this.sliceCount;
step.speed = 1;
step.reverse = false;
}
}
this.renderStepGrid();
},
renderStepGrid() {
const grid = $('#stepGrid');
grid.empty();
this.sequence.forEach((step, idx) => {
const sliceColors = ['#ff007f', '#00f0ff', '#ffe600', '#00ff66', '#a100ff', '#ff6600', '#00a8ff', '#ff0055'];
const color = sliceColors[step.slice % sliceColors.length];
const stepEl = $(`
<div class="step-card ${idx === this.currentStep ? 'active' : ''}" data-idx="${idx}">
<div class="step-num">${idx + 1}</div>
<div class="slice-badge" style="background-color: ${color}">
S-${step.slice + 1}
</div>
<div class="step-controls">
<button class="btn-slice-next btn btn-sm btn-dark p-0" title="Change Segment"><i class="fa-solid fa-arrows-rotate"></i></button>
<button class="btn-rev btn btn-sm ${step.reverse ? 'btn-danger' : 'btn-outline-secondary'} p-0" title="Reverse"><i class="fa-solid fa-backward"></i></button>
</div>
</div>
`);
stepEl.find('.btn-slice-next').on('click', (e) => {
e.stopPropagation();
step.slice = (step.slice + 1) % this.sliceCount;
this.renderStepGrid();
});
stepEl.find('.btn-rev').on('click', (e) => {
e.stopPropagation();
step.reverse = !step.reverse;
this.renderStepGrid();
});
stepEl.on('click', () => {
step.slice = (step.slice + 1) % this.sliceCount;
this.renderStepGrid();
});
grid.append(stepEl);
});
},
loop(timestamp) {
// Time step calculation
const stepDuration = (60 / this.bpm / 4) * 1000; // 16th note duration in ms
if (this.isPlaying && (timestamp - this.lastStepTime >= stepDuration)) {
this.currentStep = (this.currentStep + 1) % this.stepCount;
this.lastStepTime = timestamp;
// Highlight UI grid step
$('.step-card').removeClass('active');
$(`.step-card[data-idx="${this.currentStep}"]`).addClass('active');
// Play audio click/beat
this.playBeatSound(this.currentStep);
}
// Render Canvas Frame
this.renderFrame(timestamp);
requestAnimationFrame((t) => this.loop(t));
},
renderFrame(timestamp) {
const activeStep = this.sequence[this.currentStep] || { slice: 0, reverse: false };
const vW = this.canvas.width;
const vH = this.canvas.height;
// Calculate video position based on slice
let vidDuration = 3.0; // default for sample
if (this.sourceType !== 'sample' && this.video.duration) {
vidDuration = this.video.duration;
}
const sliceLen = vidDuration / this.sliceCount;
const sliceStart = activeStep.slice * sliceLen;
// Calculate current frame offset inside the step loop
const stepDuration = (60 / this.bpm / 4) * 1000;
const elapsedInStep = (timestamp - this.lastStepTime) / stepDuration; // 0..1
let frameOffset = activeStep.reverse ? (1 - elapsedInStep) * sliceLen : elapsedInStep * sliceLen;
let targetTime = sliceStart + frameOffset;
// Draw source image to main canvas
this.ctx.save();
this.ctx.clearRect(0, 0, vW, vH);
if (this.sourceType === 'sample') {
this.drawSampleFrame(targetTime);
this.ctx.drawImage(this.sampleCanvas, 0, 0, vW, vH);
} else if (this.video.readyState >= 2) {
if (this.sourceType === 'file') {
// Seek video carefully
if (Math.abs(this.video.currentTime - targetTime) > 0.08) {
this.video.currentTime = targetTime;
}
}
this.ctx.drawImage(this.video, 0, 0, vW, vH);
}
// Apply Selected FX Filter
this.applyFXFilter(vW, vH, timestamp);
// Overlay Step HUD Info
this.drawHUD(vW, vH, activeStep);
this.ctx.restore();
},
applyFXFilter(w, h, time) {
if (this.fxMode === 'none') return;
if (this.fxMode === 'glitch') {
if (Math.random() > 0.6) {
const sliceY = Math.random() * h;
const sliceH = Math.random() * 40 + 10;
const offsetX = (Math.random() - 0.5) * 50;
this.ctx.drawImage(this.canvas, 0, sliceY, w, sliceH, offsetX, sliceY, w, sliceH);
}
} else if (this.fxMode === 'rgbShift') {
const imgData = this.ctx.getImageData(0, 0, w, h);
const d = imgData.data;
const offset = Math.floor(Math.sin(time / 100) * 8) * 4;
for (let i = 0; i < d.length - offset; i += 4) {
d[i] = d[i + offset] || d[i]; // Shift Red channel
}
this.ctx.putImageData(imgData, 0, 0);
} else if (this.fxMode === 'vhs') {
// Scanlines + CRT tint
this.ctx.fillStyle = 'rgba(10, 255, 200, 0.05)';
this.ctx.fillRect(0, 0, w, h);
this.ctx.fillStyle = 'rgba(0,0,0,0.2)';
for (let y = 0; y < h; y += 4) {
this.ctx.fillRect(0, y, w, 2);
}
} else if (this.fxMode === 'invert') {
const imgData = this.ctx.getImageData(0, 0, w, h);
const d = imgData.data;
for (let i = 0; i < d.length; i += 4) {
d[i] = 255 - d[i];
d[i+1] = 255 - d[i+1];
d[i+2] = 255 - d[i+2];
}
this.ctx.putImageData(imgData, 0, 0);
} else if (this.fxMode === 'neon') {
this.ctx.globalCompositeOperation = 'screen';
this.ctx.fillStyle = 'rgba(255, 0, 128, 0.2)';
this.ctx.fillRect(0, 0, w, h);
this.ctx.globalCompositeOperation = 'source-over';
}
},
drawHUD(w, h, step) {
// Mini Beat Grid indicator at top
const barWidth = w / this.stepCount;
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
this.ctx.fillRect(0, 0, w, 8);
this.ctx.fillStyle = '#00f0ff';
this.ctx.fillRect(this.currentStep * barWidth, 0, barWidth, 8);
// Active Slice Tag
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
this.ctx.fillRect(10, h - 35, 120, 25);
this.ctx.fillStyle = '#ffffff';
this.ctx.font = 'bold 12px sans-serif';
this.ctx.fillText(`STEP: ${this.currentStep + 1} | SEG: ${step.slice + 1}`, 18, h - 18);
},
toggleRecord() {
if (this.isRecording) {
this.stopRecording();
} else {
this.startRecording();
}
},
startRecording() {
this.recordedChunks = [];
const stream = this.canvas.captureStream(30);
// If audio context exists, mix synth audio
if (this.audioCtx && this.audioEnabled) {
const dest = this.audioCtx.createMediaStreamDestination();
// Merge streams if supported
const audioTrack = dest.stream.getAudioTracks()[0];
if (audioTrack) stream.addTrack(audioTrack);
}
try {
this.mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
} catch (e) {
this.mediaRecorder = new MediaRecorder(stream);
}
this.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) this.recordedChunks.push(e.data);
};
this.mediaRecorder.onstop = () => {
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `repeated-sequence-${Date.now()}.webm`;
a.click();
$('#btnRecord').html('<i class="fa-solid fa-download me-1"></i> RECORD SEQUENCE').removeClass('btn-danger').addClass('btn-outline-danger');
this.isRecording = false;
};
this.mediaRecorder.start();
this.isRecording = true;
$('#btnRecord').html('<i class="fa-solid fa-stop me-1"></i> STOP & SAVE').removeClass('btn-outline-danger').addClass('btn-danger');
},
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
}
}
};
app.init();
});
} catch (error) {
console.error("App error:", error);
}
</script>
<style>
/* Mobile-first Base Styling */
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
max-width: 100%;
background-color: #0d0f17;
color: #e2e8f0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
overflow-x: hidden;
}
button, input, select {
font-size: 16px;
}
#main-container {
width: 100%;
max-width: 900px;
margin: 0 auto;
padding: 0.75rem;
}
/* Neon App Header */
.app-header {
background: linear-gradient(135deg, #16192b 0%, #251332 100%);
border: 1px solid #323753;
border-radius: 12px;
padding: 0.75rem 1rem;
margin-bottom: 0.75rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.app-title {
font-size: 1.25rem;
font-weight: 800;
background: linear-gradient(90deg, #00f0ff, #ff007f);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0;
}
/* Main Canvas Display */
.preview-container {
position: relative;
width: 100%;
background: #000;
border-radius: 12px;
overflow: hidden;
border: 2px solid #23283b;
box-shadow: 0 8px 30px rgba(0,240,255,0.1);
}
#previewCanvas {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
display: block;
}
/* Video Source Toolbar */
.source-toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
margin-top: 0.75rem;
background: #141724;
padding: 0.5rem 0.75rem;
border-radius: 8px;
}
/* Control Cards */
.control-card {
background: #151828;
border: 1px solid #282d44;
border-radius: 10px;
padding: 0.75rem;
margin-top: 0.75rem;
}
/* Step Sequencer Grid */
.step-grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 0.5rem;
margin-top: 0.5rem;
}
.step-card {
background: #1e2338;
border: 2px solid #2f3654;
border-radius: 8px;
padding: 0.35rem;
text-align: center;
cursor: pointer;
transition: all 0.15s ease;
user-select: none;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.step-card:hover {
border-color: #00f0ff;
}
.step-card.active {
border-color: #ff007f;
box-shadow: 0 0 12px rgba(255, 0, 127, 0.6);
transform: scale(1.04);
}
.step-num {
font-size: 0.7rem;
color: #8a94b8;
font-weight: 700;
}
.slice-badge {
font-size: 0.75rem;
font-weight: 800;
color: #000;
padding: 2px 4px;
border-radius: 4px;
}
.step-controls {
display: flex;
justify-content: space-between;
gap: 2px;
}
.step-controls .btn {
font-size: 0.65rem;
width: 100%;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Control Sliders & Selects */
.form-label-sm {
font-size: 0.75rem;
font-weight: 700;
color: #94a3b8;
margin-bottom: 0.25rem;
}
.form-select-sm, .form-control-sm {
background-color: #0f121e;
border-color: #2b324d;
color: #fff;
}
.form-select-sm:focus, .form-control-sm:focus {
background-color: #0f121e;
color: #fff;
border-color: #00f0ff;
box-shadow: none;
}
/* Mobile Thumb Buttons */
.btn-action {
min-height: 44px;
font-weight: 700;
letter-spacing: 0.5px;
}
.badge-status {
font-size: 0.75rem;
color: #00f0ff;
background: rgba(0,240,255,0.1);
padding: 4px 8px;
border-radius: 6px;
}
</style>
</head>
<body>
<div id="main-container">
<!-- App Header -->
<header class="app-header d-flex justify-content-between align-items-center">
<div>
<h1 class="app-title"><i class="fa-solid fa-repeat me-2"></i>Video Repeat Sequencer</h1>
<span id="sourceStatus" class="badge-status mt-1 d-inline-block">Procedural Sample Stream</span>
</div>
<button id="btnAudioToggle" class="btn btn-sm btn-outline-info" title="Toggle Beat Synth Audio">
<i class="fa-solid fa-volume-high"></i>
</button>
</header>
<!-- Main Canvas Stage -->
<div class="preview-container">
<canvas id="previewCanvas" width="640" height="360"></canvas>
</div>
<!-- Video Source Controls -->
<div class="source-toolbar">
<div class="d-flex gap-2 align-items-center flex-wrap">
<label class="btn btn-sm btn-outline-light mb-0">
<i class="fa-solid fa-upload me-1"></i> Upload Video
<input type="file" id="videoFileInput" accept="video/*" class="d-none">
</label>
<button id="btnWebcam" class="btn btn-sm btn-outline-light">
<i class="fa-solid fa-camera me-1"></i> Webcam
</button>
<button id="btnSample" class="btn btn-sm btn-outline-secondary">
<i class="fa-solid fa-wand-magic-sparkles me-1"></i> Sample
</button>
</div>
<div>
<button id="btnRecord" class="btn btn-sm btn-outline-danger">
<i class="fa-solid fa-download me-1"></i> RECORD SEQUENCE
</button>
</div>
</div>
<!-- Transport & Rhythm Bar -->
<div class="control-card">
<div class="row g-2 align-items-center">
<div class="col-6 col-sm-3">
<button id="btnPlay" class="btn btn-success btn-action w-100">
<i class="fa-solid fa-play me-1"></i> START
</button>
</div>
<div class="col-6 col-sm-3">
<label class="form-label-sm">BPM: <span id="bpmVal" class="text-info">120</span></label>
<input type="range" class="form-range" id="bpmInput" min="60" max="240" value="120">
</div>
<div class="col-6 col-sm-3">
<label class="form-label-sm">Slices</label>
<select id="sliceSelect" class="form-select form-select-sm">
<option value="2">2 Segments</option>
<option value="4" selected>4 Segments</option>
<option value="8">8 Segments</option>
</select>
</div>
<div class="col-6 col-sm-3">
<label class="form-label-sm">Visual FX Filter</label>
<select id="fxSelect" class="form-select form-select-sm">
<option value="none">None</option>
<option value="glitch">Glitch Slice</option>
<option value="rgbShift">RGB Shift</option>
<option value="vhs">Retro VHS</option>
<option value="neon">Cyber Neon</option>
<option value="invert">Invert</option>
</select>
</div>
</div>
</div>
<!-- Sequence Presets -->
<div class="control-card">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="form-label-sm mb-0"><i class="fa-solid fa-sliders me-1"></i> Pattern Presets</span>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary btn-preset" data-preset="boomerang">Boomerang</button>
<button class="btn btn-outline-secondary btn-preset" data-preset="stutter">Stutter</button>
<button class="btn btn-outline-secondary btn-preset" data-preset="glitch">Random</button>
<button class="btn btn-outline-secondary btn-preset" data-preset="pingpong">PingPong</button>
<button class="btn btn-outline-danger btn-preset" data-preset="reset">Reset</button>
</div>
</div>
<!-- 16 Step Interactive Sequencer Grid -->
<div id="stepGrid" class="step-grid-container">
<!-- Dynamic step cards generated via JS -->
</div>
</div>
<!-- Mobile Quick Guide -->
<div class="text-center mt-3 text-muted small">
<p class="mb-0"><i class="fa-solid fa-circle-info me-1"></i> Tap step pads to change slice. Click reverse icons to flip direction.</p>
</div>
</div>
</body>
</html>
NEW APPS
These are apps made by the community!