LoopMatrix - Video Sequence Repeater & Stutter Maker
About this app
LoopMatrix - Video Sequence Repeater & Stutter Maker LoopMatrix Video to Repeated Sequence & Stutter FX Interactive Sample Pause Record WebM Open Video Sequence Chops Timeline Click block to jump Trim Range & Speed Start Cut: 0.50s End Cut: 2.00s Playback Speed 1.00x Repeat Engine & Pattern Repeat Mode Stutter Chops (4x) Boomerang (Ping-Pong) Standard Loop 8-Bit Glitch Pattern Chaos Randomizer Repeats Count 1 Repeat 2 Repeats 3 Repeats 4 Repeats 6 Repeats ⚡ Stutter Glitch 🪃 Boomerang 🌐 Cyber Beat 🎲 Chaos Visual FX Filter None (Clean) Digital Glitch Slices RGB Split Offset Retro CRT Scanlines Cyber Neon Wash Pink Duotone FX Distortion Intensity Sequence Video Ready! Your repeated sequence video loop has been rendered. Download WebM Video
Related apps
Put this on your site
Source
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="I=edge">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>LoopMatrix - Video Sequence Repeater & Stutter Maker</title>
<meta name="description" content="Turn videos into repeated sequences, boomerangs, glitches, and stutter loops with instant export.">
<meta name="keywords" content="video loop, sequence repeater, video stutter, boomerang maker, glitch video, video editor web app">
<!-- Libraries -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous">
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
width: 100%;
max-width: 100%;
overflow-x: hidden;
background-color: #0b0f19;
color: #e2e8f0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
-webkit-text-size-adjust: 100%;
}
button, input, select, textarea {
font-size: 16px;
max-width: 100%;
}
#main-container {
width: 100%;
max-width: 1100px;
min-height: 100dvh;
padding: 0.75rem;
margin: 0 auto;
padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
}
/* Custom UI Styling */
.app-card {
background: rgba(23, 31, 51, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1rem;
backdrop-filter: blur(10px);
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
margin-bottom: 1rem;
}
.canvas-wrapper {
position: relative;
width: 100%;
background: #000;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 0 20px rgba(124, 58, 237, 0.25);
aspect-ratio: 16 / 9;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.badge-live {
position: absolute;
top: 10px;
left: 10px;
background: rgba(220, 38, 38, 0.85);
color: white;
font-size: 0.75rem;
font-weight: bold;
padding: 3px 8px;
border-radius: 20px;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 5px;
z-index: 10;
}
.pulse-dot {
width: 8px;
height: 8px;
background: #fff;
border-radius: 50%;
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.3; transform: scale(0.8); }
100% { opacity: 1; transform: scale(1); }
}
.control-btn {
min-height: 44px;
border-radius: 8px;
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
}
.btn-neon-purple {
background: linear-gradient(135deg, #7c3aed, #a855f7);
color: white;
border: none;
}
.btn-neon-purple:hover, .btn-neon-purple:active {
background: linear-gradient(135deg, #6d28d9, #9333ea);
color: white;
}
.btn-neon-cyan {
background: linear-gradient(135deg, #06b6d4, #3b82f6);
color: white;
border: none;
}
.btn-neon-pink {
background: linear-gradient(135deg, #ec4899, #f43f5e);
color: white;
border: none;
}
.form-control, .form-select {
background-color: #1e293b;
border: 1px solid #334155;
color: #f8fafc;
min-height: 44px;
}
.form-control:focus, .form-select:focus {
background-color: #1e293b;
border-color: #8b5cf6;
color: #f8fafc;
box-shadow: 0 0 0 0.25rem rgba(139, 92, 246, 0.25);
}
.timeline-bar {
position: relative;
height: 36px;
background: #1e293b;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
user-select: none;
border: 1px solid #334155;
}
.timeline-range {
position: absolute;
top: 0;
bottom: 0;
background: rgba(139, 92, 246, 0.4);
border-left: 2px solid #a855f7;
border-right: 2px solid #a855f7;
}
.timeline-playhead {
position: absolute;
top: 0;
bottom: 0;
width: 3px;
background: #22c55e;
box-shadow: 0 0 8px #22c55e;
z-index: 5;
}
.seq-blocks-container {
display: flex;
gap: 4px;
overflow-x: auto;
padding: 6px 0;
scrollbar-width: thin;
}
.seq-block {
flex: 0 0 auto;
width: 42px;
height: 42px;
background: #334155;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: bold;
color: #cbd5e1;
border: 2px solid transparent;
transition: all 0.15s;
}
.seq-block.active {
border-color: #22c55e;
background: #7c3aed;
color: #fff;
transform: scale(1.05);
}
.slider-label {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: #94a3b8;
margin-bottom: 4px;
}
.preset-btn {
font-size: 0.8rem;
padding: 6px 12px;
border-radius: 20px;
background: #1e293b;
border: 1px solid #334155;
color: #cbd5e1;
white-space: nowrap;
}
.preset-btn:hover, .preset-btn.active {
background: #8b5cf6;
color: #fff;
border-color: #a855f7;
}
video#hidden-video {
display: none;
}
/* Responsive tweaks */
@media (max-width: 576px) {
#main-container { padding: 0.5rem; }
.app-card { padding: 0.75rem; }
.btn-text-hide { display: none; }
}
</style>
<script type="text/javascript">
try {
document.addEventListener("DOMContentLoaded", function() {
// App State Object
const state = {
video: document.getElementById('hidden-video'),
canvas: document.getElementById('main-canvas'),
ctx: document.getElementById('main-canvas').getContext('2d'),
sampleCanvas: document.createElement('canvas'),
sampleCtx: null,
sourceType: 'sample', // 'sample', 'file', 'cam'
isPlaying: true,
videoDuration: 3.0,
startTime: 0.5,
endTime: 2.0,
repeatCount: 3,
repeatsDone: 0,
mode: 'stutter', // 'loop', 'pingpong', 'stutter', 'chop8', 'random'
speed: 1.0,
fx: 'none', // 'none', 'glitch', 'rgbsplit', 'cyber', 'crt', 'duotone', 'pixel'
fxIntensity: 0.5,
subChops: 4,
sequenceSteps: [], // Array of {start, end, reverse}
currentStepIdx: 0,
stepStartTime: 0,
animReqId: null,
mediaRecorder: null,
recordedChunks: [],
isRecording: false,
audioCtx: null,
audioEnabled: false
};
state.sampleCtx = state.sampleCanvas.getContext('2d');
state.sampleCanvas.width = 640;
state.sampleCanvas.height = 360;
// Initialize Sample Generator
let sampleAngle = 0;
function drawSampleFrame(timeSec) {
const sCtx = state.sampleCtx;
const w = 640, h = 360;
sCtx.fillStyle = '#0f172a';
sCtx.fillRect(0, 0, w, h);
// Animated background grid
sCtx.strokeStyle = 'rgba(139, 92, 246, 0.2)';
sCtx.lineWidth = 1;
const gridSize = 40;
const offset = (timeSec * 60) % gridSize;
for(let x = 0; x < w; x += gridSize) {
sCtx.beginPath(); sCtx.moveTo(x, 0); sCtx.lineTo(x, h); sCtx.stroke();
}
for(let y = offset; y < h; y += gridSize) {
sCtx.beginPath(); sCtx.moveTo(0, y); sCtx.lineTo(w, y); sCtx.stroke();
}
// Bouncing / Rotating Neon Object
const cx = w / 2 + Math.sin(timeSec * 3) * 150;
const cy = h / 2 + Math.cos(timeSec * 4) * 80;
const radius = 45 + Math.sin(timeSec * 6) * 15;
sCtx.save();
sCtx.translate(cx, cy);
sCtx.rotate(timeSec * 2);
// Outer Glow Ring
sCtx.shadowBlur = 20;
sCtx.shadowColor = '#ec4899';
sCtx.fillStyle = '#f43f5e';
sCtx.beginPath();
sCtx.arc(0, 0, radius, 0, Math.PI * 2);
sCtx.fill();
// Inner Star / Polygon
sCtx.fillStyle = '#38bdf8';
sCtx.beginPath();
for(let i=0; i<5; i++) {
const a = (i * Math.PI * 2) / 5;
const rx = Math.cos(a) * (radius * 0.6);
const ry = Math.sin(a) * (radius * 0.6);
if(i===0) sCtx.moveTo(rx, ry);
else sCtx.lineTo(rx, ry);
}
sCtx.closePath();
sCtx.fill();
sCtx.restore();
// Big Time Text
sCtx.fillStyle = '#f8fafc';
sCtx.font = '900 36px sans-serif';
sCtx.textAlign = 'center';
sCtx.fillText(`SAMPLE VIDEO: ${timeSec.toFixed(2)}s`, w / 2, h - 30);
}
// Audio Feedback Synthesizer
function playBeep(freq = 440, duration = 0.05) {
if (!state.audioEnabled) return;
try {
if (!state.audioCtx) {
state.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (state.audioCtx.state === 'suspended') {
state.audioCtx.resume();
}
const osc = state.audioCtx.createOscillator();
const gain = state.audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(freq, state.audioCtx.currentTime);
gain.gain.setValueAtTime(0.08, state.audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, state.audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(state.audioCtx.destination);
osc.start();
osc.stop(state.audioCtx.currentTime + duration);
} catch(e) {}
}
// Build Sequence Table based on settings
function generateSequence() {
const start = Math.min(state.startTime, state.endTime);
const end = Math.max(state.startTime, state.endTime);
const duration = Math.max(0.1, end - start);
const repeats = parseInt(state.repeatCount) || 1;
const mode = state.mode;
let steps = [];
if (mode === 'loop') {
for(let i=0; i<repeats; i++) {
steps.push({ start, end, reverse: false, label: `Loop ${i+1}` });
}
} else if (mode === 'pingpong') {
for(let i=0; i<repeats; i++) {
steps.push({ start, end, reverse: false, label: `Fwd ${i+1}` });
steps.push({ start, end, reverse: true, label: `Rev ${i+1}` });
}
} else if (mode === 'stutter') {
// Divide clip into 4 chops, stutter each chop 2-3 times
const chops = 4;
const chopLen = duration / chops;
for(let c=0; c<chops; c++) {
const cStart = start + c * chopLen;
const cEnd = cStart + chopLen;
for(let r=0; r<repeats; r++) {
steps.push({ start: cStart, end: cEnd, reverse: false, label: `C${c+1}.${r+1}` });
}
}
} else if (mode === 'chop8') {
// Micro 8-beat glitch sequence
const chops = 8;
const chopLen = duration / chops;
const pattern = [0, 0, 1, 2, 2, 2, 4, 7];
pattern.forEach((p, idx) => {
const cStart = start + (p % chops) * chopLen;
const cEnd = cStart + chopLen;
steps.push({ start: cStart, end: cEnd, reverse: (idx % 3 === 0), label: `S${p+1}` });
});
} else if (mode === 'random') {
const chops = 6;
const chopLen = duration / chops;
for(let i=0; i<repeats * 3; i++) {
const randChop = Math.floor(Math.random() * chops);
const cStart = start + randChop * chopLen;
const cEnd = cStart + chopLen;
steps.push({ start: cStart, end: cEnd, reverse: Math.random() > 0.6, label: `R${randChop+1}` });
}
}
state.sequenceSteps = steps;
state.currentStepIdx = 0;
state.stepStartTime = performance.now();
renderSequenceVisualizer();
}
// Render Visual Timeline Blocks
function renderSequenceVisualizer() {
const container = $('#seq-blocks');
container.empty();
state.sequenceSteps.forEach((step, idx) => {
const block = $(`<div class="seq-block ${idx === state.currentStepIdx ? 'active' : ''}" id="seq-block-${idx}">${step.label}</div>`);
block.on('click', () => {
state.currentStepIdx = idx;
state.stepStartTime = performance.now();
updatePlayhead();
});
container.append(block);
});
}
function updatePlayhead() {
if (state.sequenceSteps.length === 0) return;
const currentStep = state.sequenceSteps[state.currentStepIdx];
if (!currentStep) return;
// Highlight block
$('.seq-block').removeClass('active');
$(`#seq-block-${state.currentStepIdx}`).addClass('active');
// Scroll block into view smoothly
const activeEl = document.getElementById(`seq-block-${state.currentStepIdx}`);
if (activeEl) {
activeEl.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}
// Update timeline playhead
const dur = state.videoDuration || 1;
const pct = (currentStep.start / dur) * 100;
$('#timeline-playhead').css('left', `${Math.min(100, Math.max(0, pct))}%`);
}
// Canvas Render Engine
let sampleVirtualTime = 0;
function renderLoop(timestamp) {
state.animReqId = requestAnimationFrame(renderLoop);
if (!state.isPlaying) return;
if (state.sequenceSteps.length === 0) {
generateSequence();
}
const currentStep = state.sequenceSteps[state.currentStepIdx];
if (!currentStep) return;
const stepDuration = Math.max(0.05, (currentStep.end - currentStep.start) / state.speed);
const elapsedSec = (timestamp - state.stepStartTime) / 1000;
let currentFrameTime = currentStep.start;
if (currentStep.reverse) {
currentFrameTime = currentStep.end - (elapsedSec * state.speed);
} else {
currentFrameTime = currentStep.start + (elapsedSec * state.speed);
}
// Step Transition Check
if (elapsedSec >= stepDuration) {
state.currentStepIdx = (state.currentStepIdx + 1) % state.sequenceSteps.length;
state.stepStartTime = timestamp;
playBeep(300 + (state.currentStepIdx * 40), 0.04);
updatePlayhead();
}
// Fetch frame source
const canvas = state.canvas;
const ctx = state.ctx;
const w = canvas.width;
const h = canvas.height;
ctx.clearRect(0, 0, w, h);
if (state.sourceType === 'sample') {
drawSampleFrame(currentFrameTime);
ctx.drawImage(state.sampleCanvas, 0, 0, w, h);
} else if (state.sourceType === 'file' || state.sourceType === 'cam') {
if (state.video.readyState >= 2) {
try {
// Seek video element to virtual step time
if (Math.abs(state.video.currentTime - currentFrameTime) > 0.08) {
state.video.currentTime = currentFrameTime;
}
} catch(e) {}
ctx.drawImage(state.video, 0, 0, w, h);
} else {
// Loading placeholder
ctx.fillStyle = '#1e293b';
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = '#94a3b8';
ctx.font = '16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Loading Video Stream...', w/2, h/2);
}
}
// Apply Visual FX Filters
applyCanvasFX(ctx, w, h);
}
// FX Processing
function applyCanvasFX(ctx, w, h) {
const fx = state.fx;
if (fx === 'none') return;
const intensity = state.fxIntensity;
if (fx === 'glitch') {
if (Math.random() < intensity * 0.7) {
const slices = Math.floor(3 + intensity * 10);
for(let i=0; i<slices; i++) {
const sy = Math.random() * h;
const sh = Math.random() * (h * 0.15) + 5;
const dx = (Math.random() - 0.5) * (40 * intensity);
ctx.drawImage(ctx.canvas, 0, sy, w, sh, dx, sy, w, sh);
}
}
} else if (fx === 'rgbsplit') {
const offset = Math.floor(intensity * 12) + 2;
ctx.save();
ctx.globalCompositeOperation = 'screen';
// Red offset
ctx.fillStyle = 'rgba(255,0,0,0.3)';
ctx.fillRect(offset, 0, w, h);
// Cyan offset
ctx.fillStyle = 'rgba(0,255,255,0.3)';
ctx.fillRect(-offset, 0, w, h);
ctx.restore();
} else if (fx === 'crt') {
// Scanlines
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
for(let y=0; y<h; y+=4) {
ctx.fillRect(0, y, w, 2);
}
// Vignette
const grad = ctx.createRadialGradient(w/2, h/2, w*0.3, w/2, h/2, w*0.7);
grad.addColorStop(0, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,0.6)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
} else if (fx === 'cyber') {
ctx.save();
ctx.globalCompositeOperation = 'color-burn';
ctx.fillStyle = 'rgba(124, 58, 237, 0.4)';
ctx.fillRect(0, 0, w, h);
ctx.restore();
} else if (fx === 'duotone') {
ctx.save();
ctx.globalCompositeOperation = 'multiply';
ctx.fillStyle = '#ec4899';
ctx.fillRect(0, 0, w, h);
ctx.restore();
}
}
// Timeline Visual UI Updates
function updateTimelineRangeUI() {
const dur = state.videoDuration || 1;
const startPct = (state.startTime / dur) * 100;
const endPct = (state.endTime / dur) * 100;
const widthPct = Math.max(2, endPct - startPct);
$('#timeline-range').css({
'left': `${startPct}%`,
'width': `${widthPct}%`
});
$('#lbl-start-time').text(`${state.startTime.toFixed(2)}s`);
$('#lbl-end-time').text(`${state.endTime.toFixed(2)}s`);
$('#range-start').val(state.startTime);
$('#range-end').val(state.endTime);
}
// Video Source Handlers
function loadVideoFile(file) {
if (!file) return;
const url = URL.createObjectURL(file);
state.sourceType = 'file';
state.video.src = url;
state.video.load();
state.video.onloadedmetadata = function() {
state.videoDuration = state.video.duration || 5.0;
state.startTime = 0;
state.endTime = Math.min(2.0, state.videoDuration);
setupRangeSliders();
generateSequence();
$('#source-badge').text('Custom Video File');
};
}
function enableWebcam() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('Webcam access is not supported on this device/browser.');
return;
}
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then(stream => {
state.sourceType = 'cam';
state.video.srcObject = stream;
state.video.play();
state.videoDuration = 5.0; // Simulated buffer length
state.startTime = 0;
state.endTime = 2.0;
setupRangeSliders();
generateSequence();
$('#source-badge').text('Live Camera Feed');
})
.catch(err => {
alert('Could not access camera: ' + err.message);
});
}
function setupRangeSliders() {
const maxDur = state.videoDuration || 5.0;
$('#range-start').attr('max', maxDur);
$('#range-end').attr('max', maxDur);
updateTimelineRangeUI();
}
// Event Listeners Configuration
$('#btn-play-pause').on('click', function() {
state.isPlaying = !state.isPlaying;
$(this).html(state.isPlaying ? '<i class="fa-solid fa-pause"></i><span class="btn-text-hide"> Pause</span>' : '<i class="fa-solid fa-play"></i><span class="btn-text-hide"> Play</span>');
});
$('#range-start').on('input', function() {
let val = parseFloat($(this).val());
if (val >= state.endTime) val = Math.max(0, state.endTime - 0.1);
state.startTime = val;
updateTimelineRangeUI();
generateSequence();
});
$('#range-end').on('input', function() {
let val = parseFloat($(this).val());
if (val <= state.startTime) val = Math.min(state.videoDuration, state.startTime + 0.1);
state.endTime = val;
updateTimelineRangeUI();
generateSequence();
});
$('#select-mode').on('change', function() {
state.mode = $(this).val();
generateSequence();
});
$('#select-repeats').on('change', function() {
state.repeatCount = parseInt($(this).val());
generateSequence();
});
$('#slider-speed').on('input', function() {
state.speed = parseFloat($(this).val());
$('#lbl-speed-val').text(`${state.speed.toFixed(2)}x`);
});
$('#select-fx').on('change', function() {
state.fx = $(this).val();
});
$('#slider-fx-intensity').on('input', function() {
state.fxIntensity = parseFloat($(this).val());
});
$('#file-input').on('change', function(e) {
if (e.target.files && e.target.files[0]) {
loadVideoFile(e.target.files[0]);
}
});
$('#btn-cam').on('click', function() {
enableWebcam();
});
$('#btn-audio-toggle').on('click', function() {
state.audioEnabled = !state.audioEnabled;
$(this).toggleClass('btn-outline-info btn-info');
if (state.audioEnabled) {
playBeep(600, 0.1);
}
});
// Preset buttons
$('.preset-btn').on('click', function() {
$('.preset-btn').removeClass('active');
$(this).addClass('active');
const preset = $(this).data('preset');
if (preset === 'boomerang') {
$('#select-mode').val('pingpong');
$('#select-repeats').val('3');
$('#slider-speed').val('1.2').trigger('input');
$('#select-fx').val('none');
} else if (preset === 'stutter-glitch') {
$('#select-mode').val('stutter');
$('#select-repeats').val('4');
$('#slider-speed').val('1.5').trigger('input');
$('#select-fx').val('glitch');
} else if (preset === 'cyber-beat') {
$('#select-mode').val('chop8');
$('#select-repeats').val('2');
$('#slider-speed').val('1.0').trigger('input');
$('#select-fx').val('cyber');
} else if (preset === 'chaos') {
$('#select-mode').val('random');
$('#select-repeats').val('5');
$('#slider-speed').val('2.0').trigger('input');
$('#select-fx').val('rgbsplit');
}
state.mode = $('#select-mode').val();
state.repeatCount = $('#select-repeats').val();
state.fx = $('#select-fx').val();
generateSequence();
});
// Snapshot functionality
$('#btn-snapshot').on('click', function() {
const link = document.createElement('a');
link.download = `loopmatrix-frame-${Date.now()}.png`;
link.href = state.canvas.toDataURL('image/png');
link.click();
});
// Video Recording Exporter
$('#btn-record').on('click', function() {
if (state.isRecording) {
stopRecording();
} else {
startRecording();
}
});
function startRecording() {
try {
const stream = state.canvas.captureStream(30);
state.recordedChunks = [];
state.mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
state.mediaRecorder.ondataavailable = function(e) {
if (e.data.size > 0) state.recordedChunks.push(e.data);
};
state.mediaRecorder.onstop = function() {
const blob = new Blob(state.recordedChunks, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
$('#download-video-btn').attr('href', url).attr('download', `repeated-sequence-${Date.now()}.webm`);
const modal = new bootstrap.Modal(document.getElementById('exportModal'));
modal.show();
};
state.mediaRecorder.start();
state.isRecording = true;
$('#btn-record').html('<i class="fa-solid fa-square"></i> Stop Rec').removeClass('btn-neon-pink').addClass('btn-danger');
} catch(err) {
alert('MediaRecorder is not supported or failed: ' + err.message);
}
}
function stopRecording() {
if (state.mediaRecorder && state.isRecording) {
state.mediaRecorder.stop();
state.isRecording = false;
$('#btn-record').html('<i class="fa-solid fa-circle"></i> Record WebM').removeClass('btn-danger').addClass('btn-neon-pink');
}
}
// Start default app loop
setupRangeSliders();
generateSequence();
requestAnimationFrame(renderLoop);
});
} catch (error) {
console.error("App Initialization Error:", error);
}
</script>
</head>
<body>
<div id="main-container">
<!-- Header Bar -->
<header class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-2">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<i class="fa-solid fa-repeat fs-5"></i>
</div>
<div>
<h1 class="h5 m-0 fw-bold bg-gradient text-white">LoopMatrix</h1>
<small class="text-secondary" style="font-size: 0.75rem;">Video to Repeated Sequence & Stutter FX</small>
</div>
</div>
<div>
<button class="btn btn-sm btn-outline-light control-btn" id="btn-audio-toggle" title="Toggle Beat Audio Beep">
<i class="fa-solid fa-volume-high"></i>
</button>
</div>
</header>
<!-- Video Player Canvas Viewport -->
<div class="app-card p-2 p-md-3">
<div class="canvas-wrapper mb-2">
<span class="badge-live"><div class="pulse-dot"></div> <span id="source-badge">Interactive Sample</span></span>
<canvas id="main-canvas" width="640" height="360"></canvas>
</div>
<!-- Timeline Bar & Scrubbing -->
<div class="timeline-bar mb-2" id="timeline-container">
<div class="timeline-range" id="timeline-range"></div>
<div class="timeline-playhead" id="timeline-playhead"></div>
</div>
<!-- Quick Action Bar -->
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
<div class="d-flex align-items-center gap-2">
<button class="btn btn-neon-purple control-btn" id="btn-play-pause">
<i class="fa-solid fa-pause"></i><span class="btn-text-hide"> Pause</span>
</button>
<button class="btn btn-neon-pink control-btn" id="btn-record">
<i class="fa-solid fa-circle"></i> Record WebM
</button>
</div>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-outline-light control-btn" id="btn-snapshot" title="Take Frame Snapshot">
<i class="fa-solid fa-camera"></i>
</button>
<label class="btn btn-neon-cyan control-btn mb-0" for="file-input">
<i class="fa-solid fa-upload"></i><span class="btn-text-hide"> Open Video</span>
</label>
<input type="file" id="file-input" accept="video/*" class="d-none">
<button class="btn btn-outline-secondary control-btn" id="btn-cam" title="Use Live WebCam">
<i class="fa-solid fa-video"></i>
</button>
</div>
</div>
</div>
<!-- Sequence Blocks Display -->
<div class="app-card">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold text-light" style="font-size: 0.85rem;"><i class="fa-solid fa-layer-group text-primary me-1"></i> Sequence Chops Timeline</span>
<small class="text-muted" style="font-size: 0.75rem;">Click block to jump</small>
</div>
<div class="seq-blocks-container" id="seq-blocks">
<!-- Sequence Step Pills injected by JS -->
</div>
</div>
<!-- Controls Grid -->
<div class="row g-2">
<!-- Trim Range & Speed -->
<div class="col-12 col-md-6">
<div class="app-card h-100">
<h6 class="fw-bold text-primary mb-2"><i class="fa-solid fa-sliders me-1"></i> Trim Range & Speed</h6>
<div class="mb-2">
<div class="slider-label">
<span>Start Cut: <strong id="lbl-start-time">0.50s</strong></span>
<span>End Cut: <strong id="lbl-end-time">2.00s</strong></span>
</div>
<div class="d-flex gap-2">
<input type="range" class="form-range" id="range-start" min="0" max="5" step="0.05" value="0.5">
<input type="range" class="form-range" id="range-end" min="0" max="5" step="0.05" value="2.0">
</div>
</div>
<div>
<div class="slider-label">
<span>Playback Speed</span>
<span id="lbl-speed-val" class="fw-bold text-info">1.00x</span>
</div>
<input type="range" class="form-range" id="slider-speed" min="0.25" max="3.0" step="0.25" value="1.0">
</div>
</div>
</div>
<!-- Repeater Engine Settings -->
<div class="col-12 col-md-6">
<div class="app-card h-100">
<h6 class="fw-bold text-purple mb-2"><i class="fa-solid fa-gear me-1"></i> Repeat Engine & Pattern</h6>
<div class="row g-2 mb-2">
<div class="col-6">
<label class="form-label small text-secondary mb-1">Repeat Mode</label>
<select class="form-select form-select-sm" id="select-mode">
<option value="stutter" selected>Stutter Chops (4x)</option>
<option value="pingpong">Boomerang (Ping-Pong)</option>
<option value="loop">Standard Loop</option>
<option value="chop8">8-Bit Glitch Pattern</option>
<option value="random">Chaos Randomizer</option>
</select>
</div>
<div class="col-6">
<label class="form-label small text-secondary mb-1">Repeats Count</label>
<select class="form-select form-select-sm" id="select-repeats">
<option value="1">1 Repeat</option>
<option value="2">2 Repeats</option>
<option value="3" selected>3 Repeats</option>
<option value="4">4 Repeats</option>
<option value="6">6 Repeats</option>
</select>
</div>
</div>
<!-- Presets Bar -->
<div class="d-flex flex-wrap gap-1 mt-2">
<span class="preset-btn active" data-preset="stutter-glitch">⚡ Stutter Glitch</span>
<span class="preset-btn" data-preset="boomerang">🪃 Boomerang</span>
<span class="preset-btn" data-preset="cyber-beat">🌐 Cyber Beat</span>
<span class="preset-btn" data-preset="chaos">🎲 Chaos</span>
</div>
</div>
</div>
<!-- Visual FX Engine -->
<div class="col-12">
<div class="app-card">
<div class="row g-2 align-items-center">
<div class="col-12 col-sm-4">
<label class="form-label small text-secondary mb-1"><i class="fa-solid fa-wand-magic-sparkles me-1"></i> Visual FX Filter</label>
<select class="form-select form-select-sm" id="select-fx">
<option value="none" selected>None (Clean)</option>
<option value="glitch">Digital Glitch Slices</option>
<option value="rgbsplit">RGB Split Offset</option>
<option value="crt">Retro CRT Scanlines</option>
<option value="cyber">Cyber Neon Wash</option>
<option value="duotone">Pink Duotone</option>
</select>
</div>
<div class="col-12 col-sm-8">
<div class="slider-label">
<span>FX Distortion Intensity</span>
</div>
<input type="range" class="form-range" id="slider-fx-intensity" min="0.1" max="1.0" step="0.1" value="0.5">
</div>
</div>
</div>
</div>
</div>
<!-- Hidden Video Element for Video Decoding -->
<video id="hidden-video" playsinline loop muted></video>
<!-- Modal for Download/Export -->
<div class="modal fade" id="exportModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark text-light border-secondary">
<div class="modal-header border-secondary">
<h5 class="modal-title"><i class="fa-solid fa-file-video text-success me-2"></i> Sequence Video Ready!</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<p class="text-secondary">Your repeated sequence video loop has been rendered.</p>
<a id="download-video-btn" href="#" class="btn btn-neon-purple w-100 py-2 fs-6">
<i class="fa-solid fa-download me-2"></i> Download WebM Video
</a>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
NEW APPS
These are apps made by the community!