213c801b29
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
986 lines
33 KiB
HTML
986 lines
33 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>Motif - Procedural Music Synthesis</title>
|
||
|
||
<!-- OpenGraph / Social Media Preview -->
|
||
<meta property="og:title" content="MOTIF">
|
||
<meta property="og:description" content="Procedural Music Synthesis from MIDI">
|
||
<meta property="og:image" content="/chiptune_blog_piece_1000x.webp">
|
||
<meta property="og:image:width" content="1200">
|
||
<meta property="og:image:height" content="630">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:url" content="https://motif-self.vercel.app">
|
||
|
||
<!-- Twitter Card -->
|
||
<meta name="twitter:card" content="summary_large_image">
|
||
<meta name="twitter:title" content="MOTIF">
|
||
<meta name="twitter:description" content="Procedural Music Synthesis from MIDI">
|
||
<meta name="twitter:image" content="/chiptune_blog_piece_1000x.webp">
|
||
|
||
<!-- General Meta -->
|
||
<meta name="description" content="Transform MIDI files into retro synthesized variations. Search for songs, preview them, and generate new procedural versions with retro game console vibes.">
|
||
<style>
|
||
/* Design System: Synthwave - 8px grid, consistent neon accents */
|
||
:root {
|
||
--color-bg: #0a0a0a;
|
||
--color-surface: #141414;
|
||
--color-surface-elevated: #1a1a1a;
|
||
--color-border: #2a2a2a;
|
||
--color-text: #e8e8e8;
|
||
--color-text-dim: #999;
|
||
--color-accent-primary: #00ff88;
|
||
--color-accent-secondary: #ffaa44;
|
||
--spacing-unit: 8px;
|
||
--radius: 8px;
|
||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.6);
|
||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.7);
|
||
--glow-primary: 0 0 20px rgba(0, 255, 136, 0.3);
|
||
--glow-secondary: 0 0 20px rgba(255, 170, 68, 0.3);
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||
background:
|
||
radial-gradient(1000px 600px at 50% -150px, rgba(0, 255, 136, 0.12), transparent 60%),
|
||
radial-gradient(800px 600px at 110% 20%, rgba(255, 170, 68, 0.08), transparent 55%),
|
||
var(--color-bg);
|
||
color: var(--color-text);
|
||
margin: 0;
|
||
padding: calc(var(--spacing-unit) * 3);
|
||
line-height: 1.6;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
.container {
|
||
max-width: 960px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
h1 {
|
||
color: var(--color-accent-primary);
|
||
text-align: center;
|
||
margin: 0 0 calc(var(--spacing-unit) * 4) 0;
|
||
font-size: 2em;
|
||
font-weight: 700;
|
||
letter-spacing: 0.05em;
|
||
text-shadow: var(--glow-primary);
|
||
}
|
||
|
||
.topbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: calc(var(--spacing-unit) * 2);
|
||
margin-bottom: calc(var(--spacing-unit) * 2);
|
||
}
|
||
.topbar h1 {
|
||
margin: 0;
|
||
}
|
||
.faq-link {
|
||
margin-left: auto;
|
||
background: transparent;
|
||
border: 1px solid rgba(255,255,255,0.14);
|
||
color: rgba(255,255,255,0.85);
|
||
padding: 8px 10px;
|
||
border-radius: var(--radius);
|
||
box-shadow: none;
|
||
font-size: 13px;
|
||
}
|
||
.faq-link:hover:not(:disabled) {
|
||
box-shadow: var(--shadow-sm);
|
||
border-color: rgba(255,255,255,0.22);
|
||
}
|
||
|
||
/* FAQ modal */
|
||
.modal-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.65);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
z-index: 1000;
|
||
}
|
||
.modal-backdrop.open { display: flex; }
|
||
.modal {
|
||
width: min(720px, 100%);
|
||
background: rgba(20,20,20,0.98);
|
||
border: 1px solid rgba(255,255,255,0.12);
|
||
border-radius: 12px;
|
||
box-shadow: 0 30px 120px rgba(0,0,0,0.75);
|
||
overflow: hidden;
|
||
}
|
||
.modal header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||
}
|
||
.modal header h3 {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: rgba(255,255,255,0.9);
|
||
}
|
||
.modal header .faq-header-links {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.modal header .faq-header-links a {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(255,255,255,0.12);
|
||
background: rgba(0,0,0,0.18);
|
||
color: rgba(255,255,255,0.82);
|
||
text-decoration: none;
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
box-shadow: none;
|
||
transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
||
}
|
||
.modal header .faq-header-links a:hover {
|
||
border-color: rgba(255,255,255,0.22);
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.45);
|
||
background: rgba(0,0,0,0.28);
|
||
}
|
||
.modal header .faq-header-links a:focus-visible {
|
||
outline: none;
|
||
box-shadow: 0 0 0 3px rgba(0,255,136,0.25), 0 12px 34px rgba(0,0,0,0.55);
|
||
border-color: rgba(0,255,136,0.45);
|
||
}
|
||
.modal header .faq-header-links .dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 999px;
|
||
background: rgba(0,255,136,0.85);
|
||
box-shadow: 0 0 14px rgba(0,255,136,0.22);
|
||
flex: 0 0 auto;
|
||
}
|
||
.modal header .faq-header-links .label {
|
||
display: inline-flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
white-space: nowrap;
|
||
}
|
||
.modal header .faq-header-links .handle {
|
||
color: rgba(0,255,136,0.9);
|
||
font-weight: 600;
|
||
}
|
||
@media (max-width: 480px) {
|
||
.modal header {
|
||
gap: 10px;
|
||
padding: 12px 12px;
|
||
}
|
||
.modal header .faq-header-links a {
|
||
padding: 6px 9px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
.modal header .spacer { margin-left: auto; }
|
||
.modal header button {
|
||
padding: 8px 10px;
|
||
border-radius: 10px;
|
||
background: rgba(255,255,255,0.06);
|
||
border: 1px solid rgba(255,255,255,0.12);
|
||
box-shadow: none;
|
||
}
|
||
.modal main {
|
||
padding: 14px 16px 16px 16px;
|
||
}
|
||
.faq-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 12px;
|
||
}
|
||
.faq-item {
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
border-radius: 12px;
|
||
padding: 12px 12px;
|
||
background: rgba(0,0,0,0.18);
|
||
}
|
||
.faq-item h4 {
|
||
margin: 0 0 6px 0;
|
||
font-size: 13px;
|
||
color: rgba(0,255,136,0.9);
|
||
}
|
||
.faq-item p {
|
||
margin: 0;
|
||
color: rgba(255,255,255,0.75);
|
||
font-size: 13px;
|
||
line-height: 1.45;
|
||
}
|
||
.faq-item code {
|
||
font-size: 12px;
|
||
color: rgba(255,255,255,0.85);
|
||
}
|
||
.controls {
|
||
margin: calc(var(--spacing-unit) * 3) 0;
|
||
text-align: center;
|
||
}
|
||
|
||
button {
|
||
background: var(--color-surface);
|
||
color: var(--color-text);
|
||
border: 1px solid var(--color-border);
|
||
padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3);
|
||
border-radius: var(--radius);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
button:hover:not(:disabled) {
|
||
background: var(--color-surface-elevated);
|
||
transform: translateY(-1px);
|
||
border-color: #444;
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
button:active:not(:disabled) {
|
||
transform: translateY(0);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
button:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
button.primary {
|
||
background: var(--color-accent-primary);
|
||
color: #000;
|
||
border-color: var(--color-accent-primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
button.primary:hover:not(:disabled) {
|
||
background: #00dd77;
|
||
box-shadow: var(--glow-primary), var(--shadow-md);
|
||
}
|
||
|
||
#status {
|
||
margin: calc(var(--spacing-unit) * 3) 0;
|
||
padding: calc(var(--spacing-unit) * 2);
|
||
background: var(--color-surface);
|
||
border-left: 3px solid var(--color-accent-primary);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow-md);
|
||
font-size: 14px;
|
||
min-height: 24px;
|
||
}
|
||
|
||
input[type="text"] {
|
||
background: var(--color-surface);
|
||
color: var(--color-text);
|
||
border: 1px solid var(--color-border);
|
||
padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 2);
|
||
border-radius: var(--radius);
|
||
font-family: inherit;
|
||
font-size: 16px;
|
||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||
}
|
||
|
||
input[type="text"]:focus {
|
||
outline: none;
|
||
border-color: var(--color-accent-primary);
|
||
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
|
||
}
|
||
|
||
.results-section {
|
||
margin: 30px 0;
|
||
display: none;
|
||
}
|
||
.results-section.visible {
|
||
display: block;
|
||
}
|
||
|
||
.results-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: calc(var(--spacing-unit) * 2) 0;
|
||
background: var(--color-surface);
|
||
border-radius: var(--radius);
|
||
overflow: hidden;
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.results-table th,
|
||
.results-table td {
|
||
padding: calc(var(--spacing-unit) * 2);
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--color-border);
|
||
}
|
||
|
||
.results-table th {
|
||
background: var(--color-surface-elevated);
|
||
color: var(--color-accent-primary);
|
||
font-weight: 600;
|
||
font-size: 12px;
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.results-table .source-col {
|
||
width: 100px;
|
||
text-align: center;
|
||
}
|
||
|
||
.results-table .duration-col {
|
||
width: 80px;
|
||
text-align: center;
|
||
}
|
||
|
||
.results-table tbody tr {
|
||
cursor: pointer;
|
||
transition: background 150ms ease;
|
||
}
|
||
|
||
.results-table tbody tr:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.results-table tbody tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.results-table tr.selected {
|
||
background: rgba(0, 255, 136, 0.12);
|
||
}
|
||
|
||
.results-table tr.selected td:first-child {
|
||
border-left: 4px solid var(--color-accent-primary);
|
||
padding-left: calc(var(--spacing-unit) * 2 - 4px);
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.results-table th,
|
||
.results-table td {
|
||
padding: calc(var(--spacing-unit) * 1.5);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.results-table th {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.results-table .source-col {
|
||
display: none;
|
||
}
|
||
|
||
.results-table .duration-col {
|
||
width: 60px;
|
||
}
|
||
}
|
||
|
||
.confidence-bar {
|
||
width: 60px;
|
||
height: 4px;
|
||
background: #333;
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
.confidence-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #ff4444, #ffaa44, #00ff88);
|
||
transition: width 0.2s;
|
||
}
|
||
|
||
.player-section {
|
||
margin: 30px 0;
|
||
display: none;
|
||
}
|
||
.player-section.visible {
|
||
display: block;
|
||
}
|
||
|
||
.player-controls {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 20px;
|
||
margin: 20px 0;
|
||
}
|
||
.player-box {
|
||
background: var(--color-surface);
|
||
padding: calc(var(--spacing-unit) * 3);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow-md);
|
||
transition: transform 150ms ease, box-shadow 150ms ease;
|
||
}
|
||
|
||
.player-box:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: var(--shadow-lg);
|
||
}
|
||
|
||
.player-box h4 {
|
||
margin: 0 0 calc(var(--spacing-unit)) 0;
|
||
color: var(--color-accent-primary);
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.player-box p {
|
||
font-size: 13px;
|
||
color: var(--color-text-dim);
|
||
margin: 0 0 calc(var(--spacing-unit) * 2) 0;
|
||
}
|
||
|
||
.player-box button {
|
||
width: 100%;
|
||
margin: calc(var(--spacing-unit)) 0 0 0;
|
||
}
|
||
|
||
.player-box button:first-of-type {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.player-info {
|
||
background: var(--color-surface);
|
||
padding: calc(var(--spacing-unit) * 3);
|
||
margin: calc(var(--spacing-unit) * 3) 0;
|
||
border-left: 3px solid var(--color-accent-primary);
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
|
||
.player-info h4 {
|
||
margin: 0 0 calc(var(--spacing-unit)) 0;
|
||
color: var(--color-text);
|
||
font-size: 16px;
|
||
}
|
||
|
||
.player-info p {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
color: var(--color-text-dim);
|
||
}
|
||
|
||
.section {
|
||
margin-top: calc(var(--spacing-unit) * 5);
|
||
}
|
||
|
||
.section-header {
|
||
margin: 0 0 calc(var(--spacing-unit) * 2) 0;
|
||
}
|
||
|
||
.section-title {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
.section-subtitle {
|
||
margin: calc(var(--spacing-unit) / 2) 0 0 0;
|
||
font-size: 13px;
|
||
color: var(--color-text-dim);
|
||
}
|
||
|
||
.divider {
|
||
height: 1px;
|
||
margin: 0 0 calc(var(--spacing-unit) * 3) 0;
|
||
background: linear-gradient(90deg, transparent, var(--color-border), transparent);
|
||
}
|
||
|
||
.preview-controls {
|
||
display: flex;
|
||
gap: calc(var(--spacing-unit) * 1.5);
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
padding: calc(var(--spacing-unit) * 3);
|
||
background: var(--color-surface);
|
||
border-radius: var(--radius);
|
||
border: 1px solid var(--color-border);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.preview-controls button.preview-play {
|
||
flex: 1;
|
||
min-width: 160px;
|
||
}
|
||
|
||
.preview-controls button.next-result {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.preview-controls .volume-control {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
margin-top: 0;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.preview-controls {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.preview-controls button.next-result {
|
||
margin-left: 0;
|
||
}
|
||
}
|
||
|
||
.motif-stage {
|
||
background: var(--color-surface-elevated);
|
||
padding: calc(var(--spacing-unit) * 3);
|
||
border-radius: var(--radius);
|
||
border: 1px solid rgba(255, 170, 68, 0.2);
|
||
box-shadow: var(--shadow-lg), var(--glow-secondary);
|
||
}
|
||
.motif-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
.motif-header {
|
||
margin-bottom: 6px;
|
||
}
|
||
.motif-controls {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.motif-controls button {
|
||
flex-shrink: 0;
|
||
}
|
||
.motif-controls button.primary {
|
||
flex: 1;
|
||
min-width: 180px;
|
||
padding: 12px 16px;
|
||
font-weight: 700;
|
||
}
|
||
.motif-controls button.primary:hover {
|
||
box-shadow: 0 16px 60px rgba(255, 170, 68, 0.15);
|
||
border-color: rgba(255, 170, 68, 0.55);
|
||
}
|
||
.motif-controls .volume-control {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
margin-top: 0;
|
||
}
|
||
.motif-controls .volume-control label {
|
||
font-size: 0.85em;
|
||
min-width: 55px;
|
||
}
|
||
|
||
/* iOS audio unlock (Motif) */
|
||
.ios-audio-banner {
|
||
margin-top: calc(var(--spacing-unit) * 2);
|
||
padding: calc(var(--spacing-unit) * 2);
|
||
border-radius: var(--radius);
|
||
border: 1px solid rgba(255, 170, 68, 0.22);
|
||
background: rgba(0, 0, 0, 0.25);
|
||
box-shadow: var(--shadow-sm);
|
||
display: none; /* toggled by JS */
|
||
}
|
||
.ios-audio-banner .row {
|
||
display: flex;
|
||
gap: calc(var(--spacing-unit) * 1.5);
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.ios-audio-banner .copy {
|
||
font-size: 12px;
|
||
color: var(--color-text-dim);
|
||
line-height: 1.35;
|
||
flex: 1;
|
||
min-width: 220px;
|
||
}
|
||
.ios-audio-banner .copy strong {
|
||
color: var(--color-text);
|
||
font-weight: 600;
|
||
}
|
||
.ios-audio-banner .state {
|
||
margin-top: calc(var(--spacing-unit) * 1);
|
||
font-size: 12px;
|
||
color: rgba(255, 170, 68, 0.85);
|
||
display: none;
|
||
}
|
||
.ios-audio-banner button {
|
||
padding: calc(var(--spacing-unit) * 1.25) calc(var(--spacing-unit) * 2);
|
||
border-radius: var(--radius);
|
||
border: 1px solid rgba(255, 170, 68, 0.35);
|
||
background: rgba(255, 170, 68, 0.12);
|
||
color: var(--color-text);
|
||
box-shadow: var(--shadow-sm);
|
||
font-weight: 600;
|
||
}
|
||
.ios-audio-banner button:hover:not(:disabled) {
|
||
border-color: rgba(255, 170, 68, 0.55);
|
||
box-shadow: var(--glow-secondary), var(--shadow-md);
|
||
}
|
||
.motif-kicker {
|
||
font-size: 0.85em;
|
||
color: #b6b6b6;
|
||
margin: 0 0 10px 0;
|
||
max-width: 70ch;
|
||
}
|
||
.try-next {
|
||
margin-top: 14px;
|
||
width: 100%;
|
||
}
|
||
.try-next button {
|
||
width: 100%;
|
||
}
|
||
|
||
.volume-control {
|
||
margin-top: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
position: relative;
|
||
}
|
||
.volume-control label {
|
||
font-size: 0.9em;
|
||
color: #aaa;
|
||
}
|
||
.volume-control input[type="range"] {
|
||
flex: 1;
|
||
margin: 0;
|
||
}
|
||
|
||
.motif-progress-container {
|
||
margin-top: 8px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid rgba(255, 170, 68, 0.15);
|
||
}
|
||
.progress-time {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
font-size: 0.85em;
|
||
color: #8f8f8f;
|
||
}
|
||
.progress-bar-wrapper {
|
||
position: relative;
|
||
height: 44px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: calc(var(--spacing-unit) * 2) 0;
|
||
}
|
||
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 6px;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
background: transparent;
|
||
border-radius: 3px;
|
||
outline: none;
|
||
cursor: pointer;
|
||
position: relative;
|
||
z-index: 2;
|
||
}
|
||
|
||
.progress-bar::-webkit-slider-runnable-track {
|
||
height: 6px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.progress-bar::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: var(--color-accent-secondary);
|
||
cursor: pointer;
|
||
box-shadow: 0 0 8px rgba(255, 170, 68, 0.6), var(--shadow-sm);
|
||
margin-top: -7px;
|
||
transition: transform 100ms ease;
|
||
}
|
||
|
||
.progress-bar:active::-webkit-slider-thumb {
|
||
transform: scale(1.15);
|
||
}
|
||
|
||
.progress-bar::-moz-range-track {
|
||
height: 6px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.progress-bar::-moz-range-thumb {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
background: var(--color-accent-secondary);
|
||
cursor: pointer;
|
||
border: none;
|
||
box-shadow: 0 0 8px rgba(255, 170, 68, 0.6), var(--shadow-sm);
|
||
}
|
||
|
||
.progress-fill {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
height: 6px;
|
||
background: linear-gradient(90deg, var(--color-accent-secondary), rgba(255, 170, 68, 0.5));
|
||
border-radius: 3px;
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
width: 0%;
|
||
transition: width 0.1s linear;
|
||
}
|
||
|
||
/* Use on your website */
|
||
.embed-card {
|
||
background: var(--color-surface);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: var(--radius);
|
||
padding: calc(var(--spacing-unit) * 3);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
.embed-row {
|
||
display: flex;
|
||
gap: calc(var(--spacing-unit) * 1.5);
|
||
align-items: flex-start;
|
||
flex-wrap: wrap;
|
||
}
|
||
.embed-row button {
|
||
flex-shrink: 0;
|
||
}
|
||
.codeblock {
|
||
flex: 1;
|
||
min-width: 260px;
|
||
margin: 0;
|
||
padding: calc(var(--spacing-unit) * 2);
|
||
border-radius: calc(var(--radius) + 2px);
|
||
background: rgba(0,0,0,0.35);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
overflow: auto;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
.codeblock code {
|
||
font-size: 12px;
|
||
color: rgba(255,255,255,0.85);
|
||
white-space: pre;
|
||
}
|
||
.embed-hint {
|
||
margin-top: calc(var(--spacing-unit) * 1.5);
|
||
color: var(--color-text-dim);
|
||
font-size: 12px;
|
||
}
|
||
.embed-hint strong {
|
||
color: var(--color-text);
|
||
font-weight: 600;
|
||
}
|
||
.copy-toast {
|
||
margin-left: auto;
|
||
font-size: 12px;
|
||
color: rgba(0,255,136,0.85);
|
||
display: none;
|
||
}
|
||
|
||
.engine-selector {
|
||
margin-top: 10px;
|
||
font-size: 0.9em;
|
||
}
|
||
.engine-selector label {
|
||
color: #aaa;
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
}
|
||
.engine-selector select {
|
||
background: #333;
|
||
color: #e0e0e0;
|
||
border: 1px solid #555;
|
||
padding: 5px 10px;
|
||
font-family: inherit;
|
||
width: 100%;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="topbar">
|
||
<h1>🎵 MOTIF</h1>
|
||
<button id="faqBtn" class="faq-link" type="button">FAQ</button>
|
||
</div>
|
||
<p>Procedural Music Synthesis from MIDI Structure</p>
|
||
|
||
<div class="controls">
|
||
<input type="text" id="songInput" placeholder="Enter song name..." />
|
||
<button id="searchBtn">Search</button>
|
||
</div>
|
||
|
||
<div id="status">Ready. Enter a song name to search for MIDI files.</div>
|
||
|
||
<div id="resultsSection" class="results-section">
|
||
<h3>Search Results</h3>
|
||
<table id="resultsTable" class="results-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Title</th>
|
||
<th class="source-col">Source</th>
|
||
<th class="duration-col">Duration</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="resultsBody">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="playerSection" class="player-section">
|
||
<div id="selectedInfo" class="player-info">
|
||
<h4 id="selectedTitle">No MIDI selected</h4>
|
||
<p id="selectedMeta">Select a MIDI file from search results to enable playback</p>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h3 class="section-title">Preview</h3>
|
||
<p class="section-subtitle">Review the MIDI before generation.</p>
|
||
</div>
|
||
<div class="divider"></div>
|
||
|
||
<div class="preview-controls">
|
||
<button id="soundfontPlayBtn" class="preview-play" disabled>Play Preview</button>
|
||
<button id="soundfontStopBtn" disabled>Stop</button>
|
||
<div class="volume-control">
|
||
<label for="soundfontVolume">Volume</label>
|
||
<input type="range" id="soundfontVolume" min="0" max="1" step="0.01" value="0.8" />
|
||
</div>
|
||
<button id="nextResultBtn" class="next-result" disabled>Try Next Result</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<h3 class="section-title">Synthesis Engine</h3>
|
||
<p class="section-subtitle">Run it through the Gameboy emulator.</p>
|
||
</div>
|
||
<div class="divider"></div>
|
||
|
||
<div class="motif-stage">
|
||
<div class="motif-grid">
|
||
<div class="motif-header">
|
||
<h4 style="margin:0; color:#ffaa44; text-shadow: 0 0 18px rgba(255,170,68,0.15);">Run the engine</h4>
|
||
<p class="motif-kicker">Gameboy MIDI synthesis. Triangle, square, and sawtooth waveforms. Polyphonic 8-bit chiptune generation.</p>
|
||
</div>
|
||
|
||
<div class="motif-controls">
|
||
<button id="motifBtn" class="primary" disabled>Generate & Play</button>
|
||
<button id="motifStopBtn" disabled>Stop</button>
|
||
<div class="volume-control">
|
||
<label for="motifVolume">Volume</label>
|
||
<input type="range" id="motifVolume" min="0" max="1" step="0.01" value="0.3" />
|
||
</div>
|
||
</div>
|
||
|
||
<div id="iosAudioBanner" class="ios-audio-banner" aria-live="polite">
|
||
<div class="row">
|
||
<div class="copy">
|
||
<strong>iOS Safari:</strong> tap once to enable audio, then press <strong>Generate & Play</strong>.
|
||
</div>
|
||
<button id="enableAudioBtn" type="button">Enable Audio</button>
|
||
</div>
|
||
<div id="iosAudioState" class="state">Audio: …</div>
|
||
</div>
|
||
|
||
<div class="motif-progress-container" id="motifProgressContainer" style="display: none;">
|
||
<div class="progress-time">
|
||
<span id="motifCurrentTime">0:00</span>
|
||
<span id="motifDuration">0:00</span>
|
||
</div>
|
||
<div class="progress-bar-wrapper">
|
||
<input type="range" id="motifProgressBar" class="progress-bar" min="0" max="100" value="0" step="0.1" />
|
||
<div class="progress-fill" id="motifProgressFill"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section" id="embedSection" data-not-live="true" style="display:none;">
|
||
<div class="section-header">
|
||
<h3 class="section-title">Use on your website <span style="color: rgba(255,255,255,0.5); font-weight: 500;">(Coming soon)</span></h3>
|
||
<p class="section-subtitle">Embeddable widget + multiple synth engines.</p>
|
||
</div>
|
||
<div class="divider"></div>
|
||
|
||
<div class="embed-card" style="opacity:0.55; filter: grayscale(20%); position:relative;">
|
||
<div style="position:absolute; inset:0; background: linear-gradient(90deg, rgba(0,0,0,0.35), rgba(0,0,0,0)); pointer-events:none;"></div>
|
||
<div class="embed-row">
|
||
<pre class="codeblock"><code id="embedCode"><iframe src="..."></iframe></code></pre>
|
||
<div style="display:flex; flex-direction:column; gap: calc(var(--spacing-unit) * 1.5);">
|
||
<button id="copyEmbedBtn" class="primary" type="button" disabled>Copy embed</button>
|
||
<span id="copyToast" class="copy-toast">Copied</span>
|
||
</div>
|
||
</div>
|
||
<div class="embed-hint">
|
||
<strong>Note:</strong> This is not live yet. We’ll ship a dedicated <code>/embed</code> widget + style presets next.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="faqModalBackdrop" class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="faqTitle">
|
||
<div class="modal" role="document">
|
||
<header>
|
||
<h3 id="faqTitle">FAQ</h3>
|
||
<nav class="faq-header-links" aria-label="FAQ links">
|
||
<a href="https://x.com/b1rdmania" target="_blank" rel="noopener noreferrer" aria-label="Open @b1rdmania on X">
|
||
<span class="dot" aria-hidden="true"></span>
|
||
<span class="label"><span class="handle">@b1rdmania</span><span aria-hidden="true">X</span></span>
|
||
</a>
|
||
<a href="https://github.com/b1rdmania/motif" target="_blank" rel="noopener noreferrer" aria-label="Open the MOTIF GitHub repository">
|
||
<span class="dot" aria-hidden="true"></span>
|
||
<span class="label">GitHub</span>
|
||
</a>
|
||
</nav>
|
||
<div class="spacer"></div>
|
||
<button id="faqCloseBtn" type="button">Close</button>
|
||
</header>
|
||
<main>
|
||
<div class="faq-grid">
|
||
<div class="faq-item">
|
||
<h4>What is MOTIF?</h4>
|
||
<p>Turn any song into retro <strong>Gameboy-style</strong> synth. Search a track, preview the MIDI, hit Generate.</p>
|
||
</div>
|
||
<div class="faq-item">
|
||
<h4>How does it work?</h4>
|
||
<p>We scrape MIDI files from the web, analyse the structure (melody, bass, chords), then resynthesise with an emulation engine. Light touch and runs in any browser through <strong>Web Audio</strong>.</p>
|
||
</div>
|
||
<div class="faq-item">
|
||
<h4>Why "Enable Audio" on iPhone?</h4>
|
||
<p>Apple blocks sound until you tap something. Just hit the button once — you're good. Still silent? Check your ringer switch isn't on mute.</p>
|
||
</div>
|
||
<div class="faq-item">
|
||
<h4>What's next?</h4>
|
||
<p>More sources, better song matching, style presets (ambient / ominous / dance), embeddable widget, maybe an API. Ideas? Hit us up.</p>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module" src="/src/main.ts"></script>
|
||
</body>
|
||
</html> |