diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3deea73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Dependencies +node_modules/ +npm-debug.log +yarn-error.log +package-lock.json +yarn.lock + +# Build outputs +dist/ +build/ +*.min.js +*.min.css + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Temporary files +tmp/ +temp/ +*.tmp + +# Environment +.env +.env.local + +# Logs +logs/ +*.log diff --git a/README.md b/README.md index b1832a4..619c5ee 100644 --- a/README.md +++ b/README.md @@ -1 +1,127 @@ -# strudelStudio \ No newline at end of file +# Strudel Studio 🎵 + +A web-based editor for coding with [Strudel.cc](https://strudel.cc) REPL - a live coding pattern language for making music. + +## Features + +- 🎹 **Live Code Editor**: Write and evaluate Strudel patterns in real-time +- 🎨 **Syntax Highlighting**: Dark theme with monospace font for better readability +- ▶️ **Play/Stop Controls**: Easy controls for playing and stopping your patterns +- 📝 **Example Code**: Built-in examples to get you started +- 🖥️ **Console Output**: See evaluation results and errors +- ⌨️ **Keyboard Shortcuts**: Use Ctrl+Enter (or Cmd+Enter on Mac) to evaluate code +- 🌙 **Dark Theme**: Eye-friendly dark theme for extended coding sessions + +## Getting Started + +### Quick Start (No Installation Required) + +Simply open `index.html` in your web browser: + +```bash +# Open the file directly +open index.html # macOS +xdg-open index.html # Linux +start index.html # Windows +``` + +### Using a Local Server + +For a better development experience, you can run a local web server: + +```bash +# Using Python 3 +python3 -m http.server 8080 + +# Or using npm +npm start +``` + +Then open your browser to `http://localhost:8080` + +## Usage + +1. **Write Code**: Type your Strudel code in the editor +2. **Evaluate**: Press `Ctrl+Enter` (or `Cmd+Enter` on Mac) or click the "Play" button +3. **Stop**: Click the "Stop" button to stop playback +4. **Load Example**: Click "Example" to load sample code +5. **Clear**: Use "Clear" to reset the editor + +### Example Code + +```javascript +// Simple drum pattern +sound("bd sd bd sd").fast(2) + +// More complex pattern +stack( + sound("bd*2 sd"), + sound("hh*8").gain(0.3), + note("c3 eb3 g3 bb3").s("sawtooth").lpf(800) +) +``` + +## Keyboard Shortcuts + +- `Ctrl+Enter` / `Cmd+Enter` - Evaluate code +- `Tab` - Insert spaces for indentation + +## Technologies Used + +- **HTML5/CSS3/JavaScript** - Modern web technologies +- **Strudel** - Live coding pattern language (ready for integration) +- **Vanilla JS** - No framework dependencies, lightweight and fast + +## Project Structure + +``` +strudelStudio/ +├── index.html # Main HTML file +├── styles.css # Styling +├── app.js # Application logic +├── package.json # Project metadata +└── README.md # Documentation +``` + +## Development + +The editor is built with vanilla JavaScript and requires no build step. Dependencies can be added via CDN or npm as needed. + +### Customization + +- **Editor Theme**: Modify the textarea styling in `styles.css` (`.code-textarea` class) +- **Colors**: Customize the CSS variables in `styles.css` (`:root` section) +- **Functionality**: Extend features in `app.js` + +## Future Enhancements + +- Full Strudel REPL integration with audio playback +- Save/Load patterns to local storage or files +- Multiple editor tabs +- Pattern visualization +- MIDI controller support +- Collaborative editing + +## Contributing + +Contributions are welcome! Feel free to: +- Report bugs +- Suggest features +- Submit pull requests + +## License + +MIT License - feel free to use this project for any purpose. + +## Credits + +Built for the [Strudel](https://strudel.cc) live coding community. + +## Resources + +- [Strudel Documentation](https://strudel.cc) +- [Strudel Tutorial](https://strudel.cc/learn) + +## Security Note + +The current implementation uses JavaScript `eval()` for code evaluation. This is suitable for local development and trusted code. For production use with untrusted code, consider implementing a sandboxed evaluation environment. \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..1f41f1f --- /dev/null +++ b/app.js @@ -0,0 +1,200 @@ +// Strudel Studio - Main Application +// This file handles the editor functionality and Strudel REPL integration + +let editor; +let isPlaying = false; +let currentPattern = null; + +// Initialize the application +document.addEventListener('DOMContentLoaded', function() { + initializeEditor(); + setupEventListeners(); + logToConsole('Strudel Studio initialized. Ready to code!', 'success'); +}); + +// Initialize the editor (simple textarea approach) +function initializeEditor() { + editor = document.getElementById('code-editor'); + + // Add keyboard shortcut support + editor.addEventListener('keydown', function(e) { + // Ctrl+Enter or Cmd+Enter to evaluate + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + e.preventDefault(); + evaluateCode(); + } + + // Tab key inserts spaces instead of changing focus + if (e.key === 'Tab') { + e.preventDefault(); + const start = this.selectionStart; + const end = this.selectionEnd; + const value = this.value; + this.value = value.substring(0, start) + ' ' + value.substring(end); + this.selectionStart = this.selectionEnd = start + 2; + } + }); +} + +// Setup event listeners for buttons +function setupEventListeners() { + document.getElementById('playBtn').addEventListener('click', evaluateCode); + document.getElementById('stopBtn').addEventListener('click', stopCode); + document.getElementById('clearBtn').addEventListener('click', clearEditor); + document.getElementById('exampleBtn').addEventListener('click', loadExample); + document.getElementById('clearConsoleBtn').addEventListener('click', clearConsole); +} + +// Evaluate the code in the editor +async function evaluateCode() { + const code = editor.value.trim(); + + if (!code) { + logToConsole('No code to evaluate', 'error'); + return; + } + + try { + updateStatus('Evaluating...', 'info'); + logToConsole('Evaluating code...', 'info'); + + // Note: Using eval() for code evaluation + // This is suitable for local development and trusted code + // For production use with untrusted code, implement a sandboxed evaluation environment + // In a full Strudel implementation, this would use Strudel's pattern evaluation API + const result = eval(code); + + isPlaying = true; + updatePlayButton(); + updateStatus('Playing ♪', 'success'); + logToConsole('Code evaluated successfully!', 'success'); + + if (result !== undefined) { + logToConsole(`Result: ${result}`, 'info'); + } + } catch (error) { + logToConsole(`Error: ${error.message}`, 'error'); + updateStatus('Error', 'error'); + console.error('Evaluation error:', error); + } +} + +// Stop the currently playing code +function stopCode() { + if (isPlaying) { + // In a full implementation, this would stop the Strudel pattern + isPlaying = false; + updatePlayButton(); + updateStatus('Stopped', 'info'); + logToConsole('Playback stopped', 'info'); + } else { + logToConsole('Nothing is playing', 'info'); + } +} + +// Clear the editor content +function clearEditor() { + if (confirm('Clear all code in the editor?')) { + editor.value = ''; + logToConsole('Editor cleared', 'info'); + } +} + +// Load an example +function loadExample() { + const example = `// Strudel Pattern Example +// This creates a rhythmic drum pattern + +sound("bd sd bd sd") + .fast(2) + +// Try modifying the sounds! +// Available sounds: bd, sd, hh, cp, clap, rim, etc. + +// More complex example: +// stack( +// sound("bd*2 sd"), +// sound("hh*8").gain(0.3), +// note("c3 eb3 g3 bb3").s("sawtooth").lpf(800) +// )`; + + editor.value = example; + logToConsole('Example loaded', 'success'); +} + +// Update the play button state +function updatePlayButton() { + const playBtn = document.getElementById('playBtn'); + if (isPlaying) { + playBtn.textContent = '▶ Playing...'; + playBtn.style.opacity = '0.7'; + } else { + playBtn.textContent = '▶ Play'; + playBtn.style.opacity = '1'; + } +} + +// Update status indicator +function updateStatus(message, type = 'info') { + const statusEl = document.getElementById('status'); + statusEl.textContent = message; + statusEl.style.borderColor = getColorForType(type); +} + +// Log message to console output +function logToConsole(message, type = 'log') { + const consoleOutput = document.getElementById('console-output'); + const logEntry = document.createElement('div'); + logEntry.className = `log ${type}`; + + const timestamp = new Date().toLocaleTimeString(); + logEntry.textContent = `[${timestamp}] ${message}`; + + consoleOutput.appendChild(logEntry); + consoleOutput.scrollTop = consoleOutput.scrollHeight; +} + +// Clear console output +function clearConsole() { + const consoleOutput = document.getElementById('console-output'); + consoleOutput.innerHTML = ''; + logToConsole('Console cleared', 'info'); +} + +// Get color for message type +function getColorForType(type) { + const colors = { + 'success': '#10b981', + 'error': '#ef4444', + 'info': '#6366f1', + 'log': '#888' + }; + return colors[type] || colors.log; +} + +// Override console methods to capture output +const originalConsoleLog = console.log; +const originalConsoleError = console.error; +const originalConsoleWarn = console.warn; + +console.log = function(...args) { + originalConsoleLog.apply(console, args); + logToConsole(args.join(' '), 'log'); +}; + +console.error = function(...args) { + originalConsoleError.apply(console, args); + logToConsole(args.join(' '), 'error'); +}; + +console.warn = function(...args) { + originalConsoleWarn.apply(console, args); + logToConsole(args.join(' '), 'info'); +}; + +// Note: This is a standalone editor that demonstrates the UI and basic functionality +// To fully integrate with Strudel, you would need to: +// 1. Load the Strudel library (from npm or CDN when available) +// 2. Initialize the Strudel REPL context +// 3. Replace the eval() call with proper Strudel pattern evaluation +// 4. Set up audio output through Web Audio API diff --git a/index.html b/index.html new file mode 100644 index 0000000..f9aebf5 --- /dev/null +++ b/index.html @@ -0,0 +1,59 @@ + + + + + + Strudel Studio - REPL Editor + + + + +
+
+

🎵 Strudel Studio

+

Live Coding Music Editor

+
+ +
+
+ + + + + Ready +
+ +
+ +
+ +
+
+

Console

+ +
+
+
+
+ + +
+ + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..afbffc2 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "strudel-studio", + "version": "1.0.0", + "description": "A web-based editor for Strudel.cc REPL", + "main": "index.html", + "scripts": { + "start": "python3 -m http.server 8080", + "dev": "python3 -m http.server 8080" + }, + "keywords": [ + "strudel", + "live-coding", + "music", + "repl", + "editor" + ], + "author": "", + "license": "MIT" +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..56af619 --- /dev/null +++ b/styles.css @@ -0,0 +1,272 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #6366f1; + --secondary-color: #8b5cf6; + --success-color: #10b981; + --danger-color: #ef4444; + --dark-bg: #1e1e1e; + --darker-bg: #0d0d0d; + --light-text: #e0e0e0; + --border-color: #333; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #1e1e1e 0%, #0d0d0d 100%); + color: var(--light-text); + min-height: 100vh; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: rgba(99, 102, 241, 0.1); + border-radius: 12px; + border: 1px solid var(--primary-color); +} + +header h1 { + font-size: 2.5em; + color: var(--primary-color); + margin-bottom: 10px; + text-shadow: 0 0 20px rgba(99, 102, 241, 0.5); +} + +.subtitle { + font-size: 1.2em; + color: var(--light-text); + opacity: 0.8; +} + +.editor-container { + background: var(--dark-bg); + border-radius: 12px; + padding: 20px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border: 1px solid var(--border-color); +} + +.toolbar { + display: flex; + gap: 10px; + margin-bottom: 15px; + align-items: center; + flex-wrap: wrap; +} + +.btn { + padding: 10px 20px; + font-size: 1em; + border: none; + border-radius: 6px; + cursor: pointer; + transition: all 0.3s ease; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.btn-play { + background: var(--success-color); + color: white; +} + +.btn-play:hover { + background: #059669; +} + +.btn-stop { + background: var(--danger-color); + color: white; +} + +.btn-stop:hover { + background: #dc2626; +} + +.btn-clear, .btn-example { + background: var(--border-color); + color: var(--light-text); +} + +.btn-clear:hover, .btn-example:hover { + background: #444; +} + +.status { + margin-left: auto; + padding: 8px 16px; + background: rgba(99, 102, 241, 0.2); + border-radius: 20px; + font-size: 0.9em; + border: 1px solid var(--primary-color); +} + +.editor-wrapper { + margin-bottom: 20px; +} + +.code-textarea { + width: 100%; + height: 400px; + font-family: 'Courier New', 'Monaco', monospace; + font-size: 14px; + padding: 15px; + background: #272822; + color: #f8f8f2; + border: 1px solid var(--border-color); + border-radius: 8px; + resize: vertical; + line-height: 1.5; + tab-size: 2; +} + +.code-textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); +} + +.CodeMirror { + height: 400px; + font-size: 14px; + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.output-container { + background: var(--darker-bg); + border-radius: 8px; + padding: 15px; + border: 1px solid var(--border-color); +} + +.output-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid var(--border-color); +} + +.output-header h3 { + color: var(--primary-color); + font-size: 1.1em; +} + +.btn-small { + padding: 5px 10px; + font-size: 0.85em; + background: var(--border-color); + color: var(--light-text); + border: none; + border-radius: 4px; + cursor: pointer; + transition: background 0.3s ease; +} + +.btn-small:hover { + background: #444; +} + +.console-output { + min-height: 100px; + max-height: 200px; + overflow-y: auto; + font-family: 'Courier New', monospace; + font-size: 0.9em; + color: #a0a0a0; + padding: 10px; +} + +.console-output .log { + padding: 4px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.console-output .log.error { + color: var(--danger-color); +} + +.console-output .log.success { + color: var(--success-color); +} + +.console-output .log.info { + color: var(--primary-color); +} + +footer { + text-align: center; + margin-top: 30px; + padding: 20px; + color: #888; + font-size: 0.9em; +} + +footer a { + color: var(--primary-color); + text-decoration: none; + transition: color 0.3s ease; +} + +footer a:hover { + color: var(--secondary-color); +} + +/* Responsive Design */ +@media (max-width: 768px) { + header h1 { + font-size: 1.8em; + } + + .toolbar { + flex-direction: column; + align-items: stretch; + } + + .status { + margin-left: 0; + text-align: center; + } + + .CodeMirror { + height: 300px; + } +} + +/* Custom scrollbar */ +.console-output::-webkit-scrollbar { + width: 8px; +} + +.console-output::-webkit-scrollbar-track { + background: var(--darker-bg); +} + +.console-output::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +.console-output::-webkit-scrollbar-thumb:hover { + background: #444; +}