Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions js/activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ if (_THIS_IS_MUSIC_BLOCKS_) {
"widgets/sampler",
"widgets/reflection",
"widgets/legobricks",
"widgets/timeline",
"activity/lilypond",
"activity/abc",
"activity/midi",
Expand Down
11 changes: 11 additions & 0 deletions js/logo.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ class Logo {
this.oscilloscopeTurtles = [];
this.meterWidget = null;
this.statusMatrix = null;
this.timeline = null;
this.legobricks = null;

this.evalFlowDict = {};
Expand Down Expand Up @@ -1180,6 +1181,16 @@ class Logo {
this.statusMatrix.init(this.activity);
}

// Set up timeline widget.
if (window.widgetWindows.isOpen("timeline")) {
// Ensure widget has been created before trying to initialize it
if (this.timeline === null) {
this.timeline = new Timeline();
}

this.timeline.init(this.activity);
}

// Execute turtle code here
/*
===========================================================================
Expand Down
56 changes: 56 additions & 0 deletions js/widgets/TIMELINE_TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Timeline Widget Testing Guide

## Opening the Timeline Widget

To test the timeline widget, you can use the browser console to open it manually.

### Method 1: Using Browser Console

1. Start Music Blocks:
```bash
npm run serve
```

2. Open `http://127.0.0.1:3000` in your browser

3. Open the browser console (F12 or right-click → Inspect → Console)

4. Run the following command to open the timeline widget:
```javascript
globalActivity.logo.timeline = new Timeline();
globalActivity.logo.timeline.init(globalActivity);
```

## Testing the Playhead

1. After opening the timeline widget, create a simple music program:
- Drag a "Start" block onto the canvas
- Add some "Note" blocks inside it

2. Press the Play button (▶) in the toolbar

3. Observe the timeline widget:
- The playhead (red vertical line) should move across the timeline
- The movement should be synchronized with the music playback

4. Press Stop and Play again to verify the playhead resets

## Expected Behavior

- **Widget Window**: A window titled "timeline" should appear with a canvas
- **Timeline**: A horizontal gray line across the canvas
- **Playhead**: A red vertical line with a circle at the top that moves during playback
- **Smooth Animation**: The playhead should move smoothly using requestAnimationFrame
- **No Side Effects**: Music playback should work exactly as before

## Troubleshooting

If the widget doesn't appear:
- Check the browser console for errors
- Verify that `Timeline` class is loaded (type `Timeline` in console)
- Ensure `globalActivity` is available (type `globalActivity` in console)

If the playhead doesn't move:
- Verify music is playing
- Check that `globalActivity.turtles.ithTurtle(0).singer.currentBeat` is updating
- Look for console errors during playback
155 changes: 155 additions & 0 deletions js/widgets/timeline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/* eslint-disable no-undef */
// Copyright (c) 2026 Music Blocks Contributors
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the The GNU Affero General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// You should have received a copy of the GNU Affero General Public
// License along with this library; if not, write to the Free Software
// Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA

// This widget displays a read-only timeline with a moving playhead
// synchronized to music playback.

/* global _ */

/* exported Timeline */
class Timeline {
static CANVAS_WIDTH = 800;
static CANVAS_HEIGHT = 100;
static TIMELINE_Y = 50;
static PLAYHEAD_COLOR = "#FF0000";
static TIMELINE_COLOR = "#333333";

/**
* Initializes the timeline widget.
* @param {Object} activity - The activity object containing turtles and logo
*/
init(activity) {
this.activity = activity;
this.isOpen = true;
this.playheadPosition = 0;
this.animationFrameId = null;

// Create widget window
this.widgetWindow = window.widgetWindows.windowFor(this, "timeline", "timeline");
this.widgetWindow.clear();
this.widgetWindow.show();

// Create canvas element
this.canvas = document.createElement("canvas");
this.canvas.width = Timeline.CANVAS_WIDTH;
this.canvas.height = Timeline.CANVAS_HEIGHT;
this.canvas.style.backgroundColor = "#FFFFFF";
this.canvas.style.border = "1px solid #CCCCCC";

this.ctx = this.canvas.getContext("2d");

// Add canvas to widget body
this.widgetWindow.getWidgetBody().appendChild(this.canvas);

// Set up close handler
this.widgetWindow.onclose = () => {
this.isOpen = false;
this._stopAnimation();
this.widgetWindow.destroy();
};

// Draw initial timeline
this._drawTimeline();

// Start animation loop
this._startAnimation();

// Center the widget
this.widgetWindow.sendToCenter();
}

/**
* Draws the timeline and playhead on the canvas
* @private
*/
_drawTimeline() {
// Clear canvas
this.ctx.clearRect(0, 0, Timeline.CANVAS_WIDTH, Timeline.CANVAS_HEIGHT);

// Draw timeline base (horizontal line)
this.ctx.strokeStyle = Timeline.TIMELINE_COLOR;
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(10, Timeline.TIMELINE_Y);
this.ctx.lineTo(Timeline.CANVAS_WIDTH - 10, Timeline.TIMELINE_Y);
this.ctx.stroke();

// Draw playhead (vertical line)
this.ctx.strokeStyle = Timeline.PLAYHEAD_COLOR;
this.ctx.lineWidth = 3;
this.ctx.beginPath();
const playheadX = 10 + this.playheadPosition;
this.ctx.moveTo(playheadX, 20);
this.ctx.lineTo(playheadX, 80);
this.ctx.stroke();

// Draw playhead indicator (small circle at top)
this.ctx.fillStyle = Timeline.PLAYHEAD_COLOR;
this.ctx.beginPath();
this.ctx.arc(playheadX, 20, 5, 0, 2 * Math.PI);
this.ctx.fill();
}

/**
* Updates the playhead position based on current playback state
* @private
*/
_updatePlayhead() {
if (!this.isOpen || !this.activity) {
return;
}

// Get the first turtle's current beat
// In a more complete implementation, we might track all turtles
const turtle = this.activity.turtles.ithTurtle(0);
if (turtle && turtle.singer) {
const currentBeat = turtle.singer.currentBeat || 0;
const beatsPerMeasure = turtle.singer.beatsPerMeasure || 4;

// Calculate playhead position
// For this minimal version, we'll use a simple linear mapping
// Assuming a fixed number of measures (e.g., 8 measures visible)
const totalBeats = beatsPerMeasure * 8;
const normalizedBeat = currentBeat % totalBeats;
const maxWidth = Timeline.CANVAS_WIDTH - 20; // Account for margins
this.playheadPosition = (normalizedBeat / totalBeats) * maxWidth;
}

// Redraw timeline with updated playhead
this._drawTimeline();
}

/**
* Starts the animation loop for playhead updates
* @private
*/
_startAnimation() {
const animate = () => {
if (this.isOpen) {
this._updatePlayhead();
this.animationFrameId = requestAnimationFrame(animate);
}
};
animate();
}

/**
* Stops the animation loop
* @private
*/
_stopAnimation() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
}
Loading
Loading